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