Coverage Report

Created: 2026-10-28 07:17

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 <limits>
19
#include <string>
20
21
#include "proto_fuzzer/option_model.h"
22
#include "proto_fuzzer/scenario_limits.h"
23
#include "proto_fuzzer/telnet_scenario.h"
24
25
namespace proto_fuzzer {
26
27
namespace {
28
29
// Linux raises smaller socket-buffer requests to an implementation minimum,
30
// so 2048 is both cheap and reliably small enough to exercise short writes.
31
constexpr std::uint32_t kDefaultBackpressureBufferBytes = 2048;
32
33
// Values outside these ranges do not create useful new socket behavior for
34
// the harness's bounded 4-16 KiB writes. Keeping them small also prevents a
35
// mutated uint32 recv size from overflowing the int accepted by setsockopt.
36
constexpr std::uint32_t kMinBackpressureBufferBytes = 2048;
37
constexpr std::uint32_t kMaxBackpressureBufferBytes = 4096;
38
constexpr std::uint32_t kMaxDrainBytesPerIteration = 1024;
39
constexpr std::uint32_t kHaproxyConnectBufferBytes = 2048;
40
constexpr std::uint32_t kHaproxyConnectDrainBytes = 48;
41
42
// CURLOPT_LOCALPORTRANGE performs one synchronous bind attempt per selected
43
// port. Retain a useful retry window without letting one mutation request tens
44
// of thousands of syscalls from a single transfer.
45
constexpr std::uint64_t kMaxLocalPortRange = 16;
46
47
// A default, bounded identifier keeps newly generated inputs on the gateway's
48
// /ipfs/ or /ipns/ namespace. The path, query, and fragment after it stay
49
// mutation-controlled.
50
constexpr char kDefaultIpfsCid[] = "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi";
51
constexpr char kDefaultIpnsName[] = "k51qzi5uqu5dlvj2baxnqndepeb86cbk3ng7n3i46uzyxzyqj2xjonzllnv0v8";
52
53
/// Remove a repeated-field suffix that the runtime would ignore. Doing this
54
/// in LPM's postprocessor matters for speed as well as memory: otherwise later
55
/// mutations keep rediscovering and editing objects that cannot reach curl.
56
template <typename RepeatedField>
57
4.14M
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
4.14M
  const std::size_t size = static_cast<std::size_t>(field->size());
59
4.14M
  if (size > limit) {
60
5.24k
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
5.24k
  }
62
4.14M
}
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
57
457k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
457k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
457k
  if (size > limit) {
60
1.38k
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
1.38k
  }
62
457k
}
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
57
147k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
147k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
147k
  if (size > limit) {
60
559
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
559
  }
62
147k
}
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
57
58.9k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
58.9k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
58.9k
  if (size > limit) {
60
281
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
281
  }
62
58.9k
}
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
57
2.20M
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
2.20M
  const std::size_t size = static_cast<std::size_t>(field->size());
59
2.20M
  if (size > limit) {
60
1.43k
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
1.43k
  }
62
2.20M
}
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
57
526k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
526k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
526k
  if (size > limit) {
60
258
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
258
  }
62
526k
}
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
57
10.8k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
10.8k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
10.8k
  if (size > limit) {
60
72
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
72
  }
62
10.8k
}
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
57
12.7k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
12.7k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
12.7k
  if (size > limit) {
60
33
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
33
  }
62
12.7k
}
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
57
32.1k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
32.1k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
32.1k
  if (size > limit) {
60
191
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
191
  }
62
32.1k
}
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
57
45.4k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
45.4k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
45.4k
  if (size > limit) {
60
58
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
58
  }
62
45.4k
}
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
57
127k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
127k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
127k
  if (size > limit) {
60
27
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
27
  }
62
127k
}
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
57
24.3k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
24.3k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
24.3k
  if (size > limit) {
60
28
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
28
  }
62
24.3k
}
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
57
494k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
58
494k
  const std::size_t size = static_cast<std::size_t>(field->size());
59
494k
  if (size > limit) {
60
912
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
61
912
  }
62
494k
}
63
64
/// Bound fixed-target metadata. Compatibility inputs preserve their original
65
/// option strings; consumers of bounded request metadata cap their own views.
66
1.46M
void TrimMetadata(std::string* value) {
67
1.46M
  if (value->size() > scenario_limits::kMaxMetadataBytes) {
68
111
    value->resize(scenario_limits::kMaxMetadataBytes);
69
111
  }
70
1.46M
}
71
72
/// Give the successful-TLS lane a hostname its fixed certificate can verify
73
/// while retaining the fuzz-controlled path, query, and fragment. Arbitrary
74
/// authorities remain covered by the compatibility and legacy HTTPS lanes;
75
/// spending this lane's mutations on URL failures would keep curl's peer-cert
76
/// and encrypted application-data paths dark.
77
151k
void CanonicalizeTlsAuthority(curl::fuzzer::proto::Scenario* scenario) {
78
151k
  const std::string& host_path = scenario->host_path();
79
151k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
80
151k
  if (suffix_start == std::string::npos) {
81
5.52k
    scenario->set_host_path("tls.test/");
82
5.52k
    return;
83
5.52k
  }
84
145k
  scenario->set_host_path("tls.test" + host_path.substr(suffix_start));
85
145k
}
86
87
/// Alt-Svc lookup cannot run while the harness's wildcard CONNECT_TO route is
88
/// active. Give file-backed Alt-Svc inputs one fixed origin whose port can be
89
/// populated in curl's DNS cache before that override is safely detached,
90
/// while preserving all mutation-controlled path, query, and fragment bytes.
91
1.60k
void CanonicalizeAltSvcAuthority(curl::fuzzer::proto::Scenario* scenario) {
92
1.60k
  const std::string& host_path = scenario->host_path();
93
1.60k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
94
1.60k
  if (suffix_start == std::string::npos) {
95
7
    scenario->set_host_path("altsvc-origin.test/");
96
7
    return;
97
7
  }
98
1.59k
  scenario->set_host_path("altsvc-origin.test" + host_path.substr(suffix_start));
99
1.59k
}
100
101
/// Both fixed TLS peers support the same closed set of checked-in certificate
102
/// bundles. Unknown proto3 enum values fall back to the historical EC chain.
103
106k
void CanonicalizeTlsCertificateChain(curl::fuzzer::proto::Scenario* scenario) {
104
106k
  switch (scenario->tls_certificate_chain()) {
105
100k
    case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC:
106
106k
    case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES:
107
106k
      return;
108
116
    default:
109
116
      scenario->clear_tls_certificate_chain();
110
116
      return;
111
106k
  }
112
106k
}
113
114
/// Keep the HTTPS/HTTP2 group selector inside its closed, useful set. Unknown
115
/// proto3 enum values take the inexpensive historical-input path.
116
24.0k
void CanonicalizeTlsGroupProfile(curl::fuzzer::proto::Scenario* scenario) {
117
24.0k
  switch (scenario->tls_group_profile()) {
118
22.6k
    case curl::fuzzer::proto::TLS_GROUP_X25519:
119
24.0k
    case curl::fuzzer::proto::TLS_GROUP_PROVIDER_DEFAULT:
120
24.0k
      return;
121
3
    default:
122
3
      scenario->clear_tls_group_profile();
123
3
      return;
124
24.0k
  }
125
24.0k
}
126
127
/// Keep the client-side TLS probe inside the closed set whose backing bytes,
128
/// paths, and provider names are owned by the harness.
129
54.4k
void CanonicalizeTlsClientCredentialProfile(curl::fuzzer::proto::Scenario* scenario) {
130
54.4k
  switch (scenario->tls_client_credential_profile()) {
131
52.6k
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_NONE:
132
53.3k
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_MALFORMED_PKCS12:
133
53.5k
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_ENGINE_DEV_NULL:
134
54.2k
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_DEFAULT_PROVIDER_DEV_NULL:
135
54.4k
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_UNLOADED_PROVIDER_DEV_NULL:
136
54.4k
      return;
137
5
    default:
138
5
      scenario->clear_tls_client_credential_profile();
139
5
      return;
140
54.4k
  }
141
54.4k
}
142
143
/// Keep the OpenSSL handshake terminal inside its closed, harness-owned set.
144
54.4k
void CanonicalizeTlsHandshakeProfile(curl::fuzzer::proto::Scenario* scenario) {
145
54.4k
  switch (scenario->tls_handshake_profile()) {
146
52.8k
    case curl::fuzzer::proto::TLS_HANDSHAKE_DEFAULT:
147
54.4k
    case curl::fuzzer::proto::TLS_HANDSHAKE_OPENSSL_CLOSE_NOTIFY:
148
54.4k
      return;
149
3
    default:
150
3
      scenario->clear_tls_handshake_profile();
151
3
      return;
152
54.4k
  }
153
54.4k
}
154
155
/// Keep the proxied origin parseable while retaining every path, query, and
156
/// fragment byte. The fixed numeric proxy endpoint handles routing separately;
157
/// mutating the origin authority would therefore buy only early URL failures,
158
/// not additional HTTP/2 proxy behavior.
159
21.5k
void CanonicalizeH2ProxyOriginAuthority(curl::fuzzer::proto::Scenario* scenario) {
160
21.5k
  const std::string& host_path = scenario->host_path();
161
21.5k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
162
21.5k
  if (suffix_start == std::string::npos) {
163
484
    scenario->set_host_path("origin.test/");
164
484
    return;
165
484
  }
166
21.0k
  scenario->set_host_path("origin.test" + host_path.substr(suffix_start));
167
21.0k
}
168
169
/// Local-resolution SOCKS modes use libc/curl's deterministic localhost path,
170
/// while SOCKS4A and SOCKS5-hostname deliberately carry a fixed hostname to
171
/// the proxy. The path/query/fragment stays mutation-controlled.
172
15.9k
void CanonicalizeSocksAuthority(curl::fuzzer::proto::Scenario* scenario) {
173
15.9k
  const std::string& host_path = scenario->host_path();
174
15.9k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
175
15.9k
  const auto mode = scenario->socks_proxy_mode();
176
15.9k
  const bool resolve_at_proxy =
177
15.9k
      mode == curl::fuzzer::proto::SOCKS_PROXY_SOCKS4A || mode == curl::fuzzer::proto::SOCKS_PROXY_SOCKS5_HOSTNAME;
178
15.9k
  const char* host = resolve_at_proxy ? "socks.test" : "localhost";
179
15.9k
  if (suffix_start == std::string::npos) {
180
540
    scenario->set_host_path(std::string(host) + "/");
181
540
    return;
182
540
  }
183
15.3k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
184
15.3k
}
185
186
/// Keep proxy and Unix-domain HTTP transfers on one parseable origin while
187
/// preserving the mutation-controlled path, query, and fragment. Routing is
188
/// selected independently by a fixed numeric proxy or abstract socket name,
189
/// so authority mutation would add early URL failures without new transport
190
/// behavior.
191
2.19k
void CanonicalizeHttpTransportAuthority(curl::fuzzer::proto::Scenario* scenario) {
192
2.19k
  const std::string& host_path = scenario->host_path();
193
2.19k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
194
2.19k
  if (suffix_start == std::string::npos) {
195
40
    scenario->set_host_path("transport.test/");
196
40
    return;
197
40
  }
198
2.15k
  scenario->set_host_path("transport.test" + host_path.substr(suffix_start));
199
2.15k
}
200
201
/// Select a fixed origin that drives the chosen no-proxy parser or owned TCP
202
/// listener while retaining the mutation-controlled path, query, and fragment.
203
3.40k
void CanonicalizeProxySocketOptionAuthority(curl::fuzzer::proto::Scenario* scenario) {
204
3.40k
  const char* host;
205
3.40k
  switch (scenario->proxy_socket_option_mode()) {
206
354
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_NOPROXY_HOSTNAME:
207
354
      host = "service.proxy.test";
208
354
      break;
209
882
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_NOPROXY_IPV4_CIDR:
210
882
      host = "127.0.0.2";
211
882
      break;
212
1.52k
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_NOPROXY_IPV6_CIDR:
213
1.52k
      host = "[::1]";
214
1.52k
      break;
215
488
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_ENV_HTTP_PROXY:
216
488
      host = "proxy-origin.test";
217
488
      break;
218
152
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_LOOPBACK_INTERFACE_KEEPALIVE:
219
152
      host = "resolve.test";
220
152
      break;
221
0
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_DEFAULT:
222
0
    default:
223
0
      return;
224
3.40k
  }
225
226
3.40k
  const std::string& host_path = scenario->host_path();
227
3.40k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
228
3.40k
  scenario->set_host_path(std::string(host) +
229
3.40k
                          (suffix_start == std::string::npos ? "/" : host_path.substr(suffix_start)));
230
3.40k
}
231
232
/// Use libc/curl's localhost path when there are no structured entries, and a
233
/// fixed cache-backed name when entries are present. The DoH lane uses the
234
/// certificate's tls.test authority because its outer transfer completes a
235
/// verified TLS/ALPN h2 handshake. In every case the path, query, and fragment
236
/// remain mutation-controlled.
237
15.7k
void CanonicalizeResolverAuthority(curl::fuzzer::proto::Scenario* scenario) {
238
15.7k
  const std::string& host_path = scenario->host_path();
239
15.7k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
240
15.7k
  const char* host = "localhost";
241
15.7k
  switch (scenario->resolver_mode()) {
242
504
    case curl::fuzzer::proto::RESOLVER_MODE_THREADED_POSITIVE:
243
504
      host = "resolver-positive.test";
244
504
      break;
245
78
    case curl::fuzzer::proto::RESOLVER_MODE_THREADED_NEGATIVE:
246
78
      host = "resolver-negative.test";
247
78
      break;
248
248
    case curl::fuzzer::proto::RESOLVER_MODE_THREADED_LINK_LOCAL:
249
248
      host = "resolver-link-local.test";
250
248
      break;
251
372
    case curl::fuzzer::proto::RESOLVER_MODE_THREADED_PARTIAL:
252
372
      host = "resolver-partial.test";
253
372
      break;
254
380
    case curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL:
255
380
      host = "resolver-queued-cancel.test";
256
380
      break;
257
278
    case curl::fuzzer::proto::RESOLVER_MODE_UNIX_SOCKET:
258
278
      host = "resolver-unix.test";
259
278
      break;
260
286
    case curl::fuzzer::proto::RESOLVER_MODE_GLOBAL_LIFECYCLE:
261
286
      host = "localhost";
262
286
      break;
263
68
    case curl::fuzzer::proto::RESOLVER_MODE_DOH_ERROR:
264
68
      host = "tls.test";
265
68
      break;
266
13.5k
    case curl::fuzzer::proto::RESOLVER_MODE_DEFAULT:
267
13.5k
    default:
268
13.5k
      host =
269
13.5k
          scenario->doh_https_rr()
270
13.5k
              ? "tls.test"
271
13.5k
              : ((scenario->happy_eyeballs() || !scenario->resolve_entries().empty()) ? "resolve.test" : "localhost");
272
13.5k
      break;
273
15.7k
  }
274
15.7k
  if (suffix_start == std::string::npos) {
275
775
    scenario->set_host_path(std::string(host) + "/");
276
775
    return;
277
775
  }
278
14.9k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
279
14.9k
}
280
281
15.7k
bool IsThreadedResolverMode(curl::fuzzer::proto::ResolverMode mode) {
282
15.7k
  return mode == curl::fuzzer::proto::RESOLVER_MODE_THREADED_POSITIVE ||
283
15.2k
         mode == curl::fuzzer::proto::RESOLVER_MODE_THREADED_NEGATIVE ||
284
15.1k
         mode == curl::fuzzer::proto::RESOLVER_MODE_THREADED_LINK_LOCAL ||
285
14.8k
         mode == curl::fuzzer::proto::RESOLVER_MODE_THREADED_PARTIAL ||
286
14.5k
         mode == curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL;
287
15.7k
}
288
289
/// Give the TFTP lane a parseable filename-bearing URL while retaining the
290
/// fuzz-controlled path, query, and fragment. The UDP peer rewrites curl's
291
/// destination after URL parsing, so authority mutations cannot reach another
292
/// host; canonicalizing them here avoids spending most iterations on failures
293
/// before curl constructs a TFTP request. An explicit slash is preserved so
294
/// the missing-filename error remains reachable.
295
4.47k
void CanonicalizeTftpAuthority(curl::fuzzer::proto::Scenario* scenario) {
296
4.47k
  const std::string& host_path = scenario->host_path();
297
4.47k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
298
4.47k
  if (suffix_start == std::string::npos) {
299
388
    scenario->set_host_path("tftp.test/file");
300
388
    return;
301
388
  }
302
4.08k
  scenario->set_host_path("tftp.test" + host_path.substr(suffix_start));
303
4.08k
}
304
305
/// Keep the FTP lane inside the same parseable authority while leaving every
306
/// path segment and wildcard under mutation control. CONNECT_TO already
307
/// confines networking, but rejecting malformed authorities before USER/PWD
308
/// would waste the control/data peer this target uniquely provides.
309
/// Implicit-TLS inputs use the checked-in certificate's tls.test name so every
310
/// control and data handshake can be verified before the same exchange runs.
311
12.2k
void CanonicalizeFtpAuthority(curl::fuzzer::proto::Scenario* scenario) {
312
12.2k
  const std::string& host_path = scenario->host_path();
313
12.2k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
314
12.2k
  const char* host = scenario->scheme() == curl::fuzzer::proto::SCHEME_FTPS ? "tls.test" : "ftp.test";
315
12.2k
  if (suffix_start == std::string::npos) {
316
365
    scenario->set_host_path(std::string(host) + "/file");
317
365
    return;
318
365
  }
319
11.8k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
320
11.8k
}
321
322
/// Keep the SMTP lane inside a parseable authority while leaving the URL path
323
/// mutation-controlled. In this curl revision the path is the EHLO argument
324
/// (the client's announced domain), not the envelope recipient; recipients
325
/// arrive through Scenario.mail_rcpt. Preserving the suffix still exercises
326
/// EHLO parsing, while a fixed host avoids spending iterations on URL failures
327
/// before curl sends its first command. Implicit-TLS inputs use the checked-in
328
/// certificate's tls.test name so the verified handshake can complete before
329
/// the same command exchange runs.
330
9.69k
void CanonicalizeSmtpAuthority(curl::fuzzer::proto::Scenario* scenario) {
331
9.69k
  const std::string& host_path = scenario->host_path();
332
9.69k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
333
9.69k
  const char* host = scenario->scheme() == curl::fuzzer::proto::SCHEME_SMTPS ? "tls.test" : "smtp.test";
334
9.69k
  if (suffix_start == std::string::npos) {
335
334
    scenario->set_host_path(std::string(host) + "/client.test");
336
334
    return;
337
334
  }
338
9.35k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
339
9.35k
}
340
341
/// Keep the IMAP lane inside a parseable authority while retaining every URL
342
/// path parameter, query, and fragment byte. The path carries the mailbox and
343
/// the `;UID=`, `;MAILINDEX=`, `;SECTION=`, and `;PARTIAL=` selectors that
344
/// choose FETCH, SEARCH, and LIST states; canonicalizing only the host avoids
345
/// spending iterations on failures before curl sends its first command.
346
/// Implicit-TLS inputs use the checked-in certificate's tls.test name so the
347
/// verified handshake can complete before the same tagged exchange runs.
348
10.2k
void CanonicalizeImapAuthority(curl::fuzzer::proto::Scenario* scenario) {
349
10.2k
  const std::string& host_path = scenario->host_path();
350
10.2k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
351
10.2k
  const char* host = scenario->scheme() == curl::fuzzer::proto::SCHEME_IMAPS ? "tls.test" : "imap.test";
352
10.2k
  if (suffix_start == std::string::npos) {
353
291
    scenario->set_host_path(std::string(host) + "/INBOX");
354
291
    return;
355
291
  }
356
10.0k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
357
10.0k
}
358
359
/// Give POP3 a mailbox with a message id by default so the lane reaches RETR
360
/// rather than only LIST. The id remains mutation-controlled, and an explicit
361
/// trailing slash or query continues to select the listing and error states.
362
/// Implicit-TLS inputs use the checked-in certificate's tls.test name so the
363
/// verified handshake can complete before the same status-line exchange runs.
364
7.64k
void CanonicalizePop3Authority(curl::fuzzer::proto::Scenario* scenario) {
365
7.64k
  const std::string& host_path = scenario->host_path();
366
7.64k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
367
7.64k
  const char* host = scenario->scheme() == curl::fuzzer::proto::SCHEME_POP3S ? "tls.test" : "pop3.test";
368
7.64k
  if (suffix_start == std::string::npos) {
369
199
    scenario->set_host_path(std::string(host) + "/1");
370
199
    return;
371
199
  }
372
7.44k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
373
7.44k
}
374
375
/// Pin the MQTT broker authority while leaving the URL path mutation-
376
/// controlled: curl percent-decodes that path into the PUBLISH/SUBSCRIBE topic.
377
/// A missing path is a legitimate "No MQTT topic found" error, but a default
378
/// topic makes most newly generated inputs reach the packet peer instead of
379
/// stopping at URL validation. Implicit-TLS inputs use the checked-in
380
/// certificate's tls.test name so the verified handshake can complete before
381
/// the same packet exchange runs.
382
8.09k
void CanonicalizeMqttAuthority(curl::fuzzer::proto::Scenario* scenario) {
383
8.09k
  const std::string& host_path = scenario->host_path();
384
8.09k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
385
8.09k
  const char* host = scenario->scheme() == curl::fuzzer::proto::SCHEME_MQTTS ? "tls.test" : "mqtt.test";
386
8.09k
  if (suffix_start == std::string::npos) {
387
309
    scenario->set_host_path(std::string(host) + "/topic");
388
309
    return;
389
309
  }
390
7.78k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
391
7.78k
}
392
393
/// Pin the DICT server authority while retaining every lookup selector in the
394
/// URL path. In curl's DICT handler the path after the leading slash is the
395
/// complete command: `/d:word`, `/match:word:db`, `/show:db`, or a raw command
396
/// with colons becoming spaces. A default DEFINE keeps newly generated inputs
397
/// reaching the command peer instead of stopping at URL validation, while an
398
/// explicit path (including a bare `/`) continues to select the other states.
399
6.42k
void CanonicalizeDictAuthority(curl::fuzzer::proto::Scenario* scenario) {
400
6.42k
  const std::string& host_path = scenario->host_path();
401
6.42k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
402
6.42k
  if (suffix_start == std::string::npos) {
403
177
    scenario->set_host_path("dict.test/d:word");
404
177
    return;
405
177
  }
406
6.24k
  std::string canonical = "dict.test" + host_path.substr(suffix_start);
407
6.24k
  if (canonical.size() > scenario_limits::kMaxDictHostPathBytes) {
408
324
    canonical.resize(scenario_limits::kMaxDictHostPathBytes);
409
324
  }
410
6.24k
  scenario->set_host_path(canonical);
411
6.24k
}
412
413
/// Pin a file:// URL to a fixed local source/sink. The path component is what
414
/// libcurl opens, so retaining a fuzzed path would let a mutated input read or
415
/// write any file visible to the fuzzer process. Keep the fuzz-controlled
416
/// authority and any query/fragment so URL parsing still sees mutated bytes,
417
/// but always replace the path with /dev/null. An input with no path keeps its
418
/// authority and gains /dev/null, so the canonical empty-authority form is
419
/// `file:///dev/null`.
420
7.15k
void CanonicalizeFileAuthority(curl::fuzzer::proto::Scenario* scenario) {
421
7.15k
  const std::string& host_path = scenario->host_path();
422
7.15k
  const std::size_t path_start = host_path.find('/');
423
7.15k
  const std::size_t query_start = host_path.find_first_of("?#");
424
7.15k
  std::size_t authority_end = host_path.size();
425
7.15k
  if (path_start != std::string::npos) {
426
6.97k
    authority_end = std::min(authority_end, path_start);
427
6.97k
  }
428
7.15k
  if (query_start != std::string::npos) {
429
614
    authority_end = std::min(authority_end, query_start);
430
614
  }
431
432
7.15k
  std::string canonical = host_path.substr(0, authority_end);
433
7.15k
  canonical += "/dev/null";
434
7.15k
  if (query_start != std::string::npos) {
435
614
    canonical += host_path.substr(query_start);
436
614
  }
437
7.15k
  scenario->set_host_path(canonical);
438
7.15k
}
439
440
/// Pin the LDAP directory authority while retaining the complete URL suffix.
441
/// In this curl revision OpenLDAP parses the base DN, attributes, scope, and
442
/// filter from the URL path (`/dn?attrs?scope?filter`); CURLOPT_CUSTOMREQUEST
443
/// is not consulted. A default base DN makes newly generated inputs reach the
444
/// bind/search peer, and dropping any userinfo component keeps OpenLDAP's
445
/// strict URL parser from rejecting credentials it only accepts separately.
446
/// Implicit-TLS inputs use the checked-in certificate's tls.test name so the
447
/// verified handshake can complete before the same BER exchange runs.
448
11.5k
void CanonicalizeLdapAuthority(curl::fuzzer::proto::Scenario* scenario) {
449
11.5k
  const std::string& host_path = scenario->host_path();
450
11.5k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
451
11.5k
  const char* host = scenario->scheme() == curl::fuzzer::proto::SCHEME_LDAPS ? "tls.test" : "ldap.test";
452
11.5k
  if (suffix_start == std::string::npos) {
453
149
    scenario->set_host_path(std::string(host) + "/dc=example,dc=com");
454
149
    return;
455
149
  }
456
11.3k
  scenario->set_host_path(std::string(host) + host_path.substr(suffix_start));
457
11.3k
}
458
459
/// Pin the RTSP peer's authority while preserving the path, query, and
460
/// fragment. curl builds its request stream URI from CURLOPT_RTSP_STREAM_URI
461
/// (defaulting to "*"), but the URL still has to parse before the dedicated
462
/// request/response peer can be reached. A default stream path keeps newly
463
/// generated scenarios representative rather than spending them on URL errors.
464
12.6k
void CanonicalizeRtspAuthority(curl::fuzzer::proto::Scenario* scenario) {
465
12.6k
  const std::string& host_path = scenario->host_path();
466
12.6k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
467
12.6k
  if (suffix_start == std::string::npos) {
468
136
    scenario->set_host_path("rtsp.test/stream");
469
136
    return;
470
136
  }
471
12.4k
  scenario->set_host_path("rtsp.test" + host_path.substr(suffix_start));
472
12.4k
}
473
474
/// Pin the IPFS/IPNS identifier while preserving the gateway path, query, and
475
/// fragment. curl's command-line tool rewrites these schemes to an HTTP
476
/// gateway request; the structured lane performs that same rewrite inside the
477
/// runner, so the identifier has no local meaning beyond selecting the
478
/// /ipfs/<cid> or /ipns/<name> namespace. A default identifier makes newly
479
/// generated inputs reach the HTTP peer, while the bounded suffix retains the
480
/// fuzz-controlled request target.
481
18.5k
void CanonicalizeIpfsAuthority(curl::fuzzer::proto::Scenario* scenario) {
482
18.5k
  const std::string& host_path = scenario->host_path();
483
18.5k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
484
18.5k
  std::string canonical = scenario->scheme() == curl::fuzzer::proto::SCHEME_IPNS ? kDefaultIpnsName : kDefaultIpfsCid;
485
18.5k
  if (suffix_start != std::string::npos) {
486
1.85k
    canonical += host_path.substr(suffix_start);
487
1.85k
  }
488
18.5k
  if (canonical.size() > scenario_limits::kMaxIpfsHostPathBytes) {
489
1
    canonical.resize(scenario_limits::kMaxIpfsHostPathBytes);
490
1
  }
491
18.5k
  scenario->set_host_path(canonical);
492
18.5k
}
493
494
/// Put every handle in the multi lane on one origin so connection limits,
495
/// queueing, and reuse affect real transfers instead of independent hosts.
496
/// Path/query/fragment bytes remain mutation-controlled.
497
24.3k
void CanonicalizeMultiAuthority(curl::fuzzer::proto::Scenario* scenario) {
498
24.3k
  const std::string& host_path = scenario->host_path();
499
24.3k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
500
24.3k
  if (suffix_start == std::string::npos) {
501
1.11k
    scenario->set_host_path("multi.test/");
502
1.11k
    return;
503
1.11k
  }
504
23.2k
  scenario->set_host_path("multi.test" + host_path.substr(suffix_start));
505
23.2k
}
506
507
template <typename RepeatedBytes>
508
1.67M
void BoundStringValues(RepeatedBytes* values, std::size_t count_limit, std::size_t value_limit) {
509
1.67M
  TrimRepeated(values, count_limit);
510
1.67M
  for (std::string& value : *values) {
511
431k
    if (value.size() > value_limit) {
512
122
      value.resize(value_limit);
513
122
    }
514
431k
  }
515
1.67M
}
516
517
template <typename RepeatedBytes>
518
741k
void BoundHeaderValues(RepeatedBytes* headers, std::size_t limit) {
519
741k
  BoundStringValues(headers, limit, scenario_limits::kMaxMetadataBytes);
520
741k
}
521
522
/// Keep one response script identical to the prefix MockServer and
523
/// WebSocketMockServer can deliver. Raw chunks take precedence over structured
524
/// frames, matching both runtime serializers.
525
526k
void BoundConnectionShape(curl::fuzzer::proto::Connection* connection) {
526
526k
  TrimRepeated(connection->mutable_on_readable(), scenario_limits::kMaxResponseChunks);
527
526k
  const std::size_t raw_count = static_cast<std::size_t>(connection->on_readable_size());
528
526k
  TrimRepeated(connection->mutable_server_frames(), scenario_limits::kMaxResponseChunks - raw_count);
529
526k
}
530
531
/// Retain the observable prefix of one filename-backed parser input while
532
/// debiting the shared per-scenario file-byte budget.
533
2.28M
void BoundFileInput(std::string* input, std::size_t* remaining_bytes) {
534
2.28M
  const std::size_t limit = std::min(scenario_limits::kMaxFileInputBytes, *remaining_bytes);
535
2.28M
  if (input->size() > limit) {
536
0
    input->resize(limit);
537
0
  }
538
2.28M
  *remaining_bytes -= input->size();
539
2.28M
}
540
541
457k
void BoundFileInputs(curl::fuzzer::proto::Scenario* scenario) {
542
457k
  std::size_t remaining_bytes = scenario_limits::kMaxFileInputTotalBytes;
543
457k
  BoundFileInput(scenario->mutable_cookie_file(), &remaining_bytes);
544
457k
  BoundFileInput(scenario->mutable_altsvc_file(), &remaining_bytes);
545
457k
  BoundFileInput(scenario->mutable_hsts_file(), &remaining_bytes);
546
457k
  BoundFileInput(scenario->mutable_netrc_file(), &remaining_bytes);
547
457k
  BoundFileInput(scenario->mutable_crl_file(), &remaining_bytes);
548
457k
}
549
550
/// Apply the metadata/header limits shared by both MIME part message types.
551
template <typename Part>
552
283k
void BoundMimePartMetadata(Part* part) {
553
283k
  TrimMetadata(part->mutable_name());
554
283k
  TrimMetadata(part->mutable_filename());
555
283k
  TrimMetadata(part->mutable_content_type());
556
283k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
557
283k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundMimePartMetadata<curl::fuzzer::proto::MimePart>(curl::fuzzer::proto::MimePart*)
Line
Count
Source
552
160k
void BoundMimePartMetadata(Part* part) {
553
160k
  TrimMetadata(part->mutable_name());
554
160k
  TrimMetadata(part->mutable_filename());
555
160k
  TrimMetadata(part->mutable_content_type());
556
160k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
557
160k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundMimePartMetadata<curl::fuzzer::proto::MimeDataPart>(curl::fuzzer::proto::MimeDataPart*)
Line
Count
Source
552
123k
void BoundMimePartMetadata(Part* part) {
553
123k
  TrimMetadata(part->mutable_name());
554
123k
  TrimMetadata(part->mutable_filename());
555
123k
  TrimMetadata(part->mutable_content_type());
556
123k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
557
123k
}
558
559
42.6k
void BoundGeneratedMimeData(curl::fuzzer::proto::GeneratedBytes* generated, std::size_t* remaining_bytes) {
560
42.6k
  if (generated->pattern().size() > scenario_limits::kMaxGeneratedMimePatternBytes) {
561
39
    generated->mutable_pattern()->resize(scenario_limits::kMaxGeneratedMimePatternBytes);
562
39
  }
563
564
42.6k
  const std::size_t pattern_size = generated->pattern().size();
565
42.6k
  if (pattern_size == 0 || *remaining_bytes == 0) {
566
15.6k
    generated->clear_repeat_count();
567
15.6k
    return;
568
15.6k
  }
569
570
  // Keep every exact in-range size, including 16/32/64 KiB and their +/-1
571
  // neighborhoods. Counts beyond the shared budget all have identical runtime
572
  // behavior, so fold them onto the largest observable repetition count.
573
27.0k
  const std::size_t max_repeat_count = *remaining_bytes / pattern_size;
574
27.0k
  const std::size_t repeat_count =
575
27.0k
      std::min<std::size_t>(static_cast<std::size_t>(generated->repeat_count()), max_repeat_count);
576
27.0k
  generated->set_repeat_count(static_cast<std::uint32_t>(repeat_count));
577
27.0k
  *remaining_bytes -= repeat_count * pattern_size;
578
27.0k
}
579
580
123k
void BoundMimeLeaf(curl::fuzzer::proto::MimeDataPart* part, std::size_t* remaining_generated_bytes) {
581
123k
  BoundMimePartMetadata(part);
582
123k
  if (part->content_case() == curl::fuzzer::proto::MimeDataPart::kGeneratedData) {
583
21.9k
    BoundGeneratedMimeData(part->mutable_generated_data(), remaining_generated_bytes);
584
101k
  } else if (part->data().size() > scenario_limits::kMaxMimeDataBytes) {
585
2
    part->mutable_data()->resize(scenario_limits::kMaxMimeDataBytes);
586
2
  }
587
123k
}
588
589
/// Mirror the runtime's shared top-level/nested part budget in the protobuf
590
/// itself. A simple per-list cap is insufficient because many bounded child
591
/// lists could still leave most of the message semantically dead.
592
73.8k
void BoundMimeShape(curl::fuzzer::proto::MimePost* post) {
593
73.8k
  TrimRepeated(post->mutable_parts(), scenario_limits::kMaxTopLevelMimeParts);
594
73.8k
  std::size_t remaining = scenario_limits::kMaxTotalMimeParts;
595
73.8k
  std::size_t remaining_generated_bytes = scenario_limits::kMaxGeneratedMimeDataBytes;
596
73.8k
  std::size_t retained_top_parts = 0;
597
598
233k
  while (retained_top_parts < static_cast<std::size_t>(post->parts_size()) && remaining != 0) {
599
160k
    auto* part = post->mutable_parts(static_cast<int>(retained_top_parts));
600
160k
    ++retained_top_parts;
601
160k
    --remaining;
602
160k
    BoundMimePartMetadata(part);
603
604
160k
    if (part->content_case() == curl::fuzzer::proto::MimePart::kData) {
605
10.4k
      if (part->data().size() > scenario_limits::kMaxMimeDataBytes) {
606
3
        part->mutable_data()->resize(scenario_limits::kMaxMimeDataBytes);
607
3
      }
608
10.4k
      continue;
609
10.4k
    }
610
149k
    if (part->content_case() == curl::fuzzer::proto::MimePart::kGeneratedData) {
611
20.6k
      BoundGeneratedMimeData(part->mutable_generated_data(), &remaining_generated_bytes);
612
20.6k
      continue;
613
20.6k
    }
614
128k
    if (part->content_case() != curl::fuzzer::proto::MimePart::kSubparts) {
615
69.9k
      continue;
616
69.9k
    }
617
618
58.9k
    auto* children = part->mutable_subparts()->mutable_parts();
619
58.9k
    TrimRepeated(children, std::min(scenario_limits::kMaxNestedMimeParts, remaining));
620
123k
    for (auto& child : *children) {
621
123k
      BoundMimeLeaf(&child, &remaining_generated_bytes);
622
123k
      --remaining;
623
123k
    }
624
58.9k
  }
625
626
73.8k
  TrimRepeated(post->mutable_parts(), retained_top_parts);
627
73.8k
}
628
629
/// Remove upload bytes and read steps the callback cannot observe. Clamping
630
/// individual limits also keeps mutations concentrated on short reads instead
631
/// of many distinct uint32 values that all collapse to the same 16 KiB cap.
632
void BoundUploadShape(curl::fuzzer::proto::UploadScript* upload, std::size_t data_limit, std::size_t read_step_limit,
633
42.2k
                      std::size_t read_size_limit) {
634
42.2k
  if (upload->data().size() > data_limit) {
635
13
    upload->mutable_data()->resize(data_limit);
636
13
  }
637
  // RepeatedField<uint32_t> lacks RepeatedPtrField's DeleteSubrange helper;
638
  // removing the ignored suffix from the end is constant-time per element and
639
  // preserves the mutation-significant prefix exactly.
640
139k
  while (static_cast<std::size_t>(upload->read_sizes_size()) > read_step_limit) {
641
96.7k
    upload->mutable_read_sizes()->RemoveLast();
642
96.7k
  }
643
136k
  for (int i = 0; i < upload->read_sizes_size(); ++i) {
644
94.5k
    if (upload->read_sizes(i) > read_size_limit) {
645
1.21k
      upload->set_read_sizes(i, static_cast<std::uint32_t>(read_size_limit));
646
1.21k
    }
647
94.5k
  }
648
42.2k
}
649
650
/// Trim a protobuf repeated scalar without depending on the container's
651
/// pointer-field-only DeleteSubrange API. Keeping the mutation-significant
652
/// prefix matches every runtime selector loop.
653
template <typename RepeatedScalar>
654
33.8k
void TrimRepeatedScalar(RepeatedScalar* values, std::size_t limit) {
655
37.0k
  while (static_cast<std::size_t>(values->size()) > limit) {
656
3.29k
    values->RemoveLast();
657
3.29k
  }
658
33.8k
}
659
660
/// Canonicalize the allocation failure plan to the same fixed-size sequence
661
/// consumed by the runtime controller. Sorting lets its hot callback compare
662
/// only the next ordinal, while retaining the lowest distinct values keeps
663
/// an oversized mutation focused on reachable work.
664
698k
void CanonicalizeAllocationFailures(curl::fuzzer::proto::Scenario* scenario) {
665
698k
  std::array<std::uint32_t, scenario_limits::kMaxAllocationFailures> retained{};
666
698k
  std::size_t retained_count = 0;
667
698k
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxAllocationOrdinal);
668
669
874k
  for (const std::uint32_t raw : scenario->fail_allocations()) {
670
874k
    if (raw == 0) {
671
81.8k
      continue;
672
81.8k
    }
673
792k
    const std::uint32_t ordinal = 1U + ((raw - 1U) % maximum);
674
792k
    const auto begin = retained.begin();
675
792k
    const auto end = begin + static_cast<std::ptrdiff_t>(retained_count);
676
792k
    const auto position = std::lower_bound(begin, end, ordinal);
677
792k
    if (position != end && *position == ordinal) {
678
232k
      continue;
679
232k
    }
680
560k
    const std::size_t index = static_cast<std::size_t>(position - begin);
681
560k
    if (index >= retained.size()) {
682
358k
      continue;
683
358k
    }
684
201k
    if (retained_count < retained.size()) {
685
194k
      ++retained_count;
686
194k
    }
687
218k
    for (std::size_t destination = retained_count - 1; destination > index; --destination) {
688
16.4k
      retained[destination] = retained[destination - 1];
689
16.4k
    }
690
201k
    retained[index] = ordinal;
691
201k
  }
692
693
698k
  auto* values = scenario->mutable_fail_allocations();
694
698k
  values->Clear();
695
893k
  for (std::size_t index = 0; index < retained_count; ++index) {
696
194k
    values->Add(retained[index]);
697
194k
  }
698
698k
}
699
700
/// Keep injected failures out of curl's debug event adapter. Its socket
701
/// monitor is heap-backed, and rejecting the allocation that records a newly
702
/// announced socket leaves a later CURL_POLL_REMOVE with no matching entry.
703
/// Ordinary inputs still cover curl_easy_perform_ev; allocation plans use the
704
/// equivalent blocking easy entrypoint and retain a stable allocation trace.
705
698k
void CanonicalizeAllocationFailureDrive(curl::fuzzer::proto::Scenario* scenario) {
706
698k
  if (!scenario->fail_allocations().empty() && scenario->has_api_plan() &&
707
4.70k
      scenario->api_plan().drive_mode() == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) {
708
6
    scenario->mutable_api_plan()->set_drive_mode(curl::fuzzer::proto::API_DRIVE_EASY_PERFORM);
709
6
  }
710
698k
}
711
712
/// Fold an unknown API drive selector to the ordinary multi-perform path
713
/// before generated guards decide whether multi-only fields are eligible.
714
22.5k
void CanonicalizeApiDriveMode(curl::fuzzer::proto::ApiPlan* plan) {
715
22.5k
  switch (plan->drive_mode()) {
716
15.9k
    case curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM:
717
18.3k
    case curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET:
718
21.2k
    case curl::fuzzer::proto::API_DRIVE_EASY_PERFORM:
719
21.7k
    case curl::fuzzer::proto::API_DRIVE_EASY_EVENTS:
720
22.5k
    case curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY:
721
22.5k
      return;
722
34
    default:
723
34
      plan->set_drive_mode(curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM);
724
34
      return;
725
22.5k
  }
726
22.5k
}
727
728
/// Keep API work proportional to the fixed descriptor tables used by the
729
/// runtime. Selector magnitudes stay mutation-controlled because the runtime
730
/// folds them into the relevant typed table; only suffixes it cannot execute
731
/// are dead and therefore removed here.
732
11.2k
void BoundApiPlanShape(curl::fuzzer::proto::ApiPlan* plan) {
733
11.2k
  TrimRepeatedScalar(plan->mutable_share_data_selectors(), scenario_limits::kMaxApiShareDataSelectors);
734
11.2k
  TrimRepeatedScalar(plan->mutable_easy_info_selectors(), scenario_limits::kMaxApiInfoSelectors);
735
11.2k
  TrimRepeatedScalar(plan->mutable_reentrant_probe_selectors(), scenario_limits::kMaxApiReentrantSelectors);
736
737
11.2k
  CanonicalizeApiDriveMode(plan);
738
11.2k
  switch (plan->drive_mode()) {
739
8.00k
    case curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM:
740
9.20k
    case curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET:
741
9.20k
      break;
742
1.44k
    case curl::fuzzer::proto::API_DRIVE_EASY_PERFORM:
743
1.67k
    case curl::fuzzer::proto::API_DRIVE_EASY_EVENTS:
744
2.06k
    case curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY:
745
      // Wakeup is a multi-handle API and has no live object in easy mode.
746
      // Likewise, a body callback cannot be resumed while one of these
747
      // blocking entrypoints owns the thread. Clearing both mutations keeps
748
      // every retained API-plan value observable and bounded.
749
2.06k
      plan->set_wake_multi(false);
750
2.06k
      plan->set_pause_response_once(false);
751
2.06k
      break;
752
0
    default:
753
0
      break;
754
11.2k
  }
755
11.2k
}
756
757
/// Retain the same-easy MIME repeat only when its callback cursor and MIME
758
/// consumer both exist. Pinning this closed probe to the ordinary multi loop
759
/// avoids combining a second perform with connect-only or paused-response
760
/// lifecycles whose state cannot be meaningfully reused.
761
11.2k
void CanonicalizeApiMimeRepeat(curl::fuzzer::proto::Scenario* scenario) {
762
11.2k
  if (!scenario->has_api_plan() || !scenario->api_plan().repeat_mime_post()) {
763
10.3k
    return;
764
10.3k
  }
765
766
891
  bool has_callback_part = false;
767
891
  if (scenario->has_mime_post()) {
768
1.32k
    for (const auto& part : scenario->mime_post().parts()) {
769
1.32k
      if (part.content_case() == curl::fuzzer::proto::MimePart::kUploadCallback) {
770
890
        has_callback_part = true;
771
890
        break;
772
890
      }
773
1.32k
    }
774
891
  }
775
891
  if (!scenario->has_upload() || !has_callback_part) {
776
1
    scenario->mutable_api_plan()->clear_repeat_mime_post();
777
1
    return;
778
1
  }
779
780
890
  auto* plan = scenario->mutable_api_plan();
781
890
  plan->set_drive_mode(curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM);
782
890
  plan->set_wake_multi(false);
783
890
  plan->set_pause_response_once(false);
784
890
}
785
786
/// Keep concurrent-handle work within the mock's fixed socket and operation
787
/// budgets. Values are canonicalized here rather than only at runtime so LPM
788
/// mutates state that the target can actually distinguish.
789
24.3k
void BoundMultiPlanShape(curl::fuzzer::proto::MultiPlan* plan) {
790
24.3k
  const std::uint32_t minimum = static_cast<std::uint32_t>(scenario_limits::kMinMultiTransfers);
791
24.3k
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxMultiTransfers);
792
24.3k
  const std::uint32_t transfer_count = std::max(minimum, std::min(plan->transfer_count(), maximum));
793
24.3k
  plan->set_transfer_count(transfer_count);
794
24.3k
  plan->set_max_host_connections(std::min(plan->max_host_connections(), transfer_count));
795
24.3k
  plan->set_max_total_connections(std::min(plan->max_total_connections(), transfer_count));
796
24.3k
  plan->set_connection_cache_size(std::min(plan->connection_cache_size(), maximum * 2U));
797
24.3k
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxMultiActions);
798
799
24.3k
  switch (plan->drive_mode()) {
800
20.2k
    case curl::fuzzer::proto::MULTI_DRIVE_PERFORM:
801
24.3k
    case curl::fuzzer::proto::MULTI_DRIVE_SOCKET:
802
24.3k
      break;
803
50
    default:
804
50
      plan->set_drive_mode(curl::fuzzer::proto::MULTI_DRIVE_PERFORM);
805
50
      break;
806
24.3k
  }
807
808
24.3k
  switch (plan->state_probe_profile()) {
809
21.1k
    case curl::fuzzer::proto::MULTI_STATE_PROBE_NONE:
810
24.3k
    case curl::fuzzer::proto::MULTI_STATE_PROBE_PROGRESS_RATE_LIMIT:
811
24.3k
      break;
812
38
    default:
813
38
      plan->set_state_probe_profile(curl::fuzzer::proto::MULTI_STATE_PROBE_NONE);
814
38
      break;
815
24.3k
  }
816
817
42.1k
  for (auto& action : *plan->mutable_actions()) {
818
42.1k
    action.set_transfer_selector(action.transfer_selector() % transfer_count);
819
42.1k
    switch (action.kind()) {
820
8.33k
      case curl::fuzzer::proto::MULTI_ACTION_NONE:
821
11.4k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_RECV:
822
13.6k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_SEND:
823
15.2k
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_ALL:
824
16.4k
      case curl::fuzzer::proto::MULTI_ACTION_RESUME:
825
29.0k
      case curl::fuzzer::proto::MULTI_ACTION_REMOVE:
826
42.0k
      case curl::fuzzer::proto::MULTI_ACTION_READD:
827
42.0k
        break;
828
59
      default:
829
59
        action.set_kind(curl::fuzzer::proto::MULTI_ACTION_NONE);
830
59
        break;
831
42.1k
    }
832
42.1k
  }
833
24.3k
}
834
835
/// HTTP field names are lowercase RFC token bytes in the structured lane.
836
/// Replacing (rather than deleting) invalid bytes retains mutation-significant
837
/// positions while preventing accidental pseudo-headers and encoder failures.
838
90.6k
void CanonicalizeHttp3HeaderName(std::string* name) {
839
90.6k
  if (name->size() > scenario_limits::kMaxHttp3HeaderNameBytes) {
840
40
    name->resize(scenario_limits::kMaxHttp3HeaderNameBytes);
841
40
  }
842
90.6k
  if (name->empty()) {
843
795
    *name = "x-fuzz";
844
795
    return;
845
795
  }
846
847
829k
  for (char& byte : *name) {
848
829k
    const unsigned char value = static_cast<unsigned char>(byte);
849
829k
    const bool alpha = (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z');
850
829k
    const bool digit = value >= '0' && value <= '9';
851
829k
    const bool punctuation = value == '!' || value == '#' || value == '$' || value == '%' || value == '&' ||
852
802k
                             value == '\'' || value == '*' || value == '+' || value == '-' || value == '.' ||
853
436k
                             value == '^' || value == '_' || value == '`' || value == '|' || value == '~';
854
829k
    if (value >= 'A' && value <= 'Z') {
855
1.37k
      byte = static_cast<char>(value - 'A' + 'a');
856
828k
    } else if (!alpha && !digit && !punctuation) {
857
4.34k
      byte = '-';
858
4.34k
    }
859
829k
  }
860
89.8k
}
861
862
/// Structured field values must not inject another HTTP field or carry NUL
863
/// into the encoder. Observable malformed bytes remain available in raw stream
864
/// actions, while this path stays suitable for valid QPACK generation.
865
90.6k
void CanonicalizeHttp3HeaderValue(std::string* value) {
866
90.6k
  if (value->size() > scenario_limits::kMaxHttp3HeaderValueBytes) {
867
0
    value->resize(scenario_limits::kMaxHttp3HeaderValueBytes);
868
0
  }
869
6.93M
  for (char& byte : *value) {
870
6.93M
    const unsigned char character = static_cast<unsigned char>(byte);
871
6.93M
    if ((character < 0x20U && character != '\t') || character == 0x7fU) {
872
9.77k
      byte = ' ';
873
9.77k
    }
874
6.93M
  }
875
90.6k
}
876
877
/// Retain only a prefix of encodable fields within both a count and a shared
878
/// byte budget. A missing name is materialized as x-fuzz while budget remains,
879
/// making default-initialized structured headers useful to the peer.
880
template <typename RepeatedHeaders>
881
69.8k
void BoundHttp3Headers(RepeatedHeaders* headers, std::size_t count_limit, std::size_t* remaining_bytes) {
882
69.8k
  TrimRepeated(headers, count_limit);
883
69.8k
  std::size_t retained = 0;
884
160k
  while (retained < static_cast<std::size_t>(headers->size()) && *remaining_bytes != 0) {
885
90.6k
    auto* header = headers->Mutable(static_cast<int>(retained));
886
90.6k
    CanonicalizeHttp3HeaderName(header->mutable_name());
887
90.6k
    CanonicalizeHttp3HeaderValue(header->mutable_value());
888
889
90.6k
    if (header->name().size() > *remaining_bytes) {
890
2
      header->mutable_name()->resize(*remaining_bytes);
891
2
      header->clear_value();
892
90.6k
    } else if (header->value().size() > *remaining_bytes - header->name().size()) {
893
6
      header->mutable_value()->resize(*remaining_bytes - header->name().size());
894
6
    }
895
90.6k
    *remaining_bytes -= header->name().size() + header->value().size();
896
90.6k
    ++retained;
897
90.6k
  }
898
69.8k
  TrimRepeated(headers, retained);
899
69.8k
}
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
881
6.36k
void BoundHttp3Headers(RepeatedHeaders* headers, std::size_t count_limit, std::size_t* remaining_bytes) {
882
6.36k
  TrimRepeated(headers, count_limit);
883
6.36k
  std::size_t retained = 0;
884
10.3k
  while (retained < static_cast<std::size_t>(headers->size()) && *remaining_bytes != 0) {
885
3.96k
    auto* header = headers->Mutable(static_cast<int>(retained));
886
3.96k
    CanonicalizeHttp3HeaderName(header->mutable_name());
887
3.96k
    CanonicalizeHttp3HeaderValue(header->mutable_value());
888
889
3.96k
    if (header->name().size() > *remaining_bytes) {
890
1
      header->mutable_name()->resize(*remaining_bytes);
891
1
      header->clear_value();
892
3.96k
    } else if (header->value().size() > *remaining_bytes - header->name().size()) {
893
3
      header->mutable_value()->resize(*remaining_bytes - header->name().size());
894
3
    }
895
3.96k
    *remaining_bytes -= header->name().size() + header->value().size();
896
3.96k
    ++retained;
897
3.96k
  }
898
6.36k
  TrimRepeated(headers, retained);
899
6.36k
}
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
881
63.5k
void BoundHttp3Headers(RepeatedHeaders* headers, std::size_t count_limit, std::size_t* remaining_bytes) {
882
63.5k
  TrimRepeated(headers, count_limit);
883
63.5k
  std::size_t retained = 0;
884
150k
  while (retained < static_cast<std::size_t>(headers->size()) && *remaining_bytes != 0) {
885
86.7k
    auto* header = headers->Mutable(static_cast<int>(retained));
886
86.7k
    CanonicalizeHttp3HeaderName(header->mutable_name());
887
86.7k
    CanonicalizeHttp3HeaderValue(header->mutable_value());
888
889
86.7k
    if (header->name().size() > *remaining_bytes) {
890
1
      header->mutable_name()->resize(*remaining_bytes);
891
1
      header->clear_value();
892
86.7k
    } else if (header->value().size() > *remaining_bytes - header->name().size()) {
893
3
      header->mutable_value()->resize(*remaining_bytes - header->name().size());
894
3
    }
895
86.7k
    *remaining_bytes -= header->name().size() + header->value().size();
896
86.7k
    ++retained;
897
86.7k
  }
898
63.5k
  TrimRepeated(headers, retained);
899
63.5k
}
900
901
1.83k
void CanonicalizeHttp3StreamRole(curl::fuzzer::proto::Http3StreamRole* role) {
902
1.83k
  switch (*role) {
903
1.66k
    case curl::fuzzer::proto::HTTP3_STREAM_RESPONSE:
904
1.70k
    case curl::fuzzer::proto::HTTP3_STREAM_CONTROL:
905
1.73k
    case curl::fuzzer::proto::HTTP3_STREAM_QPACK_ENCODER:
906
1.75k
    case curl::fuzzer::proto::HTTP3_STREAM_QPACK_DECODER:
907
1.75k
      return;
908
86
    default:
909
86
      *role = curl::fuzzer::proto::HTTP3_STREAM_RESPONSE;
910
86
      return;
911
1.83k
  }
912
1.83k
}
913
914
1.97k
void BoundHttp3RawData(std::string* data, std::size_t* remaining_raw_bytes) {
915
1.97k
  const std::size_t limit = std::min(scenario_limits::kMaxHttp3RawWriteBytes, *remaining_raw_bytes);
916
1.97k
  if (data->size() > limit) {
917
11
    data->resize(limit);
918
11
  }
919
1.97k
  *remaining_raw_bytes -= data->size();
920
1.97k
}
921
922
114k
void BoundHttp2StreamRef(curl::fuzzer::proto::Http2StreamRef* stream) {
923
114k
  switch (stream->target_case()) {
924
5.88k
    case curl::fuzzer::proto::Http2StreamRef::kRequestIndex:
925
5.88k
      stream->set_request_index(stream->request_index() % 16U);
926
5.88k
      break;
927
4.92k
    case curl::fuzzer::proto::Http2StreamRef::kPushIndex:
928
4.92k
      stream->set_push_index(stream->push_index() % 16U);
929
4.92k
      break;
930
5.33k
    case curl::fuzzer::proto::Http2StreamRef::kExplicitId:
931
5.33k
      stream->set_explicit_id(stream->explicit_id() & 0x7fffffffU);
932
5.33k
      break;
933
3.46k
    case curl::fuzzer::proto::Http2StreamRef::kConnection:
934
5.52k
    case curl::fuzzer::proto::Http2StreamRef::kLatestRequest:
935
98.0k
    case curl::fuzzer::proto::Http2StreamRef::TARGET_NOT_SET:
936
98.0k
    default:
937
98.0k
      break;
938
114k
  }
939
114k
}
940
941
45.4k
void BoundHttp2Settings(curl::fuzzer::proto::Http2Settings* settings) {
942
45.4k
  TrimRepeated(settings->mutable_entries(), scenario_limits::kMaxHttp2Settings);
943
45.4k
  for (auto& entry : *settings->mutable_entries()) {
944
    // SETTINGS identifiers occupy 16 bits. Preserve unknown extensions,
945
    // which receivers ignore when they do not understand them.
946
31.2k
    entry.set_identifier(entry.identifier() & 0xffffU);
947
31.2k
    switch (entry.identifier()) {
948
2.80k
      case 2U:  // SETTINGS_ENABLE_PUSH
949
3.41k
      case 8U:  // SETTINGS_ENABLE_CONNECT_PROTOCOL
950
4.03k
      case 9U:  // SETTINGS_NO_RFC7540_PRIORITIES
951
4.03k
        entry.set_value(entry.value() & 1U);
952
4.03k
        break;
953
5.96k
      case 4U:  // SETTINGS_INITIAL_WINDOW_SIZE
954
5.96k
        entry.set_value(entry.value() & 0x7fffffffU);
955
5.96k
        break;
956
2.49k
      case 5U:  // SETTINGS_MAX_FRAME_SIZE
957
2.49k
        if (entry.value() < 16384U || entry.value() > 0x00ffffffU) {
958
36
          entry.set_value(16384U + entry.value() % (0x00ffffffU - 16384U + 1U));
959
36
        }
960
2.49k
        break;
961
4.57k
      case 1U:  // SETTINGS_HEADER_TABLE_SIZE
962
7.48k
      case 3U:  // SETTINGS_MAX_CONCURRENT_STREAMS
963
9.02k
      case 6U:  // SETTINGS_MAX_HEADER_LIST_SIZE
964
18.7k
      default:
965
18.7k
        break;
966
31.2k
    }
967
31.2k
  }
968
45.4k
}
969
970
24.8k
void BoundHttp2RawData(std::string* data, std::size_t* remaining_raw_bytes) {
971
24.8k
  const std::size_t limit = std::min(scenario_limits::kMaxHttp2RawFrameBytes, *remaining_raw_bytes);
972
24.8k
  if (data->size() > limit) {
973
4
    data->resize(limit);
974
4
  }
975
24.8k
  *remaining_raw_bytes -= data->size();
976
24.8k
}
977
978
/// Bound the structured HTTP/2 sequence to the exact prefix interpreted by
979
/// H2PlanDriver. The raw escapes retain malformed frame behavior while every
980
/// ordinary header action remains HPACK and HTTP-field valid.
981
32.1k
void BoundHttp2PlanShape(curl::fuzzer::proto::Http2Plan* plan) {
982
32.1k
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxHttp2Actions);
983
32.1k
  BoundHttp2Settings(plan->mutable_initial_settings());
984
32.1k
  plan->mutable_initial_settings()->set_ack(false);
985
32.1k
  if (plan->stream_weight_after_headers() > 256U) {
986
3
    plan->set_stream_weight_after_headers(1U + (plan->stream_weight_after_headers() - 1U) % 256U);
987
3
  }
988
32.1k
  if (plan->actions().empty()) {
989
15
    auto* response = plan->add_actions()->mutable_headers();
990
15
    response->set_status_code(200);
991
15
    response->set_end_stream(true);
992
15
  }
993
994
32.1k
  std::size_t remaining_header_bytes = scenario_limits::kMaxHttp2HeaderBytes;
995
32.1k
  std::size_t remaining_data_bytes = scenario_limits::kMaxHttp2DataBytes;
996
32.1k
  std::size_t remaining_raw_bytes = scenario_limits::kMaxHttp2RawBytes;
997
148k
  for (auto& action : *plan->mutable_actions()) {
998
148k
    switch (action.action_case()) {
999
13.3k
      case curl::fuzzer::proto::Http2Action::kSettings:
1000
13.3k
        BoundHttp2Settings(action.mutable_settings());
1001
13.3k
        break;
1002
41.5k
      case curl::fuzzer::proto::Http2Action::kHeaders: {
1003
41.5k
        auto* headers = action.mutable_headers();
1004
41.5k
        BoundHttp2StreamRef(headers->mutable_stream());
1005
41.5k
        if (!headers->trailers()) {
1006
39.9k
          if (headers->status_code() == 0U) {
1007
493
            headers->set_status_code(200U);
1008
39.4k
          } else if (headers->status_code() < 100U || headers->status_code() > 599U) {
1009
123
            headers->set_status_code(100U + headers->status_code() % 500U);
1010
123
          }
1011
39.9k
        }
1012
41.5k
        BoundHttp3Headers(headers->mutable_fields(), scenario_limits::kMaxHttp2Headers, &remaining_header_bytes);
1013
41.5k
        break;
1014
0
      }
1015
5.24k
      case curl::fuzzer::proto::Http2Action::kData: {
1016
5.24k
        auto* data = action.mutable_data();
1017
5.24k
        BoundHttp2StreamRef(data->mutable_stream());
1018
5.24k
        if (data->data().size() > remaining_data_bytes) {
1019
0
          data->mutable_data()->resize(remaining_data_bytes);
1020
0
        }
1021
5.24k
        remaining_data_bytes -= data->data().size();
1022
5.24k
        break;
1023
0
      }
1024
5.72k
      case curl::fuzzer::proto::Http2Action::kWindowUpdate: {
1025
5.72k
        auto* update = action.mutable_window_update();
1026
5.72k
        BoundHttp2StreamRef(update->mutable_stream());
1027
5.72k
        update->set_increment(std::max(1U, update->increment() & 0x7fffffffU));
1028
5.72k
        break;
1029
0
      }
1030
4.98k
      case curl::fuzzer::proto::Http2Action::kRstStream:
1031
4.98k
        BoundHttp2StreamRef(action.mutable_rst_stream()->mutable_stream());
1032
4.98k
        break;
1033
9.33k
      case curl::fuzzer::proto::Http2Action::kPing:
1034
9.33k
        if (action.ping().opaque_data().size() > 8U) {
1035
53
          action.mutable_ping()->mutable_opaque_data()->resize(8U);
1036
53
        }
1037
9.33k
        break;
1038
7.68k
      case curl::fuzzer::proto::Http2Action::kGoaway:
1039
7.68k
        BoundHttp2StreamRef(action.mutable_goaway()->mutable_last_stream());
1040
7.68k
        BoundHttp2RawData(action.mutable_goaway()->mutable_debug_data(), &remaining_raw_bytes);
1041
7.68k
        break;
1042
21.9k
      case curl::fuzzer::proto::Http2Action::kPushPromise:
1043
21.9k
        BoundHttp2StreamRef(action.mutable_push_promise()->mutable_parent_stream());
1044
21.9k
        BoundHttp2StreamRef(action.mutable_push_promise()->mutable_promised_stream());
1045
21.9k
        BoundHttp3Headers(action.mutable_push_promise()->mutable_request_headers(), scenario_limits::kMaxHttp2Headers,
1046
21.9k
                          &remaining_header_bytes);
1047
21.9k
        break;
1048
5.09k
      case curl::fuzzer::proto::Http2Action::kWait: {
1049
5.09k
        auto* wait = action.mutable_wait();
1050
5.09k
        BoundHttp2StreamRef(wait->mutable_stream());
1051
5.09k
        if (wait->event() < curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS ||
1052
5.08k
            wait->event() > curl::fuzzer::proto::HTTP2_CLIENT_EVENT_END_STREAM) {
1053
134
          wait->set_event(curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS);
1054
134
        }
1055
5.09k
        wait->set_count(std::max(1U, std::min(wait->count(), 16U)));
1056
5.09k
        break;
1057
0
      }
1058
16.1k
      case curl::fuzzer::proto::Http2Action::kYieldTurns:
1059
16.1k
        action.set_yield_turns(std::min<std::uint32_t>(action.yield_turns(), scenario_limits::kMaxHttp2YieldTurns));
1060
16.1k
        break;
1061
12.4k
      case curl::fuzzer::proto::Http2Action::kWireFrame:
1062
12.4k
        action.mutable_wire_frame()->set_type(action.wire_frame().type() & 0xffU);
1063
12.4k
        action.mutable_wire_frame()->set_flags(action.wire_frame().flags() & 0xffU);
1064
12.4k
        action.mutable_wire_frame()->set_stream_id(action.wire_frame().stream_id() & 0x7fffffffU);
1065
12.4k
        action.mutable_wire_frame()->set_declared_length(action.wire_frame().declared_length() & 0x00ffffffU);
1066
12.4k
        BoundHttp2RawData(action.mutable_wire_frame()->mutable_payload(), &remaining_raw_bytes);
1067
12.4k
        break;
1068
4.71k
      case curl::fuzzer::proto::Http2Action::kRawBytes:
1069
4.71k
        BoundHttp2RawData(action.mutable_raw_bytes(), &remaining_raw_bytes);
1070
4.71k
        break;
1071
717
      case curl::fuzzer::proto::Http2Action::ACTION_NOT_SET: {
1072
717
        auto* response = action.mutable_headers();
1073
717
        response->set_status_code(200);
1074
717
        response->set_end_stream(true);
1075
717
        break;
1076
0
      }
1077
148k
    }
1078
148k
  }
1079
32.1k
}
1080
1081
/// Canonicalize one ordered H3 script to the exact bounded prefix that the
1082
/// QUIC peer can execute. Transport setup remains peer-owned; only plaintext
1083
/// HTTP/3 operations are mutation-controlled here.
1084
13.3k
void BoundHttp3PlanShape(curl::fuzzer::proto::Http3Plan* plan) {
1085
13.3k
  if (plan->use_h1_connect_udp_proxy()) {
1086
7.93k
    plan->clear_actions();
1087
7.93k
    return;
1088
7.93k
  }
1089
5.42k
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxHttp3Actions);
1090
5.42k
  if (plan->actions().empty()) {
1091
619
    auto* response = plan->add_actions()->mutable_structured_response();
1092
619
    response->set_status_code(200);
1093
619
    response->set_finish_stream(true);
1094
619
  }
1095
1096
5.42k
  std::size_t remaining_header_bytes = scenario_limits::kMaxHttp3HeaderBytes;
1097
5.42k
  std::size_t remaining_body_bytes = scenario_limits::kMaxHttp3BodyBytes;
1098
5.42k
  std::size_t remaining_raw_bytes = scenario_limits::kMaxHttp3RawBytes;
1099
5.42k
  std::size_t retained_actions = 0;
1100
5.42k
  bool connection_closed = false;
1101
8.45k
  for (auto& action : *plan->mutable_actions()) {
1102
8.45k
    if (connection_closed) {
1103
37
      break;
1104
37
    }
1105
8.41k
    ++retained_actions;
1106
8.41k
    switch (action.action_case()) {
1107
3.18k
      case curl::fuzzer::proto::Http3Action::kStructuredResponse: {
1108
3.18k
        auto* response = action.mutable_structured_response();
1109
3.18k
        if (response->status_code() == 0U) {
1110
114
          response->set_status_code(200U);
1111
3.06k
        } else if (response->status_code() < 100U || response->status_code() > 599U) {
1112
119
          response->set_status_code(100U + response->status_code() % 500U);
1113
119
        }
1114
3.18k
        BoundHttp3Headers(response->mutable_response_headers(), scenario_limits::kMaxHttp3Headers,
1115
3.18k
                          &remaining_header_bytes);
1116
3.18k
        BoundHttp3Headers(response->mutable_response_trailers(), scenario_limits::kMaxHttp3Trailers,
1117
3.18k
                          &remaining_header_bytes);
1118
3.18k
        TrimRepeated(response->mutable_body_chunks(), scenario_limits::kMaxHttp3BodyChunks);
1119
3.18k
        for (std::string& chunk : *response->mutable_body_chunks()) {
1120
2.20k
          if (chunk.size() > remaining_body_bytes) {
1121
20
            chunk.resize(remaining_body_bytes);
1122
20
          }
1123
2.20k
          remaining_body_bytes -= chunk.size();
1124
2.20k
        }
1125
3.18k
        break;
1126
0
      }
1127
1128
1.06k
      case curl::fuzzer::proto::Http3Action::kStreamWrite: {
1129
1.06k
        auto* write = action.mutable_stream_write();
1130
1.06k
        auto role = write->role();
1131
1.06k
        CanonicalizeHttp3StreamRole(&role);
1132
1.06k
        write->set_role(role);
1133
1.06k
        BoundHttp3RawData(write->mutable_data(), &remaining_raw_bytes);
1134
1.06k
        break;
1135
0
      }
1136
1137
914
      case curl::fuzzer::proto::Http3Action::kOpenUnidirectionalStream: {
1138
914
        auto* stream = action.mutable_open_unidirectional_stream();
1139
914
        BoundHttp3RawData(stream->mutable_data(), &remaining_raw_bytes);
1140
914
        break;
1141
0
      }
1142
1143
772
      case curl::fuzzer::proto::Http3Action::kStreamReset: {
1144
772
        auto* reset = action.mutable_stream_reset();
1145
772
        auto role = reset->role();
1146
772
        CanonicalizeHttp3StreamRole(&role);
1147
772
        reset->set_role(role);
1148
772
        reset->set_application_error_code(reset->application_error_code() & scenario_limits::kMaxQuicVarint);
1149
772
        break;
1150
0
      }
1151
1152
1.03k
      case curl::fuzzer::proto::Http3Action::kGoaway:
1153
1.03k
        action.mutable_goaway()->set_id(action.goaway().id() & scenario_limits::kMaxQuicVarint & ~std::uint64_t{3});
1154
1.03k
        break;
1155
1156
1.28k
      case curl::fuzzer::proto::Http3Action::kConnectionClose:
1157
1.28k
        action.mutable_connection_close()->set_application_error_code(
1158
1.28k
            action.connection_close().application_error_code() & scenario_limits::kMaxQuicVarint);
1159
1.28k
        connection_closed = true;
1160
1.28k
        break;
1161
1162
165
      case curl::fuzzer::proto::Http3Action::ACTION_NOT_SET: {
1163
165
        auto* response = action.mutable_structured_response();
1164
165
        response->set_status_code(200);
1165
165
        response->set_finish_stream(true);
1166
165
        break;
1167
0
      }
1168
8.41k
    }
1169
8.41k
  }
1170
5.42k
  TrimRepeated(plan->mutable_actions(), retained_actions);
1171
5.42k
}
1172
1173
/// Canonicalize all shape limits enforced by the runtime. This runs only in
1174
/// fixed policy targets; the compatibility binary deliberately retains its
1175
/// historical no-postprocessor semantics for existing OSS-Fuzz reproducers.
1176
457k
void BoundScenarioShape(curl::fuzzer::proto::Scenario* scenario) {
1177
457k
  TrimRepeated(scenario->mutable_options(), scenario_limits::kMaxOptions);
1178
1179
457k
  BoundHeaderValues(scenario->mutable_request_headers(), scenario_limits::kMaxRequestHeaders);
1180
457k
  BoundStringValues(scenario->mutable_telnet_options(), scenario_limits::kMaxTelnetOptions,
1181
457k
                    scenario_limits::kMaxTelnetOptionBytes);
1182
457k
  if (scenario->has_mime_post()) {
1183
73.8k
    BoundMimeShape(scenario->mutable_mime_post());
1184
73.8k
  }
1185
457k
  if (scenario->has_upload()) {
1186
42.2k
    const bool telnet = scenario->scheme() == curl::fuzzer::proto::SCHEME_TELNET;
1187
42.2k
    const std::size_t data_limit = telnet ? scenario_limits::kMaxTelnetUploadBytes : scenario_limits::kMaxUploadBytes;
1188
42.2k
    const std::size_t read_step_limit =
1189
42.2k
        telnet ? scenario_limits::kMaxTelnetUploadReadSteps : scenario_limits::kMaxUploadReadSteps;
1190
42.2k
    const std::size_t read_size_limit =
1191
42.2k
        telnet ? scenario_limits::kMaxTelnetUploadReadSize : scenario_limits::kMaxUploadReadSize;
1192
42.2k
    BoundUploadShape(scenario->mutable_upload(), data_limit, read_step_limit, read_size_limit);
1193
42.2k
  }
1194
1195
457k
  BoundConnectionShape(scenario->mutable_connection());
1196
457k
  TrimRepeated(scenario->mutable_subsequent_connections(), scenario_limits::kMaxConnections - 1);
1197
457k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
1198
68.9k
    BoundConnectionShape(&connection);
1199
68.9k
  }
1200
457k
  BoundStringValues(scenario->mutable_mail_rcpt(), scenario_limits::kMaxMailRecipients,
1201
457k
                    scenario_limits::kMaxMailRecipientBytes);
1202
457k
  BoundFileInputs(scenario);
1203
457k
}
1204
1205
/// Apply schema-declared variant guards after lane selectors are normalized,
1206
/// then impose the shared work bounds. Static membership has already been
1207
/// enforced by the generated profile message type.
1208
void PruneAndBoundScenarioShape(curl::fuzzer::proto::Scenario* scenario, TargetProfile profile,
1209
457k
                                ConditionalEligibilityPruner prune_conditional_eligibility) {
1210
457k
  if (prune_conditional_eligibility != nullptr) {
1211
457k
    prune_conditional_eligibility(scenario, profile);
1212
457k
  }
1213
457k
  BoundScenarioShape(scenario);
1214
457k
}
1215
1216
/// Fold malformed deep-HTTP route selectors to their zero-valued direct
1217
/// modes before generated guards evaluate dependent routing fields.
1218
25.3k
void CanonicalizeDeepHttpSelectors(curl::fuzzer::proto::Scenario* scenario) {
1219
25.3k
  switch (scenario->http_transport_mode()) {
1220
23.1k
    case curl::fuzzer::proto::HTTP_TRANSPORT_DIRECT:
1221
23.6k
    case curl::fuzzer::proto::HTTP_TRANSPORT_FORWARD_PROXY:
1222
24.9k
    case curl::fuzzer::proto::HTTP_TRANSPORT_CONNECT_PROXY:
1223
25.3k
    case curl::fuzzer::proto::HTTP_TRANSPORT_UNIX_SOCKET:
1224
25.3k
      break;
1225
3
    default:
1226
3
      scenario->clear_http_transport_mode();
1227
3
      break;
1228
25.3k
  }
1229
25.3k
  switch (scenario->proxy_socket_option_mode()) {
1230
21.9k
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_DEFAULT:
1231
22.3k
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_NOPROXY_HOSTNAME:
1232
23.1k
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_NOPROXY_IPV4_CIDR:
1233
24.7k
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_NOPROXY_IPV6_CIDR:
1234
25.2k
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_ENV_HTTP_PROXY:
1235
25.3k
    case curl::fuzzer::proto::PROXY_SOCKET_OPTION_LOOPBACK_INTERFACE_KEEPALIVE:
1236
25.3k
      break;
1237
1
    default:
1238
1
      scenario->clear_proxy_socket_option_mode();
1239
1
      break;
1240
25.3k
  }
1241
25.3k
}
1242
1243
/// Decode an integral oneof locally before the generated option canonicalizer
1244
/// runs. Protocol mode bounds are policy, not setopt mechanics: folding them
1245
/// here keeps nearly every mutation on a real FTP/TFTP state while the shared
1246
/// option layer remains unaware of protocol-specific numeric ranges.
1247
26.4k
std::uint64_t IntegralMutationValue(const curl::fuzzer::proto::SetOption& option) {
1248
26.4k
  switch (option.value_case()) {
1249
26.4k
    case curl::fuzzer::proto::SetOption::kUintValue:
1250
26.4k
      return option.uint_value();
1251
0
    case curl::fuzzer::proto::SetOption::kBoolValue:
1252
0
      return option.bool_value() ? 1U : 0U;
1253
0
    case curl::fuzzer::proto::SetOption::kStringValue:
1254
0
    case curl::fuzzer::proto::SetOption::VALUE_NOT_SET:
1255
0
      return 0;
1256
26.4k
  }
1257
0
  return 0;
1258
26.4k
}
1259
1260
/// Bound the synchronous local-port search for both fixed and compatibility
1261
/// execution copies. The persistent compatibility input remains unchanged.
1262
698k
void CanonicalizeLocalPortRange(curl::fuzzer::proto::Scenario* scenario) {
1263
1.87M
  for (auto& option : *scenario->mutable_options()) {
1264
1.87M
    if (option.option_id() == curl::fuzzer::proto::CURLOPT_LOCALPORTRANGE) {
1265
4.06k
      option.set_uint_value(std::min<std::uint64_t>(IntegralMutationValue(option), kMaxLocalPortRange));
1266
4.06k
    }
1267
1.87M
  }
1268
698k
}
1269
1270
/// Keep prior-knowledge HTTP/2 and HTTP/3 out of the throughput-oriented HTTP
1271
/// lane. HTTP/1 modes and cleartext HTTP/2 upgrade remain here because h2c's
1272
/// HTTP/1 request and 101 transition are distinct from the dedicated target's
1273
/// prior-knowledge transport.
1274
42.9k
void CanonicalizeFastHttpVersion(curl::fuzzer::proto::Scenario* scenario) {
1275
53.8k
  for (auto& option : *scenario->mutable_options()) {
1276
53.8k
    if (option.option_id() != curl::fuzzer::proto::CURLOPT_HTTP_VERSION) {
1277
39.2k
      continue;
1278
39.2k
    }
1279
14.6k
    const std::uint64_t version = IntegralMutationValue(option);
1280
    // 2TLS remains HTTP/1.1 because this target fixes the scheme to HTTP.
1281
14.6k
    if (version <= CURL_HTTP_VERSION_2TLS) {
1282
14.6k
      option.set_uint_value(version);
1283
14.6k
    } else {
1284
49
      option.set_uint_value(CURL_HTTP_VERSION_NONE);
1285
49
    }
1286
14.6k
  }
1287
42.9k
}
1288
1289
/// Fold small FTP enums onto curl's documented domains so random uint64 values
1290
/// do not overwhelmingly stop at setopt validation before issuing a command.
1291
12.2k
void CanonicalizeFtpOptionModes(curl::fuzzer::proto::Scenario* scenario) {
1292
19.2k
  for (auto& option : *scenario->mutable_options()) {
1293
19.2k
    switch (option.option_id()) {
1294
204
      case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
1295
204
        option.set_uint_value(IntegralMutationValue(option) % 3U);
1296
204
        break;
1297
246
      case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
1298
246
        option.set_uint_value(IntegralMutationValue(option) % 4U);
1299
246
        break;
1300
691
      case curl::fuzzer::proto::CURLOPT_FTPPORT:
1301
        // Never let a mutated active-mode address resolve or bind outside the
1302
        // process. Empty remains reachable only by omitting the option.
1303
691
        option.set_string_value("127.0.0.1");
1304
691
        break;
1305
583
      case curl::fuzzer::proto::CURLOPT_FTP_USE_EPRT:
1306
583
        option.set_bool_value(IntegralMutationValue(option) != 0U);
1307
583
        break;
1308
17.4k
      default:
1309
17.4k
        break;
1310
19.2k
    }
1311
19.2k
  }
1312
12.2k
}
1313
1314
/// TFTP accepts block sizes from 8 through 65464. Mapping zero or a mismatched
1315
/// oneof to the default 512 preserves a common valid request, while saturating
1316
/// other values retains both lower/upper parser boundaries under mutation.
1317
4.47k
void CanonicalizeTftpOptionModes(curl::fuzzer::proto::Scenario* scenario) {
1318
12.5k
  for (auto& option : *scenario->mutable_options()) {
1319
12.5k
    if (option.option_id() != curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE) {
1320
11.0k
      continue;
1321
11.0k
    }
1322
1.49k
    std::uint64_t value = IntegralMutationValue(option);
1323
1.49k
    if (value == 0) {
1324
375
      value = 512;
1325
375
    }
1326
1.49k
    option.set_uint_value(std::max<std::uint64_t>(8, std::min<std::uint64_t>(value, 65464)));
1327
1.49k
  }
1328
4.47k
}
1329
1330
/// Fold the RTSP request selector onto curl's documented 1..11 domain so
1331
/// random uint64 mutations still issue a method instead of failing setopt
1332
/// validation. A zero or out-of-range value becomes the default OPTIONS.
1333
/// Sequence overrides are clamped to curl's int range; zero remains meaningful
1334
/// because curl rewrites it to its default on connect.
1335
12.6k
void CanonicalizeRtspOptionModes(curl::fuzzer::proto::Scenario* scenario) {
1336
33.3k
  for (auto& option : *scenario->mutable_options()) {
1337
33.3k
    switch (option.option_id()) {
1338
3.62k
      case curl::fuzzer::proto::CURLOPT_RTSP_REQUEST: {
1339
3.62k
        const std::uint64_t value = IntegralMutationValue(option);
1340
        // RECEIVE sends no request and instead waits for server-initiated
1341
        // stream data, which the request-aligned peer never emits. Fold it
1342
        // onto the default OPTIONS like zero and out-of-range values so every
1343
        // retained request still issues a scripted exchange.
1344
3.62k
        const bool supported =
1345
3.62k
            value >= CURL_RTSPREQ_OPTIONS && value < CURL_RTSPREQ_LAST && value != CURL_RTSPREQ_RECEIVE;
1346
3.62k
        option.set_uint_value(supported ? value : CURL_RTSPREQ_OPTIONS);
1347
3.62k
        break;
1348
0
      }
1349
870
      case curl::fuzzer::proto::CURLOPT_RTSP_CLIENT_CSEQ:
1350
1.57k
      case curl::fuzzer::proto::CURLOPT_RTSP_SERVER_CSEQ:
1351
1.57k
        option.set_uint_value(std::min<std::uint64_t>(IntegralMutationValue(option),
1352
1.57k
                                                      static_cast<std::uint64_t>(std::numeric_limits<int>::max())));
1353
1.57k
        break;
1354
28.1k
      default:
1355
28.1k
        break;
1356
33.3k
    }
1357
33.3k
  }
1358
12.6k
}
1359
1360
/// Remove the stateful shapes assigned to the deep HTTP target. This happens
1361
/// before BoundScenarioShape so a fast iteration never walks or normalizes a
1362
/// MIME tree, upload script, or follow-on connection that it will discard.
1363
/// Raw response chunks and request headers stay intact because they reach the
1364
/// core HTTP parser cheaply and provide much of the legacy fuzzer's coverage.
1365
74.6k
void RemoveDeepHttpShape(curl::fuzzer::proto::Scenario* scenario) {
1366
74.6k
  scenario->clear_mime_post();
1367
74.6k
  scenario->clear_upload();
1368
74.6k
  scenario->clear_subsequent_connections();
1369
1370
74.6k
  auto* connection = scenario->mutable_connection();
1371
74.6k
  connection->clear_server_frames();
1372
74.6k
  connection->clear_manual_probes();
1373
74.6k
  connection->clear_backpressure();
1374
74.6k
}
1375
1376
/// Remove response forms the proxy peer cannot interpret. Raw chunks are the
1377
/// HTTP/2 frame stream; WebSocket frames would merely add a second unrelated
1378
/// binary grammar, and follow-on Connection messages cannot describe later
1379
/// streams multiplexed on the already-open proxy socket.
1380
74.9k
void RemoveIgnoredH2ProxyShape(curl::fuzzer::proto::Scenario* scenario) {
1381
74.9k
  scenario->clear_subsequent_connections();
1382
74.9k
  auto* connection = scenario->mutable_connection();
1383
74.9k
  connection->clear_server_frames();
1384
74.9k
  connection->clear_manual_probes();
1385
74.9k
  connection->clear_backpressure();
1386
74.9k
}
1387
1388
/// Remove fields the single-socket WebSocket driver cannot consume. MIME also
1389
/// changes the HTTP request away from a useful Upgrade handshake, so retaining
1390
/// either shape in fixed WS lanes gives LPM mutation work with no WS coverage
1391
/// payoff. The mixed compatibility target has no postprocessor and keeps its
1392
/// historical behavior.
1393
37.5k
void RemoveIgnoredWebSocketShape(curl::fuzzer::proto::Scenario* scenario) {
1394
37.5k
  scenario->clear_subsequent_connections();
1395
37.5k
  scenario->clear_mime_post();
1396
37.5k
}
1397
1398
/// Remove fields whose only effect in a TELNET lane would be protobuf work or
1399
/// unsafe socket timing. TELNET's curl driver owns the thread until the peer
1400
/// closes, so response backpressure and follow-on sockets cannot be serviced
1401
/// by the outer event loop. Raw response fragments and the bounded upload stay
1402
/// mutation-controlled because the dedicated mock can preload and drain them.
1403
8.81k
void RemoveNonTelnetShape(curl::fuzzer::proto::Scenario* scenario) {
1404
8.81k
  scenario->clear_subsequent_connections();
1405
8.81k
  scenario->clear_request_headers();
1406
8.81k
  scenario->clear_mime_post();
1407
1408
8.81k
  auto* connection = scenario->mutable_connection();
1409
8.81k
  connection->clear_server_frames();
1410
8.81k
  connection->clear_manual_probes();
1411
8.81k
  connection->clear_backpressure();
1412
8.81k
}
1413
1414
/// Remove the TELNET-only list and pause outcome from fixed event-driven
1415
/// targets. The compatibility target skips postprocessing, so the runtime
1416
/// repeats the pause-to-EOF guard before installing callbacks.
1417
448k
void RemoveTelnetOnlyShape(curl::fuzzer::proto::Scenario* scenario) {
1418
448k
  scenario->clear_telnet_options();
1419
448k
  if (scenario->has_upload() && scenario->upload().terminal() == curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE) {
1420
24
    scenario->mutable_upload()->set_terminal(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF);
1421
24
  }
1422
448k
}
1423
1424
/// Keep lifecycle work out of protocol-focused lanes. The API binary retains
1425
/// this message explicitly; compatibility inputs have no postprocessor so
1426
/// existing reproducers keep their historical serialized meaning.
1427
434k
void RemoveApiOnlyShape(curl::fuzzer::proto::Scenario* scenario) { scenario->clear_api_plan(); }
1428
1429
/// Keep concurrent multi-handle work out of every other fixed lane. The
1430
/// compatibility binary deliberately preserves newly-added unknown fields.
1431
433k
void RemoveMultiOnlyShape(curl::fuzzer::proto::Scenario* scenario) { scenario->clear_multi_plan(); }
1432
1433
/// The QUIC peer consumes Http3Plan rather than the stream-socket response
1434
/// script. Request headers, MIME, upload state, and HTTP options remain useful
1435
/// because curl serializes those onto its client-initiated request stream.
1436
13.3k
void RemoveIgnoredHttp3Shape(curl::fuzzer::proto::Scenario* scenario) {
1437
13.3k
  if (!scenario->http3_plan().use_h1_connect_udp_proxy()) {
1438
5.42k
    scenario->clear_connection();
1439
5.42k
  }
1440
13.3k
  scenario->clear_subsequent_connections();
1441
13.3k
  RemoveTelnetOnlyShape(scenario);
1442
13.3k
  RemoveApiOnlyShape(scenario);
1443
13.3k
  RemoveMultiOnlyShape(scenario);
1444
13.3k
}
1445
1446
/// Remove stream-driver controls that neither file-transfer peer interprets.
1447
/// FTP consumes raw byte chunks as control/data replies, while TFTP preserves
1448
/// them as individual datagrams; structured WebSocket frames, manual probes,
1449
/// and event-loop backpressure therefore cannot affect either curl protocol.
1450
105k
void RemoveUnusedFileTransferConnectionShape(curl::fuzzer::proto::Connection* connection) {
1451
105k
  connection->clear_server_frames();
1452
105k
  connection->clear_manual_probes();
1453
105k
  connection->clear_backpressure();
1454
105k
}
1455
1456
/// Retain only the reusable shapes consumed by the FTP peer: one raw control
1457
/// script, a bounded sequence of passive-data scripts, and optional upload
1458
/// input. HTTP, TELNET, WebSocket, and public-API fields would otherwise absorb
1459
/// mutations despite having no representation in an FTP exchange.
1460
12.2k
void RemoveIgnoredFtpShape(curl::fuzzer::proto::Scenario* scenario) {
1461
12.2k
  scenario->clear_request_headers();
1462
12.2k
  scenario->clear_mime_post();
1463
12.2k
  RemoveTelnetOnlyShape(scenario);
1464
12.2k
  RemoveApiOnlyShape(scenario);
1465
12.2k
  RemoveMultiOnlyShape(scenario);
1466
1467
12.2k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1468
12.2k
  TrimRepeated(scenario->mutable_subsequent_connections(), scenario_limits::kMaxConnections - 1);
1469
12.2k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
1470
7.64k
    RemoveUnusedFileTransferConnectionShape(&connection);
1471
7.64k
  }
1472
12.2k
}
1473
1474
/// Retain the primary raw response script because its entries are the ordered
1475
/// UDP datagrams seen by curl, plus optional upload input for WRQ. TFTP cannot
1476
/// consume follow-on stream connections or any higher-level protocol shape.
1477
4.47k
void RemoveIgnoredTftpShape(curl::fuzzer::proto::Scenario* scenario) {
1478
4.47k
  scenario->clear_subsequent_connections();
1479
4.47k
  scenario->clear_request_headers();
1480
4.47k
  scenario->clear_mime_post();
1481
4.47k
  RemoveTelnetOnlyShape(scenario);
1482
4.47k
  RemoveApiOnlyShape(scenario);
1483
4.47k
  RemoveMultiOnlyShape(scenario);
1484
4.47k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1485
4.47k
}
1486
1487
/// Retain the single command-aligned control script and optional upload body
1488
/// consumed by the SMTP peer. SMTP cannot open follow-on sockets, and HTTP
1489
/// headers, TELNET negotiation, API plans, and stream-only connection controls
1490
/// have no representation in its exchange. MIME remains meaningful because
1491
/// curl can serialize it as the DATA body.
1492
9.69k
void RemoveIgnoredSmtpShape(curl::fuzzer::proto::Scenario* scenario) {
1493
9.69k
  scenario->clear_subsequent_connections();
1494
9.69k
  scenario->clear_request_headers();
1495
9.69k
  RemoveTelnetOnlyShape(scenario);
1496
9.69k
  RemoveApiOnlyShape(scenario);
1497
9.69k
  RemoveMultiOnlyShape(scenario);
1498
9.69k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1499
9.69k
}
1500
1501
/// Retain the one command-aligned control script and optional APPEND body
1502
/// consumed by the IMAP peer. IMAP cannot open follow-on sockets, and HTTP
1503
/// headers and structured stream controls have no representation in its
1504
/// exchange. MIME remains meaningful because curl serializes it as an APPEND
1505
/// literal.
1506
10.2k
void RemoveIgnoredImapShape(curl::fuzzer::proto::Scenario* scenario) {
1507
10.2k
  scenario->clear_subsequent_connections();
1508
10.2k
  scenario->clear_request_headers();
1509
10.2k
  RemoveTelnetOnlyShape(scenario);
1510
10.2k
  RemoveApiOnlyShape(scenario);
1511
10.2k
  RemoveMultiOnlyShape(scenario);
1512
10.2k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1513
10.2k
}
1514
1515
/// Retain only the command-aligned control script. POP3 is a download-only
1516
/// protocol: upload state, MIME bodies, HTTP headers, and follow-on sockets
1517
/// cannot reach its state machine.
1518
7.64k
void RemoveIgnoredPop3Shape(curl::fuzzer::proto::Scenario* scenario) {
1519
7.64k
  scenario->clear_subsequent_connections();
1520
7.64k
  scenario->clear_request_headers();
1521
7.64k
  scenario->clear_mime_post();
1522
7.64k
  scenario->clear_upload();
1523
7.64k
  RemoveTelnetOnlyShape(scenario);
1524
7.64k
  RemoveApiOnlyShape(scenario);
1525
7.64k
  RemoveMultiOnlyShape(scenario);
1526
7.64k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1527
7.64k
}
1528
1529
15.1k
void RemoveIgnoredGopherShape(curl::fuzzer::proto::Scenario* scenario) {
1530
15.1k
  scenario->clear_subsequent_connections();
1531
15.1k
  scenario->clear_request_headers();
1532
15.1k
  scenario->clear_mime_post();
1533
15.1k
  scenario->clear_upload();
1534
15.1k
  RemoveTelnetOnlyShape(scenario);
1535
15.1k
  RemoveApiOnlyShape(scenario);
1536
15.1k
  RemoveMultiOnlyShape(scenario);
1537
15.1k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1538
15.1k
}
1539
1540
/// Retain only the command-aligned control script. DICT is a download-only
1541
/// protocol: upload state, MIME bodies, HTTP headers, and follow-on sockets
1542
/// cannot reach its state machine.
1543
6.42k
void RemoveIgnoredDictShape(curl::fuzzer::proto::Scenario* scenario) {
1544
6.42k
  scenario->clear_subsequent_connections();
1545
6.42k
  scenario->clear_request_headers();
1546
6.42k
  scenario->clear_mime_post();
1547
6.42k
  scenario->clear_upload();
1548
6.42k
  RemoveTelnetOnlyShape(scenario);
1549
6.42k
  RemoveApiOnlyShape(scenario);
1550
6.42k
  RemoveMultiOnlyShape(scenario);
1551
6.42k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1552
  // curl writes CLIENT, one lookup command, and QUIT in a single send, then
1553
  // reads an EOF-framed body. The peer consumes only the first non-CLIENT
1554
  // reply chunk and half-closes, so any later chunk is permanently dead work
1555
  // the mutator keeps rediscovering.
1556
6.42k
  TrimRepeated(scenario->mutable_connection()->mutable_on_readable(), 1);
1557
6.42k
}
1558
1559
/// Retain the one binary server script and the scalar publish payload. MQTT
1560
/// cannot open follow-on sockets, does not use HTTP headers or MIME, and takes
1561
/// its publish body from CURLOPT_POSTFIELDS rather than curl's read callback.
1562
/// The remaining connection fields (initial CONNACK plus server packets) are
1563
/// exactly what the packet-aware peer consumes.
1564
8.09k
void RemoveIgnoredMqttShape(curl::fuzzer::proto::Scenario* scenario) {
1565
8.09k
  scenario->clear_subsequent_connections();
1566
8.09k
  scenario->clear_request_headers();
1567
8.09k
  scenario->clear_mime_post();
1568
8.09k
  scenario->clear_upload();
1569
8.09k
  RemoveTelnetOnlyShape(scenario);
1570
8.09k
  RemoveApiOnlyShape(scenario);
1571
8.09k
  RemoveMultiOnlyShape(scenario);
1572
8.09k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1573
8.09k
}
1574
1575
/// FILE has no peer, so response scripts, connection controls, MIME, and
1576
/// HTTP/TELNET/API state are inert. Retain the bounded upload script because
1577
/// the fixed /dev/null URL makes curl's local write path safe to exercise.
1578
7.15k
void RemoveIgnoredFileShape(curl::fuzzer::proto::Scenario* scenario) {
1579
7.15k
  scenario->clear_connection();
1580
7.15k
  scenario->clear_subsequent_connections();
1581
7.15k
  scenario->clear_request_headers();
1582
7.15k
  scenario->clear_mime_post();
1583
7.15k
  RemoveTelnetOnlyShape(scenario);
1584
7.15k
  RemoveApiOnlyShape(scenario);
1585
7.15k
  RemoveMultiOnlyShape(scenario);
1586
7.15k
}
1587
1588
/// Retain the scripted bind reply and BER search-result messages consumed by
1589
/// the LDAP peer. LDAP cannot open follow-on sockets, and HTTP headers, MIME,
1590
/// upload state, and TELNET negotiation have no representation in its
1591
/// exchange. The primary connection's initial_response is the bind reply, so
1592
/// unlike the file-transfer and mail lanes it must survive pruning.
1593
11.5k
void RemoveIgnoredLdapShape(curl::fuzzer::proto::Scenario* scenario) {
1594
11.5k
  scenario->clear_subsequent_connections();
1595
11.5k
  scenario->clear_request_headers();
1596
11.5k
  scenario->clear_mime_post();
1597
11.5k
  scenario->clear_upload();
1598
11.5k
  RemoveTelnetOnlyShape(scenario);
1599
11.5k
  RemoveApiOnlyShape(scenario);
1600
11.5k
  RemoveMultiOnlyShape(scenario);
1601
11.5k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
1602
11.5k
}
1603
1604
/// Retain the request-aligned control script and the fields curl serializes
1605
/// into an RTSP request. Custom request headers, upload/postfields bodies for
1606
/// ANNOUNCE and SET_PARAMETER, and MIME-free options stay meaningful. RTSP has
1607
/// no server greeting, cannot consume follow-on stream scripts, and does not
1608
/// use MIME bodies, so those shapes are removed before general bounds.
1609
12.6k
void RemoveIgnoredRtspShape(curl::fuzzer::proto::Scenario* scenario) {
1610
12.6k
  scenario->clear_subsequent_connections();
1611
12.6k
  scenario->clear_mime_post();
1612
12.6k
  RemoveTelnetOnlyShape(scenario);
1613
12.6k
  RemoveApiOnlyShape(scenario);
1614
12.6k
  RemoveMultiOnlyShape(scenario);
1615
1616
12.6k
  auto* connection = scenario->mutable_connection();
1617
12.6k
  connection->clear_initial_response();
1618
12.6k
  RemoveUnusedFileTransferConnectionShape(connection);
1619
12.6k
}
1620
1621
/// Retain the shapes an ordinary gateway GET can consume. The IPFS/IPNS lane
1622
/// runs through the same HTTP mock as the fast HTTP lane, so raw response
1623
/// chunks, request headers, and HTTP options stay meaningful while MIME,
1624
/// uploads, structured frames, and follow-on sockets are removed.
1625
18.5k
void RemoveIgnoredIpfsShape(curl::fuzzer::proto::Scenario* scenario) {
1626
18.5k
  RemoveApiOnlyShape(scenario);
1627
18.5k
  RemoveMultiOnlyShape(scenario);
1628
18.5k
  RemoveTelnetOnlyShape(scenario);
1629
18.5k
  RemoveDeepHttpShape(scenario);
1630
18.5k
}
1631
1632
/// Preserve useful in-range mutations while folding ineffective extremes onto
1633
/// meaningful boundaries. Zero remains special: it disables that individual
1634
/// control and lets the other control provide the timing target's pressure.
1635
46.2k
std::uint32_t CanonicalizeNonZero(std::uint32_t value, std::uint32_t minimum, std::uint32_t maximum) {
1636
46.2k
  if (value == 0) {
1637
20.0k
    return 0;
1638
20.0k
  }
1639
26.2k
  return std::max(minimum, std::min(value, maximum));
1640
46.2k
}
1641
1642
/// Keep the timing target on a protocol the timing run mode can actually
1643
/// drive. TLS setup has its own cost profile and would obscure whether
1644
/// backpressure mutations are exploring curl's send/receive state machines
1645
/// effectively. Only WebSocket has a plaintext peer in kProtocolCoverage mode,
1646
/// so WSS folds onto WS; every other scheme — including the mail, MQTT, and
1647
/// file-transfer families whose peers are gated on their dedicated run modes —
1648
/// falls back to the always-served HTTP peer rather than leaving the runner
1649
/// without a transport.
1650
20.0k
curl::fuzzer::proto::Scheme PlaintextScheme(curl::fuzzer::proto::Scheme scheme) {
1651
20.0k
  switch (scheme) {
1652
4.52k
    case curl::fuzzer::proto::SCHEME_WS:
1653
4.53k
    case curl::fuzzer::proto::SCHEME_WSS:
1654
4.53k
      return curl::fuzzer::proto::SCHEME_WS;
1655
14.8k
    case curl::fuzzer::proto::SCHEME_HTTP:
1656
14.8k
    case curl::fuzzer::proto::SCHEME_HTTPS:
1657
14.8k
    case curl::fuzzer::proto::SCHEME_TELNET:
1658
14.8k
    case curl::fuzzer::proto::SCHEME_FTP:
1659
14.8k
    case curl::fuzzer::proto::SCHEME_TFTP:
1660
14.8k
    case curl::fuzzer::proto::SCHEME_DICT:
1661
14.8k
    case curl::fuzzer::proto::SCHEME_FILE:
1662
14.9k
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
1663
15.5k
    default:
1664
15.5k
      return curl::fuzzer::proto::SCHEME_HTTP;
1665
20.0k
  }
1666
20.0k
}
1667
1668
/// Remove timing controls from every connection the structured message can
1669
/// carry. Clearing only the primary script would let a mutated redirect turn a
1670
/// fixed fast lane into the timed drive loop after its second socket opens.
1671
160k
void ClearAllBackpressure(curl::fuzzer::proto::Scenario* scenario) {
1672
160k
  if (scenario->has_connection()) {
1673
160k
    scenario->mutable_connection()->clear_backpressure();
1674
160k
  }
1675
160k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
1676
53.0k
    connection.clear_backpressure();
1677
53.0k
  }
1678
160k
}
1679
1680
/// Clamp one explicitly pressure-bearing follow-on script to the same useful
1681
/// ranges as the timing lane's primary connection. An absent configuration is
1682
/// left absent so merely adding a redirect response does not add waits.
1683
7.83k
void CanonicalizeOptionalBackpressure(curl::fuzzer::proto::Connection* connection) {
1684
7.83k
  if (!connection->has_backpressure()) {
1685
4.40k
    return;
1686
4.40k
  }
1687
3.43k
  auto* backpressure = connection->mutable_backpressure();
1688
3.43k
  if (backpressure->recv_buf_bytes() == 0 && backpressure->drain_limit() != 0) {
1689
45
    backpressure->set_recv_buf_bytes(kDefaultBackpressureBufferBytes);
1690
3.38k
  } else {
1691
3.38k
    backpressure->set_recv_buf_bytes(
1692
3.38k
        CanonicalizeNonZero(backpressure->recv_buf_bytes(), kMinBackpressureBufferBytes, kMaxBackpressureBufferBytes));
1693
3.38k
  }
1694
3.43k
  backpressure->set_drain_limit(CanonicalizeNonZero(backpressure->drain_limit(), 1, kMaxDrainBytesPerIteration));
1695
3.43k
}
1696
1697
/// Canonicalize the fields that determine which server and drive-loop policy
1698
/// execute. Fast targets discard backpressure because one mutated non-zero
1699
/// scalar otherwise opts an ordinary input into hundreds of timed waits. The
1700
/// timing target does the inverse: it guarantees a non-default buffer setting
1701
/// so its CPU allocation remains focused on the intentionally slower paths.
1702
void ApplyLanePolicy(curl::fuzzer::proto::Scenario* scenario, TargetProfile profile,
1703
457k
                     ConditionalEligibilityPruner prune_conditional_eligibility) {
1704
457k
  if (scenario == nullptr) {
1705
0
    return;
1706
0
  }
1707
1708
457k
  if (profile == TargetProfile::kCompatibility) {
1709
    // The original target's existing corpus predates profile splitting. A
1710
    // no-op here makes the type safe to pass around while its binary continues
1711
    // to omit postprocessor registration altogether. The append-only FTP/FTPS
1712
    // and TFTP scheme values do not justify rewriting historical mixed-lane
1713
    // inputs.
1714
0
    return;
1715
0
  }
1716
1717
  // The nested selector belongs exclusively to the HTTPS CONNECT proxy lane.
1718
  // Compatibility returned above and therefore preserves accumulated inputs;
1719
  // every unrelated fixed lane drops work its peer cannot observe.
1720
457k
  if (profile != TargetProfile::kH2Proxy) {
1721
436k
    scenario->mutable_connection()->clear_proxy_protocol();
1722
436k
    for (auto& connection : *scenario->mutable_subsequent_connections()) {
1723
90.0k
      connection.clear_proxy_protocol();
1724
90.0k
    }
1725
436k
  }
1726
1727
  // MQTT is append-only as well. No other fixed lane may reinterpret one of
1728
  // its packet scripts, so clear the selector before the lane-specific early
1729
  // paths install their own scheme. Compatibility returned above and keeps
1730
  // historical unknown-field semantics.
1731
457k
  if (profile != TargetProfile::kFastMqtt && scenario->scheme() == curl::fuzzer::proto::SCHEME_MQTT) {
1732
8
    scenario->clear_scheme();
1733
8
  }
1734
1735
  // DICT is append-only too. Only the dedicated lane may reinterpret a lookup
1736
  // script; every other fixed lane clears the selector before installing its
1737
  // own scheme.
1738
457k
  if (profile != TargetProfile::kFastDict && scenario->scheme() == curl::fuzzer::proto::SCHEME_DICT) {
1739
11
    scenario->clear_scheme();
1740
11
  }
1741
1742
  // FILE is append-only as well. Other fixed lanes must not inherit a local
1743
  // filesystem selector from a corpus input; only the dedicated lane may run
1744
  // the peerless file:// path.
1745
457k
  if (profile != TargetProfile::kFastFile && scenario->scheme() == curl::fuzzer::proto::SCHEME_FILE) {
1746
36
    scenario->clear_scheme();
1747
36
  }
1748
1749
  // LDAP and LDAPS are append-only too. Only the fixed BER lane may turn one
1750
  // of their scripted bind/search exchanges into a live OpenLDAP session.
1751
457k
  if (profile != TargetProfile::kFastLdap && (scenario->scheme() == curl::fuzzer::proto::SCHEME_LDAP ||
1752
446k
                                              scenario->scheme() == curl::fuzzer::proto::SCHEME_LDAPS)) {
1753
20
    scenario->clear_scheme();
1754
20
  }
1755
1756
  // RTSP is append-only too. Keep its selector away from every fixed lane
1757
  // that cannot service a request-aligned RTSP exchange. Static option
1758
  // membership is carried by each generated profile descriptor.
1759
457k
  if (profile != TargetProfile::kFastRtsp && scenario->scheme() == curl::fuzzer::proto::SCHEME_RTSP) {
1760
13
    scenario->clear_scheme();
1761
13
  }
1762
  // FTPS is append-only too. Only the fixed FTP lane may turn one of its
1763
  // scenarios into a TLS-wrapped control/data exchange.
1764
457k
  if (profile != TargetProfile::kFastFtp && scenario->scheme() == curl::fuzzer::proto::SCHEME_FTPS) {
1765
21
    scenario->clear_scheme();
1766
21
  }
1767
1768
  // IPFS/IPNS are append-only as well. Their identifiers and gateway paths
1769
  // would be inert in every other fixed lane, so clear the selector before
1770
  // the lane-specific early paths install their own scheme.
1771
457k
  if (profile != TargetProfile::kFastIpfs && (scenario->scheme() == curl::fuzzer::proto::SCHEME_IPFS ||
1772
439k
                                              scenario->scheme() == curl::fuzzer::proto::SCHEME_IPNS)) {
1773
18
    scenario->clear_scheme();
1774
18
  }
1775
1776
  // Accepted server push adds a harness-owned easy handle and therefore
1777
  // belongs only in the fixed H2 origin lanes. Compatibility remains a no-op
1778
  // above so accumulated mixed corpus entries keep their wire meaning.
1779
457k
  if (profile != TargetProfile::kHttpsH2 && profile != TargetProfile::kFastHttp2) {
1780
404k
    scenario->clear_accept_h2_push();
1781
404k
  }
1782
1783
  // Only the dedicated TLS and QUIC peers consume a certificate-chain
1784
  // selector. Remove it before protocol-specific early returns so other fixed
1785
  // targets do not spend mutations on inert TLS server state.
1786
457k
  if (profile != TargetProfile::kFastHttps && profile != TargetProfile::kHttpsH2 &&
1787
379k
      profile != TargetProfile::kFastHttp3 && profile != TargetProfile::kFastSecureWebSocket) {
1788
351k
    scenario->clear_tls_certificate_chain();
1789
351k
  }
1790
1791
  // Only the dedicated TLS HTTP/2 origin consumes the group selector. The
1792
  // compatibility profile returned above so unknown fields remain stable.
1793
457k
  if (profile != TargetProfile::kHttpsH2) {
1794
433k
    scenario->clear_tls_group_profile();
1795
433k
  }
1796
1797
  // Field 13 is append-only so the compatibility target can round-trip it,
1798
  // but every other fixed lane must discard work its peer cannot consume.
1799
457k
  if (profile != TargetProfile::kFastHttp3) {
1800
444k
    scenario->clear_http3_plan();
1801
444k
  }
1802
1803
457k
  const bool resolver_uses_doh =
1804
457k
      profile == TargetProfile::kResolver &&
1805
15.7k
      (scenario->doh_https_rr() || scenario->resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_DOH_ERROR);
1806
457k
  if (profile != TargetProfile::kHttpsH2 && profile != TargetProfile::kFastHttp2 && !resolver_uses_doh) {
1807
402k
    scenario->clear_http2_plan();
1808
402k
  }
1809
1810
457k
  const bool deep_http_haproxy = profile == TargetProfile::kDeepHttp &&
1811
25.3k
                                 scenario->http_transport_mode() == curl::fuzzer::proto::HTTP_TRANSPORT_CONNECT_PROXY;
1812
457k
  if (profile != TargetProfile::kProxyTransport) {
1813
441k
    scenario->clear_socks_proxy_mode();
1814
441k
    if (!deep_http_haproxy) {
1815
440k
      scenario->clear_haproxy_protocol();
1816
440k
    }
1817
441k
  }
1818
1819
  // StartTLS is a correlated LDAP transport mode rather than a transferable
1820
  // scalar option. Other fixed lanes discard it before their early returns.
1821
457k
  if (profile != TargetProfile::kFastLdap) {
1822
446k
    scenario->clear_ldap_starttls();
1823
446k
  }
1824
1825
457k
  if (profile != TargetProfile::kDeepHttp) {
1826
432k
    scenario->clear_http_transport_mode();
1827
432k
    scenario->clear_proxy_socket_option_mode();
1828
432k
  }
1829
1830
457k
  if (profile != TargetProfile::kResolver) {
1831
442k
    scenario->clear_resolve_entries();
1832
442k
    scenario->clear_happy_eyeballs();
1833
442k
    scenario->clear_doh_https_rr();
1834
442k
    scenario->clear_resolver_mode();
1835
442k
  }
1836
1837
  // SMTP's schema-native recipient list has no meaning in another protocol's
1838
  // exchange. Clearing it before early returns keeps other fixed lanes from
1839
  // carrying inert envelope work.
1840
457k
  if (profile != TargetProfile::kFastSmtp) {
1841
448k
    scenario->clear_mail_rcpt();
1842
448k
  }
1843
1844
  // Only these protocol peers currently interleave zero-timeout public multi
1845
  // polling with immediate service of their live connection.
1846
  // Keep the opt-in out of fixed lanes whose driver cannot honor that timing
1847
  // contract; compatibility returned above and preserves its wire input.
1848
457k
  if (profile != TargetProfile::kFastSmtp && profile != TargetProfile::kFastImap &&
1849
437k
      profile != TargetProfile::kFastPop3 && profile != TargetProfile::kFastFtp &&
1850
417k
      profile != TargetProfile::kFastMqtt && profile != TargetProfile::kProxyTransport &&
1851
393k
      profile != TargetProfile::kDeepHttp) {
1852
368k
    scenario->clear_probe_multi_pollset();
1853
368k
  }
1854
1855
  // Direct forwarding changes the H2 proxy stream from CONNECT authority-form
1856
  // to an absolute-form origin request. No other peer implements that shape.
1857
457k
  if (profile != TargetProfile::kH2Proxy) {
1858
436k
    scenario->clear_h2_forward_proxy();
1859
436k
  }
1860
1861
  // Anonymous parser files add syscalls and line parsing that belong only in
1862
  // the deep HTTP lane. Clear them before every protocol-specific early path
1863
  // so fast targets never pay to normalize content they cannot consume.
1864
457k
  if (profile != TargetProfile::kDeepHttp) {
1865
432k
    scenario->clear_cookie_file();
1866
432k
    scenario->clear_altsvc_file();
1867
432k
    scenario->clear_hsts_file();
1868
432k
    scenario->clear_netrc_file();
1869
432k
  }
1870
1871
  // A CRL is useful only when the target completes a real origin TLS setup.
1872
  // Keep malformed or oversized CRLs from turning every other lane into an
1873
  // early TLS-option failure.
1874
457k
  if (profile != TargetProfile::kFastHttps) {
1875
403k
    scenario->clear_crl_file();
1876
403k
    scenario->clear_tls_client_credential_profile();
1877
403k
    scenario->clear_tls_handshake_profile();
1878
403k
  }
1879
1880
457k
  if (profile == TargetProfile::kFastHttp3) {
1881
13.3k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTPS);
1882
13.3k
    RemoveIgnoredHttp3Shape(scenario);
1883
13.3k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
1884
    // The ordinary QUIC peer consumes Http3Plan and has no use for a stream
1885
    // response. CONNECT-UDP proxy mode instead feeds the retained Connection
1886
    // bytes to curl's HTTP/1.1 proxy state machine and capsule filter.
1887
13.3k
    if (!scenario->http3_plan().use_h1_connect_udp_proxy()) {
1888
5.42k
      scenario->clear_connection();
1889
5.42k
    }
1890
13.3k
    BoundHttp3PlanShape(scenario->mutable_http3_plan());
1891
13.3k
    CanonicalizeTlsAuthority(scenario);
1892
13.3k
    CanonicalizeTlsCertificateChain(scenario);
1893
13.3k
    return;
1894
13.3k
  }
1895
1896
444k
  if (profile == TargetProfile::kFastTelnet) {
1897
    // Set the scheme before general bounds so the TELNET-specific upload and
1898
    // PAUSE budgets are selected rather than event-driven compatibility ones.
1899
8.81k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_TELNET);
1900
8.81k
    RemoveApiOnlyShape(scenario);
1901
8.81k
    RemoveMultiOnlyShape(scenario);
1902
8.81k
    RemoveNonTelnetShape(scenario);
1903
8.81k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
1904
8.81k
    BoundTelnetResponse(scenario->mutable_connection());
1905
8.81k
    return;
1906
8.81k
  }
1907
1908
435k
  if (profile == TargetProfile::kFastHttp) {
1909
24.4k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1910
24.4k
    RemoveApiOnlyShape(scenario);
1911
24.4k
    RemoveMultiOnlyShape(scenario);
1912
24.4k
    RemoveTelnetOnlyShape(scenario);
1913
24.4k
    RemoveDeepHttpShape(scenario);
1914
24.4k
    CanonicalizeFastHttpVersion(scenario);
1915
24.4k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
1916
24.4k
    return;
1917
24.4k
  }
1918
1919
411k
  if (profile == TargetProfile::kH2Proxy) {
1920
21.5k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1921
21.5k
    RemoveApiOnlyShape(scenario);
1922
21.5k
    RemoveMultiOnlyShape(scenario);
1923
21.5k
    RemoveTelnetOnlyShape(scenario);
1924
21.5k
    RemoveIgnoredH2ProxyShape(scenario);
1925
21.5k
    switch (scenario->connection().proxy_protocol()) {
1926
20.4k
      case curl::fuzzer::proto::PROXY_PROTOCOL_HTTP2:
1927
21.5k
      case curl::fuzzer::proto::PROXY_PROTOCOL_HTTP1:
1928
21.5k
        break;
1929
1
      default:
1930
1
        scenario->mutable_connection()->set_proxy_protocol(curl::fuzzer::proto::PROXY_PROTOCOL_HTTP2);
1931
1
        break;
1932
21.5k
    }
1933
21.5k
    if (scenario->connection().proxy_protocol() == curl::fuzzer::proto::PROXY_PROTOCOL_HTTP1) {
1934
1.03k
      scenario->clear_h2_forward_proxy();
1935
1.03k
    }
1936
21.5k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
1937
21.5k
    CanonicalizeH2ProxyOriginAuthority(scenario);
1938
21.5k
    return;
1939
21.5k
  }
1940
1941
389k
  if (profile == TargetProfile::kHttpsH2 || profile == TargetProfile::kFastHttp2) {
1942
53.4k
    scenario->set_scheme(profile == TargetProfile::kHttpsH2 ? curl::fuzzer::proto::SCHEME_HTTPS
1943
53.4k
                                                            : curl::fuzzer::proto::SCHEME_HTTP);
1944
53.4k
    RemoveApiOnlyShape(scenario);
1945
53.4k
    RemoveMultiOnlyShape(scenario);
1946
53.4k
    RemoveTelnetOnlyShape(scenario);
1947
53.4k
    RemoveIgnoredH2ProxyShape(scenario);
1948
53.4k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
1949
53.4k
    if (scenario->has_http2_plan()) {
1950
30.8k
      auto* connection = scenario->mutable_connection();
1951
30.8k
      connection->clear_initial_response();
1952
30.8k
      connection->clear_on_readable();
1953
30.8k
      connection->clear_server_frames();
1954
30.8k
      BoundHttp2PlanShape(scenario->mutable_http2_plan());
1955
30.8k
    }
1956
53.4k
    CanonicalizeTlsAuthority(scenario);
1957
53.4k
    if (profile == TargetProfile::kHttpsH2) {
1958
24.0k
      CanonicalizeTlsCertificateChain(scenario);
1959
24.0k
      CanonicalizeTlsGroupProfile(scenario);
1960
24.0k
    }
1961
53.4k
    return;
1962
53.4k
  }
1963
1964
336k
  if (profile == TargetProfile::kProxyTransport) {
1965
15.9k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
1966
15.9k
    switch (scenario->socks_proxy_mode()) {
1967
12.7k
      case curl::fuzzer::proto::SOCKS_PROXY_SOCKS4:
1968
15.3k
      case curl::fuzzer::proto::SOCKS_PROXY_SOCKS4A:
1969
15.7k
      case curl::fuzzer::proto::SOCKS_PROXY_SOCKS5:
1970
15.8k
      case curl::fuzzer::proto::SOCKS_PROXY_SOCKS5_HOSTNAME:
1971
15.8k
        break;
1972
83
      default:
1973
83
        scenario->set_socks_proxy_mode(curl::fuzzer::proto::SOCKS_PROXY_SOCKS4);
1974
83
        break;
1975
15.9k
    }
1976
15.9k
    RemoveApiOnlyShape(scenario);
1977
15.9k
    RemoveMultiOnlyShape(scenario);
1978
15.9k
    RemoveTelnetOnlyShape(scenario);
1979
15.9k
    RemoveDeepHttpShape(scenario);
1980
15.9k
    scenario->mutable_connection()->clear_initial_response();
1981
15.9k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
1982
15.9k
    CanonicalizeSocksAuthority(scenario);
1983
15.9k
    return;
1984
15.9k
  }
1985
1986
320k
  if (profile == TargetProfile::kResolver) {
1987
    // The DoH variant owns the outer HTTPS exchange through a verified
1988
    // TLS/ALPN h2 peer, so its response is an Http2Plan rather than a raw
1989
    // stream script. The ordinary resolver lanes keep plaintext HTTP.
1990
15.7k
    switch (scenario->resolver_mode()) {
1991
13.4k
      case curl::fuzzer::proto::RESOLVER_MODE_DEFAULT:
1992
14.0k
      case curl::fuzzer::proto::RESOLVER_MODE_THREADED_POSITIVE:
1993
14.0k
      case curl::fuzzer::proto::RESOLVER_MODE_THREADED_NEGATIVE:
1994
14.3k
      case curl::fuzzer::proto::RESOLVER_MODE_THREADED_LINK_LOCAL:
1995
14.6k
      case curl::fuzzer::proto::RESOLVER_MODE_UNIX_SOCKET:
1996
14.6k
      case curl::fuzzer::proto::RESOLVER_MODE_DOH_ERROR:
1997
15.0k
      case curl::fuzzer::proto::RESOLVER_MODE_THREADED_PARTIAL:
1998
15.3k
      case curl::fuzzer::proto::RESOLVER_MODE_GLOBAL_LIFECYCLE:
1999
15.7k
      case curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL:
2000
15.7k
        break;
2001
2
      default:
2002
2
        scenario->clear_resolver_mode();
2003
2
        break;
2004
15.7k
    }
2005
15.7k
    const bool fixed_resolver_mode = scenario->resolver_mode() != curl::fuzzer::proto::RESOLVER_MODE_DEFAULT;
2006
15.7k
    if (fixed_resolver_mode) {
2007
2.21k
      scenario->clear_resolve_entries();
2008
2.21k
      scenario->clear_happy_eyeballs();
2009
2.21k
    }
2010
15.7k
    if (scenario->resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_DOH_ERROR) {
2011
68
      scenario->set_doh_https_rr(true);
2012
15.6k
    } else if (fixed_resolver_mode) {
2013
2.14k
      scenario->clear_doh_https_rr();
2014
2.14k
    }
2015
15.7k
    const bool doh_https_rr = scenario->doh_https_rr();
2016
15.7k
    scenario->set_scheme(doh_https_rr ? curl::fuzzer::proto::SCHEME_HTTPS : curl::fuzzer::proto::SCHEME_HTTP);
2017
15.7k
    RemoveApiOnlyShape(scenario);
2018
15.7k
    RemoveMultiOnlyShape(scenario);
2019
15.7k
    RemoveTelnetOnlyShape(scenario);
2020
15.7k
    RemoveDeepHttpShape(scenario);
2021
    // The generic fast-lane pruning above removes fuzz-controlled timing
2022
    // knobs. Threaded resolver modes add back one fixed marker so their
2023
    // bounded drive performs a pollset probe and short fdset waits while the
2024
    // target-local resolver work is outstanding.
2025
15.7k
    if (IsThreadedResolverMode(scenario->resolver_mode())) {
2026
1.58k
      scenario->mutable_connection()->mutable_backpressure()->set_drain_limit(1);
2027
1.58k
    }
2028
15.7k
    if (scenario->resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL) {
2029
      // The runner observes the pool's public THREADS trace after releasing
2030
      // its lookup gate. Seeing an idle worker proves it entered the configured
2031
      // timed condition wait; mutated trace controls must not remove that
2032
      // lifecycle contract.
2033
380
      scenario->set_trace_config("+THREADS");
2034
380
    }
2035
15.7k
    if (doh_https_rr) {
2036
      // CURLOPT_RESOLVE cannot carry an HTTPS record, and the responder owns
2037
      // both the address and HTTPS answers. Clearing the address entries keeps
2038
      // the DoH query on the only path that can reach the ALPN selector.
2039
1.76k
      scenario->clear_resolve_entries();
2040
1.76k
      scenario->clear_happy_eyeballs();
2041
1.76k
      if (scenario->has_http2_plan()) {
2042
1.23k
        auto* connection = scenario->mutable_connection();
2043
1.23k
        connection->clear_initial_response();
2044
1.23k
        connection->clear_on_readable();
2045
1.23k
        connection->clear_server_frames();
2046
1.23k
        BoundHttp2PlanShape(scenario->mutable_http2_plan());
2047
1.23k
      }
2048
13.9k
    } else {
2049
13.9k
      scenario->clear_http2_plan();
2050
13.9k
      BoundStringValues(scenario->mutable_resolve_entries(), scenario_limits::kMaxResolveEntries,
2051
13.9k
                        scenario_limits::kMaxResolveEntryBytes);
2052
13.9k
    }
2053
15.7k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2054
15.7k
    CanonicalizeResolverAuthority(scenario);
2055
15.7k
    return;
2056
15.7k
  }
2057
2058
304k
  if (profile == TargetProfile::kFastFtp) {
2059
    // The same lane services plaintext FTP and implicit-TLS FTPS through one
2060
    // command/data parser. Preserve an explicitly supplied FTPS scheme so the
2061
    // seed and mutations can select the TLS peer; every other scheme falls
2062
    // back to the historical plaintext lane.
2063
12.2k
    const auto scheme = scenario->scheme();
2064
12.2k
    const bool implicit_tls = scheme == curl::fuzzer::proto::SCHEME_FTPS;
2065
12.2k
    scenario->set_scheme(implicit_tls ? curl::fuzzer::proto::SCHEME_FTPS : curl::fuzzer::proto::SCHEME_FTP);
2066
12.2k
    RemoveIgnoredFtpShape(scenario);
2067
12.2k
    CanonicalizeFtpOptionModes(scenario);
2068
12.2k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2069
12.2k
    CanonicalizeFtpAuthority(scenario);
2070
12.2k
    return;
2071
12.2k
  }
2072
2073
292k
  if (profile == TargetProfile::kFastTftp) {
2074
4.47k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_TFTP);
2075
4.47k
    RemoveIgnoredTftpShape(scenario);
2076
4.47k
    CanonicalizeTftpOptionModes(scenario);
2077
4.47k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2078
4.47k
    CanonicalizeTftpAuthority(scenario);
2079
4.47k
    return;
2080
4.47k
  }
2081
2082
287k
  if (profile == TargetProfile::kFastSmtp) {
2083
    // The legacy target and its structured replacement both service plaintext
2084
    // SMTP and implicit-TLS SMTPS through one command/reply parser. Preserve an
2085
    // explicitly supplied SMTPS scheme so the seed and mutations can select the
2086
    // TLS peer; every other scheme falls back to the historical plaintext lane.
2087
9.69k
    const auto scheme = scenario->scheme();
2088
9.69k
    scenario->set_scheme(scheme == curl::fuzzer::proto::SCHEME_SMTPS ? curl::fuzzer::proto::SCHEME_SMTPS
2089
9.69k
                                                                     : curl::fuzzer::proto::SCHEME_SMTP);
2090
9.69k
    RemoveIgnoredSmtpShape(scenario);
2091
9.69k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2092
9.69k
    CanonicalizeSmtpAuthority(scenario);
2093
9.69k
    return;
2094
9.69k
  }
2095
2096
278k
  if (profile == TargetProfile::kFastImap) {
2097
    // Preserve an explicitly supplied IMAPS scheme so seeds and mutations can
2098
    // select the implicit-TLS peer; every other scheme falls back to the
2099
    // historical plaintext lane. Both values reuse this tag-aware parser.
2100
10.2k
    const auto scheme = scenario->scheme();
2101
10.2k
    scenario->set_scheme(scheme == curl::fuzzer::proto::SCHEME_IMAPS ? curl::fuzzer::proto::SCHEME_IMAPS
2102
10.2k
                                                                     : curl::fuzzer::proto::SCHEME_IMAP);
2103
10.2k
    RemoveIgnoredImapShape(scenario);
2104
10.2k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2105
10.2k
    CanonicalizeImapAuthority(scenario);
2106
10.2k
    return;
2107
10.2k
  }
2108
2109
267k
  if (profile == TargetProfile::kFastPop3) {
2110
    // Preserve an explicitly supplied POP3S scheme so seeds and mutations can
2111
    // select the implicit-TLS peer; every other scheme falls back to the
2112
    // historical plaintext lane. Both values reuse this status-line parser.
2113
7.64k
    const auto scheme = scenario->scheme();
2114
7.64k
    scenario->set_scheme(scheme == curl::fuzzer::proto::SCHEME_POP3S ? curl::fuzzer::proto::SCHEME_POP3S
2115
7.64k
                                                                     : curl::fuzzer::proto::SCHEME_POP3);
2116
7.64k
    RemoveIgnoredPop3Shape(scenario);
2117
7.64k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2118
7.64k
    CanonicalizePop3Authority(scenario);
2119
7.64k
    return;
2120
7.64k
  }
2121
2122
260k
  if (profile == TargetProfile::kFastMqtt) {
2123
    // The dedicated MQTT lane services both the plaintext and implicit-TLS
2124
    // variants through one packet-aware parser. Preserve an explicitly
2125
    // supplied MQTTS scheme so the seed and mutations can select the TLS peer;
2126
    // every other scheme falls back to the historical plaintext lane.
2127
8.09k
    const auto scheme = scenario->scheme();
2128
8.09k
    scenario->set_scheme(scheme == curl::fuzzer::proto::SCHEME_MQTTS ? curl::fuzzer::proto::SCHEME_MQTTS
2129
8.09k
                                                                     : curl::fuzzer::proto::SCHEME_MQTT);
2130
8.09k
    RemoveIgnoredMqttShape(scenario);
2131
8.09k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2132
8.09k
    CanonicalizeMqttAuthority(scenario);
2133
8.09k
    return;
2134
8.09k
  }
2135
2136
252k
  if (profile == TargetProfile::kFastDict) {
2137
6.42k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_DICT);
2138
6.42k
    RemoveIgnoredDictShape(scenario);
2139
6.42k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2140
6.42k
    CanonicalizeDictAuthority(scenario);
2141
6.42k
    return;
2142
6.42k
  }
2143
2144
245k
  if (profile == TargetProfile::kFastGopher) {
2145
15.1k
    scenario->set_scheme(scenario->scheme() == curl::fuzzer::proto::SCHEME_GOPHERS
2146
15.1k
                             ? curl::fuzzer::proto::SCHEME_GOPHERS
2147
15.1k
                             : curl::fuzzer::proto::SCHEME_GOPHER);
2148
15.1k
    RemoveIgnoredGopherShape(scenario);
2149
15.1k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2150
15.1k
    CanonicalizeTlsAuthority(scenario);
2151
15.1k
    return;
2152
15.1k
  }
2153
2154
230k
  if (profile == TargetProfile::kFastFile) {
2155
7.15k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_FILE);
2156
7.15k
    RemoveIgnoredFileShape(scenario);
2157
7.15k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2158
7.15k
    CanonicalizeFileAuthority(scenario);
2159
7.15k
    return;
2160
7.15k
  }
2161
2162
223k
  if (profile == TargetProfile::kFastLdap) {
2163
    // The single LDAP target services plaintext LDAP and implicit-TLS LDAPS
2164
    // through one BER parser. Preserve an explicitly supplied LDAPS scheme so
2165
    // the seed and mutations can select the TLS peer; every other scheme falls
2166
    // back to the historical plaintext lane.
2167
11.5k
    const auto scheme = scenario->scheme();
2168
11.5k
    scenario->set_scheme(scheme == curl::fuzzer::proto::SCHEME_LDAPS ? curl::fuzzer::proto::SCHEME_LDAPS
2169
11.5k
                                                                     : curl::fuzzer::proto::SCHEME_LDAP);
2170
11.5k
    if (scenario->scheme() == curl::fuzzer::proto::SCHEME_LDAPS) {
2171
5.86k
      scenario->clear_ldap_starttls();
2172
5.86k
    }
2173
11.5k
    RemoveIgnoredLdapShape(scenario);
2174
11.5k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2175
11.5k
    CanonicalizeLdapAuthority(scenario);
2176
11.5k
    return;
2177
11.5k
  }
2178
2179
211k
  if (profile == TargetProfile::kFastRtsp) {
2180
12.6k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_RTSP);
2181
12.6k
    RemoveIgnoredRtspShape(scenario);
2182
12.6k
    CanonicalizeRtspOptionModes(scenario);
2183
12.6k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2184
12.6k
    CanonicalizeRtspAuthority(scenario);
2185
12.6k
    return;
2186
12.6k
  }
2187
2188
199k
  if (profile == TargetProfile::kFastIpfs) {
2189
    // Preserve an explicitly selected IPNS name; every other pre-policy value
2190
    // falls back to the historical IPFS lane so one binary covers both.
2191
18.5k
    scenario->set_scheme(scenario->scheme() == curl::fuzzer::proto::SCHEME_IPNS ? curl::fuzzer::proto::SCHEME_IPNS
2192
18.5k
                                                                                : curl::fuzzer::proto::SCHEME_IPFS);
2193
18.5k
    RemoveIgnoredIpfsShape(scenario);
2194
18.5k
    CanonicalizeFastHttpVersion(scenario);
2195
18.5k
    PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2196
18.5k
    CanonicalizeIpfsAuthority(scenario);
2197
18.5k
    return;
2198
18.5k
  }
2199
2200
  // Select the lane's scheme before applying scheme-sensitive upload bounds.
2201
  // The scheme field is itself mutable, so bounding first could accidentally
2202
  // give an HTTP/WS case TELNET's smaller payload budget merely because that
2203
  // was the input's pre-policy value.
2204
180k
  switch (profile) {
2205
0
    case TargetProfile::kCompatibility:
2206
0
      return;
2207
25.3k
    case TargetProfile::kDeepHttp:
2208
25.3k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
2209
25.3k
      break;
2210
23.5k
    case TargetProfile::kApi:
2211
23.5k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
2212
23.5k
      break;
2213
24.3k
    case TargetProfile::kMulti:
2214
24.3k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
2215
24.3k
      break;
2216
54.4k
    case TargetProfile::kFastHttps:
2217
54.4k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTPS);
2218
54.4k
      break;
2219
0
    case TargetProfile::kHttpsH2:
2220
      // The protocol-specific early path fixes ALPN and prunes incompatible
2221
      // response shapes while retaining the raw frame script.
2222
0
      return;
2223
0
    case TargetProfile::kFastHttp2:
2224
      // The protocol-specific early path fixes prior-knowledge H2 and prunes
2225
      // incompatible response shapes while retaining its frame script.
2226
0
      return;
2227
0
    case TargetProfile::kFastHttp3:
2228
      // The protocol-specific early path owns the QUIC response plan.
2229
0
      return;
2230
0
    case TargetProfile::kH2Proxy:
2231
      // The early path removes proxy-incompatible fields before general
2232
      // bounds, keeping raw frame mutation dense.
2233
0
      return;
2234
0
    case TargetProfile::kProxyTransport:
2235
      // The early path fixes proxy routing and request-triggered replies.
2236
0
      return;
2237
0
    case TargetProfile::kResolver:
2238
      // The early path fixes safe DNS/cache routing and bounds its slist.
2239
0
      return;
2240
18.2k
    case TargetProfile::kFastWebSocket:
2241
18.2k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_WS);
2242
18.2k
      break;
2243
14.6k
    case TargetProfile::kFastSecureWebSocket:
2244
14.6k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_WSS);
2245
14.6k
      break;
2246
20.0k
    case TargetProfile::kTiming:
2247
20.0k
      scenario->set_scheme(PlaintextScheme(scenario->scheme()));
2248
20.0k
      break;
2249
0
    case TargetProfile::kFastHttp:
2250
0
    case TargetProfile::kFastTelnet:
2251
0
    case TargetProfile::kFastFtp:
2252
0
    case TargetProfile::kFastTftp:
2253
0
    case TargetProfile::kFastSmtp:
2254
0
    case TargetProfile::kFastImap:
2255
0
    case TargetProfile::kFastPop3:
2256
0
    case TargetProfile::kFastMqtt:
2257
0
    case TargetProfile::kFastDict:
2258
0
    case TargetProfile::kFastGopher:
2259
0
    case TargetProfile::kFastFile:
2260
0
    case TargetProfile::kFastLdap:
2261
0
    case TargetProfile::kFastRtsp:
2262
0
    case TargetProfile::kFastIpfs:
2263
      // Protocol-specific early-return paths selected their scheme above.
2264
0
      return;
2265
180k
  }
2266
2267
  // TELNET's retained slist and synchronous pause have no observable, safe
2268
  // meaning in the fixed event-driven lanes. Clear them before walking the
2269
  // general shape so only the compatibility and TELNET targets can retain
2270
  // those values.
2271
180k
  RemoveTelnetOnlyShape(scenario);
2272
180k
  if (profile != TargetProfile::kApi) {
2273
157k
    RemoveApiOnlyShape(scenario);
2274
157k
  }
2275
180k
  if (profile != TargetProfile::kMulti) {
2276
156k
    RemoveMultiOnlyShape(scenario);
2277
156k
  }
2278
180k
  if (profile == TargetProfile::kDeepHttp) {
2279
25.3k
    CanonicalizeDeepHttpSelectors(scenario);
2280
25.3k
  }
2281
180k
  if (profile == TargetProfile::kApi && scenario->has_api_plan()) {
2282
11.2k
    CanonicalizeApiDriveMode(scenario->mutable_api_plan());
2283
11.2k
  }
2284
180k
  PruneAndBoundScenarioShape(scenario, profile, prune_conditional_eligibility);
2285
2286
180k
  switch (profile) {
2287
0
    case TargetProfile::kCompatibility:
2288
0
      return;
2289
0
    case TargetProfile::kFastHttp:
2290
      // Handled before the general bounds so discarded deep shapes are never
2291
      // traversed on the fast path.
2292
0
      return;
2293
2294
25.3k
    case TargetProfile::kDeepHttp:
2295
25.3k
      if (scenario->proxy_socket_option_mode() != curl::fuzzer::proto::PROXY_SOCKET_OPTION_DEFAULT) {
2296
        // A scenario describes one owned route. Prefer the closed socket-option
2297
        // profile when a mutation selects both independent fields.
2298
3.40k
        scenario->clear_http_transport_mode();
2299
3.40k
        CanonicalizeProxySocketOptionAuthority(scenario);
2300
        // Alt-Svc or HSTS can replace this mode's fixed plaintext authority
2301
        // before its option path runs. Keep the focused route authoritative.
2302
3.40k
        scenario->clear_altsvc_file();
2303
3.40k
        scenario->clear_hsts_file();
2304
21.9k
      } else if (scenario->http_transport_mode() != curl::fuzzer::proto::HTTP_TRANSPORT_DIRECT) {
2305
2.19k
        CanonicalizeHttpTransportAuthority(scenario);
2306
        // The selected transport owns the route. Alt-Svc and HSTS can rewrite
2307
        // its fixed plaintext authority into a different proxy or TLS path,
2308
        // defeating the mode the scenario is meant to exercise.
2309
2.19k
        scenario->clear_altsvc_file();
2310
2.19k
        scenario->clear_hsts_file();
2311
19.7k
      } else if (!scenario->altsvc_file().empty()) {
2312
1.60k
        CanonicalizeAltSvcAuthority(scenario);
2313
1.60k
      }
2314
25.3k
      if (scenario->http_transport_mode() == curl::fuzzer::proto::HTTP_TRANSPORT_CONNECT_PROXY &&
2315
1.24k
          scenario->haproxy_protocol()) {
2316
        // This closed combination makes the pending HAProxy setup state
2317
        // reachable after CONNECT. A large checked-in request fills the
2318
        // tightened socket, while bounded peer drains and repeated public
2319
        // multi polling expose the filter's write-only pollset.
2320
494
        auto* backpressure = scenario->mutable_connection()->mutable_backpressure();
2321
494
        backpressure->set_recv_buf_bytes(kHaproxyConnectBufferBytes);
2322
494
        backpressure->set_drain_limit(kHaproxyConnectDrainBytes);
2323
494
        for (auto& connection : *scenario->mutable_subsequent_connections()) {
2324
398
          connection.clear_backpressure();
2325
398
        }
2326
24.8k
      } else {
2327
24.8k
        scenario->clear_haproxy_protocol();
2328
24.8k
        ClearAllBackpressure(scenario);
2329
24.8k
      }
2330
25.3k
      if (scenario->http_transport_mode() != curl::fuzzer::proto::HTTP_TRANSPORT_CONNECT_PROXY) {
2331
24.1k
        scenario->clear_probe_multi_pollset();
2332
24.1k
      }
2333
25.3k
      return;
2334
2335
23.5k
    case TargetProfile::kApi:
2336
23.5k
      ClearAllBackpressure(scenario);
2337
23.5k
      if (scenario->host_path().size() > scenario_limits::kMaxApiStringBytes) {
2338
1
        scenario->mutable_host_path()->resize(scenario_limits::kMaxApiStringBytes);
2339
1
      }
2340
23.5k
      if (scenario->has_api_plan()) {
2341
11.2k
        BoundApiPlanShape(scenario->mutable_api_plan());
2342
11.2k
        CanonicalizeApiMimeRepeat(scenario);
2343
11.2k
      }
2344
23.5k
      return;
2345
2346
24.3k
    case TargetProfile::kMulti: {
2347
24.3k
      ClearAllBackpressure(scenario);
2348
24.3k
      CanonicalizeMultiAuthority(scenario);
2349
      // The multi runner is HTTP-only (it drives every handle through one
2350
      // plaintext HTTP peer), so pin the scheme instead of letting a mutation
2351
      // select a protocol the runner cannot service.
2352
24.3k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
2353
24.3k
      auto* plan = scenario->mutable_multi_plan();
2354
24.3k
      BoundMultiPlanShape(plan);
2355
24.3k
      TrimRepeated(scenario->mutable_subsequent_connections(), plan->transfer_count() - 1U);
2356
24.3k
      return;
2357
0
    }
2358
2359
54.4k
    case TargetProfile::kFastHttps:
2360
54.4k
      ClearAllBackpressure(scenario);
2361
54.4k
      CanonicalizeTlsAuthority(scenario);
2362
54.4k
      CanonicalizeTlsCertificateChain(scenario);
2363
54.4k
      CanonicalizeTlsClientCredentialProfile(scenario);
2364
54.4k
      CanonicalizeTlsHandshakeProfile(scenario);
2365
54.4k
      return;
2366
2367
0
    case TargetProfile::kHttpsH2:
2368
      // Handled by the raw HTTP/2 protocol-specific early path above.
2369
0
      return;
2370
2371
0
    case TargetProfile::kFastHttp2:
2372
      // Handled by the plaintext HTTP/2 protocol-specific early path above.
2373
0
      return;
2374
2375
0
    case TargetProfile::kFastHttp3:
2376
      // Handled before generic connection bounding because Http3Plan replaces
2377
      // the stream-socket response script.
2378
0
      return;
2379
2380
0
    case TargetProfile::kH2Proxy:
2381
      // Handled by the protocol-specific early path above.
2382
0
      return;
2383
2384
0
    case TargetProfile::kProxyTransport:
2385
      // Handled by the protocol-specific early path above.
2386
0
      return;
2387
2388
0
    case TargetProfile::kResolver:
2389
      // Handled by the resolver-specific early path above.
2390
0
      return;
2391
2392
18.2k
    case TargetProfile::kFastWebSocket:
2393
18.2k
      ClearAllBackpressure(scenario);
2394
18.2k
      RemoveIgnoredWebSocketShape(scenario);
2395
18.2k
      return;
2396
2397
14.6k
    case TargetProfile::kFastSecureWebSocket:
2398
14.6k
      ClearAllBackpressure(scenario);
2399
14.6k
      RemoveIgnoredWebSocketShape(scenario);
2400
14.6k
      CanonicalizeTlsAuthority(scenario);
2401
14.6k
      CanonicalizeTlsCertificateChain(scenario);
2402
14.6k
      return;
2403
2404
0
    case TargetProfile::kFastTelnet:
2405
      // Handled before the general bounds so its protocol-specific limits are
2406
      // selected from the start.
2407
0
      return;
2408
2409
0
    case TargetProfile::kFastFtp:
2410
0
    case TargetProfile::kFastTftp:
2411
0
    case TargetProfile::kFastSmtp:
2412
0
    case TargetProfile::kFastImap:
2413
0
    case TargetProfile::kFastPop3:
2414
0
    case TargetProfile::kFastMqtt:
2415
0
    case TargetProfile::kFastDict:
2416
0
    case TargetProfile::kFastGopher:
2417
0
    case TargetProfile::kFastFile:
2418
0
    case TargetProfile::kFastLdap:
2419
0
    case TargetProfile::kFastRtsp:
2420
0
    case TargetProfile::kFastIpfs:
2421
      // Their peers consume narrower raw-script shapes, pruned before general
2422
      // bounds so ignored fields never tax these fast paths.
2423
0
      return;
2424
2425
20.0k
    case TargetProfile::kTiming: {
2426
20.0k
      if (scenario->scheme() == curl::fuzzer::proto::SCHEME_WS) {
2427
4.53k
        RemoveIgnoredWebSocketShape(scenario);
2428
4.53k
      }
2429
20.0k
      auto* backpressure = scenario->mutable_connection()->mutable_backpressure();
2430
20.0k
      if (backpressure->recv_buf_bytes() == 0) {
2431
        // A drain limit alone cannot fill the default AF_UNIX buffer with the
2432
        // harness's bounded upload. Always tighten the socket so this lane
2433
        // represents real pressure, not merely selection of the timed loop.
2434
647
        backpressure->set_recv_buf_bytes(kDefaultBackpressureBufferBytes);
2435
19.3k
      } else {
2436
19.3k
        backpressure->set_recv_buf_bytes(CanonicalizeNonZero(backpressure->recv_buf_bytes(),
2437
19.3k
                                                             kMinBackpressureBufferBytes, kMaxBackpressureBufferBytes));
2438
19.3k
      }
2439
20.0k
      backpressure->set_drain_limit(CanonicalizeNonZero(backpressure->drain_limit(), 1, kMaxDrainBytesPerIteration));
2440
20.0k
      for (auto& connection : *scenario->mutable_subsequent_connections()) {
2441
7.83k
        CanonicalizeOptionalBackpressure(&connection);
2442
7.83k
      }
2443
20.0k
      return;
2444
0
    }
2445
180k
  }
2446
180k
}
2447
2448
}  // namespace
2449
2450
/// Apply execution-only allocation-failure safety rules. Fixed target
2451
/// postprocessors call this while serializing mutations; the shared runtime
2452
/// repeats it so the compatibility target keeps its persistent wire format
2453
/// while executing the same bounded, safe plan.
2454
/// @param scenario Parsed scenario to canonicalize in place.
2455
698k
void NormalizeScenarioForExecution(curl::fuzzer::proto::Scenario* scenario) {
2456
698k
  if (scenario == nullptr) {
2457
0
    return;
2458
0
  }
2459
698k
  CanonicalizeAllocationFailures(scenario);
2460
698k
  CanonicalizeAllocationFailureDrive(scenario);
2461
698k
  CanonicalizeLocalPortRange(scenario);
2462
698k
}
2463
2464
/// Complete normalization for a loaded or mutated fixed-target Scenario.
2465
/// Besides dispatch and timing controls, fixed lanes trim repeated-field and
2466
/// metadata suffixes that the runtime cannot consume. WebSocket lanes also
2467
/// remove follow-on sockets and MIME bodies their single-upgrade driver cannot
2468
/// use; all remaining observable bytes, options, frames, and probes stay
2469
/// fuzz-controlled. Option oneofs are canonicalized before final string bounds,
2470
/// so repeated normalization preserves the same serialized message. The
2471
/// compatibility profile leaves the entire input unchanged.
2472
/// @param scenario Scenario to canonicalize in place.
2473
/// @param profile Target lane whose invariants must be restored.
2474
/// @param prune_conditional_eligibility Generated profile callback, or nullptr
2475
///        when the caller has no conditional eligibility to apply.
2476
void NormalizeScenarioForTarget(curl::fuzzer::proto::Scenario* scenario, TargetProfile profile,
2477
457k
                                ConditionalEligibilityPruner prune_conditional_eligibility) {
2478
  // Compatibility has no LPM postprocessor. Preserve the complete wire input,
2479
  // including mismatched option oneofs and newly added, otherwise inert fields.
2480
457k
  if (scenario == nullptr || profile == TargetProfile::kCompatibility) {
2481
0
    return;
2482
0
  }
2483
2484
457k
  ApplyLanePolicy(scenario, profile, prune_conditional_eligibility);
2485
457k
  NormalizeScenarioForExecution(scenario);
2486
457k
  CanonicalizeOptionValueCases(scenario);
2487
  // Canonicalization can materialize string values or prepend pin syntax.
2488
  // Charge the final representation so normalization remains a fixed point.
2489
1.20M
  for (auto& option : *scenario->mutable_options()) {
2490
1.20M
    if (option.value_case() == curl::fuzzer::proto::SetOption::kStringValue) {
2491
610k
      TrimMetadata(option.mutable_string_value());
2492
610k
    }
2493
1.20M
  }
2494
457k
}
2495
2496
}  // namespace proto_fuzzer