/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 | 104 | const char* BoundedCString(const std::string& value, std::string* truncated) { |
33 | 104 | if (value.size() <= scenario_limits::kMaxMetadataBytes) { |
34 | 104 | return value.c_str(); |
35 | 104 | } |
36 | 0 | truncated->assign(value.data(), scenario_limits::kMaxMetadataBytes); |
37 | 0 | return truncated->c_str(); |
38 | 104 | } |
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 | 10.1k | bool NeedsUploadCallbacks(const curl::fuzzer::proto::Scenario& scenario) { |
47 | 10.1k | if (scenario.has_upload()) { |
48 | 57 | return true; |
49 | 57 | } |
50 | 10.0k | const std::size_t option_count = RuntimeOptionCount(scenario); |
51 | 25.1k | for (std::size_t index = 0; index < option_count; ++index) { |
52 | 16.3k | const auto& option = scenario.options(static_cast<int>(index)); |
53 | 16.3k | if (option.option_id() == curl::fuzzer::proto::CURLOPT_UPLOAD || |
54 | 15.5k | option.option_id() == curl::fuzzer::proto::CURLOPT_POST) { |
55 | 1.30k | return true; |
56 | 1.30k | } |
57 | 16.3k | } |
58 | 8.78k | return false; |
59 | 10.0k | } |
60 | | |
61 | | /// Translate the mutation-friendly enum to curl's spelling. Restricting this |
62 | | /// field to supported encoders spends cycles in encoder implementations rather |
63 | | /// than repeatedly rediscovering the same invalid-string rejection. |
64 | 0 | const char* MimeEncoderName(curl::fuzzer::proto::MimeEncoder encoder) { |
65 | 0 | switch (encoder) { |
66 | 0 | case curl::fuzzer::proto::MIME_ENCODER_BINARY: |
67 | 0 | return "binary"; |
68 | 0 | case curl::fuzzer::proto::MIME_ENCODER_8BIT: |
69 | 0 | return "8bit"; |
70 | 0 | case curl::fuzzer::proto::MIME_ENCODER_7BIT: |
71 | 0 | return "7bit"; |
72 | 0 | case curl::fuzzer::proto::MIME_ENCODER_BASE64: |
73 | 0 | return "base64"; |
74 | 0 | case curl::fuzzer::proto::MIME_ENCODER_QUOTED_PRINTABLE: |
75 | 0 | return "quoted-printable"; |
76 | 0 | case curl::fuzzer::proto::MIME_ENCODER_UNSPECIFIED: |
77 | 0 | default: |
78 | 0 | return nullptr; |
79 | 0 | } |
80 | 0 | } |
81 | | |
82 | | /// Append at most `limit` protobuf byte strings to a curl list. curl_slist_append |
83 | | /// leaves the old head valid on allocation failure, so only replace the head |
84 | | /// after a successful append and stop rather than burning the rest of the |
85 | | /// iteration on allocations that are already failing. |
86 | | template <typename RepeatedBytes> |
87 | 10.1k | curl_slist* BuildHeaderList(const RepeatedBytes& values, std::size_t limit, std::size_t* applied) { |
88 | 10.1k | curl_slist* headers = nullptr; |
89 | 10.1k | std::string truncated; |
90 | 10.1k | const std::size_t count = std::min<std::size_t>(limit, values.size()); |
91 | 10.2k | for (std::size_t i = 0; i < count; ++i) { |
92 | 104 | const std::string& header = values.Get(static_cast<int>(i)); |
93 | 104 | curl_slist* appended = curl_slist_append(headers, BoundedCString(header, &truncated)); |
94 | 104 | if (appended == nullptr) { |
95 | 0 | break; |
96 | 0 | } |
97 | 104 | headers = appended; |
98 | 104 | ++*applied; |
99 | 104 | } |
100 | 10.1k | return headers; |
101 | 10.1k | } |
102 | | |
103 | | /// Apply the metadata shared by top-level and nested protobuf part types. The |
104 | | /// MIME API copies these strings, so temporary bounded buffers are sufficient; |
105 | | /// only the MIME root itself needs to outlive the perform loop. |
106 | | template <typename ProtoPart> |
107 | 0 | void ApplyPartMetadata(curl_mimepart* part, const ProtoPart& source, RequestBuildStats* stats) { |
108 | | // Reuse the rare compatibility-path allocation across all three fields; |
109 | | // ordinary postprocessed metadata never writes this scratch string. |
110 | 0 | std::string truncated; |
111 | 0 | if (!source.name().empty()) { |
112 | 0 | (void)curl_mime_name(part, BoundedCString(source.name(), &truncated)); |
113 | 0 | } |
114 | 0 | if (!source.filename().empty()) { |
115 | 0 | (void)curl_mime_filename(part, BoundedCString(source.filename(), &truncated)); |
116 | 0 | } |
117 | 0 | if (!source.content_type().empty()) { |
118 | 0 | (void)curl_mime_type(part, BoundedCString(source.content_type(), &truncated)); |
119 | 0 | } |
120 | 0 | if (const char* encoder = MimeEncoderName(source.encoder())) { |
121 | 0 | (void)curl_mime_encoder(part, encoder); |
122 | 0 | } |
123 | |
|
124 | 0 | std::size_t header_count = 0; |
125 | 0 | curl_slist* headers = BuildHeaderList(source.headers(), scenario_limits::kMaxMimeHeadersPerPart, &header_count); |
126 | 0 | if (headers != nullptr) { |
127 | | // take_ownership=1 is crucial: unlike the strings above, MIME retains the |
128 | | // list pointer. Once attached, the root curl_mime_free call recursively |
129 | | // releases it, including lists on nested parts. |
130 | 0 | const CURLcode result = curl_mime_headers(part, headers, 1); |
131 | 0 | if (result == CURLE_OK) { |
132 | 0 | stats->mime_headers += header_count; |
133 | 0 | } else { |
134 | 0 | curl_slist_free_all(headers); |
135 | 0 | } |
136 | 0 | } |
137 | 0 | } Unexecuted instantiation: request_data.cc:void proto_fuzzer::(anonymous namespace)::ApplyPartMetadata<curl::fuzzer::proto::MimePart>(curl_mimepart*, curl::fuzzer::proto::MimePart const&, proto_fuzzer::RequestBuildStats*) Unexecuted instantiation: request_data.cc:void proto_fuzzer::(anonymous namespace)::ApplyPartMetadata<curl::fuzzer::proto::MimeDataPart>(curl_mimepart*, curl::fuzzer::proto::MimeDataPart const&, proto_fuzzer::RequestBuildStats*) |
138 | | |
139 | | /// Copy bounded binary data into a MIME part. curl_mime_data accepts an |
140 | | /// explicit size, so embedded NUL bytes remain fuzzable here unlike in the |
141 | | /// metadata and header APIs. |
142 | 0 | void ApplyPartData(curl_mimepart* part, const std::string& data) { |
143 | 0 | const std::size_t size = std::min(data.size(), scenario_limits::kMaxMimeDataBytes); |
144 | 0 | const char* bytes = data.empty() ? "" : data.data(); |
145 | 0 | (void)curl_mime_data(part, bytes, size); |
146 | 0 | } |
147 | | |
148 | | /// Populate the fixed-depth child body and debit the shared total-part budget. |
149 | | /// Returning an empty MIME object when the child list is empty is intentional: |
150 | | /// curl's empty multipart serialization is useful coverage and remains cheap. |
151 | | void PopulateSubparts(curl_mime* mime, const curl::fuzzer::proto::MimeSubparts& source, std::size_t* remaining_parts, |
152 | 0 | RequestBuildStats* stats) { |
153 | 0 | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxNestedMimeParts, source.parts_size()); |
154 | 0 | for (std::size_t i = 0; i < count && *remaining_parts != 0; ++i) { |
155 | 0 | curl_mimepart* part = curl_mime_addpart(mime); |
156 | 0 | if (part == nullptr) { |
157 | 0 | break; |
158 | 0 | } |
159 | 0 | --*remaining_parts; |
160 | 0 | ++stats->mime_parts; |
161 | 0 | const auto& proto_part = source.parts(static_cast<int>(i)); |
162 | 0 | ApplyPartMetadata(part, proto_part, stats); |
163 | 0 | ApplyPartData(part, proto_part.data()); |
164 | 0 | } |
165 | 0 | } |
166 | | |
167 | | /// Construct the top MIME tree. curl_mime_subparts transfers ownership only |
168 | | /// on success, so failed attachments are freed immediately while successful |
169 | | /// ones are left for the top-level root to release recursively. |
170 | 1 | curl_mime* BuildMimePost(CURL* easy, const curl::fuzzer::proto::MimePost& source, RequestBuildStats* stats) { |
171 | 1 | curl_mime* mime = curl_mime_init(easy); |
172 | 1 | if (mime == nullptr) { |
173 | 0 | return nullptr; |
174 | 0 | } |
175 | | |
176 | 1 | std::size_t remaining_parts = scenario_limits::kMaxTotalMimeParts; |
177 | 1 | const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxTopLevelMimeParts, source.parts_size()); |
178 | 1 | for (std::size_t i = 0; i < count && remaining_parts != 0; ++i) { |
179 | 0 | curl_mimepart* part = curl_mime_addpart(mime); |
180 | 0 | if (part == nullptr) { |
181 | 0 | break; |
182 | 0 | } |
183 | 0 | --remaining_parts; |
184 | 0 | ++stats->mime_parts; |
185 | 0 | const auto& proto_part = source.parts(static_cast<int>(i)); |
186 | 0 | ApplyPartMetadata(part, proto_part, stats); |
187 | |
|
188 | 0 | switch (proto_part.content_case()) { |
189 | 0 | case curl::fuzzer::proto::MimePart::kData: |
190 | 0 | ApplyPartData(part, proto_part.data()); |
191 | 0 | break; |
192 | 0 | case curl::fuzzer::proto::MimePart::kSubparts: { |
193 | 0 | curl_mime* subparts = curl_mime_init(easy); |
194 | 0 | if (subparts == nullptr) { |
195 | 0 | break; |
196 | 0 | } |
197 | 0 | PopulateSubparts(subparts, proto_part.subparts(), &remaining_parts, stats); |
198 | 0 | if (curl_mime_subparts(part, subparts) != CURLE_OK) { |
199 | 0 | curl_mime_free(subparts); |
200 | 0 | } |
201 | 0 | break; |
202 | 0 | } |
203 | 0 | case curl::fuzzer::proto::MimePart::CONTENT_NOT_SET: |
204 | 0 | default: |
205 | 0 | break; |
206 | 0 | } |
207 | 0 | } |
208 | 1 | return mime; |
209 | 1 | } |
210 | | |
211 | | } // namespace |
212 | | |
213 | | /// Borrow and cap the immutable upload shape once, before libcurl receives a |
214 | | /// userdata pointer. ScenarioRunner keeps the protobuf alive for the complete |
215 | | /// drive, so a view removes a per-input body copy without weakening callback |
216 | | /// lifetime. The absent-message fallback deliberately avoids a 16 KiB |
217 | | /// allocation: Read() synthesizes the same `U` bytes the old global callback |
218 | | /// produced while retaining per-run cursor isolation. |
219 | | UploadScriptState::UploadScriptState(const curl::fuzzer::proto::Scenario& scenario) |
220 | 10.1k | : data_(), |
221 | 10.1k | read_step_count_(0), |
222 | 10.1k | total_size_(scenario_limits::kMaxUploadBytes), |
223 | 10.1k | offset_(0), |
224 | 10.1k | next_read_size_(0), |
225 | 10.1k | terminal_(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF), |
226 | 10.1k | seek_result_(curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK), |
227 | 10.1k | scripted_(scenario.has_upload()) { |
228 | 10.1k | if (!scripted_) { |
229 | 10.0k | return; |
230 | 10.0k | } |
231 | | |
232 | 57 | const auto& upload = scenario.upload(); |
233 | 57 | const std::size_t data_size = std::min(upload.data().size(), scenario_limits::kMaxUploadBytes); |
234 | 57 | data_ = std::string_view(upload.data().data(), data_size); |
235 | 57 | total_size_ = data_.size(); |
236 | 57 | terminal_ = upload.terminal(); |
237 | 57 | seek_result_ = upload.seek_result(); |
238 | | |
239 | 57 | read_step_count_ = std::min<std::size_t>(upload.read_sizes_size(), scenario_limits::kMaxUploadReadSteps); |
240 | 57 | for (std::size_t i = 0; i < read_step_count_; ++i) { |
241 | 0 | const std::size_t requested = upload.read_sizes(static_cast<int>(i)); |
242 | | // Zero-as-one ensures every retained step makes progress; see the schema |
243 | | // comment for why zero is not treated as an early EOF sentinel. |
244 | 0 | read_sizes_[i] = std::max<std::size_t>(1, std::min(requested, scenario_limits::kMaxUploadReadSize)); |
245 | 0 | } |
246 | 57 | } |
247 | | |
248 | | /// Return bytes from either the explicit payload or the allocation-free |
249 | | /// fallback. Terminal outcomes are emitted only after all data is consumed so |
250 | | /// a mutation can independently control fragmentation and completion policy. |
251 | 1.18k | std::size_t UploadScriptState::Read(char* buffer, std::size_t capacity) { |
252 | 1.18k | if (offset_ >= total_size_) { |
253 | 487 | return terminal_ == curl::fuzzer::proto::UPLOAD_TERMINAL_ABORT ? CURL_READFUNC_ABORT : 0; |
254 | 487 | } |
255 | 693 | if (buffer == nullptr || capacity == 0) { |
256 | 0 | return 0; |
257 | 0 | } |
258 | | |
259 | 693 | std::size_t chunk_limit = capacity; |
260 | 693 | if (next_read_size_ < read_step_count_) { |
261 | 0 | chunk_limit = std::min(chunk_limit, read_sizes_[next_read_size_++]); |
262 | 0 | } |
263 | 693 | const std::size_t count = std::min(chunk_limit, total_size_ - offset_); |
264 | 693 | if (scripted_) { |
265 | 4 | std::memcpy(buffer, data_.data() + offset_, count); |
266 | 689 | } else { |
267 | 689 | std::memset(buffer, 'U', count); |
268 | 689 | } |
269 | 693 | offset_ += count; |
270 | 693 | return count; |
271 | 693 | } |
272 | | |
273 | | /// Model only seeks curl can meaningfully request from a bounded memory |
274 | | /// source. Explicit range checks avoid signed overflow and keep a bogus |
275 | | /// mutation from wrapping into an in-bounds cursor. |
276 | 26 | int UploadScriptState::Seek(curl_off_t requested_offset, int origin) { |
277 | 26 | switch (seek_result_) { |
278 | 26 | case curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK: |
279 | 26 | return CURL_SEEKFUNC_CANTSEEK; |
280 | 0 | case curl::fuzzer::proto::UPLOAD_SEEK_FAIL: |
281 | 0 | return CURL_SEEKFUNC_FAIL; |
282 | 0 | case curl::fuzzer::proto::UPLOAD_SEEK_OK: |
283 | 0 | break; |
284 | 0 | default: |
285 | 0 | return CURL_SEEKFUNC_CANTSEEK; |
286 | 26 | } |
287 | | |
288 | 0 | const curl_off_t current = static_cast<curl_off_t>(offset_); |
289 | 0 | const curl_off_t end = static_cast<curl_off_t>(total_size_); |
290 | 0 | curl_off_t base = 0; |
291 | 0 | switch (origin) { |
292 | 0 | case SEEK_SET: |
293 | 0 | base = 0; |
294 | 0 | break; |
295 | 0 | case SEEK_CUR: |
296 | 0 | base = current; |
297 | 0 | break; |
298 | 0 | case SEEK_END: |
299 | 0 | base = end; |
300 | 0 | break; |
301 | 0 | default: |
302 | 0 | return CURL_SEEKFUNC_FAIL; |
303 | 0 | } |
304 | | |
305 | | // Both base and end are at most 16 KiB. Comparing the requested delta to |
306 | | // these small bounds before addition handles even CURL_OFF_T_MIN safely. |
307 | 0 | if (requested_offset < -base || requested_offset > end - base) { |
308 | 0 | return CURL_SEEKFUNC_FAIL; |
309 | 0 | } |
310 | 0 | offset_ = static_cast<std::size_t>(base + requested_offset); |
311 | 0 | next_read_size_ = 0; |
312 | 0 | return CURL_SEEKFUNC_OK; |
313 | 0 | } |
314 | | |
315 | | /// Multiplication is normally benign because curl uses size=1, but callbacks |
316 | | /// are an API boundary. Abort an impossible overflowing pair: saturating to |
317 | | /// SIZE_MAX would let Read() copy into a buffer whose real extent is unknown. |
318 | 1.18k | std::size_t UploadScriptState::ReadCallback(char* buffer, std::size_t size, std::size_t nitems, void* userdata) { |
319 | 1.18k | if (userdata == nullptr || size == 0 || nitems == 0) { |
320 | 0 | return 0; |
321 | 0 | } |
322 | 1.18k | const std::size_t max = std::numeric_limits<std::size_t>::max(); |
323 | 1.18k | if (nitems > max / size) { |
324 | 0 | return CURL_READFUNC_ABORT; |
325 | 0 | } |
326 | 1.18k | return static_cast<UploadScriptState*>(userdata)->Read(buffer, size * nitems); |
327 | 1.18k | } |
328 | | |
329 | | /// Keep the C callback a one-line type bridge so all outcome/cursor behaviour |
330 | | /// remains directly unit-testable in Seek(). |
331 | 26 | int UploadScriptState::SeekCallback(void* userdata, curl_off_t offset, int origin) { |
332 | 26 | if (userdata == nullptr) { |
333 | 0 | return CURL_SEEKFUNC_FAIL; |
334 | 0 | } |
335 | 26 | return static_cast<UploadScriptState*>(userdata)->Seek(offset, origin); |
336 | 26 | } |
337 | | |
338 | 0 | std::size_t UploadScriptState::data_size() const { return total_size_; } |
339 | | |
340 | 0 | std::size_t UploadScriptState::read_step_count() const { return read_step_count_; } |
341 | | |
342 | 0 | std::size_t UploadScriptState::offset() const { return offset_; } |
343 | | |
344 | 0 | bool UploadScriptState::scripted() const { return scripted_; } |
345 | | |
346 | | /// Build both pointer-valued request features and attach them to the easy |
347 | | /// handle. Setup errors are deliberately non-fatal: malformed or partially |
348 | | /// allocated scenarios should still exercise whatever curl state was built. |
349 | | ScenarioRequestData::ScenarioRequestData(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) |
350 | 10.1k | : easy_(easy), |
351 | 10.1k | request_headers_(nullptr), |
352 | 10.1k | mime_post_(nullptr), |
353 | 10.1k | upload_state_(scenario), |
354 | 10.1k | upload_callbacks_installed_(false) { |
355 | 10.1k | if (easy_ == nullptr) { |
356 | 0 | return; |
357 | 0 | } |
358 | | |
359 | 10.1k | if (NeedsUploadCallbacks(scenario)) { |
360 | | // Install a per-run memory source even when Scenario.upload is absent but |
361 | | // CURLOPT_UPLOAD may enable reads. Without the fallback, curl would read |
362 | | // stdin and could block OSS-Fuzz. The state remains scoped to the complete |
363 | | // multi-handle drive so retries cannot share a cursor across iterations. |
364 | 1.36k | upload_callbacks_installed_ = true; |
365 | 1.36k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, &UploadScriptState::ReadCallback); |
366 | 1.36k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, &upload_state_); |
367 | 1.36k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, &UploadScriptState::SeekCallback); |
368 | 1.36k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, &upload_state_); |
369 | 1.36k | } |
370 | | |
371 | 10.1k | request_headers_ = |
372 | 10.1k | BuildHeaderList(scenario.request_headers(), scenario_limits::kMaxRequestHeaders, &stats_.request_headers); |
373 | 10.1k | if (request_headers_ != nullptr) { |
374 | 97 | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, request_headers_); |
375 | 97 | } |
376 | | |
377 | 10.1k | if (scenario.has_mime_post()) { |
378 | 1 | mime_post_ = BuildMimePost(easy_, scenario.mime_post(), &stats_); |
379 | 1 | if (mime_post_ != nullptr) { |
380 | 1 | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, mime_post_); |
381 | 1 | } |
382 | 1 | } |
383 | 10.1k | } |
384 | | |
385 | | /// Detach resources while the easy handle is valid, then free them. libcurl |
386 | | /// does not copy headers, MIME roots, or callback userdata, so releasing any |
387 | | /// one before the mock drive ends would create a use-after-free; relying on |
388 | | /// easy cleanup to own header/MIME allocations would instead leak iterations. |
389 | 10.1k | ScenarioRequestData::~ScenarioRequestData() { |
390 | 10.1k | if (easy_ != nullptr) { |
391 | | // Clear callbacks before their userdata member is destroyed. There is no |
392 | | // perform in this destructor, but making the handle non-dangling keeps the |
393 | | // ownership rule robust if cleanup later gains diagnostics or getinfo. |
394 | 10.1k | if (upload_callbacks_installed_) { |
395 | 1.36k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, nullptr); |
396 | 1.36k | (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, nullptr); |
397 | 1.36k | (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, nullptr); |
398 | 1.36k | (void)curl_easy_setopt(easy_, CURLOPT_READDATA, nullptr); |
399 | 1.36k | } |
400 | 10.1k | if (mime_post_ != nullptr) { |
401 | 1 | (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, nullptr); |
402 | 1 | } |
403 | 10.1k | if (request_headers_ != nullptr) { |
404 | 97 | (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, nullptr); |
405 | 97 | } |
406 | 10.1k | } |
407 | 10.1k | curl_mime_free(mime_post_); |
408 | 10.1k | curl_slist_free_all(request_headers_); |
409 | 10.1k | } |
410 | | |
411 | | /// Expose cap-aware counts without exposing or transferring the owned curl |
412 | | /// pointers themselves. |
413 | 0 | const RequestBuildStats& ScenarioRequestData::stats() const { return stats_; } |
414 | | |
415 | 0 | const UploadScriptState& ScenarioRequestData::upload_state() const { return upload_state_; } |
416 | | |
417 | 0 | bool ScenarioRequestData::upload_callbacks_installed() const { return upload_callbacks_installed_; } |
418 | | |
419 | | } // namespace proto_fuzzer |