22namespace proto_fuzzer {
32const char* BoundedCString(
const std::string& value, std::size_t limit, std::string* truncated) {
33 if (value.size() <= limit) {
36 truncated->assign(value.data(), limit);
37 return truncated->c_str();
46bool NeedsUploadCallbacks(
const curl::fuzzer::proto::Scenario& scenario) {
50 if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET || scenario.has_upload()) {
54 for (std::size_t index = 0; index < option_count; ++index) {
55 const auto& option = scenario.options(
static_cast<int>(index));
56 if (option.option_id() == curl::fuzzer::proto::CURLOPT_UPLOAD ||
57 option.option_id() == curl::fuzzer::proto::CURLOPT_POST) {
67const char* MimeEncoderName(curl::fuzzer::proto::MimeEncoder encoder) {
69 case curl::fuzzer::proto::MIME_ENCODER_BINARY:
71 case curl::fuzzer::proto::MIME_ENCODER_8BIT:
73 case curl::fuzzer::proto::MIME_ENCODER_7BIT:
75 case curl::fuzzer::proto::MIME_ENCODER_BASE64:
77 case curl::fuzzer::proto::MIME_ENCODER_QUOTED_PRINTABLE:
78 return "quoted-printable";
79 case curl::fuzzer::proto::MIME_ENCODER_UNSPECIFIED:
89template <
typename RepeatedBytes>
90CurlSlistPtr BuildStringList(
const RepeatedBytes& values, std::size_t count_limit, std::size_t value_limit,
91 std::size_t* applied) {
93 std::string truncated;
94 const std::size_t count = std::min<std::size_t>(count_limit, values.size());
95 for (std::size_t i = 0; i < count; ++i) {
96 const std::string& value = values.Get(
static_cast<int>(i));
97 curl_slist* appended = curl_slist_append(list.get(), BoundedCString(value, value_limit, &truncated));
98 if (appended ==
nullptr) {
103 (void)list.release();
104 list.reset(appended);
113template <
typename ProtoPart>
114void ApplyPartMetadata(curl_mimepart* part,
const ProtoPart& source,
RequestBuildStats* stats) {
117 std::string truncated;
118 if (!source.name().empty()) {
121 if (!source.filename().empty()) {
124 if (!source.content_type().empty()) {
127 if (
const char* encoder = MimeEncoderName(source.encoder())) {
128 (void)curl_mime_encoder(part, encoder);
131 std::size_t header_count = 0;
134 if (headers !=
nullptr) {
138 const CURLcode result = curl_mime_headers(part, headers.get(), 1);
139 if (result == CURLE_OK) {
140 (void)headers.release();
141 stats->mime_headers += header_count;
149void ApplyPartData(curl_mimepart* part,
const std::string& data) {
151 const char* bytes = data.empty() ?
"" : data.data();
152 (void)curl_mime_data(part, bytes, size);
159void ApplyGeneratedPartData(curl_mimepart* part,
const curl::fuzzer::proto::GeneratedBytes& source,
162 if (pattern_size == 0 || *remaining_generated_bytes == 0) {
163 (void)curl_mime_data(part,
"", 0);
167 const std::size_t max_repeat_count = *remaining_generated_bytes / pattern_size;
168 const std::size_t repeat_count =
169 std::min<std::size_t>(
static_cast<std::size_t
>(source.repeat_count()), max_repeat_count);
170 const std::size_t materialized_size = repeat_count * pattern_size;
172 std::string materialized(materialized_size,
'\0');
173 for (std::size_t offset = 0; offset < materialized_size; offset += pattern_size) {
174 std::copy_n(source.pattern().data(), pattern_size, materialized.data() + offset);
176 const char* bytes = materialized.empty() ?
"" : materialized.data();
177 if (curl_mime_data(part, bytes, materialized.size()) == CURLE_OK) {
178 stats->generated_mime_bytes += materialized.size();
180 *remaining_generated_bytes -= materialized.size();
186void PopulateSubparts(curl_mime* mime,
const curl::fuzzer::proto::MimeSubparts& source, std::size_t* remaining_parts,
189 for (std::size_t i = 0; i < count && *remaining_parts != 0; ++i) {
190 curl_mimepart* part = curl_mime_addpart(mime);
191 if (part ==
nullptr) {
196 const auto& proto_part = source.parts(
static_cast<int>(i));
197 ApplyPartMetadata(part, proto_part, stats);
198 if (proto_part.content_case() == curl::fuzzer::proto::MimeDataPart::kGeneratedData) {
199 ApplyGeneratedPartData(part, proto_part.generated_data(), remaining_generated_bytes, stats);
203 ApplyPartData(part, proto_part.data());
214 if (mime ==
nullptr) {
221 for (std::size_t i = 0; i < count && remaining_parts != 0; ++i) {
222 curl_mimepart* part = curl_mime_addpart(mime.get());
223 if (part ==
nullptr) {
228 const auto& proto_part = source.parts(
static_cast<int>(i));
229 ApplyPartMetadata(part, proto_part, stats);
231 switch (proto_part.content_case()) {
232 case curl::fuzzer::proto::MimePart::kData:
233 ApplyPartData(part, proto_part.data());
235 case curl::fuzzer::proto::MimePart::kGeneratedData:
236 ApplyGeneratedPartData(part, proto_part.generated_data(), &remaining_generated_bytes, stats);
238 case curl::fuzzer::proto::MimePart::kUploadCallback:
239 if (upload_state !=
nullptr && upload_state->InstallAsMimePart(part) == CURLE_OK) {
240 ++stats->mime_callback_parts;
243 case curl::fuzzer::proto::MimePart::kSubparts: {
245 if (subparts ==
nullptr) {
248 PopulateSubparts(subparts.get(), proto_part.subparts(), &remaining_parts, &remaining_generated_bytes, stats);
249 if (curl_mime_subparts(part, subparts.get()) == CURLE_OK) {
250 (void)subparts.release();
254 case curl::fuzzer::proto::MimePart::CONTENT_NOT_SET:
275 total_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? 0 : scenario_limits::kMaxUploadBytes),
276 max_read_size_(scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET ? scenario_limits::kMaxTelnetUploadReadSize
277 : scenario_limits::kMaxUploadReadSize),
281 terminal_(curl::fuzzer::proto::UPLOAD_TERMINAL_EOF),
282 seek_result_(curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK),
283 before_read_callback_(nullptr),
284 before_read_userdata_(nullptr),
285 scripted_(scenario.has_upload()) {
290 const auto& upload = scenario.upload();
291 const std::size_t data_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET
294 const std::size_t
data_size = std::min(upload.data().size(), data_limit);
295 data_ = std::string_view(upload.data().data(),
data_size);
296 total_size_ = data_.size();
297 terminal_ = upload.terminal();
298 if (scenario.scheme() != curl::fuzzer::proto::SCHEME_TELNET &&
299 terminal_ == curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE) {
302 terminal_ = curl::fuzzer::proto::UPLOAD_TERMINAL_EOF;
304 seek_result_ = upload.seek_result();
306 const std::size_t read_step_limit = scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET
309 read_step_count_ = std::min<std::size_t>(upload.read_sizes_size(), read_step_limit);
310 for (std::size_t i = 0; i < read_step_count_; ++i) {
311 const std::size_t requested = upload.read_sizes(
static_cast<int>(i));
314 read_sizes_[i] = std::max<std::size_t>(1, std::min(requested, max_read_size_));
332 if (before_read_callback_ !=
nullptr) {
333 before_read_callback_(before_read_userdata_);
335 if (offset_ >= total_size_) {
337 case curl::fuzzer::proto::UPLOAD_TERMINAL_ABORT:
338 return CURL_READFUNC_ABORT;
339 case curl::fuzzer::proto::UPLOAD_TERMINAL_PAUSE:
340 return CURL_READFUNC_PAUSE;
341 case curl::fuzzer::proto::UPLOAD_TERMINAL_EOF:
346 if (buffer ==
nullptr || capacity == 0) {
350 std::size_t chunk_limit = std::min(capacity, max_read_size_);
351 if (next_read_size_ < read_step_count_) {
352 chunk_limit = std::min(chunk_limit, read_sizes_[next_read_size_++]);
354 const std::size_t count = std::min(chunk_limit, total_size_ - offset_);
356 std::memcpy(buffer, data_.data() + offset_, count);
358 std::memset(buffer,
'U', count);
374 switch (seek_result_) {
375 case curl::fuzzer::proto::UPLOAD_SEEK_CANTSEEK:
376 return CURL_SEEKFUNC_CANTSEEK;
377 case curl::fuzzer::proto::UPLOAD_SEEK_FAIL:
378 return CURL_SEEKFUNC_FAIL;
379 case curl::fuzzer::proto::UPLOAD_SEEK_OK:
382 return CURL_SEEKFUNC_CANTSEEK;
385 const curl_off_t current =
static_cast<curl_off_t
>(offset_);
386 const curl_off_t end =
static_cast<curl_off_t
>(total_size_);
399 return CURL_SEEKFUNC_FAIL;
404 if (requested_offset < -base || requested_offset > end - base) {
405 return CURL_SEEKFUNC_FAIL;
407 offset_ =
static_cast<std::size_t
>(base + requested_offset);
409 return CURL_SEEKFUNC_OK;
417std::size_t UploadScriptState::ReadCallback(
char* buffer, std::size_t size, std::size_t nitems,
void* userdata) {
418 if (userdata ==
nullptr || size == 0 || nitems == 0) {
421 const std::size_t max = std::numeric_limits<std::size_t>::max();
422 if (nitems > max / size) {
423 return CURL_READFUNC_ABORT;
431int UploadScriptState::SeekCallback(
void* userdata, curl_off_t offset,
int origin) {
432 if (userdata ==
nullptr) {
433 return CURL_SEEKFUNC_FAIL;
459 if (part ==
nullptr) {
460 return CURLE_BAD_FUNCTION_ARGUMENT;
462 return curl_mime_data_cb(part,
static_cast<curl_off_t
>(total_size_), &UploadScriptState::ReadCallback,
463 &UploadScriptState::SeekCallback,
nullptr,
this);
468void UploadScriptState::SetBeforeReadCallback(BeforeReadCallback callback,
void* userdata) {
469 before_read_callback_ = callback;
470 before_read_userdata_ = userdata;
484 bool apply_resolve_entries)
486 request_headers_(nullptr),
487 resolve_entries_(nullptr),
488 telnet_options_(nullptr),
490 upload_state_(scenario),
492 upload_callbacks_installed_(false),
493 resolve_entries_ready_(!apply_resolve_entries) {
494 if (easy_ ==
nullptr) {
501 bool resolve_entries_built =
false;
502 if (apply_resolve_entries) {
508 curl_slist* appended = curl_slist_append(resolve_entries_.get(),
"resolve.test:80:127.0.0.1");
509 if (appended !=
nullptr) {
510 (void)resolve_entries_.release();
511 resolve_entries_.reset(appended);
512 resolve_entries_built =
true;
519 if (scenario.scheme() == curl::fuzzer::proto::SCHEME_TELNET) {
520 telnet_options_ = BuildStringList(scenario.telnet_options(), scenario_limits::kMaxTelnetOptions,
521 scenario_limits::kMaxTelnetOptionBytes, &stats_.telnet_options);
523 request_headers_ = BuildStringList(scenario.request_headers(), scenario_limits::kMaxRequestHeaders,
524 scenario_limits::kMaxMetadataBytes, &stats_.request_headers);
525 if (scenario.has_mime_post()) {
526 mime_post_ = BuildMimePost(easy_, scenario.mime_post(), &upload_state_, &stats_);
530 if (scenario.scheme() == curl::fuzzer::proto::SCHEME_SMTP || scenario.scheme() == curl::fuzzer::proto::SCHEME_SMTPS ||
531 !scenario.mail_rcpt().empty()) {
534 mail_rcpt_ = BuildStringList(scenario.mail_rcpt(), scenario_limits::kMaxMailRecipients,
535 scenario_limits::kMaxMailRecipientBytes, &stats_.mail_rcpt);
538 if (resolve_entries_built) {
539 resolve_entries_ready_ = curl_easy_setopt(easy_, CURLOPT_RESOLVE, resolve_entries_.get()) == CURLE_OK;
542 if (NeedsUploadCallbacks(scenario)) {
548 upload_callbacks_installed_ =
true;
549 (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION, &UploadScriptState::ReadCallback);
550 (void)curl_easy_setopt(easy_, CURLOPT_READDATA, &upload_state_);
551 (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION, &UploadScriptState::SeekCallback);
552 (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA, &upload_state_);
555 if (telnet_options_ !=
nullptr) {
556 (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS, telnet_options_.get());
558 if (mail_rcpt_ !=
nullptr) {
559 (void)curl_easy_setopt(easy_, CURLOPT_MAIL_RCPT, mail_rcpt_.get());
561 if (request_headers_ !=
nullptr) {
562 (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER, request_headers_.get());
564 if (mime_post_ !=
nullptr) {
565 (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST, mime_post_.get());
575 if (easy_ !=
nullptr) {
579 if (upload_callbacks_installed_) {
580 (void)curl_easy_setopt(easy_, CURLOPT_SEEKFUNCTION,
nullptr);
581 (void)curl_easy_setopt(easy_, CURLOPT_SEEKDATA,
nullptr);
582 (void)curl_easy_setopt(easy_, CURLOPT_READFUNCTION,
nullptr);
583 (void)curl_easy_setopt(easy_, CURLOPT_READDATA,
nullptr);
585 if (mime_post_ !=
nullptr) {
586 (void)curl_easy_setopt(easy_, CURLOPT_MIMEPOST,
nullptr);
588 if (request_headers_ !=
nullptr) {
589 (void)curl_easy_setopt(easy_, CURLOPT_HTTPHEADER,
nullptr);
591 if (resolve_entries_ !=
nullptr) {
592 (void)curl_easy_setopt(easy_, CURLOPT_RESOLVE,
nullptr);
594 if (telnet_options_ !=
nullptr) {
595 (void)curl_easy_setopt(easy_, CURLOPT_TELNETOPTIONS,
nullptr);
597 if (mail_rcpt_ !=
nullptr) {
598 (void)curl_easy_setopt(easy_, CURLOPT_MAIL_RCPT,
nullptr);
634 upload_state_.SetBeforeReadCallback(callback, userdata);
~ScenarioRequestData()
Detach pointer options/callbacks while the easy handle is valid, then release request allocations.
const RequestBuildStats & stats() const
Return resource counts after caps and allocation failures were applied, without exposing or transferr...
const UploadScriptState & upload_state() const
Return the callback state retained for the complete drive.
bool resolve_entries_ready() const
Return whether resolver-only slist construction installed its mandatory final loopback mapping.
void SetBeforeUploadReadCallback(UploadScriptState::BeforeReadCallback callback, void *userdata)
Arrange protocol-specific work immediately before every upload read.
bool upload_callbacks_installed() const
Return whether this scenario needed raw upload callbacks.
ScenarioRequestData(CURL *easy, const curl::fuzzer::proto::Scenario &scenario, bool apply_resolve_entries=false)
Construct and apply the protocol-specific pointer-valued fields in scenario to easy.
Cursor and policy behind the request read/seek callbacks.
std::size_t Read(char *buffer, std::size_t capacity)
Copy the next bounded chunk and advance the cursor, using either the explicit payload or the allocati...
int Seek(curl_off_t requested_offset, int origin)
Apply the configured seek outcome and, for OK, move the bounded cursor.
CURLcode InstallAsMimePart(curl_mimepart *part)
Install the same bounded read/seek cursor behind a public MIME callback part.
std::size_t read_step_count() const
std::size_t offset() const
std::size_t seek_calls() const
UploadScriptState(const curl::fuzzer::proto::Scenario &scenario)
Borrow and cap a scripted upload source before libcurl receives a userdata pointer.
void(*)(void *) BeforeReadCallback
C-compatible hook invoked immediately before each read result is chosen.
std::size_t data_size() const
std::unique_ptr< curl_slist, CurlSlistDeleter > CurlSlistPtr
Unique ownership of a caller-owned curl_slist.
std::unique_ptr< curl_mime, CurlMimeDeleter > CurlMimePtr
Unique ownership of a caller-owned curl_mime tree.
std::size_t RuntimeOptionCount(const curl::fuzzer::proto::Scenario &scenario)
Return the runtime-visible option prefix length.
Translates Scenario SetOption messages into curl_easy_setopt calls, plus the fixed baseline setopts t...
Owns pointer-valued HTTP headers, TELNET options, MIME state, and upload callback state derived from ...
Shared complexity limits for structured scenarios.
constexpr std::size_t kMaxUploadReadSteps
Short-read scripts need several transitions, not mutation-sized sequences.
constexpr std::size_t kMaxMimeDataBytes
MIME payloads remain large enough to cross encoder buffer boundaries.
constexpr std::size_t kMaxTelnetUploadReadSteps
OSS-Fuzz's local AF_UNIX peer normally fills curl's 4 KiB TELNET reads, so the bounded 8 KiB response...
constexpr std::size_t kMaxNestedMimeParts
One bounded child layer is sufficient to enter recursive MIME code.
constexpr std::size_t kMaxTotalMimeParts
Share a total budget so many parents cannot multiply their child caps.
constexpr std::size_t kMaxUploadBytes
Maximum callback-backed body bytes visible in one scenario.
constexpr std::size_t kMaxMimeHeadersPerPart
Per-part headers are bounded separately from the part count.
constexpr std::size_t kMaxResolveEntries
Host-cache mutations need only a compact sequence of add/remove/wildcard entries.
constexpr std::size_t kMaxResolveEntryBytes
CURLOPT_RESOLVE consumes NUL-terminated host:port:address records.
constexpr std::size_t kMaxGeneratedMimeDataBytes
Retain both sides of curl's 64 KiB send-buffer boundary.
constexpr std::size_t kMaxTelnetUploadBytes
TELNET writes upload bytes while its blocking driver owns the thread, so keep total callback work sma...
constexpr std::size_t kMaxTopLevelMimeParts
Bound top-level MIME traversal before curl recursively serializes it.
constexpr std::size_t kMaxMetadataBytes
Metadata reaches NUL-terminated APIs, where giant suffixes add no coverage.
constexpr std::size_t kMaxGeneratedMimePatternBytes
Longer repeated patterns are better expressed as ordinary MIME data and make the compact source less ...
Counts the resources that survived allocation and runtime budgets.