/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 | 295k | const char* BoundedCString(const std::string& value, std::size_t limit, std::string* truncated) { |
33 | 295k | if (value.size() <= limit) { |
34 | 295k | return value.c_str(); |
35 | 295k | } |
36 | 134 | truncated->assign(value.data(), limit); |
37 | 134 | return truncated->c_str(); |
38 | 295k | } |
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 | 393k | 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 | 393k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET || scenario.has_upload()) { |
51 | 21.7k | return true; |
52 | 21.7k | } |
53 | 371k | const std::size_t option_count = RuntimeOptionCount(scenario); |
54 | 1.19M | for (std::size_t index = 0; index < option_count; ++index) { |
55 | 837k | const auto& option = scenario.options(static_cast<int>(index)); |
56 | 837k | if (option.option_id() == curl::fuzzer::proto::CURLOPT_UPLOAD || |
57 | 828k | option.option_id() == curl::fuzzer::proto::CURLOPT_POST) { |
58 | 11.6k | return true; |
59 | 11.6k | } |
60 | 837k | } |
61 | 360k | return false; |
62 | 371k | } |
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 | 141k | const char* MimeEncoderName(curl::fuzzer::proto::MimeEncoder encoder) { |
68 | 141k | switch (encoder) { |
69 | 6.79k | case curl::fuzzer::proto::MIME_ENCODER_BINARY: |
70 | 6.79k | return "binary"; |
71 | 6.92k | case curl::fuzzer::proto::MIME_ENCODER_8BIT: |
72 | 6.92k | return "8bit"; |
73 | 9.67k | case curl::fuzzer::proto::MIME_ENCODER_7BIT: |
74 | 9.67k | return "7bit"; |
75 | 16.8k | case curl::fuzzer::proto::MIME_ENCODER_BASE64: |
76 | 16.8k | return "base64"; |
77 | 21.5k | case curl::fuzzer::proto::MIME_ENCODER_QUOTED_PRINTABLE: |
78 | 21.5k | return "quoted-printable"; |
79 | 79.2k | case curl::fuzzer::proto::MIME_ENCODER_UNSPECIFIED: |
80 | 79.4k | default: |
81 | 79.4k | return nullptr; |
82 | 141k | } |
83 | 141k | } |
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 | | CurlSlistPtr BuildStringList(const RepeatedBytes& values, std::size_t count_limit, std::size_t value_limit, |
91 | 549k | std::size_t* applied) { |
92 | 549k | CurlSlistPtr list; |
93 | 549k | std::string truncated; |
94 | 549k | const std::size_t count = std::min<std::size_t>(count_limit, values.size()); |
95 | 747k | for (std::size_t i = 0; i < count; ++i) { |
96 | 197k | const std::string& value = values.Get(static_cast<int>(i)); |
97 | 197k | curl_slist* appended = curl_slist_append(list.get(), BoundedCString(value, value_limit, &truncated)); |
98 | 197k | if (appended == nullptr) { |
99 | 184 | break; |
100 | 184 | } |
101 | | // append retains the existing nodes and may return the same head. Release |
102 | | // before resetting so adopting the successful result cannot free them. |
103 | 197k | (void)list.release(); |
104 | 197k | list.reset(appended); |
105 | 197k | ++*applied; |
106 | 197k | } |
107 | 549k | return list; |
108 | 549k | } |
109 | | |
110 | | /// Apply the metadata shared by top-level and nested protobuf part types. The |
111 | | /// MIME API copies these strings, so temporary bounded buffers are sufficient; |
112 | | /// only the MIME root itself needs to outlive the perform loop. |
113 | | template <typename ProtoPart> |
114 | 141k | void ApplyPartMetadata(curl_mimepart* part, const ProtoPart& source, RequestBuildStats* stats) { |
115 | | // Reuse the rare compatibility-path allocation across all three fields; |
116 | | // ordinary postprocessed metadata never writes this scratch string. |
117 | 141k | std::string truncated; |
118 | 141k | if (!source.name().empty()) { |
119 | 33.5k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); |
120 | 33.5k | } |
121 | 141k | if (!source.filename().empty()) { |
122 | 34.4k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); |
123 | 34.4k | } |
124 | 141k | if (!source.content_type().empty()) { |
125 | 30.1k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); |
126 | 30.1k | } |
127 | 141k | if (const char* encoder = MimeEncoderName(source.encoder())) { |
128 | 61.8k | (void)curl_mime_encoder(part, encoder); |
129 | 61.8k | } |
130 | | |
131 | 141k | std::size_t header_count = 0; |
132 | 141k | CurlSlistPtr headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, |
133 | 141k | scenario_limits::kMaxMetadataBytes, &header_count); |
134 | 141k | if (headers != nullptr) { |
135 | | // take_ownership=1 is crucial: unlike the strings above, MIME retains the |
136 | | // list pointer. Once attached, the root curl_mime_free call recursively |
137 | | // releases it, including lists on nested parts. |
138 | 26.1k | const CURLcode result = curl_mime_headers(part, headers.get(), 1); |
139 | 26.1k | if (result == CURLE_OK) { |
140 | 26.1k | (void)headers.release(); |
141 | 26.1k | stats->mime_headers += header_count; |
142 | 26.1k | } |
143 | 26.1k | } |
144 | 141k | } 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 | 114 | 76.1k | void ApplyPartMetadata(curl_mimepart* part, const ProtoPart& source, RequestBuildStats* stats) { | 115 | | // Reuse the rare compatibility-path allocation across all three fields; | 116 | | // ordinary postprocessed metadata never writes this scratch string. | 117 | 76.1k | std::string truncated; | 118 | 76.1k | if (!source.name().empty()) { | 119 | 15.6k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); | 120 | 15.6k | } | 121 | 76.1k | if (!source.filename().empty()) { | 122 | 15.0k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); | 123 | 15.0k | } | 124 | 76.1k | if (!source.content_type().empty()) { | 125 | 14.2k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); | 126 | 14.2k | } | 127 | 76.1k | if (const char* encoder = MimeEncoderName(source.encoder())) { | 128 | 34.0k | (void)curl_mime_encoder(part, encoder); | 129 | 34.0k | } | 130 | | | 131 | 76.1k | std::size_t header_count = 0; | 132 | 76.1k | CurlSlistPtr headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, | 133 | 76.1k | scenario_limits::kMaxMetadataBytes, &header_count); | 134 | 76.1k | if (headers != nullptr) { | 135 | | // take_ownership=1 is crucial: unlike the strings above, MIME retains the | 136 | | // list pointer. Once attached, the root curl_mime_free call recursively | 137 | | // releases it, including lists on nested parts. | 138 | 11.3k | const CURLcode result = curl_mime_headers(part, headers.get(), 1); | 139 | 11.3k | if (result == CURLE_OK) { | 140 | 11.3k | (void)headers.release(); | 141 | 11.3k | stats->mime_headers += header_count; | 142 | 11.3k | } | 143 | 11.3k | } | 144 | 76.1k | } |
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 | 114 | 65.1k | void ApplyPartMetadata(curl_mimepart* part, const ProtoPart& source, RequestBuildStats* stats) { | 115 | | // Reuse the rare compatibility-path allocation across all three fields; | 116 | | // ordinary postprocessed metadata never writes this scratch string. | 117 | 65.1k | std::string truncated; | 118 | 65.1k | if (!source.name().empty()) { | 119 | 17.9k | (void)curl_mime_name(part, BoundedCString(source.name(), scenario_limits::kMaxMetadataBytes, &truncated)); | 120 | 17.9k | } | 121 | 65.1k | if (!source.filename().empty()) { | 122 | 19.3k | (void)curl_mime_filename(part, BoundedCString(source.filename(), scenario_limits::kMaxMetadataBytes, &truncated)); | 123 | 19.3k | } | 124 | 65.1k | if (!source.content_type().empty()) { | 125 | 15.9k | (void)curl_mime_type(part, BoundedCString(source.content_type(), scenario_limits::kMaxMetadataBytes, &truncated)); | 126 | 15.9k | } | 127 | 65.1k | if (const char* encoder = MimeEncoderName(source.encoder())) { | 128 | 27.8k | (void)curl_mime_encoder(part, encoder); | 129 | 27.8k | } | 130 | | | 131 | 65.1k | std::size_t header_count = 0; | 132 | 65.1k | CurlSlistPtr headers = BuildStringList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, | 133 | 65.1k | scenario_limits::kMaxMetadataBytes, &header_count); | 134 | 65.1k | if (headers != nullptr) { | 135 | | // take_ownership=1 is crucial: unlike the strings above, MIME retains the | 136 | | // list pointer. Once attached, the root curl_mime_free call recursively | 137 | | // releases it, including lists on nested parts. | 138 | 14.7k | const CURLcode result = curl_mime_headers(part, headers.get(), 1); | 139 | 14.7k | if (result == CURLE_OK) { | 140 | 14.7k | (void)headers.release(); | 141 | 14.7k | stats->mime_headers += header_count; | 142 | 14.7k | } | 143 | 14.7k | } | 144 | 65.1k | } |
|
145 | | |
146 | | /// Copy bounded binary data into a MIME part. curl_mime_data accepts an |
147 | | /// explicit size, so embedded NUL bytes remain fuzzable here unlike in the |
148 | | /// metadata and header APIs. |
149 | 60.8k | void ApplyPartData(curl_mimepart* part, const std::string& data) { |
150 | 60.8k | const std::size_t size = std::min(data.size(), scenario_limits::kMaxMimeDataBytes); |
151 | 60.8k | const char* bytes = data.empty() ? "" : data.data(); |
152 | 60.8k | (void)curl_mime_data(part, bytes, size); |
153 | 60.8k | } |
154 | | |
155 | | /// Materialize one compact repeated source without allowing the protobuf's |
156 | | /// uint32 count or a collection of MIME parts to amplify into unbounded work. |
157 | | /// Division establishes the maximum count before multiplication, so this is |
158 | | /// safe even when size_t is narrower than uint32_t. |
159 | | void ApplyGeneratedPartData(curl_mimepart* part, const curl::fuzzer::proto::GeneratedBytes& source, |
160 | 22.9k | std::size_t* remaining_generated_bytes, RequestBuildStats* stats) { |
161 | 22.9k | const std::size_t pattern_size = std::min(source.pattern().size(), scenario_limits::kMaxGeneratedMimePatternBytes); |
162 | 22.9k | if (pattern_size == 0 || *remaining_generated_bytes == 0) { |
163 | 7.04k | (void)curl_mime_data(part, "", 0); |
164 | 7.04k | return; |
165 | 7.04k | } |
166 | | |
167 | 15.9k | const std::size_t max_repeat_count = *remaining_generated_bytes / pattern_size; |
168 | 15.9k | const std::size_t repeat_count = |
169 | 15.9k | std::min<std::size_t>(static_cast<std::size_t>(source.repeat_count()), max_repeat_count); |
170 | 15.9k | const std::size_t materialized_size = repeat_count * pattern_size; |
171 | | |
172 | 15.9k | std::string materialized(materialized_size, '\0'); |
173 | 325M | for (std::size_t offset = 0; offset < materialized_size; offset += pattern_size) { |
174 | 325M | std::copy_n(source.pattern().data(), pattern_size, materialized.data() + offset); |
175 | 325M | } |
176 | 15.9k | const char* bytes = materialized.empty() ? "" : materialized.data(); |
177 | 15.9k | if (curl_mime_data(part, bytes, materialized.size()) == CURLE_OK) { |
178 | 15.9k | stats->generated_mime_bytes += materialized.size(); |
179 | 15.9k | } |
180 | 15.9k | *remaining_generated_bytes -= materialized.size(); |
181 | 15.9k | } |
182 | | |
183 | | /// Populate the fixed-depth child body and debit the shared total-part budget. |
184 | | /// Returning an empty MIME object when the child list is empty is intentional: |
185 | | /// curl's empty multipart serialization is useful coverage and remains cheap. |
186 | | void PopulateSubparts(curl_mime* mime, const curl::fuzzer::proto::MimeSubparts& source, std::size_t* remaining_parts, |
187 | 34.3k | std::size_t* remaining_generated_bytes, RequestBuildStats* stats) { |
188 | 34.3k | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxNestedMimeParts, source.parts_size()); |
189 | 99.4k | for (std::size_t i = 0; i < count && *remaining_parts != 0; ++i) { |
190 | 65.1k | curl_mimepart* part = curl_mime_addpart(mime); |
191 | 65.1k | if (part == nullptr) { |
192 | 71 | break; |
193 | 71 | } |
194 | 65.1k | --*remaining_parts; |
195 | 65.1k | ++stats->mime_parts; |
196 | 65.1k | const auto& proto_part = source.parts(static_cast<int>(i)); |
197 | 65.1k | ApplyPartMetadata(part, proto_part, stats); |
198 | 65.1k | if (proto_part.content_case() == curl::fuzzer::proto::MimeDataPart::kGeneratedData) { |
199 | 10.0k | ApplyGeneratedPartData(part, proto_part.generated_data(), remaining_generated_bytes, stats); |
200 | 55.0k | } else { |
201 | | // Preserve the historical leaf behavior: an absent source is applied as |
202 | | // empty data just like an explicitly empty field 2. |
203 | 55.0k | ApplyPartData(part, proto_part.data()); |
204 | 55.0k | } |
205 | 65.1k | } |
206 | 34.3k | } |
207 | | |
208 | | /// Construct the top MIME tree. curl_mime_subparts transfers ownership only |
209 | | /// on success, so failed attachments are freed immediately while successful |
210 | | /// ones are left for the top-level root to release recursively. |
211 | 47.8k | CurlMimePtr BuildMimePost(CURL* easy, const curl::fuzzer::proto::MimePost& source, RequestBuildStats* stats) { |
212 | 47.8k | CurlMimePtr mime(curl_mime_init(easy)); |
213 | 47.8k | if (mime == nullptr) { |
214 | 39 | return nullptr; |
215 | 39 | } |
216 | | |
217 | 47.8k | std::size_t remaining_parts = scenario_limits::kMaxTotalMimeParts; |
218 | 47.8k | std::size_t remaining_generated_bytes = scenario_limits::kMaxGeneratedMimeDataBytes; |
219 | 47.8k | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxTopLevelMimeParts, source.parts_size()); |
220 | 124k | for (std::size_t i = 0; i < count && remaining_parts != 0; ++i) { |
221 | 76.2k | curl_mimepart* part = curl_mime_addpart(mime.get()); |
222 | 76.2k | if (part == nullptr) { |
223 | 44 | break; |
224 | 44 | } |
225 | 76.1k | --remaining_parts; |
226 | 76.1k | ++stats->mime_parts; |
227 | 76.1k | const auto& proto_part = source.parts(static_cast<int>(i)); |
228 | 76.1k | ApplyPartMetadata(part, proto_part, stats); |
229 | | |
230 | 76.1k | switch (proto_part.content_case()) { |
231 | 5.75k | case curl::fuzzer::proto::MimePart::kData: |
232 | 5.75k | ApplyPartData(part, proto_part.data()); |
233 | 5.75k | break; |
234 | 12.9k | case curl::fuzzer::proto::MimePart::kGeneratedData: |
235 | 12.9k | ApplyGeneratedPartData(part, proto_part.generated_data(), &remaining_generated_bytes, stats); |
236 | 12.9k | break; |
237 | 34.3k | case curl::fuzzer::proto::MimePart::kSubparts: { |
238 | 34.3k | CurlMimePtr subparts(curl_mime_init(easy)); |
239 | 34.3k | if (subparts == nullptr) { |
240 | 41 | break; |
241 | 41 | } |
242 | 34.3k | PopulateSubparts(subparts.get(), proto_part.subparts(), &remaining_parts, &remaining_generated_bytes, stats); |
243 | 34.3k | if (curl_mime_subparts(part, subparts.get()) == CURLE_OK) { |
244 | 34.3k | (void)subparts.release(); |
245 | 34.3k | } |
246 | 34.3k | break; |
247 | 34.3k | } |
248 | 23.1k | case curl::fuzzer::proto::MimePart::CONTENT_NOT_SET: |
249 | 23.1k | default: |
250 | 23.1k | break; |
251 | 76.1k | } |
252 | 76.1k | } |
253 | 47.8k | return mime; |
254 | 47.8k | } |
255 | | |
256 | | } // namespace |
257 | | |
258 | | /// Borrow and cap a scripted upload source before libcurl receives a userdata |
259 | | /// pointer. When Scenario.upload is absent, non-TELNET schemes synthesize the |
260 | | /// historical 16 KiB `U` stream without allocating it, while TELNET reports |
261 | | /// immediate EOF so merely selecting the protocol cannot inject unsolicited |
262 | | /// terminal input. Borrowing avoids duplicating every fuzzed body before curl |
263 | | /// can read it; `scenario` must remain alive and unmodified until this state is |
264 | | /// destroyed. |
265 | | /// @param scenario Source and lifetime owner of callback bytes and outcomes. |
266 | | UploadScriptState::UploadScriptState(const curl::fuzzer::proto::Scenario& scenario) |
267 | 393k | : data_(), |
268 | 393k | read_step_count_(0), |
269 | 393k | total_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? 0 : scenario_limits::kMaxUploadBytes), |
270 | 393k | max_read_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? scenario_limits::kMaxTelnetUploadReadSize |
271 | 393k | : scenario_limits::kMaxUploadReadSize), |
272 | 393k | offset_(0), |
273 | 393k | next_read_size_(0), |
274 | 393k | terminal_(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF), |
275 | 393k | seek_result_(curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK), |
276 | 393k | before_read_callback_(nullptr), |
277 | 393k | before_read_userdata_(nullptr), |
278 | 393k | scripted_(scenario.has_upload()) { |
279 | 393k | if (!scripted_) { |
280 | 376k | return; |
281 | 376k | } |
282 | | |
283 | 16.5k | const auto& upload = scenario.upload(); |
284 | 16.5k | const std::size_t data_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET |
285 | 16.5k | ? scenario_limits::kMaxTelnetUploadBytes |
286 | 16.5k | : scenario_limits::kMaxUploadBytes; |
287 | 16.5k | const std::size_t data_size = std::min(upload.data().size(), data_limit); |
288 | 16.5k | data_ = std::string_view(upload.data().data(), data_size); |
289 | 16.5k | total_size_ = data_.size(); |
290 | 16.5k | terminal_ = upload.terminal(); |
291 | 16.5k | if (scenario.scheme() != curl::fuzzer::proto::SCHEME_TELNET && |
292 | 16.3k | terminal_ == curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE) { |
293 | | // Event-driven protocols need an external resume source. Interpret this |
294 | | // TELNET-specific outcome as EOF before curl can retain a paused transfer. |
295 | 41 | terminal_ = curl::fuzzer::proto::UPLOAD_TERMINAL_EOF; |
296 | 41 | } |
297 | 16.5k | seek_result_ = upload.seek_result(); |
298 | | |
299 | 16.5k | const std::size_t read_step_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET |
300 | 16.5k | ? scenario_limits::kMaxTelnetUploadReadSteps |
301 | 16.5k | : scenario_limits::kMaxUploadReadSteps; |
302 | 16.5k | read_step_count_ = std::min<std::size_t>(upload.read_sizes_size(), read_step_limit); |
303 | 72.6k | for (std::size_t i = 0; i < read_step_count_; ++i) { |
304 | 56.0k | const std::size_t requested = upload.read_sizes(static_cast<int>(i)); |
305 | | // Zero-as-one ensures every retained step makes progress; see the schema |
306 | | // comment for why zero is not treated as an early EOF sentinel. |
307 | 56.0k | read_sizes_[i] = std::max<std::size_t>(1, std::min(requested, max_read_size_)); |
308 | 56.0k | } |
309 | 16.5k | } |
310 | | |
311 | | /// Copy the next bounded chunk and advance the cursor, using either the explicit |
312 | | /// payload or the allocation-free fallback. Terminal outcomes are emitted only |
313 | | /// after all data is consumed so a mutation can independently control |
314 | | /// fragmentation and completion policy. Public so focused tests can validate |
315 | | /// callback semantics without relying on HTTP timing. |
316 | | /// @param buffer Destination supplied by curl. |
317 | | /// @param capacity Writable bytes in `buffer`. |
318 | | /// @return Bytes copied, zero for EOF, CURL_READFUNC_ABORT, or the TELNET- |
319 | | /// only CURL_READFUNC_PAUSE outcome. |
320 | 152k | std::size_t UploadScriptState::Read(char* buffer, std::size_t capacity) { |
321 | | // TELNET can produce negotiation replies while one curl_multi_perform call |
322 | | // owns the thread. Empty them before returning more callback bytes; |
323 | | // otherwise send_telnet_data() can consume the whole bounded transfer |
324 | | // timeout while the synchronous path waits for socket capacity. |
325 | 152k | if (before_read_callback_ != nullptr) { |
326 | 1.75k | before_read_callback_(before_read_userdata_); |
327 | 1.75k | } |
328 | 152k | if (offset_ >= total_size_) { |
329 | 12.1k | switch (terminal_) { |
330 | 366 | case curl::fuzzer::proto::UPLOAD_TERMINAL_ABORT: |
331 | 366 | return CURL_READFUNC_ABORT; |
332 | 22 | case curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE: |
333 | 22 | return CURL_READFUNC_PAUSE; |
334 | 11.7k | case curl::fuzzer::proto::UPLOAD_TERMINAL_EOF: |
335 | 11.7k | default: |
336 | 11.7k | return 0; |
337 | 12.1k | } |
338 | 12.1k | } |
339 | 140k | if (buffer == nullptr || capacity == 0) { |
340 | 0 | return 0; |
341 | 0 | } |
342 | | |
343 | 140k | std::size_t chunk_limit = std::min(capacity, max_read_size_); |
344 | 140k | if (next_read_size_ < read_step_count_) { |
345 | 128k | chunk_limit = std::min(chunk_limit, read_sizes_[next_read_size_++]); |
346 | 128k | } |
347 | 140k | const std::size_t count = std::min(chunk_limit, total_size_ - offset_); |
348 | 140k | if (scripted_) { |
349 | 135k | std::memcpy(buffer, data_.data() + offset_, count); |
350 | 135k | } else { |
351 | 4.88k | std::memset(buffer, 'U', count); |
352 | 4.88k | } |
353 | 140k | offset_ += count; |
354 | 140k | return count; |
355 | 140k | } |
356 | | |
357 | | /// Apply the configured seek outcome and, for OK, move the bounded cursor. |
358 | | /// Successful seeks restart the short-read sequence so retries replay the same |
359 | | /// callback fragmentation deterministically. Explicit range checks model only |
360 | | /// seeks curl can meaningfully request from a bounded memory source, avoiding |
361 | | /// signed overflow or a mutation wrapping into an in-bounds cursor. |
362 | | /// @param requested_offset Offset interpreted relative to `origin`. |
363 | | /// @param origin One of SEEK_SET, SEEK_CUR, or SEEK_END. |
364 | | /// @return A CURL_SEEKFUNC_* result. |
365 | 16.2k | int UploadScriptState::Seek(curl_off_t requested_offset, int origin) { |
366 | 16.2k | switch (seek_result_) { |
367 | 470 | case curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK: |
368 | 470 | return CURL_SEEKFUNC_CANTSEEK; |
369 | 34 | case curl::fuzzer::proto::UPLOAD_SEEK_FAIL: |
370 | 34 | return CURL_SEEKFUNC_FAIL; |
371 | 15.7k | case curl::fuzzer::proto::UPLOAD_SEEK_OK: |
372 | 15.7k | break; |
373 | 0 | default: |
374 | 0 | return CURL_SEEKFUNC_CANTSEEK; |
375 | 16.2k | } |
376 | | |
377 | 15.7k | const curl_off_t current = static_cast<curl_off_t>(offset_); |
378 | 15.7k | const curl_off_t end = static_cast<curl_off_t>(total_size_); |
379 | 15.7k | curl_off_t base = 0; |
380 | 15.7k | switch (origin) { |
381 | 15.7k | case SEEK_SET: |
382 | 15.7k | base = 0; |
383 | 15.7k | break; |
384 | 0 | case SEEK_CUR: |
385 | 0 | base = current; |
386 | 0 | break; |
387 | 0 | case SEEK_END: |
388 | 0 | base = end; |
389 | 0 | break; |
390 | 0 | default: |
391 | 0 | return CURL_SEEKFUNC_FAIL; |
392 | 15.7k | } |
393 | | |
394 | | // Both base and end are at most 16 KiB. Comparing the requested delta to |
395 | | // these small bounds before addition handles even CURL_OFF_T_MIN safely. |
396 | 15.7k | if (requested_offset < -base || requested_offset > end - base) { |
397 | 96 | return CURL_SEEKFUNC_FAIL; |
398 | 96 | } |
399 | 15.6k | offset_ = static_cast<std::size_t>(base + requested_offset); |
400 | 15.6k | next_read_size_ = 0; |
401 | 15.6k | return CURL_SEEKFUNC_OK; |
402 | 15.7k | } |
403 | | |
404 | | /// libcurl trampoline that protects multiplication of its two buffer-size |
405 | | /// arguments before forwarding to Read(). Multiplication is normally benign |
406 | | /// because curl uses size=1, but callbacks are an API boundary. Abort an |
407 | | /// impossible overflowing pair: saturating to SIZE_MAX would let Read() copy |
408 | | /// into a buffer whose real extent is unknown. |
409 | 152k | std::size_t UploadScriptState::ReadCallback(char* buffer, std::size_t size, std::size_t nitems, void* userdata) { |
410 | 152k | if (userdata == nullptr || size == 0 || nitems == 0) { |
411 | 0 | return 0; |
412 | 0 | } |
413 | 152k | const std::size_t max = std::numeric_limits<std::size_t>::max(); |
414 | 152k | if (nitems > max / size) { |
415 | 0 | return CURL_READFUNC_ABORT; |
416 | 0 | } |
417 | 152k | return static_cast<UploadScriptState*>(userdata)->Read(buffer, size * nitems); |
418 | 152k | } |
419 | | |
420 | | /// libcurl trampoline for the userdata-first seek callback signature. Keep the |
421 | | /// C callback a type bridge so all outcome/cursor behaviour remains directly |
422 | | /// unit-testable in Seek(). |
423 | 16.2k | int UploadScriptState::SeekCallback(void* userdata, curl_off_t offset, int origin) { |
424 | 16.2k | if (userdata == nullptr) { |
425 | 0 | return CURL_SEEKFUNC_FAIL; |
426 | 0 | } |
427 | 16.2k | return static_cast<UploadScriptState*>(userdata)->Seek(offset, origin); |
428 | 16.2k | } |
429 | | |
430 | | /// @return Number of bytes visible to the callback after caps. |
431 | 324k | std::size_t UploadScriptState::data_size() const { return total_size_; } |
432 | | |
433 | | /// @return Number of retained per-read limits after caps. |
434 | 324k | std::size_t UploadScriptState::read_step_count() const { return read_step_count_; } |
435 | | |
436 | | /// @return Current byte cursor, exposed for deterministic unit tests. |
437 | 324k | std::size_t UploadScriptState::offset() const { return offset_; } |
438 | | |
439 | | /// @return Whether Scenario.upload was present instead of using fallback. |
440 | 324k | bool UploadScriptState::scripted() const { return scripted_; } |
441 | | |
442 | | /// Install a protocol hook without exposing the mutable cursor or curl's |
443 | | /// retained callback userdata outside its owner. |
444 | 5.42k | void UploadScriptState::SetBeforeReadCallback(BeforeReadCallback callback, void* userdata) { |
445 | 5.42k | before_read_callback_ = callback; |
446 | 5.42k | before_read_userdata_ = userdata; |
447 | 5.42k | } |
448 | | |
449 | | /// Construct and apply the protocol-specific pointer-valued fields in `scenario` |
450 | | /// to `easy`. Setup errors are deliberately non-fatal: malformed or partially |
451 | | /// allocated scenarios should still exercise whatever curl state was built. |
452 | | /// `easy` must remain alive until this object is destroyed, because cleanup |
453 | | /// first detaches the pointers from the handle and only then frees them. |
454 | | /// @param easy Easy handle that will perform this scenario. |
455 | | /// @param scenario Source headers/TELNET options, optional MIME body, and |
456 | | /// upload script. |
457 | | /// @param apply_resolve_entries Whether to install the scenario's bounded |
458 | | /// resolver mappings plus the mandatory loopback mapping. |
459 | | ScenarioRequestData::ScenarioRequestData(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, |
460 | | bool apply_resolve_entries) |
461 | 393k | : easy_(easy), |
462 | 393k | request_headers_(nullptr), |
463 | 393k | resolve_entries_(nullptr), |
464 | 393k | telnet_options_(nullptr), |
465 | 393k | mime_post_(nullptr), |
466 | 393k | upload_state_(scenario), |
467 | 393k | upload_callbacks_installed_(false), |
468 | 393k | resolve_entries_ready_(!apply_resolve_entries) { |
469 | 393k | if (easy_ == nullptr) { |
470 | 0 | return; |
471 | 0 | } |
472 | | |
473 | | // Complete all allocating work before installing retained pointers. If a |
474 | | // C++ allocation throws, scoped owners unwind without leaving the easy |
475 | | // handle pointing into a partially constructed request-data object. |
476 | 393k | bool resolve_entries_built = false; |
477 | 393k | if (apply_resolve_entries) { |
478 | 15.0k | resolve_entries_ = BuildStringList(scenario.resolve_entries(), scenario_limits::kMaxResolveEntries, |
479 | 15.0k | scenario_limits::kMaxResolveEntryBytes, &stats_.resolve_entries); |
480 | | // This mapping is deliberately last: preceding wildcard or removal |
481 | | // mutations may exercise host-cache parsing, but cannot redirect the |
482 | | // canonical resolver-lane origin away from the in-process mock. |
483 | 15.0k | curl_slist* appended = curl_slist_append(resolve_entries_.get(), "resolve.test:80:127.0.0.1"); |
484 | 15.0k | if (appended != nullptr) { |
485 | 15.0k | (void)resolve_entries_.release(); |
486 | 15.0k | resolve_entries_.reset(appended); |
487 | 15.0k | resolve_entries_built = true; |
488 | 15.0k | } |
489 | 15.0k | } |
490 | | |
491 | | // HTTP headers/MIME and TELNET options are mutually exclusive because only |
492 | | // the selected protocol can observe them. Avoid allocating protocol-inert |
493 | | // lists and trees in compatibility inputs that bypass target policy. |
494 | 393k | if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET) { |
495 | 5.42k | telnet_options_ = BuildStringList(scenario.telnet_options(), scenario_limits::kMaxTelnetOptions, |
496 | 5.42k | scenario_limits::kMaxTelnetOptionBytes, &stats_.telnet_options); |
497 | 387k | } else { |
498 | 387k | request_headers_ = BuildStringList(scenario.request_headers(), scenario_limits::kMaxRequestHeaders, |
499 | 387k | scenario_limits::kMaxMetadataBytes, &stats_.request_headers); |
500 | 387k | if (scenario.has_mime_post()) { |
501 | 47.8k | mime_post_ = BuildMimePost(easy_, scenario.mime_post(), &stats_); |
502 | 47.8k | } |
503 | 387k | } |
504 | | |
505 | 393k | if (resolve_entries_built) { |
506 | 15.0k | resolve_entries_ready_ = curl_easy_setopt(easy_, CURLOPT_RESOLVE, resolve_entries_.get()) == CURLE_OK; |
507 | 15.0k | } |
508 | | |
509 | 393k | if (NeedsUploadCallbacks(scenario)) { |
510 | | // Install a per-run memory source even when Scenario.upload is absent but |
511 | | // CURLOPT_UPLOAD or TELNET may request caller input. Non-TELNET schemes |
512 | | // retain the historical fallback bytes; TELNET returns EOF. Either result |
513 | | // replaces stdin and cannot block OSS-Fuzz. The state remains scoped to |
514 | | // the complete drive so retries cannot share a cursor across iterations. |
515 | 33.3k | upload_callbacks_installed_ = true; |
516 | 33.3k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, &UploadScriptState::ReadCallback); |
517 | 33.3k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, &upload_state_); |
518 | 33.3k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, &UploadScriptState::SeekCallback); |
519 | 33.3k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, &upload_state_); |
520 | 33.3k | } |
521 | | |
522 | 393k | if (telnet_options_ != nullptr) { |
523 | 497 | (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, telnet_options_.get()); |
524 | 497 | } |
525 | 393k | if (request_headers_ != nullptr) { |
526 | 44.7k | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, request_headers_.get()); |
527 | 44.7k | } |
528 | 393k | if (mime_post_ != nullptr) { |
529 | 47.8k | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, mime_post_.get()); |
530 | 47.8k | } |
531 | 393k | } |
532 | | |
533 | | /// Detach pointer options/callbacks while the easy handle is valid, then release |
534 | | /// request allocations. libcurl does not copy headers, MIME roots, or callback |
535 | | /// userdata, so releasing any one before the mock drive ends would create a |
536 | | /// use-after-free; relying on easy cleanup to own header/MIME allocations would |
537 | | /// instead leak iterations. |
538 | 393k | ScenarioRequestData::~ScenarioRequestData() { |
539 | 393k | if (easy_ != nullptr) { |
540 | | // Clear callbacks before their userdata member is destroyed. There is no |
541 | | // perform in this destructor, but making the handle non-dangling keeps the |
542 | | // ownership rule robust if cleanup later gains diagnostics or getinfo. |
543 | 393k | if (upload_callbacks_installed_) { |
544 | 33.3k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, nullptr); |
545 | 33.3k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, nullptr); |
546 | 33.3k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, nullptr); |
547 | 33.3k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, nullptr); |
548 | 33.3k | } |
549 | 393k | if (mime_post_ != nullptr) { |
550 | 47.8k | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, nullptr); |
551 | 47.8k | } |
552 | 393k | if (request_headers_ != nullptr) { |
553 | 44.7k | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, nullptr); |
554 | 44.7k | } |
555 | 393k | if (resolve_entries_ != nullptr) { |
556 | 15.0k | (void)curl_easy_setopt(easy_, CURLOPT_RESOLVE, nullptr); |
557 | 15.0k | } |
558 | 393k | if (telnet_options_ != nullptr) { |
559 | 497 | (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, nullptr); |
560 | 497 | } |
561 | 393k | } |
562 | 393k | } |
563 | | |
564 | | /// Return resource counts after caps and allocation failures were applied, |
565 | | /// without exposing or transferring the owned curl pointers themselves. |
566 | | /// @return Immutable construction statistics. |
567 | 324k | const RequestBuildStats& ScenarioRequestData::stats() const { return stats_; } |
568 | | |
569 | | /// Return the callback state retained for the complete drive. This is mainly |
570 | | /// useful to ownership tests; the runner itself lets libcurl mutate it. |
571 | | /// @return Immutable upload state. |
572 | 324k | const UploadScriptState& ScenarioRequestData::upload_state() const { return upload_state_; } |
573 | | |
574 | | /// Return whether this scenario needed raw upload callbacks. Exposing the |
575 | | /// decision lets ownership tests ensure ordinary requests avoid redundant |
576 | | /// setopt/teardown work without exposing either userdata pointer. |
577 | | /// @return True when read and seek callbacks were attached to the handle. |
578 | 324k | bool ScenarioRequestData::upload_callbacks_installed() const { return upload_callbacks_installed_; } |
579 | | |
580 | | /// Return whether resolver-only slist construction installed its mandatory |
581 | | /// final loopback mapping. Ordinary lanes always report true. |
582 | | /// @return True when the resolver mapping is ready for the transfer. |
583 | 15.0k | bool ScenarioRequestData::resolve_entries_ready() const { return resolve_entries_ready_; } |
584 | | |
585 | | /// Arrange protocol-specific work immediately before every upload read. |
586 | | /// This is intentionally a narrow callback seam rather than exposing curl's |
587 | | /// retained READFUNCTION userdata or the mutable upload cursor. |
588 | | /// @param callback Function to invoke, or nullptr to clear the hook. |
589 | | /// @param userdata Borrowed pointer passed to `callback`. |
590 | 5.42k | void ScenarioRequestData::SetBeforeUploadReadCallback(UploadScriptState::BeforeReadCallback callback, void* userdata) { |
591 | 5.42k | upload_state_.SetBeforeReadCallback(callback, userdata); |
592 | 5.42k | } |
593 | | |
594 | | } // namespace proto_fuzzer |