/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 <chrono> |
16 | | #include <cstddef> |
17 | | #include <memory> |
18 | | #include <string> |
19 | | #include <string_view> |
20 | | |
21 | | #include "proto_fuzzer/api_lifecycle.h" |
22 | | #include "proto_fuzzer/dict_mock_server.h" |
23 | | #include "proto_fuzzer/ftp_mock_server.h" |
24 | | #include "proto_fuzzer/h2_cleartext_mock_server.h" |
25 | | #include "proto_fuzzer/happy_eyeballs_mock_server.h" |
26 | | #include "proto_fuzzer/http_transport_mock_server.h" |
27 | | #include "proto_fuzzer/imap_mock_server.h" |
28 | | #include "proto_fuzzer/ipfs_url.h" |
29 | | #include "proto_fuzzer/ldap_mock_server.h" |
30 | | #include "proto_fuzzer/mock_server.h" |
31 | | #include "proto_fuzzer/mock_server_base.h" |
32 | | #include "proto_fuzzer/mqtt_mock_server.h" |
33 | | #include "proto_fuzzer/multi_transfer_runner.h" |
34 | | #include "proto_fuzzer/option_apply.h" |
35 | | #include "proto_fuzzer/pop3_mock_server.h" |
36 | | #include "proto_fuzzer/proxy_option_mock_server.h" |
37 | | #include "proto_fuzzer/request_data.h" |
38 | | #include "proto_fuzzer/resolver_cancel_gate.h" |
39 | | #include "proto_fuzzer/rtsp_mock_server.h" |
40 | | #include "proto_fuzzer/smtp_mock_server.h" |
41 | | #include "proto_fuzzer/socks4_mock_server.h" |
42 | | #include "proto_fuzzer/telnet_mock_server.h" |
43 | | #include "proto_fuzzer/tftp_mock_server.h" |
44 | | #include "proto_fuzzer/transfer_session.h" |
45 | | #include "proto_fuzzer/websocket_mock_server.h" |
46 | | |
47 | | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
48 | | #include "proto_fuzzer/doh_https_rr_mock_server.h" |
49 | | #include "proto_fuzzer/ftps_mock_server.h" |
50 | | #include "proto_fuzzer/h2_origin_mock_server.h" |
51 | | #include "proto_fuzzer/h2_proxy_mock_server.h" |
52 | | #include "proto_fuzzer/imaps_mock_server.h" |
53 | | #include "proto_fuzzer/ldaps_mock_server.h" |
54 | | #include "proto_fuzzer/mqtts_mock_server.h" |
55 | | #include "proto_fuzzer/pop3s_mock_server.h" |
56 | | #include "proto_fuzzer/smtps_mock_server.h" |
57 | | #include "proto_fuzzer/tls_mock_server.h" |
58 | | #include "proto_fuzzer/tls_websocket_mock_server.h" |
59 | | #endif |
60 | | #if defined(PROTO_FUZZER_HAS_HTTP3_MOCK_SERVER) |
61 | | #include "proto_fuzzer/connect_udp_proxy_mock_server.h" |
62 | | #include "proto_fuzzer/http3_mock_server.h" |
63 | | #endif |
64 | | |
65 | | namespace proto_fuzzer { |
66 | | |
67 | | namespace { |
68 | | |
69 | | constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO; |
70 | | constexpr std::size_t kMaxResultHeaders = 16; |
71 | | constexpr char kResolverUnixSocketPath[] = "/curl-fuzzer-resolver.sock"; |
72 | | constexpr char kResolverCancelDuplicateUrl[] = "http://resolver-queued-cancel.test:81/queued-cancel-duplicate"; |
73 | | constexpr auto kResolverCancelGateWait = std::chrono::milliseconds(500); |
74 | | constexpr int kResolverIdlePollAttempts = 4; |
75 | | constexpr int kResolverIdlePollTimeoutMs = 250; |
76 | | |
77 | | struct ResolverIdleTraceState { |
78 | 190 | explicit ResolverIdleTraceState(CURL* handle) : easy(handle) {} |
79 | | |
80 | 380 | ~ResolverIdleTraceState() { |
81 | | // The callback is static, but its data is stack-owned. Disable tracing and |
82 | | // clear that pointer before the easy handle outlives this focused drive. |
83 | 380 | (void)curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L); |
84 | 380 | (void)curl_easy_setopt(easy, CURLOPT_DEBUGDATA, nullptr); |
85 | 380 | } scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ResolverIdleTraceState::~ResolverIdleTraceState() Line | Count | Source | 80 | 190 | ~ResolverIdleTraceState() { | 81 | | // The callback is static, but its data is stack-owned. Disable tracing and | 82 | | // clear that pointer before the easy handle outlives this focused drive. | 83 | 190 | (void)curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L); | 84 | 190 | (void)curl_easy_setopt(easy, CURLOPT_DEBUGDATA, nullptr); | 85 | 190 | } |
scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ResolverIdleTraceState::~ResolverIdleTraceState() Line | Count | Source | 80 | 190 | ~ResolverIdleTraceState() { | 81 | | // The callback is static, but its data is stack-owned. Disable tracing and | 82 | | // clear that pointer before the easy handle outlives this focused drive. | 83 | 190 | (void)curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L); | 84 | 190 | (void)curl_easy_setopt(easy, CURLOPT_DEBUGDATA, nullptr); | 85 | 190 | } |
|
86 | | |
87 | | CURL* easy; |
88 | | bool observe_idle = false; |
89 | | bool saw_idle = false; |
90 | | }; |
91 | | |
92 | 7.61k | int ObserveResolverIdleTrace(CURL* /*easy*/, curl_infotype type, char* data, std::size_t size, void* client_data) { |
93 | 7.61k | auto* state = static_cast<ResolverIdleTraceState*>(client_data); |
94 | 7.61k | if (state == nullptr || !state->observe_idle || type != CURLINFO_TEXT || data == nullptr) { |
95 | 1.61k | return 0; |
96 | 1.61k | } |
97 | 6.00k | const std::string_view trace(data, size); |
98 | 6.00k | if (trace.find("[TPOOL-DNS]") != std::string_view::npos && trace.find("idle for") != std::string_view::npos) { |
99 | 0 | state->saw_idle = true; |
100 | 0 | } |
101 | 6.00k | return 0; |
102 | 7.61k | } |
103 | | |
104 | 159 | int CountResolverStart(void* /*resolver_state*/, void* /*reserved*/, void* client_data) { |
105 | 159 | ++*static_cast<int*>(client_data); |
106 | 159 | return 0; |
107 | 159 | } |
108 | | |
109 | | /// Apply only harness-owned resolver controls. Target policy has already |
110 | | /// replaced the URL authority with a fixed name understood by the target-local |
111 | | /// getaddrinfo shim and removed fuzzed cache entries for these modes. |
112 | 15.6k | bool ApplyResolverMode(CURL* easy, curl::fuzzer::proto::ResolverMode mode) { |
113 | 15.6k | switch (mode) { |
114 | 504 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_POSITIVE: |
115 | 504 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4) == CURLE_OK && |
116 | 504 | curl_easy_setopt(easy, CURLOPT_DNS_SHUFFLE_ADDRESSES, 1L) == CURLE_OK; |
117 | 372 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_PARTIAL: |
118 | 752 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL: |
119 | 752 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER) == CURLE_OK; |
120 | 78 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_NEGATIVE: |
121 | 78 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER) == CURLE_OK; |
122 | 248 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_LINK_LOCAL: |
123 | 248 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6) == CURLE_OK; |
124 | 278 | case curl::fuzzer::proto::RESOLVER_MODE_UNIX_SOCKET: |
125 | 278 | return curl_easy_setopt(easy, CURLOPT_UNIX_SOCKET_PATH, kResolverUnixSocketPath) == CURLE_OK; |
126 | 13.4k | case curl::fuzzer::proto::RESOLVER_MODE_DEFAULT: |
127 | 13.4k | case curl::fuzzer::proto::RESOLVER_MODE_DOH_ERROR: |
128 | 13.7k | case curl::fuzzer::proto::RESOLVER_MODE_GLOBAL_LIFECYCLE: |
129 | 13.7k | default: |
130 | 13.7k | return true; |
131 | 15.6k | } |
132 | 15.6k | } scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ApplyResolverMode(void*, curl::fuzzer::proto::ResolverMode) Line | Count | Source | 112 | 7.81k | bool ApplyResolverMode(CURL* easy, curl::fuzzer::proto::ResolverMode mode) { | 113 | 7.81k | switch (mode) { | 114 | 252 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_POSITIVE: | 115 | 252 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4) == CURLE_OK && | 116 | 252 | curl_easy_setopt(easy, CURLOPT_DNS_SHUFFLE_ADDRESSES, 1L) == CURLE_OK; | 117 | 186 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_PARTIAL: | 118 | 376 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL: | 119 | 376 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER) == CURLE_OK; | 120 | 39 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_NEGATIVE: | 121 | 39 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER) == CURLE_OK; | 122 | 124 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_LINK_LOCAL: | 123 | 124 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6) == CURLE_OK; | 124 | 139 | case curl::fuzzer::proto::RESOLVER_MODE_UNIX_SOCKET: | 125 | 139 | return curl_easy_setopt(easy, CURLOPT_UNIX_SOCKET_PATH, kResolverUnixSocketPath) == CURLE_OK; | 126 | 6.71k | case curl::fuzzer::proto::RESOLVER_MODE_DEFAULT: | 127 | 6.74k | case curl::fuzzer::proto::RESOLVER_MODE_DOH_ERROR: | 128 | 6.88k | case curl::fuzzer::proto::RESOLVER_MODE_GLOBAL_LIFECYCLE: | 129 | 6.88k | default: | 130 | 6.88k | return true; | 131 | 7.81k | } | 132 | 7.81k | } |
scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ApplyResolverMode(void*, curl::fuzzer::proto::ResolverMode) Line | Count | Source | 112 | 7.81k | bool ApplyResolverMode(CURL* easy, curl::fuzzer::proto::ResolverMode mode) { | 113 | 7.81k | switch (mode) { | 114 | 252 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_POSITIVE: | 115 | 252 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4) == CURLE_OK && | 116 | 252 | curl_easy_setopt(easy, CURLOPT_DNS_SHUFFLE_ADDRESSES, 1L) == CURLE_OK; | 117 | 186 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_PARTIAL: | 118 | 376 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL: | 119 | 376 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER) == CURLE_OK; | 120 | 39 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_NEGATIVE: | 121 | 39 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER) == CURLE_OK; | 122 | 124 | case curl::fuzzer::proto::RESOLVER_MODE_THREADED_LINK_LOCAL: | 123 | 124 | return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6) == CURLE_OK; | 124 | 139 | case curl::fuzzer::proto::RESOLVER_MODE_UNIX_SOCKET: | 125 | 139 | return curl_easy_setopt(easy, CURLOPT_UNIX_SOCKET_PATH, kResolverUnixSocketPath) == CURLE_OK; | 126 | 6.71k | case curl::fuzzer::proto::RESOLVER_MODE_DEFAULT: | 127 | 6.74k | case curl::fuzzer::proto::RESOLVER_MODE_DOH_ERROR: | 128 | 6.88k | case curl::fuzzer::proto::RESOLVER_MODE_GLOBAL_LIFECYCLE: | 129 | 6.88k | default: | 130 | 6.88k | return true; | 131 | 7.81k | } | 132 | 7.81k | } |
|
133 | | |
134 | | /// Resolve two public easy handles through one worker. The first handle queues |
135 | | /// AAAA then A. Its first getaddrinfo call cannot return through the target |
136 | | /// gate, so the sole worker cannot take the second family. Only then is the |
137 | | /// duplicate added on a distinct origin and allowed to queue its own AAAA and |
138 | | /// A work behind the blocked lookup. |
139 | | /// |
140 | | /// The resolver-start callback fires before curl submits the duplicate's |
141 | | /// queries, but curl_multi_perform cannot return until that synchronous |
142 | | /// submission finishes. A successful perform, a live duplicate, and the |
143 | | /// observed callback therefore prove the duplicate resolver work is queued. |
144 | | /// Removing that active duplicate is the public cancellation operation: curl |
145 | | /// synchronously shuts down its live async resolver, scans the queue, and |
146 | | /// destroys the matching items before clearing the handle's multi pointer. |
147 | | /// The gate's scope ends after this removal and before cleanup can join the |
148 | | /// worker, so teardown cannot wait in a cycle with the shim. |
149 | 380 | CURLcode RunQueuedResolverCancellation(CURL* easy, MockServer* mock, const curl::fuzzer::proto::Scenario& scenario) { |
150 | 380 | int duplicate_resolver_starts = 0; |
151 | 380 | ResolverIdleTraceState idle_trace(easy); |
152 | | // Keep callback data alive until both handles are gone. Declaring the |
153 | | // duplicate before the multi also makes exceptional teardown destroy the |
154 | | // multi (and detach any handle whose public removal failed) first. |
155 | 380 | CurlEasyPtr duplicate(curl_easy_duphandle(easy)); |
156 | 380 | CurlMultiPtr multi(curl_multi_init()); |
157 | | // This drive is bounded by the gate wait and explicit removals. Clear the |
158 | | // ordinary 200 ms transfer/connect settings (zero selects no operation |
159 | | // deadline and curl's default connect deadline) so host scheduling cannot |
160 | | // cancel either handle before the queue invariant is established. |
161 | 380 | if (multi == nullptr || duplicate == nullptr || |
162 | 352 | curl_multi_setopt(multi.get(), CURLMOPT_RESOLVE_THREADS_MAX, 1L) != CURLM_OK || |
163 | 352 | curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 0L) != CURLE_OK || |
164 | 352 | curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 0L) != CURLE_OK || |
165 | 352 | curl_easy_setopt(duplicate.get(), CURLOPT_TIMEOUT_MS, 0L) != CURLE_OK || |
166 | 352 | curl_easy_setopt(duplicate.get(), CURLOPT_CONNECTTIMEOUT_MS, 0L) != CURLE_OK || |
167 | 352 | curl_easy_setopt(duplicate.get(), CURLOPT_URL, kResolverCancelDuplicateUrl) != CURLE_OK || |
168 | 350 | curl_easy_setopt(duplicate.get(), CURLOPT_FRESH_CONNECT, 1L) != CURLE_OK || |
169 | 350 | curl_easy_setopt(duplicate.get(), CURLOPT_RESOLVER_START_FUNCTION, &CountResolverStart) != CURLE_OK || |
170 | 350 | curl_easy_setopt(duplicate.get(), CURLOPT_RESOLVER_START_DATA, &duplicate_resolver_starts) != CURLE_OK || |
171 | 350 | curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &ObserveResolverIdleTrace) != CURLE_OK || |
172 | 350 | curl_easy_setopt(easy, CURLOPT_DEBUGDATA, &idle_trace) != CURLE_OK || |
173 | 350 | curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L) != CURLE_OK) { |
174 | 30 | return CURLE_FAILED_INIT; |
175 | 30 | } |
176 | | |
177 | 350 | mock->SetScripts(scenario); |
178 | 350 | CURLMcode add_easy_result = curl_multi_add_handle(multi.get(), easy); |
179 | 350 | CURLMcode add_duplicate_result = CURLM_BAD_HANDLE; |
180 | 350 | CURLMcode perform_result = CURLM_BAD_HANDLE; |
181 | 350 | CURLMcode remove_duplicate_result = CURLM_BAD_HANDLE; |
182 | 350 | bool lookup_started = false; |
183 | 350 | bool duplicate_resolver_queued = false; |
184 | 350 | bool resolver_worker_idle = false; |
185 | 350 | int still_running = 0; |
186 | 350 | if (add_easy_result == CURLM_OK) { |
187 | 350 | ScopedResolverCancelGate gate; |
188 | 350 | perform_result = curl_multi_perform(multi.get(), &still_running); |
189 | 350 | if (perform_result == CURLM_OK && still_running > 0) { |
190 | 330 | lookup_started = WaitForResolverCancelLookup(kResolverCancelGateWait); |
191 | 330 | } |
192 | | // The signal is sent from inside Q1's getaddrinfo. Q1 is worker-owned, |
193 | | // and the only worker cannot return to the queue to take original Q2. |
194 | 350 | if (lookup_started) { |
195 | 330 | add_duplicate_result = curl_multi_add_handle(multi.get(), duplicate.get()); |
196 | 330 | } |
197 | 350 | if (add_duplicate_result == CURLM_OK) { |
198 | 330 | perform_result = curl_multi_perform(multi.get(), &still_running); |
199 | 330 | } |
200 | | // Resolver-start precedes the two sends, but the public perform call does |
201 | | // not return until Curl_async_getaddrinfo has submitted them. Since Q1 is |
202 | | // still blocked, a live duplicate here has queue-owned work to cancel. |
203 | 350 | duplicate_resolver_queued = perform_result == CURLM_OK && still_running == 2 && duplicate_resolver_starts > 0; |
204 | | // Always detach a successfully added duplicate before its easy handle can |
205 | | // be destroyed. On the proved path this is the intentional cancellation; |
206 | | // on a failed proof it is bounded cleanup while Q1 still owns the worker. |
207 | 350 | if (add_duplicate_result == CURLM_OK) { |
208 | 330 | remove_duplicate_result = curl_multi_remove_handle(multi.get(), duplicate.get()); |
209 | 330 | } |
210 | 350 | } |
211 | | |
212 | | // Releasing the gate lets the worker finish the original handle's two |
213 | | // queries. Wait on curl's public internal wakeup and perform until the |
214 | | // THREADS trace reports that the pool is idle. Curl emits that observation |
215 | | // while holding the pool lock; the main thread can acquire it only after the |
216 | | // worker has entered Curl_cond_timedwait and released the lock. The trace is |
217 | | // therefore the proof, while the poll timeout merely bounds a failed run. |
218 | 350 | if (lookup_started && duplicate_resolver_queued && remove_duplicate_result == CURLM_OK) { |
219 | 312 | idle_trace.observe_idle = true; |
220 | 1.56k | for (int attempt = 0; attempt < kResolverIdlePollAttempts && !idle_trace.saw_idle; ++attempt) { |
221 | 1.24k | int numfds = 0; |
222 | 1.24k | const CURLMcode poll_result = curl_multi_poll(multi.get(), nullptr, 0, kResolverIdlePollTimeoutMs, &numfds); |
223 | 1.24k | if (poll_result != CURLM_OK) { |
224 | 0 | perform_result = poll_result; |
225 | 0 | break; |
226 | 0 | } |
227 | 1.24k | perform_result = curl_multi_perform(multi.get(), &still_running); |
228 | 1.24k | if (perform_result != CURLM_OK) { |
229 | 0 | break; |
230 | 0 | } |
231 | 1.24k | } |
232 | 312 | resolver_worker_idle = idle_trace.saw_idle; |
233 | 312 | } |
234 | | |
235 | 350 | const CURLMcode remove_easy_result = |
236 | 350 | add_easy_result == CURLM_OK ? curl_multi_remove_handle(multi.get(), easy) : CURLM_BAD_HANDLE; |
237 | 350 | return lookup_started && duplicate_resolver_queued && resolver_worker_idle && perform_result == CURLM_OK && |
238 | 0 | remove_duplicate_result == CURLM_OK && remove_easy_result == CURLM_OK |
239 | 350 | ? CURLE_OK |
240 | 350 | : CURLE_FAILED_INIT; |
241 | 380 | } scenario_runner.cc:proto_fuzzer::(anonymous namespace)::RunQueuedResolverCancellation(void*, proto_fuzzer::MockServer*, curl::fuzzer::proto::Scenario const&) Line | Count | Source | 149 | 190 | CURLcode RunQueuedResolverCancellation(CURL* easy, MockServer* mock, const curl::fuzzer::proto::Scenario& scenario) { | 150 | 190 | int duplicate_resolver_starts = 0; | 151 | 190 | ResolverIdleTraceState idle_trace(easy); | 152 | | // Keep callback data alive until both handles are gone. Declaring the | 153 | | // duplicate before the multi also makes exceptional teardown destroy the | 154 | | // multi (and detach any handle whose public removal failed) first. | 155 | 190 | CurlEasyPtr duplicate(curl_easy_duphandle(easy)); | 156 | 190 | CurlMultiPtr multi(curl_multi_init()); | 157 | | // This drive is bounded by the gate wait and explicit removals. Clear the | 158 | | // ordinary 200 ms transfer/connect settings (zero selects no operation | 159 | | // deadline and curl's default connect deadline) so host scheduling cannot | 160 | | // cancel either handle before the queue invariant is established. | 161 | 190 | if (multi == nullptr || duplicate == nullptr || | 162 | 176 | curl_multi_setopt(multi.get(), CURLMOPT_RESOLVE_THREADS_MAX, 1L) != CURLM_OK || | 163 | 176 | curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 0L) != CURLE_OK || | 164 | 176 | curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 0L) != CURLE_OK || | 165 | 176 | curl_easy_setopt(duplicate.get(), CURLOPT_TIMEOUT_MS, 0L) != CURLE_OK || | 166 | 176 | curl_easy_setopt(duplicate.get(), CURLOPT_CONNECTTIMEOUT_MS, 0L) != CURLE_OK || | 167 | 176 | curl_easy_setopt(duplicate.get(), CURLOPT_URL, kResolverCancelDuplicateUrl) != CURLE_OK || | 168 | 175 | curl_easy_setopt(duplicate.get(), CURLOPT_FRESH_CONNECT, 1L) != CURLE_OK || | 169 | 175 | curl_easy_setopt(duplicate.get(), CURLOPT_RESOLVER_START_FUNCTION, &CountResolverStart) != CURLE_OK || | 170 | 175 | curl_easy_setopt(duplicate.get(), CURLOPT_RESOLVER_START_DATA, &duplicate_resolver_starts) != CURLE_OK || | 171 | 175 | curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &ObserveResolverIdleTrace) != CURLE_OK || | 172 | 175 | curl_easy_setopt(easy, CURLOPT_DEBUGDATA, &idle_trace) != CURLE_OK || | 173 | 175 | curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L) != CURLE_OK) { | 174 | 15 | return CURLE_FAILED_INIT; | 175 | 15 | } | 176 | | | 177 | 175 | mock->SetScripts(scenario); | 178 | 175 | CURLMcode add_easy_result = curl_multi_add_handle(multi.get(), easy); | 179 | 175 | CURLMcode add_duplicate_result = CURLM_BAD_HANDLE; | 180 | 175 | CURLMcode perform_result = CURLM_BAD_HANDLE; | 181 | 175 | CURLMcode remove_duplicate_result = CURLM_BAD_HANDLE; | 182 | 175 | bool lookup_started = false; | 183 | 175 | bool duplicate_resolver_queued = false; | 184 | 175 | bool resolver_worker_idle = false; | 185 | 175 | int still_running = 0; | 186 | 175 | if (add_easy_result == CURLM_OK) { | 187 | 175 | ScopedResolverCancelGate gate; | 188 | 175 | perform_result = curl_multi_perform(multi.get(), &still_running); | 189 | 175 | if (perform_result == CURLM_OK && still_running > 0) { | 190 | 165 | lookup_started = WaitForResolverCancelLookup(kResolverCancelGateWait); | 191 | 165 | } | 192 | | // The signal is sent from inside Q1's getaddrinfo. Q1 is worker-owned, | 193 | | // and the only worker cannot return to the queue to take original Q2. | 194 | 175 | if (lookup_started) { | 195 | 165 | add_duplicate_result = curl_multi_add_handle(multi.get(), duplicate.get()); | 196 | 165 | } | 197 | 175 | if (add_duplicate_result == CURLM_OK) { | 198 | 165 | perform_result = curl_multi_perform(multi.get(), &still_running); | 199 | 165 | } | 200 | | // Resolver-start precedes the two sends, but the public perform call does | 201 | | // not return until Curl_async_getaddrinfo has submitted them. Since Q1 is | 202 | | // still blocked, a live duplicate here has queue-owned work to cancel. | 203 | 175 | duplicate_resolver_queued = perform_result == CURLM_OK && still_running == 2 && duplicate_resolver_starts > 0; | 204 | | // Always detach a successfully added duplicate before its easy handle can | 205 | | // be destroyed. On the proved path this is the intentional cancellation; | 206 | | // on a failed proof it is bounded cleanup while Q1 still owns the worker. | 207 | 175 | if (add_duplicate_result == CURLM_OK) { | 208 | 165 | remove_duplicate_result = curl_multi_remove_handle(multi.get(), duplicate.get()); | 209 | 165 | } | 210 | 175 | } | 211 | | | 212 | | // Releasing the gate lets the worker finish the original handle's two | 213 | | // queries. Wait on curl's public internal wakeup and perform until the | 214 | | // THREADS trace reports that the pool is idle. Curl emits that observation | 215 | | // while holding the pool lock; the main thread can acquire it only after the | 216 | | // worker has entered Curl_cond_timedwait and released the lock. The trace is | 217 | | // therefore the proof, while the poll timeout merely bounds a failed run. | 218 | 175 | if (lookup_started && duplicate_resolver_queued && remove_duplicate_result == CURLM_OK) { | 219 | 156 | idle_trace.observe_idle = true; | 220 | 780 | for (int attempt = 0; attempt < kResolverIdlePollAttempts && !idle_trace.saw_idle; ++attempt) { | 221 | 624 | int numfds = 0; | 222 | 624 | const CURLMcode poll_result = curl_multi_poll(multi.get(), nullptr, 0, kResolverIdlePollTimeoutMs, &numfds); | 223 | 624 | if (poll_result != CURLM_OK) { | 224 | 0 | perform_result = poll_result; | 225 | 0 | break; | 226 | 0 | } | 227 | 624 | perform_result = curl_multi_perform(multi.get(), &still_running); | 228 | 624 | if (perform_result != CURLM_OK) { | 229 | 0 | break; | 230 | 0 | } | 231 | 624 | } | 232 | 156 | resolver_worker_idle = idle_trace.saw_idle; | 233 | 156 | } | 234 | | | 235 | 175 | const CURLMcode remove_easy_result = | 236 | 175 | add_easy_result == CURLM_OK ? curl_multi_remove_handle(multi.get(), easy) : CURLM_BAD_HANDLE; | 237 | 175 | return lookup_started && duplicate_resolver_queued && resolver_worker_idle && perform_result == CURLM_OK && | 238 | 0 | remove_duplicate_result == CURLM_OK && remove_easy_result == CURLM_OK | 239 | 175 | ? CURLE_OK | 240 | 175 | : CURLE_FAILED_INIT; | 241 | 190 | } |
scenario_runner.cc:proto_fuzzer::(anonymous namespace)::RunQueuedResolverCancellation(void*, proto_fuzzer::MockServer*, curl::fuzzer::proto::Scenario const&) Line | Count | Source | 149 | 190 | CURLcode RunQueuedResolverCancellation(CURL* easy, MockServer* mock, const curl::fuzzer::proto::Scenario& scenario) { | 150 | 190 | int duplicate_resolver_starts = 0; | 151 | 190 | ResolverIdleTraceState idle_trace(easy); | 152 | | // Keep callback data alive until both handles are gone. Declaring the | 153 | | // duplicate before the multi also makes exceptional teardown destroy the | 154 | | // multi (and detach any handle whose public removal failed) first. | 155 | 190 | CurlEasyPtr duplicate(curl_easy_duphandle(easy)); | 156 | 190 | CurlMultiPtr multi(curl_multi_init()); | 157 | | // This drive is bounded by the gate wait and explicit removals. Clear the | 158 | | // ordinary 200 ms transfer/connect settings (zero selects no operation | 159 | | // deadline and curl's default connect deadline) so host scheduling cannot | 160 | | // cancel either handle before the queue invariant is established. | 161 | 190 | if (multi == nullptr || duplicate == nullptr || | 162 | 176 | curl_multi_setopt(multi.get(), CURLMOPT_RESOLVE_THREADS_MAX, 1L) != CURLM_OK || | 163 | 176 | curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 0L) != CURLE_OK || | 164 | 176 | curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 0L) != CURLE_OK || | 165 | 176 | curl_easy_setopt(duplicate.get(), CURLOPT_TIMEOUT_MS, 0L) != CURLE_OK || | 166 | 176 | curl_easy_setopt(duplicate.get(), CURLOPT_CONNECTTIMEOUT_MS, 0L) != CURLE_OK || | 167 | 176 | curl_easy_setopt(duplicate.get(), CURLOPT_URL, kResolverCancelDuplicateUrl) != CURLE_OK || | 168 | 175 | curl_easy_setopt(duplicate.get(), CURLOPT_FRESH_CONNECT, 1L) != CURLE_OK || | 169 | 175 | curl_easy_setopt(duplicate.get(), CURLOPT_RESOLVER_START_FUNCTION, &CountResolverStart) != CURLE_OK || | 170 | 175 | curl_easy_setopt(duplicate.get(), CURLOPT_RESOLVER_START_DATA, &duplicate_resolver_starts) != CURLE_OK || | 171 | 175 | curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &ObserveResolverIdleTrace) != CURLE_OK || | 172 | 175 | curl_easy_setopt(easy, CURLOPT_DEBUGDATA, &idle_trace) != CURLE_OK || | 173 | 175 | curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L) != CURLE_OK) { | 174 | 15 | return CURLE_FAILED_INIT; | 175 | 15 | } | 176 | | | 177 | 175 | mock->SetScripts(scenario); | 178 | 175 | CURLMcode add_easy_result = curl_multi_add_handle(multi.get(), easy); | 179 | 175 | CURLMcode add_duplicate_result = CURLM_BAD_HANDLE; | 180 | 175 | CURLMcode perform_result = CURLM_BAD_HANDLE; | 181 | 175 | CURLMcode remove_duplicate_result = CURLM_BAD_HANDLE; | 182 | 175 | bool lookup_started = false; | 183 | 175 | bool duplicate_resolver_queued = false; | 184 | 175 | bool resolver_worker_idle = false; | 185 | 175 | int still_running = 0; | 186 | 175 | if (add_easy_result == CURLM_OK) { | 187 | 175 | ScopedResolverCancelGate gate; | 188 | 175 | perform_result = curl_multi_perform(multi.get(), &still_running); | 189 | 175 | if (perform_result == CURLM_OK && still_running > 0) { | 190 | 165 | lookup_started = WaitForResolverCancelLookup(kResolverCancelGateWait); | 191 | 165 | } | 192 | | // The signal is sent from inside Q1's getaddrinfo. Q1 is worker-owned, | 193 | | // and the only worker cannot return to the queue to take original Q2. | 194 | 175 | if (lookup_started) { | 195 | 165 | add_duplicate_result = curl_multi_add_handle(multi.get(), duplicate.get()); | 196 | 165 | } | 197 | 175 | if (add_duplicate_result == CURLM_OK) { | 198 | 165 | perform_result = curl_multi_perform(multi.get(), &still_running); | 199 | 165 | } | 200 | | // Resolver-start precedes the two sends, but the public perform call does | 201 | | // not return until Curl_async_getaddrinfo has submitted them. Since Q1 is | 202 | | // still blocked, a live duplicate here has queue-owned work to cancel. | 203 | 175 | duplicate_resolver_queued = perform_result == CURLM_OK && still_running == 2 && duplicate_resolver_starts > 0; | 204 | | // Always detach a successfully added duplicate before its easy handle can | 205 | | // be destroyed. On the proved path this is the intentional cancellation; | 206 | | // on a failed proof it is bounded cleanup while Q1 still owns the worker. | 207 | 175 | if (add_duplicate_result == CURLM_OK) { | 208 | 165 | remove_duplicate_result = curl_multi_remove_handle(multi.get(), duplicate.get()); | 209 | 165 | } | 210 | 175 | } | 211 | | | 212 | | // Releasing the gate lets the worker finish the original handle's two | 213 | | // queries. Wait on curl's public internal wakeup and perform until the | 214 | | // THREADS trace reports that the pool is idle. Curl emits that observation | 215 | | // while holding the pool lock; the main thread can acquire it only after the | 216 | | // worker has entered Curl_cond_timedwait and released the lock. The trace is | 217 | | // therefore the proof, while the poll timeout merely bounds a failed run. | 218 | 175 | if (lookup_started && duplicate_resolver_queued && remove_duplicate_result == CURLM_OK) { | 219 | 156 | idle_trace.observe_idle = true; | 220 | 780 | for (int attempt = 0; attempt < kResolverIdlePollAttempts && !idle_trace.saw_idle; ++attempt) { | 221 | 624 | int numfds = 0; | 222 | 624 | const CURLMcode poll_result = curl_multi_poll(multi.get(), nullptr, 0, kResolverIdlePollTimeoutMs, &numfds); | 223 | 624 | if (poll_result != CURLM_OK) { | 224 | 0 | perform_result = poll_result; | 225 | 0 | break; | 226 | 0 | } | 227 | 624 | perform_result = curl_multi_perform(multi.get(), &still_running); | 228 | 624 | if (perform_result != CURLM_OK) { | 229 | 0 | break; | 230 | 0 | } | 231 | 624 | } | 232 | 156 | resolver_worker_idle = idle_trace.saw_idle; | 233 | 156 | } | 234 | | | 235 | 175 | const CURLMcode remove_easy_result = | 236 | 175 | add_easy_result == CURLM_OK ? curl_multi_remove_handle(multi.get(), easy) : CURLM_BAD_HANDLE; | 237 | 175 | return lookup_started && duplicate_resolver_queued && resolver_worker_idle && perform_result == CURLM_OK && | 238 | 0 | remove_duplicate_result == CURLM_OK && remove_easy_result == CURLM_OK | 239 | 175 | ? CURLE_OK | 240 | 175 | : CURLE_FAILED_INIT; | 241 | 190 | } |
|
242 | | |
243 | | /// Probe each public getinfo return family and the response-header API after |
244 | | /// curl has settled the transfer. Applications commonly inspect these APIs, |
245 | | /// but a harness that only drives I/O leaves their type dispatch and |
246 | | /// post-transfer state unexecuted even when the corresponding parser ran. |
247 | | /// The chosen values are handle-owned or scalar: notably CERTINFO exercises |
248 | | /// the pointer/slist dispatch family without materialising a separately-owned |
249 | | /// cookie/engine list. Header iteration is capped independently of response |
250 | | /// size so this unconditional coverage cannot dominate a fuzz iteration. |
251 | 390k | void ProbeTransferResults(CURL* easy) { |
252 | 390k | char* string_result = nullptr; |
253 | 390k | long long_result = 0; |
254 | 390k | double double_result = 0; |
255 | 390k | curl_off_t offset_result = 0; |
256 | 390k | curl_socket_t socket_result = CURL_SOCKET_BAD; |
257 | 390k | struct curl_certinfo* certinfo_result = nullptr; |
258 | | |
259 | 390k | (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result); |
260 | 390k | (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result); |
261 | 390k | (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result); |
262 | 390k | (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result); |
263 | 390k | (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result); |
264 | 390k | (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result); |
265 | | |
266 | 390k | struct curl_header* header = nullptr; |
267 | 390k | (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header); |
268 | 390k | header = nullptr; |
269 | 832k | for (std::size_t index = 0; index < kMaxResultHeaders; ++index) { |
270 | 820k | header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header); |
271 | 820k | if (header == nullptr) { |
272 | 378k | break; |
273 | 378k | } |
274 | 820k | } |
275 | 390k | } scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ProbeTransferResults(void*) Line | Count | Source | 251 | 195k | void ProbeTransferResults(CURL* easy) { | 252 | 195k | char* string_result = nullptr; | 253 | 195k | long long_result = 0; | 254 | 195k | double double_result = 0; | 255 | 195k | curl_off_t offset_result = 0; | 256 | 195k | curl_socket_t socket_result = CURL_SOCKET_BAD; | 257 | 195k | struct curl_certinfo* certinfo_result = nullptr; | 258 | | | 259 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result); | 260 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result); | 261 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result); | 262 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result); | 263 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result); | 264 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result); | 265 | | | 266 | 195k | struct curl_header* header = nullptr; | 267 | 195k | (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header); | 268 | 195k | header = nullptr; | 269 | 416k | for (std::size_t index = 0; index < kMaxResultHeaders; ++index) { | 270 | 410k | header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header); | 271 | 410k | if (header == nullptr) { | 272 | 189k | break; | 273 | 189k | } | 274 | 410k | } | 275 | 195k | } |
scenario_runner.cc:proto_fuzzer::(anonymous namespace)::ProbeTransferResults(void*) Line | Count | Source | 251 | 195k | void ProbeTransferResults(CURL* easy) { | 252 | 195k | char* string_result = nullptr; | 253 | 195k | long long_result = 0; | 254 | 195k | double double_result = 0; | 255 | 195k | curl_off_t offset_result = 0; | 256 | 195k | curl_socket_t socket_result = CURL_SOCKET_BAD; | 257 | 195k | struct curl_certinfo* certinfo_result = nullptr; | 258 | | | 259 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result); | 260 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result); | 261 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result); | 262 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result); | 263 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result); | 264 | 195k | (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result); | 265 | | | 266 | 195k | struct curl_header* header = nullptr; | 267 | 195k | (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header); | 268 | 195k | header = nullptr; | 269 | 416k | for (std::size_t index = 0; index < kMaxResultHeaders; ++index) { | 270 | 410k | header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header); | 271 | 410k | if (header == nullptr) { | 272 | 189k | break; | 273 | 189k | } | 274 | 410k | } | 275 | 195k | } |
|
276 | | |
277 | | /// Map a Scheme enum to the URL scheme literal. |
278 | 228k | const char* SchemePrefix(curl::fuzzer::proto::Scheme scheme) { |
279 | 228k | switch (scheme) { |
280 | 92.8k | case curl::fuzzer::proto::SCHEME_HTTP: |
281 | 92.8k | return "http"; |
282 | 47.6k | case curl::fuzzer::proto::SCHEME_HTTPS: |
283 | 47.6k | return "https"; |
284 | 12.7k | case curl::fuzzer::proto::SCHEME_WS: |
285 | 12.7k | return "ws"; |
286 | 7.82k | case curl::fuzzer::proto::SCHEME_WSS: |
287 | 7.82k | return "wss"; |
288 | 5.32k | case curl::fuzzer::proto::SCHEME_TELNET: |
289 | 5.32k | return "telnet"; |
290 | 4.15k | case curl::fuzzer::proto::SCHEME_FTP: |
291 | 4.15k | return "ftp"; |
292 | 1.95k | case curl::fuzzer::proto::SCHEME_FTPS: |
293 | 1.95k | return "ftps"; |
294 | 2.24k | case curl::fuzzer::proto::SCHEME_TFTP: |
295 | 2.24k | return "tftp"; |
296 | 2.77k | case curl::fuzzer::proto::SCHEME_SMTP: |
297 | 2.77k | return "smtp"; |
298 | 2.43k | case curl::fuzzer::proto::SCHEME_IMAP: |
299 | 2.43k | return "imap"; |
300 | 2.72k | case curl::fuzzer::proto::SCHEME_IMAPS: |
301 | 2.72k | return "imaps"; |
302 | 1.13k | case curl::fuzzer::proto::SCHEME_POP3: |
303 | 1.13k | return "pop3"; |
304 | 2.69k | case curl::fuzzer::proto::SCHEME_POP3S: |
305 | 2.69k | return "pop3s"; |
306 | 1.02k | case curl::fuzzer::proto::SCHEME_MQTT: |
307 | 1.02k | return "mqtt"; |
308 | 3.02k | case curl::fuzzer::proto::SCHEME_MQTTS: |
309 | 3.02k | return "mqtts"; |
310 | 2.83k | case curl::fuzzer::proto::SCHEME_LDAP: |
311 | 2.83k | return "ldap"; |
312 | 2.93k | case curl::fuzzer::proto::SCHEME_LDAPS: |
313 | 2.93k | return "ldaps"; |
314 | 6.30k | case curl::fuzzer::proto::SCHEME_RTSP: |
315 | 6.30k | return "rtsp"; |
316 | 9.28k | case curl::fuzzer::proto::SCHEME_IPFS: |
317 | 9.28k | return "ipfs"; |
318 | 15 | case curl::fuzzer::proto::SCHEME_IPNS: |
319 | 15 | return "ipns"; |
320 | 2.07k | case curl::fuzzer::proto::SCHEME_SMTPS: |
321 | 2.07k | return "smtps"; |
322 | 3.21k | case curl::fuzzer::proto::SCHEME_DICT: |
323 | 3.21k | return "dict"; |
324 | 4.16k | case curl::fuzzer::proto::SCHEME_GOPHER: |
325 | 4.16k | return "gopher"; |
326 | 3.40k | case curl::fuzzer::proto::SCHEME_GOPHERS: |
327 | 3.40k | return "gophers"; |
328 | 3.58k | case curl::fuzzer::proto::SCHEME_FILE: |
329 | 3.58k | return "file"; |
330 | 321 | case curl::fuzzer::proto::SCHEME_UNSPECIFIED: |
331 | 325 | default: |
332 | 325 | return nullptr; |
333 | 228k | } |
334 | 228k | } |
335 | | |
336 | | /// Build the URL curl actually receives. For IPFS/IPNS this reproduces curl's |
337 | | /// command-line gateway rewrite: `<gateway>/<protocol>/<cid-or-name><suffix>`. |
338 | | /// The gateway itself is the fixed loopback authority above; host_path already |
339 | | /// holds the canonical CID/name followed by the mutation-controlled suffix. |
340 | | /// @param scenario Scenario whose scheme and gateway path select the URL. |
341 | | /// @param prefix Scheme literal returned by SchemePrefix(). |
342 | | /// @return Effective URL before CONNECT_TO and OPENSOCKET confinement. |
343 | 226k | std::string ScenarioUrl(const curl::fuzzer::proto::Scenario& scenario, const char* prefix) { |
344 | 226k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_IPFS || scenario.scheme() == curl::fuzzer::proto::SCHEME_IPNS) { |
345 | 9.27k | return BuildIpfsGatewayUrl(scenario.scheme(), scenario.host_path()); |
346 | 9.27k | } |
347 | 217k | return std::string(prefix) + "://" + scenario.host_path(); |
348 | 226k | } |
349 | | |
350 | | /// Confirm that FILE normalization replaced the complete opened path. This is |
351 | | /// a runtime backstop for direct callers: even if target postprocessing is |
352 | | /// accidentally bypassed, neither download nor upload may name a host file. |
353 | | /// @param host_path FILE URL authority/path without the scheme prefix. |
354 | | /// @return True only for `[authority]/dev/null` plus an optional query or |
355 | | /// fragment. |
356 | 3.57k | bool HasCanonicalFilePath(const std::string& host_path) { |
357 | 3.57k | constexpr char kFilePath[] = "/dev/null"; |
358 | 3.57k | const std::size_t path_start = host_path.find('/'); |
359 | 3.57k | if (path_start == std::string::npos || host_path.compare(path_start, sizeof(kFilePath) - 1, kFilePath) != 0) { |
360 | 0 | return false; |
361 | 0 | } |
362 | 3.57k | const std::size_t suffix_start = path_start + sizeof(kFilePath) - 1; |
363 | 3.57k | return suffix_start == host_path.size() || host_path[suffix_start] == '?' || host_path[suffix_start] == '#'; |
364 | 3.57k | } |
365 | | |
366 | | /// Pick the peer implementation authorized by both protocol and target mode. |
367 | | /// The compatibility target must keep its historical raw-wire interpretation |
368 | | /// of secure schemes, while dedicated TLS lanes expose decrypted application |
369 | | /// protocols. Keeping that semantic boundary in the closed run-mode enum |
370 | | /// prevents a new peer from silently changing old OSS-Fuzz reproducers. |
371 | | std::unique_ptr<MockServerBase> MakeMockServerForScenario(const curl::fuzzer::proto::Scenario& scenario, |
372 | 220k | ScenarioRunMode mode) { |
373 | 220k | if (mode == ScenarioRunMode::kHttp3Coverage) { |
374 | | #if defined(PROTO_FUZZER_HAS_HTTP3_MOCK_SERVER) |
375 | | if (scenario.http3_plan().use_h1_connect_udp_proxy()) { |
376 | | return std::make_unique<ConnectUdpProxyMockServer>(); |
377 | | } |
378 | | return std::make_unique<Http3MockServer>(scenario.tls_certificate_chain()); |
379 | | #else |
380 | 0 | return nullptr; |
381 | 0 | #endif |
382 | 0 | } |
383 | | |
384 | 220k | if (mode == ScenarioRunMode::kH2ProxyCoverage) { |
385 | 10.7k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
386 | 10.7k | return std::make_unique<H2ProxyMockServer>( |
387 | 10.7k | scenario.h2_forward_proxy() ? H2ProxyMode::kForward : H2ProxyMode::kTunnel, |
388 | 10.7k | scenario.connection().proxy_protocol()); |
389 | | #else |
390 | | // MemorySanitizer builds deliberately omit OpenSSL. Keep the target |
391 | | // binary available to OSS-Fuzz, but do not pretend a plaintext mock can |
392 | | // negotiate the ALPN gate required to enter cf-h2-proxy. |
393 | | return nullptr; |
394 | | #endif |
395 | 10.7k | } |
396 | | |
397 | 209k | if (mode == ScenarioRunMode::kHttp2Coverage) { |
398 | 14.7k | return std::make_unique<H2CleartextMockServer>(); |
399 | 14.7k | } |
400 | | |
401 | 194k | if (mode == ScenarioRunMode::kTlsHttp2Coverage) { |
402 | 12.0k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
403 | 12.0k | return std::make_unique<H2OriginMockServer>(scenario.tls_certificate_chain(), scenario.tls_group_profile()); |
404 | | #else |
405 | | // This target remains buildable under MemorySanitizer, whose curl build |
406 | | // omits the OpenSSL server dependency required for TLS/ALPN h2. |
407 | | return nullptr; |
408 | | #endif |
409 | 12.0k | } |
410 | | |
411 | 182k | if (mode == ScenarioRunMode::kProxyTransportCoverage) { |
412 | 7.95k | return std::make_unique<ProxyTransportMockServer>(scenario.socks_proxy_mode(), scenario.haproxy_protocol()); |
413 | 7.95k | } |
414 | | |
415 | 174k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { |
416 | 12.5k | if (scenario.proxy_socket_option_mode() == curl::fuzzer::proto::PROXY_SOCKET_OPTION_LOOPBACK_INTERFACE_KEEPALIVE) { |
417 | 76 | return std::make_unique<InterfaceOptionMockServer>(); |
418 | 76 | } |
419 | 12.4k | if (scenario.proxy_socket_option_mode() != curl::fuzzer::proto::PROXY_SOCKET_OPTION_DEFAULT) { |
420 | 1.62k | return std::make_unique<ProxyOptionMockServer>(scenario.proxy_socket_option_mode()); |
421 | 1.62k | } |
422 | 10.8k | if (scenario.http_transport_mode() != curl::fuzzer::proto::HTTP_TRANSPORT_DIRECT) { |
423 | 1.09k | return std::make_unique<HttpTransportMockServer>(scenario.http_transport_mode(), scenario.haproxy_protocol()); |
424 | 1.09k | } |
425 | 10.8k | } |
426 | | |
427 | 172k | if (mode == ScenarioRunMode::kResolverCoverage && scenario.doh_https_rr()) { |
428 | | // The DoH responder answers both the address and HTTPS resource-record |
429 | | // queries; its TLS/ALPN h2 carrier then completes the outer HTTPS |
430 | | // transfer selected from the record. |
431 | 884 | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
432 | 884 | return std::make_unique<DohHttpsRrMockServer>( |
433 | 884 | scenario.tls_certificate_chain(), scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_DOH_ERROR); |
434 | | #else |
435 | | // MemorySanitizer builds deliberately omit OpenSSL, so no verified TLS |
436 | | // peer exists for the HTTPS-RR lane. Keep the target available while |
437 | | // refusing to fall through to an unrelated transport. |
438 | | return nullptr; |
439 | | #endif |
440 | 884 | } |
441 | | |
442 | 171k | if (mode == ScenarioRunMode::kResolverCoverage && scenario.happy_eyeballs()) { |
443 | | // The loopback race peer returns real unconnected sockets, so curl's |
444 | | // happy-eyeballs filter can keep several connect attempts in progress |
445 | | // instead of accepting the first preconnected socketpair. |
446 | 30 | return std::make_unique<HappyEyeballsMockServer>(); |
447 | 30 | } |
448 | | |
449 | 171k | switch (scenario.scheme()) { |
450 | 55.8k | case curl::fuzzer::proto::SCHEME_HTTP: |
451 | 55.8k | if (mode == ScenarioRunMode::kApiLifecycle) { |
452 | 11.5k | return std::make_unique<MockServer>(MultiDrivePolicy::kFromApiPlan); |
453 | 11.5k | } |
454 | 44.2k | return std::make_unique<MockServer>(); |
455 | 28.1k | case curl::fuzzer::proto::SCHEME_HTTPS: |
456 | 28.1k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
457 | 28.1k | if (mode == ScenarioRunMode::kTlsCoverage) { |
458 | 27.2k | return std::make_unique<TlsMockServer>(scenario.tls_certificate_chain(), scenario.tls_handshake_profile()); |
459 | 27.2k | } |
460 | | #else |
461 | | (void)mode; |
462 | | #endif |
463 | 873 | return std::make_unique<MockServer>(); |
464 | 4.16k | case curl::fuzzer::proto::SCHEME_GOPHER: |
465 | 4.16k | return mode == ScenarioRunMode::kGopherCoverage ? std::make_unique<MockServer>() : nullptr; |
466 | 3.40k | case curl::fuzzer::proto::SCHEME_GOPHERS: |
467 | 3.40k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
468 | 3.40k | if (mode == ScenarioRunMode::kGopherCoverage) { |
469 | 3.40k | return std::make_unique<TlsMockServer>(scenario.tls_certificate_chain()); |
470 | 3.40k | } |
471 | 2 | return nullptr; |
472 | | #else |
473 | | return nullptr; |
474 | | #endif |
475 | 12.5k | case curl::fuzzer::proto::SCHEME_WS: |
476 | 12.5k | return std::make_unique<WebSocketMockServer>(); |
477 | 7.81k | case curl::fuzzer::proto::SCHEME_WSS: |
478 | 7.81k | if (mode == ScenarioRunMode::kTlsWebSocketCoverage) { |
479 | 7.34k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
480 | 7.34k | return std::make_unique<TlsWebSocketMockServer>(scenario.tls_certificate_chain()); |
481 | | #else |
482 | | // MemorySanitizer builds deliberately omit OpenSSL. Keep the target |
483 | | // available without pretending plaintext can carry a WSS exchange. |
484 | | return nullptr; |
485 | | #endif |
486 | 7.34k | } |
487 | | // Preserve the compatibility target's historical interpretation of WSS |
488 | | // inputs, which exposed TLS bytes directly to the plaintext WS peer. |
489 | 470 | return std::make_unique<WebSocketMockServer>(); |
490 | 5.05k | case curl::fuzzer::proto::SCHEME_TELNET: |
491 | 5.05k | return std::make_unique<TelnetMockServer>(); |
492 | 4.15k | case curl::fuzzer::proto::SCHEME_FTP: |
493 | | // New numeric enum values may already occur in the historical mixed |
494 | | // corpus as unknown fields. Only the fixed FTP profile may reinterpret |
495 | | // one as a live two-channel protocol exchange. |
496 | 4.15k | if (mode == ScenarioRunMode::kFtpCoverage) { |
497 | 4.15k | return std::make_unique<FtpMockServer>(); |
498 | 4.15k | } |
499 | 3 | return nullptr; |
500 | 1.95k | case curl::fuzzer::proto::SCHEME_FTPS: |
501 | | // Implicit TLS shares the FTP lane, parser, and bounded data-channel |
502 | | // budget with the plaintext scheme; only the transport factory and the |
503 | | // passive-only restriction differ. Keep it gated on the same run mode so |
504 | | // no other target can turn an appended enum value into a TLS exchange. |
505 | 1.95k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
506 | 1.95k | if (mode == ScenarioRunMode::kFtpCoverage) { |
507 | 1.95k | return std::make_unique<FtpsMockServer>(); |
508 | 1.95k | } |
509 | 2 | #endif |
510 | 2 | return nullptr; |
511 | 2.23k | case curl::fuzzer::proto::SCHEME_TFTP: |
512 | | // TFTP changes the callback transport from a preconnected stream to a |
513 | | // real UDP endpoint, so compatibility inputs must not opt into it merely |
514 | | // because this build learned a new enum value. |
515 | 2.23k | if (mode == ScenarioRunMode::kTftpCoverage) { |
516 | 2.23k | return std::make_unique<TftpMockServer>(); |
517 | 2.23k | } |
518 | 1 | return nullptr; |
519 | 2.76k | case curl::fuzzer::proto::SCHEME_SMTP: |
520 | | // SMTP reuses the stream socketpair but needs command-aligned replies |
521 | | // and a DATA body drain. Only the dedicated lane may reinterpret an |
522 | | // append-only scheme value that historical corpus inputs could carry. |
523 | 2.76k | if (mode == ScenarioRunMode::kSmtpCoverage) { |
524 | 2.76k | return std::make_unique<SmtpMockServer>(); |
525 | 2.76k | } |
526 | 1 | return nullptr; |
527 | 2.43k | case curl::fuzzer::proto::SCHEME_IMAP: |
528 | | // IMAP replies are tag-aligned. Only the dedicated lane may reinterpret |
529 | | // an append-only scheme value that historical inputs could carry. |
530 | 2.43k | if (mode == ScenarioRunMode::kImapCoverage) { |
531 | 2.42k | return std::make_unique<ImapMockServer>(); |
532 | 2.42k | } |
533 | 4 | return nullptr; |
534 | 1.13k | case curl::fuzzer::proto::SCHEME_POP3: |
535 | 1.13k | if (mode == ScenarioRunMode::kPop3Coverage) { |
536 | 1.12k | return std::make_unique<Pop3MockServer>(); |
537 | 1.12k | } |
538 | 1 | return nullptr; |
539 | 1.02k | case curl::fuzzer::proto::SCHEME_MQTT: |
540 | | // MQTT reuses the stream socketpair but speaks a binary packet grammar |
541 | | // with server-initiated packets. Only the dedicated lane may |
542 | | // reinterpret an append-only scheme value that historical corpus |
543 | | // inputs could carry. |
544 | 1.02k | if (mode == ScenarioRunMode::kMqttCoverage) { |
545 | 1.02k | return std::make_unique<MqttMockServer>(); |
546 | 1.02k | } |
547 | 1 | return nullptr; |
548 | 3.21k | case curl::fuzzer::proto::SCHEME_DICT: |
549 | | // DICT sends its command sequence in one write and reads an EOF-framed |
550 | | // text body. Only the dedicated lane may reinterpret an append-only |
551 | | // scheme value that historical inputs could carry. |
552 | 3.21k | if (mode == ScenarioRunMode::kDictCoverage) { |
553 | 3.21k | return std::make_unique<DictMockServer>(); |
554 | 3.21k | } |
555 | 1 | return nullptr; |
556 | 2.07k | case curl::fuzzer::proto::SCHEME_SMTPS: |
557 | | // Implicit TLS shares the SMTP lane, parser, and command budget with the |
558 | | // plaintext scheme; only the transport factory differs. Keep it gated on |
559 | | // the same fixed run mode so no other target can turn an appended enum |
560 | | // value into a TLS exchange. |
561 | 2.07k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
562 | 2.07k | if (mode == ScenarioRunMode::kSmtpCoverage) { |
563 | 2.07k | return std::make_unique<SmtpsMockServer>(); |
564 | 2.07k | } |
565 | 1 | #endif |
566 | 1 | return nullptr; |
567 | 2.72k | case curl::fuzzer::proto::SCHEME_IMAPS: |
568 | | // Implicit TLS reuses the IMAP lane and tag-aware parser; only the |
569 | | // transport factory differs. Gate it on the same fixed run mode so no |
570 | | // other target can turn the appended enum value into a TLS exchange. |
571 | 2.72k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
572 | 2.72k | if (mode == ScenarioRunMode::kImapCoverage) { |
573 | 2.71k | return std::make_unique<ImapsMockServer>(); |
574 | 2.71k | } |
575 | 1 | #endif |
576 | 1 | return nullptr; |
577 | 2.69k | case curl::fuzzer::proto::SCHEME_POP3S: |
578 | | // Implicit TLS reuses the POP3 lane and status-line parser; only the |
579 | | // transport factory differs. Gate it on the same fixed run mode so no |
580 | | // other target can turn the appended enum value into a TLS exchange. |
581 | 2.69k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
582 | 2.69k | if (mode == ScenarioRunMode::kPop3Coverage) { |
583 | 2.69k | return std::make_unique<Pop3sMockServer>(); |
584 | 2.69k | } |
585 | 1 | #endif |
586 | 1 | return nullptr; |
587 | 3.02k | case curl::fuzzer::proto::SCHEME_MQTTS: |
588 | | // Implicit TLS shares the MQTT lane, packet parser, and server-packet |
589 | | // budget with the plaintext scheme; only the transport factory differs. |
590 | | // Keep it gated on the same fixed run mode so no other target can turn |
591 | | // an appended enum value into a TLS exchange. |
592 | 3.02k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
593 | 3.02k | if (mode == ScenarioRunMode::kMqttCoverage) { |
594 | 3.02k | return std::make_unique<MqttsMockServer>(); |
595 | 3.02k | } |
596 | 2 | #endif |
597 | 2 | return nullptr; |
598 | 3.57k | case curl::fuzzer::proto::SCHEME_FILE: |
599 | | // FILE reads the local filesystem directly. The dedicated lane pins its |
600 | | // path to /dev/null, so there is no socket or scriptable peer to install. |
601 | 3.57k | return nullptr; |
602 | 2.83k | case curl::fuzzer::proto::SCHEME_LDAP: |
603 | | // LDAP is BER/ASN.1 rather than line-oriented and OpenLDAP performs its |
604 | | // own reads on a duplicate of curl's socket. Only the dedicated lane |
605 | | // may turn an appended scheme value into a live bind/search exchange. |
606 | 2.83k | if (mode == ScenarioRunMode::kLdapCoverage) { |
607 | 2.83k | return std::make_unique<LdapMockServer>(); |
608 | 2.83k | } |
609 | 1 | return nullptr; |
610 | 2.93k | case curl::fuzzer::proto::SCHEME_LDAPS: |
611 | | // Implicit TLS shares the LDAP lane, BER parser, and scripted messages |
612 | | // with the plaintext scheme; only the transport factory differs. Keep it |
613 | | // gated on the same fixed run mode so no other target can turn an |
614 | | // appended enum value into a TLS exchange. |
615 | 2.93k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
616 | 2.93k | if (mode == ScenarioRunMode::kLdapCoverage) { |
617 | 2.93k | return std::make_unique<LdapsMockServer>(); |
618 | 2.93k | } |
619 | 1 | #endif |
620 | 1 | return nullptr; |
621 | 6.30k | case curl::fuzzer::proto::SCHEME_RTSP: |
622 | | // RTSP is request-aligned rather than greeting-aligned, so the |
623 | | // dedicated lane is required to select the peer that waits for the |
624 | | // request terminator before releasing each scripted response. |
625 | 6.30k | if (mode == ScenarioRunMode::kRtspCoverage) { |
626 | 6.30k | return std::make_unique<RtspMockServer>(); |
627 | 6.30k | } |
628 | 2 | return nullptr; |
629 | 9.28k | case curl::fuzzer::proto::SCHEME_IPFS: |
630 | 9.29k | case curl::fuzzer::proto::SCHEME_IPNS: |
631 | | // The rewrite above turns these into ordinary HTTP GETs. Only the |
632 | | // dedicated fast lane may reinterpret an append-only scheme value, and |
633 | | // its compatibility peer is the plaintext MockServer. |
634 | 9.29k | if (mode == ScenarioRunMode::kFastProtocol) { |
635 | 9.29k | return std::make_unique<MockServer>(); |
636 | 9.29k | } |
637 | 2 | return nullptr; |
638 | 0 | case curl::fuzzer::proto::SCHEME_UNSPECIFIED: |
639 | 0 | default: |
640 | 0 | return nullptr; |
641 | 171k | } |
642 | 171k | } |
643 | | |
644 | | #if defined(PROTO_FUZZER_OBSERVE_STATE) |
645 | | /// Retain protocol-specific outcomes in coverage and introspector builds. |
646 | | /// These peers already collect bounded state for focused tests; reading it |
647 | | /// after the transfer gives profiling builds visibility into the completed |
648 | | /// exchange without changing the protocol work performed by ordinary fuzzers. |
649 | 216k | void ObserveProtocolPeerState(MockServerBase* mock) { |
650 | 216k | if (auto* ftp = dynamic_cast<FtpMockServer*>(mock)) { |
651 | 6.07k | const volatile std::size_t ftp_observation[] = {ftp->control_transcript().size(), ftp->uploaded_data().size(), |
652 | 6.07k | ftp->opened_data_connection_count()}; |
653 | 6.07k | (void)ftp_observation; |
654 | 6.07k | } |
655 | | |
656 | 216k | if (auto* tftp = dynamic_cast<TftpMockServer*>(mock)) { |
657 | 2.22k | const volatile std::size_t tftp_observation[] = {tftp->received_datagrams().size(), tftp->request_port(), |
658 | 2.22k | tftp->transfer_port()}; |
659 | 2.22k | (void)tftp_observation; |
660 | 2.22k | } |
661 | | |
662 | 216k | if (auto* smtp = dynamic_cast<SmtpMockServer*>(mock)) { |
663 | 4.82k | const volatile std::size_t smtp_observation[] = {smtp->control_transcript().size(), smtp->uploaded_data().size()}; |
664 | 4.82k | (void)smtp_observation; |
665 | 4.82k | } |
666 | | |
667 | 216k | if (auto* imap = dynamic_cast<ImapMockServer*>(mock)) { |
668 | 5.12k | const volatile std::size_t imap_observation[] = {imap->control_transcript().size(), imap->uploaded_data().size()}; |
669 | 5.12k | (void)imap_observation; |
670 | 5.12k | } |
671 | | |
672 | 216k | if (auto* pop3 = dynamic_cast<Pop3MockServer*>(mock)) { |
673 | 3.78k | const volatile std::size_t pop3_observation[] = {pop3->control_transcript().size(), pop3->uploaded_data().size()}; |
674 | 3.78k | (void)pop3_observation; |
675 | 3.78k | } |
676 | | |
677 | 216k | if (auto* mqtt = dynamic_cast<MqttMockServer*>(mock)) { |
678 | 4.02k | const volatile std::size_t mqtt_observation[] = {mqtt->packet_transcript().size(), mqtt->packet_count(), |
679 | 4.02k | mqtt->publish_count(), mqtt->subscribe_count(), |
680 | 4.02k | mqtt->published_payload().size()}; |
681 | 4.02k | (void)mqtt_observation; |
682 | 4.02k | } |
683 | | |
684 | 216k | if (auto* dict = dynamic_cast<DictMockServer*>(mock)) { |
685 | 3.17k | const volatile std::size_t dict_observation[] = {dict->control_transcript().size(), dict->uploaded_data().size()}; |
686 | 3.17k | (void)dict_observation; |
687 | 3.17k | } |
688 | | |
689 | 216k | if (auto* ldap = dynamic_cast<LdapMockServer*>(mock)) { |
690 | 5.73k | const volatile std::size_t ldap_observation[] = {ldap->packet_transcript().size(), ldap->bind_request_count(), |
691 | 5.73k | ldap->search_request_count(), ldap->scripted_message_count(), |
692 | 5.73k | ldap->synthesized_done_count(), ldap->starttls_request_count()}; |
693 | 5.73k | (void)ldap_observation; |
694 | 5.73k | } |
695 | | |
696 | 216k | if (auto* rtsp = dynamic_cast<RtspMockServer*>(mock)) { |
697 | 6.27k | const volatile std::size_t rtsp_observation[] = {rtsp->request_count(), |
698 | 6.27k | rtsp->response_count(), |
699 | 6.27k | rtsp->last_client_cseq(), |
700 | 6.27k | rtsp->control_transcript().size(), |
701 | 6.27k | rtsp->last_request_line().size(), |
702 | 6.27k | rtsp->last_stream_uri().size()}; |
703 | 6.27k | (void)rtsp_observation; |
704 | 6.27k | } |
705 | | |
706 | 216k | if (auto* proxy = dynamic_cast<ProxyTransportMockServer*>(mock)) { |
707 | 7.93k | const volatile std::size_t proxy_observation = proxy->client_transcript().size(); |
708 | 7.93k | (void)proxy_observation; |
709 | 7.93k | } |
710 | | |
711 | 216k | if (auto* transport = dynamic_cast<HttpTransportMockServer*>(mock)) { |
712 | 1.09k | const volatile std::size_t transport_observation = transport->client_transcript().size(); |
713 | 1.09k | (void)transport_observation; |
714 | 1.09k | } |
715 | | |
716 | 216k | if (auto* options = dynamic_cast<ProxyOptionMockServer*>(mock)) { |
717 | 1.62k | const volatile std::size_t option_observation = options->client_transcript().size(); |
718 | 1.62k | (void)option_observation; |
719 | 1.62k | } |
720 | | |
721 | 216k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
722 | 216k | if (auto* ftps = dynamic_cast<FtpsMockServer*>(mock)) { |
723 | 1.94k | const volatile std::size_t ftps_observation[] = {ftps->completed_handshake_count(), |
724 | 1.94k | static_cast<std::size_t>(ftps->negotiated_tls_version()), |
725 | 1.94k | ftps->negotiated_alpn().size()}; |
726 | 1.94k | (void)ftps_observation; |
727 | 1.94k | } |
728 | | |
729 | 216k | if (auto* smtps = dynamic_cast<SmtpsMockServer*>(mock)) { |
730 | 2.07k | const volatile std::size_t smtps_observation[] = {smtps->completed_handshake_count(), |
731 | 2.07k | static_cast<std::size_t>(smtps->negotiated_tls_version()), |
732 | 2.07k | smtps->negotiated_alpn().size()}; |
733 | 2.07k | (void)smtps_observation; |
734 | 2.07k | } |
735 | | |
736 | 216k | if (auto* imaps = dynamic_cast<ImapsMockServer*>(mock)) { |
737 | 2.71k | const volatile std::size_t imaps_observation[] = {imaps->completed_handshake_count(), |
738 | 2.71k | static_cast<std::size_t>(imaps->negotiated_tls_version()), |
739 | 2.71k | imaps->negotiated_alpn().size()}; |
740 | 2.71k | (void)imaps_observation; |
741 | 2.71k | } |
742 | | |
743 | 216k | if (auto* mqtts = dynamic_cast<MqttsMockServer*>(mock)) { |
744 | 3.01k | const volatile std::size_t mqtts_observation[] = {mqtts->completed_handshake_count(), |
745 | 3.01k | static_cast<std::size_t>(mqtts->negotiated_tls_version()), |
746 | 3.01k | mqtts->negotiated_alpn().size(), mqtts->packet_count()}; |
747 | 3.01k | (void)mqtts_observation; |
748 | 3.01k | } |
749 | | |
750 | 216k | if (auto* pop3s = dynamic_cast<Pop3sMockServer*>(mock)) { |
751 | 2.68k | const volatile std::size_t pop3s_observation[] = {pop3s->completed_handshake_count(), |
752 | 2.68k | static_cast<std::size_t>(pop3s->negotiated_tls_version()), |
753 | 2.68k | pop3s->negotiated_alpn().size()}; |
754 | 2.68k | (void)pop3s_observation; |
755 | 2.68k | } |
756 | | |
757 | 216k | if (auto* ldaps = dynamic_cast<LdapsMockServer*>(mock)) { |
758 | 2.92k | const volatile std::size_t ldaps_observation[] = {ldaps->completed_handshake_count(), |
759 | 2.92k | static_cast<std::size_t>(ldaps->negotiated_tls_version()), |
760 | 2.92k | ldaps->negotiated_alpn().size()}; |
761 | 2.92k | (void)ldaps_observation; |
762 | 2.92k | } |
763 | 216k | #endif |
764 | | |
765 | 216k | if (auto* websocket = dynamic_cast<WebSocketMockServer*>(mock)) { |
766 | 20.2k | const volatile std::size_t websocket_observation = websocket->manual_received_bytes(); |
767 | 20.2k | (void)websocket_observation; |
768 | 20.2k | } |
769 | | |
770 | 216k | #if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER) |
771 | 216k | if (auto* websocket = dynamic_cast<TlsWebSocketMockServer*>(mock)) { |
772 | 7.30k | const volatile std::size_t tls_websocket_observation = websocket->completed_handshake_count(); |
773 | 7.30k | (void)tls_websocket_observation; |
774 | 7.30k | } |
775 | 216k | #endif |
776 | | |
777 | | #if defined(PROTO_FUZZER_HAS_HTTP3_MOCK_SERVER) |
778 | | if (auto* http3 = dynamic_cast<Http3MockServer*>(mock)) { |
779 | | const volatile std::size_t http3_observation[] = {http3->handshake_complete(), http3->request_headers_received(), |
780 | | http3->executed_action_count(), http3->server_port()}; |
781 | | (void)http3_observation; |
782 | | } |
783 | | #endif |
784 | 216k | } |
785 | | #endif |
786 | | |
787 | | } // namespace |
788 | | |
789 | | /// Execute one scenario under a complete target behaviour. An enum makes the |
790 | | /// supported fast, coverage, and API paths explicit and prevents callers from |
791 | | /// constructing meaningless combinations of independent switches. |
792 | | /// @param scenario Structured transfer and optional API plan. |
793 | | /// @param mode Runtime coverage and lifecycle policy for this invocation. |
794 | | /// @return zero after either a bounded run or an ignored invalid scenario. |
795 | 481k | int RunScenario(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) { |
796 | | // Every input starts from a known off trace baseline (and returns to it when |
797 | | // the scope exits), so one input's trace selection cannot leak into another. |
798 | 481k | proto_fuzzer::ScopedTraceReset trace_reset; |
799 | 481k | if (mode == ScenarioRunMode::kMultiTransfer) { |
800 | 24.3k | (void)proto_fuzzer::RunMultiTransferScenario(scenario); |
801 | 24.3k | return 0; |
802 | 24.3k | } |
803 | | |
804 | 457k | const char* prefix = SchemePrefix(scenario.scheme()); |
805 | 457k | if (prefix == nullptr || scenario.host_path().empty()) { |
806 | 3.22k | return 0; |
807 | 3.22k | } |
808 | | |
809 | | // FILE is the one peerless lane. Gate it on the dedicated run mode so the |
810 | | // compatibility target cannot reinterpret the append-only enum value as a |
811 | | // local filesystem read from an accumulated corpus entry. |
812 | 454k | const bool file_lane = |
813 | 454k | mode == ScenarioRunMode::kFileCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_FILE; |
814 | 454k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_FILE && !file_lane) { |
815 | 2 | return 0; |
816 | 2 | } |
817 | 454k | if (file_lane && !HasCanonicalFilePath(scenario.host_path())) { |
818 | 0 | return 0; |
819 | 0 | } |
820 | | |
821 | 454k | std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode); |
822 | 454k | if (!mock && !file_lane) { |
823 | 60 | return 0; |
824 | 60 | } |
825 | | |
826 | | // The peer survives every easy option/callback, including cleanup of a |
827 | | // bounded incomplete transfer. TransferSession owns the rest of that graph. |
828 | 454k | TransferSession transfer; |
829 | 454k | (void)transfer.PrepareInputFiles(scenario, mode); |
830 | 454k | if (!transfer.Initialize()) { |
831 | 1.39k | return 0; |
832 | 1.39k | } |
833 | 452k | CURL* easy = transfer.easy(); |
834 | 452k | const std::string url = ScenarioUrl(scenario, prefix); |
835 | 452k | bool resolver_mode_ready = true; |
836 | 456k | const auto configure_easy = [&] { |
837 | 456k | transfer.ApplyBaseline(scenario); |
838 | 456k | (void)curl_easy_setopt(easy, CURLOPT_URL, url.c_str()); |
839 | 456k | if (mock) { |
840 | 449k | mock->Install(easy); |
841 | 449k | } |
842 | 456k | transfer.ApplyInputFiles(); |
843 | | // Repeat the option-count bound for compatibility and direct callers. |
844 | 456k | (void)ApplyScenarioOptions(easy, scenario); |
845 | 456k | if (mode == ScenarioRunMode::kLdapCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_LDAP && |
846 | 5.62k | scenario.ldap_starttls()) { |
847 | 150 | (void)curl_easy_setopt(easy, CURLOPT_USE_SSL, static_cast<long>(CURLUSESSL_ALL)); |
848 | 150 | } |
849 | 456k | if (mode == ScenarioRunMode::kResolverCoverage) { |
850 | 15.6k | resolver_mode_ready = ApplyResolverMode(easy, scenario.resolver_mode()); |
851 | 15.6k | } |
852 | 456k | if (mode == ScenarioRunMode::kTlsCoverage) { |
853 | 54.3k | (void)ApplyTlsClientCredentialProfile(easy, scenario.tls_client_credential_profile()); |
854 | 54.3k | } |
855 | 456k | }; scenario_runner.cc:proto_fuzzer::RunScenario(curl::fuzzer::proto::Scenario const&, proto_fuzzer::ScenarioRunMode)::$_1::operator()() const Line | Count | Source | 836 | 228k | const auto configure_easy = [&] { | 837 | 228k | transfer.ApplyBaseline(scenario); | 838 | 228k | (void)curl_easy_setopt(easy, CURLOPT_URL, url.c_str()); | 839 | 228k | if (mock) { | 840 | 224k | mock->Install(easy); | 841 | 224k | } | 842 | 228k | transfer.ApplyInputFiles(); | 843 | | // Repeat the option-count bound for compatibility and direct callers. | 844 | 228k | (void)ApplyScenarioOptions(easy, scenario); | 845 | 228k | if (mode == ScenarioRunMode::kLdapCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_LDAP && | 846 | 2.81k | scenario.ldap_starttls()) { | 847 | 75 | (void)curl_easy_setopt(easy, CURLOPT_USE_SSL, static_cast<long>(CURLUSESSL_ALL)); | 848 | 75 | } | 849 | 228k | if (mode == ScenarioRunMode::kResolverCoverage) { | 850 | 7.81k | resolver_mode_ready = ApplyResolverMode(easy, scenario.resolver_mode()); | 851 | 7.81k | } | 852 | 228k | if (mode == ScenarioRunMode::kTlsCoverage) { | 853 | 27.1k | (void)ApplyTlsClientCredentialProfile(easy, scenario.tls_client_credential_profile()); | 854 | 27.1k | } | 855 | 228k | }; |
scenario_runner.cc:proto_fuzzer::RunScenario(curl::fuzzer::proto::Scenario const&, proto_fuzzer::ScenarioRunMode)::$_1::operator()() const Line | Count | Source | 836 | 228k | const auto configure_easy = [&] { | 837 | 228k | transfer.ApplyBaseline(scenario); | 838 | 228k | (void)curl_easy_setopt(easy, CURLOPT_URL, url.c_str()); | 839 | 228k | if (mock) { | 840 | 224k | mock->Install(easy); | 841 | 224k | } | 842 | 228k | transfer.ApplyInputFiles(); | 843 | | // Repeat the option-count bound for compatibility and direct callers. | 844 | 228k | (void)ApplyScenarioOptions(easy, scenario); | 845 | 228k | if (mode == ScenarioRunMode::kLdapCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_LDAP && | 846 | 2.81k | scenario.ldap_starttls()) { | 847 | 75 | (void)curl_easy_setopt(easy, CURLOPT_USE_SSL, static_cast<long>(CURLUSESSL_ALL)); | 848 | 75 | } | 849 | 228k | if (mode == ScenarioRunMode::kResolverCoverage) { | 850 | 7.81k | resolver_mode_ready = ApplyResolverMode(easy, scenario.resolver_mode()); | 851 | 7.81k | } | 852 | 228k | if (mode == ScenarioRunMode::kTlsCoverage) { | 853 | 27.1k | (void)ApplyTlsClientCredentialProfile(easy, scenario.tls_client_credential_profile()); | 854 | 27.1k | } | 855 | 228k | }; |
|
856 | 452k | configure_easy(); |
857 | 452k | if (!resolver_mode_ready) { |
858 | 6 | return 0; |
859 | 6 | } |
860 | 452k | transfer.ConfigureAltSvcRouting(scenario.host_path()); |
861 | | |
862 | 452k | if (file_lane) { |
863 | | // The file:// handler reads or writes the pinned /dev/null target (or |
864 | | // rejects a mutated authority) synchronously and never opens a socket. |
865 | | // Install the bounded request-data owner before perform so retained upload |
866 | | // options consume Scenario.upload through curl's read callback instead of |
867 | | // falling back to stdin. The baseline's short timeout still bounds the |
868 | | // transfer. |
869 | 7.03k | (void)transfer.InstallRequestData(scenario); |
870 | 7.03k | const CURLcode transfer_result = curl_easy_perform(easy); |
871 | 7.03k | (void)transfer_result; |
872 | 7.03k | ProbeTransferResults(easy); |
873 | 7.03k | return 0; |
874 | 7.03k | } |
875 | | |
876 | 445k | if (mode == ScenarioRunMode::kResolverCoverage) { |
877 | | // The normal CONNECT_TO baseline deliberately bypasses DNS. This lane |
878 | | // removes only that override; OPENSOCKET still returns the in-process |
879 | | // socketpair, so no resolved address can receive network traffic. |
880 | 15.6k | (void)curl_easy_setopt(easy, CURLOPT_CONNECT_TO, nullptr); |
881 | 15.6k | } |
882 | | |
883 | 445k | const curl::fuzzer::proto::ApiPlan* api_plan = |
884 | 445k | mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr; |
885 | 445k | if (api_plan != nullptr && api_plan->reset_easy() && transfer.ResetConfiguration()) { |
886 | 3.89k | configure_easy(); |
887 | 3.89k | } |
888 | | |
889 | 445k | ApiLifecycle* api_lifecycle = nullptr; |
890 | 445k | if (api_plan != nullptr) { |
891 | 11.1k | api_lifecycle = transfer.InstallApiLifecycle(*api_plan, url); |
892 | 11.1k | mock->SetMultiObserver([api_lifecycle](CURLM* multi) { api_lifecycle->SetActiveMulti(multi); }); |
893 | 11.1k | } |
894 | | |
895 | | // The DoH lane lets the loopback responder answer every lookup, including |
896 | | // the address query, so it must not install the resolver lane's fixed |
897 | | // CURLOPT_RESOLVE cache entries. |
898 | 445k | const bool apply_resolve_entries = mode == ScenarioRunMode::kResolverCoverage && !scenario.doh_https_rr() && |
899 | 13.9k | scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_DEFAULT; |
900 | 445k | ScenarioRequestData* request_data = transfer.InstallRequestData(scenario, apply_resolve_entries); |
901 | 445k | if (mode == ScenarioRunMode::kResolverCoverage && !request_data->resolve_entries_ready()) { |
902 | 24 | return 0; |
903 | 24 | } |
904 | 445k | mock->ConfigureRequestData(request_data); |
905 | 445k | const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode(); |
906 | 445k | CURLcode transfer_result = CURLE_FAILED_INIT; |
907 | 445k | if (mode == ScenarioRunMode::kResolverCoverage && |
908 | 15.6k | scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL) { |
909 | 380 | auto* resolver_mock = dynamic_cast<MockServer*>(mock.get()); |
910 | 380 | if (resolver_mock != nullptr) { |
911 | 380 | transfer_result = RunQueuedResolverCancellation(easy, resolver_mock, scenario); |
912 | 380 | } |
913 | 445k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) { |
914 | 1.43k | transfer_result = mock->DriveEasyScenario(easy, scenario); |
915 | 443k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) { |
916 | 224 | transfer_result = mock->DriveEasyScenario(easy, scenario, true); |
917 | 443k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY) { |
918 | 394 | transfer_result = mock->DriveConnectOnlyScenario(easy, scenario).connect_result; |
919 | 443k | } else { |
920 | 443k | transfer_result = mock->DriveScenario(easy, scenario); |
921 | 443k | } |
922 | 445k | if (api_plan != nullptr && api_plan->repeat_mime_post() && transfer_result == CURLE_OK) { |
923 | | // Re-enter the public transfer setup with the same easy handle, MIME root, |
924 | | // and callback cursor. A completed first body leaves a non-rewindable |
925 | | // callback at EOF; curl's second MIME reader setup must then report its |
926 | | // normal public rewind failure and clean up the unattached reader before |
927 | | // it can send another request. |
928 | 722 | transfer_result = mock->DriveScenario(easy, scenario); |
929 | 722 | } |
930 | 445k | if (api_lifecycle != nullptr) { |
931 | 11.1k | const bool retains_internal_multi = drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM || |
932 | 9.76k | drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS || |
933 | 9.53k | drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY; |
934 | | // An easy entrypoint can fail before attaching its private multi. The |
935 | | // drive mode alone therefore cannot establish curl_easy_upkeep's state |
936 | | // precondition. These are the two pre-attachment failure results. |
937 | 11.1k | const bool probe_upkeep = |
938 | 11.1k | retains_internal_multi && transfer_result != CURLE_OUT_OF_MEMORY && transfer_result != CURLE_FAILED_INIT; |
939 | 11.1k | api_lifecycle->ProbeTransferResults(probe_upkeep); |
940 | 11.1k | api_lifecycle->ProbeEasyDuplication(); |
941 | 11.1k | #if defined(PROTO_FUZZER_OBSERVE_STATE) |
942 | 11.1k | const volatile std::size_t api_observation[] = { |
943 | 11.1k | api_lifecycle->response_pause_returned(), api_lifecycle->response_bytes_received(), |
944 | 11.1k | api_lifecycle->reentrant_probes_run(), api_lifecycle->reentrant_recursive_rejections()}; |
945 | 11.1k | (void)api_observation; |
946 | 11.1k | #endif |
947 | 434k | } else if (mode != ScenarioRunMode::kFastProtocol) { |
948 | 383k | ProbeTransferResults(easy); |
949 | 383k | } |
950 | | |
951 | 445k | #if defined(PROTO_FUZZER_OBSERVE_STATE) |
952 | | // TransferSession still owns these completed callback states. Local volatile |
953 | | // stores retain their getter calls during profiling without adding work to |
954 | | // ordinary fuzz builds or changing the transfer's callback configuration. |
955 | 445k | const RequestBuildStats& stats = request_data->stats(); |
956 | 445k | const UploadScriptState& upload = request_data->upload_state(); |
957 | 445k | const volatile std::size_t request_observation[] = {stats.request_headers, |
958 | 445k | stats.resolve_entries, |
959 | 445k | stats.telnet_options, |
960 | 445k | stats.mime_parts, |
961 | 445k | stats.mime_callback_parts, |
962 | 445k | stats.mime_headers, |
963 | 445k | stats.generated_mime_bytes, |
964 | 445k | request_data->upload_callbacks_installed(), |
965 | 445k | upload.data_size(), |
966 | 445k | upload.read_step_count(), |
967 | 445k | upload.offset(), |
968 | 445k | upload.seek_calls(), |
969 | 445k | upload.scripted()}; |
970 | 445k | (void)request_observation; |
971 | 445k | ObserveProtocolPeerState(mock.get()); |
972 | 445k | #endif |
973 | 445k | return 0; |
974 | 445k | } proto_fuzzer::RunScenario(curl::fuzzer::proto::Scenario const&, proto_fuzzer::ScenarioRunMode) Line | Count | Source | 795 | 240k | int RunScenario(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) { | 796 | | // Every input starts from a known off trace baseline (and returns to it when | 797 | | // the scope exits), so one input's trace selection cannot leak into another. | 798 | 240k | proto_fuzzer::ScopedTraceReset trace_reset; | 799 | 240k | if (mode == ScenarioRunMode::kMultiTransfer) { | 800 | 12.1k | (void)proto_fuzzer::RunMultiTransferScenario(scenario); | 801 | 12.1k | return 0; | 802 | 12.1k | } | 803 | | | 804 | 228k | const char* prefix = SchemePrefix(scenario.scheme()); | 805 | 228k | if (prefix == nullptr || scenario.host_path().empty()) { | 806 | 1.61k | return 0; | 807 | 1.61k | } | 808 | | | 809 | | // FILE is the one peerless lane. Gate it on the dedicated run mode so the | 810 | | // compatibility target cannot reinterpret the append-only enum value as a | 811 | | // local filesystem read from an accumulated corpus entry. | 812 | 227k | const bool file_lane = | 813 | 227k | mode == ScenarioRunMode::kFileCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_FILE; | 814 | 227k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_FILE && !file_lane) { | 815 | 1 | return 0; | 816 | 1 | } | 817 | 227k | if (file_lane && !HasCanonicalFilePath(scenario.host_path())) { | 818 | 0 | return 0; | 819 | 0 | } | 820 | | | 821 | 227k | std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode); | 822 | 227k | if (!mock && !file_lane) { | 823 | 30 | return 0; | 824 | 30 | } | 825 | | | 826 | | // The peer survives every easy option/callback, including cleanup of a | 827 | | // bounded incomplete transfer. TransferSession owns the rest of that graph. | 828 | 227k | TransferSession transfer; | 829 | 227k | (void)transfer.PrepareInputFiles(scenario, mode); | 830 | 227k | if (!transfer.Initialize()) { | 831 | 696 | return 0; | 832 | 696 | } | 833 | 226k | CURL* easy = transfer.easy(); | 834 | 226k | const std::string url = ScenarioUrl(scenario, prefix); | 835 | 226k | bool resolver_mode_ready = true; | 836 | 226k | const auto configure_easy = [&] { | 837 | 226k | transfer.ApplyBaseline(scenario); | 838 | 226k | (void)curl_easy_setopt(easy, CURLOPT_URL, url.c_str()); | 839 | 226k | if (mock) { | 840 | 226k | mock->Install(easy); | 841 | 226k | } | 842 | 226k | transfer.ApplyInputFiles(); | 843 | | // Repeat the option-count bound for compatibility and direct callers. | 844 | 226k | (void)ApplyScenarioOptions(easy, scenario); | 845 | 226k | if (mode == ScenarioRunMode::kLdapCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_LDAP && | 846 | 226k | scenario.ldap_starttls()) { | 847 | 226k | (void)curl_easy_setopt(easy, CURLOPT_USE_SSL, static_cast<long>(CURLUSESSL_ALL)); | 848 | 226k | } | 849 | 226k | if (mode == ScenarioRunMode::kResolverCoverage) { | 850 | 226k | resolver_mode_ready = ApplyResolverMode(easy, scenario.resolver_mode()); | 851 | 226k | } | 852 | 226k | if (mode == ScenarioRunMode::kTlsCoverage) { | 853 | 226k | (void)ApplyTlsClientCredentialProfile(easy, scenario.tls_client_credential_profile()); | 854 | 226k | } | 855 | 226k | }; | 856 | 226k | configure_easy(); | 857 | 226k | if (!resolver_mode_ready) { | 858 | 3 | return 0; | 859 | 3 | } | 860 | 226k | transfer.ConfigureAltSvcRouting(scenario.host_path()); | 861 | | | 862 | 226k | if (file_lane) { | 863 | | // The file:// handler reads or writes the pinned /dev/null target (or | 864 | | // rejects a mutated authority) synchronously and never opens a socket. | 865 | | // Install the bounded request-data owner before perform so retained upload | 866 | | // options consume Scenario.upload through curl's read callback instead of | 867 | | // falling back to stdin. The baseline's short timeout still bounds the | 868 | | // transfer. | 869 | 3.51k | (void)transfer.InstallRequestData(scenario); | 870 | 3.51k | const CURLcode transfer_result = curl_easy_perform(easy); | 871 | 3.51k | (void)transfer_result; | 872 | 3.51k | ProbeTransferResults(easy); | 873 | 3.51k | return 0; | 874 | 3.51k | } | 875 | | | 876 | 222k | if (mode == ScenarioRunMode::kResolverCoverage) { | 877 | | // The normal CONNECT_TO baseline deliberately bypasses DNS. This lane | 878 | | // removes only that override; OPENSOCKET still returns the in-process | 879 | | // socketpair, so no resolved address can receive network traffic. | 880 | 7.81k | (void)curl_easy_setopt(easy, CURLOPT_CONNECT_TO, nullptr); | 881 | 7.81k | } | 882 | | | 883 | 222k | const curl::fuzzer::proto::ApiPlan* api_plan = | 884 | 222k | mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr; | 885 | 222k | if (api_plan != nullptr && api_plan->reset_easy() && transfer.ResetConfiguration()) { | 886 | 1.94k | configure_easy(); | 887 | 1.94k | } | 888 | | | 889 | 222k | ApiLifecycle* api_lifecycle = nullptr; | 890 | 222k | if (api_plan != nullptr) { | 891 | 5.59k | api_lifecycle = transfer.InstallApiLifecycle(*api_plan, url); | 892 | 5.59k | mock->SetMultiObserver([api_lifecycle](CURLM* multi) { api_lifecycle->SetActiveMulti(multi); }); | 893 | 5.59k | } | 894 | | | 895 | | // The DoH lane lets the loopback responder answer every lookup, including | 896 | | // the address query, so it must not install the resolver lane's fixed | 897 | | // CURLOPT_RESOLVE cache entries. | 898 | 222k | const bool apply_resolve_entries = mode == ScenarioRunMode::kResolverCoverage && !scenario.doh_https_rr() && | 899 | 6.95k | scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_DEFAULT; | 900 | 222k | ScenarioRequestData* request_data = transfer.InstallRequestData(scenario, apply_resolve_entries); | 901 | 222k | if (mode == ScenarioRunMode::kResolverCoverage && !request_data->resolve_entries_ready()) { | 902 | 12 | return 0; | 903 | 12 | } | 904 | 222k | mock->ConfigureRequestData(request_data); | 905 | 222k | const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode(); | 906 | 222k | CURLcode transfer_result = CURLE_FAILED_INIT; | 907 | 222k | if (mode == ScenarioRunMode::kResolverCoverage && | 908 | 7.80k | scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL) { | 909 | 190 | auto* resolver_mock = dynamic_cast<MockServer*>(mock.get()); | 910 | 190 | if (resolver_mock != nullptr) { | 911 | 190 | transfer_result = RunQueuedResolverCancellation(easy, resolver_mock, scenario); | 912 | 190 | } | 913 | 222k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) { | 914 | 718 | transfer_result = mock->DriveEasyScenario(easy, scenario); | 915 | 221k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) { | 916 | 112 | transfer_result = mock->DriveEasyScenario(easy, scenario, true); | 917 | 221k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY) { | 918 | 197 | transfer_result = mock->DriveConnectOnlyScenario(easy, scenario).connect_result; | 919 | 221k | } else { | 920 | 221k | transfer_result = mock->DriveScenario(easy, scenario); | 921 | 221k | } | 922 | 222k | if (api_plan != nullptr && api_plan->repeat_mime_post() && transfer_result == CURLE_OK) { | 923 | | // Re-enter the public transfer setup with the same easy handle, MIME root, | 924 | | // and callback cursor. A completed first body leaves a non-rewindable | 925 | | // callback at EOF; curl's second MIME reader setup must then report its | 926 | | // normal public rewind failure and clean up the unattached reader before | 927 | | // it can send another request. | 928 | 361 | transfer_result = mock->DriveScenario(easy, scenario); | 929 | 361 | } | 930 | 222k | if (api_lifecycle != nullptr) { | 931 | 5.59k | const bool retains_internal_multi = drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM || | 932 | 4.88k | drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS || | 933 | 4.76k | drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY; | 934 | | // An easy entrypoint can fail before attaching its private multi. The | 935 | | // drive mode alone therefore cannot establish curl_easy_upkeep's state | 936 | | // precondition. These are the two pre-attachment failure results. | 937 | 5.59k | const bool probe_upkeep = | 938 | 5.59k | retains_internal_multi && transfer_result != CURLE_OUT_OF_MEMORY && transfer_result != CURLE_FAILED_INIT; | 939 | 5.59k | api_lifecycle->ProbeTransferResults(probe_upkeep); | 940 | 5.59k | api_lifecycle->ProbeEasyDuplication(); | 941 | 5.59k | #if defined(PROTO_FUZZER_OBSERVE_STATE) | 942 | 5.59k | const volatile std::size_t api_observation[] = { | 943 | 5.59k | api_lifecycle->response_pause_returned(), api_lifecycle->response_bytes_received(), | 944 | 5.59k | api_lifecycle->reentrant_probes_run(), api_lifecycle->reentrant_recursive_rejections()}; | 945 | 5.59k | (void)api_observation; | 946 | 5.59k | #endif | 947 | 217k | } else if (mode != ScenarioRunMode::kFastProtocol) { | 948 | 191k | ProbeTransferResults(easy); | 949 | 191k | } | 950 | | | 951 | 222k | #if defined(PROTO_FUZZER_OBSERVE_STATE) | 952 | | // TransferSession still owns these completed callback states. Local volatile | 953 | | // stores retain their getter calls during profiling without adding work to | 954 | | // ordinary fuzz builds or changing the transfer's callback configuration. | 955 | 222k | const RequestBuildStats& stats = request_data->stats(); | 956 | 222k | const UploadScriptState& upload = request_data->upload_state(); | 957 | 222k | const volatile std::size_t request_observation[] = {stats.request_headers, | 958 | 222k | stats.resolve_entries, | 959 | 222k | stats.telnet_options, | 960 | 222k | stats.mime_parts, | 961 | 222k | stats.mime_callback_parts, | 962 | 222k | stats.mime_headers, | 963 | 222k | stats.generated_mime_bytes, | 964 | 222k | request_data->upload_callbacks_installed(), | 965 | 222k | upload.data_size(), | 966 | 222k | upload.read_step_count(), | 967 | 222k | upload.offset(), | 968 | 222k | upload.seek_calls(), | 969 | 222k | upload.scripted()}; | 970 | 222k | (void)request_observation; | 971 | 222k | ObserveProtocolPeerState(mock.get()); | 972 | 222k | #endif | 973 | 222k | return 0; | 974 | 222k | } |
proto_fuzzer::RunScenario(curl::fuzzer::proto::Scenario const&, proto_fuzzer::ScenarioRunMode) Line | Count | Source | 795 | 240k | int RunScenario(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) { | 796 | | // Every input starts from a known off trace baseline (and returns to it when | 797 | | // the scope exits), so one input's trace selection cannot leak into another. | 798 | 240k | proto_fuzzer::ScopedTraceReset trace_reset; | 799 | 240k | if (mode == ScenarioRunMode::kMultiTransfer) { | 800 | 12.1k | (void)proto_fuzzer::RunMultiTransferScenario(scenario); | 801 | 12.1k | return 0; | 802 | 12.1k | } | 803 | | | 804 | 228k | const char* prefix = SchemePrefix(scenario.scheme()); | 805 | 228k | if (prefix == nullptr || scenario.host_path().empty()) { | 806 | 1.61k | return 0; | 807 | 1.61k | } | 808 | | | 809 | | // FILE is the one peerless lane. Gate it on the dedicated run mode so the | 810 | | // compatibility target cannot reinterpret the append-only enum value as a | 811 | | // local filesystem read from an accumulated corpus entry. | 812 | 227k | const bool file_lane = | 813 | 227k | mode == ScenarioRunMode::kFileCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_FILE; | 814 | 227k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_FILE && !file_lane) { | 815 | 1 | return 0; | 816 | 1 | } | 817 | 227k | if (file_lane && !HasCanonicalFilePath(scenario.host_path())) { | 818 | 0 | return 0; | 819 | 0 | } | 820 | | | 821 | 227k | std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode); | 822 | 227k | if (!mock && !file_lane) { | 823 | 30 | return 0; | 824 | 30 | } | 825 | | | 826 | | // The peer survives every easy option/callback, including cleanup of a | 827 | | // bounded incomplete transfer. TransferSession owns the rest of that graph. | 828 | 227k | TransferSession transfer; | 829 | 227k | (void)transfer.PrepareInputFiles(scenario, mode); | 830 | 227k | if (!transfer.Initialize()) { | 831 | 696 | return 0; | 832 | 696 | } | 833 | 226k | CURL* easy = transfer.easy(); | 834 | 226k | const std::string url = ScenarioUrl(scenario, prefix); | 835 | 226k | bool resolver_mode_ready = true; | 836 | 226k | const auto configure_easy = [&] { | 837 | 226k | transfer.ApplyBaseline(scenario); | 838 | 226k | (void)curl_easy_setopt(easy, CURLOPT_URL, url.c_str()); | 839 | 226k | if (mock) { | 840 | 226k | mock->Install(easy); | 841 | 226k | } | 842 | 226k | transfer.ApplyInputFiles(); | 843 | | // Repeat the option-count bound for compatibility and direct callers. | 844 | 226k | (void)ApplyScenarioOptions(easy, scenario); | 845 | 226k | if (mode == ScenarioRunMode::kLdapCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_LDAP && | 846 | 226k | scenario.ldap_starttls()) { | 847 | 226k | (void)curl_easy_setopt(easy, CURLOPT_USE_SSL, static_cast<long>(CURLUSESSL_ALL)); | 848 | 226k | } | 849 | 226k | if (mode == ScenarioRunMode::kResolverCoverage) { | 850 | 226k | resolver_mode_ready = ApplyResolverMode(easy, scenario.resolver_mode()); | 851 | 226k | } | 852 | 226k | if (mode == ScenarioRunMode::kTlsCoverage) { | 853 | 226k | (void)ApplyTlsClientCredentialProfile(easy, scenario.tls_client_credential_profile()); | 854 | 226k | } | 855 | 226k | }; | 856 | 226k | configure_easy(); | 857 | 226k | if (!resolver_mode_ready) { | 858 | 3 | return 0; | 859 | 3 | } | 860 | 226k | transfer.ConfigureAltSvcRouting(scenario.host_path()); | 861 | | | 862 | 226k | if (file_lane) { | 863 | | // The file:// handler reads or writes the pinned /dev/null target (or | 864 | | // rejects a mutated authority) synchronously and never opens a socket. | 865 | | // Install the bounded request-data owner before perform so retained upload | 866 | | // options consume Scenario.upload through curl's read callback instead of | 867 | | // falling back to stdin. The baseline's short timeout still bounds the | 868 | | // transfer. | 869 | 3.51k | (void)transfer.InstallRequestData(scenario); | 870 | 3.51k | const CURLcode transfer_result = curl_easy_perform(easy); | 871 | 3.51k | (void)transfer_result; | 872 | 3.51k | ProbeTransferResults(easy); | 873 | 3.51k | return 0; | 874 | 3.51k | } | 875 | | | 876 | 222k | if (mode == ScenarioRunMode::kResolverCoverage) { | 877 | | // The normal CONNECT_TO baseline deliberately bypasses DNS. This lane | 878 | | // removes only that override; OPENSOCKET still returns the in-process | 879 | | // socketpair, so no resolved address can receive network traffic. | 880 | 7.81k | (void)curl_easy_setopt(easy, CURLOPT_CONNECT_TO, nullptr); | 881 | 7.81k | } | 882 | | | 883 | 222k | const curl::fuzzer::proto::ApiPlan* api_plan = | 884 | 222k | mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr; | 885 | 222k | if (api_plan != nullptr && api_plan->reset_easy() && transfer.ResetConfiguration()) { | 886 | 1.94k | configure_easy(); | 887 | 1.94k | } | 888 | | | 889 | 222k | ApiLifecycle* api_lifecycle = nullptr; | 890 | 222k | if (api_plan != nullptr) { | 891 | 5.59k | api_lifecycle = transfer.InstallApiLifecycle(*api_plan, url); | 892 | 5.59k | mock->SetMultiObserver([api_lifecycle](CURLM* multi) { api_lifecycle->SetActiveMulti(multi); }); | 893 | 5.59k | } | 894 | | | 895 | | // The DoH lane lets the loopback responder answer every lookup, including | 896 | | // the address query, so it must not install the resolver lane's fixed | 897 | | // CURLOPT_RESOLVE cache entries. | 898 | 222k | const bool apply_resolve_entries = mode == ScenarioRunMode::kResolverCoverage && !scenario.doh_https_rr() && | 899 | 6.95k | scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_DEFAULT; | 900 | 222k | ScenarioRequestData* request_data = transfer.InstallRequestData(scenario, apply_resolve_entries); | 901 | 222k | if (mode == ScenarioRunMode::kResolverCoverage && !request_data->resolve_entries_ready()) { | 902 | 12 | return 0; | 903 | 12 | } | 904 | 222k | mock->ConfigureRequestData(request_data); | 905 | 222k | const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode(); | 906 | 222k | CURLcode transfer_result = CURLE_FAILED_INIT; | 907 | 222k | if (mode == ScenarioRunMode::kResolverCoverage && | 908 | 7.80k | scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL) { | 909 | 190 | auto* resolver_mock = dynamic_cast<MockServer*>(mock.get()); | 910 | 190 | if (resolver_mock != nullptr) { | 911 | 190 | transfer_result = RunQueuedResolverCancellation(easy, resolver_mock, scenario); | 912 | 190 | } | 913 | 222k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) { | 914 | 718 | transfer_result = mock->DriveEasyScenario(easy, scenario); | 915 | 221k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) { | 916 | 112 | transfer_result = mock->DriveEasyScenario(easy, scenario, true); | 917 | 221k | } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY) { | 918 | 197 | transfer_result = mock->DriveConnectOnlyScenario(easy, scenario).connect_result; | 919 | 221k | } else { | 920 | 221k | transfer_result = mock->DriveScenario(easy, scenario); | 921 | 221k | } | 922 | 222k | if (api_plan != nullptr && api_plan->repeat_mime_post() && transfer_result == CURLE_OK) { | 923 | | // Re-enter the public transfer setup with the same easy handle, MIME root, | 924 | | // and callback cursor. A completed first body leaves a non-rewindable | 925 | | // callback at EOF; curl's second MIME reader setup must then report its | 926 | | // normal public rewind failure and clean up the unattached reader before | 927 | | // it can send another request. | 928 | 361 | transfer_result = mock->DriveScenario(easy, scenario); | 929 | 361 | } | 930 | 222k | if (api_lifecycle != nullptr) { | 931 | 5.59k | const bool retains_internal_multi = drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM || | 932 | 4.88k | drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS || | 933 | 4.76k | drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY; | 934 | | // An easy entrypoint can fail before attaching its private multi. The | 935 | | // drive mode alone therefore cannot establish curl_easy_upkeep's state | 936 | | // precondition. These are the two pre-attachment failure results. | 937 | 5.59k | const bool probe_upkeep = | 938 | 5.59k | retains_internal_multi && transfer_result != CURLE_OUT_OF_MEMORY && transfer_result != CURLE_FAILED_INIT; | 939 | 5.59k | api_lifecycle->ProbeTransferResults(probe_upkeep); | 940 | 5.59k | api_lifecycle->ProbeEasyDuplication(); | 941 | 5.59k | #if defined(PROTO_FUZZER_OBSERVE_STATE) | 942 | 5.59k | const volatile std::size_t api_observation[] = { | 943 | 5.59k | api_lifecycle->response_pause_returned(), api_lifecycle->response_bytes_received(), | 944 | 5.59k | api_lifecycle->reentrant_probes_run(), api_lifecycle->reentrant_recursive_rejections()}; | 945 | 5.59k | (void)api_observation; | 946 | 5.59k | #endif | 947 | 217k | } else if (mode != ScenarioRunMode::kFastProtocol) { | 948 | 191k | ProbeTransferResults(easy); | 949 | 191k | } | 950 | | | 951 | 222k | #if defined(PROTO_FUZZER_OBSERVE_STATE) | 952 | | // TransferSession still owns these completed callback states. Local volatile | 953 | | // stores retain their getter calls during profiling without adding work to | 954 | | // ordinary fuzz builds or changing the transfer's callback configuration. | 955 | 222k | const RequestBuildStats& stats = request_data->stats(); | 956 | 222k | const UploadScriptState& upload = request_data->upload_state(); | 957 | 222k | const volatile std::size_t request_observation[] = {stats.request_headers, | 958 | 222k | stats.resolve_entries, | 959 | 222k | stats.telnet_options, | 960 | 222k | stats.mime_parts, | 961 | 222k | stats.mime_callback_parts, | 962 | 222k | stats.mime_headers, | 963 | 222k | stats.generated_mime_bytes, | 964 | 222k | request_data->upload_callbacks_installed(), | 965 | 222k | upload.data_size(), | 966 | 222k | upload.read_step_count(), | 967 | 222k | upload.offset(), | 968 | 222k | upload.seek_calls(), | 969 | 222k | upload.scripted()}; | 970 | 222k | (void)request_observation; | 971 | 222k | ObserveProtocolPeerState(mock.get()); | 972 | 222k | #endif | 973 | 222k | return 0; | 974 | 222k | } |
|
975 | | |
976 | | } // namespace proto_fuzzer |