/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 | 77.4k | const char* BoundedCString(const std::string& value, std::size_t limit, std::string* truncated) { |
33 | 77.4k | if (value.size() <= limit) { |
34 | 77.3k | return value.c_str(); |
35 | 77.3k | } |
36 | 132 | truncated->assign(value.data(), limit); |
37 | 132 | return truncated->c_str(); |
38 | 77.4k | } |
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 | 38.0k | 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 | 38.0k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET || scenario.has_upload()) { |
51 | 7.07k | return true; |
52 | 7.07k | } |
53 | 30.9k | const std::size_t option_count = RuntimeOptionCount(scenario); |
54 | 91.1k | for (std::size_t index = 0; index < option_count; ++index) { |
55 | 63.2k | const auto& option = scenario.options(static_cast<int>(index)); |
56 | 63.2k | if (option.option_id() == curl::fuzzer::proto::CURLOPT_UPLOAD || |
57 | 61.3k | option.option_id() == curl::fuzzer::proto::CURLOPT_POST) { |
58 | 3.04k | return true; |
59 | 3.04k | } |
60 | 63.2k | } |
61 | 27.9k | return false; |
62 | 30.9k | } |
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 | 24.3k | const char* MimeEncoderName(curl::fuzzer::proto::MimeEncoder encoder) { |
68 | 24.3k | switch (encoder) { |
69 | 1.66k | case curl::fuzzer::proto::MIME_ENCODER_BINARY: |
70 | 1.66k | return "binary"; |
71 | 2.35k | case curl::fuzzer::proto::MIME_ENCODER_8BIT: |
72 | 2.35k | return "8bit"; |
73 | 2.33k | case curl::fuzzer::proto::MIME_ENCODER_7BIT: |
74 | 2.33k | return "7bit"; |
75 | 4.04k | case curl::fuzzer::proto::MIME_ENCODER_BASE64: |
76 | 4.04k | return "base64"; |
77 | 3.84k | case curl::fuzzer::proto::MIME_ENCODER_QUOTED_PRINTABLE: |
78 | 3.84k | return "quoted-printable"; |
79 | 10.0k | case curl::fuzzer::proto::MIME_ENCODER_UNSPECIFIED: |
80 | 10.0k | default: |
81 | 10.0k | return nullptr; |
82 | 24.3k | } |
83 | 24.3k | } |
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 | 62.3k | std::size_t* applied) { |
92 | 62.3k | curl_slist* list = nullptr; |
93 | 62.3k | std::string truncated; |
94 | 62.3k | const std::size_t count = std::min<std::size_t>(count_limit, values.size()); |
95 | 104k | for (std::size_t i = 0; i < count; ++i) { |
96 | 41.8k | const std::string& value = values.Get(static_cast<int>(i)); |
97 | 41.8k | curl_slist* appended = curl_slist_append(list, BoundedCString(value, value_limit, &truncated)); |
98 | 41.8k | if (appended == nullptr) { |
99 | 0 | break; |
100 | 0 | } |
101 | 41.8k | list = appended; |
102 | 41.8k | ++*applied; |
103 | 41.8k | } |
104 | 62.3k | return list; |
105 | 62.3k | } |
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 | 24.3k | 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 | 24.3k | std::string truncated; |
115 | 24.3k | if (!source.name().empty()) { |
116 | 12.1k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); |
117 | 12.1k | } |
118 | 24.3k | if (!source.filename().empty()) { |
119 | 11.0k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); |
120 | 11.0k | } |
121 | 24.3k | if (!source.content_type().empty()) { |
122 | 12.3k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); |
123 | 12.3k | } |
124 | 24.3k | if (const char* encoder = MimeEncoderName(source.encoder())) { |
125 | 14.2k | (void)curl_mime_encoder(part, encoder); |
126 | 14.2k | } |
127 | | |
128 | 24.3k | std::size_t header_count = 0; |
129 | 24.3k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, |
130 | 24.3k | scenario_limits::kMaxMetadataBytes, &header_count); |
131 | 24.3k | 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 | 10.0k | const CURLcode result = curl_mime_headers(part, headers, 1); |
136 | 10.0k | if (result == CURLE_OK) { |
137 | 10.0k | stats->mime_headers += header_count; |
138 | 10.0k | } else { |
139 | 0 | curl_slist_free_all(headers); |
140 | 0 | } |
141 | 10.0k | } |
142 | 24.3k | } 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 | 10.3k | 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 | 10.3k | std::string truncated; | 115 | 10.3k | if (!source.name().empty()) { | 116 | 4.78k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); | 117 | 4.78k | } | 118 | 10.3k | if (!source.filename().empty()) { | 119 | 4.21k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); | 120 | 4.21k | } | 121 | 10.3k | if (!source.content_type().empty()) { | 122 | 4.42k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); | 123 | 4.42k | } | 124 | 10.3k | if (const char* encoder = MimeEncoderName(source.encoder())) { | 125 | 5.96k | (void)curl_mime_encoder(part, encoder); | 126 | 5.96k | } | 127 | | | 128 | 10.3k | std::size_t header_count = 0; | 129 | 10.3k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, | 130 | 10.3k | scenario_limits::kMaxMetadataBytes, &header_count); | 131 | 10.3k | 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 | 3.94k | const CURLcode result = curl_mime_headers(part, headers, 1); | 136 | 3.94k | if (result == CURLE_OK) { | 137 | 3.94k | stats->mime_headers += header_count; | 138 | 3.94k | } else { | 139 | 0 | curl_slist_free_all(headers); | 140 | 0 | } | 141 | 3.94k | } | 142 | 10.3k | } |
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 | 14.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 | 14.0k | std::string truncated; | 115 | 14.0k | if (!source.name().empty()) { | 116 | 7.36k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); | 117 | 7.36k | } | 118 | 14.0k | if (!source.filename().empty()) { | 119 | 6.86k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); | 120 | 6.86k | } | 121 | 14.0k | if (!source.content_type().empty()) { | 122 | 7.93k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); | 123 | 7.93k | } | 124 | 14.0k | if (const char* encoder = MimeEncoderName(source.encoder())) { | 125 | 8.29k | (void)curl_mime_encoder(part, encoder); | 126 | 8.29k | } | 127 | | | 128 | 14.0k | std::size_t header_count = 0; | 129 | 14.0k | curl_slist* headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, | 130 | 14.0k | scenario_limits::kMaxMetadataBytes, &header_count); | 131 | 14.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 | 6.07k | const CURLcode result = curl_mime_headers(part, headers, 1); | 136 | 6.07k | if (result == CURLE_OK) { | 137 | 6.07k | stats->mime_headers += header_count; | 138 | 6.07k | } else { | 139 | 0 | curl_slist_free_all(headers); | 140 | 0 | } | 141 | 6.07k | } | 142 | 14.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 | 15.8k | void ApplyPartData(curl_mimepart* part, const std::string& data) { |
148 | 15.8k | const std::size_t size = std::min(data.size(), scenario_limits::kMaxMimeDataBytes); |
149 | 15.8k | const char* bytes = data.empty() ? "" : data.data(); |
150 | 15.8k | (void)curl_mime_data(part, bytes, size); |
151 | 15.8k | } |
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 | 5.55k | RequestBuildStats* stats) { |
158 | 5.55k | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxNestedMimeParts, source.parts_size()); |
159 | 19.5k | for (std::size_t i = 0; i < count && *remaining_parts != 0; ++i) { |
160 | 14.0k | curl_mimepart* part = curl_mime_addpart(mime); |
161 | 14.0k | if (part == nullptr) { |
162 | 0 | break; |
163 | 0 | } |
164 | 14.0k | --*remaining_parts; |
165 | 14.0k | ++stats->mime_parts; |
166 | 14.0k | const auto& proto_part = source.parts(static_cast<int>(i)); |
167 | 14.0k | ApplyPartMetadata(part, proto_part, stats); |
168 | 14.0k | ApplyPartData(part, proto_part.data()); |
169 | 14.0k | } |
170 | 5.55k | } |
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 | 5.14k | curl_mime* BuildMimePost(CURL* easy, const curl::fuzzer::proto::MimePost& source, RequestBuildStats* stats) { |
176 | 5.14k | curl_mime* mime = curl_mime_init(easy); |
177 | 5.14k | if (mime == nullptr) { |
178 | 0 | return nullptr; |
179 | 0 | } |
180 | | |
181 | 5.14k | std::size_t remaining_parts = scenario_limits::kMaxTotalMimeParts; |
182 | 5.14k | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxTopLevelMimeParts, source.parts_size()); |
183 | 15.4k | for (std::size_t i = 0; i < count && remaining_parts != 0; ++i) { |
184 | 10.3k | curl_mimepart* part = curl_mime_addpart(mime); |
185 | 10.3k | if (part == nullptr) { |
186 | 0 | break; |
187 | 0 | } |
188 | 10.3k | --remaining_parts; |
189 | 10.3k | ++stats->mime_parts; |
190 | 10.3k | const auto& proto_part = source.parts(static_cast<int>(i)); |
191 | 10.3k | ApplyPartMetadata(part, proto_part, stats); |
192 | | |
193 | 10.3k | switch (proto_part.content_case()) { |
194 | 1.85k | case curl::fuzzer::proto::MimePart::kData: |
195 | 1.85k | ApplyPartData(part, proto_part.data()); |
196 | 1.85k | break; |
197 | 5.55k | case curl::fuzzer::proto::MimePart::kSubparts: { |
198 | 5.55k | curl_mime* subparts = curl_mime_init(easy); |
199 | 5.55k | if (subparts == nullptr) { |
200 | 0 | break; |
201 | 0 | } |
202 | 5.55k | PopulateSubparts(subparts, proto_part.subparts(), &remaining_parts, stats); |
203 | 5.55k | if (curl_mime_subparts(part, subparts) != CURLE_OK) { |
204 | 0 | curl_mime_free(subparts); |
205 | 0 | } |
206 | 5.55k | break; |
207 | 5.55k | } |
208 | 2.90k | case curl::fuzzer::proto::MimePart::CONTENT_NOT_SET: |
209 | 2.90k | default: |
210 | 2.90k | break; |
211 | 10.3k | } |
212 | 10.3k | } |
213 | 5.14k | return mime; |
214 | 5.14k | } |
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 | 38.0k | : data_(), |
226 | 38.0k | read_step_count_(0), |
227 | 38.0k | total_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? 0 : scenario_limits::kMaxUploadBytes), |
228 | 38.0k | max_read_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? scenario_limits::kMaxTelnetUploadReadSize |
229 | 38.0k | : scenario_limits::kMaxUploadReadSize), |
230 | 38.0k | offset_(0), |
231 | 38.0k | next_read_size_(0), |
232 | 38.0k | terminal_(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF), |
233 | 38.0k | seek_result_(curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK), |
234 | 38.0k | before_read_callback_(nullptr), |
235 | 38.0k | before_read_userdata_(nullptr), |
236 | 38.0k | scripted_(scenario.has_upload()) { |
237 | 38.0k | if (!scripted_) { |
238 | 33.6k | return; |
239 | 33.6k | } |
240 | | |
241 | 4.38k | const auto& upload = scenario.upload(); |
242 | 4.38k | const std::size_t data_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET |
243 | 4.38k | ? scenario_limits::kMaxTelnetUploadBytes |
244 | 4.38k | : scenario_limits::kMaxUploadBytes; |
245 | 4.38k | const std::size_t data_size = std::min(upload.data().size(), data_limit); |
246 | 4.38k | data_ = std::string_view(upload.data().data(), data_size); |
247 | 4.38k | total_size_ = data_.size(); |
248 | 4.38k | terminal_ = upload.terminal(); |
249 | 4.38k | if (scenario.scheme() != curl::fuzzer::proto::SCHEME_TELNET && |
250 | 4.05k | 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 | 89 | terminal_ = curl::fuzzer::proto::UPLOAD_TERMINAL_EOF; |
254 | 89 | } |
255 | 4.38k | seek_result_ = upload.seek_result(); |
256 | | |
257 | 4.38k | const std::size_t read_step_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET |
258 | 4.38k | ? scenario_limits::kMaxTelnetUploadReadSteps |
259 | 4.38k | : scenario_limits::kMaxUploadReadSteps; |
260 | 4.38k | read_step_count_ = std::min<std::size_t>(upload.read_sizes_size(), read_step_limit); |
261 | 8.98k | for (std::size_t i = 0; i < read_step_count_; ++i) { |
262 | 4.59k | 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 | 4.59k | read_sizes_[i] = std::max<std::size_t>(1, std::min(requested, max_read_size_)); |
266 | 4.59k | } |
267 | 4.38k | } |
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 | 4.84k | 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 | 4.84k | if (before_read_callback_ != nullptr) { |
278 | 1.29k | before_read_callback_(before_read_userdata_); |
279 | 1.29k | } |
280 | 4.84k | if (offset_ >= total_size_) { |
281 | 2.20k | switch (terminal_) { |
282 | 39 | case curl::fuzzer::proto::UPLOAD_TERMINAL_ABORT: |
283 | 39 | return CURL_READFUNC_ABORT; |
284 | 32 | case curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE: |
285 | 32 | return CURL_READFUNC_PAUSE; |
286 | 2.12k | case curl::fuzzer::proto::UPLOAD_TERMINAL_EOF: |
287 | 2.12k | default: |
288 | 2.12k | return 0; |
289 | 2.20k | } |
290 | 2.20k | } |
291 | 2.64k | if (buffer == nullptr || capacity == 0) { |
292 | 0 | return 0; |
293 | 0 | } |
294 | | |
295 | 2.64k | std::size_t chunk_limit = std::min(capacity, max_read_size_); |
296 | 2.64k | if (next_read_size_ < read_step_count_) { |
297 | 1.08k | chunk_limit = std::min(chunk_limit, read_sizes_[next_read_size_++]); |
298 | 1.08k | } |
299 | 2.64k | const std::size_t count = std::min(chunk_limit, total_size_ - offset_); |
300 | 2.64k | if (scripted_) { |
301 | 1.44k | std::memcpy(buffer, data_.data() + offset_, count); |
302 | 1.44k | } else { |
303 | 1.20k | std::memset(buffer, 'U', count); |
304 | 1.20k | } |
305 | 2.64k | offset_ += count; |
306 | 2.64k | return count; |
307 | 2.64k | } |
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 | 958 | int UploadScriptState::Seek(curl_off_t requested_offset, int origin) { |
313 | 958 | switch (seek_result_) { |
314 | 218 | case curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK: |
315 | 218 | return CURL_SEEKFUNC_CANTSEEK; |
316 | 3 | case curl::fuzzer::proto::UPLOAD_SEEK_FAIL: |
317 | 3 | return CURL_SEEKFUNC_FAIL; |
318 | 737 | case curl::fuzzer::proto::UPLOAD_SEEK_OK: |
319 | 737 | break; |
320 | 0 | default: |
321 | 0 | return CURL_SEEKFUNC_CANTSEEK; |
322 | 958 | } |
323 | | |
324 | 737 | const curl_off_t current = static_cast<curl_off_t>(offset_); |
325 | 737 | const curl_off_t end = static_cast<curl_off_t>(total_size_); |
326 | 737 | curl_off_t base = 0; |
327 | 737 | switch (origin) { |
328 | 737 | case SEEK_SET: |
329 | 737 | base = 0; |
330 | 737 | 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 | 737 | } |
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 | 737 | if (requested_offset < -base || requested_offset > end - base) { |
344 | 124 | return CURL_SEEKFUNC_FAIL; |
345 | 124 | } |
346 | 613 | offset_ = static_cast<std::size_t>(base + requested_offset); |
347 | 613 | next_read_size_ = 0; |
348 | 613 | return CURL_SEEKFUNC_OK; |
349 | 737 | } |
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 | 4.84k | std::size_t UploadScriptState::ReadCallback(char* buffer, std::size_t size, std::size_t nitems, void* userdata) { |
355 | 4.84k | if (userdata == nullptr || size == 0 || nitems == 0) { |
356 | 0 | return 0; |
357 | 0 | } |
358 | 4.84k | const std::size_t max = std::numeric_limits<std::size_t>::max(); |
359 | 4.84k | if (nitems > max / size) { |
360 | 0 | return CURL_READFUNC_ABORT; |
361 | 0 | } |
362 | 4.84k | return static_cast<UploadScriptState*>(userdata)->Read(buffer, size * nitems); |
363 | 4.84k | } |
364 | | |
365 | | /// Keep the C callback a one-line type bridge so all outcome/cursor behaviour |
366 | | /// remains directly unit-testable in Seek(). |
367 | 958 | int UploadScriptState::SeekCallback(void* userdata, curl_off_t offset, int origin) { |
368 | 958 | if (userdata == nullptr) { |
369 | 0 | return CURL_SEEKFUNC_FAIL; |
370 | 0 | } |
371 | 958 | return static_cast<UploadScriptState*>(userdata)->Seek(offset, origin); |
372 | 958 | } |
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 | 3.02k | void UploadScriptState::SetBeforeReadCallback(BeforeReadCallback callback, void* userdata) { |
383 | 3.02k | before_read_callback_ = callback; |
384 | 3.02k | before_read_userdata_ = userdata; |
385 | 3.02k | } |
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 | 38.0k | : easy_(easy), |
393 | 38.0k | request_headers_(nullptr), |
394 | 38.0k | telnet_options_(nullptr), |
395 | 38.0k | mime_post_(nullptr), |
396 | 38.0k | upload_state_(scenario), |
397 | 38.0k | upload_callbacks_installed_(false) { |
398 | 38.0k | if (easy_ == nullptr) { |
399 | 0 | return; |
400 | 0 | } |
401 | | |
402 | 38.0k | 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 | 10.1k | upload_callbacks_installed_ = true; |
409 | 10.1k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, &UploadScriptState::ReadCallback); |
410 | 10.1k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, &upload_state_); |
411 | 10.1k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, &UploadScriptState::SeekCallback); |
412 | 10.1k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, &upload_state_); |
413 | 10.1k | } |
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 | 38.0k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET) { |
419 | 3.02k | telnet_options_ = BuildStringList(scenario.telnet_options(), scenario_limits::kMaxTelnetOptions, |
420 | 3.02k | scenario_limits::kMaxTelnetOptionBytes, &stats_.telnet_options); |
421 | 3.02k | if (telnet_options_ != nullptr) { |
422 | 537 | (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, telnet_options_); |
423 | 537 | } |
424 | 35.0k | } else { |
425 | 35.0k | request_headers_ = BuildStringList(scenario.request_headers(), scenario_limits::kMaxRequestHeaders, |
426 | 35.0k | scenario_limits::kMaxMetadataBytes, &stats_.request_headers); |
427 | 35.0k | if (request_headers_ != nullptr) { |
428 | 8.08k | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, request_headers_); |
429 | 8.08k | } |
430 | | |
431 | 35.0k | if (scenario.has_mime_post()) { |
432 | 5.14k | mime_post_ = BuildMimePost(easy_, scenario.mime_post(), &stats_); |
433 | 5.14k | if (mime_post_ != nullptr) { |
434 | 5.14k | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, mime_post_); |
435 | 5.14k | } |
436 | 5.14k | } |
437 | 35.0k | } |
438 | 38.0k | } |
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 | 38.0k | ScenarioRequestData::~ScenarioRequestData() { |
445 | 38.0k | 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 | 38.0k | if (upload_callbacks_installed_) { |
450 | 10.1k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, nullptr); |
451 | 10.1k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, nullptr); |
452 | 10.1k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, nullptr); |
453 | 10.1k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, nullptr); |
454 | 10.1k | } |
455 | 38.0k | if (mime_post_ != nullptr) { |
456 | 5.14k | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, nullptr); |
457 | 5.14k | } |
458 | 38.0k | if (request_headers_ != nullptr) { |
459 | 8.08k | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, nullptr); |
460 | 8.08k | } |
461 | 38.0k | if (telnet_options_ != nullptr) { |
462 | 537 | (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, nullptr); |
463 | 537 | } |
464 | 38.0k | } |
465 | 38.0k | curl_mime_free(mime_post_); |
466 | 38.0k | curl_slist_free_all(telnet_options_); |
467 | 38.0k | curl_slist_free_all(request_headers_); |
468 | 38.0k | } |
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 | 3.02k | void ScenarioRequestData::SetBeforeUploadReadCallback(UploadScriptState::BeforeReadCallback callback, void* userdata) { |
479 | 3.02k | upload_state_.SetBeforeReadCallback(callback, userdata); |
480 | 3.02k | } |
481 | | |
482 | | } // namespace proto_fuzzer |