Coverage Report

Created: 2026-10-28 07:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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