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