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