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/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
/// How a SetOption oneof should be decoded before calling curl_easy_setopt.
25
enum class OptionValueKind {
26
  kString,  ///< string_value → const char* option.
27
  kUint,    ///< uint_value → long or curl_off_t option.
28
  kBool     ///< bool_value → 0/1 long option.
29
};
30
31
/// One row in the build-time-generated option manifest: binds a proto enum
32
/// value to the matching curl_easy_setopt option id and value kind.
33
struct OptionDescriptor {
34
  /// Proto enum identifier for this option.
35
  curl::fuzzer::proto::CurlOptionId id;
36
  /// How the oneof value should be decoded.
37
  OptionValueKind kind;
38
  /// Human-readable option name (e.g. "CURLOPT_URL") for diagnostics.
39
  const char* name;
40
  /// The native CURLoption to pass to curl_easy_setopt.
41
  CURLoption curlopt;
42
};
43
44
// Pulls in kOptionManifest[] and its generated switch-based lookup.
45
#include "curl_fuzzer_option_manifest.inc"
46
47
namespace {
48
49
constexpr char kEventDrivenProtocolsAllowed[] = "http,https,ws,wss";
50
constexpr char kTelnetProtocolAllowed[] = "telnet";
51
constexpr char kFtpProtocolAllowed[] = "ftp";
52
constexpr char kTftpProtocolAllowed[] = "tftp";
53
constexpr char kConnectToOverride[] = "::127.0.1.127:";
54
constexpr char kDevNull[] = "/dev/null";
55
constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE";
56
constexpr char kAltSvcHttpEnvVar[] = "CURL_ALTSVC_HTTP";
57
constexpr char kHstsHttpEnvVar[] = "CURL_HSTS_HTTP";
58
constexpr long kConnectTimeoutMs = 200;
59
constexpr long kTimeoutMs = 200;
60
61
/// Test the same prefix curl uses to choose between an in-memory digest and a
62
/// filename. Do not reject malformed base64 here: those values are useful TLS
63
/// parser inputs and remain filesystem-safe as long as this prefix is intact.
64
15.4k
bool UsesInMemoryPublicKeyPin(const std::string& value) { return value.rfind("sha256//", 0) == 0; }
65
66
/// Keep mutated pin values on curl's digest-comparison branch while retaining
67
/// every mutation byte. Existing expressions, including correlated corpus
68
/// seeds, must remain byte-for-byte stable.
69
6.70k
void ConstrainPinnedPublicKeyValue(std::string* value) {
70
6.70k
  if (value == nullptr || UsesInMemoryPublicKeyPin(*value)) {
71
4.08k
    return;
72
4.08k
  }
73
2.62k
  value->insert(0, "sha256//");
74
2.62k
}
75
76
/// Baseline write callback for both CURLOPT_WRITEFUNCTION and
77
/// CURLOPT_HEADERFUNCTION. Consumes every byte so transfers don't stall on
78
/// backpressure and emits nothing. Protocol-specific mocks may install their
79
/// own WRITEFUNCTION afterwards if they need to poke protocol APIs while
80
/// inside a curl callback.
81
2.35M
size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; }
82
83
/// Consume libcurl's verbose records without emitting per-input diagnostics.
84
/// TELNET's debug build keeps its negotiation/suboption formatters behind the
85
/// verbose switch, so the protocol lane uses this sink to make that reachable
86
/// code fuzzable without turning millions of iterations into log traffic.
87
2.72M
int SilentDebugCallback(CURL* /*handle*/, curl_infotype /*type*/, char* /*data*/, size_t /*size*/, void* /*userdata*/) {
88
2.72M
  return 0;
89
2.72M
}
90
91
/// Let curl's debug build accept transport-security response headers over the
92
/// plaintext HTTP mock. The HTTPS lane now provides a verified peer, but making
93
/// HSTS and Alt-Svc parser coverage depend on a cryptographic handshake would
94
/// needlessly remove those parsers from the high-throughput HTTP lane.
95
327k
void EnableDebugHttpTransportMetadata() {
96
327k
  static const bool configured = [] {
97
    // CMake builds the fuzzing copy of curl with ENABLE_DEBUG specifically so
98
    // these curl-provided test hooks are available. Do not overwrite values a
99
    // reproducer deliberately supplied in its environment.
100
20
    (void)setenv(kAltSvcHttpEnvVar, "1", 0);
101
20
    (void)setenv(kHstsHttpEnvVar, "1", 0);
102
20
    return true;
103
20
  }();
104
327k
  (void)configured;
105
327k
}
106
107
25.3k
void EnableTraceIds() {
108
25.3k
  static const bool enabled = curl_global_trace("+LIB-IDS") == CURLE_OK;
109
25.3k
  (void)enabled;
110
25.3k
}
111
112
/// Decode protobuf's two integral oneof members according to the semantic
113
/// kind in the generated option descriptor. The schema cannot couple an
114
/// option id to one particular oneof member, so both retained corpus entries
115
/// and ordinary mutations can represent a flag as uint_value or a numeric
116
/// mode as bool_value. Preserving magnitude for numeric options and reducing
117
/// flags to truthiness keeps either representation useful without embedding
118
/// option-specific history in the runtime. String or unset members map to the
119
/// same zero default protobuf's inactive scalar accessors historically gave.
120
579k
std::uint64_t DecodeIntegralValue(const OptionDescriptor& descriptor, const curl::fuzzer::proto::SetOption& option) {
121
579k
  if (descriptor.kind == OptionValueKind::kString) {
122
0
    return 0;
123
0
  }
124
579k
  switch (option.value_case()) {
125
167k
    case curl::fuzzer::proto::SetOption::kBoolValue:
126
167k
      return option.bool_value() ? 1U : 0U;
127
387k
    case curl::fuzzer::proto::SetOption::kUintValue:
128
387k
      if (descriptor.kind == OptionValueKind::kBool) {
129
4.13k
        return option.uint_value() != 0 ? 1U : 0U;
130
4.13k
      }
131
383k
      return option.uint_value();
132
2.37k
    case curl::fuzzer::proto::SetOption::kStringValue:
133
23.4k
    case curl::fuzzer::proto::SetOption::VALUE_NOT_SET:
134
23.4k
      return 0;
135
579k
  }
136
0
  return 0;
137
579k
}
138
139
}  // namespace
140
141
/// Decode a recognized integral option using its generated semantic kind.
142
/// Unknown and string-valued options have no integral interpretation and
143
/// therefore return zero.
144
4.56k
std::uint64_t DecodeIntegralOptionValue(const curl::fuzzer::proto::SetOption& option) {
145
4.56k
  const OptionDescriptor* descriptor = LookupOptionDescriptor(option.option_id());
146
4.56k
  if (descriptor == nullptr || descriptor->kind == OptionValueKind::kString) {
147
0
    return 0;
148
0
  }
149
4.56k
  return DecodeIntegralValue(*descriptor, option);
150
4.56k
}
151
152
/// Make every supported option's expected oneof member explicit. Boolean and
153
/// integer representations retain their scalar meaning when crossing between
154
/// those families; string or unset mismatches become the destination family's
155
/// zero value. This focuses later mutations on a value ApplySetOption consumes
156
/// without requiring option-specific compatibility rules.
157
261k
void CanonicalizeOptionValueCases(curl::fuzzer::proto::Scenario* scenario) {
158
261k
  if (scenario == nullptr) {
159
0
    return;
160
0
  }
161
727k
  for (auto& option : *scenario->mutable_options()) {
162
727k
    const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
163
727k
    if (desc == nullptr) {
164
27.9k
      continue;
165
27.9k
    }
166
167
699k
    switch (desc->kind) {
168
258k
      case OptionValueKind::kString:
169
258k
        if (option.value_case() != curl::fuzzer::proto::SetOption::kStringValue) {
170
2.71k
          option.set_string_value("");
171
2.71k
        }
172
258k
        if (desc->curlopt == CURLOPT_PINNEDPUBLICKEY) {
173
6.34k
          ConstrainPinnedPublicKeyValue(option.mutable_string_value());
174
6.34k
        }
175
258k
        break;
176
305k
      case OptionValueKind::kUint:
177
305k
        if (option.value_case() != curl::fuzzer::proto::SetOption::kUintValue) {
178
4.55k
          option.set_uint_value(DecodeIntegralValue(*desc, option));
179
4.55k
        }
180
305k
        break;
181
136k
      case OptionValueKind::kBool:
182
136k
        if (option.value_case() != curl::fuzzer::proto::SetOption::kBoolValue) {
183
6.20k
          option.set_bool_value(DecodeIntegralValue(*desc, option) != 0);
184
6.20k
        }
185
136k
        break;
186
699k
    }
187
699k
  }
188
261k
}
189
190
/// Apply the fixed baseline options the harness always wants: output sinks,
191
/// protocol restrictions, DNS overrides, timeouts. Call before applying any
192
/// scenario options.
193
/// @param easy The curl easy handle to configure.
194
/// @param scheme Protocol whose dedicated in-process mock will service it.
195
/// @return the curl_slist owned by the caller (for CURLOPT_CONNECT_TO), which
196
///         must be freed with curl_slist_free_all after curl_easy_cleanup.
197
327k
struct curl_slist* ApplyBaselineOptions(CURL* easy, curl::fuzzer::proto::Scheme scheme, bool trace_ids) {
198
327k
  EnableDebugHttpTransportMetadata();
199
200
327k
  curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback);
201
327k
  curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback);
202
203
327k
  const bool user_requested_verbose = std::getenv(kVerboseEnvVar) != nullptr;
204
327k
  if (trace_ids) {
205
25.3k
    EnableTraceIds();
206
    // Keep trace-ID coverage enabled during fuzzing without flooding the
207
    // process log. The callback still executes curl_trc.c's formatting path.
208
25.3k
    curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
209
25.3k
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
210
302k
  } else if (scheme == curl::fuzzer::proto::SCHEME_TELNET && !user_requested_verbose) {
211
    // printoption() and printsub() contain a substantial part of curl's TELNET
212
    // parser diagnostics but run only in verbose mode. Keep those paths in the
213
    // ordinary TELNET coverage lane while suppressing their high-volume text.
214
    // An explicit FUZZ_VERBOSE still skips the sink so reproductions remain
215
    // inspectable from the terminal.
216
4.80k
    curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
217
4.80k
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
218
4.80k
  }
219
220
  // Each non-HTTP protocol must use its dedicated peer invariants. Keep them
221
  // out of the redirect allowlist so an HTTP response cannot switch a stream
222
  // mock into synchronous TELNET, two-channel FTP, or datagram TFTP semantics.
223
  // CURLOPT_PROTOCOLS_STR arrived in 7.85.0.
224
327k
  const char* direct_protocols = kEventDrivenProtocolsAllowed;
225
327k
  switch (scheme) {
226
4.83k
    case curl::fuzzer::proto::SCHEME_TELNET:
227
4.83k
      direct_protocols = kTelnetProtocolAllowed;
228
4.83k
      break;
229
9.03k
    case curl::fuzzer::proto::SCHEME_FTP:
230
9.03k
      direct_protocols = kFtpProtocolAllowed;
231
9.03k
      break;
232
2.01k
    case curl::fuzzer::proto::SCHEME_TFTP:
233
2.01k
      direct_protocols = kTftpProtocolAllowed;
234
2.01k
      break;
235
5.41k
    case curl::fuzzer::proto::SCHEME_GOPHER:
236
5.41k
      direct_protocols = "gopher";
237
5.41k
      break;
238
4.22k
    case curl::fuzzer::proto::SCHEME_GOPHERS:
239
4.22k
      direct_protocols = "gophers";
240
4.22k
      break;
241
197k
    case curl::fuzzer::proto::SCHEME_HTTP:
242
272k
    case curl::fuzzer::proto::SCHEME_HTTPS:
243
291k
    case curl::fuzzer::proto::SCHEME_WS:
244
302k
    case curl::fuzzer::proto::SCHEME_WSS:
245
302k
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
246
302k
    default:
247
302k
      break;
248
327k
  }
249
327k
  curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, direct_protocols);
250
327k
  curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kEventDrivenProtocolsAllowed);
251
252
  // CONNECT_TO confines direct connections, but an ambient http_proxy or
253
  // ALL_PROXY can select a proxy before curl asks the harness for a socket.
254
  // An explicit empty proxy keeps every transfer inside the socketpair and
255
  // makes replay independent of the machine running the fuzzer.
256
327k
  curl_easy_setopt(easy, CURLOPT_PROXY, "");
257
258
  // Keep raw secure-scheme inputs independent of the host trust store,
259
  // matching the legacy harness. The dedicated TLS mock installs its own
260
  // in-memory trust anchor after this baseline; scenario options still run
261
  // last and can deliberately select verification failures.
262
327k
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L);
263
264
  // Force every name lookup to the fuzzer's in-process mock peer. The caller
265
  // owns the returned slist and must free it after curl_easy_cleanup.
266
327k
  struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride);
267
327k
  curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to);
268
269
  // Short bounds: fuzzing should never sit waiting on real I/O. Response
270
  // volume is already bounded by libFuzzer's input-size limit, so do not rate
271
  // limit receive traffic here: a global bytes-per-second throttle turns every
272
  // otherwise-complete large response into wall-clock sleep.
273
327k
  curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
274
327k
  curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs);
275
276
  // Keep every persistence/read path deterministic and prevent scenarios from
277
  // leaking state onto the filesystem. COOKIEFILE also makes the in-memory
278
  // engine's RELOAD command traverse its loader against a harmless empty
279
  // source. These path options deliberately remain absent from the generated
280
  // mutation manifest, so a proto cannot replace /dev/null.
281
327k
  curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull);
282
327k
  curl_easy_setopt(easy, CURLOPT_COOKIEFILE, kDevNull);
283
327k
  curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull);
284
327k
  curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull);
285
327k
  curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull);
286
  // Do not set CRLFILE merely to mirror the legacy harness. An empty CRL is
287
  // not a harmless sink: when a scenario restores certificate verification,
288
  // OpenSSL rejects /dev/null before it can exercise useful handshake and
289
  // verification paths. The option is absent from the mutation manifest, so
290
  // leaving it unset introduces neither filesystem writes nor external input.
291
292
  // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's
293
  // own verbose logging on. Useful when reproducing a crashing corpus entry.
294
327k
  if (user_requested_verbose) {
295
0
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
296
0
  }
297
327k
  return connect_to;
298
327k
}
299
300
/// Apply one SetOption to the easy handle. The Scenario passed to the runner
301
/// owns every SetOption for the whole transfer, so pointer-valued options can
302
/// borrow string_value directly instead of allocating a duplicate backing
303
/// store on every iteration. This lifetime is especially important for
304
/// CURLOPT_POSTFIELDS, which curl deliberately does not copy.
305
/// @param easy   The curl easy handle to configure.
306
/// @param option The SetOption proto describing which option and value to set;
307
///               its containing Scenario must remain stable through cleanup.
308
/// @return CURLE_OK on success, an error code if the option is unsupported or
309
///         the setopt call itself failed.
310
919k
CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option) {
311
919k
  const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
312
919k
  if (desc == nullptr) {
313
33.6k
    return CURLE_UNKNOWN_OPTION;
314
33.6k
  }
315
316
885k
  switch (desc->kind) {
317
322k
    case OptionValueKind::kString: {
318
322k
      const std::string& value = option.string_value();
319
320
      // POSTFIELDS borrows its pointer and COPYPOSTFIELDS copies exactly the
321
      // previously configured size. Apply the correlated byte length first so
322
      // either option accepts embedded NULs without strlen semantics and the
323
      // copying variant can never read beyond the protobuf-owned buffer.
324
322k
      if (desc->curlopt == CURLOPT_POSTFIELDS || desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
325
16.2k
        CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(value.size()));
326
16.2k
        if (result != CURLE_OK) {
327
0
          return result;
328
0
        }
329
16.2k
      }
330
331
      // Curl otherwise treats this option as a filename during the TLS
332
      // handshake. Fixed-policy inputs normally arrive pre-constrained by the
333
      // postprocessor; this runtime check also protects compatibility inputs,
334
      // which intentionally bypass it. Curl copies this option in setopt, so
335
      // the temporary remains valid for the call's full ownership contract.
336
322k
      if (desc->curlopt == CURLOPT_PINNEDPUBLICKEY && !UsesInMemoryPublicKeyPin(value)) {
337
361
        std::string constrained = value;
338
361
        ConstrainPinnedPublicKeyValue(&constrained);
339
361
        return curl_easy_setopt(easy, desc->curlopt, constrained.c_str());
340
361
      }
341
342
321k
      return curl_easy_setopt(easy, desc->curlopt, value.c_str());
343
322k
    }
344
345
    // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option.
346
394k
    case OptionValueKind::kUint: {
347
394k
      const std::uint64_t raw = DecodeIntegralValue(*desc, option);
348
      // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a
349
      // long; everything at/above takes a curl_off_t.
350
394k
      if (static_cast<int>(desc->curlopt) >= 30000) {
351
42.2k
        return curl_easy_setopt(easy, desc->curlopt, static_cast<curl_off_t>(raw));
352
42.2k
      }
353
352k
      return curl_easy_setopt(easy, desc->curlopt, static_cast<long>(raw));
354
394k
    }
355
356
    // Decode the bool_value and pass it as a long flag (0 or 1).
357
169k
    case OptionValueKind::kBool: {
358
169k
      const long flag = static_cast<long>(DecodeIntegralValue(*desc, option));
359
169k
      return curl_easy_setopt(easy, desc->curlopt, flag);
360
394k
    }
361
885k
  }
362
0
  return CURLE_UNKNOWN_OPTION;
363
885k
}
364
365
/// Keep compatibility inputs immutable while enforcing the same observable
366
/// option prefix as postprocessed fixed lanes.
367
656k
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario& scenario) {
368
656k
  return std::min<std::size_t>(static_cast<std::size_t>(scenario.options_size()), scenario_limits::kMaxOptions);
369
656k
}
370
371
/// Apply only the prefix curl can observe in every target lane. Bounding here,
372
/// rather than relying solely on LPM's postprocessor, is important because
373
/// standalone compatibility seeds reach RunScenario without normalization.
374
327k
std::size_t ApplyScenarioOptions(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
375
327k
  const std::size_t option_count = RuntimeOptionCount(scenario);
376
1.24M
  for (std::size_t index = 0; index < option_count; ++index) {
377
919k
    (void)ApplySetOption(easy, scenario.options(static_cast<int>(index)));
378
919k
  }
379
327k
  return option_count;
380
327k
}
381
382
}  // namespace proto_fuzzer