/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"; |
30 | | constexpr char kTftpProtocolAllowed[] = "tftp"; |
31 | | constexpr char kConnectToOverride[] = "::127.0.1.127:"; |
32 | | constexpr char kDevNull[] = "/dev/null"; |
33 | | constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE"; |
34 | | constexpr char kAltSvcHttpEnvVar[] = "CURL_ALTSVC_HTTP"; |
35 | | constexpr char kHstsHttpEnvVar[] = "CURL_HSTS_HTTP"; |
36 | | constexpr long kConnectTimeoutMs = 200; |
37 | | constexpr long kTimeoutMs = 200; |
38 | | |
39 | | /// Baseline write callback for both CURLOPT_WRITEFUNCTION and |
40 | | /// CURLOPT_HEADERFUNCTION. Consumes every byte so transfers don't stall on |
41 | | /// backpressure and emits nothing. Protocol-specific mocks may install their |
42 | | /// own WRITEFUNCTION afterwards if they need to poke protocol APIs while |
43 | | /// inside a curl callback. |
44 | 4.25M | size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; } |
45 | | |
46 | | /// Consume libcurl's verbose records without emitting per-input diagnostics. |
47 | | /// TELNET's debug build keeps its negotiation/suboption formatters behind the |
48 | | /// verbose switch, so the protocol lane uses this sink to make that reachable |
49 | | /// code fuzzable without turning millions of iterations into log traffic. |
50 | 3.04M | int SilentDebugCallback(CURL* /*handle*/, curl_infotype /*type*/, char* /*data*/, size_t /*size*/, void* /*userdata*/) { |
51 | 3.04M | return 0; |
52 | 3.04M | } |
53 | | |
54 | | /// Let curl's debug build accept transport-security response headers over the |
55 | | /// plaintext HTTP mock. The HTTPS lane now provides a verified peer, but making |
56 | | /// HSTS and Alt-Svc parser coverage depend on a cryptographic handshake would |
57 | | /// needlessly remove those parsers from the high-throughput HTTP lane. |
58 | 400k | void EnableDebugHttpTransportMetadata() { |
59 | 400k | static const bool configured = [] { |
60 | | // CMake builds the fuzzing copy of curl with ENABLE_DEBUG specifically so |
61 | | // these curl-provided test hooks are available. Do not overwrite values a |
62 | | // reproducer deliberately supplied in its environment. |
63 | 21 | (void)setenv(kAltSvcHttpEnvVar, "1", 0); |
64 | 21 | (void)setenv(kHstsHttpEnvVar, "1", 0); |
65 | 21 | return true; |
66 | 21 | }(); |
67 | 400k | (void)configured; |
68 | 400k | } |
69 | | |
70 | 27.6k | void EnableTraceIds() { |
71 | 27.6k | static const bool enabled = curl_global_trace("+LIB-IDS") == CURLE_OK; |
72 | 27.6k | (void)enabled; |
73 | 27.6k | } |
74 | | |
75 | | } // namespace |
76 | | |
77 | | /// Apply deterministic routing, timeout, output, and persistence defaults. |
78 | | /// Call before applying scenario options. `scheme` identifies the dedicated |
79 | | /// in-process mock that will service the transfer and selects the safe direct- |
80 | | /// protocol allowlist. |
81 | | /// @param easy The curl easy handle to configure. |
82 | | /// @param scheme Protocol whose dedicated in-process mock will service it. |
83 | | /// @param trace_ids Enable verbose LIB-IDS tracing for this scenario. |
84 | | /// @return Caller-owned CURLOPT_CONNECT_TO list, which must outlive the easy |
85 | | /// handle and be freed with curl_slist_free_all after curl_easy_cleanup. |
86 | 400k | struct curl_slist* ApplyBaselineOptions(CURL* easy, curl::fuzzer::proto::Scheme scheme, bool trace_ids) { |
87 | 400k | EnableDebugHttpTransportMetadata(); |
88 | | |
89 | 400k | curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback); |
90 | 400k | curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback); |
91 | | |
92 | 400k | const bool user_requested_verbose = std::getenv(kVerboseEnvVar) != nullptr; |
93 | 400k | if (trace_ids) { |
94 | 27.6k | EnableTraceIds(); |
95 | | // Keep trace-ID coverage enabled during fuzzing without flooding the |
96 | | // process log. The callback still executes curl_trc.c's formatting path. |
97 | 27.6k | curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback); |
98 | 27.6k | curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); |
99 | 372k | } else if (scheme == curl::fuzzer::proto::SCHEME_TELNET && !user_requested_verbose) { |
100 | | // printoption() and printsub() contain a substantial part of curl's TELNET |
101 | | // parser diagnostics but run only in verbose mode. Keep those paths in the |
102 | | // ordinary TELNET coverage lane while suppressing their high-volume text. |
103 | | // An explicit FUZZ_VERBOSE still skips the sink so reproductions remain |
104 | | // inspectable from the terminal. |
105 | 5.46k | curl_easy_setopt(easy, CURLOPT_DEBUGFUNCTION, &SilentDebugCallback); |
106 | 5.46k | curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); |
107 | 5.46k | } |
108 | | |
109 | | // Each non-HTTP protocol must use its dedicated peer invariants. Keep them |
110 | | // out of the redirect allowlist so an HTTP response cannot switch a stream |
111 | | // mock into synchronous TELNET, two-channel FTP, or datagram TFTP semantics. |
112 | | // CURLOPT_PROTOCOLS_STR arrived in 7.85.0. |
113 | 400k | const char* direct_protocols = kEventDrivenProtocolsAllowed; |
114 | 400k | switch (scheme) { |
115 | 5.49k | case curl::fuzzer::proto::SCHEME_TELNET: |
116 | 5.49k | direct_protocols = kTelnetProtocolAllowed; |
117 | 5.49k | break; |
118 | 8.81k | case curl::fuzzer::proto::SCHEME_FTP: |
119 | 8.81k | direct_protocols = kFtpProtocolAllowed; |
120 | 8.81k | break; |
121 | 2.43k | case curl::fuzzer::proto::SCHEME_TFTP: |
122 | 2.43k | direct_protocols = kTftpProtocolAllowed; |
123 | 2.43k | break; |
124 | 5.62k | case curl::fuzzer::proto::SCHEME_GOPHER: |
125 | 5.62k | direct_protocols = "gopher"; |
126 | 5.62k | break; |
127 | 4.28k | case curl::fuzzer::proto::SCHEME_GOPHERS: |
128 | 4.28k | direct_protocols = "gophers"; |
129 | 4.28k | break; |
130 | 248k | case curl::fuzzer::proto::SCHEME_HTTP: |
131 | 340k | case curl::fuzzer::proto::SCHEME_HTTPS: |
132 | 360k | case curl::fuzzer::proto::SCHEME_WS: |
133 | 373k | case curl::fuzzer::proto::SCHEME_WSS: |
134 | 373k | case curl::fuzzer::proto::SCHEME_UNSPECIFIED: |
135 | 373k | default: |
136 | 373k | break; |
137 | 400k | } |
138 | 400k | curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, direct_protocols); |
139 | 400k | curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kEventDrivenProtocolsAllowed); |
140 | | |
141 | | // CONNECT_TO confines direct connections, but an ambient http_proxy or |
142 | | // ALL_PROXY can select a proxy before curl asks the harness for a socket. |
143 | | // An explicit empty proxy keeps every transfer inside the socketpair and |
144 | | // makes replay independent of the machine running the fuzzer. |
145 | 400k | curl_easy_setopt(easy, CURLOPT_PROXY, ""); |
146 | | |
147 | | // Keep raw secure-scheme inputs independent of the host trust store, |
148 | | // matching the legacy harness. The dedicated TLS mock installs its own |
149 | | // in-memory trust anchor after this baseline; scenario options still run |
150 | | // last and can deliberately select verification failures. |
151 | 400k | curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L); |
152 | | |
153 | | // Force every name lookup to the fuzzer's in-process mock peer. The caller |
154 | | // owns the returned slist and must free it after curl_easy_cleanup. |
155 | 400k | struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride); |
156 | 400k | curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to); |
157 | | |
158 | | // Short bounds: fuzzing should never sit waiting on real I/O. Response |
159 | | // volume is already bounded by libFuzzer's input-size limit, so do not rate |
160 | | // limit receive traffic here: a global bytes-per-second throttle turns every |
161 | | // otherwise-complete large response into wall-clock sleep. |
162 | 400k | curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs); |
163 | 400k | curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs); |
164 | | |
165 | | // Keep every persistence/read path deterministic and prevent scenarios from |
166 | | // leaking state onto the filesystem. COOKIEFILE also makes the in-memory |
167 | | // engine's RELOAD command traverse its loader against a harmless empty |
168 | | // source. These path options deliberately remain absent from the generated |
169 | | // mutation manifest, so a proto cannot replace /dev/null. |
170 | 400k | curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull); |
171 | 400k | curl_easy_setopt(easy, CURLOPT_COOKIEFILE, kDevNull); |
172 | 400k | curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull); |
173 | 400k | curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull); |
174 | 400k | curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull); |
175 | | // Do not set CRLFILE merely to mirror the legacy harness. An empty CRL is |
176 | | // not a harmless sink: when a scenario restores certificate verification, |
177 | | // OpenSSL rejects /dev/null before it can exercise useful handshake and |
178 | | // verification paths. The option is absent from the mutation manifest, so |
179 | | // leaving it unset introduces neither filesystem writes nor external input. |
180 | | |
181 | | // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's |
182 | | // own verbose logging on. Useful when reproducing a crashing corpus entry. |
183 | 400k | if (user_requested_verbose) { |
184 | 0 | curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); |
185 | 0 | } |
186 | 400k | return connect_to; |
187 | 400k | } |
188 | | |
189 | | /// Translate and apply one generated scalar/string SetOption to the easy handle. |
190 | | /// Pointer-valued options borrow the SetOption's protobuf-owned string storage, |
191 | | /// so its containing Scenario must remain alive and unmodified until the |
192 | | /// transfer has stopped and the easy handle has been cleaned up. Borrowing |
193 | | /// avoids a duplicate backing allocation on every iteration; the lifetime is |
194 | | /// especially important for CURLOPT_POSTFIELDS, which curl does not copy. |
195 | | /// @param easy The curl easy handle to configure. |
196 | | /// @param option The SetOption proto describing which option and value to set. |
197 | | /// @return curl's setopt result, or CURLE_UNKNOWN_OPTION for an unknown id. |
198 | 988k | CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option) { |
199 | 988k | const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id()); |
200 | 988k | if (desc == nullptr) { |
201 | 29.9k | return CURLE_UNKNOWN_OPTION; |
202 | 29.9k | } |
203 | 958k | const CURLoption curlopt = static_cast<CURLoption>(desc->curlopt); |
204 | | |
205 | 958k | switch (desc->kind) { |
206 | 387k | case OptionValueKind::kString: { |
207 | 387k | const std::string& value = option.string_value(); |
208 | | |
209 | | // COPYPOSTFIELDS allocates after reading the separately configured |
210 | | // POSTFIELDSIZE. First install the same protobuf-owned bytes as a |
211 | | // no-allocation borrowed fallback. A genuine OOM then still leaves a |
212 | | // coherent pointer and length for later consumers such as AWS SigV4. |
213 | 387k | if (desc->curlopt == CURLOPT_COPYPOSTFIELDS) { |
214 | 5.23k | CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDS, value.c_str()); |
215 | 5.23k | if (result != CURLE_OK) { |
216 | 0 | return result; |
217 | 0 | } |
218 | 5.23k | } |
219 | | |
220 | | // POSTFIELDS borrows its pointer and COPYPOSTFIELDS copies exactly the |
221 | | // previously configured size. Apply the correlated byte length first so |
222 | | // either option accepts embedded NULs without strlen semantics and the |
223 | | // copying variant can never read beyond the protobuf-owned buffer. |
224 | 387k | if (desc->curlopt == CURLOPT_POSTFIELDS || desc->curlopt == CURLOPT_COPYPOSTFIELDS) { |
225 | 18.7k | CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(value.size())); |
226 | 18.7k | if (result != CURLE_OK) { |
227 | 0 | return result; |
228 | 0 | } |
229 | 18.7k | } |
230 | | |
231 | 387k | if (desc->curlopt == CURLOPT_COPYPOSTFIELDS) { |
232 | | // Copy ownership is a harness invariant rather than an allocation- |
233 | | // failure target. Preserve pending ordinals for later injectable work. |
234 | 5.23k | allocation_fault_injection::ScopedFailureSuppression suppress_failures; |
235 | 5.23k | return curl_easy_setopt(easy, CURLOPT_COPYPOSTFIELDS, value.c_str()); |
236 | 5.23k | } |
237 | | |
238 | | // Curl otherwise treats this option as a filename during the TLS |
239 | | // handshake. Fixed-policy inputs normally arrive pre-constrained by the |
240 | | // postprocessor; this runtime check also protects compatibility inputs, |
241 | | // which intentionally bypass it. Curl copies this option in setopt, so |
242 | | // the temporary remains valid for the call's full ownership contract. |
243 | 382k | if (desc->curlopt == CURLOPT_PINNEDPUBLICKEY && !UsesInMemoryPublicKeyPin(value)) { |
244 | 650 | std::string constrained = value; |
245 | 650 | ConstrainPinnedPublicKeyValue(&constrained); |
246 | 650 | return curl_easy_setopt(easy, curlopt, constrained.c_str()); |
247 | 650 | } |
248 | | |
249 | 381k | return curl_easy_setopt(easy, curlopt, value.c_str()); |
250 | 382k | } |
251 | | |
252 | | // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option. |
253 | 404k | case OptionValueKind::kUint: { |
254 | 404k | const std::uint64_t raw = DecodeIntegralOptionValue(*desc, option); |
255 | | // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a |
256 | | // long; everything at/above takes a curl_off_t. |
257 | 404k | if (static_cast<int>(desc->curlopt) >= 30000) { |
258 | 42.2k | return curl_easy_setopt(easy, curlopt, static_cast<curl_off_t>(raw)); |
259 | 42.2k | } |
260 | 362k | return curl_easy_setopt(easy, curlopt, static_cast<long>(raw)); |
261 | 404k | } |
262 | | |
263 | | // Decode the bool_value and pass it as a long flag (0 or 1). |
264 | 166k | case OptionValueKind::kBool: { |
265 | 166k | const long flag = static_cast<long>(DecodeIntegralOptionValue(*desc, option)); |
266 | 166k | return curl_easy_setopt(easy, curlopt, flag); |
267 | 404k | } |
268 | 958k | } |
269 | 0 | return CURLE_UNKNOWN_OPTION; |
270 | 958k | } |
271 | | |
272 | | /// Return the runtime-visible option prefix length. Fixed targets normally |
273 | | /// trim the protobuf in their postprocessor, but the compatibility target must |
274 | | /// preserve its historical message unchanged; applying the same bound here |
275 | | /// prevents that lane from doing mutation-sized setopt work while exposing the |
276 | | /// same option prefix as postprocessed fixed lanes. |
277 | | /// @param scenario Structured input whose option prefix will be consumed. |
278 | | /// @return Number of options visible to the runtime. |
279 | 809k | std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario& scenario) { |
280 | 809k | return std::min<std::size_t>(static_cast<std::size_t>(scenario.options_size()), scenario_limits::kMaxOptions); |
281 | 809k | } |
282 | | |
283 | | /// Apply the bounded runtime-visible prefix of Scenario.options. Individual |
284 | | /// CURLcode values remain intentionally ignored, matching the fuzzer runner's |
285 | | /// historical behavior. Bounding here also covers standalone compatibility |
286 | | /// seeds that reach RunScenario without normalization. |
287 | | /// @param easy Easy handle receiving each supported option. |
288 | | /// @param scenario Structured input that owns all borrowed option strings. |
289 | | /// @return Number of option entries attempted. |
290 | 400k | std::size_t ApplyScenarioOptions(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
291 | 400k | const std::size_t option_count = RuntimeOptionCount(scenario); |
292 | 1.38M | for (std::size_t index = 0; index < option_count; ++index) { |
293 | 988k | (void)ApplySetOption(easy, scenario.options(static_cast<int>(index))); |
294 | 988k | } |
295 | 400k | return option_count; |
296 | 400k | } |
297 | | |
298 | | } // namespace proto_fuzzer |