Coverage Report

Created: 2026-10-28 07:17

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,ftps";
30
constexpr char kTftpProtocolAllowed[] = "tftp";
31
constexpr char kSmtpProtocolAllowed[] = "smtp,smtps";
32
constexpr char kImapProtocolAllowed[] = "imap,imaps";
33
constexpr char kPop3ProtocolAllowed[] = "pop3,pop3s";
34
constexpr char kMqttProtocolAllowed[] = "mqtt,mqtts";
35
constexpr char kDictProtocolAllowed[] = "dict";
36
constexpr char kFileProtocolAllowed[] = "file";
37
constexpr char kLdapProtocolAllowed[] = "ldap,ldaps";
38
constexpr char kRtspProtocolAllowed[] = "rtsp";
39
constexpr char kConnectToOverride[] = "::127.0.1.127:";
40
constexpr char kDevNull[] = "/dev/null";
41
constexpr char kDefaultProvider[] = "default";
42
constexpr char kEngineCertificateType[] = "ENG";
43
constexpr char kPkcs12CertificateType[] = "P12";
44
constexpr char kProviderCertificateType[] = "PROV";
45
constexpr unsigned char kMalformedPkcs12[] = {0x30, 0x00};
46
constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE";
47
constexpr char kAltSvcHttpEnvVar[] = "CURL_ALTSVC_HTTP";
48
constexpr char kHstsHttpEnvVar[] = "CURL_HSTS_HTTP";
49
constexpr long kConnectTimeoutMs = 200;
50
constexpr long kTimeoutMs = 200;
51
52
/// Baseline write callback for both CURLOPT_WRITEFUNCTION and
53
/// CURLOPT_HEADERFUNCTION. Consumes every byte so transfers don't stall on
54
/// backpressure and emits nothing. Protocol-specific mocks may install their
55
/// own WRITEFUNCTION afterwards if they need to poke protocol APIs while
56
/// inside a curl callback.
57
4.09M
size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; }
58
59
/// Consume libcurl's verbose records without emitting per-input diagnostics.
60
/// TELNET's debug build keeps its negotiation/suboption formatters behind the
61
/// verbose switch, so the protocol lane uses this sink to make that reachable
62
/// code fuzzable without turning millions of iterations into log traffic.
63
7.91M
int SilentDebugCallback(CURL* /*handle*/, curl_infotype /*type*/, char* /*data*/, size_t /*size*/, void* /*userdata*/) {
64
7.91M
  return 0;
65
7.91M
}
66
67
/// Let curl's debug build accept transport-security response headers over the
68
/// plaintext HTTP mock. The HTTPS lane now provides a verified peer, but making
69
/// HSTS and Alt-Svc parser coverage depend on a cryptographic handshake would
70
/// needlessly remove those parsers from the high-throughput HTTP lane.
71
266k
void EnableDebugHttpTransportMetadata() {
72
266k
  static const bool configured = [] {
73
    // CMake builds the fuzzing copy of curl with ENABLE_DEBUG specifically so
74
    // these curl-provided test hooks are available. Do not overwrite values a
75
    // reproducer deliberately supplied in its environment.
76
30
    (void)setenv(kAltSvcHttpEnvVar, "1", 0);
77
30
    (void)setenv(kHstsHttpEnvVar, "1", 0);
78
30
    return true;
79
30
  }();
80
266k
  (void)configured;
81
266k
}
82
83
34.3k
void EnableTraceIds() { (void)curl_global_trace("+LIB-IDS"); }
84
85
}  // namespace
86
87
/// Apply deterministic routing, timeout, output, and persistence defaults.
88
/// Call before applying scenario options. `scheme` identifies the dedicated
89
/// in-process mock that will service the transfer and selects the safe direct-
90
/// protocol allowlist.
91
/// @param easy The curl easy handle to configure.
92
/// @param scheme Protocol whose dedicated in-process mock will service it.
93
/// @param trace_ids Enable verbose LIB-IDS tracing for this scenario.
94
/// @param trace_config Optional curl_global_trace() configuration.
95
/// @return Caller-owned CURLOPT_CONNECT_TO list, which must outlive the easy
96
///         handle and be freed with curl_slist_free_all after curl_easy_cleanup.
97
struct curl_slist* ApplyBaselineOptions(CURL* easy, curl::fuzzer::proto::Scheme scheme, bool trace_ids,
98
266k
                                        const std::string& trace_config) {
99
266k
  EnableDebugHttpTransportMetadata();
100
101
266k
  curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback);
102
266k
  curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback);
103
104
266k
  const bool user_requested_verbose = std::getenv(kVerboseEnvVar) != nullptr;
105
266k
  if (!trace_config.empty()) {
106
    // Apply the fuzzer-selected categories before any explicit trace-ID
107
    // request so a scenario cannot accidentally disable its own ID prefixes.
108
44.6k
    (void)curl_global_trace(trace_config.c_str());
109
44.6k
  }
110
266k
  if (trace_ids) {
111
34.3k
    EnableTraceIds();
112
34.3k
  }
113
266k
  if (trace_ids || !trace_config.empty()) {
114
    // Keep trace coverage enabled during fuzzing without flooding the process
115
    // log. The callback still executes curl_trc.c's formatting path.
116
68.0k
    curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
117
68.0k
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
118
198k
  } else if (scheme == curl::fuzzer::proto::SCHEME_TELNET && !user_requested_verbose) {
119
    // printoption() and printsub() contain a substantial part of curl's TELNET
120
    // parser diagnostics but run only in verbose mode. Keep those paths in the
121
    // ordinary TELNET coverage lane while suppressing their high-volume text.
122
    // An explicit FUZZ_VERBOSE still skips the sink so reproductions remain
123
    // inspectable from the terminal.
124
4.60k
    curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
125
4.60k
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
126
4.60k
  }
127
128
  // Each non-HTTP protocol must use its dedicated peer invariants. Keep them
129
  // out of the redirect allowlist so an HTTP response cannot switch a stream
130
  // mock into synchronous TELNET, two-channel FTP, or datagram TFTP semantics.
131
  // CURLOPT_PROTOCOLS_STR arrived in 7.85.0.
132
266k
  const char* direct_protocols = kEventDrivenProtocolsAllowed;
133
266k
  switch (scheme) {
134
5.05k
    case curl::fuzzer::proto::SCHEME_TELNET:
135
5.05k
      direct_protocols = kTelnetProtocolAllowed;
136
5.05k
      break;
137
4.12k
    case curl::fuzzer::proto::SCHEME_FTP:
138
6.07k
    case curl::fuzzer::proto::SCHEME_FTPS:
139
6.07k
      direct_protocols = kFtpProtocolAllowed;
140
6.07k
      break;
141
2.22k
    case curl::fuzzer::proto::SCHEME_TFTP:
142
2.22k
      direct_protocols = kTftpProtocolAllowed;
143
2.22k
      break;
144
2.74k
    case curl::fuzzer::proto::SCHEME_SMTP:
145
4.82k
    case curl::fuzzer::proto::SCHEME_SMTPS:
146
4.82k
      direct_protocols = kSmtpProtocolAllowed;
147
4.82k
      break;
148
2.40k
    case curl::fuzzer::proto::SCHEME_IMAP:
149
5.12k
    case curl::fuzzer::proto::SCHEME_IMAPS:
150
5.12k
      direct_protocols = kImapProtocolAllowed;
151
5.12k
      break;
152
1.10k
    case curl::fuzzer::proto::SCHEME_POP3:
153
3.78k
    case curl::fuzzer::proto::SCHEME_POP3S:
154
3.78k
      direct_protocols = kPop3ProtocolAllowed;
155
3.78k
      break;
156
1.01k
    case curl::fuzzer::proto::SCHEME_MQTT:
157
4.02k
    case curl::fuzzer::proto::SCHEME_MQTTS:
158
4.02k
      direct_protocols = kMqttProtocolAllowed;
159
4.02k
      break;
160
3.17k
    case curl::fuzzer::proto::SCHEME_DICT:
161
3.17k
      direct_protocols = kDictProtocolAllowed;
162
3.17k
      break;
163
4.13k
    case curl::fuzzer::proto::SCHEME_GOPHER:
164
4.13k
      direct_protocols = "gopher";
165
4.13k
      break;
166
3.39k
    case curl::fuzzer::proto::SCHEME_GOPHERS:
167
3.39k
      direct_protocols = "gophers";
168
3.39k
      break;
169
3.51k
    case curl::fuzzer::proto::SCHEME_FILE:
170
3.51k
      direct_protocols = kFileProtocolAllowed;
171
3.51k
      break;
172
2.81k
    case curl::fuzzer::proto::SCHEME_LDAP:
173
5.73k
    case curl::fuzzer::proto::SCHEME_LDAPS:
174
5.73k
      direct_protocols = kLdapProtocolAllowed;
175
5.73k
      break;
176
6.27k
    case curl::fuzzer::proto::SCHEME_RTSP:
177
6.27k
      direct_protocols = kRtspProtocolAllowed;
178
6.27k
      break;
179
131k
    case curl::fuzzer::proto::SCHEME_HTTP:
180
179k
    case curl::fuzzer::proto::SCHEME_HTTPS:
181
191k
    case curl::fuzzer::proto::SCHEME_WS:
182
199k
    case curl::fuzzer::proto::SCHEME_WSS:
183
208k
    case curl::fuzzer::proto::SCHEME_IPFS:
184
208k
    case curl::fuzzer::proto::SCHEME_IPNS:
185
208k
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
186
208k
    default:
187
208k
      break;
188
266k
  }
189
266k
  curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, direct_protocols);
190
266k
  curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kEventDrivenProtocolsAllowed);
191
192
  // CONNECT_TO confines direct connections, but an ambient http_proxy or
193
  // ALL_PROXY can select a proxy before curl asks the harness for a socket.
194
  // An explicit empty proxy keeps every transfer inside the socketpair and
195
  // makes replay independent of the machine running the fuzzer.
196
266k
  curl_easy_setopt(easy, CURLOPT_PROXY, "");
197
198
  // Keep raw secure-scheme inputs independent of the host trust store,
199
  // matching the legacy harness. The dedicated TLS mock installs its own
200
  // in-memory trust anchor after this baseline; scenario options still run
201
  // last and can deliberately select verification failures.
202
266k
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L);
203
204
  // Force every name lookup to the fuzzer's in-process mock peer. The caller
205
  // owns the returned slist and must free it after curl_easy_cleanup.
206
266k
  struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride);
207
266k
  curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to);
208
209
  // Short bounds: fuzzing should never sit waiting on real I/O. Response
210
  // volume is already bounded by libFuzzer's input-size limit, so do not rate
211
  // limit receive traffic here: a global bytes-per-second throttle turns every
212
  // otherwise-complete large response into wall-clock sleep.
213
266k
  curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
214
266k
  curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs);
215
216
  // Keep every persistence/read path deterministic and prevent scenarios from
217
  // leaking state onto the filesystem. COOKIEFILE also makes the in-memory
218
  // engine's RELOAD command traverse its loader against a harmless empty
219
  // source. These path options deliberately remain absent from the generated
220
  // mutation manifest, so a proto cannot replace /dev/null.
221
266k
  curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull);
222
266k
  curl_easy_setopt(easy, CURLOPT_COOKIEFILE, kDevNull);
223
266k
  curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull);
224
266k
  curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull);
225
266k
  curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull);
226
  // Do not set CRLFILE merely to mirror the legacy harness. An empty CRL is
227
  // not a harmless sink: when a scenario restores certificate verification,
228
  // OpenSSL rejects /dev/null before it can exercise useful handshake and
229
  // verification paths. The option is absent from the mutation manifest, so
230
  // leaving it unset introduces neither filesystem writes nor external input.
231
232
  // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's
233
  // own verbose logging on. Useful when reproducing a crashing corpus entry.
234
266k
  if (user_requested_verbose) {
235
0
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
236
0
  }
237
266k
  return connect_to;
238
266k
}
239
240
/// Apply one closed, harness-owned TLS client-credential configuration.
241
/// No protobuf bytes become a path, provider, or engine identifier.
242
/// @param easy Handle receiving the fixed credential options.
243
/// @param profile Safe credential setup selected by the HTTPS lane.
244
/// @return The first curl_easy_setopt error, or CURLE_OK.
245
27.1k
CURLcode ApplyTlsClientCredentialProfile(CURL* easy, curl::fuzzer::proto::TlsClientCredentialProfile profile) {
246
27.1k
  if (easy == nullptr) {
247
0
    return CURLE_BAD_FUNCTION_ARGUMENT;
248
0
  }
249
250
27.1k
  const auto set_string = [easy](CURLoption option, const char* value) {
251
1.34k
    return curl_easy_setopt(easy, option, value);
252
1.34k
  };
253
27.1k
  switch (profile) {
254
26.2k
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_NONE:
255
26.2k
      return CURLE_OK;
256
257
306
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_MALFORMED_PKCS12: {
258
306
      CURLcode result = set_string(CURLOPT_SSLCERTTYPE, kPkcs12CertificateType);
259
306
      if (result != CURLE_OK) {
260
3
        return result;
261
3
      }
262
303
      const curl_blob blob = {const_cast<unsigned char*>(kMalformedPkcs12), sizeof(kMalformedPkcs12), CURL_BLOB_COPY};
263
303
      return curl_easy_setopt(easy, CURLOPT_SSLCERT_BLOB, &blob);
264
306
    }
265
266
128
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_ENGINE_DEV_NULL: {
267
128
      CURLcode result = set_string(CURLOPT_SSLCERTTYPE, kEngineCertificateType);
268
128
      if (result != CURLE_OK) {
269
6
        return result;
270
6
      }
271
122
      return set_string(CURLOPT_SSLCERT, kDevNull);
272
128
    }
273
274
356
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_DEFAULT_PROVIDER_DEV_NULL: {
275
356
      CURLcode result = set_string(CURLOPT_SSLENGINE, kDefaultProvider);
276
356
      if (result != CURLE_OK) {
277
238
        return result;
278
238
      }
279
118
      result = set_string(CURLOPT_SSLCERTTYPE, kProviderCertificateType);
280
118
      if (result != CURLE_OK) {
281
9
        return result;
282
9
      }
283
109
      return set_string(CURLOPT_SSLCERT, kDevNull);
284
118
    }
285
286
105
    case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_UNLOADED_PROVIDER_DEV_NULL: {
287
105
      CURLcode result = set_string(CURLOPT_SSLCERTTYPE, kProviderCertificateType);
288
105
      if (result != CURLE_OK) {
289
4
        return result;
290
4
      }
291
101
      return set_string(CURLOPT_SSLCERT, kDevNull);
292
105
    }
293
294
0
    default:
295
0
      return CURLE_BAD_FUNCTION_ARGUMENT;
296
27.1k
  }
297
27.1k
}
298
299
/// Translate and apply one generated scalar/string SetOption to the easy handle.
300
/// Pointer-valued options borrow the SetOption's protobuf-owned string storage,
301
/// so its containing Scenario must remain alive and unmodified until the
302
/// transfer has stopped and the easy handle has been cleaned up. Borrowing
303
/// avoids a duplicate backing allocation on every iteration; the lifetime is
304
/// especially important for CURLOPT_POSTFIELDS, which curl 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
/// @return curl's setopt result, or CURLE_UNKNOWN_OPTION for an unknown id.
308
759k
CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option) {
309
759k
  const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
310
759k
  if (desc == nullptr) {
311
23.7k
    return CURLE_UNKNOWN_OPTION;
312
23.7k
  }
313
735k
  const CURLoption curlopt = static_cast<CURLoption>(desc->curlopt);
314
315
735k
  switch (desc->kind) {
316
364k
    case OptionValueKind::kString: {
317
364k
      const std::string& value = option.string_value();
318
319
      // COPYPOSTFIELDS allocates after reading the separately configured
320
      // POSTFIELDSIZE. First install the same protobuf-owned bytes as a
321
      // no-allocation borrowed fallback. A genuine OOM then still leaves a
322
      // coherent pointer and length for later consumers such as AWS SigV4.
323
364k
      if (desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
324
5.39k
        CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDS, value.c_str());
325
5.39k
        if (result != CURLE_OK) {
326
0
          return result;
327
0
        }
328
5.39k
      }
329
330
      // POSTFIELDS borrows its pointer and COPYPOSTFIELDS copies exactly the
331
      // previously configured size. Apply the correlated byte length first so
332
      // either option accepts embedded NULs without strlen semantics and the
333
      // copying variant can never read beyond the protobuf-owned buffer.
334
364k
      if (desc->curlopt == CURLOPT_POSTFIELDS || desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
335
10.0k
        CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(value.size()));
336
10.0k
        if (result != CURLE_OK) {
337
0
          return result;
338
0
        }
339
10.0k
      }
340
341
364k
      if (desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
342
        // Copy ownership is a harness invariant rather than an allocation-
343
        // failure target. Preserve pending ordinals for later injectable work.
344
5.39k
        allocation_fault_injection::ScopedFailureSuppression suppress_failures;
345
5.39k
        return curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, value.c_str());
346
5.39k
      }
347
348
      // Curl otherwise treats this option as a filename during the TLS
349
      // handshake. Fixed-policy inputs normally arrive pre-constrained by the
350
      // postprocessor; this runtime check also protects compatibility inputs,
351
      // which intentionally bypass it. Curl copies this option in setopt, so
352
      // the temporary remains valid for the call's full ownership contract.
353
359k
      if (desc->curlopt == CURLOPT_PINNEDPUBLICKEY && !UsesInMemoryPublicKeyPin(value)) {
354
143
        std::string constrained = value;
355
143
        ConstrainPinnedPublicKeyValue(&constrained);
356
143
        return curl_easy_setopt(easy, curlopt, constrained.c_str());
357
143
      }
358
359
358k
      return curl_easy_setopt(easy, curlopt, value.c_str());
360
359k
    }
361
362
    // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option.
363
177k
    case OptionValueKind::kUint: {
364
177k
      const std::uint64_t raw = DecodeIntegralOptionValue(*desc, option);
365
      // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a
366
      // long; everything at/above takes a curl_off_t.
367
177k
      if (static_cast<int>(desc->curlopt) >= 30000) {
368
27.3k
        return curl_easy_setopt(easy, curlopt, static_cast<curl_off_t>(raw));
369
27.3k
      }
370
150k
      return curl_easy_setopt(easy, curlopt, static_cast<long>(raw));
371
177k
    }
372
373
    // Decode the bool_value and pass it as a long flag (0 or 1).
374
193k
    case OptionValueKind::kBool: {
375
193k
      const long flag = static_cast<long>(DecodeIntegralOptionValue(*desc, option));
376
193k
      return curl_easy_setopt(easy, curlopt, flag);
377
177k
    }
378
735k
  }
379
0
  return CURLE_UNKNOWN_OPTION;
380
735k
}
381
382
/// Return the runtime-visible option prefix length. Fixed targets normally
383
/// trim the protobuf in their postprocessor, but the compatibility target must
384
/// preserve its historical message unchanged; applying the same bound here
385
/// prevents that lane from doing mutation-sized setopt work while exposing the
386
/// same option prefix as postprocessed fixed lanes.
387
/// @param scenario Structured input whose option prefix will be consumed.
388
/// @return Number of options visible to the runtime.
389
519k
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario& scenario) {
390
519k
  return std::min<std::size_t>(static_cast<std::size_t>(scenario.options_size()), scenario_limits::kMaxOptions);
391
519k
}
392
393
/// Apply the bounded runtime-visible prefix of Scenario.options. Individual
394
/// CURLcode values remain intentionally ignored, matching the fuzzer runner's
395
/// historical behavior. Bounding here also covers standalone compatibility
396
/// seeds that reach RunScenario without normalization.
397
/// @param easy Easy handle receiving each supported option.
398
/// @param scenario Structured input that owns all borrowed option strings.
399
/// @return Number of option entries attempted.
400
266k
std::size_t ApplyScenarioOptions(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
401
266k
  const std::size_t option_count = RuntimeOptionCount(scenario);
402
1.02M
  for (std::size_t index = 0; index < option_count; ++index) {
403
759k
    (void)ApplySetOption(easy, scenario.options(static_cast<int>(index)));
404
759k
  }
405
266k
  return option_count;
406
266k
}
407
408
}  // namespace proto_fuzzer