Coverage Report

Created: 2026-09-01 07:00

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
0
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
0
void ConstrainPinnedPublicKeyValue(std::string* value) {
70
0
  if (value == nullptr || UsesInMemoryPublicKeyPin(*value)) {
71
0
    return;
72
0
  }
73
0
  value->insert(0, "sha256//");
74
0
}
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
203k
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
319k
int SilentDebugCallback(CURL* /*handle*/, curl_infotype /*type*/, char* /*data*/, size_t /*size*/, void* /*userdata*/) {
88
319k
  return 0;
89
319k
}
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
38.0k
void EnableDebugHttpTransportMetadata() {
96
38.0k
  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
7
    (void)setenv(kAltSvcHttpEnvVar, "1", 0);
101
7
    (void)setenv(kHstsHttpEnvVar, "1", 0);
102
7
    return true;
103
7
  }();
104
38.0k
  (void)configured;
105
38.0k
}
106
107
/// Decode protobuf's two integral oneof members according to the semantic
108
/// kind in the generated option descriptor. The schema cannot couple an
109
/// option id to one particular oneof member, so both retained corpus entries
110
/// and ordinary mutations can represent a flag as uint_value or a numeric
111
/// mode as bool_value. Preserving magnitude for numeric options and reducing
112
/// flags to truthiness keeps either representation useful without embedding
113
/// option-specific history in the runtime. String or unset members map to the
114
/// same zero default protobuf's inactive scalar accessors historically gave.
115
62.3k
std::uint64_t DecodeIntegralValue(const OptionDescriptor& descriptor, const curl::fuzzer::proto::SetOption& option) {
116
62.3k
  if (descriptor.kind == OptionValueKind::kString) {
117
0
    return 0;
118
0
  }
119
62.3k
  switch (option.value_case()) {
120
18.1k
    case curl::fuzzer::proto::SetOption::kBoolValue:
121
18.1k
      return option.bool_value() ? 1U : 0U;
122
37.6k
    case curl::fuzzer::proto::SetOption::kUintValue:
123
37.6k
      if (descriptor.kind == OptionValueKind::kBool) {
124
1.32k
        return option.uint_value() != 0 ? 1U : 0U;
125
1.32k
      }
126
36.2k
      return option.uint_value();
127
587
    case curl::fuzzer::proto::SetOption::kStringValue:
128
6.54k
    case curl::fuzzer::proto::SetOption::VALUE_NOT_SET:
129
6.54k
      return 0;
130
62.3k
  }
131
0
  return 0;
132
62.3k
}
133
134
}  // namespace
135
136
/// Decode a recognized integral option using its generated semantic kind.
137
/// Unknown and string-valued options have no integral interpretation and
138
/// therefore return zero.
139
1.41k
std::uint64_t DecodeIntegralOptionValue(const curl::fuzzer::proto::SetOption& option) {
140
1.41k
  const OptionDescriptor* descriptor = LookupOptionDescriptor(option.option_id());
141
1.41k
  if (descriptor == nullptr || descriptor->kind == OptionValueKind::kString) {
142
0
    return 0;
143
0
  }
144
1.41k
  return DecodeIntegralValue(*descriptor, option);
145
1.41k
}
146
147
/// Make every supported option's expected oneof member explicit. Boolean and
148
/// integer representations retain their scalar meaning when crossing between
149
/// those families; string or unset mismatches become the destination family's
150
/// zero value. This focuses later mutations on a value ApplySetOption consumes
151
/// without requiring option-specific compatibility rules.
152
22.6k
void CanonicalizeOptionValueCases(curl::fuzzer::proto::Scenario* scenario) {
153
22.6k
  if (scenario == nullptr) {
154
0
    return;
155
0
  }
156
60.0k
  for (auto& option : *scenario->mutable_options()) {
157
60.0k
    const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
158
60.0k
    if (desc == nullptr) {
159
2.31k
      continue;
160
2.31k
    }
161
162
57.7k
    switch (desc->kind) {
163
18.3k
      case OptionValueKind::kString:
164
18.3k
        if (option.value_case() != curl::fuzzer::proto::SetOption::kStringValue) {
165
0
          option.set_string_value("");
166
0
        }
167
18.3k
        if (desc->curlopt == CURLOPT_PINNEDPUBLICKEY) {
168
0
          ConstrainPinnedPublicKeyValue(option.mutable_string_value());
169
0
        }
170
18.3k
        break;
171
25.8k
      case OptionValueKind::kUint:
172
25.8k
        if (option.value_case() != curl::fuzzer::proto::SetOption::kUintValue) {
173
0
          option.set_uint_value(DecodeIntegralValue(*desc, option));
174
0
        }
175
25.8k
        break;
176
13.5k
      case OptionValueKind::kBool:
177
13.5k
        if (option.value_case() != curl::fuzzer::proto::SetOption::kBoolValue) {
178
0
          option.set_bool_value(DecodeIntegralValue(*desc, option) != 0);
179
0
        }
180
13.5k
        break;
181
57.7k
    }
182
57.7k
  }
183
22.6k
}
184
185
/// Apply the fixed baseline options the harness always wants: output sinks,
186
/// protocol restrictions, DNS overrides, timeouts. Call before applying any
187
/// scenario options.
188
/// @param easy The curl easy handle to configure.
189
/// @param scheme Protocol whose dedicated in-process mock will service it.
190
/// @return the curl_slist owned by the caller (for CURLOPT_CONNECT_TO), which
191
///         must be freed with curl_slist_free_all after curl_easy_cleanup.
192
38.0k
struct curl_slist* ApplyBaselineOptions(CURL* easy, curl::fuzzer::proto::Scheme scheme) {
193
38.0k
  EnableDebugHttpTransportMetadata();
194
195
38.0k
  curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback);
196
38.0k
  curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback);
197
198
38.0k
  const bool user_requested_verbose = std::getenv(kVerboseEnvVar) != nullptr;
199
38.0k
  if (scheme == curl::fuzzer::proto::SCHEME_TELNET && !user_requested_verbose) {
200
    // printoption() and printsub() contain a substantial part of curl's TELNET
201
    // parser diagnostics but run only in verbose mode. Keep those paths in the
202
    // ordinary TELNET coverage lane while suppressing their high-volume text.
203
    // An explicit FUZZ_VERBOSE still skips the sink so reproductions remain
204
    // inspectable from the terminal.
205
3.02k
    curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
206
3.02k
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
207
3.02k
  }
208
209
  // Each non-HTTP protocol must use its dedicated peer invariants. Keep them
210
  // out of the redirect allowlist so an HTTP response cannot switch a stream
211
  // mock into synchronous TELNET, two-channel FTP, or datagram TFTP semantics.
212
  // CURLOPT_PROTOCOLS_STR arrived in 7.85.0.
213
38.0k
  const char* direct_protocols = kEventDrivenProtocolsAllowed;
214
38.0k
  switch (scheme) {
215
3.02k
    case curl::fuzzer::proto::SCHEME_TELNET:
216
3.02k
      direct_protocols = kTelnetProtocolAllowed;
217
3.02k
      break;
218
0
    case curl::fuzzer::proto::SCHEME_FTP:
219
0
      direct_protocols = kFtpProtocolAllowed;
220
0
      break;
221
0
    case curl::fuzzer::proto::SCHEME_TFTP:
222
0
      direct_protocols = kTftpProtocolAllowed;
223
0
      break;
224
24.1k
    case curl::fuzzer::proto::SCHEME_HTTP:
225
28.1k
    case curl::fuzzer::proto::SCHEME_HTTPS:
226
31.3k
    case curl::fuzzer::proto::SCHEME_WS:
227
35.0k
    case curl::fuzzer::proto::SCHEME_WSS:
228
35.0k
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
229
35.0k
    default:
230
35.0k
      break;
231
38.0k
  }
232
38.0k
  curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, direct_protocols);
233
38.0k
  curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kEventDrivenProtocolsAllowed);
234
235
  // CONNECT_TO confines direct connections, but an ambient http_proxy or
236
  // ALL_PROXY can select a proxy before curl asks the harness for a socket.
237
  // An explicit empty proxy keeps every transfer inside the socketpair and
238
  // makes replay independent of the machine running the fuzzer.
239
38.0k
  curl_easy_setopt(easy, CURLOPT_PROXY, "");
240
241
  // Keep raw secure-scheme inputs independent of the host trust store,
242
  // matching the legacy harness. The dedicated TLS mock installs its own
243
  // in-memory trust anchor after this baseline; scenario options still run
244
  // last and can deliberately select verification failures.
245
38.0k
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L);
246
247
  // Force every name lookup to the fuzzer's in-process mock peer. The caller
248
  // owns the returned slist and must free it after curl_easy_cleanup.
249
38.0k
  struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride);
250
38.0k
  curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to);
251
252
  // Short bounds: fuzzing should never sit waiting on real I/O. Response
253
  // volume is already bounded by libFuzzer's input-size limit, so do not rate
254
  // limit receive traffic here: a global bytes-per-second throttle turns every
255
  // otherwise-complete large response into wall-clock sleep.
256
38.0k
  curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
257
38.0k
  curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs);
258
259
  // Keep every persistence/read path deterministic and prevent scenarios from
260
  // leaking state onto the filesystem. COOKIEFILE also makes the in-memory
261
  // engine's RELOAD command traverse its loader against a harmless empty
262
  // source. These path options deliberately remain absent from the generated
263
  // mutation manifest, so a proto cannot replace /dev/null.
264
38.0k
  curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull);
265
38.0k
  curl_easy_setopt(easy, CURLOPT_COOKIEFILE, kDevNull);
266
38.0k
  curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull);
267
38.0k
  curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull);
268
38.0k
  curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull);
269
  // Do not set CRLFILE merely to mirror the legacy harness. An empty CRL is
270
  // not a harmless sink: when a scenario restores certificate verification,
271
  // OpenSSL rejects /dev/null before it can exercise useful handshake and
272
  // verification paths. The option is absent from the mutation manifest, so
273
  // leaving it unset introduces neither filesystem writes nor external input.
274
275
  // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's
276
  // own verbose logging on. Useful when reproducing a crashing corpus entry.
277
38.0k
  if (user_requested_verbose) {
278
0
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
279
0
  }
280
38.0k
  return connect_to;
281
38.0k
}
282
283
/// Apply one SetOption to the easy handle. The Scenario passed to the runner
284
/// owns every SetOption for the whole transfer, so pointer-valued options can
285
/// borrow string_value directly instead of allocating a duplicate backing
286
/// store on every iteration. This lifetime is especially important for
287
/// CURLOPT_POSTFIELDS, which curl deliberately does not copy.
288
/// @param easy   The curl easy handle to configure.
289
/// @param option The SetOption proto describing which option and value to set;
290
///               its containing Scenario must remain stable through cleanup.
291
/// @return CURLE_OK on success, an error code if the option is unsupported or
292
///         the setopt call itself failed.
293
91.3k
CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option) {
294
91.3k
  const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
295
91.3k
  if (desc == nullptr) {
296
4.49k
    return CURLE_UNKNOWN_OPTION;
297
4.49k
  }
298
299
86.8k
  switch (desc->kind) {
300
25.8k
    case OptionValueKind::kString: {
301
25.8k
      const std::string& value = option.string_value();
302
303
      // POSTFIELDS borrows its pointer and accepts embedded NULs only when its
304
      // size is explicit. Apply the size first so curl never observes the
305
      // protobuf bytes with strlen semantics, even transiently.
306
25.8k
      if (desc->curlopt == CURLOPT_POSTFIELDS) {
307
1.41k
        CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(value.size()));
308
1.41k
        if (result != CURLE_OK) {
309
0
          return result;
310
0
        }
311
1.41k
      }
312
313
      // Curl otherwise treats this option as a filename during the TLS
314
      // handshake. Fixed-policy inputs normally arrive pre-constrained by the
315
      // postprocessor; this runtime check also protects compatibility inputs,
316
      // which intentionally bypass it. Curl copies this option in setopt, so
317
      // the temporary remains valid for the call's full ownership contract.
318
25.8k
      if (desc->curlopt == CURLOPT_PINNEDPUBLICKEY && !UsesInMemoryPublicKeyPin(value)) {
319
0
        std::string constrained = value;
320
0
        ConstrainPinnedPublicKeyValue(&constrained);
321
0
        return curl_easy_setopt(easy, desc->curlopt, constrained.c_str());
322
0
      }
323
324
25.8k
      return curl_easy_setopt(easy, desc->curlopt, value.c_str());
325
25.8k
    }
326
327
    // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option.
328
41.4k
    case OptionValueKind::kUint: {
329
41.4k
      const std::uint64_t raw = DecodeIntegralValue(*desc, option);
330
      // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a
331
      // long; everything at/above takes a curl_off_t.
332
41.4k
      if (static_cast<int>(desc->curlopt) >= 30000) {
333
5.46k
        return curl_easy_setopt(easy, desc->curlopt, static_cast<curl_off_t>(raw));
334
5.46k
      }
335
36.0k
      return curl_easy_setopt(easy, desc->curlopt, static_cast<long>(raw));
336
41.4k
    }
337
338
    // Decode the bool_value and pass it as a long flag (0 or 1).
339
19.4k
    case OptionValueKind::kBool: {
340
19.4k
      const long flag = static_cast<long>(DecodeIntegralValue(*desc, option));
341
19.4k
      return curl_easy_setopt(easy, desc->curlopt, flag);
342
41.4k
    }
343
86.8k
  }
344
0
  return CURLE_UNKNOWN_OPTION;
345
86.8k
}
346
347
/// Keep compatibility inputs immutable while enforcing the same observable
348
/// option prefix as postprocessed fixed lanes.
349
75.9k
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario& scenario) {
350
75.9k
  return std::min<std::size_t>(static_cast<std::size_t>(scenario.options_size()), scenario_limits::kMaxOptions);
351
75.9k
}
352
353
/// Apply only the prefix curl can observe in every target lane. Bounding here,
354
/// rather than relying solely on LPM's postprocessor, is important because
355
/// standalone compatibility seeds reach ScenarioRunner without normalization.
356
38.0k
std::size_t ApplyScenarioOptions(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
357
38.0k
  const std::size_t option_count = RuntimeOptionCount(scenario);
358
129k
  for (std::size_t index = 0; index < option_count; ++index) {
359
91.3k
    (void)ApplySetOption(easy, scenario.options(static_cast<int>(index)));
360
91.3k
  }
361
38.0k
  return option_count;
362
38.0k
}
363
364
}  // namespace proto_fuzzer