Coverage Report

Created: 2026-10-15 06:25

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