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