/src/curl_fuzzer/proto_fuzzer/scenario_runner.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 RunScenario. |
9 | | |
10 | | #include "proto_fuzzer/scenario_runner.h" |
11 | | |
12 | | #include <curl/curl.h> |
13 | | #include <curl/header.h> |
14 | | |
15 | | #include <algorithm> |
16 | | #include <cstddef> |
17 | | #include <memory> |
18 | | #include <string> |
19 | | |
20 | | #include "proto_fuzzer/api_lifecycle.h" |
21 | | #include "proto_fuzzer/bounded_anonymous_input_file.h" |
22 | | #include "proto_fuzzer/curl_raii.h" |
23 | | #include "proto_fuzzer/ftp_mock_server.h" |
24 | | #include "proto_fuzzer/mock_server.h" |
25 | | #include "proto_fuzzer/mock_server_base.h" |
26 | | #include "proto_fuzzer/multi_transfer_runner.h" |
27 | | #include "proto_fuzzer/option_apply.h" |
28 | | #include "proto_fuzzer/request_data.h" |
29 | | #include "proto_fuzzer/scenario_limits.h" |
30 | | #include "proto_fuzzer/socks4_mock_server.h" |
31 | | #include "proto_fuzzer/telnet_mock_server.h" |
32 | | #include "proto_fuzzer/tftp_mock_server.h" |
33 | | #include "proto_fuzzer/websocket_mock_server.h" |
34 | | |
35 | | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
36 | | #include "proto_fuzzer/h2_origin_mock_server.h" |
37 | | #include "proto_fuzzer/h2_proxy_mock_server.h" |
38 | | #include "proto_fuzzer/tls_mock_server.h" |
39 | | #endif |
40 | | #if defined(PROTO_FUZZER_HAS_HTTP3_MOCK_SERVER) |
41 | | #include "proto_fuzzer/http3_mock_server.h" |
42 | | #endif |
43 | | |
44 | | namespace proto_fuzzer { |
45 | | |
46 | | namespace { |
47 | | |
48 | | constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO; |
49 | | constexpr std::size_t kMaxResultHeaders = 16; |
50 | | constexpr char kDevNullPath[] = "/dev/null"; |
51 | | constexpr char kAltSvcOrigin[] = "altsvc-origin.test"; |
52 | | constexpr char kAltSvcLoopbackResolve[] = "*:80:127.0.1.127"; |
53 | | |
54 | | /// Refuse any resolver request not satisfied by the harness-owned Alt-Svc |
55 | | /// host-cache entry. The callback runs before every supported resolver backend |
56 | | /// starts work, so a mutated alternate host or port cannot reach ambient DNS. |
57 | 27 | int AbortUnexpectedAltSvcResolve(void* /*resolver_state*/, void* /*reserved*/, void* /*userdata*/) { return 1; } |
58 | | |
59 | | /// Direct RunScenario callers do not necessarily pass through target policy. |
60 | | /// Only detach CONNECT_TO when the URL authority is the canonical host whose |
61 | | /// default HTTP port is covered by kAltSvcLoopbackResolve. |
62 | 689 | bool HasCanonicalAltSvcAuthority(const std::string& host_path) { |
63 | 689 | const std::size_t host_size = sizeof(kAltSvcOrigin) - 1; |
64 | 689 | if (host_path.compare(0, host_size, kAltSvcOrigin) != 0) { |
65 | 0 | return false; |
66 | 0 | } |
67 | 689 | return host_path.size() == host_size || host_path[host_size] == '/' || host_path[host_size] == '?' || |
68 | 9 | host_path[host_size] == '#'; |
69 | 689 | } |
70 | | |
71 | | /// Copy the runtime-visible prefix of one protobuf field into its anonymous |
72 | | /// file. ApplyTargetPolicy normally enforces the same bounds before execution; |
73 | | /// repeating them here keeps direct RunScenario callers safe and observable. |
74 | | void PrepareInputFile(const std::string& contents, std::size_t* remaining_bytes, |
75 | 135k | BoundedAnonymousInputFile* input_file) { |
76 | 135k | if (contents.empty() || *remaining_bytes == 0) { |
77 | 133k | return; |
78 | 133k | } |
79 | 2.10k | const std::size_t size = std::min(contents.size(), std::min(scenario_limits::kMaxFileInputBytes, *remaining_bytes)); |
80 | 2.10k | const auto* bytes = reinterpret_cast<const std::uint8_t*>(contents.data()); |
81 | 2.10k | if (input_file->Write(bytes, size)) { |
82 | 2.10k | *remaining_bytes -= size; |
83 | 2.10k | } |
84 | 2.10k | } |
85 | | |
86 | | /// Prefix NETRC bytes with a parser-neutral blank line. curl's text loader |
87 | | /// drops comment-only lines and otherwise represents an empty result as a null |
88 | | /// buffer; retaining one newline keeps that upstream edge case out of this |
89 | | /// general protocol fuzzer while retaining every mutation byte in the file |
90 | | /// presented to curl's loader. |
91 | | /// The shared budget is charged only for protobuf bytes, not this fixed guard. |
92 | | void PrepareNetrcInputFile(const std::string& contents, std::size_t* remaining_bytes, |
93 | 26.5k | BoundedAnonymousInputFile* input_file) { |
94 | 26.5k | if (contents.empty() || *remaining_bytes == 0) { |
95 | 25.8k | return; |
96 | 25.8k | } |
97 | 784 | const std::size_t size = std::min(contents.size(), std::min(scenario_limits::kMaxFileInputBytes, *remaining_bytes)); |
98 | 784 | std::string guarded_contents; |
99 | 784 | guarded_contents.reserve(size + 1); |
100 | 784 | guarded_contents.push_back('\n'); |
101 | 784 | guarded_contents.append(contents.data(), size); |
102 | 784 | const auto* bytes = reinterpret_cast<const std::uint8_t*>(guarded_contents.data()); |
103 | 784 | if (input_file->Write(bytes, guarded_contents.size())) { |
104 | 784 | *remaining_bytes -= size; |
105 | 784 | } |
106 | 784 | } |
107 | | |
108 | | /// Apply parser input paths after the fixed baseline but before scenario |
109 | | /// options. COOKIEFILE is read-only by definition. Alt-Svc and HSTS use one |
110 | | /// option for both input and cleanup output, so load the anonymous file first |
111 | | /// and then restore /dev/null as the final save destination. Both loaders keep |
112 | | /// entries from earlier files. |
113 | | void ApplyDeepHttpFileOptions(CURL* easy, const BoundedAnonymousInputFile& cookie_file, |
114 | | const BoundedAnonymousInputFile& altsvc_file, const BoundedAnonymousInputFile& hsts_file, |
115 | 53.1k | const BoundedAnonymousInputFile& netrc_file) { |
116 | 53.1k | if (const char* path = cookie_file.path()) { |
117 | 1.26k | (void)curl_easy_setopt(easy, CURLOPT_COOKIEFILE, path); |
118 | 1.26k | } |
119 | 53.1k | if (const char* path = altsvc_file.path()) { |
120 | 1.37k | (void)curl_easy_setopt(easy, CURLOPT_ALTSVC, path); |
121 | 1.37k | (void)curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNullPath); |
122 | 1.37k | } |
123 | 53.1k | if (const char* path = hsts_file.path()) { |
124 | 962 | (void)curl_easy_setopt(easy, CURLOPT_HSTS, path); |
125 | 962 | (void)curl_easy_setopt(easy, CURLOPT_HSTS, kDevNullPath); |
126 | 962 | } |
127 | 53.1k | if (const char* path = netrc_file.path()) { |
128 | 1.56k | (void)curl_easy_setopt(easy, CURLOPT_NETRC_FILE, path); |
129 | 1.56k | (void)curl_easy_setopt(easy, CURLOPT_NETRC, CURL_NETRC_REQUIRED); |
130 | 1.56k | } |
131 | 53.1k | } scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ApplyDeepHttpFileOptions(void*, proto_fuzzer::BoundedAnonymousInputFile const&, proto_fuzzer::BoundedAnonymousInputFile const&, proto_fuzzer::BoundedAnonymousInputFile const&, proto_fuzzer::BoundedAnonymousInputFile const&) Line | Count | Source | 115 | 26.5k | const BoundedAnonymousInputFile& netrc_file) { | 116 | 26.5k | if (const char* path = cookie_file.path()) { | 117 | 633 | (void)curl_easy_setopt(easy, CURLOPT_COOKIEFILE, path); | 118 | 633 | } | 119 | 26.5k | if (const char* path = altsvc_file.path()) { | 120 | 689 | (void)curl_easy_setopt(easy, CURLOPT_ALTSVC, path); | 121 | 689 | (void)curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNullPath); | 122 | 689 | } | 123 | 26.5k | if (const char* path = hsts_file.path()) { | 124 | 481 | (void)curl_easy_setopt(easy, CURLOPT_HSTS, path); | 125 | 481 | (void)curl_easy_setopt(easy, CURLOPT_HSTS, kDevNullPath); | 126 | 481 | } | 127 | 26.5k | if (const char* path = netrc_file.path()) { | 128 | 784 | (void)curl_easy_setopt(easy, CURLOPT_NETRC_FILE, path); | 129 | 784 | (void)curl_easy_setopt(easy, CURLOPT_NETRC, CURL_NETRC_REQUIRED); | 130 | 784 | } | 131 | 26.5k | } |
scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ApplyDeepHttpFileOptions(void*, proto_fuzzer::BoundedAnonymousInputFile const&, proto_fuzzer::BoundedAnonymousInputFile const&, proto_fuzzer::BoundedAnonymousInputFile const&, proto_fuzzer::BoundedAnonymousInputFile const&) Line | Count | Source | 115 | 26.5k | const BoundedAnonymousInputFile& netrc_file) { | 116 | 26.5k | if (const char* path = cookie_file.path()) { | 117 | 633 | (void)curl_easy_setopt(easy, CURLOPT_COOKIEFILE, path); | 118 | 633 | } | 119 | 26.5k | if (const char* path = altsvc_file.path()) { | 120 | 689 | (void)curl_easy_setopt(easy, CURLOPT_ALTSVC, path); | 121 | 689 | (void)curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNullPath); | 122 | 689 | } | 123 | 26.5k | if (const char* path = hsts_file.path()) { | 124 | 481 | (void)curl_easy_setopt(easy, CURLOPT_HSTS, path); | 125 | 481 | (void)curl_easy_setopt(easy, CURLOPT_HSTS, kDevNullPath); | 126 | 481 | } | 127 | 26.5k | if (const char* path = netrc_file.path()) { | 128 | 784 | (void)curl_easy_setopt(easy, CURLOPT_NETRC_FILE, path); | 129 | 784 | (void)curl_easy_setopt(easy, CURLOPT_NETRC, CURL_NETRC_REQUIRED); | 130 | 784 | } | 131 | 26.5k | } |
|
132 | | |
133 | | /// Install CRL input only after the TLS mock has supplied its in-memory trust |
134 | | /// anchor. CURLOPT_CRLFILE stores the path and each TLS backend opens it while |
135 | | /// constructing the verified connection; it never writes back to the file. |
136 | 111k | void ApplyTlsFileOptions(CURL* easy, const BoundedAnonymousInputFile& crl_file) { |
137 | 111k | if (const char* path = crl_file.path()) { |
138 | 600 | (void)curl_easy_setopt(easy, CURLOPT_CRLFILE, path); |
139 | 600 | } |
140 | 111k | } scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ApplyTlsFileOptions(void*, proto_fuzzer::BoundedAnonymousInputFile const&) Line | Count | Source | 136 | 55.6k | void ApplyTlsFileOptions(CURL* easy, const BoundedAnonymousInputFile& crl_file) { | 137 | 55.6k | if (const char* path = crl_file.path()) { | 138 | 300 | (void)curl_easy_setopt(easy, CURLOPT_CRLFILE, path); | 139 | 300 | } | 140 | 55.6k | } |
scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ApplyTlsFileOptions(void*, proto_fuzzer::BoundedAnonymousInputFile const&) Line | Count | Source | 136 | 55.6k | void ApplyTlsFileOptions(CURL* easy, const BoundedAnonymousInputFile& crl_file) { | 137 | 55.6k | if (const char* path = crl_file.path()) { | 138 | 300 | (void)curl_easy_setopt(easy, CURLOPT_CRLFILE, path); | 139 | 300 | } | 140 | 55.6k | } |
|
141 | | |
142 | | /// Probe each public getinfo return family and the response-header API after |
143 | | /// curl has settled the transfer. Applications commonly inspect these APIs, |
144 | | /// but a harness that only drives I/O leaves their type dispatch and |
145 | | /// post-transfer state unexecuted even when the corresponding parser ran. |
146 | | /// The chosen values are handle-owned or scalar: notably CERTINFO exercises |
147 | | /// the pointer/slist dispatch family without materialising a separately-owned |
148 | | /// cookie/engine list. Header iteration is capped independently of response |
149 | | /// size so this unconditional coverage cannot dominate a fuzz iteration. |
150 | 473k | void ProbeTransferResults(CURL* easy) { |
151 | 473k | char* string_result = nullptr; |
152 | 473k | long long_result = 0; |
153 | 473k | double double_result = 0; |
154 | 473k | curl_off_t offset_result = 0; |
155 | 473k | curl_socket_t socket_result = CURL_SOCKET_BAD; |
156 | 473k | struct curl_certinfo* certinfo_result = nullptr; |
157 | | |
158 | 473k | (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result); |
159 | 473k | (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result); |
160 | 473k | (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result); |
161 | 473k | (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result); |
162 | 473k | (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result); |
163 | 473k | (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result); |
164 | | |
165 | 473k | struct curl_header* header = nullptr; |
166 | 473k | (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header); |
167 | 473k | header = nullptr; |
168 | 971k | for (std::size_t index = 0; index < kMaxResultHeaders; ++index) { |
169 | 959k | header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header); |
170 | 959k | if (header == nullptr) { |
171 | 461k | break; |
172 | 461k | } |
173 | 959k | } |
174 | 473k | } scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ProbeTransferResults(void*) Line | Count | Source | 150 | 236k | void ProbeTransferResults(CURL* easy) { | 151 | 236k | char* string_result = nullptr; | 152 | 236k | long long_result = 0; | 153 | 236k | double double_result = 0; | 154 | 236k | curl_off_t offset_result = 0; | 155 | 236k | curl_socket_t socket_result = CURL_SOCKET_BAD; | 156 | 236k | struct curl_certinfo* certinfo_result = nullptr; | 157 | | | 158 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result); | 159 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result); | 160 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result); | 161 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result); | 162 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result); | 163 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result); | 164 | | | 165 | 236k | struct curl_header* header = nullptr; | 166 | 236k | (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header); | 167 | 236k | header = nullptr; | 168 | 485k | for (std::size_t index = 0; index < kMaxResultHeaders; ++index) { | 169 | 479k | header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header); | 170 | 479k | if (header == nullptr) { | 171 | 230k | break; | 172 | 230k | } | 173 | 479k | } | 174 | 236k | } |
scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ProbeTransferResults(void*) Line | Count | Source | 150 | 236k | void ProbeTransferResults(CURL* easy) { | 151 | 236k | char* string_result = nullptr; | 152 | 236k | long long_result = 0; | 153 | 236k | double double_result = 0; | 154 | 236k | curl_off_t offset_result = 0; | 155 | 236k | curl_socket_t socket_result = CURL_SOCKET_BAD; | 156 | 236k | struct curl_certinfo* certinfo_result = nullptr; | 157 | | | 158 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result); | 159 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result); | 160 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result); | 161 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result); | 162 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result); | 163 | 236k | (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result); | 164 | | | 165 | 236k | struct curl_header* header = nullptr; | 166 | 236k | (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header); | 167 | 236k | header = nullptr; | 168 | 485k | for (std::size_t index = 0; index < kMaxResultHeaders; ++index) { | 169 | 479k | header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header); | 170 | 479k | if (header == nullptr) { | 171 | 230k | break; | 172 | 230k | } | 173 | 479k | } | 174 | 236k | } |
|
175 | | |
176 | | /// Map a Scheme enum to the URL scheme literal. |
177 | 267k | const char* SchemePrefix(curl::fuzzer::proto::Scheme scheme) { |
178 | 267k | switch (scheme) { |
179 | 135k | case curl::fuzzer::proto::SCHEME_HTTP: |
180 | 135k | return "http"; |
181 | 74.9k | case curl::fuzzer::proto::SCHEME_HTTPS: |
182 | 74.9k | return "https"; |
183 | 19.2k | case curl::fuzzer::proto::SCHEME_WS: |
184 | 19.2k | return "ws"; |
185 | 11.4k | case curl::fuzzer::proto::SCHEME_WSS: |
186 | 11.4k | return "wss"; |
187 | 5.09k | case curl::fuzzer::proto::SCHEME_TELNET: |
188 | 5.09k | return "telnet"; |
189 | 9.03k | case curl::fuzzer::proto::SCHEME_FTP: |
190 | 9.03k | return "ftp"; |
191 | 2.02k | case curl::fuzzer::proto::SCHEME_TFTP: |
192 | 2.02k | return "tftp"; |
193 | 5.41k | case curl::fuzzer::proto::SCHEME_GOPHER: |
194 | 5.41k | return "gopher"; |
195 | 4.22k | case curl::fuzzer::proto::SCHEME_GOPHERS: |
196 | 4.22k | return "gophers"; |
197 | 99 | case curl::fuzzer::proto::SCHEME_UNSPECIFIED: |
198 | 99 | default: |
199 | 99 | return nullptr; |
200 | 267k | } |
201 | 267k | } |
202 | | |
203 | | /// Pick the peer implementation authorized by both protocol and target mode. |
204 | | /// The compatibility target must keep treating HTTPS response bytes as raw TLS |
205 | | /// records, while the dedicated HTTPS lane interprets them as decrypted HTTP. |
206 | | /// Keeping that semantic boundary in the closed run-mode enum prevents a new |
207 | | /// protobuf field from silently changing old OSS-Fuzz reproducers. |
208 | | std::unique_ptr<MockServerBase> MakeMockServerForScenario(const curl::fuzzer::proto::Scenario& scenario, |
209 | 265k | ScenarioRunMode mode) { |
210 | 265k | if (mode == ScenarioRunMode::kHttp3Coverage) { |
211 | | #if defined(PROTO_FUZZER_HAS_HTTP3_MOCK_SERVER) |
212 | | return std::make_unique<Http3MockServer>(scenario.tls_certificate_chain()); |
213 | | #else |
214 | 7.04k | return nullptr; |
215 | 7.04k | #endif |
216 | 7.04k | } |
217 | | |
218 | 258k | if (mode == ScenarioRunMode::kH2ProxyCoverage) { |
219 | 11.3k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
220 | 11.3k | return std::make_unique<H2ProxyMockServer>(); |
221 | | #else |
222 | | // MemorySanitizer builds deliberately omit OpenSSL. Keep the target |
223 | | // binary available to OSS-Fuzz, but do not pretend a plaintext mock can |
224 | | // negotiate the ALPN gate required to enter cf-h2-proxy. |
225 | | return nullptr; |
226 | | #endif |
227 | 11.3k | } |
228 | | |
229 | 246k | if (mode == ScenarioRunMode::kTlsHttp2Coverage) { |
230 | 10.8k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
231 | 10.8k | return std::make_unique<H2OriginMockServer>(scenario.tls_certificate_chain()); |
232 | | #else |
233 | | // This target remains buildable under MemorySanitizer, whose curl build |
234 | | // omits the OpenSSL server dependency required for TLS/ALPN h2. |
235 | | return nullptr; |
236 | | #endif |
237 | 10.8k | } |
238 | | |
239 | 235k | if (mode == ScenarioRunMode::kSocks4Coverage) { |
240 | 11.7k | return std::make_unique<Socks4MockServer>(scenario.socks_proxy_mode()); |
241 | 11.7k | } |
242 | | |
243 | 224k | switch (scenario.scheme()) { |
244 | 111k | case curl::fuzzer::proto::SCHEME_HTTP: |
245 | 111k | return std::make_unique<MockServer>(); |
246 | 56.9k | case curl::fuzzer::proto::SCHEME_HTTPS: |
247 | 56.9k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
248 | 56.9k | if (mode == ScenarioRunMode::kTlsCoverage) { |
249 | 55.6k | return std::make_unique<TlsMockServer>(scenario.tls_certificate_chain()); |
250 | 55.6k | } |
251 | | #else |
252 | | (void)mode; |
253 | | #endif |
254 | 1.36k | return std::make_unique<MockServer>(); |
255 | 5.41k | case curl::fuzzer::proto::SCHEME_GOPHER: |
256 | 5.41k | return mode == ScenarioRunMode::kGopherCoverage ? std::make_unique<MockServer>() : nullptr; |
257 | 4.22k | case curl::fuzzer::proto::SCHEME_GOPHERS: |
258 | 4.22k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
259 | 4.22k | if (mode == ScenarioRunMode::kGopherCoverage) { |
260 | 4.22k | return std::make_unique<TlsMockServer>(scenario.tls_certificate_chain()); |
261 | 4.22k | } |
262 | 2 | return nullptr; |
263 | | #else |
264 | | return nullptr; |
265 | | #endif |
266 | 18.8k | case curl::fuzzer::proto::SCHEME_WS: |
267 | 29.8k | case curl::fuzzer::proto::SCHEME_WSS: |
268 | 29.8k | return std::make_unique<WebSocketMockServer>(); |
269 | 4.83k | case curl::fuzzer::proto::SCHEME_TELNET: |
270 | 4.83k | return std::make_unique<TelnetMockServer>(); |
271 | 9.03k | case curl::fuzzer::proto::SCHEME_FTP: |
272 | | // New numeric enum values may already occur in the historical mixed |
273 | | // corpus as unknown fields. Only the fixed FTP profile may reinterpret |
274 | | // one as a live two-channel protocol exchange. |
275 | 9.03k | if (mode == ScenarioRunMode::kFtpCoverage) { |
276 | 9.03k | return std::make_unique<FtpMockServer>(); |
277 | 9.03k | } |
278 | 2 | return nullptr; |
279 | 2.02k | case curl::fuzzer::proto::SCHEME_TFTP: |
280 | | // TFTP changes the callback transport from a preconnected stream to a |
281 | | // real UDP endpoint, so compatibility inputs must not opt into it merely |
282 | | // because this build learned a new enum value. |
283 | 2.02k | if (mode == ScenarioRunMode::kTftpCoverage) { |
284 | 2.01k | return std::make_unique<TftpMockServer>(); |
285 | 2.01k | } |
286 | 2 | return nullptr; |
287 | 0 | case curl::fuzzer::proto::SCHEME_UNSPECIFIED: |
288 | 0 | default: |
289 | 0 | return nullptr; |
290 | 224k | } |
291 | 224k | } |
292 | | |
293 | | } // namespace |
294 | | |
295 | | /// Implement the bounded orchestration contract documented on the public |
296 | | /// declaration; keeping argument docs there avoids two drifting descriptions. |
297 | 574k | int RunScenario(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) { |
298 | 574k | if (mode == ScenarioRunMode::kMultiTransfer) { |
299 | 39.9k | (void)proto_fuzzer::RunMultiTransferScenario(scenario); |
300 | 39.9k | return 0; |
301 | 39.9k | } |
302 | | |
303 | 534k | const char* prefix = SchemePrefix(scenario.scheme()); |
304 | 534k | if (prefix == nullptr || scenario.host_path().empty()) { |
305 | 4.18k | return 0; |
306 | 4.18k | } |
307 | | |
308 | 530k | std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode); |
309 | 530k | if (!mock) { |
310 | 16 | return 0; |
311 | 16 | } |
312 | | |
313 | | // Declaration order is an ownership invariant: reverse destruction keeps |
314 | | // anonymous parser files, CONNECT_TO/RESOLVE storage, and share callback |
315 | | // userdata alive through easy cleanup. This matters for incomplete |
316 | | // transfers, where cleanup still flushes caches and can release retained |
317 | | // references. |
318 | 530k | std::unique_ptr<ApiLifecycle> api_lifecycle; |
319 | 530k | CurlSlistPtr connect_to; |
320 | 530k | CurlSlistPtr altsvc_resolve; |
321 | 530k | BoundedAnonymousInputFile cookie_file(scenario_limits::kMaxFileInputBytes); |
322 | 530k | BoundedAnonymousInputFile altsvc_file(scenario_limits::kMaxFileInputBytes); |
323 | 530k | BoundedAnonymousInputFile hsts_file(scenario_limits::kMaxFileInputBytes); |
324 | 530k | BoundedAnonymousInputFile netrc_file(scenario_limits::kMaxFileInputBytes + 1); |
325 | 530k | BoundedAnonymousInputFile crl_file(scenario_limits::kMaxFileInputBytes); |
326 | 530k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { |
327 | 53.1k | std::size_t remaining_file_bytes = scenario_limits::kMaxFileInputTotalBytes; |
328 | 53.1k | PrepareInputFile(scenario.cookie_file(), &remaining_file_bytes, &cookie_file); |
329 | 53.1k | PrepareInputFile(scenario.altsvc_file(), &remaining_file_bytes, &altsvc_file); |
330 | 53.1k | PrepareInputFile(scenario.hsts_file(), &remaining_file_bytes, &hsts_file); |
331 | 53.1k | PrepareNetrcInputFile(scenario.netrc_file(), &remaining_file_bytes, &netrc_file); |
332 | 477k | } else if (mode == ScenarioRunMode::kTlsCoverage) { |
333 | 111k | std::size_t remaining_file_bytes = scenario_limits::kMaxFileInputTotalBytes; |
334 | 111k | PrepareInputFile(scenario.crl_file(), &remaining_file_bytes, &crl_file); |
335 | 111k | } |
336 | 530k | CurlEasyPtr easy(curl_easy_init()); |
337 | 530k | if (!easy) { |
338 | 0 | return 0; |
339 | 0 | } |
340 | | |
341 | 530k | std::string url = std::string(prefix) + "://" + scenario.host_path(); |
342 | 533k | const auto configure_easy = [&] { |
343 | 533k | connect_to.reset(ApplyBaselineOptions(easy.get(), scenario.scheme(), scenario.trace_ids())); |
344 | 533k | curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str()); |
345 | 533k | mock->Install(easy.get()); |
346 | | |
347 | 533k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { |
348 | 53.1k | ApplyDeepHttpFileOptions(easy.get(), cookie_file, altsvc_file, hsts_file, netrc_file); |
349 | 480k | } else if (mode == ScenarioRunMode::kTlsCoverage) { |
350 | 111k | ApplyTlsFileOptions(easy.get(), crl_file); |
351 | 111k | } |
352 | | |
353 | | // Compatibility inputs deliberately bypass the mutating postprocessor, |
354 | | // so enforce the shared option prefix again at the runtime boundary. The |
355 | | // helper still ignores individual CURLcodes: the fuzzer stresses curl |
356 | | // rather than treating rejected combinations as harness failures. |
357 | 533k | (void)ApplyScenarioOptions(easy.get(), scenario); |
358 | 533k | }; scenario_runner.cc:proto_fuzzer::RunScenario(curl::fuzzer::proto::Scenario const&, proto_fuzzer::ScenarioRunMode)::$_0::operator()() const Line | Count | Source | 342 | 266k | const auto configure_easy = [&] { | 343 | 266k | connect_to.reset(ApplyBaselineOptions(easy.get(), scenario.scheme(), scenario.trace_ids())); | 344 | 266k | curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str()); | 345 | 266k | mock->Install(easy.get()); | 346 | | | 347 | 266k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { | 348 | 26.5k | ApplyDeepHttpFileOptions(easy.get(), cookie_file, altsvc_file, hsts_file, netrc_file); | 349 | 240k | } else if (mode == ScenarioRunMode::kTlsCoverage) { | 350 | 55.6k | ApplyTlsFileOptions(easy.get(), crl_file); | 351 | 55.6k | } | 352 | | | 353 | | // Compatibility inputs deliberately bypass the mutating postprocessor, | 354 | | // so enforce the shared option prefix again at the runtime boundary. The | 355 | | // helper still ignores individual CURLcodes: the fuzzer stresses curl | 356 | | // rather than treating rejected combinations as harness failures. | 357 | 266k | (void)ApplyScenarioOptions(easy.get(), scenario); | 358 | 266k | }; |
scenario_runner.cc:proto_fuzzer::RunScenario(curl::fuzzer::proto::Scenario const&, proto_fuzzer::ScenarioRunMode)::$_0::operator()() const Line | Count | Source | 342 | 266k | const auto configure_easy = [&] { | 343 | 266k | connect_to.reset(ApplyBaselineOptions(easy.get(), scenario.scheme(), scenario.trace_ids())); | 344 | 266k | curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str()); | 345 | 266k | mock->Install(easy.get()); | 346 | | | 347 | 266k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { | 348 | 26.5k | ApplyDeepHttpFileOptions(easy.get(), cookie_file, altsvc_file, hsts_file, netrc_file); | 349 | 240k | } else if (mode == ScenarioRunMode::kTlsCoverage) { | 350 | 55.6k | ApplyTlsFileOptions(easy.get(), crl_file); | 351 | 55.6k | } | 352 | | | 353 | | // Compatibility inputs deliberately bypass the mutating postprocessor, | 354 | | // so enforce the shared option prefix again at the runtime boundary. The | 355 | | // helper still ignores individual CURLcodes: the fuzzer stresses curl | 356 | | // rather than treating rejected combinations as harness failures. | 357 | 266k | (void)ApplyScenarioOptions(easy.get(), scenario); | 358 | 266k | }; |
|
359 | 530k | configure_easy(); |
360 | | |
361 | 530k | if (mode == ScenarioRunMode::kDeepHttpCoverage && altsvc_file.path() != nullptr && |
362 | 1.37k | HasCanonicalAltSvcAuthority(scenario.host_path())) { |
363 | 1.37k | altsvc_resolve.reset(curl_slist_append(nullptr, kAltSvcLoopbackResolve)); |
364 | 1.37k | if (altsvc_resolve != nullptr && curl_easy_setopt(easy.get(), CURLOPT_RESOLVE, altsvc_resolve.get()) == CURLE_OK && |
365 | 1.37k | curl_easy_setopt(easy.get(), CURLOPT_RESOLVER_START_FUNCTION, &AbortUnexpectedAltSvcResolve) == CURLE_OK) { |
366 | | // CONNECT_TO wins before curl consults Alt-Svc. The wildcard DNS-cache |
367 | | // entry keeps both the fixed origin and port-80 alternate destinations |
368 | | // inside the socket callback; the resolver hook fails closed otherwise. |
369 | 1.37k | (void)curl_easy_setopt(easy.get(), CURLOPT_CONNECT_TO, nullptr); |
370 | 1.37k | } |
371 | 1.37k | } |
372 | | |
373 | 530k | if (mode == ScenarioRunMode::kResolverCoverage) { |
374 | | // The normal CONNECT_TO baseline deliberately bypasses DNS. This lane |
375 | | // removes only that override; OPENSOCKET still returns the in-process |
376 | | // socketpair, so no resolved address can receive network traffic. |
377 | 25.0k | (void)curl_easy_setopt(easy.get(), CURLOPT_CONNECT_TO, nullptr); |
378 | 25.0k | } |
379 | | |
380 | 530k | const curl::fuzzer::proto::ApiPlan* api_plan = |
381 | 530k | mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr; |
382 | 530k | if (api_plan != nullptr && api_plan->reset_easy()) { |
383 | | // Reset deliberately drops every pointer-valued option before its backing |
384 | | // list is freed. Reapplying the exact scenario then lets the transfer |
385 | | // populate post-reset state instead of turning reset coverage into a |
386 | | // guaranteed malformed request. |
387 | 3.46k | curl_easy_reset(easy.get()); |
388 | 3.46k | connect_to.reset(); |
389 | 3.46k | configure_easy(); |
390 | 3.46k | } |
391 | | |
392 | 530k | if (api_plan != nullptr) { |
393 | 10.2k | api_lifecycle = std::make_unique<ApiLifecycle>(easy.get(), *api_plan, url); |
394 | 10.2k | } |
395 | | |
396 | 530k | { |
397 | | // HTTP headers, MIME bodies, TELNET options, and callback userdata are |
398 | | // pointer-valued state that libcurl does not copy. Keep their owner around |
399 | | // the entire multi-handle drive, then let it detach them while `easy` is |
400 | | // still valid. This inner scope is deliberate: easy.reset() below must |
401 | | // never run before the owner's destructor clears those options. |
402 | 530k | ScenarioRequestData request_data(easy.get(), scenario, mode == ScenarioRunMode::kResolverCoverage); |
403 | 530k | if (mode == ScenarioRunMode::kResolverCoverage && !request_data.resolve_entries_ready()) { |
404 | 0 | return 0; |
405 | 0 | } |
406 | 530k | mock->ConfigureRequestData(&request_data); |
407 | 530k | const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode(); |
408 | 530k | if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) { |
409 | 1.88k | mock->DriveEasyScenario(easy.get(), scenario); |
410 | 528k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) { |
411 | 198 | mock->DriveEasyScenario(easy.get(), scenario, true); |
412 | 528k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY) { |
413 | 128 | (void)mock->DriveConnectOnlyScenario(easy.get(), scenario); |
414 | 528k | } else { |
415 | 528k | mock->DriveScenario( |
416 | 528k | easy.get(), scenario, |
417 | 528k | mode == ScenarioRunMode::kHttp3Coverage || drive_mode == curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET, |
418 | 528k | api_plan != nullptr && api_plan->wake_multi(), api_plan != nullptr && api_plan->pause_response_once()); |
419 | 528k | } |
420 | 530k | if (api_lifecycle != nullptr) { |
421 | 10.2k | const bool retains_internal_multi = drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM || |
422 | 8.35k | drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS || |
423 | 8.15k | drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY; |
424 | 10.2k | api_lifecycle->ProbeTransferResults(retains_internal_multi); |
425 | 10.2k | api_lifecycle->ProbeEasyDuplication(); |
426 | 520k | } else if (mode != ScenarioRunMode::kFastProtocol) { |
427 | 473k | ProbeTransferResults(easy.get()); |
428 | 473k | } |
429 | 530k | } |
430 | | |
431 | | // Easy cleanup is the reliable share-detach boundary even if the bounded |
432 | | // drive stopped with a connection attached. The lifecycle object—and thus |
433 | | // lock callback userdata—outlives it, then releases share-owned caches. |
434 | 0 | easy.reset(); |
435 | 530k | connect_to.reset(); |
436 | 530k | api_lifecycle.reset(); |
437 | 530k | return 0; |
438 | 530k | } proto_fuzzer::RunScenario(curl::fuzzer::proto::Scenario const&, proto_fuzzer::ScenarioRunMode) Line | Count | Source | 297 | 287k | int RunScenario(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) { | 298 | 287k | if (mode == ScenarioRunMode::kMultiTransfer) { | 299 | 19.9k | (void)proto_fuzzer::RunMultiTransferScenario(scenario); | 300 | 19.9k | return 0; | 301 | 19.9k | } | 302 | | | 303 | 267k | const char* prefix = SchemePrefix(scenario.scheme()); | 304 | 267k | if (prefix == nullptr || scenario.host_path().empty()) { | 305 | 2.09k | return 0; | 306 | 2.09k | } | 307 | | | 308 | 265k | std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode); | 309 | 265k | if (!mock) { | 310 | 8 | return 0; | 311 | 8 | } | 312 | | | 313 | | // Declaration order is an ownership invariant: reverse destruction keeps | 314 | | // anonymous parser files, CONNECT_TO/RESOLVE storage, and share callback | 315 | | // userdata alive through easy cleanup. This matters for incomplete | 316 | | // transfers, where cleanup still flushes caches and can release retained | 317 | | // references. | 318 | 265k | std::unique_ptr<ApiLifecycle> api_lifecycle; | 319 | 265k | CurlSlistPtr connect_to; | 320 | 265k | CurlSlistPtr altsvc_resolve; | 321 | 265k | BoundedAnonymousInputFile cookie_file(scenario_limits::kMaxFileInputBytes); | 322 | 265k | BoundedAnonymousInputFile altsvc_file(scenario_limits::kMaxFileInputBytes); | 323 | 265k | BoundedAnonymousInputFile hsts_file(scenario_limits::kMaxFileInputBytes); | 324 | 265k | BoundedAnonymousInputFile netrc_file(scenario_limits::kMaxFileInputBytes + 1); | 325 | 265k | BoundedAnonymousInputFile crl_file(scenario_limits::kMaxFileInputBytes); | 326 | 265k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { | 327 | 26.5k | std::size_t remaining_file_bytes = scenario_limits::kMaxFileInputTotalBytes; | 328 | 26.5k | PrepareInputFile(scenario.cookie_file(), &remaining_file_bytes, &cookie_file); | 329 | 26.5k | PrepareInputFile(scenario.altsvc_file(), &remaining_file_bytes, &altsvc_file); | 330 | 26.5k | PrepareInputFile(scenario.hsts_file(), &remaining_file_bytes, &hsts_file); | 331 | 26.5k | PrepareNetrcInputFile(scenario.netrc_file(), &remaining_file_bytes, &netrc_file); | 332 | 238k | } else if (mode == ScenarioRunMode::kTlsCoverage) { | 333 | 55.6k | std::size_t remaining_file_bytes = scenario_limits::kMaxFileInputTotalBytes; | 334 | 55.6k | PrepareInputFile(scenario.crl_file(), &remaining_file_bytes, &crl_file); | 335 | 55.6k | } | 336 | 265k | CurlEasyPtr easy(curl_easy_init()); | 337 | 265k | if (!easy) { | 338 | 0 | return 0; | 339 | 0 | } | 340 | | | 341 | 265k | std::string url = std::string(prefix) + "://" + scenario.host_path(); | 342 | 265k | const auto configure_easy = [&] { | 343 | 265k | connect_to.reset(ApplyBaselineOptions(easy.get(), scenario.scheme(), scenario.trace_ids())); | 344 | 265k | curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str()); | 345 | 265k | mock->Install(easy.get()); | 346 | | | 347 | 265k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { | 348 | 265k | ApplyDeepHttpFileOptions(easy.get(), cookie_file, altsvc_file, hsts_file, netrc_file); | 349 | 265k | } else if (mode == ScenarioRunMode::kTlsCoverage) { | 350 | 265k | ApplyTlsFileOptions(easy.get(), crl_file); | 351 | 265k | } | 352 | | | 353 | | // Compatibility inputs deliberately bypass the mutating postprocessor, | 354 | | // so enforce the shared option prefix again at the runtime boundary. The | 355 | | // helper still ignores individual CURLcodes: the fuzzer stresses curl | 356 | | // rather than treating rejected combinations as harness failures. | 357 | 265k | (void)ApplyScenarioOptions(easy.get(), scenario); | 358 | 265k | }; | 359 | 265k | configure_easy(); | 360 | | | 361 | 265k | if (mode == ScenarioRunMode::kDeepHttpCoverage && altsvc_file.path() != nullptr && | 362 | 689 | HasCanonicalAltSvcAuthority(scenario.host_path())) { | 363 | 689 | altsvc_resolve.reset(curl_slist_append(nullptr, kAltSvcLoopbackResolve)); | 364 | 689 | if (altsvc_resolve != nullptr && curl_easy_setopt(easy.get(), CURLOPT_RESOLVE, altsvc_resolve.get()) == CURLE_OK && | 365 | 689 | curl_easy_setopt(easy.get(), CURLOPT_RESOLVER_START_FUNCTION, &AbortUnexpectedAltSvcResolve) == CURLE_OK) { | 366 | | // CONNECT_TO wins before curl consults Alt-Svc. The wildcard DNS-cache | 367 | | // entry keeps both the fixed origin and port-80 alternate destinations | 368 | | // inside the socket callback; the resolver hook fails closed otherwise. | 369 | 689 | (void)curl_easy_setopt(easy.get(), CURLOPT_CONNECT_TO, nullptr); | 370 | 689 | } | 371 | 689 | } | 372 | | | 373 | 265k | if (mode == ScenarioRunMode::kResolverCoverage) { | 374 | | // The normal CONNECT_TO baseline deliberately bypasses DNS. This lane | 375 | | // removes only that override; OPENSOCKET still returns the in-process | 376 | | // socketpair, so no resolved address can receive network traffic. | 377 | 12.5k | (void)curl_easy_setopt(easy.get(), CURLOPT_CONNECT_TO, nullptr); | 378 | 12.5k | } | 379 | | | 380 | 265k | const curl::fuzzer::proto::ApiPlan* api_plan = | 381 | 265k | mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr; | 382 | 265k | if (api_plan != nullptr && api_plan->reset_easy()) { | 383 | | // Reset deliberately drops every pointer-valued option before its backing | 384 | | // list is freed. Reapplying the exact scenario then lets the transfer | 385 | | // populate post-reset state instead of turning reset coverage into a | 386 | | // guaranteed malformed request. | 387 | 1.73k | curl_easy_reset(easy.get()); | 388 | 1.73k | connect_to.reset(); | 389 | 1.73k | configure_easy(); | 390 | 1.73k | } | 391 | | | 392 | 265k | if (api_plan != nullptr) { | 393 | 5.12k | api_lifecycle = std::make_unique<ApiLifecycle>(easy.get(), *api_plan, url); | 394 | 5.12k | } | 395 | | | 396 | 265k | { | 397 | | // HTTP headers, MIME bodies, TELNET options, and callback userdata are | 398 | | // pointer-valued state that libcurl does not copy. Keep their owner around | 399 | | // the entire multi-handle drive, then let it detach them while `easy` is | 400 | | // still valid. This inner scope is deliberate: easy.reset() below must | 401 | | // never run before the owner's destructor clears those options. | 402 | 265k | ScenarioRequestData request_data(easy.get(), scenario, mode == ScenarioRunMode::kResolverCoverage); | 403 | 265k | if (mode == ScenarioRunMode::kResolverCoverage && !request_data.resolve_entries_ready()) { | 404 | 0 | return 0; | 405 | 0 | } | 406 | 265k | mock->ConfigureRequestData(&request_data); | 407 | 265k | const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode(); | 408 | 265k | if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) { | 409 | 944 | mock->DriveEasyScenario(easy.get(), scenario); | 410 | 264k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) { | 411 | 99 | mock->DriveEasyScenario(easy.get(), scenario, true); | 412 | 264k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY) { | 413 | 64 | (void)mock->DriveConnectOnlyScenario(easy.get(), scenario); | 414 | 264k | } else { | 415 | 264k | mock->DriveScenario( | 416 | 264k | easy.get(), scenario, | 417 | 264k | mode == ScenarioRunMode::kHttp3Coverage || drive_mode == curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET, | 418 | 264k | api_plan != nullptr && api_plan->wake_multi(), api_plan != nullptr && api_plan->pause_response_once()); | 419 | 264k | } | 420 | 265k | if (api_lifecycle != nullptr) { | 421 | 5.12k | const bool retains_internal_multi = drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM || | 422 | 4.17k | drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS || | 423 | 4.07k | drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY; | 424 | 5.12k | api_lifecycle->ProbeTransferResults(retains_internal_multi); | 425 | 5.12k | api_lifecycle->ProbeEasyDuplication(); | 426 | 260k | } else if (mode != ScenarioRunMode::kFastProtocol) { | 427 | 236k | ProbeTransferResults(easy.get()); | 428 | 236k | } | 429 | 265k | } | 430 | | | 431 | | // Easy cleanup is the reliable share-detach boundary even if the bounded | 432 | | // drive stopped with a connection attached. The lifecycle object—and thus | 433 | | // lock callback userdata—outlives it, then releases share-owned caches. | 434 | 0 | easy.reset(); | 435 | 265k | connect_to.reset(); | 436 | 265k | api_lifecycle.reset(); | 437 | 265k | return 0; | 438 | 265k | } |
proto_fuzzer::RunScenario(curl::fuzzer::proto::Scenario const&, proto_fuzzer::ScenarioRunMode) Line | Count | Source | 297 | 287k | int RunScenario(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) { | 298 | 287k | if (mode == ScenarioRunMode::kMultiTransfer) { | 299 | 19.9k | (void)proto_fuzzer::RunMultiTransferScenario(scenario); | 300 | 19.9k | return 0; | 301 | 19.9k | } | 302 | | | 303 | 267k | const char* prefix = SchemePrefix(scenario.scheme()); | 304 | 267k | if (prefix == nullptr || scenario.host_path().empty()) { | 305 | 2.09k | return 0; | 306 | 2.09k | } | 307 | | | 308 | 265k | std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode); | 309 | 265k | if (!mock) { | 310 | 8 | return 0; | 311 | 8 | } | 312 | | | 313 | | // Declaration order is an ownership invariant: reverse destruction keeps | 314 | | // anonymous parser files, CONNECT_TO/RESOLVE storage, and share callback | 315 | | // userdata alive through easy cleanup. This matters for incomplete | 316 | | // transfers, where cleanup still flushes caches and can release retained | 317 | | // references. | 318 | 265k | std::unique_ptr<ApiLifecycle> api_lifecycle; | 319 | 265k | CurlSlistPtr connect_to; | 320 | 265k | CurlSlistPtr altsvc_resolve; | 321 | 265k | BoundedAnonymousInputFile cookie_file(scenario_limits::kMaxFileInputBytes); | 322 | 265k | BoundedAnonymousInputFile altsvc_file(scenario_limits::kMaxFileInputBytes); | 323 | 265k | BoundedAnonymousInputFile hsts_file(scenario_limits::kMaxFileInputBytes); | 324 | 265k | BoundedAnonymousInputFile netrc_file(scenario_limits::kMaxFileInputBytes + 1); | 325 | 265k | BoundedAnonymousInputFile crl_file(scenario_limits::kMaxFileInputBytes); | 326 | 265k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { | 327 | 26.5k | std::size_t remaining_file_bytes = scenario_limits::kMaxFileInputTotalBytes; | 328 | 26.5k | PrepareInputFile(scenario.cookie_file(), &remaining_file_bytes, &cookie_file); | 329 | 26.5k | PrepareInputFile(scenario.altsvc_file(), &remaining_file_bytes, &altsvc_file); | 330 | 26.5k | PrepareInputFile(scenario.hsts_file(), &remaining_file_bytes, &hsts_file); | 331 | 26.5k | PrepareNetrcInputFile(scenario.netrc_file(), &remaining_file_bytes, &netrc_file); | 332 | 238k | } else if (mode == ScenarioRunMode::kTlsCoverage) { | 333 | 55.6k | std::size_t remaining_file_bytes = scenario_limits::kMaxFileInputTotalBytes; | 334 | 55.6k | PrepareInputFile(scenario.crl_file(), &remaining_file_bytes, &crl_file); | 335 | 55.6k | } | 336 | 265k | CurlEasyPtr easy(curl_easy_init()); | 337 | 265k | if (!easy) { | 338 | 0 | return 0; | 339 | 0 | } | 340 | | | 341 | 265k | std::string url = std::string(prefix) + "://" + scenario.host_path(); | 342 | 265k | const auto configure_easy = [&] { | 343 | 265k | connect_to.reset(ApplyBaselineOptions(easy.get(), scenario.scheme(), scenario.trace_ids())); | 344 | 265k | curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str()); | 345 | 265k | mock->Install(easy.get()); | 346 | | | 347 | 265k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { | 348 | 265k | ApplyDeepHttpFileOptions(easy.get(), cookie_file, altsvc_file, hsts_file, netrc_file); | 349 | 265k | } else if (mode == ScenarioRunMode::kTlsCoverage) { | 350 | 265k | ApplyTlsFileOptions(easy.get(), crl_file); | 351 | 265k | } | 352 | | | 353 | | // Compatibility inputs deliberately bypass the mutating postprocessor, | 354 | | // so enforce the shared option prefix again at the runtime boundary. The | 355 | | // helper still ignores individual CURLcodes: the fuzzer stresses curl | 356 | | // rather than treating rejected combinations as harness failures. | 357 | 265k | (void)ApplyScenarioOptions(easy.get(), scenario); | 358 | 265k | }; | 359 | 265k | configure_easy(); | 360 | | | 361 | 265k | if (mode == ScenarioRunMode::kDeepHttpCoverage && altsvc_file.path() != nullptr && | 362 | 689 | HasCanonicalAltSvcAuthority(scenario.host_path())) { | 363 | 689 | altsvc_resolve.reset(curl_slist_append(nullptr, kAltSvcLoopbackResolve)); | 364 | 689 | if (altsvc_resolve != nullptr && curl_easy_setopt(easy.get(), CURLOPT_RESOLVE, altsvc_resolve.get()) == CURLE_OK && | 365 | 689 | curl_easy_setopt(easy.get(), CURLOPT_RESOLVER_START_FUNCTION, &AbortUnexpectedAltSvcResolve) == CURLE_OK) { | 366 | | // CONNECT_TO wins before curl consults Alt-Svc. The wildcard DNS-cache | 367 | | // entry keeps both the fixed origin and port-80 alternate destinations | 368 | | // inside the socket callback; the resolver hook fails closed otherwise. | 369 | 689 | (void)curl_easy_setopt(easy.get(), CURLOPT_CONNECT_TO, nullptr); | 370 | 689 | } | 371 | 689 | } | 372 | | | 373 | 265k | if (mode == ScenarioRunMode::kResolverCoverage) { | 374 | | // The normal CONNECT_TO baseline deliberately bypasses DNS. This lane | 375 | | // removes only that override; OPENSOCKET still returns the in-process | 376 | | // socketpair, so no resolved address can receive network traffic. | 377 | 12.5k | (void)curl_easy_setopt(easy.get(), CURLOPT_CONNECT_TO, nullptr); | 378 | 12.5k | } | 379 | | | 380 | 265k | const curl::fuzzer::proto::ApiPlan* api_plan = | 381 | 265k | mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr; | 382 | 265k | if (api_plan != nullptr && api_plan->reset_easy()) { | 383 | | // Reset deliberately drops every pointer-valued option before its backing | 384 | | // list is freed. Reapplying the exact scenario then lets the transfer | 385 | | // populate post-reset state instead of turning reset coverage into a | 386 | | // guaranteed malformed request. | 387 | 1.73k | curl_easy_reset(easy.get()); | 388 | 1.73k | connect_to.reset(); | 389 | 1.73k | configure_easy(); | 390 | 1.73k | } | 391 | | | 392 | 265k | if (api_plan != nullptr) { | 393 | 5.12k | api_lifecycle = std::make_unique<ApiLifecycle>(easy.get(), *api_plan, url); | 394 | 5.12k | } | 395 | | | 396 | 265k | { | 397 | | // HTTP headers, MIME bodies, TELNET options, and callback userdata are | 398 | | // pointer-valued state that libcurl does not copy. Keep their owner around | 399 | | // the entire multi-handle drive, then let it detach them while `easy` is | 400 | | // still valid. This inner scope is deliberate: easy.reset() below must | 401 | | // never run before the owner's destructor clears those options. | 402 | 265k | ScenarioRequestData request_data(easy.get(), scenario, mode == ScenarioRunMode::kResolverCoverage); | 403 | 265k | if (mode == ScenarioRunMode::kResolverCoverage && !request_data.resolve_entries_ready()) { | 404 | 0 | return 0; | 405 | 0 | } | 406 | 265k | mock->ConfigureRequestData(&request_data); | 407 | 265k | const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode(); | 408 | 265k | if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) { | 409 | 944 | mock->DriveEasyScenario(easy.get(), scenario); | 410 | 264k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) { | 411 | 99 | mock->DriveEasyScenario(easy.get(), scenario, true); | 412 | 264k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY) { | 413 | 64 | (void)mock->DriveConnectOnlyScenario(easy.get(), scenario); | 414 | 264k | } else { | 415 | 264k | mock->DriveScenario( | 416 | 264k | easy.get(), scenario, | 417 | 264k | mode == ScenarioRunMode::kHttp3Coverage || drive_mode == curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET, | 418 | 264k | api_plan != nullptr && api_plan->wake_multi(), api_plan != nullptr && api_plan->pause_response_once()); | 419 | 264k | } | 420 | 265k | if (api_lifecycle != nullptr) { | 421 | 5.12k | const bool retains_internal_multi = drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM || | 422 | 4.17k | drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS || | 423 | 4.07k | drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY; | 424 | 5.12k | api_lifecycle->ProbeTransferResults(retains_internal_multi); | 425 | 5.12k | api_lifecycle->ProbeEasyDuplication(); | 426 | 260k | } else if (mode != ScenarioRunMode::kFastProtocol) { | 427 | 236k | ProbeTransferResults(easy.get()); | 428 | 236k | } | 429 | 265k | } | 430 | | | 431 | | // Easy cleanup is the reliable share-detach boundary even if the bounded | 432 | | // drive stopped with a connection attached. The lifecycle object—and thus | 433 | | // lock callback userdata—outlives it, then releases share-owned caches. | 434 | 0 | easy.reset(); | 435 | 265k | connect_to.reset(); | 436 | 265k | api_lifecycle.reset(); | 437 | 265k | return 0; | 438 | 265k | } |
|
439 | | |
440 | | } // namespace proto_fuzzer |