Coverage Report

Created: 2026-09-01 07:00

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
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