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