Coverage Report

Created: 2026-10-22 06:56

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/option_apply.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 the option-translation helpers declared in
9
///        option_apply.h.
10
11
#include "proto_fuzzer/option_apply.h"
12
13
#include <algorithm>
14
#include <cstddef>
15
#include <cstdint>
16
#include <cstdio>
17
#include <cstdlib>
18
#include <string>
19
20
#include "proto_fuzzer/allocation_fault_injection.h"
21
#include "proto_fuzzer/scenario_limits.h"
22
23
namespace proto_fuzzer {
24
25
namespace {
26
27
constexpr char kEventDrivenProtocolsAllowed[] = "http,https,ws,wss";
28
constexpr char kTelnetProtocolAllowed[] = "telnet";
29
constexpr char kFtpProtocolAllowed[] = "ftp";
30
constexpr char kTftpProtocolAllowed[] = "tftp";
31
constexpr char kConnectToOverride[] = "::127.0.1.127:";
32
constexpr char kDevNull[] = "/dev/null";
33
constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE";
34
constexpr char kAltSvcHttpEnvVar[] = "CURL_ALTSVC_HTTP";
35
constexpr char kHstsHttpEnvVar[] = "CURL_HSTS_HTTP";
36
constexpr long kConnectTimeoutMs = 200;
37
constexpr long kTimeoutMs = 200;
38
39
/// Baseline write callback for both CURLOPT_WRITEFUNCTION and
40
/// CURLOPT_HEADERFUNCTION. Consumes every byte so transfers don't stall on
41
/// backpressure and emits nothing. Protocol-specific mocks may install their
42
/// own WRITEFUNCTION afterwards if they need to poke protocol APIs while
43
/// inside a curl callback.
44
4.25M
size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; }
45
46
/// Consume libcurl's verbose records without emitting per-input diagnostics.
47
/// TELNET's debug build keeps its negotiation/suboption formatters behind the
48
/// verbose switch, so the protocol lane uses this sink to make that reachable
49
/// code fuzzable without turning millions of iterations into log traffic.
50
3.04M
int SilentDebugCallback(CURL* /*handle*/, curl_infotype /*type*/, char* /*data*/, size_t /*size*/, void* /*userdata*/) {
51
3.04M
  return 0;
52
3.04M
}
53
54
/// Let curl's debug build accept transport-security response headers over the
55
/// plaintext HTTP mock. The HTTPS lane now provides a verified peer, but making
56
/// HSTS and Alt-Svc parser coverage depend on a cryptographic handshake would
57
/// needlessly remove those parsers from the high-throughput HTTP lane.
58
400k
void EnableDebugHttpTransportMetadata() {
59
400k
  static const bool configured = [] {
60
    // CMake builds the fuzzing copy of curl with ENABLE_DEBUG specifically so
61
    // these curl-provided test hooks are available. Do not overwrite values a
62
    // reproducer deliberately supplied in its environment.
63
21
    (void)setenv(kAltSvcHttpEnvVar, "1", 0);
64
21
    (void)setenv(kHstsHttpEnvVar, "1", 0);
65
21
    return true;
66
21
  }();
67
400k
  (void)configured;
68
400k
}
69
70
27.6k
void EnableTraceIds() {
71
27.6k
  static const bool enabled = curl_global_trace("+LIB-IDS") == CURLE_OK;
72
27.6k
  (void)enabled;
73
27.6k
}
74
75
}  // namespace
76
77
/// Apply deterministic routing, timeout, output, and persistence defaults.
78
/// Call before applying scenario options. `scheme` identifies the dedicated
79
/// in-process mock that will service the transfer and selects the safe direct-
80
/// protocol allowlist.
81
/// @param easy The curl easy handle to configure.
82
/// @param scheme Protocol whose dedicated in-process mock will service it.
83
/// @param trace_ids Enable verbose LIB-IDS tracing for this scenario.
84
/// @return Caller-owned CURLOPT_CONNECT_TO list, which must outlive the easy
85
///         handle and be freed with curl_slist_free_all after curl_easy_cleanup.
86
400k
struct curl_slist* ApplyBaselineOptions(CURL* easy, curl::fuzzer::proto::Scheme scheme, bool trace_ids) {
87
400k
  EnableDebugHttpTransportMetadata();
88
89
400k
  curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback);
90
400k
  curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback);
91
92
400k
  const bool user_requested_verbose = std::getenv(kVerboseEnvVar) != nullptr;
93
400k
  if (trace_ids) {
94
27.6k
    EnableTraceIds();
95
    // Keep trace-ID coverage enabled during fuzzing without flooding the
96
    // process log. The callback still executes curl_trc.c's formatting path.
97
27.6k
    curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
98
27.6k
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
99
372k
  } else if (scheme == curl::fuzzer::proto::SCHEME_TELNET && !user_requested_verbose) {
100
    // printoption() and printsub() contain a substantial part of curl's TELNET
101
    // parser diagnostics but run only in verbose mode. Keep those paths in the
102
    // ordinary TELNET coverage lane while suppressing their high-volume text.
103
    // An explicit FUZZ_VERBOSE still skips the sink so reproductions remain
104
    // inspectable from the terminal.
105
5.46k
    curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
106
5.46k
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
107
5.46k
  }
108
109
  // Each non-HTTP protocol must use its dedicated peer invariants. Keep them
110
  // out of the redirect allowlist so an HTTP response cannot switch a stream
111
  // mock into synchronous TELNET, two-channel FTP, or datagram TFTP semantics.
112
  // CURLOPT_PROTOCOLS_STR arrived in 7.85.0.
113
400k
  const char* direct_protocols = kEventDrivenProtocolsAllowed;
114
400k
  switch (scheme) {
115
5.49k
    case curl::fuzzer::proto::SCHEME_TELNET:
116
5.49k
      direct_protocols = kTelnetProtocolAllowed;
117
5.49k
      break;
118
8.81k
    case curl::fuzzer::proto::SCHEME_FTP:
119
8.81k
      direct_protocols = kFtpProtocolAllowed;
120
8.81k
      break;
121
2.43k
    case curl::fuzzer::proto::SCHEME_TFTP:
122
2.43k
      direct_protocols = kTftpProtocolAllowed;
123
2.43k
      break;
124
5.62k
    case curl::fuzzer::proto::SCHEME_GOPHER:
125
5.62k
      direct_protocols = "gopher";
126
5.62k
      break;
127
4.28k
    case curl::fuzzer::proto::SCHEME_GOPHERS:
128
4.28k
      direct_protocols = "gophers";
129
4.28k
      break;
130
248k
    case curl::fuzzer::proto::SCHEME_HTTP:
131
340k
    case curl::fuzzer::proto::SCHEME_HTTPS:
132
360k
    case curl::fuzzer::proto::SCHEME_WS:
133
373k
    case curl::fuzzer::proto::SCHEME_WSS:
134
373k
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
135
373k
    default:
136
373k
      break;
137
400k
  }
138
400k
  curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, direct_protocols);
139
400k
  curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kEventDrivenProtocolsAllowed);
140
141
  // CONNECT_TO confines direct connections, but an ambient http_proxy or
142
  // ALL_PROXY can select a proxy before curl asks the harness for a socket.
143
  // An explicit empty proxy keeps every transfer inside the socketpair and
144
  // makes replay independent of the machine running the fuzzer.
145
400k
  curl_easy_setopt(easy, CURLOPT_PROXY, "");
146
147
  // Keep raw secure-scheme inputs independent of the host trust store,
148
  // matching the legacy harness. The dedicated TLS mock installs its own
149
  // in-memory trust anchor after this baseline; scenario options still run
150
  // last and can deliberately select verification failures.
151
400k
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L);
152
153
  // Force every name lookup to the fuzzer's in-process mock peer. The caller
154
  // owns the returned slist and must free it after curl_easy_cleanup.
155
400k
  struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride);
156
400k
  curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to);
157
158
  // Short bounds: fuzzing should never sit waiting on real I/O. Response
159
  // volume is already bounded by libFuzzer's input-size limit, so do not rate
160
  // limit receive traffic here: a global bytes-per-second throttle turns every
161
  // otherwise-complete large response into wall-clock sleep.
162
400k
  curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
163
400k
  curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs);
164
165
  // Keep every persistence/read path deterministic and prevent scenarios from
166
  // leaking state onto the filesystem. COOKIEFILE also makes the in-memory
167
  // engine's RELOAD command traverse its loader against a harmless empty
168
  // source. These path options deliberately remain absent from the generated
169
  // mutation manifest, so a proto cannot replace /dev/null.
170
400k
  curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull);
171
400k
  curl_easy_setopt(easy, CURLOPT_COOKIEFILE, kDevNull);
172
400k
  curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull);
173
400k
  curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull);
174
400k
  curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull);
175
  // Do not set CRLFILE merely to mirror the legacy harness. An empty CRL is
176
  // not a harmless sink: when a scenario restores certificate verification,
177
  // OpenSSL rejects /dev/null before it can exercise useful handshake and
178
  // verification paths. The option is absent from the mutation manifest, so
179
  // leaving it unset introduces neither filesystem writes nor external input.
180
181
  // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's
182
  // own verbose logging on. Useful when reproducing a crashing corpus entry.
183
400k
  if (user_requested_verbose) {
184
0
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
185
0
  }
186
400k
  return connect_to;
187
400k
}
188
189
/// Translate and apply one generated scalar/string SetOption to the easy handle.
190
/// Pointer-valued options borrow the SetOption's protobuf-owned string storage,
191
/// so its containing Scenario must remain alive and unmodified until the
192
/// transfer has stopped and the easy handle has been cleaned up. Borrowing
193
/// avoids a duplicate backing allocation on every iteration; the lifetime is
194
/// especially important for CURLOPT_POSTFIELDS, which curl does not copy.
195
/// @param easy The curl easy handle to configure.
196
/// @param option The SetOption proto describing which option and value to set.
197
/// @return curl's setopt result, or CURLE_UNKNOWN_OPTION for an unknown id.
198
988k
CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option) {
199
988k
  const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
200
988k
  if (desc == nullptr) {
201
29.9k
    return CURLE_UNKNOWN_OPTION;
202
29.9k
  }
203
958k
  const CURLoption curlopt = static_cast<CURLoption>(desc->curlopt);
204
205
958k
  switch (desc->kind) {
206
387k
    case OptionValueKind::kString: {
207
387k
      const std::string& value = option.string_value();
208
209
      // COPYPOSTFIELDS allocates after reading the separately configured
210
      // POSTFIELDSIZE. First install the same protobuf-owned bytes as a
211
      // no-allocation borrowed fallback. A genuine OOM then still leaves a
212
      // coherent pointer and length for later consumers such as AWS SigV4.
213
387k
      if (desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
214
5.23k
        CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDS, value.c_str());
215
5.23k
        if (result != CURLE_OK) {
216
0
          return result;
217
0
        }
218
5.23k
      }
219
220
      // POSTFIELDS borrows its pointer and COPYPOSTFIELDS copies exactly the
221
      // previously configured size. Apply the correlated byte length first so
222
      // either option accepts embedded NULs without strlen semantics and the
223
      // copying variant can never read beyond the protobuf-owned buffer.
224
387k
      if (desc->curlopt == CURLOPT_POSTFIELDS || desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
225
18.7k
        CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(value.size()));
226
18.7k
        if (result != CURLE_OK) {
227
0
          return result;
228
0
        }
229
18.7k
      }
230
231
387k
      if (desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
232
        // Copy ownership is a harness invariant rather than an allocation-
233
        // failure target. Preserve pending ordinals for later injectable work.
234
5.23k
        allocation_fault_injection::ScopedFailureSuppression suppress_failures;
235
5.23k
        return curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, value.c_str());
236
5.23k
      }
237
238
      // Curl otherwise treats this option as a filename during the TLS
239
      // handshake. Fixed-policy inputs normally arrive pre-constrained by the
240
      // postprocessor; this runtime check also protects compatibility inputs,
241
      // which intentionally bypass it. Curl copies this option in setopt, so
242
      // the temporary remains valid for the call's full ownership contract.
243
382k
      if (desc->curlopt == CURLOPT_PINNEDPUBLICKEY && !UsesInMemoryPublicKeyPin(value)) {
244
650
        std::string constrained = value;
245
650
        ConstrainPinnedPublicKeyValue(&constrained);
246
650
        return curl_easy_setopt(easy, curlopt, constrained.c_str());
247
650
      }
248
249
381k
      return curl_easy_setopt(easy, curlopt, value.c_str());
250
382k
    }
251
252
    // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option.
253
404k
    case OptionValueKind::kUint: {
254
404k
      const std::uint64_t raw = DecodeIntegralOptionValue(*desc, option);
255
      // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a
256
      // long; everything at/above takes a curl_off_t.
257
404k
      if (static_cast<int>(desc->curlopt) >= 30000) {
258
42.2k
        return curl_easy_setopt(easy, curlopt, static_cast<curl_off_t>(raw));
259
42.2k
      }
260
362k
      return curl_easy_setopt(easy, curlopt, static_cast<long>(raw));
261
404k
    }
262
263
    // Decode the bool_value and pass it as a long flag (0 or 1).
264
166k
    case OptionValueKind::kBool: {
265
166k
      const long flag = static_cast<long>(DecodeIntegralOptionValue(*desc, option));
266
166k
      return curl_easy_setopt(easy, curlopt, flag);
267
404k
    }
268
958k
  }
269
0
  return CURLE_UNKNOWN_OPTION;
270
958k
}
271
272
/// Return the runtime-visible option prefix length. Fixed targets normally
273
/// trim the protobuf in their postprocessor, but the compatibility target must
274
/// preserve its historical message unchanged; applying the same bound here
275
/// prevents that lane from doing mutation-sized setopt work while exposing the
276
/// same option prefix as postprocessed fixed lanes.
277
/// @param scenario Structured input whose option prefix will be consumed.
278
/// @return Number of options visible to the runtime.
279
809k
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario& scenario) {
280
809k
  return std::min<std::size_t>(static_cast<std::size_t>(scenario.options_size()), scenario_limits::kMaxOptions);
281
809k
}
282
283
/// Apply the bounded runtime-visible prefix of Scenario.options. Individual
284
/// CURLcode values remain intentionally ignored, matching the fuzzer runner's
285
/// historical behavior. Bounding here also covers standalone compatibility
286
/// seeds that reach RunScenario without normalization.
287
/// @param easy Easy handle receiving each supported option.
288
/// @param scenario Structured input that owns all borrowed option strings.
289
/// @return Number of option entries attempted.
290
400k
std::size_t ApplyScenarioOptions(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
291
400k
  const std::size_t option_count = RuntimeOptionCount(scenario);
292
1.38M
  for (std::size_t index = 0; index < option_count; ++index) {
293
988k
    (void)ApplySetOption(easy, scenario.options(static_cast<int>(index)));
294
988k
  }
295
400k
  return option_count;
296
400k
}
297
298
}  // namespace proto_fuzzer