/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 kProtocolsAllowed[] = "http,https,ws,wss"; |
50 | | constexpr char kConnectToOverride[] = "::127.0.1.127:"; |
51 | | constexpr char kDevNull[] = "/dev/null"; |
52 | | constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE"; |
53 | | constexpr char kAltSvcHttpEnvVar[] = "CURL_ALTSVC_HTTP"; |
54 | | constexpr char kHstsHttpEnvVar[] = "CURL_HSTS_HTTP"; |
55 | | constexpr long kConnectTimeoutMs = 200; |
56 | | constexpr long kTimeoutMs = 200; |
57 | | |
58 | | /// Baseline write callback for both CURLOPT_WRITEFUNCTION and |
59 | | /// CURLOPT_HEADERFUNCTION. Consumes every byte so transfers don't stall on |
60 | | /// backpressure and emits nothing. Protocol-specific mocks may install their |
61 | | /// own WRITEFUNCTION afterwards if they need to poke protocol APIs while |
62 | | /// inside a curl callback. |
63 | 74.8k | size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; } |
64 | | |
65 | | /// Let curl's debug build accept transport-security response headers over the |
66 | | /// plaintext HTTP mock. The structured secure lane currently covers TLS setup |
67 | | /// and failure handling, not a successful TLS peer, so this curl-provided test |
68 | | /// hook is what lets the high-throughput HTTP lane reach the HSTS and Alt-Svc |
69 | | /// parsers today. |
70 | 10.1k | void EnableDebugHttpTransportMetadata() { |
71 | 10.1k | static const bool configured = [] { |
72 | | // CMake builds the fuzzing copy of curl with ENABLE_DEBUG specifically so |
73 | | // these curl-provided test hooks are available. Do not overwrite values a |
74 | | // reproducer deliberately supplied in its environment. |
75 | 1 | (void)setenv(kAltSvcHttpEnvVar, "1", 0); |
76 | 1 | (void)setenv(kHstsHttpEnvVar, "1", 0); |
77 | 1 | return true; |
78 | 1 | }(); |
79 | 10.1k | (void)configured; |
80 | 10.1k | } |
81 | | |
82 | | /// Decode protobuf's two integral oneof members according to the semantic |
83 | | /// kind in the generated option descriptor. The schema cannot couple an |
84 | | /// option id to one particular oneof member, so both retained corpus entries |
85 | | /// and ordinary mutations can represent a flag as uint_value or a numeric |
86 | | /// mode as bool_value. Preserving magnitude for numeric options and reducing |
87 | | /// flags to truthiness keeps either representation useful without embedding |
88 | | /// option-specific history in the runtime. String or unset members map to the |
89 | | /// same zero default protobuf's inactive scalar accessors historically gave. |
90 | 17.1k | std::uint64_t DecodeIntegralValue(const OptionDescriptor& descriptor, const curl::fuzzer::proto::SetOption& option) { |
91 | 17.1k | if (descriptor.kind == OptionValueKind::kString) { |
92 | 0 | return 0; |
93 | 0 | } |
94 | 17.1k | switch (option.value_case()) { |
95 | 3.02k | case curl::fuzzer::proto::SetOption::kBoolValue: |
96 | 3.02k | return option.bool_value() ? 1U : 0U; |
97 | 8.41k | case curl::fuzzer::proto::SetOption::kUintValue: |
98 | 8.41k | if (descriptor.kind == OptionValueKind::kBool) { |
99 | 1.02k | return option.uint_value() != 0 ? 1U : 0U; |
100 | 1.02k | } |
101 | 7.39k | return option.uint_value(); |
102 | 171 | case curl::fuzzer::proto::SetOption::kStringValue: |
103 | 5.70k | case curl::fuzzer::proto::SetOption::VALUE_NOT_SET: |
104 | 5.70k | return 0; |
105 | 17.1k | } |
106 | 0 | return 0; |
107 | 17.1k | } |
108 | | |
109 | | } // namespace |
110 | | |
111 | | /// Decode a recognized integral option using its generated semantic kind. |
112 | | /// Unknown and string-valued options have no integral interpretation and |
113 | | /// therefore return zero. |
114 | 1.02k | std::uint64_t DecodeIntegralOptionValue(const curl::fuzzer::proto::SetOption& option) { |
115 | 1.02k | const OptionDescriptor* descriptor = LookupOptionDescriptor(option.option_id()); |
116 | 1.02k | if (descriptor == nullptr || descriptor->kind == OptionValueKind::kString) { |
117 | 0 | return 0; |
118 | 0 | } |
119 | 1.02k | return DecodeIntegralValue(*descriptor, option); |
120 | 1.02k | } |
121 | | |
122 | | /// Make every supported option's expected oneof member explicit. Boolean and |
123 | | /// integer representations retain their scalar meaning when crossing between |
124 | | /// those families; string or unset mismatches become the destination family's |
125 | | /// zero value. This focuses later mutations on a value ApplySetOption consumes |
126 | | /// without requiring option-specific compatibility rules. |
127 | 0 | void CanonicalizeOptionValueCases(curl::fuzzer::proto::Scenario* scenario) { |
128 | 0 | if (scenario == nullptr) { |
129 | 0 | return; |
130 | 0 | } |
131 | 0 | for (auto& option : *scenario->mutable_options()) { |
132 | 0 | const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id()); |
133 | 0 | if (desc == nullptr) { |
134 | 0 | continue; |
135 | 0 | } |
136 | | |
137 | 0 | switch (desc->kind) { |
138 | 0 | case OptionValueKind::kString: |
139 | 0 | if (option.value_case() != curl::fuzzer::proto::SetOption::kStringValue) { |
140 | 0 | option.set_string_value(""); |
141 | 0 | } |
142 | 0 | break; |
143 | 0 | case OptionValueKind::kUint: |
144 | 0 | if (option.value_case() != curl::fuzzer::proto::SetOption::kUintValue) { |
145 | 0 | option.set_uint_value(DecodeIntegralValue(*desc, option)); |
146 | 0 | } |
147 | 0 | break; |
148 | 0 | case OptionValueKind::kBool: |
149 | 0 | if (option.value_case() != curl::fuzzer::proto::SetOption::kBoolValue) { |
150 | 0 | option.set_bool_value(DecodeIntegralValue(*desc, option) != 0); |
151 | 0 | } |
152 | 0 | break; |
153 | 0 | } |
154 | 0 | } |
155 | 0 | } |
156 | | |
157 | | /// Apply the fixed baseline options the harness always wants: output sinks, |
158 | | /// protocol restrictions, DNS overrides, timeouts. Call before applying any |
159 | | /// scenario options. |
160 | | /// @param easy The curl easy handle to configure. |
161 | | /// @return the curl_slist owned by the caller (for CURLOPT_CONNECT_TO), which |
162 | | /// must be freed with curl_slist_free_all after curl_easy_cleanup. |
163 | 10.1k | struct curl_slist* ApplyBaselineOptions(CURL* easy) { |
164 | 10.1k | EnableDebugHttpTransportMetadata(); |
165 | | |
166 | 10.1k | curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback); |
167 | 10.1k | curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback); |
168 | | |
169 | | // Confine the easy handle to the protocols backed by in-process mocks; |
170 | | // refuse redirects to any other scheme. CURLOPT_PROTOCOLS_STR arrived in |
171 | | // 7.85.0. |
172 | 10.1k | curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, kProtocolsAllowed); |
173 | 10.1k | curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kProtocolsAllowed); |
174 | | |
175 | | // CONNECT_TO confines direct connections, but an ambient http_proxy or |
176 | | // ALL_PROXY can select a proxy before curl asks the harness for a socket. |
177 | | // An explicit empty proxy keeps every transfer inside the socketpair and |
178 | | // makes replay independent of the machine running the fuzzer. |
179 | 10.1k | curl_easy_setopt(easy, CURLOPT_PROXY, ""); |
180 | | |
181 | | // Keep ordinary secure-scheme mutations independent of host trust-store |
182 | | // state, matching the legacy harness. An explicit scenario option is applied |
183 | | // later and can restore verification to exercise that deliberate path. |
184 | 10.1k | curl_easy_setopt(easy, CURLOPT_SSL_VERIFYPEER, 0L); |
185 | | |
186 | | // Force every name lookup to the fuzzer's in-process mock peer. The caller |
187 | | // owns the returned slist and must free it after curl_easy_cleanup. |
188 | 10.1k | struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride); |
189 | 10.1k | curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to); |
190 | | |
191 | | // Short bounds: fuzzing should never sit waiting on real I/O. Response |
192 | | // volume is already bounded by libFuzzer's input-size limit, so do not rate |
193 | | // limit receive traffic here: a global bytes-per-second throttle turns every |
194 | | // otherwise-complete large response into wall-clock sleep. |
195 | 10.1k | curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs); |
196 | 10.1k | curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs); |
197 | | |
198 | | // Keep every persistence/read path deterministic and prevent scenarios from |
199 | | // leaking state onto the filesystem. COOKIEFILE also makes the in-memory |
200 | | // engine's RELOAD command traverse its loader against a harmless empty |
201 | | // source. These path options deliberately remain absent from the generated |
202 | | // mutation manifest, so a proto cannot replace /dev/null. |
203 | 10.1k | curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull); |
204 | 10.1k | curl_easy_setopt(easy, CURLOPT_COOKIEFILE, kDevNull); |
205 | 10.1k | curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull); |
206 | 10.1k | curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull); |
207 | 10.1k | curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull); |
208 | | // Do not set CRLFILE merely to mirror the legacy harness. An empty CRL is |
209 | | // not a harmless sink: when a scenario restores certificate verification, |
210 | | // OpenSSL rejects /dev/null before it can exercise useful handshake and |
211 | | // verification paths. The option is absent from the mutation manifest, so |
212 | | // leaving it unset introduces neither filesystem writes nor external input. |
213 | | |
214 | | // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's |
215 | | // own verbose logging on. Useful when reproducing a crashing corpus entry. |
216 | 10.1k | if (std::getenv(kVerboseEnvVar) != nullptr) { |
217 | 0 | curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); |
218 | 0 | } |
219 | 10.1k | return connect_to; |
220 | 10.1k | } |
221 | | |
222 | | /// Apply one SetOption to the easy handle. The Scenario passed to the runner |
223 | | /// owns every SetOption for the whole transfer, so pointer-valued options can |
224 | | /// borrow string_value directly instead of allocating a duplicate backing |
225 | | /// store on every iteration. This lifetime is especially important for |
226 | | /// CURLOPT_POSTFIELDS, which curl deliberately does not copy. |
227 | | /// @param easy The curl easy handle to configure. |
228 | | /// @param option The SetOption proto describing which option and value to set; |
229 | | /// its containing Scenario must remain stable through cleanup. |
230 | | /// @return CURLE_OK on success, an error code if the option is unsupported or |
231 | | /// the setopt call itself failed. |
232 | 22.8k | CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option) { |
233 | 22.8k | const OptionDescriptor* desc = LookupOptionDescriptor(option.option_id()); |
234 | 22.8k | if (desc == nullptr) { |
235 | 2.46k | return CURLE_UNKNOWN_OPTION; |
236 | 2.46k | } |
237 | | |
238 | 20.3k | switch (desc->kind) { |
239 | 4.25k | case OptionValueKind::kString: { |
240 | 4.25k | const std::string& value = option.string_value(); |
241 | | |
242 | | // POSTFIELDS borrows its pointer and accepts embedded NULs only when its |
243 | | // size is explicit. Apply the size first so curl never observes the |
244 | | // protobuf bytes with strlen semantics, even transiently. |
245 | 4.25k | if (desc->curlopt == CURLOPT_POSTFIELDS) { |
246 | 518 | CURLcode result = curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(value.size())); |
247 | 518 | if (result != CURLE_OK) { |
248 | 0 | return result; |
249 | 0 | } |
250 | 518 | } |
251 | | |
252 | 4.25k | return curl_easy_setopt(easy, desc->curlopt, value.c_str()); |
253 | 4.25k | } |
254 | | |
255 | | // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option. |
256 | 10.6k | case OptionValueKind::kUint: { |
257 | 10.6k | const std::uint64_t raw = DecodeIntegralValue(*desc, option); |
258 | | // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a |
259 | | // long; everything at/above takes a curl_off_t. |
260 | 10.6k | if (static_cast<int>(desc->curlopt) >= 30000) { |
261 | 711 | return curl_easy_setopt(easy, desc->curlopt, static_cast<curl_off_t>(raw)); |
262 | 711 | } |
263 | 9.92k | return curl_easy_setopt(easy, desc->curlopt, static_cast<long>(raw)); |
264 | 10.6k | } |
265 | | |
266 | | // Decode the bool_value and pass it as a long flag (0 or 1). |
267 | 5.48k | case OptionValueKind::kBool: { |
268 | 5.48k | const long flag = static_cast<long>(DecodeIntegralValue(*desc, option)); |
269 | 5.48k | return curl_easy_setopt(easy, desc->curlopt, flag); |
270 | 10.6k | } |
271 | 20.3k | } |
272 | 0 | return CURLE_UNKNOWN_OPTION; |
273 | 20.3k | } |
274 | | |
275 | | /// Keep compatibility inputs immutable while enforcing the same observable |
276 | | /// option prefix as postprocessed fixed lanes. |
277 | 22.4k | std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario& scenario) { |
278 | 22.4k | return std::min<std::size_t>(static_cast<std::size_t>(scenario.options_size()), scenario_limits::kMaxOptions); |
279 | 22.4k | } |
280 | | |
281 | | /// Apply only the prefix curl can observe in every target lane. Bounding here, |
282 | | /// rather than relying solely on LPM's postprocessor, is important because |
283 | | /// standalone compatibility seeds reach ScenarioRunner without normalization. |
284 | 10.1k | std::size_t ApplyScenarioOptions(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
285 | 10.1k | const std::size_t option_count = RuntimeOptionCount(scenario); |
286 | 32.9k | for (std::size_t index = 0; index < option_count; ++index) { |
287 | 22.8k | (void)ApplySetOption(easy, scenario.options(static_cast<int>(index))); |
288 | 22.8k | } |
289 | 10.1k | return option_count; |
290 | 10.1k | } |
291 | | |
292 | | } // namespace proto_fuzzer |