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