/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 | 261k | const char* BoundedCString(const std::string& value, std::size_t limit, std::string* truncated) { |
33 | 261k | if (value.size() <= limit) { |
34 | 260k | return value.c_str(); |
35 | 260k | } |
36 | 178 | truncated->assign(value.data(), limit); |
37 | 178 | return truncated->c_str(); |
38 | 261k | } |
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 | 137k | 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 | 137k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET || scenario.has_upload()) { |
51 | 20.2k | return true; |
52 | 20.2k | } |
53 | 116k | const std::size_t option_count = RuntimeOptionCount(scenario); |
54 | 413k | for (std::size_t index = 0; index < option_count; ++index) { |
55 | 305k | const auto& option = scenario.options(static_cast<int>(index)); |
56 | 305k | if (option.option_id() == curl::fuzzer::proto::CURLOPT_UPLOAD || |
57 | 299k | option.option_id() == curl::fuzzer::proto::CURLOPT_POST) { |
58 | 8.59k | return true; |
59 | 8.59k | } |
60 | 305k | } |
61 | 108k | return false; |
62 | 116k | } |
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 | 87.1k | const char* MimeEncoderName(curl::fuzzer::proto::MimeEncoder encoder) { |
68 | 87.1k | switch (encoder) { |
69 | 7.08k | case curl::fuzzer::proto::MIME_ENCODER_BINARY: |
70 | 7.08k | return "binary"; |
71 | 6.01k | case curl::fuzzer::proto::MIME_ENCODER_8BIT: |
72 | 6.01k | return "8bit"; |
73 | 7.35k | case curl::fuzzer::proto::MIME_ENCODER_7BIT: |
74 | 7.35k | return "7bit"; |
75 | 15.8k | case curl::fuzzer::proto::MIME_ENCODER_BASE64: |
76 | 15.8k | return "base64"; |
77 | 16.6k | case curl::fuzzer::proto::MIME_ENCODER_QUOTED_PRINTABLE: |
78 | 16.6k | return "quoted-printable"; |
79 | 34.1k | case curl::fuzzer::proto::MIME_ENCODER_UNSPECIFIED: |
80 | 34.1k | default: |
81 | 34.1k | return nullptr; |
82 | 87.1k | } |
83 | 87.1k | } |
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 | 224k | std::size_t* applied) { |
92 | 224k | curl_slist* list = nullptr; |
93 | 224k | std::string truncated; |
94 | 224k | const std::size_t count = std::min<std::size_t>(count_limit, values.size()); |
95 | 363k | for (std::size_t i = 0; i < count; ++i) { |
96 | 139k | const std::string& value = values.Get(static_cast<int>(i)); |
97 | 139k | curl_slist* appended = curl_slist_append(list, BoundedCString(value, value_limit, &truncated)); |
98 | 139k | if (appended == nullptr) { |
99 | 0 | break; |
100 | 0 | } |
101 | 139k | list = appended; |
102 | 139k | ++*applied; |
103 | 139k | } |
104 | 224k | return list; |
105 | 224k | } |
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 | 87.1k | 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 | 87.1k | std::string truncated; |
115 | 87.1k | if (!source.name().empty()) { |
116 | 39.5k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); |
117 | 39.5k | } |
118 | 87.1k | if (!source.filename().empty()) { |
119 | 41.4k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); |
120 | 41.4k | } |
121 | 87.1k | if (!source.content_type().empty()) { |
122 | 40.8k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); |
123 | 40.8k | } |
124 | 87.1k | if (const char* encoder = MimeEncoderName(source.encoder())) { |
125 | 52.9k | (void)curl_mime_encoder(part, encoder); |
126 | 52.9k | } |
127 | | |
128 | 87.1k | std::size_t header_count = 0; |
129 | 87.1k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, |
130 | 87.1k | scenario_limits::kMaxMetadataBytes, &header_count); |
131 | 87.1k | 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 | 33.0k | const CURLcode result = curl_mime_headers(part, headers, 1); |
136 | 33.0k | if (result == CURLE_OK) { |
137 | 33.0k | stats->mime_headers += header_count; |
138 | 33.0k | } else { |
139 | 0 | curl_slist_free_all(headers); |
140 | 0 | } |
141 | 33.0k | } |
142 | 87.1k | } 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 | 36.0k | 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 | 36.0k | std::string truncated; | 115 | 36.0k | if (!source.name().empty()) { | 116 | 14.6k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); | 117 | 14.6k | } | 118 | 36.0k | if (!source.filename().empty()) { | 119 | 15.3k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); | 120 | 15.3k | } | 121 | 36.0k | if (!source.content_type().empty()) { | 122 | 13.6k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); | 123 | 13.6k | } | 124 | 36.0k | if (const char* encoder = MimeEncoderName(source.encoder())) { | 125 | 21.2k | (void)curl_mime_encoder(part, encoder); | 126 | 21.2k | } | 127 | | | 128 | 36.0k | std::size_t header_count = 0; | 129 | 36.0k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, | 130 | 36.0k | scenario_limits::kMaxMetadataBytes, &header_count); | 131 | 36.0k | 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 | 12.1k | const CURLcode result = curl_mime_headers(part, headers, 1); | 136 | 12.1k | if (result == CURLE_OK) { | 137 | 12.1k | stats->mime_headers += header_count; | 138 | 12.1k | } else { | 139 | 0 | curl_slist_free_all(headers); | 140 | 0 | } | 141 | 12.1k | } | 142 | 36.0k | } |
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 | 51.0k | 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 | 51.0k | std::string truncated; | 115 | 51.0k | if (!source.name().empty()) { | 116 | 24.8k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); | 117 | 24.8k | } | 118 | 51.0k | if (!source.filename().empty()) { | 119 | 26.1k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); | 120 | 26.1k | } | 121 | 51.0k | if (!source.content_type().empty()) { | 122 | 27.1k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); | 123 | 27.1k | } | 124 | 51.0k | if (const char* encoder = MimeEncoderName(source.encoder())) { | 125 | 31.6k | (void)curl_mime_encoder(part, encoder); | 126 | 31.6k | } | 127 | | | 128 | 51.0k | std::size_t header_count = 0; | 129 | 51.0k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, | 130 | 51.0k | scenario_limits::kMaxMetadataBytes, &header_count); | 131 | 51.0k | 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 | 20.9k | const CURLcode result = curl_mime_headers(part, headers, 1); | 136 | 20.9k | if (result == CURLE_OK) { | 137 | 20.9k | stats->mime_headers += header_count; | 138 | 20.9k | } else { | 139 | 0 | curl_slist_free_all(headers); | 140 | 0 | } | 141 | 20.9k | } | 142 | 51.0k | } |
|
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 | 57.0k | void ApplyPartData(curl_mimepart* part, const std::string& data) { |
148 | 57.0k | const std::size_t size = std::min(data.size(), scenario_limits::kMaxMimeDataBytes); |
149 | 57.0k | const char* bytes = data.empty() ? "" : data.data(); |
150 | 57.0k | (void)curl_mime_data(part, bytes, size); |
151 | 57.0k | } |
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 | 19.5k | RequestBuildStats* stats) { |
158 | 19.5k | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxNestedMimeParts, source.parts_size()); |
159 | 70.6k | for (std::size_t i = 0; i < count && *remaining_parts != 0; ++i) { |
160 | 51.0k | curl_mimepart* part = curl_mime_addpart(mime); |
161 | 51.0k | if (part == nullptr) { |
162 | 0 | break; |
163 | 0 | } |
164 | 51.0k | --*remaining_parts; |
165 | 51.0k | ++stats->mime_parts; |
166 | 51.0k | const auto& proto_part = source.parts(static_cast<int>(i)); |
167 | 51.0k | ApplyPartMetadata(part, proto_part, stats); |
168 | 51.0k | ApplyPartData(part, proto_part.data()); |
169 | 51.0k | } |
170 | 19.5k | } |
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 | 20.3k | curl_mime* BuildMimePost(CURL* easy, const curl::fuzzer::proto::MimePost& source, RequestBuildStats* stats) { |
176 | 20.3k | curl_mime* mime = curl_mime_init(easy); |
177 | 20.3k | if (mime == nullptr) { |
178 | 0 | return nullptr; |
179 | 0 | } |
180 | | |
181 | 20.3k | std::size_t remaining_parts = scenario_limits::kMaxTotalMimeParts; |
182 | 20.3k | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxTopLevelMimeParts, source.parts_size()); |
183 | 56.4k | for (std::size_t i = 0; i < count && remaining_parts != 0; ++i) { |
184 | 36.0k | curl_mimepart* part = curl_mime_addpart(mime); |
185 | 36.0k | if (part == nullptr) { |
186 | 0 | break; |
187 | 0 | } |
188 | 36.0k | --remaining_parts; |
189 | 36.0k | ++stats->mime_parts; |
190 | 36.0k | const auto& proto_part = source.parts(static_cast<int>(i)); |
191 | 36.0k | ApplyPartMetadata(part, proto_part, stats); |
192 | | |
193 | 36.0k | switch (proto_part.content_case()) { |
194 | 5.99k | case curl::fuzzer::proto::MimePart::kData: |
195 | 5.99k | ApplyPartData(part, proto_part.data()); |
196 | 5.99k | break; |
197 | 19.5k | case curl::fuzzer::proto::MimePart::kSubparts: { |
198 | 19.5k | curl_mime* subparts = curl_mime_init(easy); |
199 | 19.5k | if (subparts == nullptr) { |
200 | 0 | break; |
201 | 0 | } |
202 | 19.5k | PopulateSubparts(subparts, proto_part.subparts(), &remaining_parts, stats); |
203 | 19.5k | if (curl_mime_subparts(part, subparts) != CURLE_OK) { |
204 | 0 | curl_mime_free(subparts); |
205 | 0 | } |
206 | 19.5k | break; |
207 | 19.5k | } |
208 | 10.5k | case curl::fuzzer::proto::MimePart::CONTENT_NOT_SET: |
209 | 10.5k | default: |
210 | 10.5k | break; |
211 | 36.0k | } |
212 | 36.0k | } |
213 | 20.3k | return mime; |
214 | 20.3k | } |
215 | | |
216 | | } // namespace |
217 | | |
218 | | /// Borrow and cap the immutable upload shape once, before libcurl receives a |
219 | | /// userdata pointer. ScenarioRunner 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 | 137k | : data_(), |
226 | 137k | read_step_count_(0), |
227 | 137k | total_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? 0 : scenario_limits::kMaxUploadBytes), |
228 | 137k | max_read_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? scenario_limits::kMaxTelnetUploadReadSize |
229 | 137k | : scenario_limits::kMaxUploadReadSize), |
230 | 137k | offset_(0), |
231 | 137k | next_read_size_(0), |
232 | 137k | terminal_(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF), |
233 | 137k | seek_result_(curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK), |
234 | 137k | before_read_callback_(nullptr), |
235 | 137k | before_read_userdata_(nullptr), |
236 | 137k | scripted_(scenario.has_upload()) { |
237 | 137k | if (!scripted_) { |
238 | 121k | return; |
239 | 121k | } |
240 | | |
241 | 15.6k | const auto& upload = scenario.upload(); |
242 | 15.6k | const std::size_t data_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET |
243 | 15.6k | ? scenario_limits::kMaxTelnetUploadBytes |
244 | 15.6k | : scenario_limits::kMaxUploadBytes; |
245 | 15.6k | const std::size_t data_size = std::min(upload.data().size(), data_limit); |
246 | 15.6k | data_ = std::string_view(upload.data().data(), data_size); |
247 | 15.6k | total_size_ = data_.size(); |
248 | 15.6k | terminal_ = upload.terminal(); |
249 | 15.6k | if (scenario.scheme() != curl::fuzzer::proto::SCHEME_TELNET && |
250 | 15.3k | 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 | 144 | terminal_ = curl::fuzzer::proto::UPLOAD_TERMINAL_EOF; |
254 | 144 | } |
255 | 15.6k | seek_result_ = upload.seek_result(); |
256 | | |
257 | 15.6k | const std::size_t read_step_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET |
258 | 15.6k | ? scenario_limits::kMaxTelnetUploadReadSteps |
259 | 15.6k | : scenario_limits::kMaxUploadReadSteps; |
260 | 15.6k | read_step_count_ = std::min<std::size_t>(upload.read_sizes_size(), read_step_limit); |
261 | 38.3k | for (std::size_t i = 0; i < read_step_count_; ++i) { |
262 | 22.6k | 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 | 22.6k | read_sizes_[i] = std::max<std::size_t>(1, std::min(requested, max_read_size_)); |
266 | 22.6k | } |
267 | 15.6k | } |
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 | 20.7k | 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 | 20.7k | if (before_read_callback_ != nullptr) { |
278 | 1.67k | before_read_callback_(before_read_userdata_); |
279 | 1.67k | } |
280 | 20.7k | if (offset_ >= total_size_) { |
281 | 5.89k | switch (terminal_) { |
282 | 149 | case curl::fuzzer::proto::UPLOAD_TERMINAL_ABORT: |
283 | 149 | return CURL_READFUNC_ABORT; |
284 | 29 | case curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE: |
285 | 29 | return CURL_READFUNC_PAUSE; |
286 | 5.71k | case curl::fuzzer::proto::UPLOAD_TERMINAL_EOF: |
287 | 5.72k | default: |
288 | 5.72k | return 0; |
289 | 5.89k | } |
290 | 5.89k | } |
291 | 14.8k | if (buffer == nullptr || capacity == 0) { |
292 | 0 | return 0; |
293 | 0 | } |
294 | | |
295 | 14.8k | std::size_t chunk_limit = std::min(capacity, max_read_size_); |
296 | 14.8k | if (next_read_size_ < read_step_count_) { |
297 | 8.47k | chunk_limit = std::min(chunk_limit, read_sizes_[next_read_size_++]); |
298 | 8.47k | } |
299 | 14.8k | const std::size_t count = std::min(chunk_limit, total_size_ - offset_); |
300 | 14.8k | if (scripted_) { |
301 | 10.4k | std::memcpy(buffer, data_.data() + offset_, count); |
302 | 10.4k | } else { |
303 | 4.41k | std::memset(buffer, 'U', count); |
304 | 4.41k | } |
305 | 14.8k | offset_ += count; |
306 | 14.8k | return count; |
307 | 14.8k | } |
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 | 4.96k | int UploadScriptState::Seek(curl_off_t requested_offset, int origin) { |
313 | 4.96k | switch (seek_result_) { |
314 | 1.34k | case curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK: |
315 | 1.34k | return CURL_SEEKFUNC_CANTSEEK; |
316 | 18 | case curl::fuzzer::proto::UPLOAD_SEEK_FAIL: |
317 | 18 | return CURL_SEEKFUNC_FAIL; |
318 | 3.60k | case curl::fuzzer::proto::UPLOAD_SEEK_OK: |
319 | 3.60k | break; |
320 | 0 | default: |
321 | 0 | return CURL_SEEKFUNC_CANTSEEK; |
322 | 4.96k | } |
323 | | |
324 | 3.60k | const curl_off_t current = static_cast<curl_off_t>(offset_); |
325 | 3.60k | const curl_off_t end = static_cast<curl_off_t>(total_size_); |
326 | 3.60k | curl_off_t base = 0; |
327 | 3.60k | switch (origin) { |
328 | 3.60k | case SEEK_SET: |
329 | 3.60k | base = 0; |
330 | 3.60k | 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 | 3.60k | } |
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 | 3.60k | if (requested_offset < -base || requested_offset > end - base) { |
344 | 737 | return CURL_SEEKFUNC_FAIL; |
345 | 737 | } |
346 | 2.86k | offset_ = static_cast<std::size_t>(base + requested_offset); |
347 | 2.86k | next_read_size_ = 0; |
348 | 2.86k | return CURL_SEEKFUNC_OK; |
349 | 3.60k | } |
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 | 20.7k | std::size_t UploadScriptState::ReadCallback(char* buffer, std::size_t size, std::size_t nitems, void* userdata) { |
355 | 20.7k | if (userdata == nullptr || size == 0 || nitems == 0) { |
356 | 0 | return 0; |
357 | 0 | } |
358 | 20.7k | const std::size_t max = std::numeric_limits<std::size_t>::max(); |
359 | 20.7k | if (nitems > max / size) { |
360 | 0 | return CURL_READFUNC_ABORT; |
361 | 0 | } |
362 | 20.7k | return static_cast<UploadScriptState*>(userdata)->Read(buffer, size * nitems); |
363 | 20.7k | } |
364 | | |
365 | | /// Keep the C callback a one-line type bridge so all outcome/cursor behaviour |
366 | | /// remains directly unit-testable in Seek(). |
367 | 4.96k | int UploadScriptState::SeekCallback(void* userdata, curl_off_t offset, int origin) { |
368 | 4.96k | if (userdata == nullptr) { |
369 | 0 | return CURL_SEEKFUNC_FAIL; |
370 | 0 | } |
371 | 4.96k | return static_cast<UploadScriptState*>(userdata)->Seek(offset, origin); |
372 | 4.96k | } |
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.87k | void UploadScriptState::SetBeforeReadCallback(BeforeReadCallback callback, void* userdata) { |
383 | 4.87k | before_read_callback_ = callback; |
384 | 4.87k | before_read_userdata_ = userdata; |
385 | 4.87k | } |
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 | 137k | : easy_(easy), |
393 | 137k | request_headers_(nullptr), |
394 | 137k | telnet_options_(nullptr), |
395 | 137k | mime_post_(nullptr), |
396 | 137k | upload_state_(scenario), |
397 | 137k | upload_callbacks_installed_(false) { |
398 | 137k | if (easy_ == nullptr) { |
399 | 0 | return; |
400 | 0 | } |
401 | | |
402 | 137k | if (NeedsUploadCallbacks(scenario)) { |
403 | | // Install a per-run memory source even when Scenario.upload is absent but |
404 | | // CURLOPT_UPLOAD or TELNET may request caller input. Non-TELNET schemes |
405 | | // retain the historical fallback bytes; TELNET returns EOF. Either result |
406 | | // replaces stdin and cannot block OSS-Fuzz. The state remains scoped to |
407 | | // the complete drive so retries cannot share a cursor across iterations. |
408 | 28.8k | upload_callbacks_installed_ = true; |
409 | 28.8k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, &UploadScriptState::ReadCallback); |
410 | 28.8k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, &upload_state_); |
411 | 28.8k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, &UploadScriptState::SeekCallback); |
412 | 28.8k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, &upload_state_); |
413 | 28.8k | } |
414 | | |
415 | | // HTTP headers/MIME and TELNET options are mutually exclusive because only |
416 | | // the selected protocol can observe them. Avoid allocating protocol-inert |
417 | | // lists and trees in compatibility inputs that bypass target policy. |
418 | 137k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET) { |
419 | 4.87k | telnet_options_ = BuildStringList(scenario.telnet_options(), scenario_limits::kMaxTelnetOptions, |
420 | 4.87k | scenario_limits::kMaxTelnetOptionBytes, &stats_.telnet_options); |
421 | 4.87k | if (telnet_options_ != nullptr) { |
422 | 563 | (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, telnet_options_); |
423 | 563 | } |
424 | 132k | } else { |
425 | 132k | request_headers_ = BuildStringList(scenario.request_headers(), scenario_limits::kMaxRequestHeaders, |
426 | 132k | scenario_limits::kMaxMetadataBytes, &stats_.request_headers); |
427 | 132k | if (request_headers_ != nullptr) { |
428 | 27.6k | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, request_headers_); |
429 | 27.6k | } |
430 | | |
431 | 132k | if (scenario.has_mime_post()) { |
432 | 20.3k | mime_post_ = BuildMimePost(easy_, scenario.mime_post(), &stats_); |
433 | 20.3k | if (mime_post_ != nullptr) { |
434 | 20.3k | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, mime_post_); |
435 | 20.3k | } |
436 | 20.3k | } |
437 | 132k | } |
438 | 137k | } |
439 | | |
440 | | /// Detach resources while the easy handle is valid, then free them. libcurl |
441 | | /// does not copy headers, MIME roots, or callback userdata, so releasing any |
442 | | /// one before the mock drive ends would create a use-after-free; relying on |
443 | | /// easy cleanup to own header/MIME allocations would instead leak iterations. |
444 | 137k | ScenarioRequestData::~ScenarioRequestData() { |
445 | 137k | if (easy_ != nullptr) { |
446 | | // Clear callbacks before their userdata member is destroyed. There is no |
447 | | // perform in this destructor, but making the handle non-dangling keeps the |
448 | | // ownership rule robust if cleanup later gains diagnostics or getinfo. |
449 | 137k | if (upload_callbacks_installed_) { |
450 | 28.8k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, nullptr); |
451 | 28.8k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, nullptr); |
452 | 28.8k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, nullptr); |
453 | 28.8k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, nullptr); |
454 | 28.8k | } |
455 | 137k | if (mime_post_ != nullptr) { |
456 | 20.3k | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, nullptr); |
457 | 20.3k | } |
458 | 137k | if (request_headers_ != nullptr) { |
459 | 27.6k | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, nullptr); |
460 | 27.6k | } |
461 | 137k | if (telnet_options_ != nullptr) { |
462 | 563 | (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, nullptr); |
463 | 563 | } |
464 | 137k | } |
465 | 137k | curl_mime_free(mime_post_); |
466 | 137k | curl_slist_free_all(telnet_options_); |
467 | 137k | curl_slist_free_all(request_headers_); |
468 | 137k | } |
469 | | |
470 | | /// Expose cap-aware counts without exposing or transferring the owned curl |
471 | | /// pointers themselves. |
472 | 0 | const RequestBuildStats& ScenarioRequestData::stats() const { return stats_; } |
473 | | |
474 | 0 | const UploadScriptState& ScenarioRequestData::upload_state() const { return upload_state_; } |
475 | | |
476 | 0 | bool ScenarioRequestData::upload_callbacks_installed() const { return upload_callbacks_installed_; } |
477 | | |
478 | 4.87k | void ScenarioRequestData::SetBeforeUploadReadCallback(UploadScriptState::BeforeReadCallback callback, void* userdata) { |
479 | 4.87k | upload_state_.SetBeforeReadCallback(callback, userdata); |
480 | 4.87k | } |
481 | | |
482 | | } // namespace proto_fuzzer |