Coverage Report

Created: 2026-08-31 06:49

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 kProtocolsAllowed[] = "http,https,ws,wss";
50
constexpr char kConnectToOverride[] = "::127.0.1.127:";
51
constexpr char kDevNull[] = "/dev/null";
52
constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE";
53
constexpr char kAltSvcHttpEnvVar[] = "CURL_ALTSVC_HTTP";
54
constexpr char kHstsHttpEnvVar[] = "CURL_HSTS_HTTP";
55
constexpr long kConnectTimeoutMs = 200;
56
constexpr long kTimeoutMs = 200;
57
58
/// Baseline write callback for both CURLOPT_WRITEFUNCTION and
59
/// CURLOPT_HEADERFUNCTION. Consumes every byte so transfers don't stall on
60
/// backpressure and emits nothing. Protocol-specific mocks may install their
61
/// own WRITEFUNCTION afterwards if they need to poke protocol APIs while
62
/// inside a curl callback.
63
74.8k
size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; }
64
65
/// Let curl's debug build accept transport-security response headers over the
66
/// plaintext HTTP mock. The structured secure lane currently covers TLS setup
67
/// and failure handling, not a successful TLS peer, so this curl-provided test
68
/// hook is what lets the high-throughput HTTP lane reach the HSTS and Alt-Svc
69
/// parsers today.
70
10.1k
void EnableDebugHttpTransportMetadata() {
71
10.1k
  static const bool configured = [] {
72
    // CMake builds the fuzzing copy of curl with ENABLE_DEBUG specifically so
73
    // these curl-provided test hooks are available. Do not overwrite values a
74
    // reproducer deliberately supplied in its environment.
75
1
    (void)setenv(kAltSvcHttpEnvVar, "1", 0);
76
1
    (void)setenv(kHstsHttpEnvVar, "1", 0);
77
1
    return true;
78
1
  }();
79
10.1k
  (void)configured;
80
10.1k
}
81
82
/// Decode protobuf's two integral oneof members according to the semantic
83
/// kind in the generated option descriptor. The schema cannot couple an
84
/// option id to one particular oneof member, so both retained corpus entries
85
/// and ordinary mutations can represent a flag as uint_value or a numeric
86
/// mode as bool_value. Preserving magnitude for numeric options and reducing
87
/// flags to truthiness keeps either representation useful without embedding
88
/// option-specific history in the runtime. String or unset members map to the
89
/// same zero default protobuf's inactive scalar accessors historically gave.
90
17.1k
std::uint64_t DecodeIntegralValue(const OptionDescriptor& descriptor, const curl::fuzzer::proto::SetOption& option) {
91
17.1k
  if (descriptor.kind == OptionValueKind::kString) {
92
0
    return 0;
93
0
  }
94
17.1k
  switch (option.value_case()) {
95
3.02k
    case curl::fuzzer::proto::SetOption::kBoolValue:
96
3.02k
      return option.bool_value() ? 1U : 0U;
97
8.41k
    case curl::fuzzer::proto::SetOption::kUintValue:
98
8.41k
      if (descriptor.kind == OptionValueKind::kBool) {
99
1.02k
        return option.uint_value() != 0 ? 1U : 0U;
100
1.02k
      }
101
7.39k
      return option.uint_value();
102
171
    case curl::fuzzer::proto::SetOption::kStringValue:
103
5.70k
    case curl::fuzzer::proto::SetOption::VALUE_NOT_SET:
104
5.70k
      return 0;
105
17.1k
  }
106
0
  return 0;
107
17.1k
}
108
109
}  // namespace
110
111
/// Decode a recognized integral option using its generated semantic kind.
112
/// Unknown and string-valued options have no integral interpretation and
113
/// therefore return zero.
114
1.02k
std::uint64_t DecodeIntegralOptionValue(const curl::fuzzer::proto::SetOption& option) {
115
1.02k
  const OptionDescriptor* descriptor = LookupOptionDescriptor(option.option_id());
116
1.02k
  if (descriptor == nullptr || descriptor->kind == OptionValueKind::kString) {
117
0
    return 0;
118
0
  }
119
1.02k
  return DecodeIntegralValue(*descriptor, option);
120
1.02k
}
121
122
/// Make every supported option's expected oneof member explicit. Boolean and
123
/// integer representations retain their scalar meaning when crossing between
124
/// those families; string or unset mismatches become the destination family's
125
/// zero value. This focuses later mutations on a value ApplySetOption consumes
126
/// without requiring option-specific compatibility rules.
127
0
void CanonicalizeOptionValueCases(curl::fuzzer::proto::Scenario* scenario) {
128
0
  if (scenario == nullptr) {
129
0
    return;
130
0
  }
131
0
  for (auto& option : *scenario->mutable_options()) {
132
0
    const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
133
0
    if (desc == nullptr) {
134
0
      continue;
135
0
    }
136
137
0
    switch (desc->kind) {
138
0
      case OptionValueKind::kString:
139
0
        if (option.value_case() != curl::fuzzer::proto::SetOption::kStringValue) {
140
0
          option.set_string_value("");
141
0
        }
142
0
        break;
143
0
      case OptionValueKind::kUint:
144
0
        if (option.value_case() != curl::fuzzer::proto::SetOption::kUintValue) {
145
0
          option.set_uint_value(DecodeIntegralValue(*desc, option));
146
0
        }
147
0
        break;
148
0
      case OptionValueKind::kBool:
149
0
        if (option.value_case() != curl::fuzzer::proto::SetOption::kBoolValue) {
150
0
          option.set_bool_value(DecodeIntegralValue(*desc, option) != 0);
151
0
        }
152
0
        break;
153
0
    }
154
0
  }
155
0
}
156
157
/// Apply the fixed baseline options the harness always wants: output sinks,
158
/// protocol restrictions, DNS overrides, timeouts. Call before applying any
159
/// scenario options.
160
/// @param easy The curl easy handle to configure.
161
/// @return the curl_slist owned by the caller (for CURLOPT_CONNECT_TO), which
162
///         must be freed with curl_slist_free_all after curl_easy_cleanup.
163
10.1k
struct curl_slist* ApplyBaselineOptions(CURL* easy) {
164
10.1k
  EnableDebugHttpTransportMetadata();
165
166
10.1k
  curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback);
167
10.1k
  curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback);
168
169
  // Confine the easy handle to the protocols backed by in-process mocks;
170
  // refuse redirects to any other scheme. CURLOPT_PROTOCOLS_STR arrived in
171
  // 7.85.0.
172
10.1k
  curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, kProtocolsAllowed);
173
10.1k
  curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kProtocolsAllowed);
174
175
  // CONNECT_TO confines direct connections, but an ambient http_proxy or
176
  // ALL_PROXY can select a proxy before curl asks the harness for a socket.
177
  // An explicit empty proxy keeps every transfer inside the socketpair and
178
  // makes replay independent of the machine running the fuzzer.
179
10.1k
  curl_easy_setopt(easy, CURLOPT_PROXY, "");
180
181
  // Keep ordinary secure-scheme mutations independent of host trust-store
182
  // state, matching the legacy harness. An explicit scenario option is applied
183
  // later and can restore verification to exercise that deliberate path.
184
10.1k
  curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L);
185
186
  // Force every name lookup to the fuzzer's in-process mock peer. The caller
187
  // owns the returned slist and must free it after curl_easy_cleanup.
188
10.1k
  struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride);
189
10.1k
  curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to);
190
191
  // Short bounds: fuzzing should never sit waiting on real I/O. Response
192
  // volume is already bounded by libFuzzer's input-size limit, so do not rate
193
  // limit receive traffic here: a global bytes-per-second throttle turns every
194
  // otherwise-complete large response into wall-clock sleep.
195
10.1k
  curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
196
10.1k
  curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs);
197
198
  // Keep every persistence/read path deterministic and prevent scenarios from
199
  // leaking state onto the filesystem. COOKIEFILE also makes the in-memory
200
  // engine's RELOAD command traverse its loader against a harmless empty
201
  // source. These path options deliberately remain absent from the generated
202
  // mutation manifest, so a proto cannot replace /dev/null.
203
10.1k
  curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull);
204
10.1k
  curl_easy_setopt(easy, CURLOPT_COOKIEFILE, kDevNull);
205
10.1k
  curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull);
206
10.1k
  curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull);
207
10.1k
  curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull);
208
  // Do not set CRLFILE merely to mirror the legacy harness. An empty CRL is
209
  // not a harmless sink: when a scenario restores certificate verification,
210
  // OpenSSL rejects /dev/null before it can exercise useful handshake and
211
  // verification paths. The option is absent from the mutation manifest, so
212
  // leaving it unset introduces neither filesystem writes nor external input.
213
214
  // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's
215
  // own verbose logging on. Useful when reproducing a crashing corpus entry.
216
10.1k
  if (std::getenv(kVerboseEnvVar) != nullptr) {
217
0
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
218
0
  }
219
10.1k
  return connect_to;
220
10.1k
}
221
222
/// Apply one SetOption to the easy handle. The Scenario passed to the runner
223
/// owns every SetOption for the whole transfer, so pointer-valued options can
224
/// borrow string_value directly instead of allocating a duplicate backing
225
/// store on every iteration. This lifetime is especially important for
226
/// CURLOPT_POSTFIELDS, which curl deliberately does not copy.
227
/// @param easy   The curl easy handle to configure.
228
/// @param option The SetOption proto describing which option and value to set;
229
///               its containing Scenario must remain stable through cleanup.
230
/// @return CURLE_OK on success, an error code if the option is unsupported or
231
///         the setopt call itself failed.
232
22.8k
CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option) {
233
22.8k
  const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
234
22.8k
  if (desc == nullptr) {
235
2.46k
    return CURLE_UNKNOWN_OPTION;
236
2.46k
  }
237
238
20.3k
  switch (desc->kind) {
239
4.25k
    case OptionValueKind::kString: {
240
4.25k
      const std::string& value = option.string_value();
241
242
      // POSTFIELDS borrows its pointer and accepts embedded NULs only when its
243
      // size is explicit. Apply the size first so curl never observes the
244
      // protobuf bytes with strlen semantics, even transiently.
245
4.25k
      if (desc->curlopt == CURLOPT_POSTFIELDS) {
246
518
        CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(value.size()));
247
518
        if (result != CURLE_OK) {
248
0
          return result;
249
0
        }
250
518
      }
251
252
4.25k
      return curl_easy_setopt(easy, desc->curlopt, value.c_str());
253
4.25k
    }
254
255
    // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option.
256
10.6k
    case OptionValueKind::kUint: {
257
10.6k
      const std::uint64_t raw = DecodeIntegralValue(*desc, option);
258
      // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a
259
      // long; everything at/above takes a curl_off_t.
260
10.6k
      if (static_cast<int>(desc->curlopt) >= 30000) {
261
711
        return curl_easy_setopt(easy, desc->curlopt, static_cast<curl_off_t>(raw));
262
711
      }
263
9.92k
      return curl_easy_setopt(easy, desc->curlopt, static_cast<long>(raw));
264
10.6k
    }
265
266
    // Decode the bool_value and pass it as a long flag (0 or 1).
267
5.48k
    case OptionValueKind::kBool: {
268
5.48k
      const long flag = static_cast<long>(DecodeIntegralValue(*desc, option));
269
5.48k
      return curl_easy_setopt(easy, desc->curlopt, flag);
270
10.6k
    }
271
20.3k
  }
272
0
  return CURLE_UNKNOWN_OPTION;
273
20.3k
}
274
275
/// Keep compatibility inputs immutable while enforcing the same observable
276
/// option prefix as postprocessed fixed lanes.
277
22.4k
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario& scenario) {
278
22.4k
  return std::min<std::size_t>(static_cast<std::size_t>(scenario.options_size()), scenario_limits::kMaxOptions);
279
22.4k
}
280
281
/// Apply only the prefix curl can observe in every target lane. Bounding here,
282
/// rather than relying solely on LPM's postprocessor, is important because
283
/// standalone compatibility seeds reach ScenarioRunner without normalization.
284
10.1k
std::size_t ApplyScenarioOptions(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
285
10.1k
  const std::size_t option_count = RuntimeOptionCount(scenario);
286
32.9k
  for (std::size_t index = 0; index < option_count; ++index) {
287
22.8k
    (void)ApplySetOption(easy, scenario.options(static_cast<int>(index)));
288
22.8k
  }
289
10.1k
  return option_count;
290
10.1k
}
291
292
}  // namespace proto_fuzzer