Coverage Report

Created: 2026-09-14 07:12

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/target_policy.cc
Line
Count
Source
1
/*
2
 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3
 *
4
 * SPDX-License-Identifier: curl
5
 */
6
7
/// @file
8
/// @brief Implementation of the per-binary proto mutation policies.
9
10
#include "proto_fuzzer/target_policy.h"
11
12
#include <algorithm>
13
#include <cstdint>
14
#include <string>
15
16
#include "proto_fuzzer/scenario_limits.h"
17
#include "proto_fuzzer/telnet_scenario.h"
18
19
namespace proto_fuzzer {
20
21
namespace {
22
23
// Linux raises smaller socket-buffer requests to an implementation minimum,
24
// so 2048 is both cheap and reliably small enough to exercise short writes.
25
constexpr std::uint32_t kDefaultBackpressureBufferBytes = 2048;
26
27
// Values outside these ranges do not create useful new socket behavior for
28
// the harness's bounded 4-16 KiB writes. Keeping them small also prevents a
29
// mutated uint32 recv size from overflowing the int accepted by setsockopt.
30
constexpr std::uint32_t kMinBackpressureBufferBytes = 2048;
31
constexpr std::uint32_t kMaxBackpressureBufferBytes = 4096;
32
constexpr std::uint32_t kMaxDrainBytesPerIteration = 1024;
33
34
/// Remove a repeated-field suffix that the runtime would ignore. Doing this
35
/// in LPM's postprocessor matters for speed as well as memory: otherwise later
36
/// mutations keep rediscovering and editing objects that cannot reach curl.
37
template <typename RepeatedField>
38
2.05M
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
2.05M
  const std::size_t size = static_cast<std::size_t>(field->size());
40
2.05M
  if (size > limit) {
41
2.69k
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
2.69k
  }
43
2.05M
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::SetOption> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::SetOption>*, unsigned long)
Line
Count
Source
38
261k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
261k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
261k
  if (size > limit) {
41
908
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
908
  }
43
261k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MimePart> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MimePart>*, unsigned long)
Line
Count
Source
38
65.7k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
65.7k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
65.7k
  if (size > limit) {
41
254
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
254
  }
43
65.7k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MimeDataPart> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MimeDataPart>*, unsigned long)
Line
Count
Source
38
35.3k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
35.3k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
35.3k
  if (size > limit) {
41
99
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
99
  }
43
35.3k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > >(google::protobuf::RepeatedPtrField<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >*, unsigned long)
Line
Count
Source
38
1.02M
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
1.02M
  const std::size_t size = static_cast<std::size_t>(field->size());
40
1.02M
  if (size > limit) {
41
698
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
698
  }
43
1.02M
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::WebSocketFrame> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::WebSocketFrame>*, unsigned long)
Line
Count
Source
38
339k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
339k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
339k
  if (size > limit) {
41
177
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
177
  }
43
339k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http3Action> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http3Action>*, unsigned long)
Line
Count
Source
38
14.0k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
14.0k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
14.0k
  if (size > limit) {
41
87
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
87
  }
43
14.0k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http3Header> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http3Header>*, unsigned long)
Line
Count
Source
38
5.96k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
5.96k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
5.96k
  if (size > limit) {
41
24
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
24
  }
43
5.96k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MultiAction> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MultiAction>*, unsigned long)
Line
Count
Source
38
19.9k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
19.9k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
19.9k
  if (size > limit) {
41
13
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
13
  }
43
19.9k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Connection> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Connection>*, unsigned long)
Line
Count
Source
38
290k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
290k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
290k
  if (size > limit) {
41
432
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
432
  }
43
290k
}
44
45
/// Bound strings passed to NUL-terminated metadata APIs. The runtime applies
46
/// the same prefix, so deleting the invisible suffix increases useful
47
/// mutation density without removing any behavior curl could observe.
48
692k
void TrimMetadata(std::string* value) {
49
692k
  if (value->size() > scenario_limits::kMaxMetadataBytes) {
50
70
    value->resize(scenario_limits::kMaxMetadataBytes);
51
70
  }
52
692k
}
53
54
/// Give the successful-TLS lane a hostname its fixed certificate can verify
55
/// while retaining the fuzz-controlled path, query, and fragment. Arbitrary
56
/// authorities remain covered by the compatibility and legacy HTTPS lanes;
57
/// spending this lane's mutations on URL failures would keep curl's peer-cert
58
/// and encrypted application-data paths dark.
59
83.1k
void CanonicalizeTlsAuthority(curl::fuzzer::proto::Scenario* scenario) {
60
83.1k
  const std::string& host_path = scenario->host_path();
61
83.1k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
62
83.1k
  if (suffix_start == std::string::npos) {
63
4.14k
    scenario->set_host_path("tls.test/");
64
4.14k
    return;
65
4.14k
  }
66
79.0k
  scenario->set_host_path("tls.test" + host_path.substr(suffix_start));
67
79.0k
}
68
69
/// Alt-Svc lookup cannot run while the harness's wildcard CONNECT_TO route is
70
/// active. Give file-backed Alt-Svc inputs one fixed origin whose port can be
71
/// populated in curl's DNS cache before that override is safely detached,
72
/// while preserving all mutation-controlled path, query, and fragment bytes.
73
689
void CanonicalizeAltSvcAuthority(curl::fuzzer::proto::Scenario* scenario) {
74
689
  const std::string& host_path = scenario->host_path();
75
689
  const std::size_t suffix_start = host_path.find_first_of("/?#");
76
689
  if (suffix_start == std::string::npos) {
77
1
    scenario->set_host_path("altsvc-origin.test/");
78
1
    return;
79
1
  }
80
688
  scenario->set_host_path("altsvc-origin.test" + host_path.substr(suffix_start));
81
688
}
82
83
/// Both fixed TLS peers support the same closed set of checked-in certificate
84
/// bundles. Unknown proto3 enum values fall back to the historical EC chain.
85
73.5k
void CanonicalizeTlsCertificateChain(curl::fuzzer::proto::Scenario* scenario) {
86
73.5k
  switch (scenario->tls_certificate_chain()) {
87
67.6k
    case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC:
88
73.5k
    case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES:
89
73.5k
      return;
90
45
    default:
91
45
      scenario->clear_tls_certificate_chain();
92
45
      return;
93
73.5k
  }
94
73.5k
}
95
96
/// Keep the tunneled origin parseable while retaining every path, query, and
97
/// fragment byte. The fixed numeric proxy endpoint handles routing separately;
98
/// mutating the origin authority would therefore buy only early URL failures,
99
/// not additional HTTP/2 proxy behavior.
100
11.3k
void CanonicalizeH2ProxyOriginAuthority(curl::fuzzer::proto::Scenario* scenario) {
101
11.3k
  const std::string& host_path = scenario->host_path();
102
11.3k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
103
11.3k
  if (suffix_start == std::string::npos) {
104
580
    scenario->set_host_path("origin.test/");
105
580
    return;
106
580
  }
107
10.7k
  scenario->set_host_path("origin.test" + host_path.substr(suffix_start));
108
10.7k
}
109
110
/// SOCKS4 must resolve a real local name before constructing its request,
111
/// while SOCKS4A deliberately carries a hostname to the proxy. Both
112
/// authorities remain fixed and the path/query/fragment stays mutable.
113
11.7k
void CanonicalizeSocksAuthority(curl::fuzzer::proto::Scenario* scenario) {
114
11.7k
  const std::string& host_path = scenario->host_path();
115
11.7k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
116
11.7k
  const char* host =
117
11.7k
      scenario->socks_proxy_mode() == curl::fuzzer::proto::SOCKS_PROXY_SOCKS4A ? "socks.test" : "localhost";
118
11.7k
  if (suffix_start == std::string::npos) {
119
342
    scenario->set_host_path(std::string(host) + "/");
120
342
    return;
121
342
  }
122
11.4k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
123
11.4k
}
124
125
/// Use libc/curl's localhost path when there are no structured entries, and a
126
/// fixed cache-backed name when entries are present. In both cases the path,
127
/// query, and fragment remain mutation-controlled.
128
12.5k
void CanonicalizeResolverAuthority(curl::fuzzer::proto::Scenario* scenario) {
129
12.5k
  const std::string& host_path = scenario->host_path();
130
12.5k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
131
12.5k
  const char* host = scenario->resolve_entries().empty() ? "localhost" : "resolve.test";
132
12.5k
  if (suffix_start == std::string::npos) {
133
249
    scenario->set_host_path(std::string(host) + "/");
134
249
    return;
135
249
  }
136
12.2k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
137
12.2k
}
138
139
/// Give the TFTP lane a parseable filename-bearing URL while retaining the
140
/// fuzz-controlled path, query, and fragment. The UDP peer rewrites curl's
141
/// destination after URL parsing, so authority mutations cannot reach another
142
/// host; canonicalizing them here avoids spending most iterations on failures
143
/// before curl constructs a TFTP request. An explicit slash is preserved so
144
/// the missing-filename error remains reachable.
145
2.01k
void CanonicalizeTftpAuthority(curl::fuzzer::proto::Scenario* scenario) {
146
2.01k
  const std::string& host_path = scenario->host_path();
147
2.01k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
148
2.01k
  if (suffix_start == std::string::npos) {
149
402
    scenario->set_host_path("tftp.test/file");
150
402
    return;
151
402
  }
152
1.61k
  scenario->set_host_path("tftp.test" + host_path.substr(suffix_start));
153
1.61k
}
154
155
/// Keep the FTP lane inside the same parseable authority while leaving every
156
/// path segment and wildcard under mutation control. CONNECT_TO already
157
/// confines networking, but rejecting malformed authorities before USER/PWD
158
/// would waste the control/data peer this target uniquely provides.
159
9.03k
void CanonicalizeFtpAuthority(curl::fuzzer::proto::Scenario* scenario) {
160
9.03k
  const std::string& host_path = scenario->host_path();
161
9.03k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
162
9.03k
  if (suffix_start == std::string::npos) {
163
399
    scenario->set_host_path("ftp.test/file");
164
399
    return;
165
399
  }
166
8.63k
  scenario->set_host_path("ftp.test" + host_path.substr(suffix_start));
167
8.63k
}
168
169
/// Put every handle in the multi lane on one origin so connection limits,
170
/// queueing, and reuse affect real transfers instead of independent hosts.
171
/// Path/query/fragment bytes remain mutation-controlled.
172
19.9k
void CanonicalizeMultiAuthority(curl::fuzzer::proto::Scenario* scenario) {
173
19.9k
  const std::string& host_path = scenario->host_path();
174
19.9k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
175
19.9k
  if (suffix_start == std::string::npos) {
176
466
    scenario->set_host_path("multi.test/");
177
466
    return;
178
466
  }
179
19.5k
  scenario->set_host_path("multi.test" + host_path.substr(suffix_start));
180
19.5k
}
181
182
template <typename RepeatedBytes>
183
680k
void BoundStringValues(RepeatedBytes* values, std::size_t count_limit, std::size_t value_limit) {
184
680k
  TrimRepeated(values, count_limit);
185
680k
  for (std::string& value : *values) {
186
201k
    if (value.size() > value_limit) {
187
64
      value.resize(value_limit);
188
64
    }
189
201k
  }
190
680k
}
191
192
template <typename RepeatedBytes>
193
406k
void BoundHeaderValues(RepeatedBytes* headers, std::size_t limit) {
194
406k
  BoundStringValues(headers, limit, scenario_limits::kMaxMetadataBytes);
195
406k
}
196
197
/// Keep one response script identical to the prefix MockServer and
198
/// WebSocketMockServer can deliver. Raw chunks take precedence over structured
199
/// frames, matching both runtime serializers.
200
339k
void BoundConnectionShape(curl::fuzzer::proto::Connection* connection) {
201
339k
  TrimRepeated(connection->mutable_on_readable(), scenario_limits::kMaxResponseChunks);
202
339k
  const std::size_t raw_count = static_cast<std::size_t>(connection->on_readable_size());
203
339k
  TrimRepeated(connection->mutable_server_frames(), scenario_limits::kMaxResponseChunks - raw_count);
204
339k
}
205
206
/// Retain the observable prefix of one filename-backed parser input while
207
/// debiting the shared per-scenario file-byte budget.
208
1.30M
void BoundFileInput(std::string* input, std::size_t* remaining_bytes) {
209
1.30M
  const std::size_t limit = std::min(scenario_limits::kMaxFileInputBytes, *remaining_bytes);
210
1.30M
  if (input->size() > limit) {
211
0
    input->resize(limit);
212
0
  }
213
1.30M
  *remaining_bytes -= input->size();
214
1.30M
}
215
216
261k
void BoundFileInputs(curl::fuzzer::proto::Scenario* scenario) {
217
261k
  std::size_t remaining_bytes = scenario_limits::kMaxFileInputTotalBytes;
218
261k
  BoundFileInput(scenario->mutable_cookie_file(), &remaining_bytes);
219
261k
  BoundFileInput(scenario->mutable_altsvc_file(), &remaining_bytes);
220
261k
  BoundFileInput(scenario->mutable_hsts_file(), &remaining_bytes);
221
261k
  BoundFileInput(scenario->mutable_netrc_file(), &remaining_bytes);
222
261k
  BoundFileInput(scenario->mutable_crl_file(), &remaining_bytes);
223
261k
}
224
225
/// Apply the metadata/header limits shared by both MIME part message types.
226
template <typename Part>
227
144k
void BoundMimePartMetadata(Part* part) {
228
144k
  TrimMetadata(part->mutable_name());
229
144k
  TrimMetadata(part->mutable_filename());
230
144k
  TrimMetadata(part->mutable_content_type());
231
144k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
232
144k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundMimePartMetadata<curl::fuzzer::proto::MimePart>(curl::fuzzer::proto::MimePart*)
Line
Count
Source
227
61.6k
void BoundMimePartMetadata(Part* part) {
228
61.6k
  TrimMetadata(part->mutable_name());
229
61.6k
  TrimMetadata(part->mutable_filename());
230
61.6k
  TrimMetadata(part->mutable_content_type());
231
61.6k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
232
61.6k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundMimePartMetadata<curl::fuzzer::proto::MimeDataPart>(curl::fuzzer::proto::MimeDataPart*)
Line
Count
Source
227
83.0k
void BoundMimePartMetadata(Part* part) {
228
83.0k
  TrimMetadata(part->mutable_name());
229
83.0k
  TrimMetadata(part->mutable_filename());
230
83.0k
  TrimMetadata(part->mutable_content_type());
231
83.0k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
232
83.0k
}
233
234
83.0k
void BoundMimeLeaf(curl::fuzzer::proto::MimeDataPart* part) {
235
83.0k
  BoundMimePartMetadata(part);
236
83.0k
  if (part->data().size() > scenario_limits::kMaxMimeDataBytes) {
237
4
    part->mutable_data()->resize(scenario_limits::kMaxMimeDataBytes);
238
4
  }
239
83.0k
}
240
241
/// Mirror the runtime's shared top-level/nested part budget in the protobuf
242
/// itself. A simple per-list cap is insufficient because many bounded child
243
/// lists could still leave most of the message semantically dead.
244
32.8k
void BoundMimeShape(curl::fuzzer::proto::MimePost* post) {
245
32.8k
  TrimRepeated(post->mutable_parts(), scenario_limits::kMaxTopLevelMimeParts);
246
32.8k
  std::size_t remaining = scenario_limits::kMaxTotalMimeParts;
247
32.8k
  std::size_t retained_top_parts = 0;
248
249
94.5k
  while (retained_top_parts < static_cast<std::size_t>(post->parts_size()) && remaining != 0) {
250
61.6k
    auto* part = post->mutable_parts(static_cast<int>(retained_top_parts));
251
61.6k
    ++retained_top_parts;
252
61.6k
    --remaining;
253
61.6k
    BoundMimePartMetadata(part);
254
255
61.6k
    if (part->content_case() == curl::fuzzer::proto::MimePart::kData) {
256
8.40k
      if (part->data().size() > scenario_limits::kMaxMimeDataBytes) {
257
4
        part->mutable_data()->resize(scenario_limits::kMaxMimeDataBytes);
258
4
      }
259
8.40k
      continue;
260
8.40k
    }
261
53.2k
    if (part->content_case() != curl::fuzzer::proto::MimePart::kSubparts) {
262
17.9k
      continue;
263
17.9k
    }
264
265
35.3k
    auto* children = part->mutable_subparts()->mutable_parts();
266
35.3k
    TrimRepeated(children, std::min(scenario_limits::kMaxNestedMimeParts, remaining));
267
83.0k
    for (auto& child : *children) {
268
83.0k
      BoundMimeLeaf(&child);
269
83.0k
      --remaining;
270
83.0k
    }
271
35.3k
  }
272
273
32.8k
  TrimRepeated(post->mutable_parts(), retained_top_parts);
274
32.8k
}
275
276
/// Remove upload bytes and read steps the callback cannot observe. Clamping
277
/// individual limits also keeps mutations concentrated on short reads instead
278
/// of many distinct uint32 values that all collapse to the same 16 KiB cap.
279
void BoundUploadShape(curl::fuzzer::proto::UploadScript* upload, std::size_t data_limit, std::size_t read_step_limit,
280
18.2k
                      std::size_t read_size_limit) {
281
18.2k
  if (upload->data().size() > data_limit) {
282
9
    upload->mutable_data()->resize(data_limit);
283
9
  }
284
  // RepeatedField<uint32_t> lacks RepeatedPtrField's DeleteSubrange helper;
285
  // removing the ignored suffix from the end is constant-time per element and
286
  // preserves the mutation-significant prefix exactly.
287
69.2k
  while (static_cast<std::size_t>(upload->read_sizes_size()) > read_step_limit) {
288
51.0k
    upload->mutable_read_sizes()->RemoveLast();
289
51.0k
  }
290
62.7k
  for (int i = 0; i < upload->read_sizes_size(); ++i) {
291
44.5k
    if (upload->read_sizes(i) > read_size_limit) {
292
660
      upload->set_read_sizes(i, static_cast<std::uint32_t>(read_size_limit));
293
660
    }
294
44.5k
  }
295
18.2k
}
296
297
/// Trim a protobuf repeated scalar without depending on the container's
298
/// pointer-field-only DeleteSubrange API. Keeping the mutation-significant
299
/// prefix matches every runtime selector loop.
300
template <typename RepeatedScalar>
301
10.3k
void TrimRepeatedScalar(RepeatedScalar* values, std::size_t limit) {
302
13.6k
  while (static_cast<std::size_t>(values->size()) > limit) {
303
3.36k
    values->RemoveLast();
304
3.36k
  }
305
10.3k
}
306
307
/// Keep API work proportional to the fixed descriptor tables used by the
308
/// runtime. Selector magnitudes stay mutation-controlled because the runtime
309
/// folds them into the relevant typed table; only suffixes it cannot execute
310
/// are dead and therefore removed here.
311
5.15k
void BoundApiPlanShape(curl::fuzzer::proto::ApiPlan* plan) {
312
5.15k
  TrimRepeatedScalar(plan->mutable_share_data_selectors(), scenario_limits::kMaxApiShareDataSelectors);
313
5.15k
  TrimRepeatedScalar(plan->mutable_easy_info_selectors(), scenario_limits::kMaxApiInfoSelectors);
314
315
5.15k
  switch (plan->drive_mode()) {
316
3.54k
    case curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM:
317
4.04k
    case curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET:
318
4.04k
      break;
319
945
    case curl::fuzzer::proto::API_DRIVE_EASY_PERFORM:
320
1.04k
    case curl::fuzzer::proto::API_DRIVE_EASY_EVENTS:
321
1.10k
    case curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY:
322
      // Wakeup is a multi-handle API and has no live object in easy mode.
323
      // Likewise, a body callback cannot be resumed while one of these
324
      // blocking entrypoints owns the thread. Clearing both mutations keeps
325
      // every retained API-plan value observable and bounded.
326
1.10k
      plan->set_wake_multi(false);
327
1.10k
      plan->set_pause_response_once(false);
328
1.10k
      break;
329
1
    default:
330
1
      plan->set_drive_mode(curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM);
331
1
      break;
332
5.15k
  }
333
5.15k
}
334
335
/// Keep concurrent-handle work within the mock's fixed socket and operation
336
/// budgets. Values are canonicalized here rather than only at runtime so LPM
337
/// mutates state that the target can actually distinguish.
338
19.9k
void BoundMultiPlanShape(curl::fuzzer::proto::MultiPlan* plan) {
339
19.9k
  const std::uint32_t minimum = static_cast<std::uint32_t>(scenario_limits::kMinMultiTransfers);
340
19.9k
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxMultiTransfers);
341
19.9k
  const std::uint32_t transfer_count = std::max(minimum, std::min(plan->transfer_count(), maximum));
342
19.9k
  plan->set_transfer_count(transfer_count);
343
19.9k
  plan->set_max_host_connections(std::min(plan->max_host_connections(), transfer_count));
344
19.9k
  plan->set_max_total_connections(std::min(plan->max_total_connections(), transfer_count));
345
19.9k
  plan->set_connection_cache_size(std::min(plan->connection_cache_size(), maximum * 2U));
346
19.9k
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxMultiActions);
347
348
19.9k
  switch (plan->drive_mode()) {
349
18.6k
    case curl::fuzzer::proto::MULTI_DRIVE_PERFORM:
350
19.9k
    case curl::fuzzer::proto::MULTI_DRIVE_SOCKET:
351
19.9k
      break;
352
12
    default:
353
12
      plan->set_drive_mode(curl::fuzzer::proto::MULTI_DRIVE_PERFORM);
354
12
      break;
355
19.9k
  }
356
357
28.5k
  for (auto& action : *plan->mutable_actions()) {
358
28.5k
    action.set_transfer_selector(action.transfer_selector() % transfer_count);
359
28.5k
    switch (action.kind()) {
360
9.07k
      case curl::fuzzer::proto::MULTI_ACTION_NONE:
361
10.3k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_RECV:
362
11.3k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_SEND:
363
12.3k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_ALL:
364
13.0k
      case curl::fuzzer::proto::MULTI_ACTION_RESUME:
365
20.6k
      case curl::fuzzer::proto::MULTI_ACTION_REMOVE:
366
28.5k
      case curl::fuzzer::proto::MULTI_ACTION_READD:
367
28.5k
        break;
368
17
      default:
369
17
        action.set_kind(curl::fuzzer::proto::MULTI_ACTION_NONE);
370
17
        break;
371
28.5k
    }
372
28.5k
  }
373
19.9k
}
374
375
/// HTTP field names are lowercase RFC token bytes in the structured lane.
376
/// Replacing (rather than deleting) invalid bytes retains mutation-significant
377
/// positions while preventing accidental pseudo-headers and encoder failures.
378
2.08k
void CanonicalizeHttp3HeaderName(std::string* name) {
379
2.08k
  if (name->size() > scenario_limits::kMaxHttp3HeaderNameBytes) {
380
19
    name->resize(scenario_limits::kMaxHttp3HeaderNameBytes);
381
19
  }
382
2.08k
  if (name->empty()) {
383
132
    *name = "x-fuzz";
384
132
    return;
385
132
  }
386
387
39.2k
  for (char& byte : *name) {
388
39.2k
    const unsigned char value = static_cast<unsigned char>(byte);
389
39.2k
    const bool alpha = (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z');
390
39.2k
    const bool digit = value >= '0' && value <= '9';
391
39.2k
    const bool punctuation = value == '!' || value == '#' || value == '$' || value == '%' || value == '&' ||
392
33.6k
                             value == '\'' || value == '*' || value == '+' || value == '-' || value == '.' ||
393
18.3k
                             value == '^' || value == '_' || value == '`' || value == '|' || value == '~';
394
39.2k
    if (value >= 'A' && value <= 'Z') {
395
371
      byte = static_cast<char>(value - 'A' + 'a');
396
38.9k
    } else if (!alpha && !digit && !punctuation) {
397
971
      byte = '-';
398
971
    }
399
39.2k
  }
400
1.95k
}
401
402
/// Structured field values must not inject another HTTP field or carry NUL
403
/// into the encoder. Observable malformed bytes remain available in raw stream
404
/// actions, while this path stays suitable for valid QPACK generation.
405
2.08k
void CanonicalizeHttp3HeaderValue(std::string* value) {
406
2.08k
  if (value->size() > scenario_limits::kMaxHttp3HeaderValueBytes) {
407
0
    value->resize(scenario_limits::kMaxHttp3HeaderValueBytes);
408
0
  }
409
100k
  for (char& byte : *value) {
410
100k
    const unsigned char character = static_cast<unsigned char>(byte);
411
100k
    if ((character < 0x20U && character != '\t') || character == 0x7fU) {
412
740
      byte = ' ';
413
740
    }
414
100k
  }
415
2.08k
}
416
417
/// Retain only a prefix of encodable fields within both a count and a shared
418
/// byte budget. A missing name is materialized as x-fuzz while budget remains,
419
/// making default-initialized structured headers useful to the peer.
420
template <typename RepeatedHeaders>
421
2.98k
void BoundHttp3Headers(RepeatedHeaders* headers, std::size_t count_limit, std::size_t* remaining_bytes) {
422
2.98k
  TrimRepeated(headers, count_limit);
423
2.98k
  std::size_t retained = 0;
424
5.07k
  while (retained < static_cast<std::size_t>(headers->size()) && *remaining_bytes != 0) {
425
2.08k
    auto* header = headers->Mutable(static_cast<int>(retained));
426
2.08k
    CanonicalizeHttp3HeaderName(header->mutable_name());
427
2.08k
    CanonicalizeHttp3HeaderValue(header->mutable_value());
428
429
2.08k
    if (header->name().size() > *remaining_bytes) {
430
2
      header->mutable_name()->resize(*remaining_bytes);
431
2
      header->clear_value();
432
2.08k
    } else if (header->value().size() > *remaining_bytes - header->name().size()) {
433
2
      header->mutable_value()->resize(*remaining_bytes - header->name().size());
434
2
    }
435
2.08k
    *remaining_bytes -= header->name().size() + header->value().size();
436
2.08k
    ++retained;
437
2.08k
  }
438
2.98k
  TrimRepeated(headers, retained);
439
2.98k
}
440
441
2.17k
void CanonicalizeHttp3StreamRole(curl::fuzzer::proto::Http3StreamRole* role) {
442
2.17k
  switch (*role) {
443
2.05k
    case curl::fuzzer::proto::HTTP3_STREAM_RESPONSE:
444
2.09k
    case curl::fuzzer::proto::HTTP3_STREAM_CONTROL:
445
2.11k
    case curl::fuzzer::proto::HTTP3_STREAM_QPACK_ENCODER:
446
2.12k
    case curl::fuzzer::proto::HTTP3_STREAM_QPACK_DECODER:
447
2.12k
      return;
448
49
    default:
449
49
      *role = curl::fuzzer::proto::HTTP3_STREAM_RESPONSE;
450
49
      return;
451
2.17k
  }
452
2.17k
}
453
454
2.24k
void BoundHttp3RawData(std::string* data, std::size_t* remaining_raw_bytes) {
455
2.24k
  const std::size_t limit = std::min(scenario_limits::kMaxHttp3RawWriteBytes, *remaining_raw_bytes);
456
2.24k
  if (data->size() > limit) {
457
1
    data->resize(limit);
458
1
  }
459
2.24k
  *remaining_raw_bytes -= data->size();
460
2.24k
}
461
462
/// Canonicalize one ordered H3 script to the exact bounded prefix that the
463
/// QUIC peer can execute. Transport setup remains peer-owned; only plaintext
464
/// HTTP/3 operations are mutation-controlled here.
465
7.04k
void BoundHttp3PlanShape(curl::fuzzer::proto::Http3Plan* plan) {
466
7.04k
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxHttp3Actions);
467
7.04k
  if (plan->actions().empty()) {
468
487
    auto* response = plan->add_actions()->mutable_structured_response();
469
487
    response->set_status_code(200);
470
487
    response->set_finish_stream(true);
471
487
  }
472
473
7.04k
  std::size_t remaining_header_bytes = scenario_limits::kMaxHttp3HeaderBytes;
474
7.04k
  std::size_t remaining_body_bytes = scenario_limits::kMaxHttp3BodyBytes;
475
7.04k
  std::size_t remaining_raw_bytes = scenario_limits::kMaxHttp3RawBytes;
476
7.04k
  std::size_t retained_actions = 0;
477
7.04k
  bool connection_closed = false;
478
9.05k
  for (auto& action : *plan->mutable_actions()) {
479
9.05k
    if (connection_closed) {
480
47
      break;
481
47
    }
482
9.00k
    ++retained_actions;
483
9.00k
    switch (action.action_case()) {
484
1.49k
      case curl::fuzzer::proto::Http3Action::kStructuredResponse: {
485
1.49k
        auto* response = action.mutable_structured_response();
486
1.49k
        if (response->status_code() == 0U) {
487
51
          response->set_status_code(200U);
488
1.44k
        } else if (response->status_code() < 100U || response->status_code() > 599U) {
489
119
          response->set_status_code(100U + response->status_code() % 500U);
490
119
        }
491
1.49k
        BoundHttp3Headers(response->mutable_response_headers(), scenario_limits::kMaxHttp3Headers,
492
1.49k
                          &remaining_header_bytes);
493
1.49k
        BoundHttp3Headers(response->mutable_response_trailers(), scenario_limits::kMaxHttp3Trailers,
494
1.49k
                          &remaining_header_bytes);
495
1.49k
        TrimRepeated(response->mutable_body_chunks(), scenario_limits::kMaxHttp3BodyChunks);
496
1.49k
        for (std::string& chunk : *response->mutable_body_chunks()) {
497
745
          if (chunk.size() > remaining_body_bytes) {
498
10
            chunk.resize(remaining_body_bytes);
499
10
          }
500
745
          remaining_body_bytes -= chunk.size();
501
745
        }
502
1.49k
        break;
503
0
      }
504
505
1.09k
      case curl::fuzzer::proto::Http3Action::kStreamWrite: {
506
1.09k
        auto* write = action.mutable_stream_write();
507
1.09k
        auto role = write->role();
508
1.09k
        CanonicalizeHttp3StreamRole(&role);
509
1.09k
        write->set_role(role);
510
1.09k
        BoundHttp3RawData(write->mutable_data(), &remaining_raw_bytes);
511
1.09k
        break;
512
0
      }
513
514
1.14k
      case curl::fuzzer::proto::Http3Action::kOpenUnidirectionalStream: {
515
1.14k
        auto* stream = action.mutable_open_unidirectional_stream();
516
1.14k
        BoundHttp3RawData(stream->mutable_data(), &remaining_raw_bytes);
517
1.14k
        break;
518
0
      }
519
520
1.07k
      case curl::fuzzer::proto::Http3Action::kStreamReset: {
521
1.07k
        auto* reset = action.mutable_stream_reset();
522
1.07k
        auto role = reset->role();
523
1.07k
        CanonicalizeHttp3StreamRole(&role);
524
1.07k
        reset->set_role(role);
525
1.07k
        reset->set_application_error_code(reset->application_error_code() & scenario_limits::kMaxQuicVarint);
526
1.07k
        break;
527
0
      }
528
529
1.46k
      case curl::fuzzer::proto::Http3Action::kGoaway:
530
1.46k
        action.mutable_goaway()->set_id(action.goaway().id() & scenario_limits::kMaxQuicVarint & ~std::uint64_t{3});
531
1.46k
        break;
532
533
2.44k
      case curl::fuzzer::proto::Http3Action::kConnectionClose:
534
2.44k
        action.mutable_connection_close()->set_application_error_code(
535
2.44k
            action.connection_close().application_error_code() & scenario_limits::kMaxQuicVarint);
536
2.44k
        connection_closed = true;
537
2.44k
        break;
538
539
289
      case curl::fuzzer::proto::Http3Action::ACTION_NOT_SET: {
540
289
        auto* response = action.mutable_structured_response();
541
289
        response->set_status_code(200);
542
289
        response->set_finish_stream(true);
543
289
        break;
544
0
      }
545
9.00k
    }
546
9.00k
  }
547
7.04k
  TrimRepeated(plan->mutable_actions(), retained_actions);
548
7.04k
}
549
550
/// Canonicalize all shape limits enforced by the runtime. This runs only in
551
/// fixed policy targets; the compatibility binary deliberately retains its
552
/// historical no-postprocessor semantics for existing OSS-Fuzz reproducers.
553
261k
void BoundScenarioShape(curl::fuzzer::proto::Scenario* scenario) {
554
261k
  TrimRepeated(scenario->mutable_options(), scenario_limits::kMaxOptions);
555
727k
  for (auto& option : *scenario->mutable_options()) {
556
727k
    if (option.value_case() == curl::fuzzer::proto::SetOption::kStringValue) {
557
258k
      TrimMetadata(option.mutable_string_value());
558
258k
    }
559
727k
  }
560
561
261k
  BoundHeaderValues(scenario->mutable_request_headers(), scenario_limits::kMaxRequestHeaders);
562
261k
  BoundStringValues(scenario->mutable_telnet_options(), scenario_limits::kMaxTelnetOptions,
563
261k
                    scenario_limits::kMaxTelnetOptionBytes);
564
261k
  if (scenario->has_mime_post()) {
565
32.8k
    BoundMimeShape(scenario->mutable_mime_post());
566
32.8k
  }
567
261k
  if (scenario->has_upload()) {
568
18.2k
    const bool telnet = scenario->scheme() == curl::fuzzer::proto::SCHEME_TELNET;
569
18.2k
    const std::size_t data_limit = telnet ? scenario_limits::kMaxTelnetUploadBytes : scenario_limits::kMaxUploadBytes;
570
18.2k
    const std::size_t read_step_limit =
571
18.2k
        telnet ? scenario_limits::kMaxTelnetUploadReadSteps : scenario_limits::kMaxUploadReadSteps;
572
18.2k
    const std::size_t read_size_limit =
573
18.2k
        telnet ? scenario_limits::kMaxTelnetUploadReadSize : scenario_limits::kMaxUploadReadSize;
574
18.2k
    BoundUploadShape(scenario->mutable_upload(), data_limit, read_step_limit, read_size_limit);
575
18.2k
  }
576
577
261k
  BoundConnectionShape(scenario->mutable_connection());
578
261k
  TrimRepeated(scenario->mutable_subsequent_connections(), scenario_limits::kMaxConnections - 1);
579
261k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
580
77.7k
    BoundConnectionShape(&connection);
581
77.7k
  }
582
261k
  BoundFileInputs(scenario);
583
261k
}
584
585
/// Return whether an option belongs in the high-throughput HTTP lane. This is
586
/// deliberately an allowlist rather than a denylist: adding a new structured
587
/// option should expand deep coverage first, not silently make the fast lane
588
/// slower before its cost has been measured.
589
83.1k
bool IsCheapHttpOption(curl::fuzzer::proto::CurlOptionId option_id) {
590
83.1k
  switch (option_id) {
591
14.5k
    case curl::fuzzer::proto::CURLOPT_ACCEPT_ENCODING:
592
17.2k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
593
18.9k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
594
20.6k
    case curl::fuzzer::proto::CURLOPT_DISALLOW_USERNAME_IN_URL:
595
23.5k
    case curl::fuzzer::proto::CURLOPT_FAILONERROR:
596
24.7k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
597
26.6k
    case curl::fuzzer::proto::CURLOPT_HEADER:
598
27.8k
    case curl::fuzzer::proto::CURLOPT_HTTP09_ALLOWED:
599
28.8k
    case curl::fuzzer::proto::CURLOPT_HTTP_CONTENT_DECODING:
600
30.7k
    case curl::fuzzer::proto::CURLOPT_HTTP_TRANSFER_DECODING:
601
59.4k
    case curl::fuzzer::proto::CURLOPT_HTTP_VERSION:
602
62.9k
    case curl::fuzzer::proto::CURLOPT_HTTPGET:
603
63.8k
    case curl::fuzzer::proto::CURLOPT_IGNORE_CONTENT_LENGTH:
604
66.1k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
605
70.2k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
606
70.8k
    case curl::fuzzer::proto::CURLOPT_PATH_AS_IS:
607
72.0k
    case curl::fuzzer::proto::CURLOPT_RANGE:
608
75.8k
    case curl::fuzzer::proto::CURLOPT_REQUEST_TARGET:
609
78.5k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
610
79.3k
    case curl::fuzzer::proto::CURLOPT_TRANSFER_ENCODING:
611
80.2k
    case curl::fuzzer::proto::CURLOPT_USERAGENT:
612
80.2k
      return true;
613
614
1.04k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
615
2.93k
    default:
616
2.93k
      return false;
617
83.1k
  }
618
83.1k
}
619
620
/// Return whether an option can affect the HTTP/1.1 request carried inside the
621
/// fixed HTTP/2 CONNECT tunnel. Proxy routing, ALPN, and TLS verification are
622
/// owned by H2ProxyMockServer and must not be mutation-controlled; HTTP/2 as an
623
/// inner origin protocol is also excluded because it would require a second
624
/// frame script and obscure coverage of the outer proxy filter. Stateful HTTP
625
/// options remain useful here because their wire effects traverse cf-h2-proxy.
626
150k
bool IsH2ProxyOriginOption(curl::fuzzer::proto::CurlOptionId option_id) {
627
150k
  switch (option_id) {
628
2.58k
    case curl::fuzzer::proto::CURLOPT_ACCEPT_ENCODING:
629
4.24k
    case curl::fuzzer::proto::CURLOPT_ALTSVC_CTRL:
630
7.52k
    case curl::fuzzer::proto::CURLOPT_AUTOREFERER:
631
10.8k
    case curl::fuzzer::proto::CURLOPT_AWS_SIGV4:
632
14.5k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
633
16.1k
    case curl::fuzzer::proto::CURLOPT_COOKIE:
634
37.7k
    case curl::fuzzer::proto::CURLOPT_COOKIELIST:
635
38.9k
    case curl::fuzzer::proto::CURLOPT_COOKIESESSION:
636
42.3k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
637
43.8k
    case curl::fuzzer::proto::CURLOPT_DISALLOW_USERNAME_IN_URL:
638
47.3k
    case curl::fuzzer::proto::CURLOPT_EXPECT_100_TIMEOUT_MS:
639
49.0k
    case curl::fuzzer::proto::CURLOPT_FAILONERROR:
640
50.5k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
641
58.8k
    case curl::fuzzer::proto::CURLOPT_FOLLOWLOCATION:
642
63.2k
    case curl::fuzzer::proto::CURLOPT_FORBID_REUSE:
643
64.9k
    case curl::fuzzer::proto::CURLOPT_FRESH_CONNECT:
644
66.0k
    case curl::fuzzer::proto::CURLOPT_HEADER:
645
69.9k
    case curl::fuzzer::proto::CURLOPT_HSTS_CTRL:
646
71.4k
    case curl::fuzzer::proto::CURLOPT_HTTP09_ALLOWED:
647
76.0k
    case curl::fuzzer::proto::CURLOPT_HTTPAUTH:
648
79.9k
    case curl::fuzzer::proto::CURLOPT_HTTPGET:
649
84.3k
    case curl::fuzzer::proto::CURLOPT_HTTP_CONTENT_DECODING:
650
85.6k
    case curl::fuzzer::proto::CURLOPT_HTTP_TRANSFER_DECODING:
651
86.6k
    case curl::fuzzer::proto::CURLOPT_IGNORE_CONTENT_LENGTH:
652
88.5k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
653
89.3k
    case curl::fuzzer::proto::CURLOPT_KEEP_SENDING_ON_ERROR:
654
91.3k
    case curl::fuzzer::proto::CURLOPT_MAXAGE_CONN:
655
93.2k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
656
95.4k
    case curl::fuzzer::proto::CURLOPT_MAXLIFETIME_CONN:
657
98.8k
    case curl::fuzzer::proto::CURLOPT_MAXREDIRS:
658
99.5k
    case curl::fuzzer::proto::CURLOPT_MIME_OPTIONS:
659
102k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
660
104k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
661
105k
    case curl::fuzzer::proto::CURLOPT_PATH_AS_IS:
662
108k
    case curl::fuzzer::proto::CURLOPT_POST:
663
111k
    case curl::fuzzer::proto::CURLOPT_POSTFIELDS:
664
113k
    case curl::fuzzer::proto::CURLOPT_POSTREDIR:
665
114k
    case curl::fuzzer::proto::CURLOPT_RANGE:
666
117k
    case curl::fuzzer::proto::CURLOPT_REFERER:
667
119k
    case curl::fuzzer::proto::CURLOPT_REQUEST_TARGET:
668
122k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
669
127k
    case curl::fuzzer::proto::CURLOPT_TIMECONDITION:
670
128k
    case curl::fuzzer::proto::CURLOPT_TIMEVALUE_LARGE:
671
128k
    case curl::fuzzer::proto::CURLOPT_TRANSFER_ENCODING:
672
130k
    case curl::fuzzer::proto::CURLOPT_UNRESTRICTED_AUTH:
673
132k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
674
136k
    case curl::fuzzer::proto::CURLOPT_UPLOAD_BUFFERSIZE:
675
138k
    case curl::fuzzer::proto::CURLOPT_USERAGENT:
676
139k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
677
144k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
678
146k
    case curl::fuzzer::proto::CURLOPT_XOAUTH2_BEARER:
679
146k
      return true;
680
681
2.29k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
682
4.46k
    default:
683
4.46k
      return false;
684
150k
  }
685
150k
}
686
687
/// Compact an option list before applying the general option-count bound.
688
/// Keeping a relevant option that appears after a long rejected prefix is
689
/// important for mutation density: bounding first would let unrelated options
690
/// crowd useful ones out of a protocol-specific lane.
691
template <typename Predicate>
692
261k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
693
261k
  auto* options = scenario->mutable_options();
694
261k
  int retained = 0;
695
1.29M
  for (int index = 0; index < options->size(); ++index) {
696
1.03M
    if (!predicate(options->Get(index).option_id())) {
697
72.5k
      continue;
698
72.5k
    }
699
961k
    if (retained != index) {
700
115k
      options->SwapElements(retained, index);
701
115k
    }
702
961k
    ++retained;
703
961k
  }
704
261k
  options->DeleteSubrange(retained, options->size() - retained);
705
261k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::RetainMatchingOptions<bool (*)(curl::fuzzer::proto::CurlOptionId)>(curl::fuzzer::proto::Scenario*, bool (*)(curl::fuzzer::proto::CurlOptionId))
Line
Count
Source
692
88.2k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
693
88.2k
  auto* options = scenario->mutable_options();
694
88.2k
  int retained = 0;
695
461k
  for (int index = 0; index < options->size(); ++index) {
696
373k
    if (!predicate(options->Get(index).option_id())) {
697
57.1k
      continue;
698
57.1k
    }
699
316k
    if (retained != index) {
700
75.6k
      options->SwapElements(retained, index);
701
75.6k
    }
702
316k
    ++retained;
703
316k
  }
704
88.2k
  options->DeleteSubrange(retained, options->size() - retained);
705
88.2k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::RetainMatchingOptions<proto_fuzzer::(anonymous namespace)::RemoveFileTransferOnlyOptions(curl::fuzzer::proto::Scenario*)::$_0>(curl::fuzzer::proto::Scenario*, proto_fuzzer::(anonymous namespace)::RemoveFileTransferOnlyOptions(curl::fuzzer::proto::Scenario*)::$_0)
Line
Count
Source
692
173k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
693
173k
  auto* options = scenario->mutable_options();
694
173k
  int retained = 0;
695
833k
  for (int index = 0; index < options->size(); ++index) {
696
660k
    if (!predicate(options->Get(index).option_id())) {
697
15.3k
      continue;
698
15.3k
    }
699
644k
    if (retained != index) {
700
39.9k
      options->SwapElements(retained, index);
701
39.9k
    }
702
644k
    ++retained;
703
644k
  }
704
173k
  options->DeleteSubrange(retained, options->size() - retained);
705
173k
}
706
707
/// Keep the high-throughput HTTP lane free of options whose setup or state is
708
/// assigned to a deeper or protocol-specific target.
709
43.9k
void RetainCheapHttpOptions(curl::fuzzer::proto::Scenario* scenario) {
710
43.9k
  RetainMatchingOptions(scenario, &IsCheapHttpOption);
711
43.9k
}
712
713
/// Compact the option list before its shared cap so irrelevant TLS, WebSocket,
714
/// and file-transfer entries cannot crowd out origin traffic mutations.
715
22.2k
void RetainH2ProxyOriginOptions(curl::fuzzer::proto::Scenario* scenario) {
716
22.2k
  RetainMatchingOptions(scenario, &IsH2ProxyOriginOption);
717
22.2k
}
718
719
/// HTTP/3 owns QUIC selection, ALPN, routing, and certificate verification in
720
/// its peer. The tunneled-origin allowlist is deliberately the same set of
721
/// request-level HTTP controls, and notably excludes CURLOPT_HTTP_VERSION and
722
/// CURLOPT_CONNECT_ONLY, which could bypass the dedicated transport.
723
7.04k
void RetainHttp3RequestOptions(curl::fuzzer::proto::Scenario* scenario) {
724
7.04k
  RetainMatchingOptions(scenario, &IsH2ProxyOriginOption);
725
7.04k
}
726
727
/// Return whether a scalar option can influence TELNET without selecting an
728
/// incompatible transfer mode or introducing external state. Protocol-
729
/// specific negotiation preferences use Scenario.telnet_options instead.
730
26.9k
bool IsCheapTelnetOption(curl::fuzzer::proto::CurlOptionId option_id) {
731
26.9k
  switch (option_id) {
732
9.96k
    case curl::fuzzer::proto::CURLOPT_CRLF:
733
15.6k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
734
16.0k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
735
17.3k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
736
18.1k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
737
18.1k
      return true;
738
739
4.73k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
740
8.78k
    default:
741
8.78k
      return false;
742
26.9k
  }
743
26.9k
}
744
745
/// Compact the TELNET option prefix so unrelated HTTP mutations cannot crowd
746
/// useful credentials, CRLF handling, and transfer-size controls out of the
747
/// fixed target's general option budget.
748
3.99k
void RetainCheapTelnetOptions(curl::fuzzer::proto::Scenario* scenario) {
749
3.99k
  RetainMatchingOptions(scenario, &IsCheapTelnetOption);
750
3.99k
}
751
752
/// Return whether an option can change a plaintext FTP transfer serviced by
753
/// the bounded control/data peer. Active mode is confined to the mock's
754
/// loopback listener; FTPS settings remain omitted until that peer speaks TLS.
755
46.2k
bool IsFtpOption(curl::fuzzer::proto::CurlOptionId option_id) {
756
46.2k
  switch (option_id) {
757
240
    case curl::fuzzer::proto::CURLOPT_APPEND:
758
1.26k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
759
1.89k
    case curl::fuzzer::proto::CURLOPT_CRLF:
760
2.49k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
761
3.15k
    case curl::fuzzer::proto::CURLOPT_DIRLISTONLY:
762
3.66k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
763
4.58k
    case curl::fuzzer::proto::CURLOPT_FTP_ACCOUNT:
764
6.49k
    case curl::fuzzer::proto::CURLOPT_FTP_ALTERNATIVE_TO_USER:
765
13.2k
    case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
766
14.4k
    case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
767
15.5k
    case curl::fuzzer::proto::CURLOPT_FTP_SKIP_PASV_IP:
768
15.8k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPSV:
769
16.2k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_PRET:
770
16.4k
    case curl::fuzzer::proto::CURLOPT_FTPPORT:
771
21.3k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPRT:
772
22.4k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
773
23.2k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
774
24.1k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
775
24.9k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
776
25.5k
    case curl::fuzzer::proto::CURLOPT_RANGE:
777
26.7k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
778
31.6k
    case curl::fuzzer::proto::CURLOPT_TIMECONDITION:
779
32.1k
    case curl::fuzzer::proto::CURLOPT_TIMEVALUE_LARGE:
780
32.8k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
781
37.0k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
782
37.8k
    case curl::fuzzer::proto::CURLOPT_UPLOAD_BUFFERSIZE:
783
38.2k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
784
41.1k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
785
43.2k
    case curl::fuzzer::proto::CURLOPT_WILDCARDMATCH:
786
43.2k
      return true;
787
788
2.21k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
789
2.99k
    default:
790
2.99k
      return false;
791
46.2k
  }
792
46.2k
}
793
794
/// Keep the FTP target's general option budget focused on states its passive
795
/// peer can actually advance.
796
9.03k
void RetainFtpOptions(curl::fuzzer::proto::Scenario* scenario) { RetainMatchingOptions(scenario, &IsFtpOption); }
797
798
/// Return whether an option affects TFTP request construction, option
799
/// negotiation, transfer direction, or bounded body delivery. TFTP has no
800
/// connection reuse or stream-level controls, so retaining those settings
801
/// would add protobuf work without another state-machine edge in curl.
802
66.4k
bool IsTftpOption(curl::fuzzer::proto::CurlOptionId option_id) {
803
66.4k
  switch (option_id) {
804
9.59k
    case curl::fuzzer::proto::CURLOPT_CRLF:
805
11.1k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
806
13.8k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
807
21.4k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
808
23.6k
    case curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE:
809
25.8k
    case curl::fuzzer::proto::CURLOPT_TFTP_NO_OPTIONS:
810
26.3k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
811
28.4k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
812
28.4k
      return true;
813
814
36.2k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
815
37.9k
    default:
816
37.9k
      return false;
817
66.4k
  }
818
66.4k
}
819
820
/// Keep the datagram lane from spending mutations on stream-only options.
821
2.01k
void RetainTftpOptions(curl::fuzzer::proto::Scenario* scenario) { RetainMatchingOptions(scenario, &IsTftpOption); }
822
823
/// Decode an integral oneof locally before the generated option canonicalizer
824
/// runs. Protocol mode bounds are policy, not setopt mechanics: folding them
825
/// here keeps nearly every mutation on a real FTP/TFTP state while the shared
826
/// option layer remains unaware of protocol-specific numeric ranges.
827
15.1k
std::uint64_t IntegralMutationValue(const curl::fuzzer::proto::SetOption& option) {
828
15.1k
  switch (option.value_case()) {
829
9.73k
    case curl::fuzzer::proto::SetOption::kUintValue:
830
9.73k
      return option.uint_value();
831
1.02k
    case curl::fuzzer::proto::SetOption::kBoolValue:
832
1.02k
      return option.bool_value() ? 1U : 0U;
833
420
    case curl::fuzzer::proto::SetOption::kStringValue:
834
4.35k
    case curl::fuzzer::proto::SetOption::VALUE_NOT_SET:
835
4.35k
      return 0;
836
15.1k
  }
837
0
  return 0;
838
15.1k
}
839
840
/// Fold small FTP enums onto curl's documented domains so random uint64 values
841
/// do not overwhelmingly stop at setopt validation before issuing a command.
842
9.03k
void CanonicalizeFtpOptionModes(curl::fuzzer::proto::Scenario* scenario) {
843
43.2k
  for (auto& option : *scenario->mutable_options()) {
844
43.2k
    switch (option.option_id()) {
845
6.79k
      case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
846
6.79k
        option.set_uint_value(IntegralMutationValue(option) % 3U);
847
6.79k
        break;
848
1.19k
      case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
849
1.19k
        option.set_uint_value(IntegralMutationValue(option) % 4U);
850
1.19k
        break;
851
169
      case curl::fuzzer::proto::CURLOPT_FTPPORT:
852
        // Never let a mutated active-mode address resolve or bind outside the
853
        // process. Empty remains reachable only by omitting the option.
854
169
        option.set_string_value("127.0.0.1");
855
169
        break;
856
4.89k
      case curl::fuzzer::proto::CURLOPT_FTP_USE_EPRT:
857
4.89k
        option.set_bool_value(IntegralMutationValue(option) != 0U);
858
4.89k
        break;
859
30.2k
      default:
860
30.2k
        break;
861
43.2k
    }
862
43.2k
  }
863
9.03k
}
864
865
/// TFTP accepts block sizes from 8 through 65464. Mapping zero or a mismatched
866
/// oneof to the default 512 preserves a common valid request, while saturating
867
/// other values retains both lower/upper parser boundaries under mutation.
868
2.01k
void CanonicalizeTftpOptionModes(curl::fuzzer::proto::Scenario* scenario) {
869
28.4k
  for (auto& option : *scenario->mutable_options()) {
870
28.4k
    if (option.option_id() != curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE) {
871
26.2k
      continue;
872
26.2k
    }
873
2.22k
    std::uint64_t value = IntegralMutationValue(option);
874
2.22k
    if (value == 0) {
875
383
      value = 512;
876
383
    }
877
2.22k
    option.set_uint_value(std::max<std::uint64_t>(8, std::min<std::uint64_t>(value, 65464)));
878
2.22k
  }
879
2.01k
}
880
881
/// Identify options introduced for FTP/TFTP so existing fixed lanes do not
882
/// silently inherit dead mutations when the shared generated manifest grows.
883
/// Generic options retained by the FTP/TFTP allowlists are deliberately absent
884
/// here because they remain useful to HTTP, WebSocket, API, or timing targets.
885
660k
bool IsFileTransferOnlyOption(curl::fuzzer::proto::CurlOptionId option_id) {
886
660k
  switch (option_id) {
887
1.01k
    case curl::fuzzer::proto::CURLOPT_APPEND:
888
2.14k
    case curl::fuzzer::proto::CURLOPT_DIRLISTONLY:
889
3.78k
    case curl::fuzzer::proto::CURLOPT_FTP_ACCOUNT:
890
3.93k
    case curl::fuzzer::proto::CURLOPT_FTP_ALTERNATIVE_TO_USER:
891
4.46k
    case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
892
4.78k
    case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
893
5.63k
    case curl::fuzzer::proto::CURLOPT_FTP_SKIP_PASV_IP:
894
8.15k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPSV:
895
9.24k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_PRET:
896
9.31k
    case curl::fuzzer::proto::CURLOPT_FTPPORT:
897
9.42k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPRT:
898
11.7k
    case curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE:
899
13.2k
    case curl::fuzzer::proto::CURLOPT_TFTP_NO_OPTIONS:
900
13.9k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
901
15.3k
    case curl::fuzzer::proto::CURLOPT_WILDCARDMATCH:
902
15.3k
      return true;
903
904
33.3k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
905
644k
    default:
906
644k
      return false;
907
660k
  }
908
660k
}
909
910
/// Remove FTP/TFTP-only options while preserving the relative order of every
911
/// generic option an existing fixed target already consumed.
912
173k
void RemoveFileTransferOnlyOptions(curl::fuzzer::proto::Scenario* scenario) {
913
173k
  RetainMatchingOptions(
914
660k
      scenario, [](curl::fuzzer::proto::CurlOptionId option_id) { return !IsFileTransferOnlyOption(option_id); });
915
173k
}
916
917
/// Remove the stateful shapes assigned to the deep HTTP target. This happens
918
/// before BoundScenarioShape so a fast iteration never walks or normalizes a
919
/// MIME tree, upload script, or follow-on connection that it will discard.
920
/// Raw response chunks and request headers stay intact because they reach the
921
/// core HTTP parser cheaply and provide much of the legacy fuzzer's coverage.
922
43.9k
void RemoveDeepHttpShape(curl::fuzzer::proto::Scenario* scenario) {
923
43.9k
  scenario->clear_mime_post();
924
43.9k
  scenario->clear_upload();
925
43.9k
  scenario->clear_subsequent_connections();
926
927
43.9k
  auto* connection = scenario->mutable_connection();
928
43.9k
  connection->clear_server_frames();
929
43.9k
  connection->clear_manual_probes();
930
43.9k
  connection->clear_backpressure();
931
43.9k
}
932
933
/// Remove response forms the proxy peer cannot interpret. Raw chunks are the
934
/// HTTP/2 frame stream; WebSocket frames would merely add a second unrelated
935
/// binary grammar, and follow-on Connection messages cannot describe later
936
/// streams multiplexed on the already-open proxy socket.
937
22.2k
void RemoveIgnoredH2ProxyShape(curl::fuzzer::proto::Scenario* scenario) {
938
22.2k
  scenario->clear_subsequent_connections();
939
22.2k
  auto* connection = scenario->mutable_connection();
940
22.2k
  connection->clear_server_frames();
941
22.2k
  connection->clear_manual_probes();
942
22.2k
  connection->clear_backpressure();
943
22.2k
}
944
945
/// Remove fields the single-socket WebSocket driver cannot consume. MIME also
946
/// changes the HTTP request away from a useful Upgrade handshake, so retaining
947
/// either shape in fixed WS lanes gives LPM mutation work with no WS coverage
948
/// payoff. The mixed compatibility target has no postprocessor and keeps its
949
/// historical behavior.
950
27.1k
void RemoveIgnoredWebSocketShape(curl::fuzzer::proto::Scenario* scenario) {
951
27.1k
  scenario->clear_subsequent_connections();
952
27.1k
  scenario->clear_mime_post();
953
27.1k
}
954
955
/// Remove fields whose only effect in a TELNET lane would be protobuf work or
956
/// unsafe socket timing. TELNET's curl driver owns the thread until the peer
957
/// closes, so response backpressure and follow-on sockets cannot be serviced
958
/// by the outer event loop. Raw response fragments and the bounded upload stay
959
/// mutation-controlled because the dedicated mock can preload and drain them.
960
3.99k
void RemoveNonTelnetShape(curl::fuzzer::proto::Scenario* scenario) {
961
3.99k
  scenario->clear_subsequent_connections();
962
3.99k
  scenario->clear_request_headers();
963
3.99k
  scenario->clear_mime_post();
964
965
3.99k
  auto* connection = scenario->mutable_connection();
966
3.99k
  connection->clear_server_frames();
967
3.99k
  connection->clear_manual_probes();
968
3.99k
  connection->clear_backpressure();
969
3.99k
}
970
971
/// Remove the TELNET-only list and pause outcome from fixed event-driven
972
/// targets. The compatibility target skips postprocessing, so the runtime
973
/// repeats the pause-to-EOF guard before installing callbacks.
974
257k
void RemoveTelnetOnlyShape(curl::fuzzer::proto::Scenario* scenario) {
975
257k
  scenario->clear_telnet_options();
976
257k
  if (scenario->has_upload() && scenario->upload().terminal() == curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE) {
977
17
    scenario->mutable_upload()->set_terminal(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF);
978
17
  }
979
257k
}
980
981
/// Keep lifecycle work out of protocol-focused lanes. The API binary retains
982
/// this message explicitly; compatibility inputs have no postprocessor so
983
/// existing reproducers keep their historical serialized meaning.
984
240k
void RemoveApiOnlyShape(curl::fuzzer::proto::Scenario* scenario) { scenario->clear_api_plan(); }
985
986
/// Keep concurrent multi-handle work out of every other fixed lane. The
987
/// compatibility binary deliberately preserves newly-added unknown fields.
988
241k
void RemoveMultiOnlyShape(curl::fuzzer::proto::Scenario* scenario) { scenario->clear_multi_plan(); }
989
990
/// The QUIC peer consumes Http3Plan rather than the stream-socket response
991
/// script. Request headers, MIME, upload state, and HTTP options remain useful
992
/// because curl serializes those onto its client-initiated request stream.
993
7.04k
void RemoveIgnoredHttp3Shape(curl::fuzzer::proto::Scenario* scenario) {
994
7.04k
  scenario->clear_connection();
995
7.04k
  scenario->clear_subsequent_connections();
996
7.04k
  RemoveTelnetOnlyShape(scenario);
997
7.04k
  RemoveApiOnlyShape(scenario);
998
7.04k
  RemoveMultiOnlyShape(scenario);
999
7.04k
}
1000
1001
/// Remove stream-driver controls that neither file-transfer peer interprets.
1002
/// FTP consumes raw byte chunks as control/data replies, while TFTP preserves
1003
/// them as individual datagrams; structured WebSocket frames, manual probes,
1004
/// and event-loop backpressure therefore cannot affect either curl protocol.
1005
25.8k
void RemoveUnusedFileTransferConnectionShape(curl::fuzzer::proto::Connection* connection) {
1006
25.8k
  connection->clear_server_frames();
1007
25.8k
  connection->clear_manual_probes();
1008
25.8k
  connection->clear_backpressure();
1009
25.8k
}
1010
1011
/// Retain only the reusable shapes consumed by the FTP peer: one raw control
1012
/// script, a bounded sequence of passive-data scripts, and optional upload
1013
/// input. HTTP, TELNET, WebSocket, and public-API fields would otherwise absorb
1014
/// mutations despite having no representation in an FTP exchange.
1015
9.03k
void RemoveIgnoredFtpShape(curl::fuzzer::proto::Scenario* scenario) {
1016
9.03k
  scenario->clear_request_headers();
1017
9.03k
  scenario->clear_mime_post();
1018
9.03k
  RemoveTelnetOnlyShape(scenario);
1019
9.03k
  RemoveApiOnlyShape(scenario);
1020
9.03k
  RemoveMultiOnlyShape(scenario);
1021
1022
9.03k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1023
9.03k
  TrimRepeated(scenario->mutable_subsequent_connections(), scenario_limits::kMaxConnections - 1);
1024
9.03k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
1025
5.16k
    RemoveUnusedFileTransferConnectionShape(&connection);
1026
5.16k
  }
1027
9.03k
}
1028
1029
/// Retain the primary raw response script because its entries are the ordered
1030
/// UDP datagrams seen by curl, plus optional upload input for WRQ. TFTP cannot
1031
/// consume follow-on stream connections or any higher-level protocol shape.
1032
2.01k
void RemoveIgnoredTftpShape(curl::fuzzer::proto::Scenario* scenario) {
1033
2.01k
  scenario->clear_subsequent_connections();
1034
2.01k
  scenario->clear_request_headers();
1035
2.01k
  scenario->clear_mime_post();
1036
2.01k
  RemoveTelnetOnlyShape(scenario);
1037
2.01k
  RemoveApiOnlyShape(scenario);
1038
2.01k
  RemoveMultiOnlyShape(scenario);
1039
2.01k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1040
2.01k
}
1041
1042
9.63k
void RemoveIgnoredGopherShape(curl::fuzzer::proto::Scenario* scenario) {
1043
9.63k
  scenario->clear_subsequent_connections();
1044
9.63k
  scenario->clear_request_headers();
1045
9.63k
  scenario->clear_mime_post();
1046
9.63k
  scenario->clear_upload();
1047
9.63k
  RemoveTelnetOnlyShape(scenario);
1048
9.63k
  RemoveApiOnlyShape(scenario);
1049
9.63k
  RemoveMultiOnlyShape(scenario);
1050
9.63k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1051
9.63k
}
1052
1053
/// Preserve useful in-range mutations while folding ineffective extremes onto
1054
/// meaningful boundaries. Zero remains special: it disables that individual
1055
/// control and lets the other control provide the timing target's pressure.
1056
38.8k
std::uint32_t CanonicalizeNonZero(std::uint32_t value, std::uint32_t minimum, std::uint32_t maximum) {
1057
38.8k
  if (value == 0) {
1058
17.7k
    return 0;
1059
17.7k
  }
1060
21.0k
  return std::max(minimum, std::min(value, maximum));
1061
38.8k
}
1062
1063
/// Keep the timing target on the plaintext member of the protocol family.
1064
/// TLS setup has its own cost profile and would obscure whether backpressure
1065
/// mutations are exploring curl's send/receive state machines effectively.
1066
17.5k
curl::fuzzer::proto::Scheme PlaintextScheme(curl::fuzzer::proto::Scheme scheme) {
1067
17.5k
  switch (scheme) {
1068
4.15k
    case curl::fuzzer::proto::SCHEME_WS:
1069
4.15k
    case curl::fuzzer::proto::SCHEME_WSS:
1070
4.15k
      return curl::fuzzer::proto::SCHEME_WS;
1071
13.2k
    case curl::fuzzer::proto::SCHEME_HTTP:
1072
13.2k
    case curl::fuzzer::proto::SCHEME_HTTPS:
1073
13.2k
    case curl::fuzzer::proto::SCHEME_TELNET:
1074
13.2k
    case curl::fuzzer::proto::SCHEME_FTP:
1075
13.2k
    case curl::fuzzer::proto::SCHEME_TFTP:
1076
13.2k
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
1077
13.4k
    default:
1078
13.4k
      return curl::fuzzer::proto::SCHEME_HTTP;
1079
17.5k
  }
1080
17.5k
}
1081
1082
/// Remove timing controls from every connection the structured message can
1083
/// carry. Clearing only the primary script would let a mutated redirect turn a
1084
/// fixed fast lane into the timed drive loop after its second socket opens.
1085
146k
void ClearAllBackpressure(curl::fuzzer::proto::Scenario* scenario) {
1086
146k
  if (scenario->has_connection()) {
1087
146k
    scenario->mutable_connection()->clear_backpressure();
1088
146k
  }
1089
146k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
1090
65.8k
    connection.clear_backpressure();
1091
65.8k
  }
1092
146k
}
1093
1094
/// Clamp one explicitly pressure-bearing follow-on script to the same useful
1095
/// ranges as the timing lane's primary connection. An absent configuration is
1096
/// left absent so merely adding a redirect response does not add waits.
1097
6.73k
void CanonicalizeOptionalBackpressure(curl::fuzzer::proto::Connection* connection) {
1098
6.73k
  if (!connection->has_backpressure()) {
1099
4.70k
    return;
1100
4.70k
  }
1101
2.03k
  auto* backpressure = connection->mutable_backpressure();
1102
2.03k
  if (backpressure->recv_buf_bytes() == 0 && backpressure->drain_limit() != 0) {
1103
9
    backpressure->set_recv_buf_bytes(kDefaultBackpressureBufferBytes);
1104
2.02k
  } else {
1105
2.02k
    backpressure->set_recv_buf_bytes(
1106
2.02k
        CanonicalizeNonZero(backpressure->recv_buf_bytes(), kMinBackpressureBufferBytes, kMaxBackpressureBufferBytes));
1107
2.02k
  }
1108
2.03k
  backpressure->set_drain_limit(CanonicalizeNonZero(backpressure->drain_limit(), 1, kMaxDrainBytesPerIteration));
1109
2.03k
}
1110
1111
}  // namespace
1112
1113
/// Canonicalize the fields that determine which server and drive-loop policy
1114
/// execute. Fast targets discard backpressure because one mutated non-zero
1115
/// scalar otherwise opts an ordinary input into hundreds of timed waits. The
1116
/// timing target does the inverse: it guarantees a non-default buffer setting
1117
/// so its CPU allocation remains focused on the intentionally slower paths.
1118
261k
void ApplyTargetPolicy(curl::fuzzer::proto::Scenario* scenario, TargetProfile profile) {
1119
261k
  if (scenario == nullptr) {
1120
0
    return;
1121
0
  }
1122
1123
261k
  if (profile == TargetProfile::kCompatibility) {
1124
    // The original target's existing corpus predates profile splitting. A
1125
    // no-op here makes the type safe to pass around while its binary continues
1126
    // to omit postprocessor registration altogether. The append-only FTP/TFTP
1127
    // scheme values do not justify rewriting historical mixed-lane inputs.
1128
0
    return;
1129
0
  }
1130
1131
  // Accepted server push adds a harness-owned easy handle and therefore
1132
  // belongs only in the fixed-ALPN H2 origin lane. Compatibility remains a
1133
  // no-op above so accumulated mixed corpus entries keep their wire meaning.
1134
261k
  if (profile != TargetProfile::kHttpsH2) {
1135
250k
    scenario->clear_accept_h2_push();
1136
250k
  }
1137
1138
  // Only the dedicated TLS and QUIC peers consume a certificate-chain
1139
  // selector. Remove it before protocol-specific early returns so other fixed
1140
  // targets do not spend mutations on inert TLS server state.
1141
261k
  if (profile != TargetProfile::kFastHttps && profile != TargetProfile::kHttpsH2 &&
1142
194k
      profile != TargetProfile::kFastHttp3) {
1143
187k
    scenario->clear_tls_certificate_chain();
1144
187k
  }
1145
1146
  // Field 13 is append-only so the compatibility target can round-trip it,
1147
  // but every other fixed lane must discard work its peer cannot consume.
1148
261k
  if (profile != TargetProfile::kFastHttp3) {
1149
254k
    scenario->clear_http3_plan();
1150
254k
  }
1151
1152
261k
  if (profile != TargetProfile::kSocks4) {
1153
249k
    scenario->clear_socks_proxy_mode();
1154
249k
  }
1155
1156
261k
  if (profile != TargetProfile::kResolver) {
1157
248k
    scenario->clear_resolve_entries();
1158
248k
  }
1159
1160
  // Anonymous parser files add syscalls and line parsing that belong only in
1161
  // the deep HTTP lane. Clear them before every protocol-specific early path
1162
  // so fast targets never pay to normalize content they cannot consume.
1163
261k
  if (profile != TargetProfile::kDeepHttp) {
1164
234k
    scenario->clear_cookie_file();
1165
234k
    scenario->clear_altsvc_file();
1166
234k
    scenario->clear_hsts_file();
1167
234k
    scenario->clear_netrc_file();
1168
234k
  }
1169
1170
  // A CRL is useful only when the target completes a real origin TLS setup.
1171
  // Keep malformed or oversized CRLs from turning every other lane into an
1172
  // early TLS-option failure.
1173
261k
  if (profile != TargetProfile::kFastHttps) {
1174
205k
    scenario->clear_crl_file();
1175
205k
  }
1176
1177
261k
  if (profile == TargetProfile::kFastHttp3) {
1178
7.04k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTPS);
1179
7.04k
    RemoveIgnoredHttp3Shape(scenario);
1180
7.04k
    RetainHttp3RequestOptions(scenario);
1181
7.04k
    BoundScenarioShape(scenario);
1182
    // BoundScenarioShape materializes an empty primary Connection while
1183
    // sharing request-side limits. Do not retain that protocol-inert message.
1184
7.04k
    scenario->clear_connection();
1185
7.04k
    BoundHttp3PlanShape(scenario->mutable_http3_plan());
1186
7.04k
    CanonicalizeTlsAuthority(scenario);
1187
7.04k
    CanonicalizeTlsCertificateChain(scenario);
1188
7.04k
    return;
1189
7.04k
  }
1190
1191
254k
  if (profile == TargetProfile::kFastTelnet) {
1192
    // Set the scheme before general bounds so the TELNET-specific upload and
1193
    // PAUSE budgets are selected rather than event-driven compatibility ones.
1194
3.99k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_TELNET);
1195
3.99k
    RemoveApiOnlyShape(scenario);
1196
3.99k
    RemoveMultiOnlyShape(scenario);
1197
3.99k
    RemoveNonTelnetShape(scenario);
1198
3.99k
    RetainCheapTelnetOptions(scenario);
1199
3.99k
    BoundScenarioShape(scenario);
1200
3.99k
    BoundTelnetResponse(scenario->mutable_connection());
1201
3.99k
    return;
1202
3.99k
  }
1203
1204
250k
  if (profile == TargetProfile::kFastHttp) {
1205
19.6k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1206
19.6k
    RemoveApiOnlyShape(scenario);
1207
19.6k
    RemoveMultiOnlyShape(scenario);
1208
19.6k
    RemoveTelnetOnlyShape(scenario);
1209
19.6k
    RemoveDeepHttpShape(scenario);
1210
19.6k
    RetainCheapHttpOptions(scenario);
1211
19.6k
    BoundScenarioShape(scenario);
1212
19.6k
    return;
1213
19.6k
  }
1214
1215
230k
  if (profile == TargetProfile::kH2Proxy) {
1216
11.3k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1217
11.3k
    RemoveApiOnlyShape(scenario);
1218
11.3k
    RemoveMultiOnlyShape(scenario);
1219
11.3k
    RemoveTelnetOnlyShape(scenario);
1220
11.3k
    RemoveIgnoredH2ProxyShape(scenario);
1221
11.3k
    RetainH2ProxyOriginOptions(scenario);
1222
11.3k
    BoundScenarioShape(scenario);
1223
11.3k
    CanonicalizeH2ProxyOriginAuthority(scenario);
1224
11.3k
    return;
1225
11.3k
  }
1226
1227
219k
  if (profile == TargetProfile::kHttpsH2) {
1228
10.8k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTPS);
1229
10.8k
    RemoveApiOnlyShape(scenario);
1230
10.8k
    RemoveMultiOnlyShape(scenario);
1231
10.8k
    RemoveTelnetOnlyShape(scenario);
1232
10.8k
    RemoveIgnoredH2ProxyShape(scenario);
1233
10.8k
    RetainH2ProxyOriginOptions(scenario);
1234
10.8k
    BoundScenarioShape(scenario);
1235
10.8k
    CanonicalizeTlsAuthority(scenario);
1236
10.8k
    CanonicalizeTlsCertificateChain(scenario);
1237
10.8k
    return;
1238
10.8k
  }
1239
1240
208k
  if (profile == TargetProfile::kSocks4) {
1241
11.7k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1242
11.7k
    if (scenario->socks_proxy_mode() != curl::fuzzer::proto::SOCKS_PROXY_SOCKS4A) {
1243
11.7k
      scenario->set_socks_proxy_mode(curl::fuzzer::proto::SOCKS_PROXY_SOCKS4);
1244
11.7k
    }
1245
11.7k
    RemoveApiOnlyShape(scenario);
1246
11.7k
    RemoveMultiOnlyShape(scenario);
1247
11.7k
    RemoveTelnetOnlyShape(scenario);
1248
11.7k
    RemoveDeepHttpShape(scenario);
1249
11.7k
    RetainCheapHttpOptions(scenario);
1250
11.7k
    scenario->mutable_connection()->clear_initial_response();
1251
11.7k
    BoundScenarioShape(scenario);
1252
11.7k
    CanonicalizeSocksAuthority(scenario);
1253
11.7k
    return;
1254
11.7k
  }
1255
1256
196k
  if (profile == TargetProfile::kResolver) {
1257
12.5k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1258
12.5k
    RemoveApiOnlyShape(scenario);
1259
12.5k
    RemoveMultiOnlyShape(scenario);
1260
12.5k
    RemoveTelnetOnlyShape(scenario);
1261
12.5k
    RemoveDeepHttpShape(scenario);
1262
12.5k
    RetainCheapHttpOptions(scenario);
1263
12.5k
    BoundStringValues(scenario->mutable_resolve_entries(), scenario_limits::kMaxResolveEntries,
1264
12.5k
                      scenario_limits::kMaxResolveEntryBytes);
1265
12.5k
    BoundScenarioShape(scenario);
1266
12.5k
    CanonicalizeResolverAuthority(scenario);
1267
12.5k
    return;
1268
12.5k
  }
1269
1270
184k
  if (profile == TargetProfile::kFastFtp) {
1271
9.03k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_FTP);
1272
9.03k
    RemoveIgnoredFtpShape(scenario);
1273
9.03k
    RetainFtpOptions(scenario);
1274
9.03k
    CanonicalizeFtpOptionModes(scenario);
1275
9.03k
    BoundScenarioShape(scenario);
1276
9.03k
    CanonicalizeFtpAuthority(scenario);
1277
9.03k
    return;
1278
9.03k
  }
1279
1280
175k
  if (profile == TargetProfile::kFastTftp) {
1281
2.01k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_TFTP);
1282
2.01k
    RemoveIgnoredTftpShape(scenario);
1283
2.01k
    RetainTftpOptions(scenario);
1284
2.01k
    CanonicalizeTftpOptionModes(scenario);
1285
2.01k
    BoundScenarioShape(scenario);
1286
2.01k
    CanonicalizeTftpAuthority(scenario);
1287
2.01k
    return;
1288
2.01k
  }
1289
1290
173k
  if (profile == TargetProfile::kFastGopher) {
1291
9.63k
    scenario->set_scheme(scenario->scheme() == curl::fuzzer::proto::SCHEME_GOPHERS
1292
9.63k
                             ? curl::fuzzer::proto::SCHEME_GOPHERS
1293
9.63k
                             : curl::fuzzer::proto::SCHEME_GOPHER);
1294
9.63k
    RemoveIgnoredGopherShape(scenario);
1295
9.63k
    RemoveFileTransferOnlyOptions(scenario);
1296
9.63k
    BoundScenarioShape(scenario);
1297
9.63k
    CanonicalizeTlsAuthority(scenario);
1298
9.63k
    return;
1299
9.63k
  }
1300
1301
  // Select the lane's scheme before applying scheme-sensitive upload bounds.
1302
  // The scheme field is itself mutable, so bounding first could accidentally
1303
  // give an HTTP/WS case TELNET's smaller payload budget merely because that
1304
  // was the input's pre-policy value.
1305
163k
  switch (profile) {
1306
0
    case TargetProfile::kCompatibility:
1307
0
      return;
1308
26.8k
    case TargetProfile::kDeepHttp:
1309
26.8k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1310
26.8k
      break;
1311
20.6k
    case TargetProfile::kApi:
1312
20.6k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1313
20.6k
      break;
1314
19.9k
    case TargetProfile::kMulti:
1315
19.9k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1316
19.9k
      break;
1317
55.6k
    case TargetProfile::kFastHttps:
1318
55.6k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTPS);
1319
55.6k
      break;
1320
0
    case TargetProfile::kHttpsH2:
1321
      // The protocol-specific early path fixes ALPN and prunes incompatible
1322
      // response shapes while retaining the raw frame script.
1323
0
      return;
1324
0
    case TargetProfile::kFastHttp3:
1325
      // The protocol-specific early path owns the QUIC response plan.
1326
0
      return;
1327
0
    case TargetProfile::kH2Proxy:
1328
      // The early path removes proxy-incompatible fields before general
1329
      // bounds, keeping raw frame mutation dense.
1330
0
      return;
1331
0
    case TargetProfile::kSocks4:
1332
      // The early path fixes proxy routing and request-triggered replies.
1333
0
      return;
1334
0
    case TargetProfile::kResolver:
1335
      // The early path fixes safe DNS/cache routing and bounds its slist.
1336
0
      return;
1337
12.5k
    case TargetProfile::kFastWebSocket:
1338
12.5k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_WS);
1339
12.5k
      break;
1340
10.4k
    case TargetProfile::kFastSecureWebSocket:
1341
10.4k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_WSS);
1342
10.4k
      break;
1343
17.5k
    case TargetProfile::kTiming:
1344
17.5k
      scenario->set_scheme(PlaintextScheme(scenario->scheme()));
1345
17.5k
      break;
1346
0
    case TargetProfile::kFastHttp:
1347
0
    case TargetProfile::kFastTelnet:
1348
0
    case TargetProfile::kFastFtp:
1349
0
    case TargetProfile::kFastTftp:
1350
0
    case TargetProfile::kFastGopher:
1351
      // Protocol-specific early-return paths selected their scheme above.
1352
0
      return;
1353
163k
  }
1354
1355
  // TELNET's retained slist and synchronous pause have no observable, safe
1356
  // meaning in the fixed event-driven lanes. Clear them before walking the
1357
  // general shape so only the compatibility and TELNET targets can retain
1358
  // those values.
1359
163k
  RemoveTelnetOnlyShape(scenario);
1360
163k
  if (profile != TargetProfile::kApi) {
1361
142k
    RemoveApiOnlyShape(scenario);
1362
142k
  }
1363
163k
  if (profile != TargetProfile::kMulti) {
1364
143k
    RemoveMultiOnlyShape(scenario);
1365
143k
  }
1366
163k
  RemoveFileTransferOnlyOptions(scenario);
1367
163k
  BoundScenarioShape(scenario);
1368
1369
163k
  switch (profile) {
1370
0
    case TargetProfile::kCompatibility:
1371
0
      return;
1372
0
    case TargetProfile::kFastHttp:
1373
      // Handled before the general bounds so discarded deep shapes are never
1374
      // traversed on the fast path.
1375
0
      return;
1376
1377
26.8k
    case TargetProfile::kDeepHttp:
1378
26.8k
      ClearAllBackpressure(scenario);
1379
26.8k
      if (!scenario->altsvc_file().empty()) {
1380
689
        CanonicalizeAltSvcAuthority(scenario);
1381
689
      }
1382
26.8k
      return;
1383
1384
20.6k
    case TargetProfile::kApi:
1385
20.6k
      ClearAllBackpressure(scenario);
1386
20.6k
      if (scenario->host_path().size() > scenario_limits::kMaxApiStringBytes) {
1387
3
        scenario->mutable_host_path()->resize(scenario_limits::kMaxApiStringBytes);
1388
3
      }
1389
20.6k
      if (scenario->has_api_plan()) {
1390
5.15k
        BoundApiPlanShape(scenario->mutable_api_plan());
1391
5.15k
      }
1392
20.6k
      return;
1393
1394
19.9k
    case TargetProfile::kMulti: {
1395
19.9k
      ClearAllBackpressure(scenario);
1396
19.9k
      CanonicalizeMultiAuthority(scenario);
1397
19.9k
      auto* plan = scenario->mutable_multi_plan();
1398
19.9k
      BoundMultiPlanShape(plan);
1399
19.9k
      TrimRepeated(scenario->mutable_subsequent_connections(), plan->transfer_count() - 1U);
1400
19.9k
      return;
1401
0
    }
1402
1403
55.6k
    case TargetProfile::kFastHttps:
1404
55.6k
      ClearAllBackpressure(scenario);
1405
55.6k
      CanonicalizeTlsAuthority(scenario);
1406
55.6k
      CanonicalizeTlsCertificateChain(scenario);
1407
55.6k
      return;
1408
1409
0
    case TargetProfile::kHttpsH2:
1410
      // Handled by the raw HTTP/2 protocol-specific early path above.
1411
0
      return;
1412
1413
0
    case TargetProfile::kFastHttp3:
1414
      // Handled before generic connection bounding because Http3Plan replaces
1415
      // the stream-socket response script.
1416
0
      return;
1417
1418
0
    case TargetProfile::kH2Proxy:
1419
      // Handled by the protocol-specific early path above.
1420
0
      return;
1421
1422
0
    case TargetProfile::kSocks4:
1423
      // Handled by the protocol-specific early path above.
1424
0
      return;
1425
1426
0
    case TargetProfile::kResolver:
1427
      // Handled by the resolver-specific early path above.
1428
0
      return;
1429
1430
12.5k
    case TargetProfile::kFastWebSocket:
1431
12.5k
      ClearAllBackpressure(scenario);
1432
12.5k
      RemoveIgnoredWebSocketShape(scenario);
1433
12.5k
      return;
1434
1435
10.4k
    case TargetProfile::kFastSecureWebSocket:
1436
10.4k
      ClearAllBackpressure(scenario);
1437
10.4k
      RemoveIgnoredWebSocketShape(scenario);
1438
10.4k
      return;
1439
1440
0
    case TargetProfile::kFastTelnet:
1441
      // Handled before the general bounds so its protocol-specific limits are
1442
      // selected from the start.
1443
0
      return;
1444
1445
0
    case TargetProfile::kFastFtp:
1446
0
    case TargetProfile::kFastTftp:
1447
0
    case TargetProfile::kFastGopher:
1448
      // Their peers consume narrower raw-script shapes, pruned before general
1449
      // bounds so ignored fields never tax these fast paths.
1450
0
      return;
1451
1452
17.5k
    case TargetProfile::kTiming: {
1453
17.5k
      if (scenario->scheme() == curl::fuzzer::proto::SCHEME_WS) {
1454
4.15k
        RemoveIgnoredWebSocketShape(scenario);
1455
4.15k
      }
1456
17.5k
      auto* backpressure = scenario->mutable_connection()->mutable_backpressure();
1457
17.5k
      if (backpressure->recv_buf_bytes() == 0) {
1458
        // A drain limit alone cannot fill the default AF_UNIX buffer with the
1459
        // harness's bounded upload. Always tighten the socket so this lane
1460
        // represents real pressure, not merely selection of the timed loop.
1461
347
        backpressure->set_recv_buf_bytes(kDefaultBackpressureBufferBytes);
1462
17.2k
      } else {
1463
17.2k
        backpressure->set_recv_buf_bytes(CanonicalizeNonZero(backpressure->recv_buf_bytes(),
1464
17.2k
                                                             kMinBackpressureBufferBytes, kMaxBackpressureBufferBytes));
1465
17.2k
      }
1466
17.5k
      backpressure->set_drain_limit(CanonicalizeNonZero(backpressure->drain_limit(), 1, kMaxDrainBytesPerIteration));
1467
17.5k
      for (auto& connection : *scenario->mutable_subsequent_connections()) {
1468
6.73k
        CanonicalizeOptionalBackpressure(&connection);
1469
6.73k
      }
1470
17.5k
      return;
1471
0
    }
1472
163k
  }
1473
163k
}
1474
1475
}  // namespace proto_fuzzer