Coverage Report

Created: 2026-04-28 07:09

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 <cstdio>
16
#include <cstdlib>
17
#include <string>
18
19
namespace proto_fuzzer {
20
21
/// How a SetOption oneof should be decoded before calling curl_easy_setopt.
22
enum class OptionValueKind {
23
  kString,  ///< string_value → const char* option.
24
  kUint,    ///< uint_value → long or curl_off_t option.
25
  kBool     ///< bool_value → 0/1 long option.
26
};
27
28
/// One row in the build-time-generated option manifest: binds a proto enum
29
/// value to the matching curl_easy_setopt option id and value kind.
30
struct OptionDescriptor {
31
  /// Proto enum identifier for this option.
32
  curl::fuzzer::proto::CurlOptionId id;
33
  /// How the oneof value should be decoded.
34
  OptionValueKind kind;
35
  /// Human-readable option name (e.g. "CURLOPT_URL") for diagnostics.
36
  const char* name;
37
  /// The native CURLoption to pass to curl_easy_setopt.
38
  CURLoption curlopt;
39
};
40
41
// Pulls in kOptionManifest[] and kOptionManifestSize.
42
#include "curl_fuzzer_option_manifest.inc"
43
44
namespace {
45
46
constexpr char kProtocolsAllowed[] = "http,ws,wss";
47
constexpr char kConnectToOverride[] = "::127.0.1.127:";
48
constexpr char kDevNull[] = "/dev/null";
49
constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE";
50
constexpr long kConnectTimeoutMs = 200;
51
constexpr long kTimeoutMs = 2000;
52
constexpr long kMaxRecvSpeed = 16 * 1024;
53
54
/// Baseline write callback for both CURLOPT_WRITEFUNCTION and
55
/// CURLOPT_HEADERFUNCTION. Consumes every byte so transfers don't stall on
56
/// backpressure and emits nothing. Protocol-specific mocks may install their
57
/// own WRITEFUNCTION afterwards if they need to poke protocol APIs while
58
/// inside a curl callback.
59
7.15k
size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; }
60
61
/// Bounded stream of bytes fed to curl_easy when CURLOPT_UPLOAD is enabled.
62
/// The fuzzer can't use stdin as the default UPLOAD source — it'd hang — so we
63
/// always install a read callback that emits a finite payload. The budget is
64
/// sized to comfortably outrun a backpressure scenario's kernel recv buffer
65
/// (typically ~4 KB after Linux doubles SO_RCVBUF=2048) so curl's send() is
66
/// forced to short-write and re-enter the partial-write paths we care about,
67
/// while still capping total runaway upload bytes per fuzz case.
68
constexpr std::size_t kMaxUploadBytes = 16 * 1024;
69
70
struct ReadState {
71
  std::size_t remaining;
72
};
73
74
75
size_t BoundedReadCallback(char* buffer, size_t size, size_t nitems, void* userdata) {
75
75
  if (userdata == nullptr) {
76
0
    return 0;
77
0
  }
78
75
  auto* state = static_cast<ReadState*>(userdata);
79
75
  const std::size_t cap = size * nitems;
80
75
  const std::size_t n = std::min(cap, state->remaining);
81
75
  if (n == 0) {
82
23
    return 0;  // signals EOF to curl
83
23
  }
84
  // Fill with a deterministic byte pattern; content doesn't matter for fuzz
85
  // coverage of the reader framing / encoder path.
86
852k
  for (std::size_t i = 0; i < n; ++i) {
87
851k
    buffer[i] = 'U';
88
851k
  }
89
52
  state->remaining -= n;
90
52
  return n;
91
75
}
92
93
/// File-static read state. Reset at the start of each scenario by
94
/// ApplyBaselineOptions; the cr_ws_read path only needs it populated when
95
/// CURLOPT_UPLOAD has been enabled on a WS transfer.
96
ReadState g_read_state{};
97
98
47.3k
const OptionDescriptor* Lookup(curl::fuzzer::proto::CurlOptionId id) {
99
283k
  for (std::size_t i = 0; i < kOptionManifestSize; ++i) {
100
281k
    if (kOptionManifest[i].id == id) {
101
45.2k
      return &kOptionManifest[i];
102
45.2k
    }
103
281k
  }
104
2.12k
  return nullptr;
105
47.3k
}
106
107
}  // namespace
108
109
/// Apply the fixed baseline options the harness always wants: output sinks,
110
/// protocol restrictions, DNS overrides, timeouts. Call before applying any
111
/// scenario options.
112
/// @param easy The curl easy handle to configure.
113
/// @return the curl_slist owned by the caller (for CURLOPT_CONNECT_TO), which
114
///         must be freed with curl_slist_free_all after curl_easy_cleanup.
115
3.28k
struct curl_slist* ApplyBaselineOptions(CURL* easy) {
116
3.28k
  curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback);
117
3.28k
  curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback);
118
119
  // Pre-seed a bounded upload buffer in case the scenario enables
120
  // CURLOPT_UPLOAD. This is the only safe way to let scenarios reach the
121
  // client-reader path (cr_ws_read etc.) — without a read callback curl would
122
  // fall back to stdin and block the fuzzer.
123
3.28k
  g_read_state.remaining = kMaxUploadBytes;
124
3.28k
  curl_easy_setopt(easy, CURLOPT_READFUNCTION, &BoundedReadCallback);
125
3.28k
  curl_easy_setopt(easy, CURLOPT_READDATA, &g_read_state);
126
127
  // Confine the easy handle to plain HTTP; refuse redirects to any other
128
  // scheme. CURLOPT_PROTOCOLS_STR arrived in 7.85.0.
129
3.28k
  curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, kProtocolsAllowed);
130
3.28k
  curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kProtocolsAllowed);
131
132
  // Force every name lookup to the fuzzer's in-process mock peer. The caller
133
  // owns the returned slist and must free it after curl_easy_cleanup.
134
3.28k
  struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride);
135
3.28k
  curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to);
136
137
  // Short bounds: fuzzing should never sit waiting on real I/O.
138
3.28k
  curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
139
3.28k
  curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs);
140
3.28k
  curl_easy_setopt(easy, CURLOPT_MAX_RECV_SPEED_LARGE, static_cast<curl_off_t>(kMaxRecvSpeed));
141
142
  // Prevent scenarios from leaking state onto the filesystem.
143
3.28k
  curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull);
144
3.28k
  curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull);
145
3.28k
  curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull);
146
3.28k
  curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull);
147
148
  // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's
149
  // own verbose logging on. Useful when reproducing a crashing corpus entry.
150
3.28k
  if (std::getenv(kVerboseEnvVar) != nullptr) {
151
0
    curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
152
0
  }
153
3.28k
  return connect_to;
154
3.28k
}
155
156
/// Apply one SetOption to the easy handle, copying any owned strings into
157
/// 'string_storage' so the pointer stays alive for the duration of
158
/// curl_easy_perform.
159
/// @param easy           The curl easy handle to configure.
160
/// @param option         The SetOption proto describing which option and
161
///                       value to set.
162
/// @param string_storage Backing store that the option's string value is
163
///                       copied into; must outlive curl_easy_perform.
164
/// @return CURLE_OK on success, an error code if the option is unsupported or
165
///         the setopt call itself failed.
166
CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option,
167
47.3k
                        std::vector<std::string>* string_storage) {
168
47.3k
  const OptionDescriptor* desc = Lookup(option.option_id());
169
47.3k
  if (desc == nullptr) {
170
2.12k
    return CURLE_UNKNOWN_OPTION;
171
2.12k
  }
172
173
45.2k
  switch (desc->kind) {
174
    // Store a copy of the string in string_storage and pass a pointer to the copy to curl_easy_setopt.
175
8.30k
    case OptionValueKind::kString: {
176
8.30k
      const std::string& src = option.string_value();
177
8.30k
      string_storage->emplace_back(src.data(), src.size());
178
8.30k
      const std::string& stored = string_storage->back();
179
180
      // Handling for POSTFIELDS to set the size.
181
8.30k
      if (desc->curlopt == CURLOPT_POSTFIELDS) {
182
1.15k
        curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(stored.size()));
183
1.15k
      }
184
185
8.30k
      return curl_easy_setopt(easy, desc->curlopt, stored.c_str());
186
0
    }
187
188
    // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option.
189
3.77k
    case OptionValueKind::kUint: {
190
3.77k
      std::uint64_t raw = option.uint_value();
191
      // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a
192
      // long; everything at/above takes a curl_off_t.
193
3.77k
      if (static_cast<int>(desc->curlopt) >= 30000) {
194
2
        return curl_easy_setopt(easy, desc->curlopt, static_cast<curl_off_t>(raw));
195
2
      }
196
3.77k
      return curl_easy_setopt(easy, desc->curlopt, static_cast<long>(raw));
197
3.77k
    }
198
199
    // Decode the bool_value and pass it as a long flag (0 or 1).
200
33.1k
    case OptionValueKind::kBool: {
201
33.1k
      long flag = option.bool_value() ? 1L : 0L;
202
33.1k
      return curl_easy_setopt(easy, desc->curlopt, flag);
203
3.77k
    }
204
45.2k
  }
205
0
  return CURLE_UNKNOWN_OPTION;
206
45.2k
}
207
208
}  // namespace proto_fuzzer