Coverage Report

Created: 2026-10-15 06:25

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 <curl/curl.h>
13
14
#include <algorithm>
15
#include <array>
16
#include <cstddef>
17
#include <cstdint>
18
#include <string>
19
20
#include "proto_fuzzer/option_model.h"
21
#include "proto_fuzzer/scenario_limits.h"
22
#include "proto_fuzzer/telnet_scenario.h"
23
24
namespace proto_fuzzer {
25
26
namespace {
27
28
// Linux raises smaller socket-buffer requests to an implementation minimum,
29
// so 2048 is both cheap and reliably small enough to exercise short writes.
30
constexpr std::uint32_t kDefaultBackpressureBufferBytes = 2048;
31
32
// Values outside these ranges do not create useful new socket behavior for
33
// the harness's bounded 4-16 KiB writes. Keeping them small also prevents a
34
// mutated uint32 recv size from overflowing the int accepted by setsockopt.
35
constexpr std::uint32_t kMinBackpressureBufferBytes = 2048;
36
constexpr std::uint32_t kMaxBackpressureBufferBytes = 4096;
37
constexpr std::uint32_t kMaxDrainBytesPerIteration = 1024;
38
39
/// Remove a repeated-field suffix that the runtime would ignore. Doing this
40
/// in LPM's postprocessor matters for speed as well as memory: otherwise later
41
/// mutations keep rediscovering and editing objects that cannot reach curl.
42
template <typename RepeatedField>
43
2.56M
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
2.56M
  const std::size_t size = static_cast<std::size_t>(field->size());
45
2.56M
  if (size > limit) {
46
2.89k
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
2.89k
  }
48
2.56M
}
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
43
322k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
322k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
322k
  if (size > limit) {
46
810
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
810
  }
48
322k
}
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
43
73.7k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
73.7k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
73.7k
  if (size > limit) {
46
311
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
311
  }
48
73.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
43
28.1k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
28.1k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
28.1k
  if (size > limit) {
46
148
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
148
  }
48
28.1k
}
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
43
1.17M
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
1.17M
  const std::size_t size = static_cast<std::size_t>(field->size());
45
1.17M
  if (size > limit) {
46
754
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
754
  }
48
1.17M
}
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
43
399k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
399k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
399k
  if (size > limit) {
46
188
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
188
  }
48
399k
}
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
43
9.34k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
9.34k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
9.34k
  if (size > limit) {
46
76
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
76
  }
48
9.34k
}
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
43
7.18k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
7.18k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
7.18k
  if (size > limit) {
46
24
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
24
  }
48
7.18k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http2Action> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http2Action>*, unsigned long)
Line
Count
Source
43
19.8k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
19.8k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
19.8k
  if (size > limit) {
46
60
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
60
  }
48
19.8k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http2Setting> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http2Setting>*, unsigned long)
Line
Count
Source
43
28.0k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
28.0k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
28.0k
  if (size > limit) {
46
22
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
22
  }
48
28.0k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http2Header> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http2Header>*, unsigned long)
Line
Count
Source
43
119k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
119k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
119k
  if (size > limit) {
46
11
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
11
  }
48
119k
}
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
43
22.7k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
22.7k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
22.7k
  if (size > limit) {
46
14
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
14
  }
48
22.7k
}
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
43
354k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
44
354k
  const std::size_t size = static_cast<std::size_t>(field->size());
45
354k
  if (size > limit) {
46
480
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
47
480
  }
48
354k
}
49
50
/// Bound fixed-target metadata. Compatibility inputs preserve their original
51
/// option strings; consumers of bounded request metadata cap their own views.
52
662k
void TrimMetadata(std::string* value) {
53
662k
  if (value->size() > scenario_limits::kMaxMetadataBytes) {
54
99
    value->resize(scenario_limits::kMaxMetadataBytes);
55
99
  }
56
662k
}
57
58
/// Give the successful-TLS lane a hostname its fixed certificate can verify
59
/// while retaining the fuzz-controlled path, query, and fragment. Arbitrary
60
/// authorities remain covered by the compatibility and legacy HTTPS lanes;
61
/// spending this lane's mutations on URL failures would keep curl's peer-cert
62
/// and encrypted application-data paths dark.
63
131k
void CanonicalizeTlsAuthority(curl::fuzzer::proto::Scenario* scenario) {
64
131k
  const std::string& host_path = scenario->host_path();
65
131k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
66
131k
  if (suffix_start == std::string::npos) {
67
12.6k
    scenario->set_host_path("tls.test/");
68
12.6k
    return;
69
12.6k
  }
70
119k
  scenario->set_host_path("tls.test" + host_path.substr(suffix_start));
71
119k
}
72
73
/// Alt-Svc lookup cannot run while the harness's wildcard CONNECT_TO route is
74
/// active. Give file-backed Alt-Svc inputs one fixed origin whose port can be
75
/// populated in curl's DNS cache before that override is safely detached,
76
/// while preserving all mutation-controlled path, query, and fragment bytes.
77
1.53k
void CanonicalizeAltSvcAuthority(curl::fuzzer::proto::Scenario* scenario) {
78
1.53k
  const std::string& host_path = scenario->host_path();
79
1.53k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
80
1.53k
  if (suffix_start == std::string::npos) {
81
9
    scenario->set_host_path("altsvc-origin.test/");
82
9
    return;
83
9
  }
84
1.53k
  scenario->set_host_path("altsvc-origin.test" + host_path.substr(suffix_start));
85
1.53k
}
86
87
/// Both fixed TLS peers support the same closed set of checked-in certificate
88
/// bundles. Unknown proto3 enum values fall back to the historical EC chain.
89
101k
void CanonicalizeTlsCertificateChain(curl::fuzzer::proto::Scenario* scenario) {
90
101k
  switch (scenario->tls_certificate_chain()) {
91
98.3k
    case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC:
92
101k
    case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES:
93
101k
      return;
94
102
    default:
95
102
      scenario->clear_tls_certificate_chain();
96
102
      return;
97
101k
  }
98
101k
}
99
100
/// Keep the HTTPS/HTTP2 group selector inside its closed, useful set. Unknown
101
/// proto3 enum values take the inexpensive historical-input path.
102
16.5k
void CanonicalizeTlsGroupProfile(curl::fuzzer::proto::Scenario* scenario) {
103
16.5k
  switch (scenario->tls_group_profile()) {
104
16.1k
    case curl::fuzzer::proto::TLS_GROUP_X25519:
105
16.5k
    case curl::fuzzer::proto::TLS_GROUP_PROVIDER_DEFAULT:
106
16.5k
      return;
107
1
    default:
108
1
      scenario->clear_tls_group_profile();
109
1
      return;
110
16.5k
  }
111
16.5k
}
112
113
/// Keep the tunneled origin parseable while retaining every path, query, and
114
/// fragment byte. The fixed numeric proxy endpoint handles routing separately;
115
/// mutating the origin authority would therefore buy only early URL failures,
116
/// not additional HTTP/2 proxy behavior.
117
13.0k
void CanonicalizeH2ProxyOriginAuthority(curl::fuzzer::proto::Scenario* scenario) {
118
13.0k
  const std::string& host_path = scenario->host_path();
119
13.0k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
120
13.0k
  if (suffix_start == std::string::npos) {
121
936
    scenario->set_host_path("origin.test/");
122
936
    return;
123
936
  }
124
12.0k
  scenario->set_host_path("origin.test" + host_path.substr(suffix_start));
125
12.0k
}
126
127
/// SOCKS4 must resolve a real local name before constructing its request,
128
/// while SOCKS4A deliberately carries a hostname to the proxy. Both
129
/// authorities remain fixed and the path/query/fragment stays mutable.
130
16.9k
void CanonicalizeSocksAuthority(curl::fuzzer::proto::Scenario* scenario) {
131
16.9k
  const std::string& host_path = scenario->host_path();
132
16.9k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
133
16.9k
  const char* host =
134
16.9k
      scenario->socks_proxy_mode() == curl::fuzzer::proto::SOCKS_PROXY_SOCKS4A ? "socks.test" : "localhost";
135
16.9k
  if (suffix_start == std::string::npos) {
136
729
    scenario->set_host_path(std::string(host) + "/");
137
729
    return;
138
729
  }
139
16.2k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
140
16.2k
}
141
142
/// Use libc/curl's localhost path when there are no structured entries, and a
143
/// fixed cache-backed name when entries are present. In both cases the path,
144
/// query, and fragment remain mutation-controlled.
145
15.1k
void CanonicalizeResolverAuthority(curl::fuzzer::proto::Scenario* scenario) {
146
15.1k
  const std::string& host_path = scenario->host_path();
147
15.1k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
148
15.1k
  const char* host = scenario->resolve_entries().empty() ? "localhost" : "resolve.test";
149
15.1k
  if (suffix_start == std::string::npos) {
150
794
    scenario->set_host_path(std::string(host) + "/");
151
794
    return;
152
794
  }
153
14.3k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
154
14.3k
}
155
156
/// Give the TFTP lane a parseable filename-bearing URL while retaining the
157
/// fuzz-controlled path, query, and fragment. The UDP peer rewrites curl's
158
/// destination after URL parsing, so authority mutations cannot reach another
159
/// host; canonicalizing them here avoids spending most iterations on failures
160
/// before curl constructs a TFTP request. An explicit slash is preserved so
161
/// the missing-filename error remains reachable.
162
2.50k
void CanonicalizeTftpAuthority(curl::fuzzer::proto::Scenario* scenario) {
163
2.50k
  const std::string& host_path = scenario->host_path();
164
2.50k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
165
2.50k
  if (suffix_start == std::string::npos) {
166
797
    scenario->set_host_path("tftp.test/file");
167
797
    return;
168
797
  }
169
1.70k
  scenario->set_host_path("tftp.test" + host_path.substr(suffix_start));
170
1.70k
}
171
172
/// Keep the FTP lane inside the same parseable authority while leaving every
173
/// path segment and wildcard under mutation control. CONNECT_TO already
174
/// confines networking, but rejecting malformed authorities before USER/PWD
175
/// would waste the control/data peer this target uniquely provides.
176
8.90k
void CanonicalizeFtpAuthority(curl::fuzzer::proto::Scenario* scenario) {
177
8.90k
  const std::string& host_path = scenario->host_path();
178
8.90k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
179
8.90k
  if (suffix_start == std::string::npos) {
180
811
    scenario->set_host_path("ftp.test/file");
181
811
    return;
182
811
  }
183
8.09k
  scenario->set_host_path("ftp.test" + host_path.substr(suffix_start));
184
8.09k
}
185
186
/// Put every handle in the multi lane on one origin so connection limits,
187
/// queueing, and reuse affect real transfers instead of independent hosts.
188
/// Path/query/fragment bytes remain mutation-controlled.
189
22.7k
void CanonicalizeMultiAuthority(curl::fuzzer::proto::Scenario* scenario) {
190
22.7k
  const std::string& host_path = scenario->host_path();
191
22.7k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
192
22.7k
  if (suffix_start == std::string::npos) {
193
796
    scenario->set_host_path("multi.test/");
194
796
    return;
195
796
  }
196
21.9k
  scenario->set_host_path("multi.test" + host_path.substr(suffix_start));
197
21.9k
}
198
199
template <typename RepeatedBytes>
200
775k
void BoundStringValues(RepeatedBytes* values, std::size_t count_limit, std::size_t value_limit) {
201
775k
  TrimRepeated(values, count_limit);
202
775k
  for (std::string& value : *values) {
203
168k
    if (value.size() > value_limit) {
204
88
      value.resize(value_limit);
205
88
    }
206
168k
  }
207
775k
}
208
209
template <typename RepeatedBytes>
210
437k
void BoundHeaderValues(RepeatedBytes* headers, std::size_t limit) {
211
437k
  BoundStringValues(headers, limit, scenario_limits::kMaxMetadataBytes);
212
437k
}
213
214
/// Keep one response script identical to the prefix MockServer and
215
/// WebSocketMockServer can deliver. Raw chunks take precedence over structured
216
/// frames, matching both runtime serializers.
217
399k
void BoundConnectionShape(curl::fuzzer::proto::Connection* connection) {
218
399k
  TrimRepeated(connection->mutable_on_readable(), scenario_limits::kMaxResponseChunks);
219
399k
  const std::size_t raw_count = static_cast<std::size_t>(connection->on_readable_size());
220
399k
  TrimRepeated(connection->mutable_server_frames(), scenario_limits::kMaxResponseChunks - raw_count);
221
399k
}
222
223
/// Retain the observable prefix of one filename-backed parser input while
224
/// debiting the shared per-scenario file-byte budget.
225
1.61M
void BoundFileInput(std::string* input, std::size_t* remaining_bytes) {
226
1.61M
  const std::size_t limit = std::min(scenario_limits::kMaxFileInputBytes, *remaining_bytes);
227
1.61M
  if (input->size() > limit) {
228
0
    input->resize(limit);
229
0
  }
230
1.61M
  *remaining_bytes -= input->size();
231
1.61M
}
232
233
322k
void BoundFileInputs(curl::fuzzer::proto::Scenario* scenario) {
234
322k
  std::size_t remaining_bytes = scenario_limits::kMaxFileInputTotalBytes;
235
322k
  BoundFileInput(scenario->mutable_cookie_file(), &remaining_bytes);
236
322k
  BoundFileInput(scenario->mutable_altsvc_file(), &remaining_bytes);
237
322k
  BoundFileInput(scenario->mutable_hsts_file(), &remaining_bytes);
238
322k
  BoundFileInput(scenario->mutable_netrc_file(), &remaining_bytes);
239
322k
  BoundFileInput(scenario->mutable_crl_file(), &remaining_bytes);
240
322k
}
241
242
/// Apply the metadata/header limits shared by both MIME part message types.
243
template <typename Part>
244
115k
void BoundMimePartMetadata(Part* part) {
245
115k
  TrimMetadata(part->mutable_name());
246
115k
  TrimMetadata(part->mutable_filename());
247
115k
  TrimMetadata(part->mutable_content_type());
248
115k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
249
115k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundMimePartMetadata<curl::fuzzer::proto::MimePart>(curl::fuzzer::proto::MimePart*)
Line
Count
Source
244
62.6k
void BoundMimePartMetadata(Part* part) {
245
62.6k
  TrimMetadata(part->mutable_name());
246
62.6k
  TrimMetadata(part->mutable_filename());
247
62.6k
  TrimMetadata(part->mutable_content_type());
248
62.6k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
249
62.6k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundMimePartMetadata<curl::fuzzer::proto::MimeDataPart>(curl::fuzzer::proto::MimeDataPart*)
Line
Count
Source
244
52.7k
void BoundMimePartMetadata(Part* part) {
245
52.7k
  TrimMetadata(part->mutable_name());
246
52.7k
  TrimMetadata(part->mutable_filename());
247
52.7k
  TrimMetadata(part->mutable_content_type());
248
52.7k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
249
52.7k
}
250
251
18.3k
void BoundGeneratedMimeData(curl::fuzzer::proto::GeneratedBytes* generated, std::size_t* remaining_bytes) {
252
18.3k
  if (generated->pattern().size() > scenario_limits::kMaxGeneratedMimePatternBytes) {
253
28
    generated->mutable_pattern()->resize(scenario_limits::kMaxGeneratedMimePatternBytes);
254
28
  }
255
256
18.3k
  const std::size_t pattern_size = generated->pattern().size();
257
18.3k
  if (pattern_size == 0 || *remaining_bytes == 0) {
258
6.24k
    generated->clear_repeat_count();
259
6.24k
    return;
260
6.24k
  }
261
262
  // Keep every exact in-range size, including 16/32/64 KiB and their +/-1
263
  // neighborhoods. Counts beyond the shared budget all have identical runtime
264
  // behavior, so fold them onto the largest observable repetition count.
265
12.1k
  const std::size_t max_repeat_count = *remaining_bytes / pattern_size;
266
12.1k
  const std::size_t repeat_count =
267
12.1k
      std::min<std::size_t>(static_cast<std::size_t>(generated->repeat_count()), max_repeat_count);
268
12.1k
  generated->set_repeat_count(static_cast<std::uint32_t>(repeat_count));
269
12.1k
  *remaining_bytes -= repeat_count * pattern_size;
270
12.1k
}
271
272
52.7k
void BoundMimeLeaf(curl::fuzzer::proto::MimeDataPart* part, std::size_t* remaining_generated_bytes) {
273
52.7k
  BoundMimePartMetadata(part);
274
52.7k
  if (part->content_case() == curl::fuzzer::proto::MimeDataPart::kGeneratedData) {
275
8.10k
    BoundGeneratedMimeData(part->mutable_generated_data(), remaining_generated_bytes);
276
44.6k
  } else if (part->data().size() > scenario_limits::kMaxMimeDataBytes) {
277
4
    part->mutable_data()->resize(scenario_limits::kMaxMimeDataBytes);
278
4
  }
279
52.7k
}
280
281
/// Mirror the runtime's shared top-level/nested part budget in the protobuf
282
/// itself. A simple per-list cap is insufficient because many bounded child
283
/// lists could still leave most of the message semantically dead.
284
36.8k
void BoundMimeShape(curl::fuzzer::proto::MimePost* post) {
285
36.8k
  TrimRepeated(post->mutable_parts(), scenario_limits::kMaxTopLevelMimeParts);
286
36.8k
  std::size_t remaining = scenario_limits::kMaxTotalMimeParts;
287
36.8k
  std::size_t remaining_generated_bytes = scenario_limits::kMaxGeneratedMimeDataBytes;
288
36.8k
  std::size_t retained_top_parts = 0;
289
290
99.4k
  while (retained_top_parts < static_cast<std::size_t>(post->parts_size()) && remaining != 0) {
291
62.6k
    auto* part = post->mutable_parts(static_cast<int>(retained_top_parts));
292
62.6k
    ++retained_top_parts;
293
62.6k
    --remaining;
294
62.6k
    BoundMimePartMetadata(part);
295
296
62.6k
    if (part->content_case() == curl::fuzzer::proto::MimePart::kData) {
297
4.66k
      if (part->data().size() > scenario_limits::kMaxMimeDataBytes) {
298
6
        part->mutable_data()->resize(scenario_limits::kMaxMimeDataBytes);
299
6
      }
300
4.66k
      continue;
301
4.66k
    }
302
57.9k
    if (part->content_case() == curl::fuzzer::proto::MimePart::kGeneratedData) {
303
10.2k
      BoundGeneratedMimeData(part->mutable_generated_data(), &remaining_generated_bytes);
304
10.2k
      continue;
305
10.2k
    }
306
47.6k
    if (part->content_case() != curl::fuzzer::proto::MimePart::kSubparts) {
307
19.5k
      continue;
308
19.5k
    }
309
310
28.1k
    auto* children = part->mutable_subparts()->mutable_parts();
311
28.1k
    TrimRepeated(children, std::min(scenario_limits::kMaxNestedMimeParts, remaining));
312
52.7k
    for (auto& child : *children) {
313
52.7k
      BoundMimeLeaf(&child, &remaining_generated_bytes);
314
52.7k
      --remaining;
315
52.7k
    }
316
28.1k
  }
317
318
36.8k
  TrimRepeated(post->mutable_parts(), retained_top_parts);
319
36.8k
}
320
321
/// Remove upload bytes and read steps the callback cannot observe. Clamping
322
/// individual limits also keeps mutations concentrated on short reads instead
323
/// of many distinct uint32 values that all collapse to the same 16 KiB cap.
324
void BoundUploadShape(curl::fuzzer::proto::UploadScript* upload, std::size_t data_limit, std::size_t read_step_limit,
325
13.9k
                      std::size_t read_size_limit) {
326
13.9k
  if (upload->data().size() > data_limit) {
327
13
    upload->mutable_data()->resize(data_limit);
328
13
  }
329
  // RepeatedField<uint32_t> lacks RepeatedPtrField's DeleteSubrange helper;
330
  // removing the ignored suffix from the end is constant-time per element and
331
  // preserves the mutation-significant prefix exactly.
332
83.7k
  while (static_cast<std::size_t>(upload->read_sizes_size()) > read_step_limit) {
333
69.8k
    upload->mutable_read_sizes()->RemoveLast();
334
69.8k
  }
335
64.1k
  for (int i = 0; i < upload->read_sizes_size(); ++i) {
336
50.2k
    if (upload->read_sizes(i) > read_size_limit) {
337
678
      upload->set_read_sizes(i, static_cast<std::uint32_t>(read_size_limit));
338
678
    }
339
50.2k
  }
340
13.9k
}
341
342
/// Trim a protobuf repeated scalar without depending on the container's
343
/// pointer-field-only DeleteSubrange API. Keeping the mutation-significant
344
/// prefix matches every runtime selector loop.
345
template <typename RepeatedScalar>
346
17.4k
void TrimRepeatedScalar(RepeatedScalar* values, std::size_t limit) {
347
18.3k
  while (static_cast<std::size_t>(values->size()) > limit) {
348
910
    values->RemoveLast();
349
910
  }
350
17.4k
}
351
352
/// Canonicalize the allocation failure plan to the same fixed-size sequence
353
/// consumed by the runtime controller. Sorting lets its hot callback compare
354
/// only the next ordinal, while retaining the lowest distinct values keeps
355
/// an oversized mutation focused on reachable work.
356
673k
void CanonicalizeAllocationFailures(curl::fuzzer::proto::Scenario* scenario) {
357
673k
  std::array<std::uint32_t, scenario_limits::kMaxAllocationFailures> retained{};
358
673k
  std::size_t retained_count = 0;
359
673k
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxAllocationOrdinal);
360
361
673k
  for (const std::uint32_t raw : scenario->fail_allocations()) {
362
124k
    if (raw == 0) {
363
4.76k
      continue;
364
4.76k
    }
365
119k
    const std::uint32_t ordinal = 1U + ((raw - 1U) % maximum);
366
119k
    const auto begin = retained.begin();
367
119k
    const auto end = begin + static_cast<std::ptrdiff_t>(retained_count);
368
119k
    const auto position = std::lower_bound(begin, end, ordinal);
369
119k
    if (position != end && *position == ordinal) {
370
25.0k
      continue;
371
25.0k
    }
372
94.6k
    const std::size_t index = static_cast<std::size_t>(position - begin);
373
94.6k
    if (index >= retained.size()) {
374
40.8k
      continue;
375
40.8k
    }
376
53.7k
    if (retained_count < retained.size()) {
377
51.6k
      ++retained_count;
378
51.6k
    }
379
59.0k
    for (std::size_t destination = retained_count - 1; destination > index; --destination) {
380
5.27k
      retained[destination] = retained[destination - 1];
381
5.27k
    }
382
53.7k
    retained[index] = ordinal;
383
53.7k
  }
384
385
673k
  auto* values = scenario->mutable_fail_allocations();
386
673k
  values->Clear();
387
725k
  for (std::size_t index = 0; index < retained_count; ++index) {
388
51.6k
    values->Add(retained[index]);
389
51.6k
  }
390
673k
}
391
392
/// Keep injected failures out of curl's debug event adapter. Its socket
393
/// monitor is heap-backed, and rejecting the allocation that records a newly
394
/// announced socket leaves a later CURL_POLL_REMOVE with no matching entry.
395
/// Ordinary inputs still cover curl_easy_perform_ev; allocation plans use the
396
/// equivalent blocking easy entrypoint and retain a stable allocation trace.
397
673k
void CanonicalizeAllocationFailureDrive(curl::fuzzer::proto::Scenario* scenario) {
398
673k
  if (!scenario->fail_allocations().empty() && scenario->has_api_plan() &&
399
1.30k
      scenario->api_plan().drive_mode() == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) {
400
6
    scenario->mutable_api_plan()->set_drive_mode(curl::fuzzer::proto::API_DRIVE_EASY_PERFORM);
401
6
  }
402
673k
}
403
404
/// Return whether a scenario can install MIME through curl's HTTP reader path.
405
8.12k
bool UsesHttpMimeReader(const curl::fuzzer::proto::Scenario& scenario) {
406
8.12k
  switch (scenario.scheme()) {
407
4.79k
    case curl::fuzzer::proto::SCHEME_HTTP:
408
8.06k
    case curl::fuzzer::proto::SCHEME_HTTPS:
409
8.09k
    case curl::fuzzer::proto::SCHEME_WS:
410
8.11k
    case curl::fuzzer::proto::SCHEME_WSS:
411
8.11k
      return true;
412
413
2
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
414
8
    case curl::fuzzer::proto::SCHEME_TELNET:
415
8
    case curl::fuzzer::proto::SCHEME_FTP:
416
8
    case curl::fuzzer::proto::SCHEME_TFTP:
417
9
    case curl::fuzzer::proto::SCHEME_GOPHER:
418
9
    case curl::fuzzer::proto::SCHEME_GOPHERS:
419
9
    default:
420
9
      return false;
421
8.12k
  }
422
8.12k
}
423
424
/// Keep an injected failure from leaving curl's MIME client-reader stack
425
/// dangling. With line-ending conversion enabled, curl installs the MIME
426
/// reader before allocating the conversion reader. If that second allocation
427
/// fails, the current error path frees the MIME reader without clearing the
428
/// installed stack pointer. Ordinary inputs still cover MIME plus conversion;
429
/// failure plans retain MIME processing and use curl's unconverted reader
430
/// path. Non-HTTP schemes do not install this reader, so their conversion
431
/// options remain useful and safe even if a compatibility input carries MIME.
432
673k
void CanonicalizeAllocationFailureOptions(curl::fuzzer::proto::Scenario* scenario) {
433
673k
  if (scenario->fail_allocations().empty() || !scenario->has_mime_post() || !UsesHttpMimeReader(*scenario)) {
434
665k
    return;
435
665k
  }
436
437
8.11k
  auto* options = scenario->mutable_options();
438
8.11k
  int retained = 0;
439
27.8k
  for (int index = 0; index < options->size(); ++index) {
440
19.7k
    const auto option_id = options->Get(index).option_id();
441
19.7k
    if (option_id == curl::fuzzer::proto::CURLOPT_CRLF || option_id == curl::fuzzer::proto::CURLOPT_TRANSFERTEXT) {
442
70
      continue;
443
70
    }
444
19.6k
    if (retained != index) {
445
140
      options->SwapElements(retained, index);
446
140
    }
447
19.6k
    ++retained;
448
19.6k
  }
449
8.11k
  options->DeleteSubrange(retained, options->size() - retained);
450
8.11k
}
451
452
/// Keep API work proportional to the fixed descriptor tables used by the
453
/// runtime. Selector magnitudes stay mutation-controlled because the runtime
454
/// folds them into the relevant typed table; only suffixes it cannot execute
455
/// are dead and therefore removed here.
456
5.82k
void BoundApiPlanShape(curl::fuzzer::proto::ApiPlan* plan) {
457
5.82k
  TrimRepeatedScalar(plan->mutable_share_data_selectors(), scenario_limits::kMaxApiShareDataSelectors);
458
5.82k
  TrimRepeatedScalar(plan->mutable_easy_info_selectors(), scenario_limits::kMaxApiInfoSelectors);
459
5.82k
  TrimRepeatedScalar(plan->mutable_reentrant_probe_selectors(), scenario_limits::kMaxApiReentrantSelectors);
460
461
5.82k
  switch (plan->drive_mode()) {
462
4.00k
    case curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM:
463
4.51k
    case curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET:
464
4.51k
      break;
465
872
    case curl::fuzzer::proto::API_DRIVE_EASY_PERFORM:
466
1.22k
    case curl::fuzzer::proto::API_DRIVE_EASY_EVENTS:
467
1.31k
    case curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY:
468
      // Wakeup is a multi-handle API and has no live object in easy mode.
469
      // Likewise, a body callback cannot be resumed while one of these
470
      // blocking entrypoints owns the thread. Clearing both mutations keeps
471
      // every retained API-plan value observable and bounded.
472
1.31k
      plan->set_wake_multi(false);
473
1.31k
      plan->set_pause_response_once(false);
474
1.31k
      break;
475
1
    default:
476
1
      plan->set_drive_mode(curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM);
477
1
      break;
478
5.82k
  }
479
5.82k
}
480
481
/// Keep concurrent-handle work within the mock's fixed socket and operation
482
/// budgets. Values are canonicalized here rather than only at runtime so LPM
483
/// mutates state that the target can actually distinguish.
484
22.7k
void BoundMultiPlanShape(curl::fuzzer::proto::MultiPlan* plan) {
485
22.7k
  const std::uint32_t minimum = static_cast<std::uint32_t>(scenario_limits::kMinMultiTransfers);
486
22.7k
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxMultiTransfers);
487
22.7k
  const std::uint32_t transfer_count = std::max(minimum, std::min(plan->transfer_count(), maximum));
488
22.7k
  plan->set_transfer_count(transfer_count);
489
22.7k
  plan->set_max_host_connections(std::min(plan->max_host_connections(), transfer_count));
490
22.7k
  plan->set_max_total_connections(std::min(plan->max_total_connections(), transfer_count));
491
22.7k
  plan->set_connection_cache_size(std::min(plan->connection_cache_size(), maximum * 2U));
492
22.7k
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxMultiActions);
493
494
22.7k
  switch (plan->drive_mode()) {
495
21.5k
    case curl::fuzzer::proto::MULTI_DRIVE_PERFORM:
496
22.7k
    case curl::fuzzer::proto::MULTI_DRIVE_SOCKET:
497
22.7k
      break;
498
8
    default:
499
8
      plan->set_drive_mode(curl::fuzzer::proto::MULTI_DRIVE_PERFORM);
500
8
      break;
501
22.7k
  }
502
503
22.7k
  for (auto& action : *plan->mutable_actions()) {
504
19.8k
    action.set_transfer_selector(action.transfer_selector() % transfer_count);
505
19.8k
    switch (action.kind()) {
506
4.04k
      case curl::fuzzer::proto::MULTI_ACTION_NONE:
507
4.68k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_RECV:
508
5.26k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_SEND:
509
5.87k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_ALL:
510
6.16k
      case curl::fuzzer::proto::MULTI_ACTION_RESUME:
511
13.1k
      case curl::fuzzer::proto::MULTI_ACTION_REMOVE:
512
19.8k
      case curl::fuzzer::proto::MULTI_ACTION_READD:
513
19.8k
        break;
514
36
      default:
515
36
        action.set_kind(curl::fuzzer::proto::MULTI_ACTION_NONE);
516
36
        break;
517
19.8k
    }
518
19.8k
  }
519
22.7k
}
520
521
/// HTTP field names are lowercase RFC token bytes in the structured lane.
522
/// Replacing (rather than deleting) invalid bytes retains mutation-significant
523
/// positions while preventing accidental pseudo-headers and encoder failures.
524
65.9k
void CanonicalizeHttp3HeaderName(std::string* name) {
525
65.9k
  if (name->size() > scenario_limits::kMaxHttp3HeaderNameBytes) {
526
32
    name->resize(scenario_limits::kMaxHttp3HeaderNameBytes);
527
32
  }
528
65.9k
  if (name->empty()) {
529
420
    *name = "x-fuzz";
530
420
    return;
531
420
  }
532
533
572k
  for (char& byte : *name) {
534
572k
    const unsigned char value = static_cast<unsigned char>(byte);
535
572k
    const bool alpha = (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z');
536
572k
    const bool digit = value >= '0' && value <= '9';
537
572k
    const bool punctuation = value == '!' || value == '#' || value == '$' || value == '%' || value == '&' ||
538
561k
                             value == '\'' || value == '*' || value == '+' || value == '-' || value == '.' ||
539
432k
                             value == '^' || value == '_' || value == '`' || value == '|' || value == '~';
540
572k
    if (value >= 'A' && value <= 'Z') {
541
737
      byte = static_cast<char>(value - 'A' + 'a');
542
572k
    } else if (!alpha && !digit && !punctuation) {
543
2.61k
      byte = '-';
544
2.61k
    }
545
572k
  }
546
65.5k
}
547
548
/// Structured field values must not inject another HTTP field or carry NUL
549
/// into the encoder. Observable malformed bytes remain available in raw stream
550
/// actions, while this path stays suitable for valid QPACK generation.
551
65.9k
void CanonicalizeHttp3HeaderValue(std::string* value) {
552
65.9k
  if (value->size() > scenario_limits::kMaxHttp3HeaderValueBytes) {
553
0
    value->resize(scenario_limits::kMaxHttp3HeaderValueBytes);
554
0
  }
555
5.23M
  for (char& byte : *value) {
556
5.23M
    const unsigned char character = static_cast<unsigned char>(byte);
557
5.23M
    if ((character < 0x20U && character != '\t') || character == 0x7fU) {
558
2.18k
      byte = ' ';
559
2.18k
    }
560
5.23M
  }
561
65.9k
}
562
563
/// Retain only a prefix of encodable fields within both a count and a shared
564
/// byte budget. A missing name is materialized as x-fuzz while budget remains,
565
/// making default-initialized structured headers useful to the peer.
566
template <typename RepeatedHeaders>
567
63.3k
void BoundHttp3Headers(RepeatedHeaders* headers, std::size_t count_limit, std::size_t* remaining_bytes) {
568
63.3k
  TrimRepeated(headers, count_limit);
569
63.3k
  std::size_t retained = 0;
570
129k
  while (retained < static_cast<std::size_t>(headers->size()) && *remaining_bytes != 0) {
571
65.9k
    auto* header = headers->Mutable(static_cast<int>(retained));
572
65.9k
    CanonicalizeHttp3HeaderName(header->mutable_name());
573
65.9k
    CanonicalizeHttp3HeaderValue(header->mutable_value());
574
575
65.9k
    if (header->name().size() > *remaining_bytes) {
576
4
      header->mutable_name()->resize(*remaining_bytes);
577
4
      header->clear_value();
578
65.9k
    } else if (header->value().size() > *remaining_bytes - header->name().size()) {
579
6
      header->mutable_value()->resize(*remaining_bytes - header->name().size());
580
6
    }
581
65.9k
    *remaining_bytes -= header->name().size() + header->value().size();
582
65.9k
    ++retained;
583
65.9k
  }
584
63.3k
  TrimRepeated(headers, retained);
585
63.3k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundHttp3Headers<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http3Header> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http3Header>*, unsigned long, unsigned long*)
Line
Count
Source
567
3.59k
void BoundHttp3Headers(RepeatedHeaders* headers, std::size_t count_limit, std::size_t* remaining_bytes) {
568
3.59k
  TrimRepeated(headers, count_limit);
569
3.59k
  std::size_t retained = 0;
570
5.41k
  while (retained < static_cast<std::size_t>(headers->size()) && *remaining_bytes != 0) {
571
1.82k
    auto* header = headers->Mutable(static_cast<int>(retained));
572
1.82k
    CanonicalizeHttp3HeaderName(header->mutable_name());
573
1.82k
    CanonicalizeHttp3HeaderValue(header->mutable_value());
574
575
1.82k
    if (header->name().size() > *remaining_bytes) {
576
2
      header->mutable_name()->resize(*remaining_bytes);
577
2
      header->clear_value();
578
1.82k
    } else if (header->value().size() > *remaining_bytes - header->name().size()) {
579
3
      header->mutable_value()->resize(*remaining_bytes - header->name().size());
580
3
    }
581
1.82k
    *remaining_bytes -= header->name().size() + header->value().size();
582
1.82k
    ++retained;
583
1.82k
  }
584
3.59k
  TrimRepeated(headers, retained);
585
3.59k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundHttp3Headers<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http2Header> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Http2Header>*, unsigned long, unsigned long*)
Line
Count
Source
567
59.7k
void BoundHttp3Headers(RepeatedHeaders* headers, std::size_t count_limit, std::size_t* remaining_bytes) {
568
59.7k
  TrimRepeated(headers, count_limit);
569
59.7k
  std::size_t retained = 0;
570
123k
  while (retained < static_cast<std::size_t>(headers->size()) && *remaining_bytes != 0) {
571
64.1k
    auto* header = headers->Mutable(static_cast<int>(retained));
572
64.1k
    CanonicalizeHttp3HeaderName(header->mutable_name());
573
64.1k
    CanonicalizeHttp3HeaderValue(header->mutable_value());
574
575
64.1k
    if (header->name().size() > *remaining_bytes) {
576
2
      header->mutable_name()->resize(*remaining_bytes);
577
2
      header->clear_value();
578
64.1k
    } else if (header->value().size() > *remaining_bytes - header->name().size()) {
579
3
      header->mutable_value()->resize(*remaining_bytes - header->name().size());
580
3
    }
581
64.1k
    *remaining_bytes -= header->name().size() + header->value().size();
582
64.1k
    ++retained;
583
64.1k
  }
584
59.7k
  TrimRepeated(headers, retained);
585
59.7k
}
586
587
1.41k
void CanonicalizeHttp3StreamRole(curl::fuzzer::proto::Http3StreamRole* role) {
588
1.41k
  switch (*role) {
589
1.29k
    case curl::fuzzer::proto::HTTP3_STREAM_RESPONSE:
590
1.30k
    case curl::fuzzer::proto::HTTP3_STREAM_CONTROL:
591
1.32k
    case curl::fuzzer::proto::HTTP3_STREAM_QPACK_ENCODER:
592
1.32k
    case curl::fuzzer::proto::HTTP3_STREAM_QPACK_DECODER:
593
1.32k
      return;
594
86
    default:
595
86
      *role = curl::fuzzer::proto::HTTP3_STREAM_RESPONSE;
596
86
      return;
597
1.41k
  }
598
1.41k
}
599
600
1.51k
void BoundHttp3RawData(std::string* data, std::size_t* remaining_raw_bytes) {
601
1.51k
  const std::size_t limit = std::min(scenario_limits::kMaxHttp3RawWriteBytes, *remaining_raw_bytes);
602
1.51k
  if (data->size() > limit) {
603
7
    data->resize(limit);
604
7
  }
605
1.51k
  *remaining_raw_bytes -= data->size();
606
1.51k
}
607
608
87.5k
void BoundHttp2StreamRef(curl::fuzzer::proto::Http2StreamRef* stream) {
609
87.5k
  switch (stream->target_case()) {
610
2.59k
    case curl::fuzzer::proto::Http2StreamRef::kRequestIndex:
611
2.59k
      stream->set_request_index(stream->request_index() % 16U);
612
2.59k
      break;
613
2.32k
    case curl::fuzzer::proto::Http2StreamRef::kPushIndex:
614
2.32k
      stream->set_push_index(stream->push_index() % 16U);
615
2.32k
      break;
616
2.54k
    case curl::fuzzer::proto::Http2StreamRef::kExplicitId:
617
2.54k
      stream->set_explicit_id(stream->explicit_id() & 0x7fffffffU);
618
2.54k
      break;
619
1.84k
    case curl::fuzzer::proto::Http2StreamRef::kConnection:
620
2.68k
    case curl::fuzzer::proto::Http2StreamRef::kLatestRequest:
621
80.0k
    case curl::fuzzer::proto::Http2StreamRef::TARGET_NOT_SET:
622
80.0k
    default:
623
80.0k
      break;
624
87.5k
  }
625
87.5k
}
626
627
28.0k
void BoundHttp2Settings(curl::fuzzer::proto::Http2Settings* settings) {
628
28.0k
  TrimRepeated(settings->mutable_entries(), scenario_limits::kMaxHttp2Settings);
629
28.0k
  for (auto& entry : *settings->mutable_entries()) {
630
    // SETTINGS identifiers occupy 16 bits. Preserve unknown extensions,
631
    // which receivers ignore when they do not understand them.
632
14.1k
    entry.set_identifier(entry.identifier() & 0xffffU);
633
14.1k
    switch (entry.identifier()) {
634
1.52k
      case 2U:  // SETTINGS_ENABLE_PUSH
635
1.81k
      case 8U:  // SETTINGS_ENABLE_CONNECT_PROTOCOL
636
2.01k
      case 9U:  // SETTINGS_NO_RFC7540_PRIORITIES
637
2.01k
        entry.set_value(entry.value() & 1U);
638
2.01k
        break;
639
3.15k
      case 4U:  // SETTINGS_INITIAL_WINDOW_SIZE
640
3.15k
        entry.set_value(entry.value() & 0x7fffffffU);
641
3.15k
        break;
642
899
      case 5U:  // SETTINGS_MAX_FRAME_SIZE
643
899
        if (entry.value() < 16384U || entry.value() > 0x00ffffffU) {
644
4
          entry.set_value(16384U + entry.value() % (0x00ffffffU - 16384U + 1U));
645
4
        }
646
899
        break;
647
2.53k
      case 1U:  // SETTINGS_HEADER_TABLE_SIZE
648
4.20k
      case 3U:  // SETTINGS_MAX_CONCURRENT_STREAMS
649
5.11k
      case 6U:  // SETTINGS_MAX_HEADER_LIST_SIZE
650
8.06k
      default:
651
8.06k
        break;
652
14.1k
    }
653
14.1k
  }
654
28.0k
}
655
656
13.7k
void BoundHttp2RawData(std::string* data, std::size_t* remaining_raw_bytes) {
657
13.7k
  const std::size_t limit = std::min(scenario_limits::kMaxHttp2RawFrameBytes, *remaining_raw_bytes);
658
13.7k
  if (data->size() > limit) {
659
3
    data->resize(limit);
660
3
  }
661
13.7k
  *remaining_raw_bytes -= data->size();
662
13.7k
}
663
664
/// Bound the structured HTTP/2 sequence to the exact prefix interpreted by
665
/// H2PlanDriver. The raw escapes retain malformed frame behavior while every
666
/// ordinary header action remains HPACK and HTTP-field valid.
667
19.8k
void BoundHttp2PlanShape(curl::fuzzer::proto::Http2Plan* plan) {
668
19.8k
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxHttp2Actions);
669
19.8k
  BoundHttp2Settings(plan->mutable_initial_settings());
670
19.8k
  plan->mutable_initial_settings()->set_ack(false);
671
19.8k
  if (plan->actions().empty()) {
672
9
    auto* response = plan->add_actions()->mutable_headers();
673
9
    response->set_status_code(200);
674
9
    response->set_end_stream(true);
675
9
  }
676
677
19.8k
  std::size_t remaining_header_bytes = scenario_limits::kMaxHttp2HeaderBytes;
678
19.8k
  std::size_t remaining_data_bytes = scenario_limits::kMaxHttp2DataBytes;
679
19.8k
  std::size_t remaining_raw_bytes = scenario_limits::kMaxHttp2RawBytes;
680
116k
  for (auto& action : *plan->mutable_actions()) {
681
116k
    switch (action.action_case()) {
682
8.19k
      case curl::fuzzer::proto::Http2Action::kSettings:
683
8.19k
        BoundHttp2Settings(action.mutable_settings());
684
8.19k
        break;
685
49.3k
      case curl::fuzzer::proto::Http2Action::kHeaders: {
686
49.3k
        auto* headers = action.mutable_headers();
687
49.3k
        BoundHttp2StreamRef(headers->mutable_stream());
688
49.3k
        if (!headers->trailers()) {
689
48.5k
          if (headers->status_code() == 0U) {
690
24
            headers->set_status_code(200U);
691
48.5k
          } else if (headers->status_code() < 100U || headers->status_code() > 599U) {
692
97
            headers->set_status_code(100U + headers->status_code() % 500U);
693
97
          }
694
48.5k
        }
695
49.3k
        BoundHttp3Headers(headers->mutable_fields(), scenario_limits::kMaxHttp2Headers, &remaining_header_bytes);
696
49.3k
        break;
697
0
      }
698
2.94k
      case curl::fuzzer::proto::Http2Action::kData: {
699
2.94k
        auto* data = action.mutable_data();
700
2.94k
        BoundHttp2StreamRef(data->mutable_stream());
701
2.94k
        if (data->data().size() > remaining_data_bytes) {
702
0
          data->mutable_data()->resize(remaining_data_bytes);
703
0
        }
704
2.94k
        remaining_data_bytes -= data->data().size();
705
2.94k
        break;
706
0
      }
707
3.49k
      case curl::fuzzer::proto::Http2Action::kWindowUpdate: {
708
3.49k
        auto* update = action.mutable_window_update();
709
3.49k
        BoundHttp2StreamRef(update->mutable_stream());
710
3.49k
        update->set_increment(std::max(1U, update->increment() & 0x7fffffffU));
711
3.49k
        break;
712
0
      }
713
3.95k
      case curl::fuzzer::proto::Http2Action::kRstStream:
714
3.95k
        BoundHttp2StreamRef(action.mutable_rst_stream()->mutable_stream());
715
3.95k
        break;
716
7.07k
      case curl::fuzzer::proto::Http2Action::kPing:
717
7.07k
        if (action.ping().opaque_data().size() > 8U) {
718
34
          action.mutable_ping()->mutable_opaque_data()->resize(8U);
719
34
        }
720
7.07k
        break;
721
3.97k
      case curl::fuzzer::proto::Http2Action::kGoaway:
722
3.97k
        BoundHttp2StreamRef(action.mutable_goaway()->mutable_last_stream());
723
3.97k
        BoundHttp2RawData(action.mutable_goaway()->mutable_debug_data(), &remaining_raw_bytes);
724
3.97k
        break;
725
10.4k
      case curl::fuzzer::proto::Http2Action::kPushPromise:
726
10.4k
        BoundHttp2StreamRef(action.mutable_push_promise()->mutable_parent_stream());
727
10.4k
        BoundHttp2StreamRef(action.mutable_push_promise()->mutable_promised_stream());
728
10.4k
        BoundHttp3Headers(action.mutable_push_promise()->mutable_request_headers(), scenario_limits::kMaxHttp2Headers,
729
10.4k
                          &remaining_header_bytes);
730
10.4k
        break;
731
2.91k
      case curl::fuzzer::proto::Http2Action::kWait: {
732
2.91k
        auto* wait = action.mutable_wait();
733
2.91k
        BoundHttp2StreamRef(wait->mutable_stream());
734
2.91k
        if (wait->event() < curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS ||
735
2.90k
            wait->event() > curl::fuzzer::proto::HTTP2_CLIENT_EVENT_END_STREAM) {
736
82
          wait->set_event(curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS);
737
82
        }
738
2.91k
        wait->set_count(std::max(1U, std::min(wait->count(), 16U)));
739
2.91k
        break;
740
0
      }
741
13.4k
      case curl::fuzzer::proto::Http2Action::kYieldTurns:
742
13.4k
        action.set_yield_turns(std::min<std::uint32_t>(action.yield_turns(), scenario_limits::kMaxHttp2YieldTurns));
743
13.4k
        break;
744
7.21k
      case curl::fuzzer::proto::Http2Action::kWireFrame:
745
7.21k
        action.mutable_wire_frame()->set_type(action.wire_frame().type() & 0xffU);
746
7.21k
        action.mutable_wire_frame()->set_flags(action.wire_frame().flags() & 0xffU);
747
7.21k
        action.mutable_wire_frame()->set_stream_id(action.wire_frame().stream_id() & 0x7fffffffU);
748
7.21k
        action.mutable_wire_frame()->set_declared_length(action.wire_frame().declared_length() & 0x00ffffffU);
749
7.21k
        BoundHttp2RawData(action.mutable_wire_frame()->mutable_payload(), &remaining_raw_bytes);
750
7.21k
        break;
751
2.57k
      case curl::fuzzer::proto::Http2Action::kRawBytes:
752
2.57k
        BoundHttp2RawData(action.mutable_raw_bytes(), &remaining_raw_bytes);
753
2.57k
        break;
754
467
      case curl::fuzzer::proto::Http2Action::ACTION_NOT_SET: {
755
467
        auto* response = action.mutable_headers();
756
467
        response->set_status_code(200);
757
467
        response->set_end_stream(true);
758
467
        break;
759
0
      }
760
116k
    }
761
116k
  }
762
19.8k
}
763
764
/// Canonicalize one ordered H3 script to the exact bounded prefix that the
765
/// QUIC peer can execute. Transport setup remains peer-owned; only plaintext
766
/// HTTP/3 operations are mutation-controlled here.
767
10.3k
void BoundHttp3PlanShape(curl::fuzzer::proto::Http3Plan* plan) {
768
10.3k
  if (plan->use_h1_connect_udp_proxy()) {
769
5.69k
    plan->clear_actions();
770
5.69k
    return;
771
5.69k
  }
772
4.67k
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxHttp3Actions);
773
4.67k
  if (plan->actions().empty()) {
774
817
    auto* response = plan->add_actions()->mutable_structured_response();
775
817
    response->set_status_code(200);
776
817
    response->set_finish_stream(true);
777
817
  }
778
779
4.67k
  std::size_t remaining_header_bytes = scenario_limits::kMaxHttp3HeaderBytes;
780
4.67k
  std::size_t remaining_body_bytes = scenario_limits::kMaxHttp3BodyBytes;
781
4.67k
  std::size_t remaining_raw_bytes = scenario_limits::kMaxHttp3RawBytes;
782
4.67k
  std::size_t retained_actions = 0;
783
4.67k
  bool connection_closed = false;
784
6.52k
  for (auto& action : *plan->mutable_actions()) {
785
6.52k
    if (connection_closed) {
786
39
      break;
787
39
    }
788
6.48k
    ++retained_actions;
789
6.48k
    switch (action.action_case()) {
790
1.79k
      case curl::fuzzer::proto::Http3Action::kStructuredResponse: {
791
1.79k
        auto* response = action.mutable_structured_response();
792
1.79k
        if (response->status_code() == 0U) {
793
127
          response->set_status_code(200U);
794
1.66k
        } else if (response->status_code() < 100U || response->status_code() > 599U) {
795
105
          response->set_status_code(100U + response->status_code() % 500U);
796
105
        }
797
1.79k
        BoundHttp3Headers(response->mutable_response_headers(), scenario_limits::kMaxHttp3Headers,
798
1.79k
                          &remaining_header_bytes);
799
1.79k
        BoundHttp3Headers(response->mutable_response_trailers(), scenario_limits::kMaxHttp3Trailers,
800
1.79k
                          &remaining_header_bytes);
801
1.79k
        TrimRepeated(response->mutable_body_chunks(), scenario_limits::kMaxHttp3BodyChunks);
802
1.79k
        for (std::string& chunk : *response->mutable_body_chunks()) {
803
1.25k
          if (chunk.size() > remaining_body_bytes) {
804
20
            chunk.resize(remaining_body_bytes);
805
20
          }
806
1.25k
          remaining_body_bytes -= chunk.size();
807
1.25k
        }
808
1.79k
        break;
809
0
      }
810
811
725
      case curl::fuzzer::proto::Http3Action::kStreamWrite: {
812
725
        auto* write = action.mutable_stream_write();
813
725
        auto role = write->role();
814
725
        CanonicalizeHttp3StreamRole(&role);
815
725
        write->set_role(role);
816
725
        BoundHttp3RawData(write->mutable_data(), &remaining_raw_bytes);
817
725
        break;
818
0
      }
819
820
791
      case curl::fuzzer::proto::Http3Action::kOpenUnidirectionalStream: {
821
791
        auto* stream = action.mutable_open_unidirectional_stream();
822
791
        BoundHttp3RawData(stream->mutable_data(), &remaining_raw_bytes);
823
791
        break;
824
0
      }
825
826
688
      case curl::fuzzer::proto::Http3Action::kStreamReset: {
827
688
        auto* reset = action.mutable_stream_reset();
828
688
        auto role = reset->role();
829
688
        CanonicalizeHttp3StreamRole(&role);
830
688
        reset->set_role(role);
831
688
        reset->set_application_error_code(reset->application_error_code() & scenario_limits::kMaxQuicVarint);
832
688
        break;
833
0
      }
834
835
919
      case curl::fuzzer::proto::Http3Action::kGoaway:
836
919
        action.mutable_goaway()->set_id(action.goaway().id() & scenario_limits::kMaxQuicVarint & ~std::uint64_t{3});
837
919
        break;
838
839
1.27k
      case curl::fuzzer::proto::Http3Action::kConnectionClose:
840
1.27k
        action.mutable_connection_close()->set_application_error_code(
841
1.27k
            action.connection_close().application_error_code() & scenario_limits::kMaxQuicVarint);
842
1.27k
        connection_closed = true;
843
1.27k
        break;
844
845
294
      case curl::fuzzer::proto::Http3Action::ACTION_NOT_SET: {
846
294
        auto* response = action.mutable_structured_response();
847
294
        response->set_status_code(200);
848
294
        response->set_finish_stream(true);
849
294
        break;
850
0
      }
851
6.48k
    }
852
6.48k
  }
853
4.67k
  TrimRepeated(plan->mutable_actions(), retained_actions);
854
4.67k
}
855
856
/// Canonicalize all shape limits enforced by the runtime. This runs only in
857
/// fixed policy targets; the compatibility binary deliberately retains its
858
/// historical no-postprocessor semantics for existing OSS-Fuzz reproducers.
859
322k
void BoundScenarioShape(curl::fuzzer::proto::Scenario* scenario) {
860
322k
  TrimRepeated(scenario->mutable_options(), scenario_limits::kMaxOptions);
861
862
322k
  BoundHeaderValues(scenario->mutable_request_headers(), scenario_limits::kMaxRequestHeaders);
863
322k
  BoundStringValues(scenario->mutable_telnet_options(), scenario_limits::kMaxTelnetOptions,
864
322k
                    scenario_limits::kMaxTelnetOptionBytes);
865
322k
  if (scenario->has_mime_post()) {
866
36.8k
    BoundMimeShape(scenario->mutable_mime_post());
867
36.8k
  }
868
322k
  if (scenario->has_upload()) {
869
13.9k
    const bool telnet = scenario->scheme() == curl::fuzzer::proto::SCHEME_TELNET;
870
13.9k
    const std::size_t data_limit = telnet ? scenario_limits::kMaxTelnetUploadBytes : scenario_limits::kMaxUploadBytes;
871
13.9k
    const std::size_t read_step_limit =
872
13.9k
        telnet ? scenario_limits::kMaxTelnetUploadReadSteps : scenario_limits::kMaxUploadReadSteps;
873
13.9k
    const std::size_t read_size_limit =
874
13.9k
        telnet ? scenario_limits::kMaxTelnetUploadReadSize : scenario_limits::kMaxUploadReadSize;
875
13.9k
    BoundUploadShape(scenario->mutable_upload(), data_limit, read_step_limit, read_size_limit);
876
13.9k
  }
877
878
322k
  BoundConnectionShape(scenario->mutable_connection());
879
322k
  TrimRepeated(scenario->mutable_subsequent_connections(), scenario_limits::kMaxConnections - 1);
880
322k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
881
77.2k
    BoundConnectionShape(&connection);
882
77.2k
  }
883
322k
  BoundFileInputs(scenario);
884
322k
}
885
886
/// Return whether an option belongs in the high-throughput HTTP lane. This is
887
/// deliberately an allowlist rather than a denylist: adding a new structured
888
/// option should expand deep coverage first, not silently make the fast lane
889
/// slower before its cost has been measured.
890
85.2k
bool IsCheapHttpOption(curl::fuzzer::proto::CurlOptionId option_id) {
891
85.2k
  switch (option_id) {
892
11.3k
    case curl::fuzzer::proto::CURLOPT_ACCEPT_ENCODING:
893
14.5k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
894
16.2k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
895
18.2k
    case curl::fuzzer::proto::CURLOPT_DISALLOW_USERNAME_IN_URL:
896
19.9k
    case curl::fuzzer::proto::CURLOPT_FAILONERROR:
897
22.3k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
898
23.8k
    case curl::fuzzer::proto::CURLOPT_HEADER:
899
25.1k
    case curl::fuzzer::proto::CURLOPT_HTTP09_ALLOWED:
900
26.1k
    case curl::fuzzer::proto::CURLOPT_HTTP_CONTENT_DECODING:
901
27.0k
    case curl::fuzzer::proto::CURLOPT_HTTP_TRANSFER_DECODING:
902
55.0k
    case curl::fuzzer::proto::CURLOPT_HTTP_VERSION:
903
62.0k
    case curl::fuzzer::proto::CURLOPT_HTTPGET:
904
63.1k
    case curl::fuzzer::proto::CURLOPT_IGNORE_CONTENT_LENGTH:
905
65.5k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
906
67.4k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
907
70.6k
    case curl::fuzzer::proto::CURLOPT_PATH_AS_IS:
908
72.1k
    case curl::fuzzer::proto::CURLOPT_RANGE:
909
74.9k
    case curl::fuzzer::proto::CURLOPT_REQUEST_TARGET:
910
77.3k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
911
81.6k
    case curl::fuzzer::proto::CURLOPT_TRANSFER_ENCODING:
912
82.4k
    case curl::fuzzer::proto::CURLOPT_USERAGENT:
913
82.4k
      return true;
914
915
1.52k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
916
2.73k
    default:
917
2.73k
      return false;
918
85.2k
  }
919
85.2k
}
920
921
/// Return whether an option can affect the HTTP/1.1 request carried inside the
922
/// fixed HTTP/2 CONNECT tunnel. Proxy routing, ALPN, and TLS verification are
923
/// owned by H2ProxyMockServer and must not be mutation-controlled; HTTP/2 as an
924
/// inner origin protocol is also excluded because it would require a second
925
/// frame script and obscure coverage of the outer proxy filter. Stateful HTTP
926
/// options remain useful here because their wire effects traverse cf-h2-proxy.
927
181k
bool IsH2ProxyOriginOption(curl::fuzzer::proto::CurlOptionId option_id) {
928
181k
  switch (option_id) {
929
2.80k
    case curl::fuzzer::proto::CURLOPT_ACCEPT_ENCODING:
930
5.40k
    case curl::fuzzer::proto::CURLOPT_ALTSVC_CTRL:
931
7.66k
    case curl::fuzzer::proto::CURLOPT_AUTOREFERER:
932
13.2k
    case curl::fuzzer::proto::CURLOPT_AWS_SIGV4:
933
16.6k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
934
18.7k
    case curl::fuzzer::proto::CURLOPT_COOKIE:
935
50.4k
    case curl::fuzzer::proto::CURLOPT_COOKIELIST:
936
52.2k
    case curl::fuzzer::proto::CURLOPT_COOKIESESSION:
937
54.0k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
938
55.9k
    case curl::fuzzer::proto::CURLOPT_DISALLOW_USERNAME_IN_URL:
939
59.0k
    case curl::fuzzer::proto::CURLOPT_EXPECT_100_TIMEOUT_MS:
940
61.3k
    case curl::fuzzer::proto::CURLOPT_FAILONERROR:
941
62.9k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
942
75.3k
    case curl::fuzzer::proto::CURLOPT_FOLLOWLOCATION:
943
77.0k
    case curl::fuzzer::proto::CURLOPT_FORBID_REUSE:
944
78.9k
    case curl::fuzzer::proto::CURLOPT_FRESH_CONNECT:
945
80.9k
    case curl::fuzzer::proto::CURLOPT_HEADER:
946
83.4k
    case curl::fuzzer::proto::CURLOPT_HSTS_CTRL:
947
84.7k
    case curl::fuzzer::proto::CURLOPT_HTTP09_ALLOWED:
948
89.9k
    case curl::fuzzer::proto::CURLOPT_HTTPAUTH:
949
91.8k
    case curl::fuzzer::proto::CURLOPT_HTTPGET:
950
95.7k
    case curl::fuzzer::proto::CURLOPT_HTTP_CONTENT_DECODING:
951
96.9k
    case curl::fuzzer::proto::CURLOPT_HTTP_TRANSFER_DECODING:
952
98.0k
    case curl::fuzzer::proto::CURLOPT_IGNORE_CONTENT_LENGTH:
953
100k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
954
101k
    case curl::fuzzer::proto::CURLOPT_KEEP_SENDING_ON_ERROR:
955
104k
    case curl::fuzzer::proto::CURLOPT_MAXAGE_CONN:
956
107k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
957
110k
    case curl::fuzzer::proto::CURLOPT_MAXLIFETIME_CONN:
958
113k
    case curl::fuzzer::proto::CURLOPT_MAXREDIRS:
959
115k
    case curl::fuzzer::proto::CURLOPT_MIME_OPTIONS:
960
117k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
961
123k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
962
125k
    case curl::fuzzer::proto::CURLOPT_PATH_AS_IS:
963
128k
    case curl::fuzzer::proto::CURLOPT_POST:
964
132k
    case curl::fuzzer::proto::CURLOPT_POSTFIELDS:
965
134k
    case curl::fuzzer::proto::CURLOPT_POSTREDIR:
966
136k
    case curl::fuzzer::proto::CURLOPT_RANGE:
967
138k
    case curl::fuzzer::proto::CURLOPT_REFERER:
968
142k
    case curl::fuzzer::proto::CURLOPT_REQUEST_TARGET:
969
145k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
970
150k
    case curl::fuzzer::proto::CURLOPT_TIMECONDITION:
971
151k
    case curl::fuzzer::proto::CURLOPT_TIMEVALUE_LARGE:
972
152k
    case curl::fuzzer::proto::CURLOPT_TRANSFER_ENCODING:
973
154k
    case curl::fuzzer::proto::CURLOPT_UNRESTRICTED_AUTH:
974
157k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
975
161k
    case curl::fuzzer::proto::CURLOPT_UPLOAD_BUFFERSIZE:
976
163k
    case curl::fuzzer::proto::CURLOPT_USERAGENT:
977
165k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
978
174k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
979
176k
    case curl::fuzzer::proto::CURLOPT_XOAUTH2_BEARER:
980
176k
      return true;
981
982
4.31k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
983
5.17k
    default:
984
5.17k
      return false;
985
181k
  }
986
181k
}
987
988
/// Compact an option list before applying the general option-count bound.
989
/// Keeping a relevant option that appears after a long rejected prefix is
990
/// important for mutation density: bounding first would let unrelated options
991
/// crowd useful ones out of a protocol-specific lane.
992
template <typename Predicate>
993
322k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
994
322k
  auto* options = scenario->mutable_options();
995
322k
  int retained = 0;
996
1.45M
  for (int index = 0; index < options->size(); ++index) {
997
1.13M
    if (!predicate(options->Get(index).option_id())) {
998
128k
      continue;
999
128k
    }
1000
1.00M
    if (retained != index) {
1001
84.5k
      options->SwapElements(retained, index);
1002
84.5k
    }
1003
1.00M
    ++retained;
1004
1.00M
  }
1005
322k
  options->DeleteSubrange(retained, options->size() - retained);
1006
322k
}
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
993
126k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
994
126k
  auto* options = scenario->mutable_options();
995
126k
  int retained = 0;
996
566k
  for (int index = 0; index < options->size(); ++index) {
997
439k
    if (!predicate(options->Get(index).option_id())) {
998
104k
      continue;
999
104k
    }
1000
335k
    if (retained != index) {
1001
51.3k
      options->SwapElements(retained, index);
1002
51.3k
    }
1003
335k
    ++retained;
1004
335k
  }
1005
126k
  options->DeleteSubrange(retained, options->size() - retained);
1006
126k
}
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
993
195k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
994
195k
  auto* options = scenario->mutable_options();
995
195k
  int retained = 0;
996
892k
  for (int index = 0; index < options->size(); ++index) {
997
697k
    if (!predicate(options->Get(index).option_id())) {
998
23.7k
      continue;
999
23.7k
    }
1000
673k
    if (retained != index) {
1001
33.2k
      options->SwapElements(retained, index);
1002
33.2k
    }
1003
673k
    ++retained;
1004
673k
  }
1005
195k
  options->DeleteSubrange(retained, options->size() - retained);
1006
195k
}
1007
1008
/// Keep the high-throughput HTTP lane free of options whose setup or state is
1009
/// assigned to a deeper or protocol-specific target.
1010
51.1k
void RetainCheapHttpOptions(curl::fuzzer::proto::Scenario* scenario) {
1011
51.1k
  RetainMatchingOptions(scenario, &IsCheapHttpOption);
1012
51.1k
}
1013
1014
/// Compact the option list before its shared cap so irrelevant TLS, WebSocket,
1015
/// and file-transfer entries cannot crowd out origin traffic mutations.
1016
49.2k
void RetainH2ProxyOriginOptions(curl::fuzzer::proto::Scenario* scenario) {
1017
49.2k
  RetainMatchingOptions(scenario, &IsH2ProxyOriginOption);
1018
49.2k
}
1019
1020
/// HTTP/3 owns QUIC selection, ALPN, routing, and certificate verification in
1021
/// its peer. The tunneled-origin allowlist is deliberately the same set of
1022
/// request-level HTTP controls, and notably excludes CURLOPT_HTTP_VERSION and
1023
/// CURLOPT_CONNECT_ONLY, which could bypass the dedicated transport.
1024
10.3k
void RetainHttp3RequestOptions(curl::fuzzer::proto::Scenario* scenario) {
1025
10.3k
  RetainMatchingOptions(scenario, &IsH2ProxyOriginOption);
1026
10.3k
}
1027
1028
/// Return whether a scalar option can influence TELNET without selecting an
1029
/// incompatible transfer mode or introducing external state. Protocol-
1030
/// specific negotiation preferences use Scenario.telnet_options instead.
1031
29.5k
bool IsCheapTelnetOption(curl::fuzzer::proto::CurlOptionId option_id) {
1032
29.5k
  switch (option_id) {
1033
10.3k
    case curl::fuzzer::proto::CURLOPT_CRLF:
1034
14.9k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
1035
15.3k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
1036
18.4k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
1037
19.2k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
1038
19.2k
      return true;
1039
1040
6.19k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
1041
10.2k
    default:
1042
10.2k
      return false;
1043
29.5k
  }
1044
29.5k
}
1045
1046
/// Compact the TELNET option prefix so unrelated HTTP mutations cannot crowd
1047
/// useful credentials, CRLF handling, and transfer-size controls out of the
1048
/// fixed target's general option budget.
1049
4.64k
void RetainCheapTelnetOptions(curl::fuzzer::proto::Scenario* scenario) {
1050
4.64k
  RetainMatchingOptions(scenario, &IsCheapTelnetOption);
1051
4.64k
}
1052
1053
/// Return whether an option can change a plaintext FTP transfer serviced by
1054
/// the bounded control/data peer. Active mode is confined to the mock's
1055
/// loopback listener; FTPS settings remain omitted until that peer speaks TLS.
1056
35.3k
bool IsFtpOption(curl::fuzzer::proto::CurlOptionId option_id) {
1057
35.3k
  switch (option_id) {
1058
379
    case curl::fuzzer::proto::CURLOPT_APPEND:
1059
1.31k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
1060
1.89k
    case curl::fuzzer::proto::CURLOPT_CRLF:
1061
2.53k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
1062
3.00k
    case curl::fuzzer::proto::CURLOPT_DIRLISTONLY:
1063
3.51k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
1064
4.39k
    case curl::fuzzer::proto::CURLOPT_FTP_ACCOUNT:
1065
5.48k
    case curl::fuzzer::proto::CURLOPT_FTP_ALTERNATIVE_TO_USER:
1066
6.54k
    case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
1067
7.67k
    case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
1068
8.33k
    case curl::fuzzer::proto::CURLOPT_FTP_SKIP_PASV_IP:
1069
8.64k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPSV:
1070
8.98k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_PRET:
1071
10.0k
    case curl::fuzzer::proto::CURLOPT_FTPPORT:
1072
15.6k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPRT:
1073
16.6k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
1074
17.5k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
1075
18.2k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
1076
19.0k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
1077
19.7k
    case curl::fuzzer::proto::CURLOPT_RANGE:
1078
20.9k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
1079
23.1k
    case curl::fuzzer::proto::CURLOPT_TIMECONDITION:
1080
23.6k
    case curl::fuzzer::proto::CURLOPT_TIMEVALUE_LARGE:
1081
24.1k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
1082
27.9k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
1083
28.7k
    case curl::fuzzer::proto::CURLOPT_UPLOAD_BUFFERSIZE:
1084
29.0k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
1085
31.8k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
1086
33.7k
    case curl::fuzzer::proto::CURLOPT_WILDCARDMATCH:
1087
33.7k
      return true;
1088
1089
1.21k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
1090
1.59k
    default:
1091
1.59k
      return false;
1092
35.3k
  }
1093
35.3k
}
1094
1095
/// Keep the FTP target's general option budget focused on states its passive
1096
/// peer can actually advance.
1097
8.90k
void RetainFtpOptions(curl::fuzzer::proto::Scenario* scenario) { RetainMatchingOptions(scenario, &IsFtpOption); }
1098
1099
/// Return whether an option affects TFTP request construction, option
1100
/// negotiation, transfer direction, or bounded body delivery. TFTP has no
1101
/// connection reuse or stream-level controls, so retaining those settings
1102
/// would add protobuf work without another state-machine edge in curl.
1103
108k
bool IsTftpOption(curl::fuzzer::proto::CurlOptionId option_id) {
1104
108k
  switch (option_id) {
1105
2.04k
    case curl::fuzzer::proto::CURLOPT_CRLF:
1106
3.31k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
1107
3.78k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
1108
6.10k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
1109
20.6k
    case curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE:
1110
21.9k
    case curl::fuzzer::proto::CURLOPT_TFTP_NO_OPTIONS:
1111
22.1k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
1112
23.3k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
1113
23.3k
      return true;
1114
1115
84.1k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
1116
84.7k
    default:
1117
84.7k
      return false;
1118
108k
  }
1119
108k
}
1120
1121
/// Keep the datagram lane from spending mutations on stream-only options.
1122
2.50k
void RetainTftpOptions(curl::fuzzer::proto::Scenario* scenario) { RetainMatchingOptions(scenario, &IsTftpOption); }
1123
1124
/// Decode an integral oneof locally before the generated option canonicalizer
1125
/// runs. Protocol mode bounds are policy, not setopt mechanics: folding them
1126
/// here keeps nearly every mutation on a real FTP/TFTP state while the shared
1127
/// option layer remains unaware of protocol-specific numeric ranges.
1128
30.1k
std::uint64_t IntegralMutationValue(const curl::fuzzer::proto::SetOption& option) {
1129
30.1k
  switch (option.value_case()) {
1130
26.9k
    case curl::fuzzer::proto::SetOption::kUintValue:
1131
26.9k
      return option.uint_value();
1132
1.11k
    case curl::fuzzer::proto::SetOption::kBoolValue:
1133
1.11k
      return option.bool_value() ? 1U : 0U;
1134
184
    case curl::fuzzer::proto::SetOption::kStringValue:
1135
2.09k
    case curl::fuzzer::proto::SetOption::VALUE_NOT_SET:
1136
2.09k
      return 0;
1137
30.1k
  }
1138
0
  return 0;
1139
30.1k
}
1140
1141
/// Keep prior-knowledge HTTP/2 and HTTP/3 out of the throughput-oriented HTTP
1142
/// lane. HTTP/1 modes and cleartext HTTP/2 upgrade remain here because h2c's
1143
/// HTTP/1 request and 101 transition are distinct from the dedicated target's
1144
/// prior-knowledge transport.
1145
19.0k
void CanonicalizeFastHttpVersion(curl::fuzzer::proto::Scenario* scenario) {
1146
33.9k
  for (auto& option : *scenario->mutable_options()) {
1147
33.9k
    if (option.option_id() != curl::fuzzer::proto::CURLOPT_HTTP_VERSION) {
1148
26.1k
      continue;
1149
26.1k
    }
1150
7.79k
    const std::uint64_t version = IntegralMutationValue(option);
1151
    // 2TLS remains HTTP/1.1 because this target fixes the scheme to HTTP.
1152
7.79k
    if (version <= CURL_HTTP_VERSION_2TLS) {
1153
6.22k
      option.set_uint_value(version);
1154
6.22k
    } else {
1155
1.57k
      option.set_uint_value(CURL_HTTP_VERSION_NONE);
1156
1.57k
    }
1157
7.79k
  }
1158
19.0k
}
1159
1160
/// Fold small FTP enums onto curl's documented domains so random uint64 values
1161
/// do not overwhelmingly stop at setopt validation before issuing a command.
1162
8.90k
void CanonicalizeFtpOptionModes(curl::fuzzer::proto::Scenario* scenario) {
1163
33.7k
  for (auto& option : *scenario->mutable_options()) {
1164
33.7k
    switch (option.option_id()) {
1165
1.05k
      case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
1166
1.05k
        option.set_uint_value(IntegralMutationValue(option) % 3U);
1167
1.05k
        break;
1168
1.12k
      case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
1169
1.12k
        option.set_uint_value(IntegralMutationValue(option) % 4U);
1170
1.12k
        break;
1171
1.08k
      case curl::fuzzer::proto::CURLOPT_FTPPORT:
1172
        // Never let a mutated active-mode address resolve or bind outside the
1173
        // process. Empty remains reachable only by omitting the option.
1174
1.08k
        option.set_string_value("127.0.0.1");
1175
1.08k
        break;
1176
5.62k
      case curl::fuzzer::proto::CURLOPT_FTP_USE_EPRT:
1177
5.62k
        option.set_bool_value(IntegralMutationValue(option) != 0U);
1178
5.62k
        break;
1179
24.8k
      default:
1180
24.8k
        break;
1181
33.7k
    }
1182
33.7k
  }
1183
8.90k
}
1184
1185
/// TFTP accepts block sizes from 8 through 65464. Mapping zero or a mismatched
1186
/// oneof to the default 512 preserves a common valid request, while saturating
1187
/// other values retains both lower/upper parser boundaries under mutation.
1188
2.50k
void CanonicalizeTftpOptionModes(curl::fuzzer::proto::Scenario* scenario) {
1189
23.3k
  for (auto& option : *scenario->mutable_options()) {
1190
23.3k
    if (option.option_id() != curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE) {
1191
8.79k
      continue;
1192
8.79k
    }
1193
14.5k
    std::uint64_t value = IntegralMutationValue(option);
1194
14.5k
    if (value == 0) {
1195
474
      value = 512;
1196
474
    }
1197
14.5k
    option.set_uint_value(std::max<std::uint64_t>(8, std::min<std::uint64_t>(value, 65464)));
1198
14.5k
  }
1199
2.50k
}
1200
1201
/// Identify options introduced for FTP/TFTP so existing fixed lanes do not
1202
/// silently inherit dead mutations when the shared generated manifest grows.
1203
/// Generic options retained by the FTP/TFTP allowlists are deliberately absent
1204
/// here because they remain useful to HTTP, WebSocket, API, or timing targets.
1205
697k
bool IsFileTransferOnlyOption(curl::fuzzer::proto::CurlOptionId option_id) {
1206
697k
  switch (option_id) {
1207
1.52k
    case curl::fuzzer::proto::CURLOPT_APPEND:
1208
4.62k
    case curl::fuzzer::proto::CURLOPT_DIRLISTONLY:
1209
6.09k
    case curl::fuzzer::proto::CURLOPT_FTP_ACCOUNT:
1210
6.95k
    case curl::fuzzer::proto::CURLOPT_FTP_ALTERNATIVE_TO_USER:
1211
7.87k
    case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
1212
8.74k
    case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
1213
10.5k
    case curl::fuzzer::proto::CURLOPT_FTP_SKIP_PASV_IP:
1214
13.4k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPSV:
1215
14.8k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_PRET:
1216
15.6k
    case curl::fuzzer::proto::CURLOPT_FTPPORT:
1217
17.2k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPRT:
1218
20.9k
    case curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE:
1219
21.5k
    case curl::fuzzer::proto::CURLOPT_TFTP_NO_OPTIONS:
1220
22.6k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
1221
23.7k
    case curl::fuzzer::proto::CURLOPT_WILDCARDMATCH:
1222
23.7k
      return true;
1223
1224
62.1k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
1225
673k
    default:
1226
673k
      return false;
1227
697k
  }
1228
697k
}
1229
1230
/// Remove FTP/TFTP-only options while preserving the relative order of every
1231
/// generic option an existing fixed target already consumed.
1232
195k
void RemoveFileTransferOnlyOptions(curl::fuzzer::proto::Scenario* scenario) {
1233
195k
  RetainMatchingOptions(
1234
697k
      scenario, [](curl::fuzzer::proto::CurlOptionId option_id) { return !IsFileTransferOnlyOption(option_id); });
1235
195k
}
1236
1237
/// Remove the stateful shapes assigned to the deep HTTP target. This happens
1238
/// before BoundScenarioShape so a fast iteration never walks or normalizes a
1239
/// MIME tree, upload script, or follow-on connection that it will discard.
1240
/// Raw response chunks and request headers stay intact because they reach the
1241
/// core HTTP parser cheaply and provide much of the legacy fuzzer's coverage.
1242
51.1k
void RemoveDeepHttpShape(curl::fuzzer::proto::Scenario* scenario) {
1243
51.1k
  scenario->clear_mime_post();
1244
51.1k
  scenario->clear_upload();
1245
51.1k
  scenario->clear_subsequent_connections();
1246
1247
51.1k
  auto* connection = scenario->mutable_connection();
1248
51.1k
  connection->clear_server_frames();
1249
51.1k
  connection->clear_manual_probes();
1250
51.1k
  connection->clear_backpressure();
1251
51.1k
}
1252
1253
/// Remove response forms the proxy peer cannot interpret. Raw chunks are the
1254
/// HTTP/2 frame stream; WebSocket frames would merely add a second unrelated
1255
/// binary grammar, and follow-on Connection messages cannot describe later
1256
/// streams multiplexed on the already-open proxy socket.
1257
49.2k
void RemoveIgnoredH2ProxyShape(curl::fuzzer::proto::Scenario* scenario) {
1258
49.2k
  scenario->clear_subsequent_connections();
1259
49.2k
  auto* connection = scenario->mutable_connection();
1260
49.2k
  connection->clear_server_frames();
1261
49.2k
  connection->clear_manual_probes();
1262
49.2k
  connection->clear_backpressure();
1263
49.2k
}
1264
1265
/// Remove fields the single-socket WebSocket driver cannot consume. MIME also
1266
/// changes the HTTP request away from a useful Upgrade handshake, so retaining
1267
/// either shape in fixed WS lanes gives LPM mutation work with no WS coverage
1268
/// payoff. The mixed compatibility target has no postprocessor and keeps its
1269
/// historical behavior.
1270
29.4k
void RemoveIgnoredWebSocketShape(curl::fuzzer::proto::Scenario* scenario) {
1271
29.4k
  scenario->clear_subsequent_connections();
1272
29.4k
  scenario->clear_mime_post();
1273
29.4k
}
1274
1275
/// Remove fields whose only effect in a TELNET lane would be protobuf work or
1276
/// unsafe socket timing. TELNET's curl driver owns the thread until the peer
1277
/// closes, so response backpressure and follow-on sockets cannot be serviced
1278
/// by the outer event loop. Raw response fragments and the bounded upload stay
1279
/// mutation-controlled because the dedicated mock can preload and drain them.
1280
4.64k
void RemoveNonTelnetShape(curl::fuzzer::proto::Scenario* scenario) {
1281
4.64k
  scenario->clear_subsequent_connections();
1282
4.64k
  scenario->clear_request_headers();
1283
4.64k
  scenario->clear_mime_post();
1284
1285
4.64k
  auto* connection = scenario->mutable_connection();
1286
4.64k
  connection->clear_server_frames();
1287
4.64k
  connection->clear_manual_probes();
1288
4.64k
  connection->clear_backpressure();
1289
4.64k
}
1290
1291
/// Remove the TELNET-only list and pause outcome from fixed event-driven
1292
/// targets. The compatibility target skips postprocessing, so the runtime
1293
/// repeats the pause-to-EOF guard before installing callbacks.
1294
317k
void RemoveTelnetOnlyShape(curl::fuzzer::proto::Scenario* scenario) {
1295
317k
  scenario->clear_telnet_options();
1296
317k
  if (scenario->has_upload() && scenario->upload().terminal() == curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE) {
1297
18
    scenario->mutable_upload()->set_terminal(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF);
1298
18
  }
1299
317k
}
1300
1301
/// Keep lifecycle work out of protocol-focused lanes. The API binary retains
1302
/// this message explicitly; compatibility inputs have no postprocessor so
1303
/// existing reproducers keep their historical serialized meaning.
1304
298k
void RemoveApiOnlyShape(curl::fuzzer::proto::Scenario* scenario) { scenario->clear_api_plan(); }
1305
1306
/// Keep concurrent multi-handle work out of every other fixed lane. The
1307
/// compatibility binary deliberately preserves newly-added unknown fields.
1308
299k
void RemoveMultiOnlyShape(curl::fuzzer::proto::Scenario* scenario) { scenario->clear_multi_plan(); }
1309
1310
/// The QUIC peer consumes Http3Plan rather than the stream-socket response
1311
/// script. Request headers, MIME, upload state, and HTTP options remain useful
1312
/// because curl serializes those onto its client-initiated request stream.
1313
10.3k
void RemoveIgnoredHttp3Shape(curl::fuzzer::proto::Scenario* scenario) {
1314
10.3k
  if (!scenario->http3_plan().use_h1_connect_udp_proxy()) {
1315
4.67k
    scenario->clear_connection();
1316
4.67k
  }
1317
10.3k
  scenario->clear_subsequent_connections();
1318
10.3k
  RemoveTelnetOnlyShape(scenario);
1319
10.3k
  RemoveApiOnlyShape(scenario);
1320
10.3k
  RemoveMultiOnlyShape(scenario);
1321
10.3k
}
1322
1323
/// Remove stream-driver controls that neither file-transfer peer interprets.
1324
/// FTP consumes raw byte chunks as control/data replies, while TFTP preserves
1325
/// them as individual datagrams; structured WebSocket frames, manual probes,
1326
/// and event-loop backpressure therefore cannot affect either curl protocol.
1327
26.5k
void RemoveUnusedFileTransferConnectionShape(curl::fuzzer::proto::Connection* connection) {
1328
26.5k
  connection->clear_server_frames();
1329
26.5k
  connection->clear_manual_probes();
1330
26.5k
  connection->clear_backpressure();
1331
26.5k
}
1332
1333
/// Retain only the reusable shapes consumed by the FTP peer: one raw control
1334
/// script, a bounded sequence of passive-data scripts, and optional upload
1335
/// input. HTTP, TELNET, WebSocket, and public-API fields would otherwise absorb
1336
/// mutations despite having no representation in an FTP exchange.
1337
8.90k
void RemoveIgnoredFtpShape(curl::fuzzer::proto::Scenario* scenario) {
1338
8.90k
  scenario->clear_request_headers();
1339
8.90k
  scenario->clear_mime_post();
1340
8.90k
  RemoveTelnetOnlyShape(scenario);
1341
8.90k
  RemoveApiOnlyShape(scenario);
1342
8.90k
  RemoveMultiOnlyShape(scenario);
1343
1344
8.90k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1345
8.90k
  TrimRepeated(scenario->mutable_subsequent_connections(), scenario_limits::kMaxConnections - 1);
1346
8.90k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
1347
4.91k
    RemoveUnusedFileTransferConnectionShape(&connection);
1348
4.91k
  }
1349
8.90k
}
1350
1351
/// Retain the primary raw response script because its entries are the ordered
1352
/// UDP datagrams seen by curl, plus optional upload input for WRQ. TFTP cannot
1353
/// consume follow-on stream connections or any higher-level protocol shape.
1354
2.50k
void RemoveIgnoredTftpShape(curl::fuzzer::proto::Scenario* scenario) {
1355
2.50k
  scenario->clear_subsequent_connections();
1356
2.50k
  scenario->clear_request_headers();
1357
2.50k
  scenario->clear_mime_post();
1358
2.50k
  RemoveTelnetOnlyShape(scenario);
1359
2.50k
  RemoveApiOnlyShape(scenario);
1360
2.50k
  RemoveMultiOnlyShape(scenario);
1361
2.50k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1362
2.50k
}
1363
1364
10.1k
void RemoveIgnoredGopherShape(curl::fuzzer::proto::Scenario* scenario) {
1365
10.1k
  scenario->clear_subsequent_connections();
1366
10.1k
  scenario->clear_request_headers();
1367
10.1k
  scenario->clear_mime_post();
1368
10.1k
  scenario->clear_upload();
1369
10.1k
  RemoveTelnetOnlyShape(scenario);
1370
10.1k
  RemoveApiOnlyShape(scenario);
1371
10.1k
  RemoveMultiOnlyShape(scenario);
1372
10.1k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1373
10.1k
}
1374
1375
/// Preserve useful in-range mutations while folding ineffective extremes onto
1376
/// meaningful boundaries. Zero remains special: it disables that individual
1377
/// control and lets the other control provide the timing target's pressure.
1378
40.2k
std::uint32_t CanonicalizeNonZero(std::uint32_t value, std::uint32_t minimum, std::uint32_t maximum) {
1379
40.2k
  if (value == 0) {
1380
19.6k
    return 0;
1381
19.6k
  }
1382
20.5k
  return std::max(minimum, std::min(value, maximum));
1383
40.2k
}
1384
1385
/// Keep the timing target on the plaintext member of the protocol family.
1386
/// TLS setup has its own cost profile and would obscure whether backpressure
1387
/// mutations are exploring curl's send/receive state machines effectively.
1388
19.7k
curl::fuzzer::proto::Scheme PlaintextScheme(curl::fuzzer::proto::Scheme scheme) {
1389
19.7k
  switch (scheme) {
1390
4.30k
    case curl::fuzzer::proto::SCHEME_WS:
1391
4.31k
    case curl::fuzzer::proto::SCHEME_WSS:
1392
4.31k
      return curl::fuzzer::proto::SCHEME_WS;
1393
15.0k
    case curl::fuzzer::proto::SCHEME_HTTP:
1394
15.0k
    case curl::fuzzer::proto::SCHEME_HTTPS:
1395
15.0k
    case curl::fuzzer::proto::SCHEME_TELNET:
1396
15.0k
    case curl::fuzzer::proto::SCHEME_FTP:
1397
15.0k
    case curl::fuzzer::proto::SCHEME_TFTP:
1398
15.1k
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
1399
15.4k
    default:
1400
15.4k
      return curl::fuzzer::proto::SCHEME_HTTP;
1401
19.7k
  }
1402
19.7k
}
1403
1404
/// Remove timing controls from every connection the structured message can
1405
/// carry. Clearing only the primary script would let a mutated redirect turn a
1406
/// fixed fast lane into the timed drive loop after its second socket opens.
1407
165k
void ClearAllBackpressure(curl::fuzzer::proto::Scenario* scenario) {
1408
165k
  if (scenario->has_connection()) {
1409
165k
    scenario->mutable_connection()->clear_backpressure();
1410
165k
  }
1411
165k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
1412
66.9k
    connection.clear_backpressure();
1413
66.9k
  }
1414
165k
}
1415
1416
/// Clamp one explicitly pressure-bearing follow-on script to the same useful
1417
/// ranges as the timing lane's primary connection. An absent configuration is
1418
/// left absent so merely adding a redirect response does not add waits.
1419
5.35k
void CanonicalizeOptionalBackpressure(curl::fuzzer::proto::Connection* connection) {
1420
5.35k
  if (!connection->has_backpressure()) {
1421
4.59k
    return;
1422
4.59k
  }
1423
756
  auto* backpressure = connection->mutable_backpressure();
1424
756
  if (backpressure->recv_buf_bytes() == 0 && backpressure->drain_limit() != 0) {
1425
11
    backpressure->set_recv_buf_bytes(kDefaultBackpressureBufferBytes);
1426
745
  } else {
1427
745
    backpressure->set_recv_buf_bytes(
1428
745
        CanonicalizeNonZero(backpressure->recv_buf_bytes(), kMinBackpressureBufferBytes, kMaxBackpressureBufferBytes));
1429
745
  }
1430
756
  backpressure->set_drain_limit(CanonicalizeNonZero(backpressure->drain_limit(), 1, kMaxDrainBytesPerIteration));
1431
756
}
1432
1433
/// Canonicalize the fields that determine which server and drive-loop policy
1434
/// execute. Fast targets discard backpressure because one mutated non-zero
1435
/// scalar otherwise opts an ordinary input into hundreds of timed waits. The
1436
/// timing target does the inverse: it guarantees a non-default buffer setting
1437
/// so its CPU allocation remains focused on the intentionally slower paths.
1438
322k
void ApplyLanePolicy(curl::fuzzer::proto::Scenario* scenario, TargetProfile profile) {
1439
322k
  if (scenario == nullptr) {
1440
0
    return;
1441
0
  }
1442
1443
322k
  if (profile == TargetProfile::kCompatibility) {
1444
    // The original target's existing corpus predates profile splitting. A
1445
    // no-op here makes the type safe to pass around while its binary continues
1446
    // to omit postprocessor registration altogether. The append-only FTP/TFTP
1447
    // scheme values do not justify rewriting historical mixed-lane inputs.
1448
0
    return;
1449
0
  }
1450
1451
  // Accepted server push adds a harness-owned easy handle and therefore
1452
  // belongs only in the fixed H2 origin lanes. Compatibility remains a no-op
1453
  // above so accumulated mixed corpus entries keep their wire meaning.
1454
322k
  if (profile != TargetProfile::kHttpsH2 && profile != TargetProfile::kFastHttp2) {
1455
286k
    scenario->clear_accept_h2_push();
1456
286k
  }
1457
1458
  // Only the dedicated TLS and QUIC peers consume a certificate-chain
1459
  // selector. Remove it before protocol-specific early returns so other fixed
1460
  // targets do not spend mutations on inert TLS server state.
1461
322k
  if (profile != TargetProfile::kFastHttps && profile != TargetProfile::kHttpsH2 &&
1462
242k
      profile != TargetProfile::kFastHttp3 && profile != TargetProfile::kFastSecureWebSocket) {
1463
220k
    scenario->clear_tls_certificate_chain();
1464
220k
  }
1465
1466
  // Only the dedicated TLS HTTP/2 origin consumes the group selector. The
1467
  // compatibility profile returned above so unknown fields remain stable.
1468
322k
  if (profile != TargetProfile::kHttpsH2) {
1469
305k
    scenario->clear_tls_group_profile();
1470
305k
  }
1471
1472
  // Field 13 is append-only so the compatibility target can round-trip it,
1473
  // but every other fixed lane must discard work its peer cannot consume.
1474
322k
  if (profile != TargetProfile::kFastHttp3) {
1475
312k
    scenario->clear_http3_plan();
1476
312k
  }
1477
1478
322k
  if (profile != TargetProfile::kHttpsH2 && profile != TargetProfile::kFastHttp2) {
1479
286k
    scenario->clear_http2_plan();
1480
286k
  }
1481
1482
322k
  if (profile != TargetProfile::kSocks4) {
1483
305k
    scenario->clear_socks_proxy_mode();
1484
305k
  }
1485
1486
322k
  if (profile != TargetProfile::kResolver) {
1487
307k
    scenario->clear_resolve_entries();
1488
307k
  }
1489
1490
  // Anonymous parser files add syscalls and line parsing that belong only in
1491
  // the deep HTTP lane. Clear them before every protocol-specific early path
1492
  // so fast targets never pay to normalize content they cannot consume.
1493
322k
  if (profile != TargetProfile::kDeepHttp) {
1494
292k
    scenario->clear_cookie_file();
1495
292k
    scenario->clear_altsvc_file();
1496
292k
    scenario->clear_hsts_file();
1497
292k
    scenario->clear_netrc_file();
1498
292k
  }
1499
1500
  // A CRL is useful only when the target completes a real origin TLS setup.
1501
  // Keep malformed or oversized CRLs from turning every other lane into an
1502
  // early TLS-option failure.
1503
322k
  if (profile != TargetProfile::kFastHttps) {
1504
259k
    scenario->clear_crl_file();
1505
259k
  }
1506
1507
322k
  if (profile == TargetProfile::kFastHttp3) {
1508
10.3k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTPS);
1509
10.3k
    RemoveIgnoredHttp3Shape(scenario);
1510
10.3k
    RetainHttp3RequestOptions(scenario);
1511
10.3k
    BoundScenarioShape(scenario);
1512
    // The ordinary QUIC peer consumes Http3Plan and has no use for a stream
1513
    // response. CONNECT-UDP proxy mode instead feeds the retained Connection
1514
    // bytes to curl's HTTP/1.1 proxy state machine and capsule filter.
1515
10.3k
    if (!scenario->http3_plan().use_h1_connect_udp_proxy()) {
1516
4.67k
      scenario->clear_connection();
1517
4.67k
    }
1518
10.3k
    BoundHttp3PlanShape(scenario->mutable_http3_plan());
1519
10.3k
    CanonicalizeTlsAuthority(scenario);
1520
10.3k
    CanonicalizeTlsCertificateChain(scenario);
1521
10.3k
    return;
1522
10.3k
  }
1523
1524
312k
  if (profile == TargetProfile::kFastTelnet) {
1525
    // Set the scheme before general bounds so the TELNET-specific upload and
1526
    // PAUSE budgets are selected rather than event-driven compatibility ones.
1527
4.64k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_TELNET);
1528
4.64k
    RemoveApiOnlyShape(scenario);
1529
4.64k
    RemoveMultiOnlyShape(scenario);
1530
4.64k
    RemoveNonTelnetShape(scenario);
1531
4.64k
    RetainCheapTelnetOptions(scenario);
1532
4.64k
    BoundScenarioShape(scenario);
1533
4.64k
    BoundTelnetResponse(scenario->mutable_connection());
1534
4.64k
    return;
1535
4.64k
  }
1536
1537
307k
  if (profile == TargetProfile::kFastHttp) {
1538
19.0k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1539
19.0k
    RemoveApiOnlyShape(scenario);
1540
19.0k
    RemoveMultiOnlyShape(scenario);
1541
19.0k
    RemoveTelnetOnlyShape(scenario);
1542
19.0k
    RemoveDeepHttpShape(scenario);
1543
19.0k
    RetainCheapHttpOptions(scenario);
1544
19.0k
    CanonicalizeFastHttpVersion(scenario);
1545
19.0k
    BoundScenarioShape(scenario);
1546
19.0k
    return;
1547
19.0k
  }
1548
1549
288k
  if (profile == TargetProfile::kH2Proxy) {
1550
13.0k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1551
13.0k
    RemoveApiOnlyShape(scenario);
1552
13.0k
    RemoveMultiOnlyShape(scenario);
1553
13.0k
    RemoveTelnetOnlyShape(scenario);
1554
13.0k
    RemoveIgnoredH2ProxyShape(scenario);
1555
13.0k
    RetainH2ProxyOriginOptions(scenario);
1556
13.0k
    BoundScenarioShape(scenario);
1557
13.0k
    CanonicalizeH2ProxyOriginAuthority(scenario);
1558
13.0k
    return;
1559
13.0k
  }
1560
1561
275k
  if (profile == TargetProfile::kHttpsH2 || profile == TargetProfile::kFastHttp2) {
1562
36.2k
    scenario->set_scheme(profile == TargetProfile::kHttpsH2 ? curl::fuzzer::proto::SCHEME_HTTPS
1563
36.2k
                                                            : curl::fuzzer::proto::SCHEME_HTTP);
1564
36.2k
    RemoveApiOnlyShape(scenario);
1565
36.2k
    RemoveMultiOnlyShape(scenario);
1566
36.2k
    RemoveTelnetOnlyShape(scenario);
1567
36.2k
    RemoveIgnoredH2ProxyShape(scenario);
1568
36.2k
    RetainH2ProxyOriginOptions(scenario);
1569
36.2k
    BoundScenarioShape(scenario);
1570
36.2k
    if (scenario->has_http2_plan()) {
1571
19.8k
      auto* connection = scenario->mutable_connection();
1572
19.8k
      connection->clear_initial_response();
1573
19.8k
      connection->clear_on_readable();
1574
19.8k
      connection->clear_server_frames();
1575
19.8k
      BoundHttp2PlanShape(scenario->mutable_http2_plan());
1576
19.8k
    }
1577
36.2k
    CanonicalizeTlsAuthority(scenario);
1578
36.2k
    if (profile == TargetProfile::kHttpsH2) {
1579
16.5k
      CanonicalizeTlsCertificateChain(scenario);
1580
16.5k
      CanonicalizeTlsGroupProfile(scenario);
1581
16.5k
    }
1582
36.2k
    return;
1583
36.2k
  }
1584
1585
239k
  if (profile == TargetProfile::kSocks4) {
1586
16.9k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1587
16.9k
    if (scenario->socks_proxy_mode() != curl::fuzzer::proto::SOCKS_PROXY_SOCKS4A) {
1588
16.8k
      scenario->set_socks_proxy_mode(curl::fuzzer::proto::SOCKS_PROXY_SOCKS4);
1589
16.8k
    }
1590
16.9k
    RemoveApiOnlyShape(scenario);
1591
16.9k
    RemoveMultiOnlyShape(scenario);
1592
16.9k
    RemoveTelnetOnlyShape(scenario);
1593
16.9k
    RemoveDeepHttpShape(scenario);
1594
16.9k
    RetainCheapHttpOptions(scenario);
1595
16.9k
    scenario->mutable_connection()->clear_initial_response();
1596
16.9k
    BoundScenarioShape(scenario);
1597
16.9k
    CanonicalizeSocksAuthority(scenario);
1598
16.9k
    return;
1599
16.9k
  }
1600
1601
222k
  if (profile == TargetProfile::kResolver) {
1602
15.1k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1603
15.1k
    RemoveApiOnlyShape(scenario);
1604
15.1k
    RemoveMultiOnlyShape(scenario);
1605
15.1k
    RemoveTelnetOnlyShape(scenario);
1606
15.1k
    RemoveDeepHttpShape(scenario);
1607
15.1k
    RetainCheapHttpOptions(scenario);
1608
15.1k
    BoundStringValues(scenario->mutable_resolve_entries(), scenario_limits::kMaxResolveEntries,
1609
15.1k
                      scenario_limits::kMaxResolveEntryBytes);
1610
15.1k
    BoundScenarioShape(scenario);
1611
15.1k
    CanonicalizeResolverAuthority(scenario);
1612
15.1k
    return;
1613
15.1k
  }
1614
1615
207k
  if (profile == TargetProfile::kFastFtp) {
1616
8.90k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_FTP);
1617
8.90k
    RemoveIgnoredFtpShape(scenario);
1618
8.90k
    RetainFtpOptions(scenario);
1619
8.90k
    CanonicalizeFtpOptionModes(scenario);
1620
8.90k
    BoundScenarioShape(scenario);
1621
8.90k
    CanonicalizeFtpAuthority(scenario);
1622
8.90k
    return;
1623
8.90k
  }
1624
1625
198k
  if (profile == TargetProfile::kFastTftp) {
1626
2.50k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_TFTP);
1627
2.50k
    RemoveIgnoredTftpShape(scenario);
1628
2.50k
    RetainTftpOptions(scenario);
1629
2.50k
    CanonicalizeTftpOptionModes(scenario);
1630
2.50k
    BoundScenarioShape(scenario);
1631
2.50k
    CanonicalizeTftpAuthority(scenario);
1632
2.50k
    return;
1633
2.50k
  }
1634
1635
195k
  if (profile == TargetProfile::kFastGopher) {
1636
10.1k
    scenario->set_scheme(scenario->scheme() == curl::fuzzer::proto::SCHEME_GOPHERS
1637
10.1k
                             ? curl::fuzzer::proto::SCHEME_GOPHERS
1638
10.1k
                             : curl::fuzzer::proto::SCHEME_GOPHER);
1639
10.1k
    RemoveIgnoredGopherShape(scenario);
1640
10.1k
    RemoveFileTransferOnlyOptions(scenario);
1641
10.1k
    BoundScenarioShape(scenario);
1642
10.1k
    CanonicalizeTlsAuthority(scenario);
1643
10.1k
    return;
1644
10.1k
  }
1645
1646
  // Select the lane's scheme before applying scheme-sensitive upload bounds.
1647
  // The scheme field is itself mutable, so bounding first could accidentally
1648
  // give an HTTP/WS case TELNET's smaller payload budget merely because that
1649
  // was the input's pre-policy value.
1650
185k
  switch (profile) {
1651
0
    case TargetProfile::kCompatibility:
1652
0
      return;
1653
30.4k
    case TargetProfile::kDeepHttp:
1654
30.4k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1655
30.4k
      break;
1656
24.2k
    case TargetProfile::kApi:
1657
24.2k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1658
24.2k
      break;
1659
22.7k
    case TargetProfile::kMulti:
1660
22.7k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1661
22.7k
      break;
1662
63.2k
    case TargetProfile::kFastHttps:
1663
63.2k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTPS);
1664
63.2k
      break;
1665
0
    case TargetProfile::kHttpsH2:
1666
      // The protocol-specific early path fixes ALPN and prunes incompatible
1667
      // response shapes while retaining the raw frame script.
1668
0
      return;
1669
0
    case TargetProfile::kFastHttp2:
1670
      // The protocol-specific early path fixes prior-knowledge H2 and prunes
1671
      // incompatible response shapes while retaining its frame script.
1672
0
      return;
1673
0
    case TargetProfile::kFastHttp3:
1674
      // The protocol-specific early path owns the QUIC response plan.
1675
0
      return;
1676
0
    case TargetProfile::kH2Proxy:
1677
      // The early path removes proxy-incompatible fields before general
1678
      // bounds, keeping raw frame mutation dense.
1679
0
      return;
1680
0
    case TargetProfile::kSocks4:
1681
      // The early path fixes proxy routing and request-triggered replies.
1682
0
      return;
1683
0
    case TargetProfile::kResolver:
1684
      // The early path fixes safe DNS/cache routing and bounds its slist.
1685
0
      return;
1686
13.4k
    case TargetProfile::kFastWebSocket:
1687
13.4k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_WS);
1688
13.4k
      break;
1689
11.6k
    case TargetProfile::kFastSecureWebSocket:
1690
11.6k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_WSS);
1691
11.6k
      break;
1692
19.7k
    case TargetProfile::kTiming:
1693
19.7k
      scenario->set_scheme(PlaintextScheme(scenario->scheme()));
1694
19.7k
      break;
1695
0
    case TargetProfile::kFastHttp:
1696
0
    case TargetProfile::kFastTelnet:
1697
0
    case TargetProfile::kFastFtp:
1698
0
    case TargetProfile::kFastTftp:
1699
0
    case TargetProfile::kFastGopher:
1700
      // Protocol-specific early-return paths selected their scheme above.
1701
0
      return;
1702
185k
  }
1703
1704
  // TELNET's retained slist and synchronous pause have no observable, safe
1705
  // meaning in the fixed event-driven lanes. Clear them before walking the
1706
  // general shape so only the compatibility and TELNET targets can retain
1707
  // those values.
1708
185k
  RemoveTelnetOnlyShape(scenario);
1709
185k
  if (profile != TargetProfile::kApi) {
1710
161k
    RemoveApiOnlyShape(scenario);
1711
161k
  }
1712
185k
  if (profile != TargetProfile::kMulti) {
1713
162k
    RemoveMultiOnlyShape(scenario);
1714
162k
  }
1715
185k
  RemoveFileTransferOnlyOptions(scenario);
1716
185k
  BoundScenarioShape(scenario);
1717
1718
185k
  switch (profile) {
1719
0
    case TargetProfile::kCompatibility:
1720
0
      return;
1721
0
    case TargetProfile::kFastHttp:
1722
      // Handled before the general bounds so discarded deep shapes are never
1723
      // traversed on the fast path.
1724
0
      return;
1725
1726
30.4k
    case TargetProfile::kDeepHttp:
1727
30.4k
      ClearAllBackpressure(scenario);
1728
30.4k
      if (!scenario->altsvc_file().empty()) {
1729
1.53k
        CanonicalizeAltSvcAuthority(scenario);
1730
1.53k
      }
1731
30.4k
      return;
1732
1733
24.2k
    case TargetProfile::kApi:
1734
24.2k
      ClearAllBackpressure(scenario);
1735
24.2k
      if (scenario->host_path().size() > scenario_limits::kMaxApiStringBytes) {
1736
3
        scenario->mutable_host_path()->resize(scenario_limits::kMaxApiStringBytes);
1737
3
      }
1738
24.2k
      if (scenario->has_api_plan()) {
1739
5.82k
        BoundApiPlanShape(scenario->mutable_api_plan());
1740
5.82k
      }
1741
24.2k
      return;
1742
1743
22.7k
    case TargetProfile::kMulti: {
1744
22.7k
      ClearAllBackpressure(scenario);
1745
22.7k
      CanonicalizeMultiAuthority(scenario);
1746
22.7k
      auto* plan = scenario->mutable_multi_plan();
1747
22.7k
      BoundMultiPlanShape(plan);
1748
22.7k
      TrimRepeated(scenario->mutable_subsequent_connections(), plan->transfer_count() - 1U);
1749
22.7k
      return;
1750
0
    }
1751
1752
63.2k
    case TargetProfile::kFastHttps:
1753
63.2k
      ClearAllBackpressure(scenario);
1754
63.2k
      CanonicalizeTlsAuthority(scenario);
1755
63.2k
      CanonicalizeTlsCertificateChain(scenario);
1756
63.2k
      return;
1757
1758
0
    case TargetProfile::kHttpsH2:
1759
      // Handled by the raw HTTP/2 protocol-specific early path above.
1760
0
      return;
1761
1762
0
    case TargetProfile::kFastHttp2:
1763
      // Handled by the plaintext HTTP/2 protocol-specific early path above.
1764
0
      return;
1765
1766
0
    case TargetProfile::kFastHttp3:
1767
      // Handled before generic connection bounding because Http3Plan replaces
1768
      // the stream-socket response script.
1769
0
      return;
1770
1771
0
    case TargetProfile::kH2Proxy:
1772
      // Handled by the protocol-specific early path above.
1773
0
      return;
1774
1775
0
    case TargetProfile::kSocks4:
1776
      // Handled by the protocol-specific early path above.
1777
0
      return;
1778
1779
0
    case TargetProfile::kResolver:
1780
      // Handled by the resolver-specific early path above.
1781
0
      return;
1782
1783
13.4k
    case TargetProfile::kFastWebSocket:
1784
13.4k
      ClearAllBackpressure(scenario);
1785
13.4k
      RemoveIgnoredWebSocketShape(scenario);
1786
13.4k
      return;
1787
1788
11.6k
    case TargetProfile::kFastSecureWebSocket:
1789
11.6k
      ClearAllBackpressure(scenario);
1790
11.6k
      RemoveIgnoredWebSocketShape(scenario);
1791
11.6k
      CanonicalizeTlsAuthority(scenario);
1792
11.6k
      CanonicalizeTlsCertificateChain(scenario);
1793
11.6k
      return;
1794
1795
0
    case TargetProfile::kFastTelnet:
1796
      // Handled before the general bounds so its protocol-specific limits are
1797
      // selected from the start.
1798
0
      return;
1799
1800
0
    case TargetProfile::kFastFtp:
1801
0
    case TargetProfile::kFastTftp:
1802
0
    case TargetProfile::kFastGopher:
1803
      // Their peers consume narrower raw-script shapes, pruned before general
1804
      // bounds so ignored fields never tax these fast paths.
1805
0
      return;
1806
1807
19.7k
    case TargetProfile::kTiming: {
1808
19.7k
      if (scenario->scheme() == curl::fuzzer::proto::SCHEME_WS) {
1809
4.31k
        RemoveIgnoredWebSocketShape(scenario);
1810
4.31k
      }
1811
19.7k
      auto* backpressure = scenario->mutable_connection()->mutable_backpressure();
1812
19.7k
      if (backpressure->recv_buf_bytes() == 0) {
1813
        // A drain limit alone cannot fill the default AF_UNIX buffer with the
1814
        // harness's bounded upload. Always tighten the socket so this lane
1815
        // represents real pressure, not merely selection of the timed loop.
1816
766
        backpressure->set_recv_buf_bytes(kDefaultBackpressureBufferBytes);
1817
18.9k
      } else {
1818
18.9k
        backpressure->set_recv_buf_bytes(CanonicalizeNonZero(backpressure->recv_buf_bytes(),
1819
18.9k
                                                             kMinBackpressureBufferBytes, kMaxBackpressureBufferBytes));
1820
18.9k
      }
1821
19.7k
      backpressure->set_drain_limit(CanonicalizeNonZero(backpressure->drain_limit(), 1, kMaxDrainBytesPerIteration));
1822
19.7k
      for (auto& connection : *scenario->mutable_subsequent_connections()) {
1823
5.35k
        CanonicalizeOptionalBackpressure(&connection);
1824
5.35k
      }
1825
19.7k
      return;
1826
0
    }
1827
185k
  }
1828
185k
}
1829
1830
}  // namespace
1831
1832
/// Apply execution-only allocation-failure safety rules. Fixed target
1833
/// postprocessors call this while serializing mutations; the shared runtime
1834
/// repeats it so the compatibility target keeps its persistent wire format
1835
/// while executing the same bounded, safe plan.
1836
/// @param scenario Parsed scenario to canonicalize in place.
1837
673k
void NormalizeScenarioForExecution(curl::fuzzer::proto::Scenario* scenario) {
1838
673k
  if (scenario == nullptr) {
1839
0
    return;
1840
0
  }
1841
673k
  CanonicalizeAllocationFailures(scenario);
1842
673k
  CanonicalizeAllocationFailureDrive(scenario);
1843
673k
  CanonicalizeAllocationFailureOptions(scenario);
1844
673k
}
1845
1846
/// Complete normalization for a loaded or mutated fixed-target Scenario.
1847
/// Besides dispatch and timing controls, fixed lanes trim repeated-field and
1848
/// metadata suffixes that the runtime cannot consume. WebSocket lanes also
1849
/// remove follow-on sockets and MIME bodies their single-upgrade driver cannot
1850
/// use; all remaining observable bytes, options, frames, and probes stay
1851
/// fuzz-controlled. Option oneofs are canonicalized before final string bounds,
1852
/// so repeated normalization preserves the same serialized message. The
1853
/// compatibility profile leaves the entire input unchanged.
1854
/// @param scenario Scenario to canonicalize in place.
1855
/// @param profile Target lane whose invariants must be restored.
1856
322k
void NormalizeScenarioForTarget(curl::fuzzer::proto::Scenario* scenario, TargetProfile profile) {
1857
  // Compatibility has no LPM postprocessor. Preserve the complete wire input,
1858
  // including mismatched option oneofs and newly added, otherwise inert fields.
1859
322k
  if (scenario == nullptr || profile == TargetProfile::kCompatibility) {
1860
0
    return;
1861
0
  }
1862
1863
322k
  ApplyLanePolicy(scenario, profile);
1864
322k
  NormalizeScenarioForExecution(scenario);
1865
322k
  CanonicalizeOptionValueCases(scenario);
1866
  // Canonicalization can materialize string values or prepend pin syntax.
1867
  // Charge the final representation so normalization remains a fixed point.
1868
797k
  for (auto& option : *scenario->mutable_options()) {
1869
797k
    if (option.value_case() == curl::fuzzer::proto::SetOption::kStringValue) {
1870
316k
      TrimMetadata(option.mutable_string_value());
1871
316k
    }
1872
797k
  }
1873
322k
}
1874
1875
}  // namespace proto_fuzzer