curl-fuzzer C++ reference
Loading...
Searching...
No Matches
api_lifecycle.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#include <curl/curl.h>
13#include <curl/curlver.h>
14#include <curl/easy.h>
15#include <curl/header.h>
16#include <curl/options.h>
17#include <curl/urlapi.h>
18
19#include <algorithm>
20#include <array>
21#include <cstddef>
22#include <cstdint>
23#include <limits>
24#include <string>
25#include <string_view>
26
28
29namespace proto_fuzzer {
30
31namespace {
32
33constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO;
34constexpr std::size_t kMaxResultHeaders = 16;
35
38constexpr CURLUPart kUrlParts[] = {
39 CURLUPART_URL, CURLUPART_SCHEME, CURLUPART_USER, CURLUPART_PASSWORD, CURLUPART_OPTIONS, CURLUPART_HOST,
40 CURLUPART_PORT, CURLUPART_PATH, CURLUPART_QUERY, CURLUPART_FRAGMENT, CURLUPART_ZONEID,
41};
42
46void ProbeUrlPart(CURLU* url, CURLUPart part, unsigned int flags) {
47 char* result = nullptr;
48 if (curl_url_get(url, part, &result, flags) == CURLUE_OK) {
49 curl_free(result);
50 }
51}
52
54enum class InfoResultType {
55 kString,
56 kLong,
57 kDouble,
58 kOffset,
59 kSocket,
60 kCertificateInfo,
61 kTlsSessionInfo,
62 kOwnedSlist,
63 kUnknown,
64};
65
69struct InfoDescriptor {
70 CURLINFO info;
71 InfoResultType result_type;
72};
73
74// Put one value from every dispatch family first so even a small corpus seed
75// reaches all typed getinfo paths; the remaining entries broaden state and
76// result-specific coverage as selectors mutate.
77constexpr InfoDescriptor kInfoDescriptors[] = {
78 {CURLINFO_EFFECTIVE_URL, InfoResultType::kString},
79 {CURLINFO_RESPONSE_CODE, InfoResultType::kLong},
80 {CURLINFO_TOTAL_TIME, InfoResultType::kDouble},
81 {CURLINFO_SIZE_DOWNLOAD_T, InfoResultType::kOffset},
82 {CURLINFO_ACTIVESOCKET, InfoResultType::kSocket},
83 {CURLINFO_CERTINFO, InfoResultType::kCertificateInfo},
84 {CURLINFO_TLS_SSL_PTR, InfoResultType::kTlsSessionInfo},
85 {CURLINFO_SSL_ENGINES, InfoResultType::kOwnedSlist},
86 {CURLINFO_NONE, InfoResultType::kUnknown},
87 {CURLINFO_CONTENT_TYPE, InfoResultType::kString},
88 {CURLINFO_PRIVATE, InfoResultType::kString},
89 {CURLINFO_FTP_ENTRY_PATH, InfoResultType::kString},
90 {CURLINFO_REDIRECT_URL, InfoResultType::kString},
91 {CURLINFO_PRIMARY_IP, InfoResultType::kString},
92 {CURLINFO_RTSP_SESSION_ID, InfoResultType::kString},
93 {CURLINFO_LOCAL_IP, InfoResultType::kString},
94 {CURLINFO_SCHEME, InfoResultType::kString},
95 {CURLINFO_EFFECTIVE_METHOD, InfoResultType::kString},
96 {CURLINFO_REFERER, InfoResultType::kString},
97 {CURLINFO_CAINFO, InfoResultType::kString},
98 {CURLINFO_CAPATH, InfoResultType::kString},
99 {CURLINFO_HEADER_SIZE, InfoResultType::kLong},
100 {CURLINFO_REQUEST_SIZE, InfoResultType::kLong},
101 {CURLINFO_SSL_VERIFYRESULT, InfoResultType::kLong},
102 {CURLINFO_FILETIME, InfoResultType::kLong},
103 {CURLINFO_REDIRECT_COUNT, InfoResultType::kLong},
104 {CURLINFO_HTTP_CONNECTCODE, InfoResultType::kLong},
105 {CURLINFO_HTTPAUTH_AVAIL, InfoResultType::kLong},
106 {CURLINFO_PROXYAUTH_AVAIL, InfoResultType::kLong},
107 {CURLINFO_OS_ERRNO, InfoResultType::kLong},
108 {CURLINFO_NUM_CONNECTS, InfoResultType::kLong},
109 {CURLINFO_CONDITION_UNMET, InfoResultType::kLong},
110 {CURLINFO_RTSP_CLIENT_CSEQ, InfoResultType::kLong},
111 {CURLINFO_RTSP_SERVER_CSEQ, InfoResultType::kLong},
112 {CURLINFO_RTSP_CSEQ_RECV, InfoResultType::kLong},
113 {CURLINFO_PRIMARY_PORT, InfoResultType::kLong},
114 {CURLINFO_LOCAL_PORT, InfoResultType::kLong},
115 {CURLINFO_HTTP_VERSION, InfoResultType::kLong},
116 {CURLINFO_PROXY_SSL_VERIFYRESULT, InfoResultType::kLong},
117 {CURLINFO_PROXY_ERROR, InfoResultType::kLong},
118 {CURLINFO_NAMELOOKUP_TIME, InfoResultType::kDouble},
119 {CURLINFO_CONNECT_TIME, InfoResultType::kDouble},
120 {CURLINFO_PRETRANSFER_TIME, InfoResultType::kDouble},
121 {CURLINFO_STARTTRANSFER_TIME, InfoResultType::kDouble},
122 {CURLINFO_REDIRECT_TIME, InfoResultType::kDouble},
123 {CURLINFO_APPCONNECT_TIME, InfoResultType::kDouble},
124 {CURLINFO_SIZE_UPLOAD_T, InfoResultType::kOffset},
125 {CURLINFO_SPEED_DOWNLOAD_T, InfoResultType::kOffset},
126 {CURLINFO_SPEED_UPLOAD_T, InfoResultType::kOffset},
127 {CURLINFO_FILETIME_T, InfoResultType::kOffset},
128 {CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, InfoResultType::kOffset},
129 {CURLINFO_CONTENT_LENGTH_UPLOAD_T, InfoResultType::kOffset},
130 {CURLINFO_TOTAL_TIME_T, InfoResultType::kOffset},
131 {CURLINFO_NAMELOOKUP_TIME_T, InfoResultType::kOffset},
132 {CURLINFO_CONNECT_TIME_T, InfoResultType::kOffset},
133 {CURLINFO_PRETRANSFER_TIME_T, InfoResultType::kOffset},
134 {CURLINFO_STARTTRANSFER_TIME_T, InfoResultType::kOffset},
135 {CURLINFO_REDIRECT_TIME_T, InfoResultType::kOffset},
136 {CURLINFO_APPCONNECT_TIME_T, InfoResultType::kOffset},
137 {CURLINFO_RETRY_AFTER, InfoResultType::kOffset},
138#if LIBCURL_VERSION_NUM >= 0x080200
139 {CURLINFO_XFER_ID, InfoResultType::kOffset},
140 {CURLINFO_CONN_ID, InfoResultType::kOffset},
141#endif
142#if LIBCURL_VERSION_NUM >= 0x080600
143 {CURLINFO_QUEUE_TIME_T, InfoResultType::kOffset},
144#endif
145#if LIBCURL_VERSION_NUM >= 0x080700
146 {CURLINFO_USED_PROXY, InfoResultType::kLong},
147#endif
148#if LIBCURL_VERSION_NUM >= 0x080a00
149 {CURLINFO_POSTTRANSFER_TIME_T, InfoResultType::kOffset},
150#endif
151#if LIBCURL_VERSION_NUM >= 0x080b00
152 {CURLINFO_EARLYDATA_SENT_T, InfoResultType::kOffset},
153#endif
154#if LIBCURL_VERSION_NUM >= 0x080c00
155 {CURLINFO_HTTPAUTH_USED, InfoResultType::kLong},
156 {CURLINFO_PROXYAUTH_USED, InfoResultType::kLong},
157#endif
158#if LIBCURL_VERSION_NUM >= 0x081400
159 {CURLINFO_SIZE_DELIVERED, InfoResultType::kOffset},
160#endif
161 {CURLINFO_COOKIELIST, InfoResultType::kOwnedSlist},
162};
163
164constexpr std::size_t kInfoDescriptorCount = sizeof(kInfoDescriptors) / sizeof(kInfoDescriptors[0]);
165static_assert(kInfoDescriptorCount <= 96, "the three API result seeds cover selector indexes 0 through 95");
166
171constexpr curl_lock_data kShareData[] = {
172 CURL_LOCK_DATA_COOKIE, CURL_LOCK_DATA_DNS, CURL_LOCK_DATA_SSL_SESSION, CURL_LOCK_DATA_CONNECT, CURL_LOCK_DATA_PSL,
173 CURL_LOCK_DATA_HSTS, CURL_LOCK_DATA_NONE, CURL_LOCK_DATA_SHARE, CURL_LOCK_DATA_LAST,
174};
175constexpr std::size_t kShareDataCount = sizeof(kShareData) / sizeof(kShareData[0]);
176
186enum class ReentrantProbeKind {
187 kEasySetopt,
188 kEasyGetinfo,
189 kEasyHeader,
190 kEasyDuphandle,
191 kEasyReset,
192 kEasyPerform,
193 kEasyUpkeep,
194 kEasySend,
195 kEasyRecv,
196 kEasyPause,
197 kMultiPerform,
198 kMultiSetopt,
199 kMultiAddHandle,
200 kMultiWait,
201};
202
205struct ReentrantProbe {
206 ReentrantProbeKind kind;
207 bool needs_multi;
208};
209
220constexpr ReentrantProbe kReentrantProbes[] = {
221 {ReentrantProbeKind::kEasySetopt, false}, {ReentrantProbeKind::kEasyGetinfo, false},
222 {ReentrantProbeKind::kEasyHeader, false}, {ReentrantProbeKind::kEasyDuphandle, false},
223 {ReentrantProbeKind::kEasyReset, false}, {ReentrantProbeKind::kEasyPerform, false},
224 {ReentrantProbeKind::kEasyUpkeep, false}, {ReentrantProbeKind::kEasySend, false},
225 {ReentrantProbeKind::kEasyRecv, false}, {ReentrantProbeKind::kEasyPause, false},
226 {ReentrantProbeKind::kMultiPerform, true}, {ReentrantProbeKind::kMultiSetopt, true},
227 {ReentrantProbeKind::kMultiAddHandle, true}, {ReentrantProbeKind::kMultiWait, true},
228};
229constexpr std::size_t kReentrantProbeCount = sizeof(kReentrantProbes) / sizeof(kReentrantProbes[0]);
230
233bool RecursiveRejected(CURLcode code) { return code == CURLE_RECURSIVE_API_CALL; }
234
236bool RecursiveRejected(CURLMcode code) { return code == CURLM_RECURSIVE_API_CALL; }
237
241bool ProbePublicApiOnce(CURL* easy, CURLM* multi, ReentrantProbeKind kind) {
242 switch (kind) {
243 case ReentrantProbeKind::kEasySetopt:
244 return RecursiveRejected(curl_easy_setopt(easy, CURLOPT_REFERER, "reentrant.test"));
245 case ReentrantProbeKind::kEasyGetinfo: {
246 long response_code = 0;
247 return RecursiveRejected(curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code));
248 }
249 case ReentrantProbeKind::kEasyHeader: {
250 // curl_easy_header is reentrant and maps any guard failure to a header
251 // error code, so it never reports a recursive call here.
252 struct curl_header* header = nullptr;
253 (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
254 return false;
255 }
256 case ReentrantProbeKind::kEasyDuphandle: {
257 // duphandle is reentrant; duplicate, reset, and release immediately so
258 // the copied option pointers never outlive this callback.
259 CURL* duplicate = curl_easy_duphandle(easy);
260 if (duplicate != nullptr) {
261 curl_easy_reset(duplicate);
262 curl_easy_cleanup(duplicate);
263 }
264 return false;
265 }
266 case ReentrantProbeKind::kEasyReset:
267 // curl_easy_reset returns void, so the guard's rejection is observable
268 // only as the reset not happening. It still exercises the refusal path.
269 curl_easy_reset(easy);
270 return false;
271 case ReentrantProbeKind::kEasyPerform:
272 return RecursiveRejected(curl_easy_perform(easy));
273 case ReentrantProbeKind::kEasyUpkeep:
274 return RecursiveRejected(curl_easy_upkeep(easy));
275 case ReentrantProbeKind::kEasySend: {
276 char byte = 'x';
277 std::size_t sent = 0;
278 return RecursiveRejected(curl_easy_send(easy, &byte, 1, &sent));
279 }
280 case ReentrantProbeKind::kEasyRecv: {
281 char byte = 0;
282 std::size_t received = 0;
283 return RecursiveRejected(curl_easy_recv(easy, &byte, 1, &received));
284 }
285 case ReentrantProbeKind::kEasyPause:
286 return RecursiveRejected(curl_easy_pause(easy, CURLPAUSE_CONT));
287 case ReentrantProbeKind::kMultiPerform: {
288 int running = 0;
289 return RecursiveRejected(curl_multi_perform(multi, &running));
290 }
291 case ReentrantProbeKind::kMultiSetopt:
292 return RecursiveRejected(curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, 0L));
293 case ReentrantProbeKind::kMultiAddHandle:
294 return RecursiveRejected(curl_multi_add_handle(multi, easy));
295 case ReentrantProbeKind::kMultiWait: {
296 int numfds = 0;
297 return RecursiveRejected(curl_multi_wait(multi, nullptr, 0, 0, &numfds));
298 }
299 }
300 return false;
301}
302
304void ProbeInfoDescriptor(CURL* easy, const InfoDescriptor& descriptor) {
305 switch (descriptor.result_type) {
306 case InfoResultType::kString: {
307 char* result = nullptr;
308 (void)curl_easy_getinfo(easy, descriptor.info, &result);
309 return;
310 }
311 case InfoResultType::kLong: {
312 long result = 0;
313 (void)curl_easy_getinfo(easy, descriptor.info, &result);
314 return;
315 }
316 case InfoResultType::kDouble: {
317 double result = 0;
318 (void)curl_easy_getinfo(easy, descriptor.info, &result);
319 return;
320 }
321 case InfoResultType::kOffset: {
322 curl_off_t result = 0;
323 (void)curl_easy_getinfo(easy, descriptor.info, &result);
324 return;
325 }
326 case InfoResultType::kSocket: {
327 curl_socket_t result = CURL_SOCKET_BAD;
328 (void)curl_easy_getinfo(easy, descriptor.info, &result);
329 return;
330 }
331 case InfoResultType::kCertificateInfo: {
332 struct curl_certinfo* result = nullptr;
333 (void)curl_easy_getinfo(easy, descriptor.info, &result);
334 return;
335 }
336 case InfoResultType::kTlsSessionInfo: {
337 struct curl_tlssessioninfo* result = nullptr;
338 (void)curl_easy_getinfo(easy, descriptor.info, &result);
339 return;
340 }
341 case InfoResultType::kOwnedSlist: {
342 struct curl_slist* result = nullptr;
343 (void)curl_easy_getinfo(easy, descriptor.info, &result);
344 curl_slist_free_all(result);
345 return;
346 }
347 case InfoResultType::kUnknown: {
348 void* result = nullptr;
349 (void)curl_easy_getinfo(easy, descriptor.info, &result);
350 return;
351 }
352 }
353}
354
358void ProbeKnownErrorStringsOnce() {
359 static const bool probed = [] {
360 for (int code = 0; code <= static_cast<int>(CURL_LAST); ++code) {
361 (void)curl_easy_strerror(static_cast<CURLcode>(code));
362 }
363 (void)curl_multi_strerror(CURLM_CALL_MULTI_PERFORM);
364 for (int code = 0; code <= static_cast<int>(CURLM_LAST); ++code) {
365 (void)curl_multi_strerror(static_cast<CURLMcode>(code));
366 }
367 for (int code = 0; code <= static_cast<int>(CURLSHE_LAST); ++code) {
368 (void)curl_share_strerror(static_cast<CURLSHcode>(code));
369 }
370 for (int code = 0; code <= static_cast<int>(CURLUE_LAST); ++code) {
371 (void)curl_url_strerror(static_cast<CURLUcode>(code));
372 }
373 return true;
374 }();
375 (void)probed;
376}
377
382void ProbeEasyOptionMetadataOnce() {
383 static const bool probed = [] {
384 const struct curl_easyoption* option = nullptr;
385 std::size_t count = 0;
386 while (count++ < 512 && (option = curl_easy_option_next(option)) != nullptr) {
387 (void)curl_easy_option_by_name(option->name);
388 (void)curl_easy_option_by_id(option->id);
389 }
390 (void)curl_easy_option_by_name("");
391 (void)curl_easy_option_by_name("NOT_A_CURL_OPTION");
392 (void)curl_easy_option_by_id(CURLOPT_LASTENTRY);
393 return true;
394 }();
395 (void)probed;
396}
397
398} // namespace
399
409ApiLifecycle::ApiLifecycle(CURL* easy, const curl::fuzzer::proto::ApiPlan& plan, std::string_view url)
410 : easy_(easy),
411 plan_(plan),
412 share_(nullptr),
413 active_multi_(nullptr),
414 guessed_scheme_url_(false),
415 version_info_probed_(false),
416 https_proxy_feature_present_(false) {
417 ProbeKnownErrorStringsOnce();
418 ProbeEasyOptionMetadataOnce();
419 if (plan_.probe_version_info()) {
420 const curl_version_info_data* info = curl_version_info(CURLVERSION_NOW);
421 version_info_probed_ = info != nullptr;
422 if (info != nullptr) {
423 https_proxy_feature_present_ = (info->features & CURL_VERSION_HTTPS_PROXY) != 0;
424 // Feature names are null-terminated public data. Keep even a malformed
425 // provider response from turning this observation into an unbounded walk.
426 if (info->feature_names != nullptr) {
427 for (std::size_t index = 0; index < 64 && info->feature_names[index] != nullptr; ++index) {
428 (void)std::string_view(info->feature_names[index]);
429 }
430 }
431 }
432 }
433 ProbeUrlAndEscaping(url);
434 response_callback_state_.owner = this;
435 const bool wants_response_callback = plan_.pause_response_once() || plan_.reentrant_probe_selectors_size() > 0;
436 if (wants_response_callback) {
437 response_callback_state_.pause_once = plan_.pause_response_once();
438 // Install userdata first: if the callback setopt were ever rejected, the
439 // baseline sink safely ignores the non-null pointer. The opposite order
440 // could dispatch this callback with libcurl's default FILE* userdata.
441 (void)curl_easy_setopt(easy_, CURLOPT_WRITEDATA, &response_callback_state_);
442 (void)curl_easy_setopt(easy_, CURLOPT_WRITEFUNCTION, &ApiLifecycle::ResponseWrite);
443 }
444 if (plan_.attach_share()) {
445 ConfigureShare();
446 }
447}
448
451ApiLifecycle::~ApiLifecycle() { CleanupShare(); }
452
457std::size_t ApiLifecycle::ResponseWrite(char* /*contents*/, std::size_t size, std::size_t nmemb, void* user_data) {
458 auto* state = static_cast<ResponseCallbackState*>(user_data);
459 if (state == nullptr || (size != 0 && nmemb > std::numeric_limits<std::size_t>::max() / size)) {
460 return 0;
461 }
462 const std::size_t bytes = size * nmemb;
463 if (bytes != 0 && state->pause_once && !state->pause_returned) {
464 state->pause_returned = true;
465 return CURL_WRITEFUNC_PAUSE;
466 }
467 if (bytes > std::numeric_limits<std::size_t>::max() - state->bytes_received) {
468 return 0;
469 }
470 state->bytes_received += bytes;
471 if (bytes != 0 && state->owner != nullptr && !state->probes_done) {
472 state->probes_done = true;
473 state->owner->RunReentrantProbes();
474 }
475 return bytes;
476}
477
479bool ApiLifecycle::response_pause_returned() const { return response_callback_state_.pause_returned; }
480
482std::size_t ApiLifecycle::response_bytes_received() const { return response_callback_state_.bytes_received; }
483
489void ApiLifecycle::SetActiveMulti(CURLM* multi) { active_multi_ = multi; }
490
492std::size_t ApiLifecycle::reentrant_probes_run() const { return response_callback_state_.probes_run; }
493
496 return response_callback_state_.recursive_rejections;
497}
498
503void ApiLifecycle::RunReentrantProbes() {
504 const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxApiReentrantSelectors,
505 static_cast<std::size_t>(plan_.reentrant_probe_selectors_size()));
506 for (std::size_t index = 0; index < count; ++index) {
507 const auto selector = static_cast<std::size_t>(plan_.reentrant_probe_selectors(static_cast<int>(index)));
508 const ReentrantProbe& probe = kReentrantProbes[selector % kReentrantProbeCount];
509 if (probe.needs_multi && active_multi_ == nullptr) {
510 continue;
511 }
512 ++response_callback_state_.probes_run;
513 if (ProbePublicApiOnce(easy_, active_multi_, probe.kind)) {
514 ++response_callback_state_.recursive_rejections;
515 }
516 }
517}
518
522void ApiLifecycle::ShareLock(CURL* /*easy*/, curl_lock_data /*data*/, curl_lock_access /*access*/, void* user_data) {
523 auto* state = static_cast<ShareCallbackState*>(user_data);
524 ++state->locks;
525}
526
529void ApiLifecycle::ShareUnlock(CURL* /*easy*/, curl_lock_data /*data*/, void* user_data) {
530 auto* state = static_cast<ShareCallbackState*>(user_data);
531 ++state->unlocks;
532}
533
538void ApiLifecycle::ConfigureShare() {
539 share_ = curl_share_init();
540 if (share_ == nullptr) {
541 return;
542 }
543
544 // Install userdata before exposing either callback. curl_share_setopt does
545 // not invoke them itself, but every later easy/share operation must observe
546 // a fully formed callback tuple if curl begins using the configured domains.
547 (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
548 (void)curl_share_setopt(share_, CURLSHOPT_LOCKFUNC, &ApiLifecycle::ShareLock);
549 (void)curl_share_setopt(share_, CURLSHOPT_UNLOCKFUNC, &ApiLifecycle::ShareUnlock);
550
551 const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiShareDataSelectors,
552 static_cast<std::size_t>(plan_.share_data_selectors_size()));
553 for (std::size_t index = 0; index < selector_count; ++index) {
554 const std::uint32_t selector = plan_.share_data_selectors(static_cast<int>(index));
555 const curl_lock_data data = kShareData[selector % kShareDataCount];
556 if (curl_share_setopt(share_, CURLSHOPT_SHARE, data) == CURLSHE_OK) {
557 // Exercise the reversible transition before any transfer can populate
558 // the selected cache, then leave the domain enabled. In particular,
559 // unsharing CONNECT after use makes current curl stop treating its
560 // connection pool as cleanup-owned, so doing teardown in that order
561 // would manufacture a deterministic leak in the harness.
562 (void)curl_share_setopt(share_, CURLSHOPT_UNSHARE, data);
563 (void)curl_share_setopt(share_, CURLSHOPT_SHARE, data);
564 }
565 }
566
567 if (curl_easy_setopt(easy_, CURLOPT_SHARE, share_) == CURLE_OK) {
568 // USERDATA remains mutable while attached; cleanup is the complementary
569 // ownership check and returns CURLSHE_IN_USE without destroying the share.
570 (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
571 (void)curl_share_cleanup(share_);
572 }
573}
574
580void ApiLifecycle::CleanupShare() {
581 if (share_ == nullptr) {
582 return;
583 }
584 if (curl_share_cleanup(share_) == CURLSHE_OK) {
585 share_ = nullptr;
586 }
587}
588
594void ApiLifecycle::ProbeUrlAndEscaping(std::string_view url) {
595 const std::size_t bounded_size = std::min(url.size(), scenario_limits::kMaxApiStringBytes);
596 const std::string input(url.substr(0, bounded_size));
597
598 CURLU* url_handle = curl_url();
599 if (url_handle != nullptr) {
600 if (plan_.guess_scheme_url()) {
601 constexpr char kGuessInput[] = "ftp.example.test/path";
602 if (curl_url_set(url_handle, CURLUPART_URL, kGuessInput, CURLU_GUESS_SCHEME) == CURLUE_OK) {
603 char* scheme = nullptr;
604 if (curl_url_get(url_handle, CURLUPART_SCHEME, &scheme, 0) == CURLUE_OK) {
605 guessed_scheme_url_ = scheme != nullptr && std::string_view(scheme) == "ftp";
606 curl_free(scheme);
607 }
608 }
609 curl_url_cleanup(url_handle);
610 url_handle = curl_url();
611 }
612 }
613 if (url_handle != nullptr) {
614 const unsigned int parse_flags = CURLU_ALLOW_SPACE | CURLU_NON_SUPPORT_SCHEME;
615 if (curl_url_set(url_handle, CURLUPART_URL, input.c_str(), parse_flags) != CURLUE_OK) {
616 (void)curl_url_set(url_handle, CURLUPART_SCHEME, "http", 0);
617 (void)curl_url_set(url_handle, CURLUPART_HOST, "api.test", 0);
618 (void)curl_url_set(url_handle, CURLUPART_PATH, input.c_str(), CURLU_URLENCODE);
619 }
620
621 // Zero and URLDECODE take distinct getter paths for most components;
622 // unsupported combinations are documented errors rather than unsafe raw
623 // varargs, so traversing the full typed part table is intentional.
624 for (const CURLUPart part : kUrlParts) {
625 ProbeUrlPart(url_handle, part, 0);
626 ProbeUrlPart(url_handle, part, CURLU_URLDECODE);
627 }
628 ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_DEFAULT_PORT);
629 ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_DEFAULT_PORT);
630 ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_URLENCODE);
631#if LIBCURL_VERSION_NUM >= 0x075800
632 ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNYCODE);
633#endif
634#if LIBCURL_VERSION_NUM >= 0x080300
635 ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNY2IDN);
636#endif
637#if LIBCURL_VERSION_NUM >= 0x080800
638 ProbeUrlPart(url_handle, CURLUPART_QUERY, CURLU_GET_EMPTY);
639 ProbeUrlPart(url_handle, CURLUPART_FRAGMENT, CURLU_GET_EMPTY);
640#endif
641#if LIBCURL_VERSION_NUM >= 0x080900
642 ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_GUESS_SCHEME);
643#endif
644
645 // Mutate only the duplicate so the original handle's getter state remains
646 // attributable to parsing the scenario URL rather than this lifecycle
647 // probe's own append operation.
648 CURLU* duplicate = curl_url_dup(url_handle);
649 if (duplicate != nullptr) {
650 (void)curl_url_set(duplicate, CURLUPART_QUERY, input.c_str(), CURLU_APPENDQUERY | CURLU_URLENCODE);
651 ProbeUrlPart(duplicate, CURLUPART_URL, 0);
652 curl_url_cleanup(duplicate);
653 }
654 curl_url_cleanup(url_handle);
655 }
656
657 // Exercise explicit binary lengths as well as the NUL-terminated API path.
658 // The API policy caps input far below INT_MAX, making the signed conversion
659 // and worst-case threefold escaping allocation deterministic.
660 const int input_length = static_cast<int>(input.size());
661 char* escaped = curl_easy_escape(easy_, input.data(), input_length);
662 if (escaped != nullptr) {
663 int decoded_length = 0;
664 char* decoded = curl_easy_unescape(easy_, escaped, 0, &decoded_length);
665 curl_free(decoded);
666 curl_free(escaped);
667 }
668 int decoded_length = 0;
669 char* decoded = curl_easy_unescape(easy_, input.data(), input_length, &decoded_length);
670 curl_free(decoded);
671}
672
674bool ApiLifecycle::guessed_scheme_url() const { return guessed_scheme_url_; }
675
677bool ApiLifecycle::version_info_probed() const { return version_info_probed_; }
678
680bool ApiLifecycle::https_proxy_feature_present() const { return https_proxy_feature_present_; }
681
690void ApiLifecycle::ProbeTransferResults(bool probe_upkeep) {
691 std::array<bool, kInfoDescriptorCount> seen{};
692 const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiInfoSelectors,
693 static_cast<std::size_t>(plan_.easy_info_selectors_size()));
694 for (std::size_t index = 0; index < selector_count; ++index) {
695 const std::uint32_t selector = plan_.easy_info_selectors(static_cast<int>(index));
696 const std::size_t descriptor_index = selector % kInfoDescriptorCount;
697 if (!seen[descriptor_index]) {
698 ProbeInfoDescriptor(easy_, kInfoDescriptors[descriptor_index]);
699 seen[descriptor_index] = true;
700 }
701 }
702
703 struct curl_header* header = nullptr;
704 (void)curl_easy_header(easy_, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
705 header = nullptr;
706 for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
707 header = curl_easy_nextheader(easy_, kAllHeaderOrigins, -1, header);
708 if (header == nullptr) {
709 break;
710 }
711 }
712
713 // curl_easy_perform retains an internal multi that upkeep expects. The
714 // external multi paths destroy theirs before result probing, and current
715 // debug builds deliberately reject upkeep on that detached handle state.
716 if (probe_upkeep) {
717 (void)curl_easy_upkeep(easy_);
718 }
719
720 // Post-transfer pause calls deliberately cover the public API's rejected
721 // inactive-handle path without changing the request that populated results.
722 (void)curl_easy_pause(easy_, CURLPAUSE_ALL);
723 (void)curl_easy_pause(easy_, CURLPAUSE_CONT);
724}
725
732 if (!plan_.duplicate_easy()) {
733 return;
734 }
735 CurlEasyPtr duplicate(curl_easy_duphandle(easy_));
736 if (duplicate != nullptr) {
737 curl_easy_reset(duplicate.get());
738 }
739}
740
741} // namespace proto_fuzzer
Safe ownership and typed probes for the dedicated public-API lane.
~ApiLifecycle()
RunScenario keeps this owner alive through easy cleanup, which releases even an incomplete connection...
std::size_t reentrant_recursive_rejections() const
void ProbeEasyDuplication()
Duplicate, reset, and destroy a scratch easy handle while every pointer- valued option copied from th...
ApiLifecycle(CURL *easy, const curl::fuzzer::proto::ApiPlan &plan, std::string_view url)
Configure the plan's share handle and cover public error-string tables.
std::size_t reentrant_probes_run() const
void SetActiveMulti(CURLM *multi)
Hand the driving multi handle to the reentrancy probes so the multi-entry probes can target the live ...
void ProbeTransferResults(bool probe_upkeep)
Run correctly typed CURLINFO and response-header probes selected by the plan.
bool https_proxy_feature_present() const
std::size_t response_bytes_received() const
Common ownership types for independently cleaned-up curl handles.
std::unique_ptr< CURL, CurlEasyDeleter > CurlEasyPtr
Unique ownership of a CURL easy handle.
Definition curl_raii.h:56
@ kString
string_value passed to a string or byte-data option.
constexpr std::size_t kMaxApiReentrantSelectors
Each reentrancy selector fires one public API call from inside a callback.
constexpr std::size_t kMaxApiInfoSelectors
One API iteration can cheaply inspect several result families, but an unbounded list would repeatedly...