/src/curl_fuzzer/proto_fuzzer/request_data.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 Builds bounded curl_slist and curl_mime state from a Scenario. |
9 | | |
10 | | #include "proto_fuzzer/request_data.h" |
11 | | |
12 | | #include <algorithm> |
13 | | #include <cstddef> |
14 | | #include <cstdio> |
15 | | #include <cstring> |
16 | | #include <limits> |
17 | | #include <string> |
18 | | |
19 | | #include "proto_fuzzer/option_apply.h" |
20 | | #include "proto_fuzzer/scenario_limits.h" |
21 | | |
22 | | namespace proto_fuzzer { |
23 | | |
24 | | namespace { |
25 | | |
26 | | /// Borrow the protobuf string when it already fits curl's NUL-terminated API, |
27 | | /// allocating `truncated` only for compatibility inputs that bypass the fixed |
28 | | /// target postprocessor. The callers below all synchronously copy this value, |
29 | | /// so neither pointer escapes the call. Embedded NUL bytes deliberately remain: |
30 | | /// curl observes the same prefix as before while an oversized invisible suffix |
31 | | /// cannot dominate allocation. |
32 | 483k | const char* BoundedCString(const std::string& value, std::size_t limit, std::string* truncated) { |
33 | 483k | if (value.size() <= limit) { |
34 | 482k | return value.c_str(); |
35 | 482k | } |
36 | 143 | truncated->assign(value.data(), limit); |
37 | 143 | return truncated->c_str(); |
38 | 483k | } |
39 | | |
40 | | /// Raw read/seek callbacks are useful only when a script exists or a supported |
41 | | /// SetOption can make curl request caller-provided body bytes. CURLOPT_POST is |
42 | | /// included because POST without POSTFIELDS/MIME also falls back to the read |
43 | | /// callback. Avoiding callbacks for ordinary requests removes eight setopt |
44 | | /// calls across setup and teardown, while any potentially body-reading option |
45 | | /// retains the non-blocking fallback regardless of its mutated value. |
46 | 326k | bool NeedsUploadCallbacks(const curl::fuzzer::proto::Scenario& scenario) { |
47 | | // curl's TELNET implementation polls stdin unless a READFUNCTION was |
48 | | // explicitly installed. Always provide the bounded per-scenario source, |
49 | | // even when there is no upload payload to send. |
50 | 326k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET || scenario.has_upload()) { |
51 | 27.6k | return true; |
52 | 27.6k | } |
53 | 298k | const std::size_t option_count = RuntimeOptionCount(scenario); |
54 | 1.02M | for (std::size_t index = 0; index < option_count; ++index) { |
55 | 738k | const auto& option = scenario.options(static_cast<int>(index)); |
56 | 738k | if (option.option_id() == curl::fuzzer::proto::CURLOPT_UPLOAD || |
57 | 729k | option.option_id() == curl::fuzzer::proto::CURLOPT_POST) { |
58 | 13.5k | return true; |
59 | 13.5k | } |
60 | 738k | } |
61 | 284k | return false; |
62 | 298k | } |
63 | | |
64 | | /// Translate the mutation-friendly enum to curl's spelling. Restricting this |
65 | | /// field to supported encoders spends cycles in encoder implementations rather |
66 | | /// than repeatedly rediscovering the same invalid-string rejection. |
67 | 188k | const char* MimeEncoderName(curl::fuzzer::proto::MimeEncoder encoder) { |
68 | 188k | switch (encoder) { |
69 | 11.6k | case curl::fuzzer::proto::MIME_ENCODER_BINARY: |
70 | 11.6k | return "binary"; |
71 | 12.0k | case curl::fuzzer::proto::MIME_ENCODER_8BIT: |
72 | 12.0k | return "8bit"; |
73 | 14.6k | case curl::fuzzer::proto::MIME_ENCODER_7BIT: |
74 | 14.6k | return "7bit"; |
75 | 26.7k | case curl::fuzzer::proto::MIME_ENCODER_BASE64: |
76 | 26.7k | return "base64"; |
77 | 44.6k | case curl::fuzzer::proto::MIME_ENCODER_QUOTED_PRINTABLE: |
78 | 44.6k | return "quoted-printable"; |
79 | 78.3k | case curl::fuzzer::proto::MIME_ENCODER_UNSPECIFIED: |
80 | 78.6k | default: |
81 | 78.6k | return nullptr; |
82 | 188k | } |
83 | 188k | } |
84 | | |
85 | | /// Append at most `limit` protobuf byte strings to a curl list. curl_slist_append |
86 | | /// leaves the old head valid on allocation failure, so only replace the head |
87 | | /// after a successful append and stop rather than burning the rest of the |
88 | | /// iteration on allocations that are already failing. |
89 | | template <typename RepeatedBytes> |
90 | | curl_slist* BuildStringList(const RepeatedBytes& values, std::size_t count_limit, std::size_t value_limit, |
91 | 527k | std::size_t* applied) { |
92 | 527k | curl_slist* list = nullptr; |
93 | 527k | std::string truncated; |
94 | 527k | const std::size_t count = std::min<std::size_t>(count_limit, values.size()); |
95 | 780k | for (std::size_t i = 0; i < count; ++i) { |
96 | 253k | const std::string& value = values.Get(static_cast<int>(i)); |
97 | 253k | curl_slist* appended = curl_slist_append(list, BoundedCString(value, value_limit, &truncated)); |
98 | 253k | if (appended == nullptr) { |
99 | 0 | break; |
100 | 0 | } |
101 | 253k | list = appended; |
102 | 253k | ++*applied; |
103 | 253k | } |
104 | 527k | return list; |
105 | 527k | } |
106 | | |
107 | | /// Apply the metadata shared by top-level and nested protobuf part types. The |
108 | | /// MIME API copies these strings, so temporary bounded buffers are sufficient; |
109 | | /// only the MIME root itself needs to outlive the perform loop. |
110 | | template <typename ProtoPart> |
111 | 188k | void ApplyPartMetadata(curl_mimepart* part, const ProtoPart& source, RequestBuildStats* stats) { |
112 | | // Reuse the rare compatibility-path allocation across all three fields; |
113 | | // ordinary postprocessed metadata never writes this scratch string. |
114 | 188k | std::string truncated; |
115 | 188k | if (!source.name().empty()) { |
116 | 78.6k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); |
117 | 78.6k | } |
118 | 188k | if (!source.filename().empty()) { |
119 | 76.9k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); |
120 | 76.9k | } |
121 | 188k | if (!source.content_type().empty()) { |
122 | 74.2k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); |
123 | 74.2k | } |
124 | 188k | if (const char* encoder = MimeEncoderName(source.encoder())) { |
125 | 109k | (void)curl_mime_encoder(part, encoder); |
126 | 109k | } |
127 | | |
128 | 188k | std::size_t header_count = 0; |
129 | 188k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, |
130 | 188k | scenario_limits::kMaxMetadataBytes, &header_count); |
131 | 188k | if (headers != nullptr) { |
132 | | // take_ownership=1 is crucial: unlike the strings above, MIME retains the |
133 | | // list pointer. Once attached, the root curl_mime_free call recursively |
134 | | // releases it, including lists on nested parts. |
135 | 55.5k | const CURLcode result = curl_mime_headers(part, headers, 1); |
136 | 55.5k | if (result == CURLE_OK) { |
137 | 55.5k | stats->mime_headers += header_count; |
138 | 55.5k | } else { |
139 | 0 | curl_slist_free_all(headers); |
140 | 0 | } |
141 | 55.5k | } |
142 | 188k | } request_data.cc:void proto_fuzzer::(anonymous namespace)::ApplyPartMetadata<curl::fuzzer::proto::MimePart>(curl_mimepart*, curl::fuzzer::proto::MimePart const&, proto_fuzzer::RequestBuildStats*) Line | Count | Source | 111 | 79.2k | void ApplyPartMetadata(curl_mimepart* part, const ProtoPart& source, RequestBuildStats* stats) { | 112 | | // Reuse the rare compatibility-path allocation across all three fields; | 113 | | // ordinary postprocessed metadata never writes this scratch string. | 114 | 79.2k | std::string truncated; | 115 | 79.2k | if (!source.name().empty()) { | 116 | 29.7k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); | 117 | 29.7k | } | 118 | 79.2k | if (!source.filename().empty()) { | 119 | 27.4k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); | 120 | 27.4k | } | 121 | 79.2k | if (!source.content_type().empty()) { | 122 | 26.2k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); | 123 | 26.2k | } | 124 | 79.2k | if (const char* encoder = MimeEncoderName(source.encoder())) { | 125 | 45.6k | (void)curl_mime_encoder(part, encoder); | 126 | 45.6k | } | 127 | | | 128 | 79.2k | std::size_t header_count = 0; | 129 | 79.2k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, | 130 | 79.2k | scenario_limits::kMaxMetadataBytes, &header_count); | 131 | 79.2k | if (headers != nullptr) { | 132 | | // take_ownership=1 is crucial: unlike the strings above, MIME retains the | 133 | | // list pointer. Once attached, the root curl_mime_free call recursively | 134 | | // releases it, including lists on nested parts. | 135 | 19.1k | const CURLcode result = curl_mime_headers(part, headers, 1); | 136 | 19.1k | if (result == CURLE_OK) { | 137 | 19.1k | stats->mime_headers += header_count; | 138 | 19.1k | } else { | 139 | 0 | curl_slist_free_all(headers); | 140 | 0 | } | 141 | 19.1k | } | 142 | 79.2k | } |
request_data.cc:void proto_fuzzer::(anonymous namespace)::ApplyPartMetadata<curl::fuzzer::proto::MimeDataPart>(curl_mimepart*, curl::fuzzer::proto::MimeDataPart const&, proto_fuzzer::RequestBuildStats*) Line | Count | Source | 111 | 109k | void ApplyPartMetadata(curl_mimepart* part, const ProtoPart& source, RequestBuildStats* stats) { | 112 | | // Reuse the rare compatibility-path allocation across all three fields; | 113 | | // ordinary postprocessed metadata never writes this scratch string. | 114 | 109k | std::string truncated; | 115 | 109k | if (!source.name().empty()) { | 116 | 48.8k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); | 117 | 48.8k | } | 118 | 109k | if (!source.filename().empty()) { | 119 | 49.5k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); | 120 | 49.5k | } | 121 | 109k | if (!source.content_type().empty()) { | 122 | 48.0k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); | 123 | 48.0k | } | 124 | 109k | if (const char* encoder = MimeEncoderName(source.encoder())) { | 125 | 64.2k | (void)curl_mime_encoder(part, encoder); | 126 | 64.2k | } | 127 | | | 128 | 109k | std::size_t header_count = 0; | 129 | 109k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, | 130 | 109k | scenario_limits::kMaxMetadataBytes, &header_count); | 131 | 109k | if (headers != nullptr) { | 132 | | // take_ownership=1 is crucial: unlike the strings above, MIME retains the | 133 | | // list pointer. Once attached, the root curl_mime_free call recursively | 134 | | // releases it, including lists on nested parts. | 135 | 36.4k | const CURLcode result = curl_mime_headers(part, headers, 1); | 136 | 36.4k | if (result == CURLE_OK) { | 137 | 36.4k | stats->mime_headers += header_count; | 138 | 36.4k | } else { | 139 | 0 | curl_slist_free_all(headers); | 140 | 0 | } | 141 | 36.4k | } | 142 | 109k | } |
|
143 | | |
144 | | /// Copy bounded binary data into a MIME part. curl_mime_data accepts an |
145 | | /// explicit size, so embedded NUL bytes remain fuzzable here unlike in the |
146 | | /// metadata and header APIs. |
147 | 120k | void ApplyPartData(curl_mimepart* part, const std::string& data) { |
148 | 120k | const std::size_t size = std::min(data.size(), scenario_limits::kMaxMimeDataBytes); |
149 | 120k | const char* bytes = data.empty() ? "" : data.data(); |
150 | 120k | (void)curl_mime_data(part, bytes, size); |
151 | 120k | } |
152 | | |
153 | | /// Populate the fixed-depth child body and debit the shared total-part budget. |
154 | | /// Returning an empty MIME object when the child list is empty is intentional: |
155 | | /// curl's empty multipart serialization is useful coverage and remains cheap. |
156 | | void PopulateSubparts(curl_mime* mime, const curl::fuzzer::proto::MimeSubparts& source, std::size_t* remaining_parts, |
157 | 45.8k | RequestBuildStats* stats) { |
158 | 45.8k | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxNestedMimeParts, source.parts_size()); |
159 | 155k | for (std::size_t i = 0; i < count && *remaining_parts != 0; ++i) { |
160 | 109k | curl_mimepart* part = curl_mime_addpart(mime); |
161 | 109k | if (part == nullptr) { |
162 | 0 | break; |
163 | 0 | } |
164 | 109k | --*remaining_parts; |
165 | 109k | ++stats->mime_parts; |
166 | 109k | const auto& proto_part = source.parts(static_cast<int>(i)); |
167 | 109k | ApplyPartMetadata(part, proto_part, stats); |
168 | 109k | ApplyPartData(part, proto_part.data()); |
169 | 109k | } |
170 | 45.8k | } |
171 | | |
172 | | /// Construct the top MIME tree. curl_mime_subparts transfers ownership only |
173 | | /// on success, so failed attachments are freed immediately while successful |
174 | | /// ones are left for the top-level root to release recursively. |
175 | 45.2k | curl_mime* BuildMimePost(CURL* easy, const curl::fuzzer::proto::MimePost& source, RequestBuildStats* stats) { |
176 | 45.2k | curl_mime* mime = curl_mime_init(easy); |
177 | 45.2k | if (mime == nullptr) { |
178 | 0 | return nullptr; |
179 | 0 | } |
180 | | |
181 | 45.2k | std::size_t remaining_parts = scenario_limits::kMaxTotalMimeParts; |
182 | 45.2k | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxTopLevelMimeParts, source.parts_size()); |
183 | 124k | for (std::size_t i = 0; i < count && remaining_parts != 0; ++i) { |
184 | 79.2k | curl_mimepart* part = curl_mime_addpart(mime); |
185 | 79.2k | if (part == nullptr) { |
186 | 0 | break; |
187 | 0 | } |
188 | 79.2k | --remaining_parts; |
189 | 79.2k | ++stats->mime_parts; |
190 | 79.2k | const auto& proto_part = source.parts(static_cast<int>(i)); |
191 | 79.2k | ApplyPartMetadata(part, proto_part, stats); |
192 | | |
193 | 79.2k | switch (proto_part.content_case()) { |
194 | 11.1k | case curl::fuzzer::proto::MimePart::kData: |
195 | 11.1k | ApplyPartData(part, proto_part.data()); |
196 | 11.1k | break; |
197 | 45.8k | case curl::fuzzer::proto::MimePart::kSubparts: { |
198 | 45.8k | curl_mime* subparts = curl_mime_init(easy); |
199 | 45.8k | if (subparts == nullptr) { |
200 | 0 | break; |
201 | 0 | } |
202 | 45.8k | PopulateSubparts(subparts, proto_part.subparts(), &remaining_parts, stats); |
203 | 45.8k | if (curl_mime_subparts(part, subparts) != CURLE_OK) { |
204 | 0 | curl_mime_free(subparts); |
205 | 0 | } |
206 | 45.8k | break; |
207 | 45.8k | } |
208 | 22.2k | case curl::fuzzer::proto::MimePart::CONTENT_NOT_SET: |
209 | 22.2k | default: |
210 | 22.2k | break; |
211 | 79.2k | } |
212 | 79.2k | } |
213 | 45.2k | return mime; |
214 | 45.2k | } |
215 | | |
216 | | } // namespace |
217 | | |
218 | | /// Borrow and cap the immutable upload shape once, before libcurl receives a |
219 | | /// userdata pointer. RunScenario keeps the protobuf alive for the complete |
220 | | /// drive, so a view removes a per-input body copy without weakening callback |
221 | | /// lifetime. For non-TELNET schemes, the absent-message fallback avoids a |
222 | | /// 16 KiB allocation by synthesizing the same `U` bytes as the old callback. |
223 | | /// TELNET deliberately starts at EOF so an absent script cannot become input. |
224 | | UploadScriptState::UploadScriptState(const curl::fuzzer::proto::Scenario& scenario) |
225 | 326k | : data_(), |
226 | 326k | read_step_count_(0), |
227 | 326k | total_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? 0 : scenario_limits::kMaxUploadBytes), |
228 | 326k | max_read_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? scenario_limits::kMaxTelnetUploadReadSize |
229 | 326k | : scenario_limits::kMaxUploadReadSize), |
230 | 326k | offset_(0), |
231 | 326k | next_read_size_(0), |
232 | 326k | terminal_(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF), |
233 | 326k | seek_result_(curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK), |
234 | 326k | before_read_callback_(nullptr), |
235 | 326k | before_read_userdata_(nullptr), |
236 | 326k | scripted_(scenario.has_upload()) { |
237 | 326k | if (!scripted_) { |
238 | 302k | return; |
239 | 302k | } |
240 | | |
241 | 23.1k | const auto& upload = scenario.upload(); |
242 | 23.1k | const std::size_t data_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET |
243 | 23.1k | ? scenario_limits::kMaxTelnetUploadBytes |
244 | 23.1k | : scenario_limits::kMaxUploadBytes; |
245 | 23.1k | const std::size_t data_size = std::min(upload.data().size(), data_limit); |
246 | 23.1k | data_ = std::string_view(upload.data().data(), data_size); |
247 | 23.1k | total_size_ = data_.size(); |
248 | 23.1k | terminal_ = upload.terminal(); |
249 | 23.1k | if (scenario.scheme() != curl::fuzzer::proto::SCHEME_TELNET && |
250 | 22.8k | terminal_ == curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE) { |
251 | | // Event-driven protocols need an external resume source. Interpret this |
252 | | // TELNET-specific outcome as EOF before curl can retain a paused transfer. |
253 | 86 | terminal_ = curl::fuzzer::proto::UPLOAD_TERMINAL_EOF; |
254 | 86 | } |
255 | 23.1k | seek_result_ = upload.seek_result(); |
256 | | |
257 | 23.1k | const std::size_t read_step_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET |
258 | 23.1k | ? scenario_limits::kMaxTelnetUploadReadSteps |
259 | 23.1k | : scenario_limits::kMaxUploadReadSteps; |
260 | 23.1k | read_step_count_ = std::min<std::size_t>(upload.read_sizes_size(), read_step_limit); |
261 | 77.2k | for (std::size_t i = 0; i < read_step_count_; ++i) { |
262 | 54.0k | const std::size_t requested = upload.read_sizes(static_cast<int>(i)); |
263 | | // Zero-as-one ensures every retained step makes progress; see the schema |
264 | | // comment for why zero is not treated as an early EOF sentinel. |
265 | 54.0k | read_sizes_[i] = std::max<std::size_t>(1, std::min(requested, max_read_size_)); |
266 | 54.0k | } |
267 | 23.1k | } |
268 | | |
269 | | /// Return bytes from either the explicit payload or the allocation-free |
270 | | /// fallback. Terminal outcomes are emitted only after all data is consumed so |
271 | | /// a mutation can independently control fragmentation and completion policy. |
272 | 129k | std::size_t UploadScriptState::Read(char* buffer, std::size_t capacity) { |
273 | | // TELNET can produce negotiation replies while one curl_multi_perform call |
274 | | // owns the thread. Empty them before returning more callback bytes; |
275 | | // otherwise send_telnet_data() can consume the whole bounded transfer |
276 | | // timeout while the synchronous path waits for socket capacity. |
277 | 129k | if (before_read_callback_ != nullptr) { |
278 | 1.83k | before_read_callback_(before_read_userdata_); |
279 | 1.83k | } |
280 | 129k | if (offset_ >= total_size_) { |
281 | 11.0k | switch (terminal_) { |
282 | 358 | case curl::fuzzer::proto::UPLOAD_TERMINAL_ABORT: |
283 | 358 | return CURL_READFUNC_ABORT; |
284 | 24 | case curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE: |
285 | 24 | return CURL_READFUNC_PAUSE; |
286 | 10.6k | case curl::fuzzer::proto::UPLOAD_TERMINAL_EOF: |
287 | 10.6k | default: |
288 | 10.6k | return 0; |
289 | 11.0k | } |
290 | 11.0k | } |
291 | 118k | if (buffer == nullptr || capacity == 0) { |
292 | 0 | return 0; |
293 | 0 | } |
294 | | |
295 | 118k | std::size_t chunk_limit = std::min(capacity, max_read_size_); |
296 | 118k | if (next_read_size_ < read_step_count_) { |
297 | 105k | chunk_limit = std::min(chunk_limit, read_sizes_[next_read_size_++]); |
298 | 105k | } |
299 | 118k | const std::size_t count = std::min(chunk_limit, total_size_ - offset_); |
300 | 118k | if (scripted_) { |
301 | 112k | std::memcpy(buffer, data_.data() + offset_, count); |
302 | 112k | } else { |
303 | 5.49k | std::memset(buffer, 'U', count); |
304 | 5.49k | } |
305 | 118k | offset_ += count; |
306 | 118k | return count; |
307 | 118k | } |
308 | | |
309 | | /// Model only seeks curl can meaningfully request from a bounded memory |
310 | | /// source. Explicit range checks avoid signed overflow and keep a bogus |
311 | | /// mutation from wrapping into an in-bounds cursor. |
312 | 17.6k | int UploadScriptState::Seek(curl_off_t requested_offset, int origin) { |
313 | 17.6k | switch (seek_result_) { |
314 | 575 | case curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK: |
315 | 575 | return CURL_SEEKFUNC_CANTSEEK; |
316 | 51 | case curl::fuzzer::proto::UPLOAD_SEEK_FAIL: |
317 | 51 | return CURL_SEEKFUNC_FAIL; |
318 | 16.9k | case curl::fuzzer::proto::UPLOAD_SEEK_OK: |
319 | 16.9k | break; |
320 | 0 | default: |
321 | 0 | return CURL_SEEKFUNC_CANTSEEK; |
322 | 17.6k | } |
323 | | |
324 | 16.9k | const curl_off_t current = static_cast<curl_off_t>(offset_); |
325 | 16.9k | const curl_off_t end = static_cast<curl_off_t>(total_size_); |
326 | 16.9k | curl_off_t base = 0; |
327 | 16.9k | switch (origin) { |
328 | 16.9k | case SEEK_SET: |
329 | 16.9k | base = 0; |
330 | 16.9k | break; |
331 | 0 | case SEEK_CUR: |
332 | 0 | base = current; |
333 | 0 | break; |
334 | 0 | case SEEK_END: |
335 | 0 | base = end; |
336 | 0 | break; |
337 | 0 | default: |
338 | 0 | return CURL_SEEKFUNC_FAIL; |
339 | 16.9k | } |
340 | | |
341 | | // Both base and end are at most 16 KiB. Comparing the requested delta to |
342 | | // these small bounds before addition handles even CURL_OFF_T_MIN safely. |
343 | 16.9k | if (requested_offset < -base || requested_offset > end - base) { |
344 | 98 | return CURL_SEEKFUNC_FAIL; |
345 | 98 | } |
346 | 16.8k | offset_ = static_cast<std::size_t>(base + requested_offset); |
347 | 16.8k | next_read_size_ = 0; |
348 | 16.8k | return CURL_SEEKFUNC_OK; |
349 | 16.9k | } |
350 | | |
351 | | /// Multiplication is normally benign because curl uses size=1, but callbacks |
352 | | /// are an API boundary. Abort an impossible overflowing pair: saturating to |
353 | | /// SIZE_MAX would let Read() copy into a buffer whose real extent is unknown. |
354 | 129k | std::size_t UploadScriptState::ReadCallback(char* buffer, std::size_t size, std::size_t nitems, void* userdata) { |
355 | 129k | if (userdata == nullptr || size == 0 || nitems == 0) { |
356 | 0 | return 0; |
357 | 0 | } |
358 | 129k | const std::size_t max = std::numeric_limits<std::size_t>::max(); |
359 | 129k | if (nitems > max / size) { |
360 | 0 | return CURL_READFUNC_ABORT; |
361 | 0 | } |
362 | 129k | return static_cast<UploadScriptState*>(userdata)->Read(buffer, size * nitems); |
363 | 129k | } |
364 | | |
365 | | /// Keep the C callback a one-line type bridge so all outcome/cursor behaviour |
366 | | /// remains directly unit-testable in Seek(). |
367 | 17.6k | int UploadScriptState::SeekCallback(void* userdata, curl_off_t offset, int origin) { |
368 | 17.6k | if (userdata == nullptr) { |
369 | 0 | return CURL_SEEKFUNC_FAIL; |
370 | 0 | } |
371 | 17.6k | return static_cast<UploadScriptState*>(userdata)->Seek(offset, origin); |
372 | 17.6k | } |
373 | | |
374 | 0 | std::size_t UploadScriptState::data_size() const { return total_size_; } |
375 | | |
376 | 0 | std::size_t UploadScriptState::read_step_count() const { return read_step_count_; } |
377 | | |
378 | 0 | std::size_t UploadScriptState::offset() const { return offset_; } |
379 | | |
380 | 0 | bool UploadScriptState::scripted() const { return scripted_; } |
381 | | |
382 | 4.83k | void UploadScriptState::SetBeforeReadCallback(BeforeReadCallback callback, void* userdata) { |
383 | 4.83k | before_read_callback_ = callback; |
384 | 4.83k | before_read_userdata_ = userdata; |
385 | 4.83k | } |
386 | | |
387 | | /// Build the protocol-specific pointer-valued request features and attach them |
388 | | /// to the easy handle. Setup errors are deliberately non-fatal: malformed or |
389 | | /// partially allocated scenarios should still exercise whatever curl state |
390 | | /// was built. |
391 | | ScenarioRequestData::ScenarioRequestData(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, |
392 | | bool apply_resolve_entries) |
393 | 326k | : easy_(easy), |
394 | 326k | request_headers_(nullptr), |
395 | 326k | resolve_entries_(nullptr), |
396 | 326k | telnet_options_(nullptr), |
397 | 326k | mime_post_(nullptr), |
398 | 326k | upload_state_(scenario), |
399 | 326k | upload_callbacks_installed_(false), |
400 | 326k | resolve_entries_ready_(!apply_resolve_entries) { |
401 | 326k | if (easy_ == nullptr) { |
402 | 0 | return; |
403 | 0 | } |
404 | | |
405 | 326k | if (apply_resolve_entries) { |
406 | 12.5k | resolve_entries_ = BuildStringList(scenario.resolve_entries(), scenario_limits::kMaxResolveEntries, |
407 | 12.5k | scenario_limits::kMaxResolveEntryBytes, &stats_.resolve_entries); |
408 | | // This mapping is deliberately last: preceding wildcard or removal |
409 | | // mutations may exercise host-cache parsing, but cannot redirect the |
410 | | // canonical resolver-lane origin away from the in-process mock. |
411 | 12.5k | curl_slist* appended = curl_slist_append(resolve_entries_, "resolve.test:80:127.0.0.1"); |
412 | 12.5k | if (appended != nullptr) { |
413 | 12.5k | resolve_entries_ = appended; |
414 | 12.5k | resolve_entries_ready_ = curl_easy_setopt(easy_, CURLOPT_RESOLVE, resolve_entries_) == CURLE_OK; |
415 | 12.5k | } |
416 | 12.5k | } |
417 | | |
418 | 326k | if (NeedsUploadCallbacks(scenario)) { |
419 | | // Install a per-run memory source even when Scenario.upload is absent but |
420 | | // CURLOPT_UPLOAD or TELNET may request caller input. Non-TELNET schemes |
421 | | // retain the historical fallback bytes; TELNET returns EOF. Either result |
422 | | // replaces stdin and cannot block OSS-Fuzz. The state remains scoped to |
423 | | // the complete drive so retries cannot share a cursor across iterations. |
424 | 41.2k | upload_callbacks_installed_ = true; |
425 | 41.2k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, &UploadScriptState::ReadCallback); |
426 | 41.2k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, &upload_state_); |
427 | 41.2k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, &UploadScriptState::SeekCallback); |
428 | 41.2k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, &upload_state_); |
429 | 41.2k | } |
430 | | |
431 | | // HTTP headers/MIME and TELNET options are mutually exclusive because only |
432 | | // the selected protocol can observe them. Avoid allocating protocol-inert |
433 | | // lists and trees in compatibility inputs that bypass target policy. |
434 | 326k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET) { |
435 | 4.83k | telnet_options_ = BuildStringList(scenario.telnet_options(), scenario_limits::kMaxTelnetOptions, |
436 | 4.83k | scenario_limits::kMaxTelnetOptionBytes, &stats_.telnet_options); |
437 | 4.83k | if (telnet_options_ != nullptr) { |
438 | 489 | (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, telnet_options_); |
439 | 489 | } |
440 | 321k | } else { |
441 | 321k | request_headers_ = BuildStringList(scenario.request_headers(), scenario_limits::kMaxRequestHeaders, |
442 | 321k | scenario_limits::kMaxMetadataBytes, &stats_.request_headers); |
443 | 321k | if (request_headers_ != nullptr) { |
444 | 50.9k | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, request_headers_); |
445 | 50.9k | } |
446 | | |
447 | 321k | if (scenario.has_mime_post()) { |
448 | 45.2k | mime_post_ = BuildMimePost(easy_, scenario.mime_post(), &stats_); |
449 | 45.2k | if (mime_post_ != nullptr) { |
450 | 45.2k | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, mime_post_); |
451 | 45.2k | } |
452 | 45.2k | } |
453 | 321k | } |
454 | 326k | } |
455 | | |
456 | | /// Detach resources while the easy handle is valid, then free them. libcurl |
457 | | /// does not copy headers, MIME roots, or callback userdata, so releasing any |
458 | | /// one before the mock drive ends would create a use-after-free; relying on |
459 | | /// easy cleanup to own header/MIME allocations would instead leak iterations. |
460 | 326k | ScenarioRequestData::~ScenarioRequestData() { |
461 | 326k | if (easy_ != nullptr) { |
462 | | // Clear callbacks before their userdata member is destroyed. There is no |
463 | | // perform in this destructor, but making the handle non-dangling keeps the |
464 | | // ownership rule robust if cleanup later gains diagnostics or getinfo. |
465 | 326k | if (upload_callbacks_installed_) { |
466 | 41.2k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, nullptr); |
467 | 41.2k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, nullptr); |
468 | 41.2k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, nullptr); |
469 | 41.2k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, nullptr); |
470 | 41.2k | } |
471 | 326k | if (mime_post_ != nullptr) { |
472 | 45.2k | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, nullptr); |
473 | 45.2k | } |
474 | 326k | if (request_headers_ != nullptr) { |
475 | 50.9k | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, nullptr); |
476 | 50.9k | } |
477 | 326k | if (resolve_entries_ != nullptr) { |
478 | 12.5k | (void)curl_easy_setopt(easy_, CURLOPT_RESOLVE, nullptr); |
479 | 12.5k | } |
480 | 326k | if (telnet_options_ != nullptr) { |
481 | 489 | (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, nullptr); |
482 | 489 | } |
483 | 326k | } |
484 | 326k | curl_mime_free(mime_post_); |
485 | 326k | curl_slist_free_all(telnet_options_); |
486 | 326k | curl_slist_free_all(resolve_entries_); |
487 | 326k | curl_slist_free_all(request_headers_); |
488 | 326k | } |
489 | | |
490 | | /// Expose cap-aware counts without exposing or transferring the owned curl |
491 | | /// pointers themselves. |
492 | 0 | const RequestBuildStats& ScenarioRequestData::stats() const { return stats_; } |
493 | | |
494 | 0 | const UploadScriptState& ScenarioRequestData::upload_state() const { return upload_state_; } |
495 | | |
496 | 0 | bool ScenarioRequestData::upload_callbacks_installed() const { return upload_callbacks_installed_; } |
497 | | |
498 | 12.5k | bool ScenarioRequestData::resolve_entries_ready() const { return resolve_entries_ready_; } |
499 | | |
500 | 4.83k | void ScenarioRequestData::SetBeforeUploadReadCallback(UploadScriptState::BeforeReadCallback callback, void* userdata) { |
501 | 4.83k | upload_state_.SetBeforeReadCallback(callback, userdata); |
502 | 4.83k | } |
503 | | |
504 | | } // namespace proto_fuzzer |