Coverage Report

Created: 2026-09-04 07:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/target_policy.cc
Line
Count
Source
1
/*
2
 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3
 *
4
 * SPDX-License-Identifier: curl
5
 */
6
7
/// @file
8
/// @brief Implementation of the per-binary proto mutation policies.
9
10
#include "proto_fuzzer/target_policy.h"
11
12
#include <algorithm>
13
#include <cstdint>
14
#include <string>
15
16
#include "proto_fuzzer/scenario_limits.h"
17
#include "proto_fuzzer/telnet_scenario.h"
18
19
namespace proto_fuzzer {
20
21
namespace {
22
23
// Linux raises smaller socket-buffer requests to an implementation minimum,
24
// so 2048 is both cheap and reliably small enough to exercise short writes.
25
constexpr std::uint32_t kDefaultBackpressureBufferBytes = 2048;
26
27
// Values outside these ranges do not create useful new socket behavior for
28
// the harness's bounded 4-16 KiB writes. Keeping them small also prevents a
29
// mutated uint32 recv size from overflowing the int accepted by setsockopt.
30
constexpr std::uint32_t kMinBackpressureBufferBytes = 2048;
31
constexpr std::uint32_t kMaxBackpressureBufferBytes = 4096;
32
constexpr std::uint32_t kMaxDrainBytesPerIteration = 1024;
33
34
/// Remove a repeated-field suffix that the runtime would ignore. Doing this
35
/// in LPM's postprocessor matters for speed as well as memory: otherwise later
36
/// mutations keep rediscovering and editing objects that cannot reach curl.
37
template <typename RepeatedField>
38
896k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
896k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
896k
  if (size > limit) {
41
1.31k
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
1.31k
  }
43
896k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::SetOption> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::SetOption>*, unsigned long)
Line
Count
Source
38
116k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
116k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
116k
  if (size > limit) {
41
474
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
474
  }
43
116k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > >(google::protobuf::RepeatedPtrField<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >*, unsigned long)
Line
Count
Source
38
456k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
456k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
456k
  if (size > limit) {
41
366
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
366
  }
43
456k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MimePart> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MimePart>*, unsigned long)
Line
Count
Source
38
33.2k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
33.2k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
33.2k
  if (size > limit) {
41
119
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
119
  }
43
33.2k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MimeDataPart> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MimeDataPart>*, unsigned long)
Line
Count
Source
38
17.0k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
17.0k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
17.0k
  if (size > limit) {
41
36
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
36
  }
43
17.0k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::WebSocketFrame> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::WebSocketFrame>*, unsigned long)
Line
Count
Source
38
148k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
148k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
148k
  if (size > limit) {
41
100
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
100
  }
43
148k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MultiAction> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::MultiAction>*, unsigned long)
Line
Count
Source
38
502
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
502
  const std::size_t size = static_cast<std::size_t>(field->size());
40
502
  if (size > limit) {
41
0
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
0
  }
43
502
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::TrimRepeated<google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Connection> >(google::protobuf::RepeatedPtrField<curl::fuzzer::proto::Connection>*, unsigned long)
Line
Count
Source
38
124k
void TrimRepeated(RepeatedField* field, std::size_t limit) {
39
124k
  const std::size_t size = static_cast<std::size_t>(field->size());
40
124k
  if (size > limit) {
41
221
    field->DeleteSubrange(static_cast<int>(limit), static_cast<int>(size - limit));
42
221
  }
43
124k
}
44
45
/// Bound strings passed to NUL-terminated metadata APIs. The runtime applies
46
/// the same prefix, so deleting the invisible suffix increases useful
47
/// mutation density without removing any behavior curl could observe.
48
349k
void TrimMetadata(std::string* value) {
49
349k
  if (value->size() > scenario_limits::kMaxMetadataBytes) {
50
32
    value->resize(scenario_limits::kMaxMetadataBytes);
51
32
  }
52
349k
}
53
54
/// Give the successful-TLS lane a hostname its fixed certificate can verify
55
/// while retaining the fuzz-controlled path, query, and fragment. Arbitrary
56
/// authorities remain covered by the compatibility and legacy HTTPS lanes;
57
/// spending this lane's mutations on URL failures would keep curl's peer-cert
58
/// and encrypted application-data paths dark.
59
17.8k
void CanonicalizeTlsAuthority(curl::fuzzer::proto::Scenario* scenario) {
60
17.8k
  const std::string& host_path = scenario->host_path();
61
17.8k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
62
17.8k
  if (suffix_start == std::string::npos) {
63
2.87k
    scenario->set_host_path("tls.test/");
64
2.87k
    return;
65
2.87k
  }
66
14.9k
  scenario->set_host_path("tls.test" + host_path.substr(suffix_start));
67
14.9k
}
68
69
/// Keep the tunneled origin parseable while retaining every path, query, and
70
/// fragment byte. The fixed numeric proxy endpoint handles routing separately;
71
/// mutating the origin authority would therefore buy only early URL failures,
72
/// not additional HTTP/2 proxy behavior.
73
5.54k
void CanonicalizeH2ProxyOriginAuthority(curl::fuzzer::proto::Scenario* scenario) {
74
5.54k
  const std::string& host_path = scenario->host_path();
75
5.54k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
76
5.54k
  if (suffix_start == std::string::npos) {
77
185
    scenario->set_host_path("origin.test/");
78
185
    return;
79
185
  }
80
5.36k
  scenario->set_host_path("origin.test" + host_path.substr(suffix_start));
81
5.36k
}
82
83
/// Give the TFTP lane a parseable filename-bearing URL while retaining the
84
/// fuzz-controlled path, query, and fragment. The UDP peer rewrites curl's
85
/// destination after URL parsing, so authority mutations cannot reach another
86
/// host; canonicalizing them here avoids spending most iterations on failures
87
/// before curl constructs a TFTP request. An explicit slash is preserved so
88
/// the missing-filename error remains reachable.
89
2.01k
void CanonicalizeTftpAuthority(curl::fuzzer::proto::Scenario* scenario) {
90
2.01k
  const std::string& host_path = scenario->host_path();
91
2.01k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
92
2.01k
  if (suffix_start == std::string::npos) {
93
196
    scenario->set_host_path("tftp.test/file");
94
196
    return;
95
196
  }
96
1.81k
  scenario->set_host_path("tftp.test" + host_path.substr(suffix_start));
97
1.81k
}
98
99
/// Keep the FTP lane inside the same parseable authority while leaving every
100
/// path segment and wildcard under mutation control. CONNECT_TO already
101
/// confines networking, but rejecting malformed authorities before USER/PWD
102
/// would waste the control/data peer this target uniquely provides.
103
7.19k
void CanonicalizeFtpAuthority(curl::fuzzer::proto::Scenario* scenario) {
104
7.19k
  const std::string& host_path = scenario->host_path();
105
7.19k
  const std::size_t suffix_start = host_path.find_first_of("/?#");
106
7.19k
  if (suffix_start == std::string::npos) {
107
147
    scenario->set_host_path("ftp.test/file");
108
147
    return;
109
147
  }
110
7.04k
  scenario->set_host_path("ftp.test" + host_path.substr(suffix_start));
111
7.04k
}
112
113
/// Put every handle in the multi lane on one origin so connection limits,
114
/// queueing, and reuse affect real transfers instead of independent hosts.
115
/// Path/query/fragment bytes remain mutation-controlled.
116
502
void CanonicalizeMultiAuthority(curl::fuzzer::proto::Scenario* scenario) {
117
502
  const std::string& host_path = scenario->host_path();
118
502
  const std::size_t suffix_start = host_path.find_first_of("/?#");
119
502
  if (suffix_start == std::string::npos) {
120
0
    scenario->set_host_path("multi.test/");
121
0
    return;
122
0
  }
123
502
  scenario->set_host_path("multi.test" + host_path.substr(suffix_start));
124
502
}
125
126
template <typename RepeatedBytes>
127
308k
void BoundStringValues(RepeatedBytes* values, std::size_t count_limit, std::size_t value_limit) {
128
308k
  TrimRepeated(values, count_limit);
129
308k
  for (std::string& value : *values) {
130
121k
    if (value.size() > value_limit) {
131
42
      value.resize(value_limit);
132
42
    }
133
121k
  }
134
308k
}
135
136
template <typename RepeatedBytes>
137
191k
void BoundHeaderValues(RepeatedBytes* headers, std::size_t limit) {
138
191k
  BoundStringValues(headers, limit, scenario_limits::kMaxMetadataBytes);
139
191k
}
140
141
/// Keep one response script identical to the prefix MockServer and
142
/// WebSocketMockServer can deliver. Raw chunks take precedence over structured
143
/// frames, matching both runtime serializers.
144
148k
void BoundConnectionShape(curl::fuzzer::proto::Connection* connection) {
145
148k
  TrimRepeated(connection->mutable_on_readable(), scenario_limits::kMaxResponseChunks);
146
148k
  const std::size_t raw_count = static_cast<std::size_t>(connection->on_readable_size());
147
148k
  TrimRepeated(connection->mutable_server_frames(), scenario_limits::kMaxResponseChunks - raw_count);
148
148k
}
149
150
/// Apply the metadata/header limits shared by both MIME part message types.
151
template <typename Part>
152
74.6k
void BoundMimePartMetadata(Part* part) {
153
74.6k
  TrimMetadata(part->mutable_name());
154
74.6k
  TrimMetadata(part->mutable_filename());
155
74.6k
  TrimMetadata(part->mutable_content_type());
156
74.6k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
157
74.6k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundMimePartMetadata<curl::fuzzer::proto::MimePart>(curl::fuzzer::proto::MimePart*)
Line
Count
Source
152
31.7k
void BoundMimePartMetadata(Part* part) {
153
31.7k
  TrimMetadata(part->mutable_name());
154
31.7k
  TrimMetadata(part->mutable_filename());
155
31.7k
  TrimMetadata(part->mutable_content_type());
156
31.7k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
157
31.7k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::BoundMimePartMetadata<curl::fuzzer::proto::MimeDataPart>(curl::fuzzer::proto::MimeDataPart*)
Line
Count
Source
152
42.9k
void BoundMimePartMetadata(Part* part) {
153
42.9k
  TrimMetadata(part->mutable_name());
154
42.9k
  TrimMetadata(part->mutable_filename());
155
42.9k
  TrimMetadata(part->mutable_content_type());
156
42.9k
  BoundHeaderValues(part->mutable_headers(), scenario_limits::kMaxMimeHeadersPerPart);
157
42.9k
}
158
159
42.9k
void BoundMimeLeaf(curl::fuzzer::proto::MimeDataPart* part) {
160
42.9k
  BoundMimePartMetadata(part);
161
42.9k
  if (part->data().size() > scenario_limits::kMaxMimeDataBytes) {
162
0
    part->mutable_data()->resize(scenario_limits::kMaxMimeDataBytes);
163
0
  }
164
42.9k
}
165
166
/// Mirror the runtime's shared top-level/nested part budget in the protobuf
167
/// itself. A simple per-list cap is insufficient because many bounded child
168
/// lists could still leave most of the message semantically dead.
169
16.6k
void BoundMimeShape(curl::fuzzer::proto::MimePost* post) {
170
16.6k
  TrimRepeated(post->mutable_parts(), scenario_limits::kMaxTopLevelMimeParts);
171
16.6k
  std::size_t remaining = scenario_limits::kMaxTotalMimeParts;
172
16.6k
  std::size_t retained_top_parts = 0;
173
174
48.3k
  while (retained_top_parts < static_cast<std::size_t>(post->parts_size()) && remaining != 0) {
175
31.7k
    auto* part = post->mutable_parts(static_cast<int>(retained_top_parts));
176
31.7k
    ++retained_top_parts;
177
31.7k
    --remaining;
178
31.7k
    BoundMimePartMetadata(part);
179
180
31.7k
    if (part->content_case() == curl::fuzzer::proto::MimePart::kData) {
181
5.13k
      if (part->data().size() > scenario_limits::kMaxMimeDataBytes) {
182
0
        part->mutable_data()->resize(scenario_limits::kMaxMimeDataBytes);
183
0
      }
184
5.13k
      continue;
185
5.13k
    }
186
26.5k
    if (part->content_case() != curl::fuzzer::proto::MimePart::kSubparts) {
187
9.57k
      continue;
188
9.57k
    }
189
190
17.0k
    auto* children = part->mutable_subparts()->mutable_parts();
191
17.0k
    TrimRepeated(children, std::min(scenario_limits::kMaxNestedMimeParts, remaining));
192
42.9k
    for (auto& child : *children) {
193
42.9k
      BoundMimeLeaf(&child);
194
42.9k
      --remaining;
195
42.9k
    }
196
17.0k
  }
197
198
16.6k
  TrimRepeated(post->mutable_parts(), retained_top_parts);
199
16.6k
}
200
201
/// Remove upload bytes and read steps the callback cannot observe. Clamping
202
/// individual limits also keeps mutations concentrated on short reads instead
203
/// of many distinct uint32 values that all collapse to the same 16 KiB cap.
204
void BoundUploadShape(curl::fuzzer::proto::UploadScript* upload, std::size_t data_limit, std::size_t read_step_limit,
205
13.2k
                      std::size_t read_size_limit) {
206
13.2k
  if (upload->data().size() > data_limit) {
207
6
    upload->mutable_data()->resize(data_limit);
208
6
  }
209
  // RepeatedField<uint32_t> lacks RepeatedPtrField's DeleteSubrange helper;
210
  // removing the ignored suffix from the end is constant-time per element and
211
  // preserves the mutation-significant prefix exactly.
212
51.8k
  while (static_cast<std::size_t>(upload->read_sizes_size()) > read_step_limit) {
213
38.6k
    upload->mutable_read_sizes()->RemoveLast();
214
38.6k
  }
215
34.6k
  for (int i = 0; i < upload->read_sizes_size(); ++i) {
216
21.4k
    if (upload->read_sizes(i) > read_size_limit) {
217
370
      upload->set_read_sizes(i, static_cast<std::uint32_t>(read_size_limit));
218
370
    }
219
21.4k
  }
220
13.2k
}
221
222
/// Trim a protobuf repeated scalar without depending on the container's
223
/// pointer-field-only DeleteSubrange API. Keeping the mutation-significant
224
/// prefix matches every runtime selector loop.
225
template <typename RepeatedScalar>
226
9.70k
void TrimRepeatedScalar(RepeatedScalar* values, std::size_t limit) {
227
19.8k
  while (static_cast<std::size_t>(values->size()) > limit) {
228
10.1k
    values->RemoveLast();
229
10.1k
  }
230
9.70k
}
231
232
/// Keep API work proportional to the fixed descriptor tables used by the
233
/// runtime. Selector magnitudes stay mutation-controlled because the runtime
234
/// folds them into the relevant typed table; only suffixes it cannot execute
235
/// are dead and therefore removed here.
236
4.85k
void BoundApiPlanShape(curl::fuzzer::proto::ApiPlan* plan) {
237
4.85k
  TrimRepeatedScalar(plan->mutable_share_data_selectors(), scenario_limits::kMaxApiShareDataSelectors);
238
4.85k
  TrimRepeatedScalar(plan->mutable_easy_info_selectors(), scenario_limits::kMaxApiInfoSelectors);
239
240
4.85k
  switch (plan->drive_mode()) {
241
3.02k
    case curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM:
242
3.90k
    case curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET:
243
3.90k
      break;
244
950
    case curl::fuzzer::proto::API_DRIVE_EASY_PERFORM:
245
      // Wakeup is a multi-handle API and has no live object in easy mode.
246
      // Clearing it keeps every retained mutation observable.
247
950
      plan->set_wake_multi(false);
248
950
      break;
249
3
    default:
250
3
      plan->set_drive_mode(curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM);
251
3
      break;
252
4.85k
  }
253
4.85k
}
254
255
/// Keep concurrent-handle work within the mock's fixed socket and operation
256
/// budgets. Values are canonicalized here rather than only at runtime so LPM
257
/// mutates state that the target can actually distinguish.
258
502
void BoundMultiPlanShape(curl::fuzzer::proto::MultiPlan* plan) {
259
502
  const std::uint32_t minimum = static_cast<std::uint32_t>(scenario_limits::kMinMultiTransfers);
260
502
  const std::uint32_t maximum = static_cast<std::uint32_t>(scenario_limits::kMaxMultiTransfers);
261
502
  const std::uint32_t transfer_count = std::max(minimum, std::min(plan->transfer_count(), maximum));
262
502
  plan->set_transfer_count(transfer_count);
263
502
  plan->set_max_host_connections(std::min(plan->max_host_connections(), transfer_count));
264
502
  plan->set_max_total_connections(std::min(plan->max_total_connections(), transfer_count));
265
502
  plan->set_connection_cache_size(std::min(plan->connection_cache_size(), maximum * 2U));
266
502
  TrimRepeated(plan->mutable_actions(), scenario_limits::kMaxMultiActions);
267
268
502
  switch (plan->drive_mode()) {
269
319
    case curl::fuzzer::proto::MULTI_DRIVE_PERFORM:
270
502
    case curl::fuzzer::proto::MULTI_DRIVE_SOCKET:
271
502
      break;
272
0
    default:
273
0
      plan->set_drive_mode(curl::fuzzer::proto::MULTI_DRIVE_PERFORM);
274
0
      break;
275
502
  }
276
277
1.42k
  for (auto& action : *plan->mutable_actions()) {
278
1.42k
    action.set_transfer_selector(action.transfer_selector() % transfer_count);
279
1.42k
    switch (action.kind()) {
280
220
      case curl::fuzzer::proto::MULTI_ACTION_NONE:
281
369
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_RECV:
282
472
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_SEND:
283
697
      case curl::fuzzer::proto::MULTI_ACTION_PAUSE_ALL:
284
823
      case curl::fuzzer::proto::MULTI_ACTION_RESUME:
285
1.18k
      case curl::fuzzer::proto::MULTI_ACTION_REMOVE:
286
1.42k
      case curl::fuzzer::proto::MULTI_ACTION_READD:
287
1.42k
        break;
288
0
      default:
289
0
        action.set_kind(curl::fuzzer::proto::MULTI_ACTION_NONE);
290
0
        break;
291
1.42k
    }
292
1.42k
  }
293
502
}
294
295
/// Canonicalize all shape limits enforced by the runtime. This runs only in
296
/// fixed policy targets; the compatibility binary deliberately retains its
297
/// historical no-postprocessor semantics for existing OSS-Fuzz reproducers.
298
116k
void BoundScenarioShape(curl::fuzzer::proto::Scenario* scenario) {
299
116k
  TrimRepeated(scenario->mutable_options(), scenario_limits::kMaxOptions);
300
363k
  for (auto& option : *scenario->mutable_options()) {
301
363k
    if (option.value_case() == curl::fuzzer::proto::SetOption::kStringValue) {
302
125k
      TrimMetadata(option.mutable_string_value());
303
125k
    }
304
363k
  }
305
306
116k
  BoundHeaderValues(scenario->mutable_request_headers(), scenario_limits::kMaxRequestHeaders);
307
116k
  BoundStringValues(scenario->mutable_telnet_options(), scenario_limits::kMaxTelnetOptions,
308
116k
                    scenario_limits::kMaxTelnetOptionBytes);
309
116k
  if (scenario->has_mime_post()) {
310
16.6k
    BoundMimeShape(scenario->mutable_mime_post());
311
16.6k
  }
312
116k
  if (scenario->has_upload()) {
313
13.2k
    const bool telnet = scenario->scheme() == curl::fuzzer::proto::SCHEME_TELNET;
314
13.2k
    const std::size_t data_limit = telnet ? scenario_limits::kMaxTelnetUploadBytes : scenario_limits::kMaxUploadBytes;
315
13.2k
    const std::size_t read_step_limit =
316
13.2k
        telnet ? scenario_limits::kMaxTelnetUploadReadSteps : scenario_limits::kMaxUploadReadSteps;
317
13.2k
    const std::size_t read_size_limit =
318
13.2k
        telnet ? scenario_limits::kMaxTelnetUploadReadSize : scenario_limits::kMaxUploadReadSize;
319
13.2k
    BoundUploadShape(scenario->mutable_upload(), data_limit, read_step_limit, read_size_limit);
320
13.2k
  }
321
322
116k
  BoundConnectionShape(scenario->mutable_connection());
323
116k
  TrimRepeated(scenario->mutable_subsequent_connections(), scenario_limits::kMaxConnections - 1);
324
116k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
325
31.3k
    BoundConnectionShape(&connection);
326
31.3k
  }
327
116k
}
328
329
/// Return whether an option belongs in the high-throughput HTTP lane. This is
330
/// deliberately an allowlist rather than a denylist: adding a new structured
331
/// option should expand deep coverage first, not silently make the fast lane
332
/// slower before its cost has been measured.
333
25.3k
bool IsCheapHttpOption(curl::fuzzer::proto::CurlOptionId option_id) {
334
25.3k
  switch (option_id) {
335
7.01k
    case curl::fuzzer::proto::CURLOPT_ACCEPT_ENCODING:
336
7.87k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
337
9.10k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
338
9.28k
    case curl::fuzzer::proto::CURLOPT_DISALLOW_USERNAME_IN_URL:
339
10.6k
    case curl::fuzzer::proto::CURLOPT_FAILONERROR:
340
11.2k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
341
11.2k
    case curl::fuzzer::proto::CURLOPT_HEADER:
342
11.3k
    case curl::fuzzer::proto::CURLOPT_HTTP09_ALLOWED:
343
11.4k
    case curl::fuzzer::proto::CURLOPT_HTTP_CONTENT_DECODING:
344
11.6k
    case curl::fuzzer::proto::CURLOPT_HTTP_TRANSFER_DECODING:
345
18.0k
    case curl::fuzzer::proto::CURLOPT_HTTP_VERSION:
346
20.2k
    case curl::fuzzer::proto::CURLOPT_HTTPGET:
347
20.3k
    case curl::fuzzer::proto::CURLOPT_IGNORE_CONTENT_LENGTH:
348
21.2k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
349
22.1k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
350
22.4k
    case curl::fuzzer::proto::CURLOPT_PATH_AS_IS:
351
22.5k
    case curl::fuzzer::proto::CURLOPT_RANGE:
352
23.4k
    case curl::fuzzer::proto::CURLOPT_REQUEST_TARGET:
353
24.1k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
354
24.8k
    case curl::fuzzer::proto::CURLOPT_TRANSFER_ENCODING:
355
24.9k
    case curl::fuzzer::proto::CURLOPT_USERAGENT:
356
24.9k
      return true;
357
358
178
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
359
342
    default:
360
342
      return false;
361
25.3k
  }
362
25.3k
}
363
364
/// Return whether an option can affect the HTTP/1.1 request carried inside the
365
/// fixed HTTP/2 CONNECT tunnel. Proxy routing, ALPN, and TLS verification are
366
/// owned by H2ProxyMockServer and must not be mutation-controlled; HTTP/2 as an
367
/// inner origin protocol is also excluded because it would require a second
368
/// frame script and obscure coverage of the outer proxy filter. Stateful HTTP
369
/// options remain useful here because their wire effects traverse cf-h2-proxy.
370
36.1k
bool IsH2ProxyOriginOption(curl::fuzzer::proto::CurlOptionId option_id) {
371
36.1k
  switch (option_id) {
372
1.19k
    case curl::fuzzer::proto::CURLOPT_ACCEPT_ENCODING:
373
1.82k
    case curl::fuzzer::proto::CURLOPT_ALTSVC_CTRL:
374
1.88k
    case curl::fuzzer::proto::CURLOPT_AUTOREFERER:
375
2.28k
    case curl::fuzzer::proto::CURLOPT_AWS_SIGV4:
376
2.80k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
377
2.84k
    case curl::fuzzer::proto::CURLOPT_COOKIE:
378
7.40k
    case curl::fuzzer::proto::CURLOPT_COOKIELIST:
379
8.76k
    case curl::fuzzer::proto::CURLOPT_COOKIESESSION:
380
8.81k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
381
9.47k
    case curl::fuzzer::proto::CURLOPT_DISALLOW_USERNAME_IN_URL:
382
10.7k
    case curl::fuzzer::proto::CURLOPT_EXPECT_100_TIMEOUT_MS:
383
11.4k
    case curl::fuzzer::proto::CURLOPT_FAILONERROR:
384
12.1k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
385
12.7k
    case curl::fuzzer::proto::CURLOPT_FOLLOWLOCATION:
386
12.8k
    case curl::fuzzer::proto::CURLOPT_FORBID_REUSE:
387
13.1k
    case curl::fuzzer::proto::CURLOPT_FRESH_CONNECT:
388
15.1k
    case curl::fuzzer::proto::CURLOPT_HEADER:
389
15.8k
    case curl::fuzzer::proto::CURLOPT_HSTS_CTRL:
390
15.9k
    case curl::fuzzer::proto::CURLOPT_HTTP09_ALLOWED:
391
17.0k
    case curl::fuzzer::proto::CURLOPT_HTTPAUTH:
392
21.4k
    case curl::fuzzer::proto::CURLOPT_HTTPGET:
393
21.4k
    case curl::fuzzer::proto::CURLOPT_HTTP_CONTENT_DECODING:
394
21.5k
    case curl::fuzzer::proto::CURLOPT_HTTP_TRANSFER_DECODING:
395
21.7k
    case curl::fuzzer::proto::CURLOPT_IGNORE_CONTENT_LENGTH:
396
22.0k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
397
22.3k
    case curl::fuzzer::proto::CURLOPT_KEEP_SENDING_ON_ERROR:
398
22.8k
    case curl::fuzzer::proto::CURLOPT_MAXAGE_CONN:
399
23.1k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
400
24.4k
    case curl::fuzzer::proto::CURLOPT_MAXLIFETIME_CONN:
401
25.1k
    case curl::fuzzer::proto::CURLOPT_MAXREDIRS:
402
25.2k
    case curl::fuzzer::proto::CURLOPT_MIME_OPTIONS:
403
25.7k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
404
25.9k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
405
26.2k
    case curl::fuzzer::proto::CURLOPT_PATH_AS_IS:
406
26.8k
    case curl::fuzzer::proto::CURLOPT_POST:
407
26.9k
    case curl::fuzzer::proto::CURLOPT_POSTFIELDS:
408
27.0k
    case curl::fuzzer::proto::CURLOPT_POSTREDIR:
409
27.3k
    case curl::fuzzer::proto::CURLOPT_RANGE:
410
27.3k
    case curl::fuzzer::proto::CURLOPT_REFERER:
411
28.2k
    case curl::fuzzer::proto::CURLOPT_REQUEST_TARGET:
412
29.4k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
413
30.4k
    case curl::fuzzer::proto::CURLOPT_TIMECONDITION:
414
30.5k
    case curl::fuzzer::proto::CURLOPT_TIMEVALUE_LARGE:
415
31.0k
    case curl::fuzzer::proto::CURLOPT_TRANSFER_ENCODING:
416
31.3k
    case curl::fuzzer::proto::CURLOPT_UNRESTRICTED_AUTH:
417
32.1k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
418
32.5k
    case curl::fuzzer::proto::CURLOPT_UPLOAD_BUFFERSIZE:
419
33.0k
    case curl::fuzzer::proto::CURLOPT_USERAGENT:
420
33.1k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
421
34.4k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
422
35.5k
    case curl::fuzzer::proto::CURLOPT_XOAUTH2_BEARER:
423
35.5k
      return true;
424
425
405
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
426
606
    default:
427
606
      return false;
428
36.1k
  }
429
36.1k
}
430
431
/// Compact an option list before applying the general option-count bound.
432
/// Keeping a relevant option that appears after a long rejected prefix is
433
/// important for mutation density: bounding first would let unrelated options
434
/// crowd useful ones out of a protocol-specific lane.
435
template <typename Predicate>
436
116k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
437
116k
  auto* options = scenario->mutable_options();
438
116k
  int retained = 0;
439
598k
  for (int index = 0; index < options->size(); ++index) {
440
482k
    if (!predicate(options->Get(index).option_id())) {
441
14.6k
      continue;
442
14.6k
    }
443
467k
    if (retained != index) {
444
38.7k
      options->SwapElements(retained, index);
445
38.7k
    }
446
467k
    ++retained;
447
467k
  }
448
116k
  options->DeleteSubrange(retained, options->size() - retained);
449
116k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::RetainMatchingOptions<bool (*)(curl::fuzzer::proto::CurlOptionId)>(curl::fuzzer::proto::Scenario*, bool (*)(curl::fuzzer::proto::CurlOptionId))
Line
Count
Source
436
33.6k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
437
33.6k
  auto* options = scenario->mutable_options();
438
33.6k
  int retained = 0;
439
169k
  for (int index = 0; index < options->size(); ++index) {
440
135k
    if (!predicate(options->Get(index).option_id())) {
441
12.2k
      continue;
442
12.2k
    }
443
123k
    if (retained != index) {
444
31.1k
      options->SwapElements(retained, index);
445
31.1k
    }
446
123k
    ++retained;
447
123k
  }
448
33.6k
  options->DeleteSubrange(retained, options->size() - retained);
449
33.6k
}
target_policy.cc:void proto_fuzzer::(anonymous namespace)::RetainMatchingOptions<proto_fuzzer::(anonymous namespace)::RemoveFileTransferOnlyOptions(curl::fuzzer::proto::Scenario*)::$_0>(curl::fuzzer::proto::Scenario*, proto_fuzzer::(anonymous namespace)::RemoveFileTransferOnlyOptions(curl::fuzzer::proto::Scenario*)::$_0)
Line
Count
Source
436
83.1k
void RetainMatchingOptions(curl::fuzzer::proto::Scenario* scenario, Predicate predicate) {
437
83.1k
  auto* options = scenario->mutable_options();
438
83.1k
  int retained = 0;
439
429k
  for (int index = 0; index < options->size(); ++index) {
440
346k
    if (!predicate(options->Get(index).option_id())) {
441
2.43k
      continue;
442
2.43k
    }
443
344k
    if (retained != index) {
444
7.59k
      options->SwapElements(retained, index);
445
7.59k
    }
446
344k
    ++retained;
447
344k
  }
448
83.1k
  options->DeleteSubrange(retained, options->size() - retained);
449
83.1k
}
450
451
/// Keep the high-throughput HTTP lane free of options whose setup or state is
452
/// assigned to a deeper or protocol-specific target.
453
14.6k
void RetainCheapHttpOptions(curl::fuzzer::proto::Scenario* scenario) {
454
14.6k
  RetainMatchingOptions(scenario, &IsCheapHttpOption);
455
14.6k
}
456
457
/// Compact the option list before its shared cap so irrelevant TLS, WebSocket,
458
/// and file-transfer entries cannot crowd out origin traffic mutations.
459
5.54k
void RetainH2ProxyOriginOptions(curl::fuzzer::proto::Scenario* scenario) {
460
5.54k
  RetainMatchingOptions(scenario, &IsH2ProxyOriginOption);
461
5.54k
}
462
463
/// Return whether a scalar option can influence TELNET without selecting an
464
/// incompatible transfer mode or introducing external state. Protocol-
465
/// specific negotiation preferences use Scenario.telnet_options instead.
466
23.2k
bool IsCheapTelnetOption(curl::fuzzer::proto::CurlOptionId option_id) {
467
23.2k
  switch (option_id) {
468
6.95k
    case curl::fuzzer::proto::CURLOPT_CRLF:
469
13.0k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
470
14.4k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
471
16.3k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
472
17.4k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
473
17.4k
      return true;
474
475
2.83k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
476
5.78k
    default:
477
5.78k
      return false;
478
23.2k
  }
479
23.2k
}
480
481
/// Compact the TELNET option prefix so unrelated HTTP mutations cannot crowd
482
/// useful credentials, CRLF handling, and transfer-size controls out of the
483
/// fixed target's general option budget.
484
4.20k
void RetainCheapTelnetOptions(curl::fuzzer::proto::Scenario* scenario) {
485
4.20k
  RetainMatchingOptions(scenario, &IsCheapTelnetOption);
486
4.20k
}
487
488
/// Return whether an option can change a plaintext FTP transfer serviced by
489
/// the bounded control/data peer. Active mode and FTPS settings are omitted:
490
/// retaining them would select socket and TLS behavior this target does not
491
/// provide, turning otherwise-useful mutations into early setup failures.
492
29.6k
bool IsFtpOption(curl::fuzzer::proto::CurlOptionId option_id) {
493
29.6k
  switch (option_id) {
494
270
    case curl::fuzzer::proto::CURLOPT_APPEND:
495
1.38k
    case curl::fuzzer::proto::CURLOPT_BUFFERSIZE:
496
2.54k
    case curl::fuzzer::proto::CURLOPT_CRLF:
497
2.93k
    case curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST:
498
3.74k
    case curl::fuzzer::proto::CURLOPT_DIRLISTONLY:
499
4.21k
    case curl::fuzzer::proto::CURLOPT_FILETIME:
500
4.98k
    case curl::fuzzer::proto::CURLOPT_FTP_ACCOUNT:
501
8.53k
    case curl::fuzzer::proto::CURLOPT_FTP_ALTERNATIVE_TO_USER:
502
10.1k
    case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
503
12.2k
    case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
504
13.5k
    case curl::fuzzer::proto::CURLOPT_FTP_SKIP_PASV_IP:
505
14.0k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPSV:
506
14.7k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_PRET:
507
15.8k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
508
16.6k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
509
17.5k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
510
18.4k
    case curl::fuzzer::proto::CURLOPT_PASSWORD:
511
18.6k
    case curl::fuzzer::proto::CURLOPT_RANGE:
512
19.8k
    case curl::fuzzer::proto::CURLOPT_RESUME_FROM_LARGE:
513
21.0k
    case curl::fuzzer::proto::CURLOPT_TIMECONDITION:
514
21.2k
    case curl::fuzzer::proto::CURLOPT_TIMEVALUE_LARGE:
515
22.0k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
516
23.9k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
517
24.7k
    case curl::fuzzer::proto::CURLOPT_UPLOAD_BUFFERSIZE:
518
25.3k
    case curl::fuzzer::proto::CURLOPT_USERNAME:
519
26.7k
    case curl::fuzzer::proto::CURLOPT_USERPWD:
520
28.3k
    case curl::fuzzer::proto::CURLOPT_WILDCARDMATCH:
521
28.3k
      return true;
522
523
584
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
524
1.29k
    default:
525
1.29k
      return false;
526
29.6k
  }
527
29.6k
}
528
529
/// Keep the FTP target's general option budget focused on states its passive
530
/// peer can actually advance.
531
7.19k
void RetainFtpOptions(curl::fuzzer::proto::Scenario* scenario) { RetainMatchingOptions(scenario, &IsFtpOption); }
532
533
/// Return whether an option affects TFTP request construction, option
534
/// negotiation, transfer direction, or bounded body delivery. TFTP has no
535
/// connection reuse or stream-level controls, so retaining those settings
536
/// would add protobuf work without another state-machine edge in curl.
537
20.9k
bool IsTftpOption(curl::fuzzer::proto::CurlOptionId option_id) {
538
20.9k
  switch (option_id) {
539
2.66k
    case curl::fuzzer::proto::CURLOPT_CRLF:
540
3.59k
    case curl::fuzzer::proto::CURLOPT_INFILESIZE_LARGE:
541
4.16k
    case curl::fuzzer::proto::CURLOPT_MAXFILESIZE_LARGE:
542
11.1k
    case curl::fuzzer::proto::CURLOPT_NOBODY:
543
12.4k
    case curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE:
544
14.7k
    case curl::fuzzer::proto::CURLOPT_TFTP_NO_OPTIONS:
545
15.0k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
546
16.7k
    case curl::fuzzer::proto::CURLOPT_UPLOAD:
547
16.7k
      return true;
548
549
2.34k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
550
4.23k
    default:
551
4.23k
      return false;
552
20.9k
  }
553
20.9k
}
554
555
/// Keep the datagram lane from spending mutations on stream-only options.
556
2.01k
void RetainTftpOptions(curl::fuzzer::proto::Scenario* scenario) { RetainMatchingOptions(scenario, &IsTftpOption); }
557
558
/// Decode an integral oneof locally before the generated option canonicalizer
559
/// runs. Protocol mode bounds are policy, not setopt mechanics: folding them
560
/// here keeps nearly every mutation on a real FTP/TFTP state while the shared
561
/// option layer remains unaware of protocol-specific numeric ranges.
562
5.07k
std::uint64_t IntegralMutationValue(const curl::fuzzer::proto::SetOption& option) {
563
5.07k
  switch (option.value_case()) {
564
2.97k
    case curl::fuzzer::proto::SetOption::kUintValue:
565
2.97k
      return option.uint_value();
566
445
    case curl::fuzzer::proto::SetOption::kBoolValue:
567
445
      return option.bool_value() ? 1U : 0U;
568
499
    case curl::fuzzer::proto::SetOption::kStringValue:
569
1.65k
    case curl::fuzzer::proto::SetOption::VALUE_NOT_SET:
570
1.65k
      return 0;
571
5.07k
  }
572
0
  return 0;
573
5.07k
}
574
575
/// Fold small FTP enums onto curl's documented domains so random uint64 values
576
/// do not overwhelmingly stop at setopt validation before issuing a command.
577
7.19k
void CanonicalizeFtpOptionModes(curl::fuzzer::proto::Scenario* scenario) {
578
28.3k
  for (auto& option : *scenario->mutable_options()) {
579
28.3k
    switch (option.option_id()) {
580
1.59k
      case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
581
1.59k
        option.set_uint_value(IntegralMutationValue(option) % 3U);
582
1.59k
        break;
583
2.14k
      case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
584
2.14k
        option.set_uint_value(IntegralMutationValue(option) % 4U);
585
2.14k
        break;
586
24.6k
      default:
587
24.6k
        break;
588
28.3k
    }
589
28.3k
  }
590
7.19k
}
591
592
/// TFTP accepts block sizes from 8 through 65464. Mapping zero or a mismatched
593
/// oneof to the default 512 preserves a common valid request, while saturating
594
/// other values retains both lower/upper parser boundaries under mutation.
595
2.01k
void CanonicalizeTftpOptionModes(curl::fuzzer::proto::Scenario* scenario) {
596
16.7k
  for (auto& option : *scenario->mutable_options()) {
597
16.7k
    if (option.option_id() != curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE) {
598
15.4k
      continue;
599
15.4k
    }
600
1.34k
    std::uint64_t value = IntegralMutationValue(option);
601
1.34k
    if (value == 0) {
602
365
      value = 512;
603
365
    }
604
1.34k
    option.set_uint_value(std::max<std::uint64_t>(8, std::min<std::uint64_t>(value, 65464)));
605
1.34k
  }
606
2.01k
}
607
608
/// Identify options introduced for FTP/TFTP so existing fixed lanes do not
609
/// silently inherit dead mutations when the shared generated manifest grows.
610
/// Generic options retained by the FTP/TFTP allowlists are deliberately absent
611
/// here because they remain useful to HTTP, WebSocket, API, or timing targets.
612
346k
bool IsFileTransferOnlyOption(curl::fuzzer::proto::CurlOptionId option_id) {
613
346k
  switch (option_id) {
614
287
    case curl::fuzzer::proto::CURLOPT_APPEND:
615
460
    case curl::fuzzer::proto::CURLOPT_DIRLISTONLY:
616
524
    case curl::fuzzer::proto::CURLOPT_FTP_ACCOUNT:
617
584
    case curl::fuzzer::proto::CURLOPT_FTP_ALTERNATIVE_TO_USER:
618
858
    case curl::fuzzer::proto::CURLOPT_FTP_CREATE_MISSING_DIRS:
619
947
    case curl::fuzzer::proto::CURLOPT_FTP_FILEMETHOD:
620
1.06k
    case curl::fuzzer::proto::CURLOPT_FTP_SKIP_PASV_IP:
621
1.36k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_EPSV:
622
1.69k
    case curl::fuzzer::proto::CURLOPT_FTP_USE_PRET:
623
1.82k
    case curl::fuzzer::proto::CURLOPT_TFTP_BLKSIZE:
624
2.10k
    case curl::fuzzer::proto::CURLOPT_TFTP_NO_OPTIONS:
625
2.31k
    case curl::fuzzer::proto::CURLOPT_TRANSFERTEXT:
626
2.43k
    case curl::fuzzer::proto::CURLOPT_WILDCARDMATCH:
627
2.43k
      return true;
628
629
19.9k
    case curl::fuzzer::proto::CURL_OPTION_UNSPECIFIED:
630
344k
    default:
631
344k
      return false;
632
346k
  }
633
346k
}
634
635
/// Remove FTP/TFTP-only options while preserving the relative order of every
636
/// generic option an existing fixed target already consumed.
637
83.1k
void RemoveFileTransferOnlyOptions(curl::fuzzer::proto::Scenario* scenario) {
638
83.1k
  RetainMatchingOptions(
639
346k
      scenario, [](curl::fuzzer::proto::CurlOptionId option_id) { return !IsFileTransferOnlyOption(option_id); });
640
83.1k
}
641
642
/// Remove the stateful shapes assigned to the deep HTTP target. This happens
643
/// before BoundScenarioShape so a fast iteration never walks or normalizes a
644
/// MIME tree, upload script, or follow-on connection that it will discard.
645
/// Raw response chunks and request headers stay intact because they reach the
646
/// core HTTP parser cheaply and provide much of the legacy fuzzer's coverage.
647
14.6k
void RemoveDeepHttpShape(curl::fuzzer::proto::Scenario* scenario) {
648
14.6k
  scenario->clear_mime_post();
649
14.6k
  scenario->clear_upload();
650
14.6k
  scenario->clear_subsequent_connections();
651
652
14.6k
  auto* connection = scenario->mutable_connection();
653
14.6k
  connection->clear_server_frames();
654
14.6k
  connection->clear_manual_probes();
655
14.6k
  connection->clear_backpressure();
656
14.6k
}
657
658
/// Remove response forms the proxy peer cannot interpret. Raw chunks are the
659
/// HTTP/2 frame stream; WebSocket frames would merely add a second unrelated
660
/// binary grammar, and follow-on Connection messages cannot describe later
661
/// streams multiplexed on the already-open proxy socket.
662
5.54k
void RemoveIgnoredH2ProxyShape(curl::fuzzer::proto::Scenario* scenario) {
663
5.54k
  scenario->clear_subsequent_connections();
664
5.54k
  auto* connection = scenario->mutable_connection();
665
5.54k
  connection->clear_server_frames();
666
5.54k
  connection->clear_manual_probes();
667
5.54k
  connection->clear_backpressure();
668
5.54k
}
669
670
/// Remove fields the single-socket WebSocket driver cannot consume. MIME also
671
/// changes the HTTP request away from a useful Upgrade handshake, so retaining
672
/// either shape in fixed WS lanes gives LPM mutation work with no WS coverage
673
/// payoff. The mixed compatibility target has no postprocessor and keeps its
674
/// historical behavior.
675
22.8k
void RemoveIgnoredWebSocketShape(curl::fuzzer::proto::Scenario* scenario) {
676
22.8k
  scenario->clear_subsequent_connections();
677
22.8k
  scenario->clear_mime_post();
678
22.8k
}
679
680
/// Remove fields whose only effect in a TELNET lane would be protobuf work or
681
/// unsafe socket timing. TELNET's curl driver owns the thread until the peer
682
/// closes, so response backpressure and follow-on sockets cannot be serviced
683
/// by the outer event loop. Raw response fragments and the bounded upload stay
684
/// mutation-controlled because the dedicated mock can preload and drain them.
685
4.20k
void RemoveNonTelnetShape(curl::fuzzer::proto::Scenario* scenario) {
686
4.20k
  scenario->clear_subsequent_connections();
687
4.20k
  scenario->clear_request_headers();
688
4.20k
  scenario->clear_mime_post();
689
690
4.20k
  auto* connection = scenario->mutable_connection();
691
4.20k
  connection->clear_server_frames();
692
4.20k
  connection->clear_manual_probes();
693
4.20k
  connection->clear_backpressure();
694
4.20k
}
695
696
/// Remove the TELNET-only list and pause outcome from fixed event-driven
697
/// targets. The compatibility target skips postprocessing, so the runtime
698
/// repeats the pause-to-EOF guard before installing callbacks.
699
112k
void RemoveTelnetOnlyShape(curl::fuzzer::proto::Scenario* scenario) {
700
112k
  scenario->clear_telnet_options();
701
112k
  if (scenario->has_upload() && scenario->upload().terminal() == curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE) {
702
10
    scenario->mutable_upload()->set_terminal(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF);
703
10
  }
704
112k
}
705
706
/// Keep lifecycle work out of protocol-focused lanes. The API binary retains
707
/// this message explicitly; compatibility inputs have no postprocessor so
708
/// existing reproducers keep their historical serialized meaning.
709
104k
void RemoveApiOnlyShape(curl::fuzzer::proto::Scenario* scenario) { scenario->clear_api_plan(); }
710
711
/// Keep concurrent multi-handle work out of every other fixed lane. The
712
/// compatibility binary deliberately preserves newly-added unknown fields.
713
116k
void RemoveMultiOnlyShape(curl::fuzzer::proto::Scenario* scenario) { scenario->clear_multi_plan(); }
714
715
/// Remove stream-driver controls that neither file-transfer peer interprets.
716
/// FTP consumes raw byte chunks as control/data replies, while TFTP preserves
717
/// them as individual datagrams; structured WebSocket frames, manual probes,
718
/// and event-loop backpressure therefore cannot affect either curl protocol.
719
13.2k
void RemoveUnusedFileTransferConnectionShape(curl::fuzzer::proto::Connection* connection) {
720
13.2k
  connection->clear_server_frames();
721
13.2k
  connection->clear_manual_probes();
722
13.2k
  connection->clear_backpressure();
723
13.2k
}
724
725
/// Retain only the reusable shapes consumed by the FTP peer: one raw control
726
/// script, a bounded sequence of passive-data scripts, and optional upload
727
/// input. HTTP, TELNET, WebSocket, and public-API fields would otherwise absorb
728
/// mutations despite having no representation in an FTP exchange.
729
7.19k
void RemoveIgnoredFtpShape(curl::fuzzer::proto::Scenario* scenario) {
730
7.19k
  scenario->clear_request_headers();
731
7.19k
  scenario->clear_mime_post();
732
7.19k
  RemoveTelnetOnlyShape(scenario);
733
7.19k
  RemoveApiOnlyShape(scenario);
734
7.19k
  RemoveMultiOnlyShape(scenario);
735
736
7.19k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
737
7.19k
  TrimRepeated(scenario->mutable_subsequent_connections(), scenario_limits::kMaxConnections - 1);
738
7.19k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
739
4.05k
    RemoveUnusedFileTransferConnectionShape(&connection);
740
4.05k
  }
741
7.19k
}
742
743
/// Retain the primary raw response script because its entries are the ordered
744
/// UDP datagrams seen by curl, plus optional upload input for WRQ. TFTP cannot
745
/// consume follow-on stream connections or any higher-level protocol shape.
746
2.01k
void RemoveIgnoredTftpShape(curl::fuzzer::proto::Scenario* scenario) {
747
2.01k
  scenario->clear_subsequent_connections();
748
2.01k
  scenario->clear_request_headers();
749
2.01k
  scenario->clear_mime_post();
750
2.01k
  RemoveTelnetOnlyShape(scenario);
751
2.01k
  RemoveApiOnlyShape(scenario);
752
2.01k
  RemoveMultiOnlyShape(scenario);
753
2.01k
  RemoveUnusedFileTransferConnectionShape(scenario->mutable_connection());
754
2.01k
}
755
756
/// Preserve useful in-range mutations while folding ineffective extremes onto
757
/// meaningful boundaries. Zero remains special: it disables that individual
758
/// control and lets the other control provide the timing target's pressure.
759
31.0k
std::uint32_t CanonicalizeNonZero(std::uint32_t value, std::uint32_t minimum, std::uint32_t maximum) {
760
31.0k
  if (value == 0) {
761
12.9k
    return 0;
762
12.9k
  }
763
18.0k
  return std::max(minimum, std::min(value, maximum));
764
31.0k
}
765
766
/// Keep the timing target on the plaintext member of the protocol family.
767
/// TLS setup has its own cost profile and would obscure whether backpressure
768
/// mutations are exploring curl's send/receive state machines effectively.
769
13.3k
curl::fuzzer::proto::Scheme PlaintextScheme(curl::fuzzer::proto::Scheme scheme) {
770
13.3k
  switch (scheme) {
771
3.37k
    case curl::fuzzer::proto::SCHEME_WS:
772
3.37k
    case curl::fuzzer::proto::SCHEME_WSS:
773
3.37k
      return curl::fuzzer::proto::SCHEME_WS;
774
9.81k
    case curl::fuzzer::proto::SCHEME_HTTP:
775
9.81k
    case curl::fuzzer::proto::SCHEME_HTTPS:
776
9.81k
    case curl::fuzzer::proto::SCHEME_TELNET:
777
9.81k
    case curl::fuzzer::proto::SCHEME_FTP:
778
9.81k
    case curl::fuzzer::proto::SCHEME_TFTP:
779
9.83k
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
780
9.95k
    default:
781
9.95k
      return curl::fuzzer::proto::SCHEME_HTTP;
782
13.3k
  }
783
13.3k
}
784
785
/// Remove timing controls from every connection the structured message can
786
/// carry. Clearing only the primary script would let a mutated redirect turn a
787
/// fixed fast lane into the timed drive loop after its second socket opens.
788
69.8k
void ClearAllBackpressure(curl::fuzzer::proto::Scenario* scenario) {
789
69.8k
  if (scenario->has_connection()) {
790
69.8k
    scenario->mutable_connection()->clear_backpressure();
791
69.8k
  }
792
69.8k
  for (auto& connection : *scenario->mutable_subsequent_connections()) {
793
23.2k
    connection.clear_backpressure();
794
23.2k
  }
795
69.8k
}
796
797
/// Clamp one explicitly pressure-bearing follow-on script to the same useful
798
/// ranges as the timing lane's primary connection. An absent configuration is
799
/// left absent so merely adding a redirect response does not add waits.
800
4.11k
void CanonicalizeOptionalBackpressure(curl::fuzzer::proto::Connection* connection) {
801
4.11k
  if (!connection->has_backpressure()) {
802
1.81k
    return;
803
1.81k
  }
804
2.29k
  auto* backpressure = connection->mutable_backpressure();
805
2.29k
  if (backpressure->recv_buf_bytes() == 0 && backpressure->drain_limit() != 0) {
806
7
    backpressure->set_recv_buf_bytes(kDefaultBackpressureBufferBytes);
807
2.28k
  } else {
808
2.28k
    backpressure->set_recv_buf_bytes(
809
2.28k
        CanonicalizeNonZero(backpressure->recv_buf_bytes(), kMinBackpressureBufferBytes, kMaxBackpressureBufferBytes));
810
2.28k
  }
811
2.29k
  backpressure->set_drain_limit(CanonicalizeNonZero(backpressure->drain_limit(), 1, kMaxDrainBytesPerIteration));
812
2.29k
}
813
814
}  // namespace
815
816
/// Canonicalize the fields that determine which server and drive-loop policy
817
/// execute. Fast targets discard backpressure because one mutated non-zero
818
/// scalar otherwise opts an ordinary input into hundreds of timed waits. The
819
/// timing target does the inverse: it guarantees a non-default buffer setting
820
/// so its CPU allocation remains focused on the intentionally slower paths.
821
116k
void ApplyTargetPolicy(curl::fuzzer::proto::Scenario* scenario, TargetProfile profile) {
822
116k
  if (scenario == nullptr) {
823
0
    return;
824
0
  }
825
826
116k
  if (profile == TargetProfile::kCompatibility) {
827
    // The original target's existing corpus predates profile splitting. A
828
    // no-op here makes the type safe to pass around while its binary continues
829
    // to omit postprocessor registration altogether. The append-only FTP/TFTP
830
    // scheme values do not justify rewriting historical mixed-lane inputs.
831
0
    return;
832
0
  }
833
834
  // Only the dedicated HTTPS peer consumes a certificate-chain selector.
835
  // Remove it before protocol-specific early returns so other fixed targets
836
  // do not spend mutations on inert TLS server state.
837
116k
  if (profile != TargetProfile::kFastHttps) {
838
98.9k
    scenario->clear_tls_certificate_chain();
839
98.9k
  }
840
841
116k
  if (profile == TargetProfile::kFastTelnet) {
842
    // Set the scheme before general bounds so the TELNET-specific upload and
843
    // PAUSE budgets are selected rather than event-driven compatibility ones.
844
4.20k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_TELNET);
845
4.20k
    RemoveApiOnlyShape(scenario);
846
4.20k
    RemoveMultiOnlyShape(scenario);
847
4.20k
    RemoveNonTelnetShape(scenario);
848
4.20k
    RetainCheapTelnetOptions(scenario);
849
4.20k
    BoundScenarioShape(scenario);
850
4.20k
    BoundTelnetResponse(scenario->mutable_connection());
851
4.20k
    return;
852
4.20k
  }
853
854
112k
  if (profile == TargetProfile::kFastHttp) {
855
14.6k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
856
14.6k
    RemoveApiOnlyShape(scenario);
857
14.6k
    RemoveMultiOnlyShape(scenario);
858
14.6k
    RemoveTelnetOnlyShape(scenario);
859
14.6k
    RemoveDeepHttpShape(scenario);
860
14.6k
    RetainCheapHttpOptions(scenario);
861
14.6k
    BoundScenarioShape(scenario);
862
14.6k
    return;
863
14.6k
  }
864
865
97.8k
  if (profile == TargetProfile::kH2Proxy) {
866
5.54k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
867
5.54k
    RemoveApiOnlyShape(scenario);
868
5.54k
    RemoveMultiOnlyShape(scenario);
869
5.54k
    RemoveTelnetOnlyShape(scenario);
870
5.54k
    RemoveIgnoredH2ProxyShape(scenario);
871
5.54k
    RetainH2ProxyOriginOptions(scenario);
872
5.54k
    BoundScenarioShape(scenario);
873
5.54k
    CanonicalizeH2ProxyOriginAuthority(scenario);
874
5.54k
    return;
875
5.54k
  }
876
877
92.3k
  if (profile == TargetProfile::kFastFtp) {
878
7.19k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_FTP);
879
7.19k
    RemoveIgnoredFtpShape(scenario);
880
7.19k
    RetainFtpOptions(scenario);
881
7.19k
    CanonicalizeFtpOptionModes(scenario);
882
7.19k
    BoundScenarioShape(scenario);
883
7.19k
    CanonicalizeFtpAuthority(scenario);
884
7.19k
    return;
885
7.19k
  }
886
887
85.1k
  if (profile == TargetProfile::kFastTftp) {
888
2.01k
    scenario->set_scheme(curl::fuzzer::proto::SCHEME_TFTP);
889
2.01k
    RemoveIgnoredTftpShape(scenario);
890
2.01k
    RetainTftpOptions(scenario);
891
2.01k
    CanonicalizeTftpOptionModes(scenario);
892
2.01k
    BoundScenarioShape(scenario);
893
2.01k
    CanonicalizeTftpAuthority(scenario);
894
2.01k
    return;
895
2.01k
  }
896
897
  // Select the lane's scheme before applying scheme-sensitive upload bounds.
898
  // The scheme field is itself mutable, so bounding first could accidentally
899
  // give an HTTP/WS case TELNET's smaller payload budget merely because that
900
  // was the input's pre-policy value.
901
83.1k
  switch (profile) {
902
0
    case TargetProfile::kCompatibility:
903
0
      return;
904
20.0k
    case TargetProfile::kDeepHttp:
905
20.0k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
906
20.0k
      break;
907
11.9k
    case TargetProfile::kApi:
908
11.9k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
909
11.9k
      break;
910
502
    case TargetProfile::kMulti:
911
502
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTP);
912
502
      break;
913
17.8k
    case TargetProfile::kFastHttps:
914
17.8k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_HTTPS);
915
17.8k
      break;
916
0
    case TargetProfile::kH2Proxy:
917
      // The early path removes proxy-incompatible fields before general
918
      // bounds, keeping raw frame mutation dense.
919
0
      return;
920
10.6k
    case TargetProfile::kFastWebSocket:
921
10.6k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_WS);
922
10.6k
      break;
923
8.74k
    case TargetProfile::kFastSecureWebSocket:
924
8.74k
      scenario->set_scheme(curl::fuzzer::proto::SCHEME_WSS);
925
8.74k
      break;
926
13.3k
    case TargetProfile::kTiming:
927
13.3k
      scenario->set_scheme(PlaintextScheme(scenario->scheme()));
928
13.3k
      break;
929
0
    case TargetProfile::kFastHttp:
930
0
    case TargetProfile::kFastTelnet:
931
0
    case TargetProfile::kFastFtp:
932
0
    case TargetProfile::kFastTftp:
933
      // Protocol-specific early-return paths selected their scheme above.
934
0
      return;
935
83.1k
  }
936
937
  // TELNET's retained slist and synchronous pause have no observable, safe
938
  // meaning in the fixed event-driven lanes. Clear them before walking the
939
  // general shape so only the compatibility and TELNET targets can retain
940
  // those values.
941
83.1k
  RemoveTelnetOnlyShape(scenario);
942
83.1k
  if (profile != TargetProfile::kApi) {
943
71.1k
    RemoveApiOnlyShape(scenario);
944
71.1k
  }
945
83.1k
  if (profile != TargetProfile::kMulti) {
946
82.6k
    RemoveMultiOnlyShape(scenario);
947
82.6k
  }
948
83.1k
  RemoveFileTransferOnlyOptions(scenario);
949
83.1k
  BoundScenarioShape(scenario);
950
951
83.1k
  switch (profile) {
952
0
    case TargetProfile::kCompatibility:
953
0
      return;
954
0
    case TargetProfile::kFastHttp:
955
      // Handled before the general bounds so discarded deep shapes are never
956
      // traversed on the fast path.
957
0
      return;
958
959
20.0k
    case TargetProfile::kDeepHttp:
960
20.0k
      ClearAllBackpressure(scenario);
961
20.0k
      return;
962
963
11.9k
    case TargetProfile::kApi:
964
11.9k
      ClearAllBackpressure(scenario);
965
11.9k
      if (scenario->host_path().size() > scenario_limits::kMaxApiStringBytes) {
966
11
        scenario->mutable_host_path()->resize(scenario_limits::kMaxApiStringBytes);
967
11
      }
968
11.9k
      if (scenario->has_api_plan()) {
969
4.85k
        BoundApiPlanShape(scenario->mutable_api_plan());
970
4.85k
      }
971
11.9k
      return;
972
973
502
    case TargetProfile::kMulti: {
974
502
      ClearAllBackpressure(scenario);
975
502
      CanonicalizeMultiAuthority(scenario);
976
502
      auto* plan = scenario->mutable_multi_plan();
977
502
      BoundMultiPlanShape(plan);
978
502
      TrimRepeated(scenario->mutable_subsequent_connections(), plan->transfer_count() - 1U);
979
502
      return;
980
0
    }
981
982
17.8k
    case TargetProfile::kFastHttps:
983
17.8k
      ClearAllBackpressure(scenario);
984
17.8k
      CanonicalizeTlsAuthority(scenario);
985
17.8k
      switch (scenario->tls_certificate_chain()) {
986
17.7k
        case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_DEFAULT_EC:
987
17.8k
        case curl::fuzzer::proto::TLS_CERTIFICATE_CHAIN_ALL_KEY_TYPES:
988
17.8k
          break;
989
4
        default:
990
4
          scenario->clear_tls_certificate_chain();
991
4
          break;
992
17.8k
      }
993
17.8k
      return;
994
995
17.8k
    case TargetProfile::kH2Proxy:
996
      // Handled by the protocol-specific early path above.
997
0
      return;
998
999
10.6k
    case TargetProfile::kFastWebSocket:
1000
10.6k
      ClearAllBackpressure(scenario);
1001
10.6k
      RemoveIgnoredWebSocketShape(scenario);
1002
10.6k
      return;
1003
1004
8.74k
    case TargetProfile::kFastSecureWebSocket:
1005
8.74k
      ClearAllBackpressure(scenario);
1006
8.74k
      RemoveIgnoredWebSocketShape(scenario);
1007
8.74k
      return;
1008
1009
0
    case TargetProfile::kFastTelnet:
1010
      // Handled before the general bounds so its protocol-specific limits are
1011
      // selected from the start.
1012
0
      return;
1013
1014
0
    case TargetProfile::kFastFtp:
1015
0
    case TargetProfile::kFastTftp:
1016
      // Their peers consume narrower raw-script shapes, pruned before general
1017
      // bounds so ignored fields never tax these fast paths.
1018
0
      return;
1019
1020
13.3k
    case TargetProfile::kTiming: {
1021
13.3k
      if (scenario->scheme() == curl::fuzzer::proto::SCHEME_WS) {
1022
3.37k
        RemoveIgnoredWebSocketShape(scenario);
1023
3.37k
      }
1024
13.3k
      auto* backpressure = scenario->mutable_connection()->mutable_backpressure();
1025
13.3k
      if (backpressure->recv_buf_bytes() == 0) {
1026
        // A drain limit alone cannot fill the default AF_UNIX buffer with the
1027
        // harness's bounded upload. Always tighten the socket so this lane
1028
        // represents real pressure, not merely selection of the timed loop.
1029
205
        backpressure->set_recv_buf_bytes(kDefaultBackpressureBufferBytes);
1030
13.1k
      } else {
1031
13.1k
        backpressure->set_recv_buf_bytes(CanonicalizeNonZero(backpressure->recv_buf_bytes(),
1032
13.1k
                                                             kMinBackpressureBufferBytes, kMaxBackpressureBufferBytes));
1033
13.1k
      }
1034
13.3k
      backpressure->set_drain_limit(CanonicalizeNonZero(backpressure->drain_limit(), 1, kMaxDrainBytesPerIteration));
1035
13.3k
      for (auto& connection : *scenario->mutable_subsequent_connections()) {
1036
4.11k
        CanonicalizeOptionalBackpressure(&connection);
1037
4.11k
      }
1038
13.3k
      return;
1039
0
    }
1040
83.1k
  }
1041
83.1k
}
1042
1043
}  // namespace proto_fuzzer