Coverage Report

Created: 2026-09-14 07:12

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