curl-fuzzer C++ reference
Loading...
Searching...
No Matches
scenario_runner.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#include <curl/curl.h>
13#include <curl/header.h>
14
15#include <cassert>
16#include <chrono>
17#include <cstddef>
18#include <memory>
19#include <string>
20#include <string_view>
21
48
49#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
61#endif
62#if defined(PROTO_FUZZER_HAS_HTTP3_MOCK_SERVER)
65#endif
66
67namespace proto_fuzzer {
68
69namespace {
70
71constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO;
72constexpr std::size_t kMaxResultHeaders = 16;
73constexpr char kResolverUnixSocketPath[] = "/curl-fuzzer-resolver.sock";
74constexpr char kResolverCancelDuplicateUrl[] = "http://resolver-queued-cancel.test:81/queued-cancel-duplicate";
75constexpr auto kResolverCancelGateWait = std::chrono::milliseconds(500);
76constexpr int kResolverIdlePollAttempts = 4;
77constexpr int kResolverIdlePollTimeoutMs = 250;
78
82bool ScenarioRequestsStartTls(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) {
83 switch (mode) {
85 return scenario.scheme() == curl::fuzzer::proto::SCHEME_LDAP && scenario.ldap_starttls();
87 return scenario.scheme() == curl::fuzzer::proto::SCHEME_FTP && scenario.ftp_auth_tls_filter_allocation_failure();
89 return scenario.scheme() == curl::fuzzer::proto::SCHEME_POP3 &&
90 scenario.pop3_starttls_filter_allocation_failure();
91 default:
92 return false;
93 }
94}
95
96struct ResolverIdleTraceState {
97 explicit ResolverIdleTraceState(CURL* handle) : easy(handle) {}
98
99 ~ResolverIdleTraceState() {
100 // The callback is static, but its data is stack-owned. Disable tracing and
101 // clear that pointer before the easy handle outlives this focused drive.
102 (void)curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L);
103 (void)curl_easy_setopt(easy, CURLOPT_DEBUGDATA, nullptr);
104 }
105
106 CURL* easy;
107 bool observe_idle = false;
108 bool saw_idle = false;
109};
110
111int ObserveResolverIdleTrace(CURL* /*easy*/, curl_infotype type, char* data, std::size_t size, void* client_data) {
112 auto* state = static_cast<ResolverIdleTraceState*>(client_data);
113 if (state == nullptr || !state->observe_idle || type != CURLINFO_TEXT || data == nullptr) {
114 return 0;
115 }
116 const std::string_view trace(data, size);
117 if (trace.find("[TPOOL-DNS]") != std::string_view::npos && trace.find("idle for") != std::string_view::npos) {
118 state->saw_idle = true;
119 }
120 return 0;
121}
122
123int CountResolverStart(void* /*resolver_state*/, void* /*reserved*/, void* client_data) {
124 ++*static_cast<int*>(client_data);
125 return 0;
126}
127
131bool ApplyResolverMode(CURL* easy, curl::fuzzer::proto::ResolverMode mode) {
132 switch (mode) {
133 case curl::fuzzer::proto::RESOLVER_MODE_THREADED_POSITIVE:
134 return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4) == CURLE_OK &&
135 curl_easy_setopt(easy, CURLOPT_DNS_SHUFFLE_ADDRESSES, 1L) == CURLE_OK;
136 case curl::fuzzer::proto::RESOLVER_MODE_THREADED_PARTIAL:
137 case curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL:
138 return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER) == CURLE_OK;
139 case curl::fuzzer::proto::RESOLVER_MODE_THREADED_NEGATIVE:
140 return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_WHATEVER) == CURLE_OK;
141 case curl::fuzzer::proto::RESOLVER_MODE_THREADED_LINK_LOCAL:
142 return curl_easy_setopt(easy, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V6) == CURLE_OK;
143 case curl::fuzzer::proto::RESOLVER_MODE_UNIX_SOCKET:
144 return curl_easy_setopt(easy, CURLOPT_UNIX_SOCKET_PATH, kResolverUnixSocketPath) == CURLE_OK;
145 case curl::fuzzer::proto::RESOLVER_MODE_DEFAULT:
146 case curl::fuzzer::proto::RESOLVER_MODE_GLOBAL_LIFECYCLE:
147 default:
148 return true;
149 }
150}
151
167CURLcode RunQueuedResolverCancellation(CURL* easy, MockServer* mock, const curl::fuzzer::proto::Scenario& scenario) {
168 int duplicate_resolver_starts = 0;
169 ResolverIdleTraceState idle_trace(easy);
170 // Keep callback data alive until both handles are gone. Declaring the
171 // duplicate before the multi also makes exceptional teardown destroy the
172 // multi (and detach any handle whose public removal failed) first.
173 CurlEasyPtr duplicate(curl_easy_duphandle(easy));
174 CurlMultiPtr multi(curl_multi_init());
175 // This drive is bounded by the gate wait and explicit removals. Clear the
176 // ordinary 200 ms transfer/connect settings (zero selects no operation
177 // deadline and curl's default connect deadline) so host scheduling cannot
178 // cancel either handle before the queue invariant is established.
179 if (multi == nullptr || duplicate == nullptr ||
180 curl_multi_setopt(multi.get(), CURLMOPT_RESOLVE_THREADS_MAX, 1L) != CURLM_OK ||
181 curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 0L) != CURLE_OK ||
182 curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 0L) != CURLE_OK ||
183 curl_easy_setopt(duplicate.get(), CURLOPT_TIMEOUT_MS, 0L) != CURLE_OK ||
184 curl_easy_setopt(duplicate.get(), CURLOPT_CONNECTTIMEOUT_MS, 0L) != CURLE_OK ||
185 curl_easy_setopt(duplicate.get(), CURLOPT_URL, kResolverCancelDuplicateUrl) != CURLE_OK ||
186 curl_easy_setopt(duplicate.get(), CURLOPT_FRESH_CONNECT, 1L) != CURLE_OK ||
187 curl_easy_setopt(duplicate.get(), CURLOPT_RESOLVER_START_FUNCTION, &CountResolverStart) != CURLE_OK ||
188 curl_easy_setopt(duplicate.get(), CURLOPT_RESOLVER_START_DATA, &duplicate_resolver_starts) != CURLE_OK ||
189 curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &ObserveResolverIdleTrace) != CURLE_OK ||
190 curl_easy_setopt(easy, CURLOPT_DEBUGDATA, &idle_trace) != CURLE_OK ||
191 curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L) != CURLE_OK) {
192 return CURLE_FAILED_INIT;
193 }
194
195 mock->SetScripts(scenario);
196 CURLMcode add_easy_result = curl_multi_add_handle(multi.get(), easy);
197 CURLMcode add_duplicate_result = CURLM_BAD_HANDLE;
198 CURLMcode perform_result = CURLM_BAD_HANDLE;
199 CURLMcode remove_duplicate_result = CURLM_BAD_HANDLE;
200 bool lookup_started = false;
201 bool duplicate_resolver_queued = false;
202 bool resolver_worker_idle = false;
203 int still_running = 0;
204 if (add_easy_result == CURLM_OK) {
206 perform_result = curl_multi_perform(multi.get(), &still_running);
207 if (perform_result == CURLM_OK && still_running > 0) {
208 lookup_started = WaitForResolverCancelLookup(kResolverCancelGateWait);
209 }
210 // The signal is sent from inside Q1's getaddrinfo. Q1 is worker-owned,
211 // and the only worker cannot return to the queue to take original Q2.
212 if (lookup_started) {
213 add_duplicate_result = curl_multi_add_handle(multi.get(), duplicate.get());
214 }
215 if (add_duplicate_result == CURLM_OK) {
216 perform_result = curl_multi_perform(multi.get(), &still_running);
217 }
218 // Resolver-start precedes the two sends, but the public perform call does
219 // not return until Curl_async_getaddrinfo has submitted them. Since Q1 is
220 // still blocked, a live duplicate here has queue-owned work to cancel.
221 duplicate_resolver_queued = perform_result == CURLM_OK && still_running == 2 && duplicate_resolver_starts > 0;
222 // Always detach a successfully added duplicate before its easy handle can
223 // be destroyed. On the proved path this is the intentional cancellation;
224 // on a failed proof it is bounded cleanup while Q1 still owns the worker.
225 if (add_duplicate_result == CURLM_OK) {
226 remove_duplicate_result = curl_multi_remove_handle(multi.get(), duplicate.get());
227 }
228 }
229
230 // Releasing the gate lets the worker finish the original handle's two
231 // queries. Wait on curl's public internal wakeup and perform until the
232 // THREADS trace reports that the pool is idle. Curl emits that observation
233 // while holding the pool lock; the main thread can acquire it only after the
234 // worker has entered Curl_cond_timedwait and released the lock. The trace is
235 // therefore the proof, while the poll timeout merely bounds a failed run.
236 if (lookup_started && duplicate_resolver_queued && remove_duplicate_result == CURLM_OK) {
237 idle_trace.observe_idle = true;
238 for (int attempt = 0; attempt < kResolverIdlePollAttempts && !idle_trace.saw_idle; ++attempt) {
239 int numfds = 0;
240 const CURLMcode poll_result = curl_multi_poll(multi.get(), nullptr, 0, kResolverIdlePollTimeoutMs, &numfds);
241 if (poll_result != CURLM_OK) {
242 perform_result = poll_result;
243 break;
244 }
245 perform_result = curl_multi_perform(multi.get(), &still_running);
246 if (perform_result != CURLM_OK) {
247 break;
248 }
249 }
250 resolver_worker_idle = idle_trace.saw_idle;
251 }
252
253 const CURLMcode remove_easy_result =
254 add_easy_result == CURLM_OK ? curl_multi_remove_handle(multi.get(), easy) : CURLM_BAD_HANDLE;
255 return lookup_started && duplicate_resolver_queued && resolver_worker_idle && perform_result == CURLM_OK &&
256 remove_duplicate_result == CURLM_OK && remove_easy_result == CURLM_OK
257 ? CURLE_OK
258 : CURLE_FAILED_INIT;
259}
260
269void ProbeTransferResults(CURL* easy) {
270 char* string_result = nullptr;
271 long long_result = 0;
272 double double_result = 0;
273 curl_off_t offset_result = 0;
274 curl_socket_t socket_result = CURL_SOCKET_BAD;
275 struct curl_certinfo* certinfo_result = nullptr;
276
277 (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result);
278 (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result);
279 (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result);
280 (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result);
281 (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result);
282 (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result);
283
284 struct curl_header* header = nullptr;
285 (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
286 header = nullptr;
287 for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
288 header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header);
289 if (header == nullptr) {
290 break;
291 }
292 }
293}
294
296const char* SchemePrefix(curl::fuzzer::proto::Scheme scheme) {
297 switch (scheme) {
298 case curl::fuzzer::proto::SCHEME_HTTP:
299 return "http";
300 case curl::fuzzer::proto::SCHEME_HTTPS:
301 return "https";
302 case curl::fuzzer::proto::SCHEME_WS:
303 return "ws";
304 case curl::fuzzer::proto::SCHEME_WSS:
305 return "wss";
306 case curl::fuzzer::proto::SCHEME_TELNET:
307 return "telnet";
308 case curl::fuzzer::proto::SCHEME_FTP:
309 return "ftp";
310 case curl::fuzzer::proto::SCHEME_FTPS:
311 return "ftps";
312 case curl::fuzzer::proto::SCHEME_TFTP:
313 return "tftp";
314 case curl::fuzzer::proto::SCHEME_SMTP:
315 return "smtp";
316 case curl::fuzzer::proto::SCHEME_IMAP:
317 return "imap";
318 case curl::fuzzer::proto::SCHEME_IMAPS:
319 return "imaps";
320 case curl::fuzzer::proto::SCHEME_POP3:
321 return "pop3";
322 case curl::fuzzer::proto::SCHEME_POP3S:
323 return "pop3s";
324 case curl::fuzzer::proto::SCHEME_MQTT:
325 return "mqtt";
326 case curl::fuzzer::proto::SCHEME_MQTTS:
327 return "mqtts";
328 case curl::fuzzer::proto::SCHEME_LDAP:
329 return "ldap";
330 case curl::fuzzer::proto::SCHEME_LDAPS:
331 return "ldaps";
332 case curl::fuzzer::proto::SCHEME_RTSP:
333 return "rtsp";
334 case curl::fuzzer::proto::SCHEME_IPFS:
335 return "ipfs";
336 case curl::fuzzer::proto::SCHEME_IPNS:
337 return "ipns";
338 case curl::fuzzer::proto::SCHEME_SMTPS:
339 return "smtps";
340 case curl::fuzzer::proto::SCHEME_DICT:
341 return "dict";
342 case curl::fuzzer::proto::SCHEME_GOPHER:
343 return "gopher";
344 case curl::fuzzer::proto::SCHEME_GOPHERS:
345 return "gophers";
346 case curl::fuzzer::proto::SCHEME_FILE:
347 return "file";
348 case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
349 default:
350 return nullptr;
351 }
352}
353
361std::string ScenarioUrl(const curl::fuzzer::proto::Scenario& scenario, const char* prefix) {
362 if (scenario.scheme() == curl::fuzzer::proto::SCHEME_IPFS || scenario.scheme() == curl::fuzzer::proto::SCHEME_IPNS) {
363 return BuildIpfsGatewayUrl(scenario.scheme(), scenario.host_path());
364 }
365 return std::string(prefix) + "://" + scenario.host_path();
366}
367
373std::unique_ptr<MockServerBase> MakeMockServerForScenario(const curl::fuzzer::proto::Scenario& scenario,
374 ScenarioRunMode mode) {
376#if defined(PROTO_FUZZER_HAS_HTTP3_MOCK_SERVER)
377 if (scenario.http3_plan().use_h1_connect_udp_proxy()) {
378 return std::make_unique<ConnectUdpProxyMockServer>();
379 }
380 return std::make_unique<Http3MockServer>(scenario.tls_certificate_chain());
381#else
382 return nullptr;
383#endif
384 }
385
387#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
388 return std::make_unique<H2ProxyMockServer>(
389 scenario.h2_forward_proxy() ? H2ProxyMode::kForward : H2ProxyMode::kTunnel,
390 scenario.connection().proxy_protocol());
391#else
392 // MemorySanitizer builds deliberately omit OpenSSL. Keep the target
393 // binary available to OSS-Fuzz, but do not pretend a plaintext mock can
394 // negotiate the ALPN gate required to enter cf-h2-proxy.
395 return nullptr;
396#endif
397 }
398
400 return std::make_unique<H2CleartextMockServer>(scenario.http2_connect_flush_probe());
401 }
402
404#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
405 return std::make_unique<H2OriginMockServer>(scenario.tls_certificate_chain(), scenario.tls_group_profile());
406#else
407 // This target remains buildable under MemorySanitizer, whose curl build
408 // omits the OpenSSL server dependency required for TLS/ALPN h2.
409 return nullptr;
410#endif
411 }
412
414 return std::make_unique<ProxyTransportMockServer>(scenario.socks_proxy_mode(), scenario.haproxy_protocol());
415 }
416
418 if (scenario.proxy_socket_option_mode() == curl::fuzzer::proto::PROXY_SOCKET_OPTION_LOOPBACK_INTERFACE_KEEPALIVE) {
419 return std::make_unique<InterfaceOptionMockServer>();
420 }
421 if (scenario.proxy_socket_option_mode() != curl::fuzzer::proto::PROXY_SOCKET_OPTION_DEFAULT) {
422 return std::make_unique<ProxyOptionMockServer>(scenario.proxy_socket_option_mode());
423 }
424 if (scenario.http_transport_mode() != curl::fuzzer::proto::HTTP_TRANSPORT_DIRECT) {
425 return std::make_unique<HttpTransportMockServer>(scenario.http_transport_mode(), scenario.haproxy_protocol());
426 }
427 }
428
429 if (mode == ScenarioRunMode::kResolverCoverage && scenario.has_doh()) {
430 // The DoH responder answers both the address and HTTPS resource-record
431 // queries; its TLS/ALPN h2 carrier then completes the outer HTTPS
432 // transfer selected from the record.
433#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
434 return std::make_unique<DohHttpsRrMockServer>(scenario);
435#else
436 // MemorySanitizer builds deliberately omit OpenSSL, so no verified TLS
437 // peer exists for the HTTPS-RR lane. Keep the target available while
438 // refusing to fall through to an unrelated transport.
439 return nullptr;
440#endif
441 }
442
443 if (mode == ScenarioRunMode::kResolverCoverage && scenario.happy_eyeballs()) {
444 // The loopback race peer returns real unconnected sockets, so curl's
445 // happy-eyeballs filter can keep several connect attempts in progress
446 // instead of accepting the first preconnected socketpair.
447 return std::make_unique<HappyEyeballsMockServer>();
448 }
449
450 switch (scenario.scheme()) {
451 case curl::fuzzer::proto::SCHEME_HTTP:
452 if (mode == ScenarioRunMode::kApiLifecycle) {
453 return std::make_unique<MockServer>(MultiDrivePolicy::kFromApiPlan);
454 }
455 return std::make_unique<MockServer>();
456 case curl::fuzzer::proto::SCHEME_HTTPS:
457#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
458 if (mode == ScenarioRunMode::kTlsCoverage) {
459 return std::make_unique<TlsMockServer>(scenario.tls_certificate_chain(), scenario.tls_handshake_profile());
460 }
461#else
462 (void)mode;
463#endif
464 return std::make_unique<MockServer>();
465 case curl::fuzzer::proto::SCHEME_GOPHER:
466 return mode == ScenarioRunMode::kGopherCoverage ? std::make_unique<GopherMockServer>(scenario.gopher_probe_mode())
467 : nullptr;
468 case curl::fuzzer::proto::SCHEME_GOPHERS:
469#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
471 return std::make_unique<TlsMockServer>(scenario.tls_certificate_chain());
472 }
473 return nullptr;
474#else
475 return nullptr;
476#endif
477 case curl::fuzzer::proto::SCHEME_WS:
478 return std::make_unique<WebSocketMockServer>();
479 case curl::fuzzer::proto::SCHEME_WSS:
481#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
482 return std::make_unique<TlsWebSocketMockServer>(scenario.tls_certificate_chain());
483#else
484 // MemorySanitizer builds deliberately omit OpenSSL. Keep the target
485 // available without pretending plaintext can carry a WSS exchange.
486 return nullptr;
487#endif
488 }
489 // Preserve the compatibility target's historical interpretation of WSS
490 // inputs, which exposed TLS bytes directly to the plaintext WS peer.
491 return std::make_unique<WebSocketMockServer>();
492 case curl::fuzzer::proto::SCHEME_TELNET:
493 return std::make_unique<TelnetMockServer>();
494 case curl::fuzzer::proto::SCHEME_FTP:
495 // New numeric enum values may already occur in the historical mixed
496 // corpus as unknown fields. Only the fixed FTP profile may reinterpret
497 // one as a live two-channel protocol exchange.
498 if (mode == ScenarioRunMode::kFtpCoverage) {
499 return std::make_unique<FtpMockServer>();
500 }
501 return nullptr;
502 case curl::fuzzer::proto::SCHEME_FTPS:
503 // Implicit TLS shares the FTP lane, parser, and bounded data-channel
504 // budget with the plaintext scheme; only the transport factory and the
505 // passive-only restriction differ. Keep it gated on the same run mode so
506 // no other target can turn an appended enum value into a TLS exchange.
507#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
508 if (mode == ScenarioRunMode::kFtpCoverage) {
509 return std::make_unique<FtpsMockServer>();
510 }
511#endif
512 return nullptr;
513 case curl::fuzzer::proto::SCHEME_TFTP:
514 // TFTP changes the callback transport from a preconnected stream to a
515 // real UDP endpoint, so compatibility inputs must not opt into it merely
516 // because this build learned a new enum value.
517 if (mode == ScenarioRunMode::kTftpCoverage) {
518 return std::make_unique<TftpMockServer>();
519 }
520 return nullptr;
521 case curl::fuzzer::proto::SCHEME_SMTP:
522 // SMTP reuses the stream socketpair but needs command-aligned replies
523 // and a DATA body drain. Only the dedicated lane may reinterpret an
524 // append-only scheme value that historical corpus inputs could carry.
525 if (mode == ScenarioRunMode::kSmtpCoverage) {
526 return std::make_unique<SmtpMockServer>(scenario.smtp_probe_mode());
527 }
528 return nullptr;
529 case curl::fuzzer::proto::SCHEME_IMAP:
530 // IMAP replies are tag-aligned. Only the dedicated lane may reinterpret
531 // an append-only scheme value that historical inputs could carry.
532 if (mode == ScenarioRunMode::kImapCoverage) {
533 return std::make_unique<ImapMockServer>();
534 }
535 return nullptr;
536 case curl::fuzzer::proto::SCHEME_POP3:
537 if (mode == ScenarioRunMode::kPop3Coverage) {
538 return std::make_unique<Pop3MockServer>();
539 }
540 return nullptr;
541 case curl::fuzzer::proto::SCHEME_MQTT:
542 // MQTT reuses the stream socketpair but speaks a binary packet grammar
543 // with server-initiated packets. Only the dedicated lane may
544 // reinterpret an append-only scheme value that historical corpus
545 // inputs could carry.
546 if (mode == ScenarioRunMode::kMqttCoverage) {
547 return std::make_unique<MqttMockServer>(scenario.mqtt_probe_mode());
548 }
549 return nullptr;
550 case curl::fuzzer::proto::SCHEME_DICT:
551 // DICT sends its command sequence in one write and reads an EOF-framed
552 // text body. Only the dedicated lane may reinterpret an append-only
553 // scheme value that historical inputs could carry.
554 if (mode == ScenarioRunMode::kDictCoverage) {
555 return std::make_unique<DictMockServer>();
556 }
557 return nullptr;
558 case curl::fuzzer::proto::SCHEME_SMTPS:
559 // Implicit TLS shares the SMTP lane, parser, and command budget with the
560 // plaintext scheme; only the transport factory differs. Keep it gated on
561 // the same fixed run mode so no other target can turn an appended enum
562 // value into a TLS exchange.
563#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
564 if (mode == ScenarioRunMode::kSmtpCoverage) {
565 return std::make_unique<SmtpsMockServer>();
566 }
567#endif
568 return nullptr;
569 case curl::fuzzer::proto::SCHEME_IMAPS:
570 // Implicit TLS reuses the IMAP lane and tag-aware parser; only the
571 // transport factory differs. Gate it on the same fixed run mode so no
572 // other target can turn the appended enum value into a TLS exchange.
573#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
574 if (mode == ScenarioRunMode::kImapCoverage) {
575 return std::make_unique<ImapsMockServer>();
576 }
577#endif
578 return nullptr;
579 case curl::fuzzer::proto::SCHEME_POP3S:
580 // Implicit TLS reuses the POP3 lane and status-line parser; only the
581 // transport factory differs. Gate it on the same fixed run mode so no
582 // other target can turn the appended enum value into a TLS exchange.
583#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
584 if (mode == ScenarioRunMode::kPop3Coverage) {
585 return std::make_unique<Pop3sMockServer>();
586 }
587#endif
588 return nullptr;
589 case curl::fuzzer::proto::SCHEME_MQTTS:
590 // Implicit TLS shares the MQTT lane, packet parser, and server-packet
591 // budget with the plaintext scheme; only the transport factory differs.
592 // Keep it gated on the same fixed run mode so no other target can turn
593 // an appended enum value into a TLS exchange.
594#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
595 if (mode == ScenarioRunMode::kMqttCoverage) {
596 return std::make_unique<MqttsMockServer>();
597 }
598#endif
599 return nullptr;
600 case curl::fuzzer::proto::SCHEME_FILE:
601 // FILE reads a session-owned anonymous descriptor directly, so there is
602 // no socket or scriptable peer to install.
603 return nullptr;
604 case curl::fuzzer::proto::SCHEME_LDAP:
605 // LDAP is BER/ASN.1 rather than line-oriented and OpenLDAP performs its
606 // own reads on a duplicate of curl's socket. Only the dedicated lane
607 // may turn an appended scheme value into a live bind/search exchange.
608 if (mode == ScenarioRunMode::kLdapCoverage) {
609 return std::make_unique<LdapMockServer>();
610 }
611 return nullptr;
612 case curl::fuzzer::proto::SCHEME_LDAPS:
613 // Implicit TLS shares the LDAP lane, BER parser, and scripted messages
614 // with the plaintext scheme; only the transport factory differs. Keep it
615 // gated on the same fixed run mode so no other target can turn an
616 // appended enum value into a TLS exchange.
617#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
618 if (mode == ScenarioRunMode::kLdapCoverage) {
619 return std::make_unique<LdapsMockServer>();
620 }
621#endif
622 return nullptr;
623 case curl::fuzzer::proto::SCHEME_RTSP:
624 // RTSP is request-aligned rather than greeting-aligned, so the
625 // dedicated lane is required to select the peer that waits for the
626 // request terminator before releasing each scripted response.
627 if (mode == ScenarioRunMode::kRtspCoverage) {
628 return std::make_unique<RtspMockServer>();
629 }
630 return nullptr;
631 case curl::fuzzer::proto::SCHEME_IPFS:
632 case curl::fuzzer::proto::SCHEME_IPNS:
633 // The rewrite above turns these into ordinary HTTP GETs. Only the
634 // dedicated fast lane may reinterpret an append-only scheme value, and
635 // its compatibility peer is the plaintext MockServer.
636 if (mode == ScenarioRunMode::kFastProtocol) {
637 return std::make_unique<MockServer>();
638 }
639 return nullptr;
640 case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
641 default:
642 return nullptr;
643 }
644}
645
646} // namespace
647
654int RunScenario(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) {
655 // Every input starts from a known off trace baseline (and returns to it when
656 // the scope exits), so one input's trace selection cannot leak into another.
660 return 0;
661 }
662
663 const char* prefix = SchemePrefix(scenario.scheme());
664 if (prefix == nullptr) {
665 return 0;
666 }
667
668 // FILE is the one peerless lane. Gate it on the dedicated run mode so the
669 // compatibility target cannot reinterpret the append-only enum value as a
670 // local filesystem read from an accumulated corpus entry.
671 const bool file_lane =
672 mode == ScenarioRunMode::kFileCoverage && scenario.scheme() == curl::fuzzer::proto::SCHEME_FILE;
673 if (scenario.scheme() == curl::fuzzer::proto::SCHEME_FILE && !file_lane) {
674 return 0;
675 }
676 if (!file_lane && scenario.host_path().empty()) {
677 return 0;
678 }
679
680 std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode);
681 if (!mock && !file_lane) {
682 return 0;
683 }
684
685 // The peer survives every easy option/callback, including cleanup of a
686 // bounded incomplete transfer. TransferSession owns the rest of that graph.
687 TransferSession transfer;
688 if (!transfer.PrepareInputFiles(scenario, mode)) {
689 return 0;
690 }
691 if (!transfer.Initialize()) {
692 return 0;
693 }
694 CURL* easy = transfer.easy();
695 std::string url;
696 if (file_lane) {
697 const char* source_path = transfer.FileSourcePath();
698 if (source_path == nullptr) {
699 return 0;
700 }
701 url = std::string("file://") + source_path;
702 } else {
703 url = ScenarioUrl(scenario, prefix);
704 }
705 const auto configure_easy = [&] {
706 transfer.ApplyBaseline(scenario);
707 (void)curl_easy_setopt(easy, CURLOPT_URL, url.c_str());
708 if (mock) {
709 mock->Install(easy);
710 }
711 transfer.ApplyInputFiles();
712 // Repeat the option-count bound for compatibility and direct callers.
713 (void)ApplyScenarioOptions(easy, scenario);
714 // A schema-native pin file is authoritative over the generic digest option.
715 // Its session-owned path remains valid until after easy cleanup.
716 transfer.ApplyPinnedPublicKeyFile();
717 if (auto* rtsp = dynamic_cast<RtspMockServer*>(mock.get())) {
718 rtsp->ConfigureClient(easy, scenario.rtsp_probe_mode());
719 }
720 if (auto* smtp = dynamic_cast<SmtpMockServer*>(mock.get())) {
721 smtp->ConfigureClient(easy);
722 }
724 scenario.gopher_probe_mode() == curl::fuzzer::proto::GOPHER_PROBE_TLS_SESSION_KEY_LIMIT) {
725 (void)curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L);
726 (void)curl_easy_setopt(easy, CURLOPT_SSL_OPTIONS, static_cast<long>(CURLSSLOPT_ALLOW_BEAST));
727 }
728 if (ScenarioRequestsStartTls(scenario, mode)) {
729 (void)curl_easy_setopt(easy, CURLOPT_USE_SSL, static_cast<long>(CURLUSESSL_ALL));
730 }
731 if (mode == ScenarioRunMode::kResolverCoverage && !ApplyResolverMode(easy, scenario.resolver_mode())) {
732 return false;
733 }
734 if (mode == ScenarioRunMode::kTlsCoverage) {
735 (void)ApplyTlsClientCredentialProfile(easy, scenario.tls_client_credential_profile());
736 }
737 return true;
738 };
739 if (!configure_easy()) {
740 return 0;
741 }
742 transfer.ConfigureAltSvcRouting(scenario.host_path());
743
744 if (file_lane) {
745 // The file:// handler reads or writes only the session-owned anonymous
746 // descriptor and never opens a socket. Install the bounded request-data
747 // owner before perform so retained upload options consume Scenario.upload
748 // through curl's read callback instead of falling back to stdin. The
749 // baseline's short timeout still bounds the transfer.
750 ScenarioRequestData* request_data = transfer.InstallRequestData(scenario);
751 assert(request_data != nullptr);
752 (void)request_data;
753 const CURLcode transfer_result = curl_easy_perform(easy);
754 (void)transfer_result;
755 ProbeTransferResults(easy);
756 return 0;
757 }
758
760 // The normal CONNECT_TO baseline deliberately bypasses DNS. This lane
761 // removes only that override; OPENSOCKET still returns the in-process
762 // socketpair, so no resolved address can receive network traffic.
763 (void)curl_easy_setopt(easy, CURLOPT_CONNECT_TO, nullptr);
764 }
765
766 const curl::fuzzer::proto::ApiPlan* api_plan =
767 mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr;
768 if (api_plan != nullptr && api_plan->reset_easy() && transfer.ResetConfiguration()) {
769 configure_easy();
770 }
771
772 ApiLifecycle* api_lifecycle = nullptr;
773 if (api_plan != nullptr) {
774 api_lifecycle = transfer.InstallApiLifecycle(*api_plan, url);
775 assert(api_lifecycle != nullptr);
776 mock->SetMultiObserver([api_lifecycle](CURLM* multi) { api_lifecycle->SetActiveMulti(multi); });
777 }
778
779 // The DoH lane lets the loopback responder answer every lookup, including
780 // the address query, so it must not install the resolver lane's fixed
781 // CURLOPT_RESOLVE cache entries.
782 const bool apply_resolve_entries = mode == ScenarioRunMode::kResolverCoverage && !scenario.has_doh() &&
783 scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_DEFAULT;
784 ScenarioRequestData* request_data = transfer.InstallRequestData(scenario, apply_resolve_entries);
785 assert(request_data != nullptr);
786 if (mode == ScenarioRunMode::kResolverCoverage && !request_data->resolve_entries_ready()) {
787 return 0;
788 }
789 mock->ConfigureRequestData(request_data);
790 const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode();
791 CURLcode transfer_result = CURLE_FAILED_INIT;
793 scenario.resolver_mode() == curl::fuzzer::proto::RESOLVER_MODE_THREADED_QUEUED_CANCEL) {
794 auto* resolver_mock = dynamic_cast<MockServer*>(mock.get());
795 if (resolver_mock != nullptr) {
796 transfer_result = RunQueuedResolverCancellation(easy, resolver_mock, scenario);
797 }
798 } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) {
799 transfer_result = mock->DriveEasyScenario(easy, scenario);
800 } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS) {
801 transfer_result = mock->DriveEasyScenario(easy, scenario, true);
802 } else if (drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY) {
803 transfer_result = mock->DriveConnectOnlyScenario(easy, scenario).connect_result;
804 } else if (mode == ScenarioRunMode::kRtspCoverage &&
805 scenario.rtsp_probe_mode() == curl::fuzzer::proto::RTSP_PROBE_CROSS_ORIGIN_REUSE) {
806 auto* rtsp = dynamic_cast<RtspMockServer*>(mock.get());
807 if (rtsp != nullptr) {
808 transfer_result = rtsp->DriveCrossOriginReuseScenario(easy, scenario);
809 }
810 } else {
811 transfer_result = mock->DriveScenario(easy, scenario);
812 }
813 if (api_plan != nullptr && api_plan->repeat_mime_post() && transfer_result == CURLE_OK) {
814 // Re-enter the public transfer setup with the same easy handle, MIME root,
815 // and callback cursor. A completed first body leaves a non-rewindable
816 // callback at EOF; curl's second MIME reader setup must then report its
817 // normal public rewind failure and clean up the unattached reader before
818 // it can send another request.
819 transfer_result = mock->DriveScenario(easy, scenario);
820 }
821 if (api_lifecycle != nullptr) {
822 const bool retains_internal_multi = drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM ||
823 drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_EVENTS ||
824 drive_mode == curl::fuzzer::proto::API_DRIVE_CONNECT_ONLY;
825 // An easy entrypoint can fail before attaching its private multi. The
826 // drive mode alone therefore cannot establish curl_easy_upkeep's state
827 // precondition. These are the two pre-attachment failure results.
828 const bool probe_upkeep =
829 retains_internal_multi && transfer_result != CURLE_OUT_OF_MEMORY && transfer_result != CURLE_FAILED_INIT;
830 api_lifecycle->ProbeTransferResults(probe_upkeep);
831 api_lifecycle->ProbeEasyDuplication();
832#if defined(PROTO_FUZZER_OBSERVE_STATE)
833 const volatile std::size_t api_observation[] = {
834 api_lifecycle->response_pause_returned(), api_lifecycle->response_bytes_received(),
835 api_lifecycle->reentrant_probes_run(), api_lifecycle->reentrant_recursive_rejections()};
836 (void)api_observation;
837#endif
838 } else if (mode != ScenarioRunMode::kFastProtocol) {
839 ProbeTransferResults(easy);
840 }
841
842#if defined(PROTO_FUZZER_OBSERVE_STATE)
843 // TransferSession still owns these completed callback states. Local volatile
844 // stores retain their getter calls during profiling without adding work to
845 // ordinary fuzz builds or changing the transfer's callback configuration.
846 const RequestBuildStats& stats = request_data->stats();
847 const UploadScriptState& upload = request_data->upload_state();
848 const volatile std::size_t request_observation[] = {stats.request_headers,
849 stats.resolve_entries,
850 stats.telnet_options,
851 stats.mime_parts,
853 stats.mime_headers,
855 request_data->upload_callbacks_installed(),
856 upload.data_size(),
857 upload.read_step_count(),
858 upload.offset(),
859 upload.seek_calls(),
860 upload.scripted()};
861 (void)request_observation;
862 mock->ObserveState();
863#endif
864 return 0;
865}
866
867} // namespace proto_fuzzer
Safe ownership and typed probes for the dedicated public-API lane.
Exercises public easy/share/query APIs while preserving all caller- owned option and callback lifetim...
std::size_t reentrant_recursive_rejections() const
void ProbeEasyDuplication()
Duplicate, reset, and destroy a scratch easy handle while every pointer- valued option copied from th...
std::size_t reentrant_probes_run() const
void SetActiveMulti(CURLM *multi)
Hand the driving multi handle to the reentrancy probes so the multi-entry probes can target the live ...
void ProbeTransferResults(bool probe_upkeep)
Run correctly typed CURLINFO and response-header probes selected by the plan.
std::size_t response_bytes_received() const
Orchestrates a bounded sequence of mock HTTP exchanges: installs socket callbacks on an easy handle,...
Definition mock_server.h:72
Services curl's single-socket RTSP exchange.
Builds HTTP headers or TELNET options, MIME state, and upload callbacks whose storage or userdata lib...
const RequestBuildStats & stats() const
Return resource counts after caps and allocation failures were applied, without exposing or transferr...
const UploadScriptState & upload_state() const
Return the callback state retained for the complete drive.
bool resolve_entries_ready() const
Return whether resolver-only slist construction installed its mandatory final loopback mapping.
bool upload_callbacks_installed() const
Return whether this scenario needed raw upload callbacks.
Hold the first opt-in getaddrinfo call until the public multi owner has queued the cancellation handl...
Reset curl's global trace configuration to a known off baseline for the lifetime of one input.
Services curl's single-connection, line-based SMTP exchange.
Own per-transfer resources with one teardown contract: detach request callbacks and lists,...
bool Initialize()
Allocate the easy lazily so unused concurrent-transfer slots stay empty.
CURL * easy() const
Borrow the owned easy; callers must not clean it themselves.
const char * FileSourcePath() const
Return the pathname of the prepared FILE source/sink.
void ApplyInputFiles()
Apply prepared parser paths after baseline and peer installation.
ScenarioRequestData * InstallRequestData(const curl::fuzzer::proto::Scenario &scenario, bool apply_resolve_entries=false)
Install owned request attachments.
bool ResetConfiguration()
Reset pre-transfer configuration while preserving prepared parser files.
void ConfigureAltSvcRouting(const std::string &host_path)
Enable isolated Alt-Svc routing for the canonical deep-HTTP authority.
void ApplyPinnedPublicKeyFile()
Install the prepared public-key pin path after generic scalar options.
void ApplyBaseline(const curl::fuzzer::proto::Scenario &scenario)
Apply baseline options and retain their CONNECT_TO storage.
bool PrepareInputFiles(const curl::fuzzer::proto::Scenario &scenario, ScenarioRunMode mode)
Copy bounded parser inputs into lazily allocated anonymous files.
ApiLifecycle * InstallApiLifecycle(const curl::fuzzer::proto::ApiPlan &plan, std::string_view url)
Install API/share state that survives easy cleanup.
Cursor and policy behind the request read/seek callbacks.
std::size_t read_step_count() const
Deterministic HTTP/1.1 CONNECT-UDP proxy for HTTP/3 scenarios.
std::unique_ptr< CURLM, CurlMultiDeleter > CurlMultiPtr
Unique ownership of a CURL multi handle.
Definition curl_raii.h:58
std::unique_ptr< CURL, CurlEasyDeleter > CurlEasyPtr
Unique ownership of a CURL easy handle.
Definition curl_raii.h:56
Command-aware in-process peer for curl's DICT request/response.
DoH responder that feeds a synthesized HTTPS resource record to curl's HTTPS-connect ALPN filter.
Command-aware in-process peer for curl's FTP state machine.
Implicit-TLS in-process peer for curl's FTPS state machine.
Bounded transport probes for curl's synchronous Gopher selector.
Plaintext HTTP/2 prior-knowledge origin peer.
HTTPS origin peer with a fixed HTTP/2 application protocol.
Deterministic HTTPS/HTTP2 proxy for structured scenarios.
@ kForward
Submit an absolute-form origin request directly as an HTTP/2 stream.
@ kTunnel
Establish CONNECT, then carry a plaintext HTTP/1.1 origin exchange.
HappyEyeballsMockServer — a loopback peer that lets curl's happy-eyeballs connection filter race seve...
Bounded ngtcp2/OpenSSL QUIC peer for HTTP/3 scenarios.
Bounded HTTP proxy and Unix-domain transports for deep scenarios.
Command-aware in-process peer for curl's IMAP state machine.
Implicit-TLS in-process peer for curl's IMAPS state machine.
std::string BuildIpfsGatewayUrl(curl::fuzzer::proto::Scheme scheme, const std::string &host_path)
Build the IPFS/IPNS gateway URL for the given scheme and pinned host_path.
Definition ipfs_url.cc:99
IPFS/IPNS gateway URL construction shared by the scenario runner and its transcript-level tests.
BER/ASN.1-aware in-process peer for curl's OpenLDAP-backed LDAP client.
Implicit-TLS in-process peer for curl's OpenLDAP-backed LDAPS exchange.
MockConnection and MockServer — the in-process peer that feeds canned response bytes to libcurl over ...
MockServerBase — common plumbing shared by every protocol-specific mock server.
@ kFromApiPlan
Read the multi-loop choice and probes from Scenario::api_plan.
Packet-aware in-process peer for curl's binary MQTT state machine.
Implicit-TLS in-process peer for curl's MQTTS state machine.
MultiTransferRunStats RunMultiTransferScenario(const curl::fuzzer::proto::Scenario &scenario)
Drive two to four easy handles through one CURLM and one bounded in-process HTTP peer.
Bounded shared-multi orchestration for concurrent HTTP transfers.
CURLcode ApplyTlsClientCredentialProfile(CURL *easy, curl::fuzzer::proto::TlsClientCredentialProfile profile)
Apply one closed, harness-owned TLS client-credential configuration.
std::size_t ApplyScenarioOptions(CURL *easy, const curl::fuzzer::proto::Scenario &scenario)
Apply the bounded runtime-visible prefix of Scenario.options.
Translates Scenario SetOption messages into curl_easy_setopt calls, plus the fixed baseline setopts t...
Command-aware in-process peer for curl's POP3 state machine.
Implicit-TLS in-process peer for curl's POP3S state machine.
Closed proxy, no-proxy, interface, and TCP-option peers.
Owns pointer-valued HTTP headers, TELNET options, MIME state, and upload callback state derived from ...
Bounded rendezvous for the resolver queued-cancellation scenario.
bool WaitForResolverCancelLookup(std::chrono::milliseconds timeout)
Give curl's single resolver worker a bounded opportunity to take the first family before the main thr...
Request-aligned in-process peer for curl's RTSP state machine.
int RunScenario(const curl::fuzzer::proto::Scenario &scenario, ScenarioRunMode mode)
Execute one scenario under a complete target behaviour.
Orchestrates a single fuzz iteration from a Scenario proto through to a completed curl transfer.
Command-aware in-process peer for curl's SMTP state machine.
Implicit-TLS in-process peer for curl's SMTPS state machine.
Deterministic SOCKS/HAProxy transport for HTTP scenarios.
Counts the resources that survived allocation and runtime budgets.
std::size_t generated_mime_bytes
Materialized bytes supplied by compact generated MIME sources.
std::size_t telnet_options
Number of CURLOPT_TELNETOPTIONS entries retained.
std::size_t mime_parts
Number of top-level and nested curl_mimepart objects constructed.
std::size_t request_headers
Number of top-level CURLOPT_HTTPHEADER entries retained.
std::size_t mime_callback_parts
Number of top-level parts backed by Scenario.upload callbacks.
std::size_t mime_headers
Number of per-part header entries transferred to curl MIME ownership.
std::size_t resolve_entries
Number of fuzzed CURLOPT_RESOLVE entries retained before the fixed loopback mapping was appended.
ScenarioRunMode
Selects one of the complete, valid RunScenario behaviours.
@ kH2ProxyCoverage
Drive raw HTTP/2 proxy frames around one CONNECT tunnel.
@ kFtpCoverage
Drive FTP or FTPS through its concurrent control/passive-data peer.
@ kRtspCoverage
Drive RTSP through its request-aligned control peer, including response bodies and interleaved RTP da...
@ kPop3Coverage
Drive POP3 through its command-aware control peer, including RETR bodies.
@ kFileCoverage
Drive file:// without a peer, reading only the canonical local target.
@ kLdapCoverage
Drive LDAP through its BER-aware single-socket peer, including the bind reply and scripted search-res...
@ kTlsCoverage
Drive HTTPS through a real TLS peer and inspect live TLS result state.
@ kTftpCoverage
Drive TFTP through its datagram-preserving loopback peer.
@ kDictCoverage
Drive DICT lookups through its command-aware stream peer and EOF-framed response body.
@ kTlsHttp2Coverage
Drive an HTTPS origin through fixed ALPN h2 with push/upkeep probes.
@ kHttp2Coverage
Drive a plaintext HTTP/2 prior-knowledge origin with the H2 peer.
@ kFastProtocol
Drive the protocol without charging its hot loop for generic API probes.
@ kImapCoverage
Drive IMAP through its command-aware control peer, including literals.
@ kDeepHttpCoverage
Drive deep HTTP plus its bounded filename-backed parser inputs.
@ kSmtpCoverage
Drive SMTP or SMTPS through its command-aware control peer, including the DATA body exchange.
@ kMqttCoverage
Drive MQTT or MQTTS through its packet-aware single-socket peer.
@ kTlsWebSocketCoverage
Drive WebSocket framing through a verified in-process TLS peer.
@ kMultiTransfer
Honour MultiPlan and drive several HTTP transfers through one multi.
@ kProxyTransportCoverage
Drive request-triggered SOCKS4/SOCKS5 negotiation, optional HAProxy framing, and the tunneled HTTP ex...
@ kResolverCoverage
Let curl resolve the origin while the socket callback still owns I/O.
@ kGopherCoverage
Drive Gopher through the bounded stream peer.
@ kApiLifecycle
Honour ApiPlan and run the dedicated lifecycle and typed-result probes.
@ kHttp3Coverage
Drive ordered plaintext HTTP/3 actions through the QUIC peer.
Non-blocking in-process peer for curl's blocking TELNET driver.
Bounded loopback UDP peer for structured TFTP scenarios.
Reusable TLS transport for structured in-process protocol peers.
Secure WebSocket peer carried by the in-process TLS transport.
Owns an easy handle and the backing storage retained by its options.
WebSocketMockServer — an in-process WebSocket peer that drives a dynamic 101 handshake and then feeds...