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/api_lifecycle.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 Implementation of safe easy/share/query lifecycle probes.
9
10
#include "proto_fuzzer/api_lifecycle.h"
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
27
#include "proto_fuzzer/curl_raii.h"
28
29
namespace proto_fuzzer {
30
31
namespace {
32
33
constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO;
34
constexpr std::size_t kMaxResultHeaders = 16;
35
36
/// Every public CURLUPart value uses the same `char**` output contract, so it
37
/// is safe and cheap to traverse the complete table for each mutated URL.
38
constexpr 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
43
/// Retrieve one owned URL result and release it on the same path. Keeping the
44
/// ownership rule next to the call prevents later table expansion from
45
/// turning successful getters into one leak per fuzz iteration.
46
158k
void ProbeUrlPart(CURLU* url, CURLUPart part, unsigned int flags) {
47
158k
  char* result = nullptr;
48
158k
  if (curl_url_get(url, part, &result, flags) == CURLUE_OK) {
49
77.3k
    curl_free(result);
50
77.3k
  }
51
158k
}
52
53
/// Output storage family required by curl_easy_getinfo's varargs contract.
54
enum class InfoResultType {
55
  kString,
56
  kLong,
57
  kDouble,
58
  kOffset,
59
  kSocket,
60
  kCertificateInfo,
61
  kTlsSessionInfo,
62
  kOwnedSlist,
63
  kUnknown,
64
};
65
66
/// A CURLINFO value paired with the exact output type libcurl expects. Raw
67
/// protobuf numbers never become CURLINFO values because a mismatched varargs
68
/// pointer would be undefined behavior in the harness rather than fuzz input.
69
struct 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.
77
constexpr 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
164
constexpr std::size_t kInfoDescriptorCount = sizeof(kInfoDescriptors) / sizeof(kInfoDescriptors[0]);
165
static_assert(kInfoDescriptorCount <= 96, "the three API result seeds cover selector indexes 0 through 95");
166
167
/// Typed data domains accepted by CURLSHOPT_SHARE. The table intentionally
168
/// includes reserved/sentinel values: libcurl safely rejects them with
169
/// CURLSHE_BAD_OPTION, covering the public error path without fabricated
170
/// pointers or undefined varargs types.
171
constexpr 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
};
175
constexpr std::size_t kShareDataCount = sizeof(kShareData) / sizeof(kShareData[0]);
176
177
/// Call one CURLINFO descriptor with storage matching its encoded type.
178
2.16k
void ProbeInfoDescriptor(CURL* easy, const InfoDescriptor& descriptor) {
179
2.16k
  switch (descriptor.result_type) {
180
592
    case InfoResultType::kString: {
181
592
      char* result = nullptr;
182
592
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
183
592
      return;
184
0
    }
185
648
    case InfoResultType::kLong: {
186
648
      long result = 0;
187
648
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
188
648
      return;
189
0
    }
190
201
    case InfoResultType::kDouble: {
191
201
      double result = 0;
192
201
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
193
201
      return;
194
0
    }
195
436
    case InfoResultType::kOffset: {
196
436
      curl_off_t result = 0;
197
436
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
198
436
      return;
199
0
    }
200
69
    case InfoResultType::kSocket: {
201
69
      curl_socket_t result = CURL_SOCKET_BAD;
202
69
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
203
69
      return;
204
0
    }
205
49
    case InfoResultType::kCertificateInfo: {
206
49
      struct curl_certinfo* result = nullptr;
207
49
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
208
49
      return;
209
0
    }
210
38
    case InfoResultType::kTlsSessionInfo: {
211
38
      struct curl_tlssessioninfo* result = nullptr;
212
38
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
213
38
      return;
214
0
    }
215
78
    case InfoResultType::kOwnedSlist: {
216
78
      struct curl_slist* result = nullptr;
217
78
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
218
78
      curl_slist_free_all(result);
219
78
      return;
220
0
    }
221
57
    case InfoResultType::kUnknown: {
222
57
      void* result = nullptr;
223
57
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
224
57
      return;
225
0
    }
226
2.16k
  }
227
2.16k
}
228
229
/// Exercise every documented error-string table once per process. These APIs
230
/// are pure enum lookups; repeatedly asking LPM to rediscover all consecutive
231
/// values would consume corpus energy without adding state-dependent behavior.
232
5.12k
void ProbeKnownErrorStringsOnce() {
233
5.12k
  static const bool probed = [] {
234
104
    for (int code = 0; code <= static_cast<int>(CURL_LAST); ++code) {
235
103
      (void)curl_easy_strerror(static_cast<CURLcode>(code));
236
103
    }
237
1
    (void)curl_multi_strerror(CURLM_CALL_MULTI_PERFORM);
238
15
    for (int code = 0; code <= static_cast<int>(CURLM_LAST); ++code) {
239
14
      (void)curl_multi_strerror(static_cast<CURLMcode>(code));
240
14
    }
241
8
    for (int code = 0; code <= static_cast<int>(CURLSHE_LAST); ++code) {
242
7
      (void)curl_share_strerror(static_cast<CURLSHcode>(code));
243
7
    }
244
35
    for (int code = 0; code <= static_cast<int>(CURLUE_LAST); ++code) {
245
34
      (void)curl_url_strerror(static_cast<CURLUcode>(code));
246
34
    }
247
1
    return true;
248
1
  }();
249
5.12k
  (void)probed;
250
5.12k
}
251
252
/// Traverse the immutable public setopt metadata once per process. Iterating
253
/// every entry exercises option_next's table walk and gives both lookup APIs a
254
/// successful query for every supported type without charging every fuzz case
255
/// for the same version-dependent static data.
256
5.12k
void ProbeEasyOptionMetadataOnce() {
257
5.12k
  static const bool probed = [] {
258
1
    const struct curl_easyoption* option = nullptr;
259
1
    std::size_t count = 0;
260
328
    while (count++ < 512 && (option = curl_easy_option_next(option)) != nullptr) {
261
327
      (void)curl_easy_option_by_name(option->name);
262
327
      (void)curl_easy_option_by_id(option->id);
263
327
    }
264
1
    (void)curl_easy_option_by_name("");
265
1
    (void)curl_easy_option_by_name("NOT_A_CURL_OPTION");
266
1
    (void)curl_easy_option_by_id(CURLOPT_LASTENTRY);
267
1
    return true;
268
1
  }();
269
5.12k
  (void)probed;
270
5.12k
}
271
272
}  // namespace
273
274
/// Preserve the caller's plan by reference because RunScenario keeps the
275
/// source Scenario alive and unmodified for this object's complete lifetime.
276
ApiLifecycle::ApiLifecycle(CURL* easy, const curl::fuzzer::proto::ApiPlan& plan, std::string_view url)
277
5.12k
    : easy_(easy), plan_(plan), share_(nullptr) {
278
5.12k
  ProbeKnownErrorStringsOnce();
279
5.12k
  ProbeEasyOptionMetadataOnce();
280
5.12k
  ProbeUrlAndEscaping(url);
281
5.12k
  if (plan_.pause_response_once()) {
282
223
    response_callback_state_.pause_once = true;
283
    // Install userdata first: if the callback setopt were ever rejected, the
284
    // baseline sink safely ignores the non-null pointer. The opposite order
285
    // could dispatch this callback with libcurl's default FILE* userdata.
286
223
    (void)curl_easy_setopt(easy_, CURLOPT_WRITEDATA, &response_callback_state_);
287
223
    (void)curl_easy_setopt(easy_, CURLOPT_WRITEFUNCTION, &ApiLifecycle::ResponseWrite);
288
223
  }
289
5.12k
  if (plan_.attach_share()) {
290
363
    ConfigureShare();
291
363
  }
292
5.12k
}
293
294
/// RunScenario keeps this owner alive through easy cleanup, which releases
295
/// even an incomplete connection's share reference before CleanupShare runs.
296
5.12k
ApiLifecycle::~ApiLifecycle() { CleanupShare(); }
297
298
/// Pause only one non-empty delivery. curl replays the same bytes when the
299
/// event loop calls curl_easy_pause(CURLPAUSE_CONT), at which point accepting
300
/// the full count exercises its callback-output buffering path.
301
2.64k
std::size_t ApiLifecycle::ResponseWrite(char* /*contents*/, std::size_t size, std::size_t nmemb, void* user_data) {
302
2.64k
  auto* state = static_cast<ResponseCallbackState*>(user_data);
303
2.64k
  if (state == nullptr || (size != 0 && nmemb > std::numeric_limits<std::size_t>::max() / size)) {
304
0
    return 0;
305
0
  }
306
2.64k
  const std::size_t bytes = size * nmemb;
307
2.64k
  if (bytes != 0 && state->pause_once && !state->pause_returned) {
308
131
    state->pause_returned = true;
309
131
    return CURL_WRITEFUNC_PAUSE;
310
131
  }
311
2.51k
  if (bytes > std::numeric_limits<std::size_t>::max() - state->bytes_received) {
312
0
    return 0;
313
0
  }
314
2.51k
  state->bytes_received += bytes;
315
2.51k
  return bytes;
316
2.51k
}
317
318
0
bool ApiLifecycle::response_pause_returned() const { return response_callback_state_.pause_returned; }
319
320
0
std::size_t ApiLifecycle::response_bytes_received() const { return response_callback_state_.bytes_received; }
321
322
/// Count callback dispatch while leaving synchronization to applications that
323
/// actually use multiple threads. The state is owned by this lifecycle and
324
/// remains valid until after the final share cleanup callback.
325
2.90k
void ApiLifecycle::ShareLock(CURL* /*easy*/, curl_lock_data /*data*/, curl_lock_access /*access*/, void* user_data) {
326
2.90k
  auto* state = static_cast<ShareCallbackState*>(user_data);
327
2.90k
  ++state->locks;
328
2.90k
}
329
330
/// Match ShareLock without recursively entering any libcurl API.
331
2.90k
void ApiLifecycle::ShareUnlock(CURL* /*easy*/, curl_lock_data /*data*/, void* user_data) {
332
2.90k
  auto* state = static_cast<ShareCallbackState*>(user_data);
333
2.90k
  ++state->unlocks;
334
2.90k
}
335
336
/// Configure cache domains before attachment, when SHARE/UNSHARE transitions
337
/// are valid. Once attached, probe mutable userdata plus the cleanup API's
338
/// safe CURLSHE_IN_USE refusal without destroying the referenced handle.
339
363
void ApiLifecycle::ConfigureShare() {
340
363
  share_ = curl_share_init();
341
363
  if (share_ == nullptr) {
342
0
    return;
343
0
  }
344
345
  // Install userdata before exposing either callback. curl_share_setopt does
346
  // not invoke them itself, but every later easy/share operation must observe
347
  // a fully formed callback tuple if curl begins using the configured domains.
348
363
  (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
349
363
  (void)curl_share_setopt(share_, CURLSHOPT_LOCKFUNC, &ApiLifecycle::ShareLock);
350
363
  (void)curl_share_setopt(share_, CURLSHOPT_UNLOCKFUNC, &ApiLifecycle::ShareUnlock);
351
352
363
  const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiShareDataSelectors,
353
363
                                                           static_cast<std::size_t>(plan_.share_data_selectors_size()));
354
1.20k
  for (std::size_t index = 0; index < selector_count; ++index) {
355
845
    const std::uint32_t selector = plan_.share_data_selectors(static_cast<int>(index));
356
845
    const curl_lock_data data = kShareData[selector % kShareDataCount];
357
845
    if (curl_share_setopt(share_, CURLSHOPT_SHARE, data) == CURLSHE_OK) {
358
      // Exercise the reversible transition before any transfer can populate
359
      // the selected cache, then leave the domain enabled. In particular,
360
      // unsharing CONNECT after use makes current curl stop treating its
361
      // connection pool as cleanup-owned, so doing teardown in that order
362
      // would manufacture a deterministic leak in the harness.
363
665
      (void)curl_share_setopt(share_, CURLSHOPT_UNSHARE, data);
364
665
      (void)curl_share_setopt(share_, CURLSHOPT_SHARE, data);
365
665
    }
366
845
  }
367
368
363
  if (curl_easy_setopt(easy_, CURLOPT_SHARE, share_) == CURLE_OK) {
369
    // USERDATA remains mutable while attached; cleanup is the complementary
370
    // ownership check and returns CURLSHE_IN_USE without destroying the share.
371
363
    (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
372
363
    (void)curl_share_cleanup(share_);
373
363
  }
374
363
}
375
376
/// Destroy share state only after the owner has cleaned the easy. Explicitly
377
/// detaching first is not equivalent: curl rejects that setopt while an
378
/// incomplete transfer still has a connection, but easy cleanup always drops
379
/// the reference. Keep successful domains configured because share cleanup
380
/// uses those bits to identify caches populated during the transfer.
381
5.12k
void ApiLifecycle::CleanupShare() {
382
5.12k
  if (share_ == nullptr) {
383
4.75k
    return;
384
4.75k
  }
385
363
  if (curl_share_cleanup(share_) == CURLSHE_OK) {
386
363
    share_ = nullptr;
387
363
  }
388
363
}
389
390
/// Feed URL and percent-encoding APIs bytes from the same bounded scenario as
391
/// the transfer. A valid fallback URL keeps getter success paths reachable
392
/// even when a mutation makes the complete URL unparsable; the rejected parse
393
/// still executes first, so this does not hide malformed-input branches.
394
5.12k
void ApiLifecycle::ProbeUrlAndEscaping(std::string_view url) {
395
5.12k
  const std::size_t bounded_size = std::min(url.size(), scenario_limits::kMaxApiStringBytes);
396
5.12k
  const std::string input(url.substr(0, bounded_size));
397
398
5.12k
  CURLU* url_handle = curl_url();
399
5.12k
  if (url_handle != nullptr) {
400
5.12k
    const unsigned int parse_flags = CURLU_ALLOW_SPACE | CURLU_NON_SUPPORT_SCHEME;
401
5.12k
    if (curl_url_set(url_handle, CURLUPART_URL, input.c_str(), parse_flags) != CURLUE_OK) {
402
968
      (void)curl_url_set(url_handle, CURLUPART_SCHEME, "http", 0);
403
968
      (void)curl_url_set(url_handle, CURLUPART_HOST, "api.test", 0);
404
968
      (void)curl_url_set(url_handle, CURLUPART_PATH, input.c_str(), CURLU_URLENCODE);
405
968
    }
406
407
    // Zero and URLDECODE take distinct getter paths for most components;
408
    // unsupported combinations are documented errors rather than unsafe raw
409
    // varargs, so traversing the full typed part table is intentional.
410
56.3k
    for (const CURLUPart part : kUrlParts) {
411
56.3k
      ProbeUrlPart(url_handle, part, 0);
412
56.3k
      ProbeUrlPart(url_handle, part, CURLU_URLDECODE);
413
56.3k
    }
414
5.12k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_DEFAULT_PORT);
415
5.12k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_DEFAULT_PORT);
416
5.12k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_URLENCODE);
417
5.12k
#if LIBCURL_VERSION_NUM >= 0x075800
418
5.12k
    ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNYCODE);
419
5.12k
#endif
420
5.12k
#if LIBCURL_VERSION_NUM >= 0x080300
421
5.12k
    ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNY2IDN);
422
5.12k
#endif
423
5.12k
#if LIBCURL_VERSION_NUM >= 0x080800
424
5.12k
    ProbeUrlPart(url_handle, CURLUPART_QUERY, CURLU_GET_EMPTY);
425
5.12k
    ProbeUrlPart(url_handle, CURLUPART_FRAGMENT, CURLU_GET_EMPTY);
426
5.12k
#endif
427
5.12k
#if LIBCURL_VERSION_NUM >= 0x080900
428
5.12k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_GUESS_SCHEME);
429
5.12k
#endif
430
431
    // Mutate only the duplicate so the original handle's getter state remains
432
    // attributable to parsing the scenario URL rather than this lifecycle
433
    // probe's own append operation.
434
5.12k
    CURLU* duplicate = curl_url_dup(url_handle);
435
5.12k
    if (duplicate != nullptr) {
436
5.12k
      (void)curl_url_set(duplicate, CURLUPART_QUERY, input.c_str(), CURLU_APPENDQUERY | CURLU_URLENCODE);
437
5.12k
      ProbeUrlPart(duplicate, CURLUPART_URL, 0);
438
5.12k
      curl_url_cleanup(duplicate);
439
5.12k
    }
440
5.12k
    curl_url_cleanup(url_handle);
441
5.12k
  }
442
443
  // Exercise explicit binary lengths as well as the NUL-terminated API path.
444
  // The API policy caps input far below INT_MAX, making the signed conversion
445
  // and worst-case threefold escaping allocation deterministic.
446
5.12k
  const int input_length = static_cast<int>(input.size());
447
5.12k
  char* escaped = curl_easy_escape(easy_, input.data(), input_length);
448
5.12k
  if (escaped != nullptr) {
449
5.12k
    int decoded_length = 0;
450
5.12k
    char* decoded = curl_easy_unescape(easy_, escaped, 0, &decoded_length);
451
5.12k
    curl_free(decoded);
452
5.12k
    curl_free(escaped);
453
5.12k
  }
454
5.12k
  int decoded_length = 0;
455
5.12k
  char* decoded = curl_easy_unescape(easy_, input.data(), input_length, &decoded_length);
456
5.12k
  curl_free(decoded);
457
5.12k
}
458
459
/// Select through the typed descriptor table, suppressing duplicates whose
460
/// only effect would be charging an iteration for the same immutable result.
461
/// Header traversal remains unconditional in the API lane because it exposes
462
/// a separate public API and is independently capped.
463
5.12k
void ApiLifecycle::ProbeTransferResults(bool probe_upkeep) {
464
5.12k
  std::array<bool, kInfoDescriptorCount> seen{};
465
5.12k
  const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiInfoSelectors,
466
5.12k
                                                           static_cast<std::size_t>(plan_.easy_info_selectors_size()));
467
7.90k
  for (std::size_t index = 0; index < selector_count; ++index) {
468
2.78k
    const std::uint32_t selector = plan_.easy_info_selectors(static_cast<int>(index));
469
2.78k
    const std::size_t descriptor_index = selector % kInfoDescriptorCount;
470
2.78k
    if (!seen[descriptor_index]) {
471
2.16k
      ProbeInfoDescriptor(easy_, kInfoDescriptors[descriptor_index]);
472
2.16k
      seen[descriptor_index] = true;
473
2.16k
    }
474
2.78k
  }
475
476
5.12k
  struct curl_header* header = nullptr;
477
5.12k
  (void)curl_easy_header(easy_, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
478
5.12k
  header = nullptr;
479
7.81k
  for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
480
7.68k
    header = curl_easy_nextheader(easy_, kAllHeaderOrigins, -1, header);
481
7.68k
    if (header == nullptr) {
482
4.98k
      break;
483
4.98k
    }
484
7.68k
  }
485
486
  // curl_easy_perform retains an internal multi that upkeep expects. The
487
  // external multi paths destroy theirs before result probing, and current
488
  // debug builds deliberately reject upkeep on that detached handle state.
489
5.12k
  if (probe_upkeep) {
490
1.10k
    (void)curl_easy_upkeep(easy_);
491
1.10k
  }
492
493
  // Post-transfer pause calls deliberately cover the public API's rejected
494
  // inactive-handle path without changing the request that populated results.
495
5.12k
  (void)curl_easy_pause(easy_, CURLPAUSE_ALL);
496
5.12k
  (void)curl_easy_pause(easy_, CURLPAUSE_CONT);
497
5.12k
}
498
499
/// The duplicate inherits borrowed slists and callback userdata but not the
500
/// source share. Reset it immediately while those owners are still alive,
501
/// then cleanup; performing it would reuse mock/request cursors and test a
502
/// harness artifact instead of libcurl's duplication lifecycle.
503
5.12k
void ApiLifecycle::ProbeEasyDuplication() {
504
5.12k
  if (!plan_.duplicate_easy()) {
505
4.67k
    return;
506
4.67k
  }
507
443
  CurlEasyPtr duplicate(curl_easy_duphandle(easy_));
508
443
  if (duplicate != nullptr) {
509
443
    curl_easy_reset(duplicate.get());
510
443
  }
511
443
}
512
513
}  // namespace proto_fuzzer