Coverage Report

Created: 2026-09-14 07:12

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