curl-fuzzer C++ reference
Loading...
Searching...
No Matches
option_apply.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
10
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"
22
23namespace proto_fuzzer {
24
25namespace {
26
27constexpr char kEventDrivenProtocolsAllowed[] = "http,https,ws,wss";
28constexpr char kTelnetProtocolAllowed[] = "telnet";
29constexpr char kFtpProtocolAllowed[] = "ftp,ftps";
30constexpr char kTftpProtocolAllowed[] = "tftp";
31constexpr char kSmtpProtocolAllowed[] = "smtp,smtps";
32constexpr char kImapProtocolAllowed[] = "imap,imaps";
33constexpr char kPop3ProtocolAllowed[] = "pop3,pop3s";
34constexpr char kMqttProtocolAllowed[] = "mqtt,mqtts";
35constexpr char kDictProtocolAllowed[] = "dict";
36constexpr char kFileProtocolAllowed[] = "file";
37constexpr char kLdapProtocolAllowed[] = "ldap,ldaps";
38constexpr char kRtspProtocolAllowed[] = "rtsp";
39constexpr char kConnectToOverride[] = "::127.0.1.127:";
40constexpr char kDevNull[] = "/dev/null";
41constexpr char kDefaultProvider[] = "default";
42constexpr char kEngineCertificateType[] = "ENG";
43constexpr char kPkcs12CertificateType[] = "P12";
44constexpr char kProviderCertificateType[] = "PROV";
45constexpr unsigned char kMalformedPkcs12[] = {0x30, 0x00};
46constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE";
47constexpr char kAltSvcHttpEnvVar[] = "CURL_ALTSVC_HTTP";
48constexpr char kHstsHttpEnvVar[] = "CURL_HSTS_HTTP";
49constexpr long kConnectTimeoutMs = 200;
50constexpr long kTimeoutMs = 200;
51
57size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; }
58
63int SilentDebugCallback(CURL* /*handle*/, curl_infotype /*type*/, char* /*data*/, size_t /*size*/, void* /*userdata*/) {
64 return 0;
65}
66
71void EnableDebugHttpTransportMetadata() {
72 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 (void)setenv(kAltSvcHttpEnvVar, "1", 0);
77 (void)setenv(kHstsHttpEnvVar, "1", 0);
78 return true;
79 }();
80 (void)configured;
81}
82
83void EnableTraceIds() { (void)curl_global_trace("+LIB-IDS"); }
84
85} // namespace
86
96CurlSlistPtr ApplyBaselineOptions(CURL* easy, curl::fuzzer::proto::Scheme scheme, bool trace_ids,
97 const std::string& trace_config) {
98 EnableDebugHttpTransportMetadata();
99
100 curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback);
101 curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback);
102
103 const bool user_requested_verbose = std::getenv(kVerboseEnvVar) != nullptr;
104 if (!trace_config.empty()) {
105 // Apply the fuzzer-selected categories before any explicit trace-ID
106 // request so a scenario cannot accidentally disable its own ID prefixes.
107 (void)curl_global_trace(trace_config.c_str());
108 }
109 if (trace_ids) {
110 EnableTraceIds();
111 }
112 if (trace_ids || !trace_config.empty()) {
113 // Keep trace coverage enabled during fuzzing without flooding the process
114 // log. The callback still executes curl_trc.c's formatting path. A stable
115 // ignored userdata pointer also lets internal DoH handles exercise the
116 // public debug-data propagation path without adding per-input ownership.
117 curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
118 curl_easy_setopt(easy, CURLOPT_DEBUGDATA, easy);
119 curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
120 } else if (scheme == curl::fuzzer::proto::SCHEME_TELNET && !user_requested_verbose) {
121 // printoption() and printsub() contain a substantial part of curl's TELNET
122 // parser diagnostics but run only in verbose mode. Keep those paths in the
123 // ordinary TELNET coverage lane while suppressing their high-volume text.
124 // An explicit FUZZ_VERBOSE still skips the sink so reproductions remain
125 // inspectable from the terminal.
126 curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback);
127 curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
128 }
129
130 // Each non-HTTP protocol must use its dedicated peer invariants. Keep them
131 // out of the redirect allowlist so an HTTP response cannot switch a stream
132 // mock into synchronous TELNET, two-channel FTP, or datagram TFTP semantics.
133 // CURLOPT_PROTOCOLS_STR arrived in 7.85.0.
134 const char* direct_protocols = kEventDrivenProtocolsAllowed;
135 switch (scheme) {
136 case curl::fuzzer::proto::SCHEME_TELNET:
137 direct_protocols = kTelnetProtocolAllowed;
138 break;
139 case curl::fuzzer::proto::SCHEME_FTP:
140 case curl::fuzzer::proto::SCHEME_FTPS:
141 direct_protocols = kFtpProtocolAllowed;
142 break;
143 case curl::fuzzer::proto::SCHEME_TFTP:
144 direct_protocols = kTftpProtocolAllowed;
145 break;
146 case curl::fuzzer::proto::SCHEME_SMTP:
147 case curl::fuzzer::proto::SCHEME_SMTPS:
148 direct_protocols = kSmtpProtocolAllowed;
149 break;
150 case curl::fuzzer::proto::SCHEME_IMAP:
151 case curl::fuzzer::proto::SCHEME_IMAPS:
152 direct_protocols = kImapProtocolAllowed;
153 break;
154 case curl::fuzzer::proto::SCHEME_POP3:
155 case curl::fuzzer::proto::SCHEME_POP3S:
156 direct_protocols = kPop3ProtocolAllowed;
157 break;
158 case curl::fuzzer::proto::SCHEME_MQTT:
159 case curl::fuzzer::proto::SCHEME_MQTTS:
160 direct_protocols = kMqttProtocolAllowed;
161 break;
162 case curl::fuzzer::proto::SCHEME_DICT:
163 direct_protocols = kDictProtocolAllowed;
164 break;
165 case curl::fuzzer::proto::SCHEME_GOPHER:
166 direct_protocols = "gopher";
167 break;
168 case curl::fuzzer::proto::SCHEME_GOPHERS:
169 direct_protocols = "gophers";
170 break;
171 case curl::fuzzer::proto::SCHEME_FILE:
172 direct_protocols = kFileProtocolAllowed;
173 break;
174 case curl::fuzzer::proto::SCHEME_LDAP:
175 case curl::fuzzer::proto::SCHEME_LDAPS:
176 direct_protocols = kLdapProtocolAllowed;
177 break;
178 case curl::fuzzer::proto::SCHEME_RTSP:
179 direct_protocols = kRtspProtocolAllowed;
180 break;
181 case curl::fuzzer::proto::SCHEME_HTTP:
182 case curl::fuzzer::proto::SCHEME_HTTPS:
183 case curl::fuzzer::proto::SCHEME_WS:
184 case curl::fuzzer::proto::SCHEME_WSS:
185 case curl::fuzzer::proto::SCHEME_IPFS:
186 case curl::fuzzer::proto::SCHEME_IPNS:
187 case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
188 default:
189 break;
190 }
191 curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, direct_protocols);
192 curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kEventDrivenProtocolsAllowed);
193
194 // CONNECT_TO confines direct connections, but an ambient http_proxy or
195 // ALL_PROXY can select a proxy before curl asks the harness for a socket.
196 // An explicit empty proxy keeps every transfer inside the socketpair and
197 // makes replay independent of the machine running the fuzzer.
198 curl_easy_setopt(easy, CURLOPT_PROXY, "");
199
200 // Keep raw secure-scheme inputs independent of the host trust store. The
201 // dedicated TLS mock installs its own in-memory trust anchor after this
202 // baseline; scenario options still run last and can deliberately select
203 // verification failures.
204 curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L);
205
206 // Force every name lookup to the fuzzer's in-process mock peer. The returned
207 // owner keeps the list alive through curl_easy_cleanup.
208 CurlSlistPtr connect_to(curl_slist_append(nullptr, kConnectToOverride));
209 curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to.get());
210
211 // Short bounds: fuzzing should never sit waiting on real I/O. Response
212 // volume is already bounded by libFuzzer's input-size limit, so do not rate
213 // limit receive traffic here: a global bytes-per-second throttle turns every
214 // otherwise-complete large response into wall-clock sleep.
215 curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs);
216 curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs);
217
218 // Keep every persistence/read path deterministic and prevent scenarios from
219 // leaking state onto the filesystem. COOKIEFILE also makes the in-memory
220 // engine's RELOAD command traverse its loader against a harmless empty
221 // source. These path options deliberately remain absent from the generated
222 // mutation manifest, so a proto cannot replace /dev/null.
223 curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull);
224 curl_easy_setopt(easy, CURLOPT_COOKIEFILE, kDevNull);
225 curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull);
226 curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull);
227 curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull);
228 // Do not set CRLFILE to a placeholder path. An empty CRL is
229 // not a harmless sink: when a scenario restores certificate verification,
230 // OpenSSL rejects /dev/null before it can exercise useful handshake and
231 // verification paths. The option is absent from the mutation manifest, so
232 // leaving it unset introduces neither filesystem writes nor external input.
233
234 // FUZZ_VERBOSE in the environment flips curl's own verbose logging on. This
235 // is useful when reproducing a crashing corpus entry.
236 if (user_requested_verbose) {
237 curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L);
238 }
239 return connect_to;
240}
241
247CURLcode ApplyTlsClientCredentialProfile(CURL* easy, curl::fuzzer::proto::TlsClientCredentialProfile profile) {
248 if (easy == nullptr) {
249 return CURLE_BAD_FUNCTION_ARGUMENT;
250 }
251
252 const auto set_string = [easy](CURLoption option, const char* value) {
253 return curl_easy_setopt(easy, option, value);
254 };
255 switch (profile) {
256 case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_NONE:
257 return CURLE_OK;
258
259 case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_MALFORMED_PKCS12: {
260 CURLcode result = set_string(CURLOPT_SSLCERTTYPE, kPkcs12CertificateType);
261 if (result != CURLE_OK) {
262 return result;
263 }
264 const curl_blob blob = {const_cast<unsigned char*>(kMalformedPkcs12), sizeof(kMalformedPkcs12), CURL_BLOB_COPY};
265 return curl_easy_setopt(easy, CURLOPT_SSLCERT_BLOB, &blob);
266 }
267
268 case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_ENGINE_DEV_NULL: {
269 CURLcode result = set_string(CURLOPT_SSLCERTTYPE, kEngineCertificateType);
270 if (result != CURLE_OK) {
271 return result;
272 }
273 return set_string(CURLOPT_SSLCERT, kDevNull);
274 }
275
276 case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_DEFAULT_PROVIDER_DEV_NULL: {
277 CURLcode result = set_string(CURLOPT_SSLENGINE, kDefaultProvider);
278 if (result != CURLE_OK) {
279 return result;
280 }
281 result = set_string(CURLOPT_SSLCERTTYPE, kProviderCertificateType);
282 if (result != CURLE_OK) {
283 return result;
284 }
285 return set_string(CURLOPT_SSLCERT, kDevNull);
286 }
287
288 case curl::fuzzer::proto::TLS_CLIENT_CREDENTIAL_UNLOADED_PROVIDER_DEV_NULL: {
289 CURLcode result = set_string(CURLOPT_SSLCERTTYPE, kProviderCertificateType);
290 if (result != CURLE_OK) {
291 return result;
292 }
293 return set_string(CURLOPT_SSLCERT, kDevNull);
294 }
295
296 default:
297 return CURLE_BAD_FUNCTION_ARGUMENT;
298 }
299}
300
310CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option) {
311 const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id());
312 if (desc == nullptr) {
313 return CURLE_UNKNOWN_OPTION;
314 }
315 const CURLoption curlopt = static_cast<CURLoption>(desc->curlopt);
316
317 switch (desc->kind) {
319 const std::string& value = option.string_value();
320
321 // COPYPOSTFIELDS allocates after reading the separately configured
322 // POSTFIELDSIZE. First install the same protobuf-owned bytes as a
323 // no-allocation borrowed fallback. A genuine OOM then still leaves a
324 // coherent pointer and length for later consumers such as AWS SigV4.
325 if (desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
326 CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDS, value.c_str());
327 if (result != CURLE_OK) {
328 return result;
329 }
330 }
331
332 // POSTFIELDS borrows its pointer and COPYPOSTFIELDS copies exactly the
333 // previously configured size. Apply the correlated byte length first so
334 // either option accepts embedded NULs without strlen semantics and the
335 // copying variant can never read beyond the protobuf-owned buffer.
336 if (desc->curlopt == CURLOPT_POSTFIELDS || desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
337 CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(value.size()));
338 if (result != CURLE_OK) {
339 return result;
340 }
341 }
342
343 if (desc->curlopt == CURLOPT_COPYPOSTFIELDS) {
344 // Copy ownership is a harness invariant rather than an allocation-
345 // failure target. Preserve pending ordinals for later injectable work.
347 return curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, value.c_str());
348 }
349
350 // Curl otherwise treats this option as a filename during the TLS
351 // handshake. Fixed-policy inputs normally arrive pre-constrained by the
352 // postprocessor; this runtime check also protects compatibility inputs,
353 // which intentionally bypass it. Curl copies this option in setopt, so
354 // the temporary remains valid for the call's full ownership contract.
355 if (desc->curlopt == CURLOPT_PINNEDPUBLICKEY && !UsesInMemoryPublicKeyPin(value)) {
356 std::string constrained = value;
357 ConstrainPinnedPublicKeyValue(&constrained);
358 return curl_easy_setopt(easy, curlopt, constrained.c_str());
359 }
360
361 return curl_easy_setopt(easy, curlopt, value.c_str());
362 }
363
364 // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option.
366 const std::uint64_t raw = DecodeIntegralOptionValue(*desc, option);
367 // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a
368 // long; everything at/above takes a curl_off_t.
369 if (static_cast<int>(desc->curlopt) >= 30000) {
370 return curl_easy_setopt(easy, curlopt, static_cast<curl_off_t>(raw));
371 }
372 return curl_easy_setopt(easy, curlopt, static_cast<long>(raw));
373 }
374
375 // Decode the bool_value and pass it as a long flag (0 or 1).
377 const long flag = static_cast<long>(DecodeIntegralOptionValue(*desc, option));
378 return curl_easy_setopt(easy, curlopt, flag);
379 }
380 }
381 return CURLE_UNKNOWN_OPTION;
382}
383
391std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario& scenario) {
392 return std::min<std::size_t>(static_cast<std::size_t>(scenario.options_size()), scenario_limits::kMaxOptions);
393}
394
402std::size_t ApplyScenarioOptions(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
403 const std::size_t option_count = RuntimeOptionCount(scenario);
404 for (std::size_t index = 0; index < option_count; ++index) {
405 (void)ApplySetOption(easy, scenario.options(static_cast<int>(index)));
406 }
407 return option_count;
408}
409
410} // namespace proto_fuzzer
Exclude a narrow harness-only operation from the allocation-failure plan.
std::unique_ptr< curl_slist, CurlSlistDeleter > CurlSlistPtr
Unique ownership of a caller-owned curl_slist.
Definition curl_raii.h:60
CURLcode ApplyTlsClientCredentialProfile(CURL *easy, curl::fuzzer::proto::TlsClientCredentialProfile profile)
Apply one closed, harness-owned TLS client-credential configuration.
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario &scenario)
Return the runtime-visible option prefix length.
std::size_t ApplyScenarioOptions(CURL *easy, const curl::fuzzer::proto::Scenario &scenario)
Apply the bounded runtime-visible prefix of Scenario.options.
CurlSlistPtr ApplyBaselineOptions(CURL *easy, curl::fuzzer::proto::Scheme scheme, bool trace_ids, const std::string &trace_config)
Apply deterministic routing, timeout, output, and persistence defaults.
CURLcode ApplySetOption(CURL *easy, const curl::fuzzer::proto::SetOption &option)
Translate and apply one generated scalar/string SetOption to the easy handle.
Translates Scenario SetOption messages into curl_easy_setopt calls, plus the fixed baseline setopts t...
void ConstrainPinnedPublicKeyValue(std::string *value)
Prefix filename-like pins with in-memory digest syntax.
bool UsesInMemoryPublicKeyPin(const std::string &value)
Whether curl will interpret this pin as an in-memory digest expression.
const OptionDescriptor * LookupOptionDescriptor(curl::fuzzer::proto::CurlOptionId id)
Return the generated description, or nullptr for an unsupported option.
std::uint64_t DecodeIntegralOptionValue(const OptionDescriptor &descriptor, const curl::fuzzer::proto::SetOption &option)
Decode using an already-resolved descriptor, avoiding another lookup when the runtime has selected th...
@ kBool
bool_value passed as a zero-or-one long flag.
@ kString
string_value passed to a string or byte-data option.
@ kUint
uint_value passed to a long or curl_off_t option.
Shared complexity limits for structured scenarios.
constexpr std::size_t kMaxOptions
Scalar options are cheap, but a finite list prevents protobuf allocation from growing independently o...
A generated option description.
OptionValueKind kind
Value family consumed by the native option.
int curlopt
Native CURLoption value, stored without exposing curl handle types.