Coverage Report

Created: 2026-09-04 07:25

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 <string>
24
#include <string_view>
25
26
#include "proto_fuzzer/curl_raii.h"
27
28
namespace proto_fuzzer {
29
30
namespace {
31
32
constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO;
33
constexpr std::size_t kMaxResultHeaders = 16;
34
35
/// Every public CURLUPart value uses the same `char**` output contract, so it
36
/// is safe and cheap to traverse the complete table for each mutated URL.
37
constexpr CURLUPart kUrlParts[] = {
38
    CURLUPART_URL,  CURLUPART_SCHEME, CURLUPART_USER,  CURLUPART_PASSWORD, CURLUPART_OPTIONS, CURLUPART_HOST,
39
    CURLUPART_PORT, CURLUPART_PATH,   CURLUPART_QUERY, CURLUPART_FRAGMENT, CURLUPART_ZONEID,
40
};
41
42
/// Retrieve one owned URL result and release it on the same path. Keeping the
43
/// ownership rule next to the call prevents later table expansion from
44
/// turning successful getters into one leak per fuzz iteration.
45
149k
void ProbeUrlPart(CURLU* url, CURLUPart part, unsigned int flags) {
46
149k
  char* result = nullptr;
47
149k
  if (curl_url_get(url, part, &result, flags) == CURLUE_OK) {
48
72.8k
    curl_free(result);
49
72.8k
  }
50
149k
}
51
52
/// Output storage family required by curl_easy_getinfo's varargs contract.
53
enum class InfoResultType {
54
  kString,
55
  kLong,
56
  kDouble,
57
  kOffset,
58
  kSocket,
59
  kCertificateInfo,
60
  kTlsSessionInfo,
61
  kOwnedSlist,
62
  kUnknown,
63
};
64
65
/// A CURLINFO value paired with the exact output type libcurl expects. Raw
66
/// protobuf numbers never become CURLINFO values because a mismatched varargs
67
/// pointer would be undefined behavior in the harness rather than fuzz input.
68
struct InfoDescriptor {
69
  CURLINFO info;
70
  InfoResultType result_type;
71
};
72
73
// Put one value from every dispatch family first so even a small corpus seed
74
// reaches all typed getinfo paths; the remaining entries broaden state and
75
// result-specific coverage as selectors mutate.
76
constexpr InfoDescriptor kInfoDescriptors[] = {
77
    {CURLINFO_EFFECTIVE_URL, InfoResultType::kString},
78
    {CURLINFO_RESPONSE_CODE, InfoResultType::kLong},
79
    {CURLINFO_TOTAL_TIME, InfoResultType::kDouble},
80
    {CURLINFO_SIZE_DOWNLOAD_T, InfoResultType::kOffset},
81
    {CURLINFO_ACTIVESOCKET, InfoResultType::kSocket},
82
    {CURLINFO_CERTINFO, InfoResultType::kCertificateInfo},
83
    {CURLINFO_TLS_SSL_PTR, InfoResultType::kTlsSessionInfo},
84
    {CURLINFO_SSL_ENGINES, InfoResultType::kOwnedSlist},
85
    {CURLINFO_NONE, InfoResultType::kUnknown},
86
    {CURLINFO_CONTENT_TYPE, InfoResultType::kString},
87
    {CURLINFO_PRIVATE, InfoResultType::kString},
88
    {CURLINFO_FTP_ENTRY_PATH, InfoResultType::kString},
89
    {CURLINFO_REDIRECT_URL, InfoResultType::kString},
90
    {CURLINFO_PRIMARY_IP, InfoResultType::kString},
91
    {CURLINFO_RTSP_SESSION_ID, InfoResultType::kString},
92
    {CURLINFO_LOCAL_IP, InfoResultType::kString},
93
    {CURLINFO_SCHEME, InfoResultType::kString},
94
    {CURLINFO_EFFECTIVE_METHOD, InfoResultType::kString},
95
    {CURLINFO_REFERER, InfoResultType::kString},
96
    {CURLINFO_CAINFO, InfoResultType::kString},
97
    {CURLINFO_CAPATH, InfoResultType::kString},
98
    {CURLINFO_HEADER_SIZE, InfoResultType::kLong},
99
    {CURLINFO_REQUEST_SIZE, InfoResultType::kLong},
100
    {CURLINFO_SSL_VERIFYRESULT, InfoResultType::kLong},
101
    {CURLINFO_FILETIME, InfoResultType::kLong},
102
    {CURLINFO_REDIRECT_COUNT, InfoResultType::kLong},
103
    {CURLINFO_HTTP_CONNECTCODE, InfoResultType::kLong},
104
    {CURLINFO_HTTPAUTH_AVAIL, InfoResultType::kLong},
105
    {CURLINFO_PROXYAUTH_AVAIL, InfoResultType::kLong},
106
    {CURLINFO_OS_ERRNO, InfoResultType::kLong},
107
    {CURLINFO_NUM_CONNECTS, InfoResultType::kLong},
108
    {CURLINFO_CONDITION_UNMET, InfoResultType::kLong},
109
    {CURLINFO_RTSP_CLIENT_CSEQ, InfoResultType::kLong},
110
    {CURLINFO_RTSP_SERVER_CSEQ, InfoResultType::kLong},
111
    {CURLINFO_RTSP_CSEQ_RECV, InfoResultType::kLong},
112
    {CURLINFO_PRIMARY_PORT, InfoResultType::kLong},
113
    {CURLINFO_LOCAL_PORT, InfoResultType::kLong},
114
    {CURLINFO_HTTP_VERSION, InfoResultType::kLong},
115
    {CURLINFO_PROXY_SSL_VERIFYRESULT, InfoResultType::kLong},
116
    {CURLINFO_PROXY_ERROR, InfoResultType::kLong},
117
    {CURLINFO_NAMELOOKUP_TIME, InfoResultType::kDouble},
118
    {CURLINFO_CONNECT_TIME, InfoResultType::kDouble},
119
    {CURLINFO_PRETRANSFER_TIME, InfoResultType::kDouble},
120
    {CURLINFO_STARTTRANSFER_TIME, InfoResultType::kDouble},
121
    {CURLINFO_REDIRECT_TIME, InfoResultType::kDouble},
122
    {CURLINFO_APPCONNECT_TIME, InfoResultType::kDouble},
123
    {CURLINFO_SIZE_UPLOAD_T, InfoResultType::kOffset},
124
    {CURLINFO_SPEED_DOWNLOAD_T, InfoResultType::kOffset},
125
    {CURLINFO_SPEED_UPLOAD_T, InfoResultType::kOffset},
126
    {CURLINFO_FILETIME_T, InfoResultType::kOffset},
127
    {CURLINFO_CONTENT_LENGTH_DOWNLOAD_T, InfoResultType::kOffset},
128
    {CURLINFO_CONTENT_LENGTH_UPLOAD_T, InfoResultType::kOffset},
129
    {CURLINFO_TOTAL_TIME_T, InfoResultType::kOffset},
130
    {CURLINFO_NAMELOOKUP_TIME_T, InfoResultType::kOffset},
131
    {CURLINFO_CONNECT_TIME_T, InfoResultType::kOffset},
132
    {CURLINFO_PRETRANSFER_TIME_T, InfoResultType::kOffset},
133
    {CURLINFO_STARTTRANSFER_TIME_T, InfoResultType::kOffset},
134
    {CURLINFO_REDIRECT_TIME_T, InfoResultType::kOffset},
135
    {CURLINFO_APPCONNECT_TIME_T, InfoResultType::kOffset},
136
    {CURLINFO_RETRY_AFTER, InfoResultType::kOffset},
137
#if LIBCURL_VERSION_NUM >= 0x080200
138
    {CURLINFO_XFER_ID, InfoResultType::kOffset},
139
    {CURLINFO_CONN_ID, InfoResultType::kOffset},
140
#endif
141
#if LIBCURL_VERSION_NUM >= 0x080600
142
    {CURLINFO_QUEUE_TIME_T, InfoResultType::kOffset},
143
#endif
144
#if LIBCURL_VERSION_NUM >= 0x080700
145
    {CURLINFO_USED_PROXY, InfoResultType::kLong},
146
#endif
147
#if LIBCURL_VERSION_NUM >= 0x080a00
148
    {CURLINFO_POSTTRANSFER_TIME_T, InfoResultType::kOffset},
149
#endif
150
#if LIBCURL_VERSION_NUM >= 0x080b00
151
    {CURLINFO_EARLYDATA_SENT_T, InfoResultType::kOffset},
152
#endif
153
#if LIBCURL_VERSION_NUM >= 0x080c00
154
    {CURLINFO_HTTPAUTH_USED, InfoResultType::kLong},
155
    {CURLINFO_PROXYAUTH_USED, InfoResultType::kLong},
156
#endif
157
#if LIBCURL_VERSION_NUM >= 0x081400
158
    {CURLINFO_SIZE_DELIVERED, InfoResultType::kOffset},
159
#endif
160
    {CURLINFO_COOKIELIST, InfoResultType::kOwnedSlist},
161
};
162
163
constexpr std::size_t kInfoDescriptorCount = sizeof(kInfoDescriptors) / sizeof(kInfoDescriptors[0]);
164
static_assert(kInfoDescriptorCount <= 96, "the three API result seeds cover selector indexes 0 through 95");
165
166
/// Typed data domains accepted by CURLSHOPT_SHARE. The table intentionally
167
/// includes reserved/sentinel values: libcurl safely rejects them with
168
/// CURLSHE_BAD_OPTION, covering the public error path without fabricated
169
/// pointers or undefined varargs types.
170
constexpr curl_lock_data kShareData[] = {
171
    CURL_LOCK_DATA_COOKIE, CURL_LOCK_DATA_DNS,  CURL_LOCK_DATA_SSL_SESSION, CURL_LOCK_DATA_CONNECT, CURL_LOCK_DATA_PSL,
172
    CURL_LOCK_DATA_HSTS,   CURL_LOCK_DATA_NONE, CURL_LOCK_DATA_SHARE,       CURL_LOCK_DATA_LAST,
173
};
174
constexpr std::size_t kShareDataCount = sizeof(kShareData) / sizeof(kShareData[0]);
175
176
/// Call one CURLINFO descriptor with storage matching its encoded type.
177
23.1k
void ProbeInfoDescriptor(CURL* easy, const InfoDescriptor& descriptor) {
178
23.1k
  switch (descriptor.result_type) {
179
6.19k
    case InfoResultType::kString: {
180
6.19k
      char* result = nullptr;
181
6.19k
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
182
6.19k
      return;
183
0
    }
184
7.13k
    case InfoResultType::kLong: {
185
7.13k
      long result = 0;
186
7.13k
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
187
7.13k
      return;
188
0
    }
189
2.29k
    case InfoResultType::kDouble: {
190
2.29k
      double result = 0;
191
2.29k
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
192
2.29k
      return;
193
0
    }
194
5.06k
    case InfoResultType::kOffset: {
195
5.06k
      curl_off_t result = 0;
196
5.06k
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
197
5.06k
      return;
198
0
    }
199
465
    case InfoResultType::kSocket: {
200
465
      curl_socket_t result = CURL_SOCKET_BAD;
201
465
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
202
465
      return;
203
0
    }
204
510
    case InfoResultType::kCertificateInfo: {
205
510
      struct curl_certinfo* result = nullptr;
206
510
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
207
510
      return;
208
0
    }
209
450
    case InfoResultType::kTlsSessionInfo: {
210
450
      struct curl_tlssessioninfo* result = nullptr;
211
450
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
212
450
      return;
213
0
    }
214
513
    case InfoResultType::kOwnedSlist: {
215
513
      struct curl_slist* result = nullptr;
216
513
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
217
513
      curl_slist_free_all(result);
218
513
      return;
219
0
    }
220
497
    case InfoResultType::kUnknown: {
221
497
      void* result = nullptr;
222
497
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
223
497
      return;
224
0
    }
225
23.1k
  }
226
23.1k
}
227
228
/// Exercise every documented error-string table once per process. These APIs
229
/// are pure enum lookups; repeatedly asking LPM to rediscover all consecutive
230
/// values would consume corpus energy without adding state-dependent behavior.
231
4.82k
void ProbeKnownErrorStringsOnce() {
232
4.82k
  static const bool probed = [] {
233
104
    for (int code = 0; code <= static_cast<int>(CURL_LAST); ++code) {
234
103
      (void)curl_easy_strerror(static_cast<CURLcode>(code));
235
103
    }
236
1
    (void)curl_multi_strerror(CURLM_CALL_MULTI_PERFORM);
237
15
    for (int code = 0; code <= static_cast<int>(CURLM_LAST); ++code) {
238
14
      (void)curl_multi_strerror(static_cast<CURLMcode>(code));
239
14
    }
240
8
    for (int code = 0; code <= static_cast<int>(CURLSHE_LAST); ++code) {
241
7
      (void)curl_share_strerror(static_cast<CURLSHcode>(code));
242
7
    }
243
35
    for (int code = 0; code <= static_cast<int>(CURLUE_LAST); ++code) {
244
34
      (void)curl_url_strerror(static_cast<CURLUcode>(code));
245
34
    }
246
1
    return true;
247
1
  }();
248
4.82k
  (void)probed;
249
4.82k
}
250
251
/// Traverse the immutable public setopt metadata once per process. Iterating
252
/// every entry exercises option_next's table walk and gives both lookup APIs a
253
/// successful query for every supported type without charging every fuzz case
254
/// for the same version-dependent static data.
255
4.82k
void ProbeEasyOptionMetadataOnce() {
256
4.82k
  static const bool probed = [] {
257
1
    const struct curl_easyoption* option = nullptr;
258
1
    std::size_t count = 0;
259
328
    while (count++ < 512 && (option = curl_easy_option_next(option)) != nullptr) {
260
327
      (void)curl_easy_option_by_name(option->name);
261
327
      (void)curl_easy_option_by_id(option->id);
262
327
    }
263
1
    (void)curl_easy_option_by_name("");
264
1
    (void)curl_easy_option_by_name("NOT_A_CURL_OPTION");
265
1
    (void)curl_easy_option_by_id(CURLOPT_LASTENTRY);
266
1
    return true;
267
1
  }();
268
4.82k
  (void)probed;
269
4.82k
}
270
271
}  // namespace
272
273
/// Preserve the caller's plan by reference because ScenarioRunner keeps the
274
/// source Scenario alive and unmodified for this object's complete lifetime.
275
ApiLifecycle::ApiLifecycle(CURL* easy, const curl::fuzzer::proto::ApiPlan& plan, std::string_view url)
276
4.82k
    : easy_(easy), plan_(plan), share_(nullptr) {
277
4.82k
  ProbeKnownErrorStringsOnce();
278
4.82k
  ProbeEasyOptionMetadataOnce();
279
4.82k
  ProbeUrlAndEscaping(url);
280
4.82k
  if (plan_.attach_share()) {
281
968
    ConfigureShare();
282
968
  }
283
4.82k
}
284
285
/// ScenarioRunner keeps this owner alive through easy cleanup, which releases
286
/// even an incomplete connection's share reference before CleanupShare runs.
287
4.82k
ApiLifecycle::~ApiLifecycle() { CleanupShare(); }
288
289
/// Count callback dispatch while leaving synchronization to applications that
290
/// actually use multiple threads. The state is owned by this lifecycle and
291
/// remains valid until after the final share cleanup callback.
292
7.62k
void ApiLifecycle::ShareLock(CURL* /*easy*/, curl_lock_data /*data*/, curl_lock_access /*access*/, void* user_data) {
293
7.62k
  auto* state = static_cast<ShareCallbackState*>(user_data);
294
7.62k
  ++state->locks;
295
7.62k
}
296
297
/// Match ShareLock without recursively entering any libcurl API.
298
7.62k
void ApiLifecycle::ShareUnlock(CURL* /*easy*/, curl_lock_data /*data*/, void* user_data) {
299
7.62k
  auto* state = static_cast<ShareCallbackState*>(user_data);
300
7.62k
  ++state->unlocks;
301
7.62k
}
302
303
/// Configure cache domains before attachment, when SHARE/UNSHARE transitions
304
/// are valid. Once attached, probe mutable userdata plus the cleanup API's
305
/// safe CURLSHE_IN_USE refusal without destroying the referenced handle.
306
968
void ApiLifecycle::ConfigureShare() {
307
968
  share_ = curl_share_init();
308
968
  if (share_ == nullptr) {
309
0
    return;
310
0
  }
311
312
  // Install userdata before exposing either callback. curl_share_setopt does
313
  // not invoke them itself, but every later easy/share operation must observe
314
  // a fully formed callback tuple if curl begins using the configured domains.
315
968
  (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
316
968
  (void)curl_share_setopt(share_, CURLSHOPT_LOCKFUNC, &ApiLifecycle::ShareLock);
317
968
  (void)curl_share_setopt(share_, CURLSHOPT_UNLOCKFUNC, &ApiLifecycle::ShareUnlock);
318
319
968
  const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiShareDataSelectors,
320
968
                                                           static_cast<std::size_t>(plan_.share_data_selectors_size()));
321
4.52k
  for (std::size_t index = 0; index < selector_count; ++index) {
322
3.55k
    const std::uint32_t selector = plan_.share_data_selectors(static_cast<int>(index));
323
3.55k
    const curl_lock_data data = kShareData[selector % kShareDataCount];
324
3.55k
    if (curl_share_setopt(share_, CURLSHOPT_SHARE, data) == CURLSHE_OK) {
325
      // Exercise the reversible transition before any transfer can populate
326
      // the selected cache, then leave the domain enabled. In particular,
327
      // unsharing CONNECT after use makes current curl stop treating its
328
      // connection pool as cleanup-owned, so doing teardown in that order
329
      // would manufacture a deterministic leak in the harness.
330
2.32k
      (void)curl_share_setopt(share_, CURLSHOPT_UNSHARE, data);
331
2.32k
      (void)curl_share_setopt(share_, CURLSHOPT_SHARE, data);
332
2.32k
    }
333
3.55k
  }
334
335
968
  if (curl_easy_setopt(easy_, CURLOPT_SHARE, share_) == CURLE_OK) {
336
    // USERDATA remains mutable while attached; cleanup is the complementary
337
    // ownership check and returns CURLSHE_IN_USE without destroying the share.
338
968
    (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
339
968
    (void)curl_share_cleanup(share_);
340
968
  }
341
968
}
342
343
/// Destroy share state only after the owner has cleaned the easy. Explicitly
344
/// detaching first is not equivalent: curl rejects that setopt while an
345
/// incomplete transfer still has a connection, but easy cleanup always drops
346
/// the reference. Keep successful domains configured because share cleanup
347
/// uses those bits to identify caches populated during the transfer.
348
4.82k
void ApiLifecycle::CleanupShare() {
349
4.82k
  if (share_ == nullptr) {
350
3.85k
    return;
351
3.85k
  }
352
968
  if (curl_share_cleanup(share_) == CURLSHE_OK) {
353
968
    share_ = nullptr;
354
968
  }
355
968
}
356
357
/// Feed URL and percent-encoding APIs bytes from the same bounded scenario as
358
/// the transfer. A valid fallback URL keeps getter success paths reachable
359
/// even when a mutation makes the complete URL unparsable; the rejected parse
360
/// still executes first, so this does not hide malformed-input branches.
361
4.82k
void ApiLifecycle::ProbeUrlAndEscaping(std::string_view url) {
362
4.82k
  const std::size_t bounded_size = std::min(url.size(), scenario_limits::kMaxApiStringBytes);
363
4.82k
  const std::string input(url.substr(0, bounded_size));
364
365
4.82k
  CURLU* url_handle = curl_url();
366
4.82k
  if (url_handle != nullptr) {
367
4.82k
    const unsigned int parse_flags = CURLU_ALLOW_SPACE | CURLU_NON_SUPPORT_SCHEME;
368
4.82k
    if (curl_url_set(url_handle, CURLUPART_URL, input.c_str(), parse_flags) != CURLUE_OK) {
369
956
      (void)curl_url_set(url_handle, CURLUPART_SCHEME, "http", 0);
370
956
      (void)curl_url_set(url_handle, CURLUPART_HOST, "api.test", 0);
371
956
      (void)curl_url_set(url_handle, CURLUPART_PATH, input.c_str(), CURLU_URLENCODE);
372
956
    }
373
374
    // Zero and URLDECODE take distinct getter paths for most components;
375
    // unsupported combinations are documented errors rather than unsafe raw
376
    // varargs, so traversing the full typed part table is intentional.
377
53.0k
    for (const CURLUPart part : kUrlParts) {
378
53.0k
      ProbeUrlPart(url_handle, part, 0);
379
53.0k
      ProbeUrlPart(url_handle, part, CURLU_URLDECODE);
380
53.0k
    }
381
4.82k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_DEFAULT_PORT);
382
4.82k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_DEFAULT_PORT);
383
4.82k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_URLENCODE);
384
4.82k
#if LIBCURL_VERSION_NUM >= 0x075800
385
4.82k
    ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNYCODE);
386
4.82k
#endif
387
4.82k
#if LIBCURL_VERSION_NUM >= 0x080300
388
4.82k
    ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNY2IDN);
389
4.82k
#endif
390
4.82k
#if LIBCURL_VERSION_NUM >= 0x080800
391
4.82k
    ProbeUrlPart(url_handle, CURLUPART_QUERY, CURLU_GET_EMPTY);
392
4.82k
    ProbeUrlPart(url_handle, CURLUPART_FRAGMENT, CURLU_GET_EMPTY);
393
4.82k
#endif
394
4.82k
#if LIBCURL_VERSION_NUM >= 0x080900
395
4.82k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_GUESS_SCHEME);
396
4.82k
#endif
397
398
    // Mutate only the duplicate so the original handle's getter state remains
399
    // attributable to parsing the scenario URL rather than this lifecycle
400
    // probe's own append operation.
401
4.82k
    CURLU* duplicate = curl_url_dup(url_handle);
402
4.82k
    if (duplicate != nullptr) {
403
4.82k
      (void)curl_url_set(duplicate, CURLUPART_QUERY, input.c_str(), CURLU_APPENDQUERY | CURLU_URLENCODE);
404
4.82k
      ProbeUrlPart(duplicate, CURLUPART_URL, 0);
405
4.82k
      curl_url_cleanup(duplicate);
406
4.82k
    }
407
4.82k
    curl_url_cleanup(url_handle);
408
4.82k
  }
409
410
  // Exercise explicit binary lengths as well as the NUL-terminated API path.
411
  // The API policy caps input far below INT_MAX, making the signed conversion
412
  // and worst-case threefold escaping allocation deterministic.
413
4.82k
  const int input_length = static_cast<int>(input.size());
414
4.82k
  char* escaped = curl_easy_escape(easy_, input.data(), input_length);
415
4.82k
  if (escaped != nullptr) {
416
4.82k
    int decoded_length = 0;
417
4.82k
    char* decoded = curl_easy_unescape(easy_, escaped, 0, &decoded_length);
418
4.82k
    curl_free(decoded);
419
4.82k
    curl_free(escaped);
420
4.82k
  }
421
4.82k
  int decoded_length = 0;
422
4.82k
  char* decoded = curl_easy_unescape(easy_, input.data(), input_length, &decoded_length);
423
4.82k
  curl_free(decoded);
424
4.82k
}
425
426
/// Select through the typed descriptor table, suppressing duplicates whose
427
/// only effect would be charging an iteration for the same immutable result.
428
/// Header traversal remains unconditional in the API lane because it exposes
429
/// a separate public API and is independently capped.
430
4.82k
void ApiLifecycle::ProbeTransferResults(bool probe_upkeep) {
431
4.82k
  std::array<bool, kInfoDescriptorCount> seen{};
432
4.82k
  const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiInfoSelectors,
433
4.82k
                                                           static_cast<std::size_t>(plan_.easy_info_selectors_size()));
434
30.6k
  for (std::size_t index = 0; index < selector_count; ++index) {
435
25.8k
    const std::uint32_t selector = plan_.easy_info_selectors(static_cast<int>(index));
436
25.8k
    const std::size_t descriptor_index = selector % kInfoDescriptorCount;
437
25.8k
    if (!seen[descriptor_index]) {
438
23.1k
      ProbeInfoDescriptor(easy_, kInfoDescriptors[descriptor_index]);
439
23.1k
      seen[descriptor_index] = true;
440
23.1k
    }
441
25.8k
  }
442
443
4.82k
  struct curl_header* header = nullptr;
444
4.82k
  (void)curl_easy_header(easy_, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
445
4.82k
  header = nullptr;
446
7.59k
  for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
447
7.50k
    header = curl_easy_nextheader(easy_, kAllHeaderOrigins, -1, header);
448
7.50k
    if (header == nullptr) {
449
4.72k
      break;
450
4.72k
    }
451
7.50k
  }
452
453
  // curl_easy_perform retains an internal multi that upkeep expects. The
454
  // external multi paths destroy theirs before result probing, and current
455
  // debug builds deliberately reject upkeep on that detached handle state.
456
4.82k
  if (probe_upkeep) {
457
945
    (void)curl_easy_upkeep(easy_);
458
945
  }
459
460
  // Post-transfer pause calls deliberately cover the public API's rejected
461
  // inactive-handle path without changing the request that populated results.
462
4.82k
  (void)curl_easy_pause(easy_, CURLPAUSE_ALL);
463
4.82k
  (void)curl_easy_pause(easy_, CURLPAUSE_CONT);
464
4.82k
}
465
466
/// The duplicate inherits borrowed slists and callback userdata but not the
467
/// source share. Reset it immediately while those owners are still alive,
468
/// then cleanup; performing it would reuse mock/request cursors and test a
469
/// harness artifact instead of libcurl's duplication lifecycle.
470
4.82k
void ApiLifecycle::ProbeEasyDuplication() {
471
4.82k
  if (!plan_.duplicate_easy()) {
472
3.77k
    return;
473
3.77k
  }
474
1.05k
  CurlEasyPtr duplicate(curl_easy_duphandle(easy_));
475
1.05k
  if (duplicate != nullptr) {
476
1.05k
    curl_easy_reset(duplicate.get());
477
1.05k
  }
478
1.05k
}
479
480
}  // namespace proto_fuzzer