/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 <cstdio> |
16 | | #include <cstdlib> |
17 | | #include <string> |
18 | | |
19 | | namespace proto_fuzzer { |
20 | | |
21 | | /// How a SetOption oneof should be decoded before calling curl_easy_setopt. |
22 | | enum class OptionValueKind { |
23 | | kString, ///< string_value → const char* option. |
24 | | kUint, ///< uint_value → long or curl_off_t option. |
25 | | kBool ///< bool_value → 0/1 long option. |
26 | | }; |
27 | | |
28 | | /// One row in the build-time-generated option manifest: binds a proto enum |
29 | | /// value to the matching curl_easy_setopt option id and value kind. |
30 | | struct OptionDescriptor { |
31 | | /// Proto enum identifier for this option. |
32 | | curl::fuzzer::proto::CurlOptionId id; |
33 | | /// How the oneof value should be decoded. |
34 | | OptionValueKind kind; |
35 | | /// Human-readable option name (e.g. "CURLOPT_URL") for diagnostics. |
36 | | const char* name; |
37 | | /// The native CURLoption to pass to curl_easy_setopt. |
38 | | CURLoption curlopt; |
39 | | }; |
40 | | |
41 | | // Pulls in kOptionManifest[] and kOptionManifestSize. |
42 | | #include "curl_fuzzer_option_manifest.inc" |
43 | | |
44 | | namespace { |
45 | | |
46 | | constexpr char kProtocolsAllowed[] = "http,ws,wss"; |
47 | | constexpr char kConnectToOverride[] = "::127.0.1.127:"; |
48 | | constexpr char kDevNull[] = "/dev/null"; |
49 | | constexpr char kVerboseEnvVar[] = "FUZZ_VERBOSE"; |
50 | | constexpr long kConnectTimeoutMs = 200; |
51 | | constexpr long kTimeoutMs = 2000; |
52 | | constexpr long kMaxRecvSpeed = 16 * 1024; |
53 | | |
54 | | /// Baseline write callback for both CURLOPT_WRITEFUNCTION and |
55 | | /// CURLOPT_HEADERFUNCTION. Consumes every byte so transfers don't stall on |
56 | | /// backpressure and emits nothing. Protocol-specific mocks may install their |
57 | | /// own WRITEFUNCTION afterwards if they need to poke protocol APIs while |
58 | | /// inside a curl callback. |
59 | 7.15k | size_t SilentWriteCallback(void* /*contents*/, size_t size, size_t nmemb, void* /*userdata*/) { return size * nmemb; } |
60 | | |
61 | | /// Bounded stream of bytes fed to curl_easy when CURLOPT_UPLOAD is enabled. |
62 | | /// The fuzzer can't use stdin as the default UPLOAD source — it'd hang — so we |
63 | | /// always install a read callback that emits a finite payload. The budget is |
64 | | /// sized to comfortably outrun a backpressure scenario's kernel recv buffer |
65 | | /// (typically ~4 KB after Linux doubles SO_RCVBUF=2048) so curl's send() is |
66 | | /// forced to short-write and re-enter the partial-write paths we care about, |
67 | | /// while still capping total runaway upload bytes per fuzz case. |
68 | | constexpr std::size_t kMaxUploadBytes = 16 * 1024; |
69 | | |
70 | | struct ReadState { |
71 | | std::size_t remaining; |
72 | | }; |
73 | | |
74 | 75 | size_t BoundedReadCallback(char* buffer, size_t size, size_t nitems, void* userdata) { |
75 | 75 | if (userdata == nullptr) { |
76 | 0 | return 0; |
77 | 0 | } |
78 | 75 | auto* state = static_cast<ReadState*>(userdata); |
79 | 75 | const std::size_t cap = size * nitems; |
80 | 75 | const std::size_t n = std::min(cap, state->remaining); |
81 | 75 | if (n == 0) { |
82 | 23 | return 0; // signals EOF to curl |
83 | 23 | } |
84 | | // Fill with a deterministic byte pattern; content doesn't matter for fuzz |
85 | | // coverage of the reader framing / encoder path. |
86 | 852k | for (std::size_t i = 0; i < n; ++i) { |
87 | 851k | buffer[i] = 'U'; |
88 | 851k | } |
89 | 52 | state->remaining -= n; |
90 | 52 | return n; |
91 | 75 | } |
92 | | |
93 | | /// File-static read state. Reset at the start of each scenario by |
94 | | /// ApplyBaselineOptions; the cr_ws_read path only needs it populated when |
95 | | /// CURLOPT_UPLOAD has been enabled on a WS transfer. |
96 | | ReadState g_read_state{}; |
97 | | |
98 | 47.3k | const OptionDescriptor* Lookup(curl::fuzzer::proto::CurlOptionId id) { |
99 | 283k | for (std::size_t i = 0; i < kOptionManifestSize; ++i) { |
100 | 281k | if (kOptionManifest[i].id == id) { |
101 | 45.2k | return &kOptionManifest[i]; |
102 | 45.2k | } |
103 | 281k | } |
104 | 2.12k | return nullptr; |
105 | 47.3k | } |
106 | | |
107 | | } // namespace |
108 | | |
109 | | /// Apply the fixed baseline options the harness always wants: output sinks, |
110 | | /// protocol restrictions, DNS overrides, timeouts. Call before applying any |
111 | | /// scenario options. |
112 | | /// @param easy The curl easy handle to configure. |
113 | | /// @return the curl_slist owned by the caller (for CURLOPT_CONNECT_TO), which |
114 | | /// must be freed with curl_slist_free_all after curl_easy_cleanup. |
115 | 3.28k | struct curl_slist* ApplyBaselineOptions(CURL* easy) { |
116 | 3.28k | curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, &SilentWriteCallback); |
117 | 3.28k | curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, &SilentWriteCallback); |
118 | | |
119 | | // Pre-seed a bounded upload buffer in case the scenario enables |
120 | | // CURLOPT_UPLOAD. This is the only safe way to let scenarios reach the |
121 | | // client-reader path (cr_ws_read etc.) — without a read callback curl would |
122 | | // fall back to stdin and block the fuzzer. |
123 | 3.28k | g_read_state.remaining = kMaxUploadBytes; |
124 | 3.28k | curl_easy_setopt(easy, CURLOPT_READFUNCTION, &BoundedReadCallback); |
125 | 3.28k | curl_easy_setopt(easy, CURLOPT_READDATA, &g_read_state); |
126 | | |
127 | | // Confine the easy handle to plain HTTP; refuse redirects to any other |
128 | | // scheme. CURLOPT_PROTOCOLS_STR arrived in 7.85.0. |
129 | 3.28k | curl_easy_setopt(easy, CURLOPT_PROTOCOLS_STR, kProtocolsAllowed); |
130 | 3.28k | curl_easy_setopt(easy, CURLOPT_REDIR_PROTOCOLS_STR, kProtocolsAllowed); |
131 | | |
132 | | // Force every name lookup to the fuzzer's in-process mock peer. The caller |
133 | | // owns the returned slist and must free it after curl_easy_cleanup. |
134 | 3.28k | struct curl_slist* connect_to = curl_slist_append(nullptr, kConnectToOverride); |
135 | 3.28k | curl_easy_setopt(easy, CURLOPT_CONNECT_TO, connect_to); |
136 | | |
137 | | // Short bounds: fuzzing should never sit waiting on real I/O. |
138 | 3.28k | curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, kConnectTimeoutMs); |
139 | 3.28k | curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kTimeoutMs); |
140 | 3.28k | curl_easy_setopt(easy, CURLOPT_MAX_RECV_SPEED_LARGE, static_cast<curl_off_t>(kMaxRecvSpeed)); |
141 | | |
142 | | // Prevent scenarios from leaking state onto the filesystem. |
143 | 3.28k | curl_easy_setopt(easy, CURLOPT_COOKIEJAR, kDevNull); |
144 | 3.28k | curl_easy_setopt(easy, CURLOPT_ALTSVC, kDevNull); |
145 | 3.28k | curl_easy_setopt(easy, CURLOPT_HSTS, kDevNull); |
146 | 3.28k | curl_easy_setopt(easy, CURLOPT_NETRC_FILE, kDevNull); |
147 | | |
148 | | // Match the legacy TLV fuzzer: FUZZ_VERBOSE in the environment flips curl's |
149 | | // own verbose logging on. Useful when reproducing a crashing corpus entry. |
150 | 3.28k | if (std::getenv(kVerboseEnvVar) != nullptr) { |
151 | 0 | curl_easy_setopt(easy, CURLOPT_VERBOSE, 1L); |
152 | 0 | } |
153 | 3.28k | return connect_to; |
154 | 3.28k | } |
155 | | |
156 | | /// Apply one SetOption to the easy handle, copying any owned strings into |
157 | | /// 'string_storage' so the pointer stays alive for the duration of |
158 | | /// curl_easy_perform. |
159 | | /// @param easy The curl easy handle to configure. |
160 | | /// @param option The SetOption proto describing which option and |
161 | | /// value to set. |
162 | | /// @param string_storage Backing store that the option's string value is |
163 | | /// copied into; must outlive curl_easy_perform. |
164 | | /// @return CURLE_OK on success, an error code if the option is unsupported or |
165 | | /// the setopt call itself failed. |
166 | | CURLcode ApplySetOption(CURL* easy, const curl::fuzzer::proto::SetOption& option, |
167 | 47.3k | std::vector<std::string>* string_storage) { |
168 | 47.3k | const OptionDescriptor* desc = Lookup(option.option_id()); |
169 | 47.3k | if (desc == nullptr) { |
170 | 2.12k | return CURLE_UNKNOWN_OPTION; |
171 | 2.12k | } |
172 | | |
173 | 45.2k | switch (desc->kind) { |
174 | | // Store a copy of the string in string_storage and pass a pointer to the copy to curl_easy_setopt. |
175 | 8.30k | case OptionValueKind::kString: { |
176 | 8.30k | const std::string& src = option.string_value(); |
177 | 8.30k | string_storage->emplace_back(src.data(), src.size()); |
178 | 8.30k | const std::string& stored = string_storage->back(); |
179 | | |
180 | | // Handling for POSTFIELDS to set the size. |
181 | 8.30k | if (desc->curlopt == CURLOPT_POSTFIELDS) { |
182 | 1.15k | curl_easy_setopt(easy, CURLOPT_POSTFIELDSIZE_LARGE, static_cast<curl_off_t>(stored.size())); |
183 | 1.15k | } |
184 | | |
185 | 8.30k | return curl_easy_setopt(easy, desc->curlopt, stored.c_str()); |
186 | 0 | } |
187 | | |
188 | | // Decode the uint_value and pass it as either a long or a curl_off_t depending on the option. |
189 | 3.77k | case OptionValueKind::kUint: { |
190 | 3.77k | std::uint64_t raw = option.uint_value(); |
191 | | // CURLOPTTYPE_OFF_T options start at 30000. Everything below takes a |
192 | | // long; everything at/above takes a curl_off_t. |
193 | 3.77k | if (static_cast<int>(desc->curlopt) >= 30000) { |
194 | 2 | return curl_easy_setopt(easy, desc->curlopt, static_cast<curl_off_t>(raw)); |
195 | 2 | } |
196 | 3.77k | return curl_easy_setopt(easy, desc->curlopt, static_cast<long>(raw)); |
197 | 3.77k | } |
198 | | |
199 | | // Decode the bool_value and pass it as a long flag (0 or 1). |
200 | 33.1k | case OptionValueKind::kBool: { |
201 | 33.1k | long flag = option.bool_value() ? 1L : 0L; |
202 | 33.1k | return curl_easy_setopt(easy, desc->curlopt, flag); |
203 | 3.77k | } |
204 | 45.2k | } |
205 | 0 | return CURLE_UNKNOWN_OPTION; |
206 | 45.2k | } |
207 | | |
208 | | } // namespace proto_fuzzer |