Coverage Report

Created: 2026-10-15 06: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 <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
179k
void ProbeUrlPart(CURLU* url, CURLUPart part, unsigned int flags) {
47
179k
  char* result = nullptr;
48
179k
  if (curl_url_get(url, part, &result, flags) == CURLUE_OK) {
49
87.0k
    curl_free(result);
50
87.0k
  }
51
179k
}
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
/// One public API probe that runs from inside the response callback. The kinds
178
/// are limited to calls that are safe to attempt while a callback owns the
179
/// thread: libcurl's eapi/mapi guards reject most non-reentrant entries before
180
/// they can change transfer state, and the reentrant ones have defined
181
/// in-callback behaviour. The write callback itself runs under
182
/// CURL_CBAPI_START, which puts it on multi->callstack, so non-reentrant multi
183
/// entrypoints are refused even on the replay that curl_easy_pause() triggers.
184
/// The table below still holds only calls that stay safe if the guard admits
185
/// them.
186
enum 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
203
/// A probe plus whether it needs the live multi handle. Multi probes are
204
/// skipped in easy-only drives, where no multi object is exposed.
205
struct ReentrantProbe {
206
  ReentrantProbeKind kind;
207
  bool needs_multi;
208
};
209
210
/// curl_multi_cleanup and curl_multi_remove_handle are deliberately absent.
211
/// The write callback runs under CURL_CBAPI_START, so multi->callstack.count is
212
/// already nonzero while the probes fire and Curl_mapi_enter refuses these
213
/// non-reentrant multi entries. They stay out regardless, so the harness never
214
/// relies on that refusal: if the guard ever admitted one, cleanup would free
215
/// the multi that DriveScenario keeps driving (use-after-free plus a second
216
/// cleanup in multi.reset()), and remove_handle would run multi_done() and
217
/// detach the still-driven easy, leaving the drive loop on a detached transfer
218
/// so it never reports CURLE_OK. The probes below neither release the multi nor
219
/// detach the driven easy.
220
constexpr 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
};
229
constexpr std::size_t kReentrantProbeCount = sizeof(kReentrantProbes) / sizeof(kReentrantProbes[0]);
230
231
/// @return true when libcurl refused an easy entrypoint because a callback was
232
///         already on the handle's call stack.
233
349
bool RecursiveRejected(CURLcode code) { return code == CURLE_RECURSIVE_API_CALL; }
234
235
/// @return true when libcurl refused a multi entrypoint for the same reason.
236
155
bool RecursiveRejected(CURLMcode code) { return code == CURLM_RECURSIVE_API_CALL; }
237
238
/// Fire one probe and report whether the guard rejected it as recursive. The
239
/// reentrant calls run for real, so their return values are intentionally not
240
/// interpreted beyond that check.
241
3.41k
bool ProbePublicApiOnce(CURL* easy, CURLM* multi, ReentrantProbeKind kind) {
242
3.41k
  switch (kind) {
243
127
    case ReentrantProbeKind::kEasySetopt:
244
127
      return RecursiveRejected(curl_easy_setopt(easy, CURLOPT_REFERER, "reentrant.test"));
245
32
    case ReentrantProbeKind::kEasyGetinfo: {
246
32
      long response_code = 0;
247
32
      return RecursiveRejected(curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code));
248
0
    }
249
172
    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
172
      struct curl_header* header = nullptr;
253
172
      (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
254
172
      return false;
255
0
    }
256
2.69k
    case ReentrantProbeKind::kEasyDuphandle: {
257
      // duphandle is reentrant; duplicate, reset, and release immediately so
258
      // the copied option pointers never outlive this callback.
259
2.69k
      CURL* duplicate = curl_easy_duphandle(easy);
260
2.69k
      if (duplicate != nullptr) {
261
2.61k
        curl_easy_reset(duplicate);
262
2.61k
        curl_easy_cleanup(duplicate);
263
2.61k
      }
264
2.69k
      return false;
265
0
    }
266
42
    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
42
      curl_easy_reset(easy);
270
42
      return false;
271
62
    case ReentrantProbeKind::kEasyPerform:
272
62
      return RecursiveRejected(curl_easy_perform(easy));
273
33
    case ReentrantProbeKind::kEasyUpkeep:
274
33
      return RecursiveRejected(curl_easy_upkeep(easy));
275
33
    case ReentrantProbeKind::kEasySend: {
276
33
      char byte = 'x';
277
33
      std::size_t sent = 0;
278
33
      return RecursiveRejected(curl_easy_send(easy, &byte, 1, &sent));
279
0
    }
280
34
    case ReentrantProbeKind::kEasyRecv: {
281
34
      char byte = 0;
282
34
      std::size_t received = 0;
283
34
      return RecursiveRejected(curl_easy_recv(easy, &byte, 1, &received));
284
0
    }
285
28
    case ReentrantProbeKind::kEasyPause:
286
28
      return RecursiveRejected(curl_easy_pause(easy, CURLPAUSE_CONT));
287
25
    case ReentrantProbeKind::kMultiPerform: {
288
25
      int running = 0;
289
25
      return RecursiveRejected(curl_multi_perform(multi, &running));
290
0
    }
291
55
    case ReentrantProbeKind::kMultiSetopt:
292
55
      return RecursiveRejected(curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, 0L));
293
40
    case ReentrantProbeKind::kMultiAddHandle:
294
40
      return RecursiveRejected(curl_multi_add_handle(multi, easy));
295
35
    case ReentrantProbeKind::kMultiWait: {
296
35
      int numfds = 0;
297
35
      return RecursiveRejected(curl_multi_wait(multi, nullptr, 0, 0, &numfds));
298
0
    }
299
3.41k
  }
300
0
  return false;
301
3.41k
}
302
303
/// Call one CURLINFO descriptor with storage matching its encoded type.
304
1.49k
void ProbeInfoDescriptor(CURL* easy, const InfoDescriptor& descriptor) {
305
1.49k
  switch (descriptor.result_type) {
306
496
    case InfoResultType::kString: {
307
496
      char* result = nullptr;
308
496
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
309
496
      return;
310
0
    }
311
398
    case InfoResultType::kLong: {
312
398
      long result = 0;
313
398
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
314
398
      return;
315
0
    }
316
127
    case InfoResultType::kDouble: {
317
127
      double result = 0;
318
127
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
319
127
      return;
320
0
    }
321
293
    case InfoResultType::kOffset: {
322
293
      curl_off_t result = 0;
323
293
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
324
293
      return;
325
0
    }
326
60
    case InfoResultType::kSocket: {
327
60
      curl_socket_t result = CURL_SOCKET_BAD;
328
60
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
329
60
      return;
330
0
    }
331
23
    case InfoResultType::kCertificateInfo: {
332
23
      struct curl_certinfo* result = nullptr;
333
23
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
334
23
      return;
335
0
    }
336
23
    case InfoResultType::kTlsSessionInfo: {
337
23
      struct curl_tlssessioninfo* result = nullptr;
338
23
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
339
23
      return;
340
0
    }
341
54
    case InfoResultType::kOwnedSlist: {
342
54
      struct curl_slist* result = nullptr;
343
54
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
344
54
      curl_slist_free_all(result);
345
54
      return;
346
0
    }
347
22
    case InfoResultType::kUnknown: {
348
22
      void* result = nullptr;
349
22
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
350
22
      return;
351
0
    }
352
1.49k
  }
353
1.49k
}
354
355
/// Exercise every documented error-string table once per process. These APIs
356
/// are pure enum lookups; repeatedly asking LPM to rediscover all consecutive
357
/// values would consume corpus energy without adding state-dependent behavior.
358
5.79k
void ProbeKnownErrorStringsOnce() {
359
5.79k
  static const bool probed = [] {
360
104
    for (int code = 0; code <= static_cast<int>(CURL_LAST); ++code) {
361
103
      (void)curl_easy_strerror(static_cast<CURLcode>(code));
362
103
    }
363
1
    (void)curl_multi_strerror(CURLM_CALL_MULTI_PERFORM);
364
15
    for (int code = 0; code <= static_cast<int>(CURLM_LAST); ++code) {
365
14
      (void)curl_multi_strerror(static_cast<CURLMcode>(code));
366
14
    }
367
8
    for (int code = 0; code <= static_cast<int>(CURLSHE_LAST); ++code) {
368
7
      (void)curl_share_strerror(static_cast<CURLSHcode>(code));
369
7
    }
370
35
    for (int code = 0; code <= static_cast<int>(CURLUE_LAST); ++code) {
371
34
      (void)curl_url_strerror(static_cast<CURLUcode>(code));
372
34
    }
373
1
    return true;
374
1
  }();
375
5.79k
  (void)probed;
376
5.79k
}
377
378
/// Traverse the immutable public setopt metadata once per process. Iterating
379
/// every entry exercises option_next's table walk and gives both lookup APIs a
380
/// successful query for every supported type without charging every fuzz case
381
/// for the same version-dependent static data.
382
5.79k
void ProbeEasyOptionMetadataOnce() {
383
5.79k
  static const bool probed = [] {
384
1
    const struct curl_easyoption* option = nullptr;
385
1
    std::size_t count = 0;
386
328
    while (count++ < 512 && (option = curl_easy_option_next(option)) != nullptr) {
387
327
      (void)curl_easy_option_by_name(option->name);
388
327
      (void)curl_easy_option_by_id(option->id);
389
327
    }
390
1
    (void)curl_easy_option_by_name("");
391
1
    (void)curl_easy_option_by_name("NOT_A_CURL_OPTION");
392
1
    (void)curl_easy_option_by_id(CURLOPT_LASTENTRY);
393
1
    return true;
394
1
  }();
395
5.79k
  (void)probed;
396
5.79k
}
397
398
}  // namespace
399
400
/// Configure the plan's share handle and cover public error-string tables.
401
/// The plan is borrowed because RunScenario keeps its source Scenario alive and
402
/// unmodified for this object's complete lifetime.
403
/// @param easy Live easy handle used by explicit probes; clean it before
404
///        destroying this lifecycle so its share reference is gone first.
405
/// @param plan Bounded API plan retained by the API target policy; it must
406
///        remain alive and unmodified for this lifecycle's complete lifetime.
407
/// @param url Bounded scenario URL used by URL and escaping probes during
408
///        construction; it is not retained.
409
ApiLifecycle::ApiLifecycle(CURL* easy, const curl::fuzzer::proto::ApiPlan& plan, std::string_view url)
410
5.79k
    : easy_(easy), plan_(plan), share_(nullptr), active_multi_(nullptr) {
411
5.79k
  ProbeKnownErrorStringsOnce();
412
5.79k
  ProbeEasyOptionMetadataOnce();
413
5.79k
  ProbeUrlAndEscaping(url);
414
5.79k
  response_callback_state_.owner = this;
415
5.79k
  const bool wants_response_callback = plan_.pause_response_once() || plan_.reentrant_probe_selectors_size() > 0;
416
5.79k
  if (wants_response_callback) {
417
837
    response_callback_state_.pause_once = plan_.pause_response_once();
418
    // Install userdata first: if the callback setopt were ever rejected, the
419
    // baseline sink safely ignores the non-null pointer. The opposite order
420
    // could dispatch this callback with libcurl's default FILE* userdata.
421
837
    (void)curl_easy_setopt(easy_, CURLOPT_WRITEDATA, &response_callback_state_);
422
837
    (void)curl_easy_setopt(easy_, CURLOPT_WRITEFUNCTION, &ApiLifecycle::ResponseWrite);
423
837
  }
424
5.79k
  if (plan_.attach_share()) {
425
451
    ConfigureShare();
426
451
  }
427
5.79k
}
428
429
/// RunScenario keeps this owner alive through easy cleanup, which releases
430
/// even an incomplete connection's share reference before CleanupShare runs.
431
5.79k
ApiLifecycle::~ApiLifecycle() { CleanupShare(); }
432
433
/// Pause the first non-empty body delivery, then accept its replay. Curl replays
434
/// the same bytes when the event loop calls curl_easy_pause(CURLPAUSE_CONT), at
435
/// which point accepting the full count exercises its callback-output buffering
436
/// path.
437
18.4k
std::size_t ApiLifecycle::ResponseWrite(char* /*contents*/, std::size_t size, std::size_t nmemb, void* user_data) {
438
18.4k
  auto* state = static_cast<ResponseCallbackState*>(user_data);
439
18.4k
  if (state == nullptr || (size != 0 && nmemb > std::numeric_limits<std::size_t>::max() / size)) {
440
0
    return 0;
441
0
  }
442
18.4k
  const std::size_t bytes = size * nmemb;
443
18.4k
  if (bytes != 0 && state->pause_once && !state->pause_returned) {
444
235
    state->pause_returned = true;
445
235
    return CURL_WRITEFUNC_PAUSE;
446
235
  }
447
18.2k
  if (bytes > std::numeric_limits<std::size_t>::max() - state->bytes_received) {
448
0
    return 0;
449
0
  }
450
18.2k
  state->bytes_received += bytes;
451
18.2k
  if (bytes != 0 && state->owner != nullptr && !state->probes_done) {
452
581
    state->probes_done = true;
453
581
    state->owner->RunReentrantProbes();
454
581
  }
455
18.2k
  return bytes;
456
18.2k
}
457
458
/// @return whether the API-plan response callback returned PAUSE once.
459
5.79k
bool ApiLifecycle::response_pause_returned() const { return response_callback_state_.pause_returned; }
460
461
/// @return response bytes accepted after the paused chunk was replayed.
462
5.79k
std::size_t ApiLifecycle::response_bytes_received() const { return response_callback_state_.bytes_received; }
463
464
/// Hand the driving multi handle to the reentrancy probes so the multi-entry
465
/// probes can target the live object. Called before the drive loop starts;
466
/// null in easy-only drives, where multi probes are skipped.
467
/// @param multi Live multi handle driving the transfer, or null in easy-only
468
///        drives where multi probes are skipped.
469
4.45k
void ApiLifecycle::SetActiveMulti(CURLM* multi) { active_multi_ = multi; }
470
471
/// @return number of public API probes fired from the response callback.
472
5.79k
std::size_t ApiLifecycle::reentrant_probes_run() const { return response_callback_state_.probes_run; }
473
474
/// @return number of probes libcurl rejected with a recursive-API-call code.
475
5.79k
std::size_t ApiLifecycle::reentrant_recursive_rejections() const {
476
5.79k
  return response_callback_state_.recursive_rejections;
477
5.79k
}
478
479
/// Fire each selected public API call once, in scenario order, from inside the
480
/// first non-empty response callback. Multi probes are skipped when the drive
481
/// exposes no multi handle. Record how many probes ran and how many libcurl
482
/// rejected as recursive so tests can prove the guard branch was actually taken.
483
581
void ApiLifecycle::RunReentrantProbes() {
484
581
  const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxApiReentrantSelectors,
485
581
                                                  static_cast<std::size_t>(plan_.reentrant_probe_selectors_size()));
486
4.03k
  for (std::size_t index = 0; index < count; ++index) {
487
3.45k
    const auto selector = static_cast<std::size_t>(plan_.reentrant_probe_selectors(static_cast<int>(index)));
488
3.45k
    const ReentrantProbe& probe = kReentrantProbes[selector % kReentrantProbeCount];
489
3.45k
    if (probe.needs_multi && active_multi_ == nullptr) {
490
38
      continue;
491
38
    }
492
3.41k
    ++response_callback_state_.probes_run;
493
3.41k
    if (ProbePublicApiOnce(easy_, active_multi_, probe.kind)) {
494
317
      ++response_callback_state_.recursive_rejections;
495
317
    }
496
3.41k
  }
497
581
}
498
499
/// Count one share lock callback without introducing synchronization into the
500
/// fuzzer's single-threaded lifecycle. The state is owned by this lifecycle and
501
/// remains valid until after the final share cleanup callback.
502
3.27k
void ApiLifecycle::ShareLock(CURL* /*easy*/, curl_lock_data /*data*/, curl_lock_access /*access*/, void* user_data) {
503
3.27k
  auto* state = static_cast<ShareCallbackState*>(user_data);
504
3.27k
  ++state->locks;
505
3.27k
}
506
507
/// Count the matching unlock callback using the same live userdata, without
508
/// recursively entering any libcurl API.
509
3.27k
void ApiLifecycle::ShareUnlock(CURL* /*easy*/, curl_lock_data /*data*/, void* user_data) {
510
3.27k
  auto* state = static_cast<ShareCallbackState*>(user_data);
511
3.27k
  ++state->unlocks;
512
3.27k
}
513
514
/// Install and attach a share according to bounded typed selectors. Configure
515
/// cache domains before attachment, when SHARE/UNSHARE transitions are valid.
516
/// Once attached, probe mutable userdata plus the cleanup API's safe
517
/// CURLSHE_IN_USE refusal without destroying the referenced handle.
518
451
void ApiLifecycle::ConfigureShare() {
519
451
  share_ = curl_share_init();
520
451
  if (share_ == nullptr) {
521
2
    return;
522
2
  }
523
524
  // Install userdata before exposing either callback. curl_share_setopt does
525
  // not invoke them itself, but every later easy/share operation must observe
526
  // a fully formed callback tuple if curl begins using the configured domains.
527
449
  (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
528
449
  (void)curl_share_setopt(share_, CURLSHOPT_LOCKFUNC, &ApiLifecycle::ShareLock);
529
449
  (void)curl_share_setopt(share_, CURLSHOPT_UNLOCKFUNC, &ApiLifecycle::ShareUnlock);
530
531
449
  const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiShareDataSelectors,
532
449
                                                           static_cast<std::size_t>(plan_.share_data_selectors_size()));
533
1.48k
  for (std::size_t index = 0; index < selector_count; ++index) {
534
1.03k
    const std::uint32_t selector = plan_.share_data_selectors(static_cast<int>(index));
535
1.03k
    const curl_lock_data data = kShareData[selector % kShareDataCount];
536
1.03k
    if (curl_share_setopt(share_, CURLSHOPT_SHARE, data) == CURLSHE_OK) {
537
      // Exercise the reversible transition before any transfer can populate
538
      // the selected cache, then leave the domain enabled. In particular,
539
      // unsharing CONNECT after use makes current curl stop treating its
540
      // connection pool as cleanup-owned, so doing teardown in that order
541
      // would manufacture a deterministic leak in the harness.
542
849
      (void)curl_share_setopt(share_, CURLSHOPT_UNSHARE, data);
543
849
      (void)curl_share_setopt(share_, CURLSHOPT_SHARE, data);
544
849
    }
545
1.03k
  }
546
547
449
  if (curl_easy_setopt(easy_, CURLOPT_SHARE, share_) == CURLE_OK) {
548
    // USERDATA remains mutable while attached; cleanup is the complementary
549
    // ownership check and returns CURLSHE_IN_USE without destroying the share.
550
449
    (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
551
449
    (void)curl_share_cleanup(share_);
552
449
  }
553
449
}
554
555
/// Destroy the share after easy cleanup has released its final reference.
556
/// Explicitly detaching first is not equivalent: curl rejects that setopt while
557
/// an incomplete transfer still has a connection, but easy cleanup always drops
558
/// the reference. Keep successful shared domains configured so curl can identify
559
/// and release caches populated during the transfer.
560
5.79k
void ApiLifecycle::CleanupShare() {
561
5.79k
  if (share_ == nullptr) {
562
5.34k
    return;
563
5.34k
  }
564
449
  if (curl_share_cleanup(share_) == CURLSHE_OK) {
565
449
    share_ = nullptr;
566
449
  }
567
449
}
568
569
/// Exercise URL parsing, typed part retrieval, duplication, and escaping
570
/// against bytes already selected by the bounded scenario. A valid fallback URL
571
/// keeps getter success paths reachable even when a mutation makes the complete
572
/// URL unparsable; the rejected parse still executes first, so this does not
573
/// hide malformed-input branches.
574
5.79k
void ApiLifecycle::ProbeUrlAndEscaping(std::string_view url) {
575
5.79k
  const std::size_t bounded_size = std::min(url.size(), scenario_limits::kMaxApiStringBytes);
576
5.79k
  const std::string input(url.substr(0, bounded_size));
577
578
5.79k
  CURLU* url_handle = curl_url();
579
5.79k
  if (url_handle != nullptr) {
580
5.79k
    const unsigned int parse_flags = CURLU_ALLOW_SPACE | CURLU_NON_SUPPORT_SCHEME;
581
5.79k
    if (curl_url_set(url_handle, CURLUPART_URL, input.c_str(), parse_flags) != CURLUE_OK) {
582
993
      (void)curl_url_set(url_handle, CURLUPART_SCHEME, "http", 0);
583
993
      (void)curl_url_set(url_handle, CURLUPART_HOST, "api.test", 0);
584
993
      (void)curl_url_set(url_handle, CURLUPART_PATH, input.c_str(), CURLU_URLENCODE);
585
993
    }
586
587
    // Zero and URLDECODE take distinct getter paths for most components;
588
    // unsupported combinations are documented errors rather than unsafe raw
589
    // varargs, so traversing the full typed part table is intentional.
590
63.7k
    for (const CURLUPart part : kUrlParts) {
591
63.7k
      ProbeUrlPart(url_handle, part, 0);
592
63.7k
      ProbeUrlPart(url_handle, part, CURLU_URLDECODE);
593
63.7k
    }
594
5.79k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_DEFAULT_PORT);
595
5.79k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_DEFAULT_PORT);
596
5.79k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_URLENCODE);
597
5.79k
#if LIBCURL_VERSION_NUM >= 0x075800
598
5.79k
    ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNYCODE);
599
5.79k
#endif
600
5.79k
#if LIBCURL_VERSION_NUM >= 0x080300
601
5.79k
    ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNY2IDN);
602
5.79k
#endif
603
5.79k
#if LIBCURL_VERSION_NUM >= 0x080800
604
5.79k
    ProbeUrlPart(url_handle, CURLUPART_QUERY, CURLU_GET_EMPTY);
605
5.79k
    ProbeUrlPart(url_handle, CURLUPART_FRAGMENT, CURLU_GET_EMPTY);
606
5.79k
#endif
607
5.79k
#if LIBCURL_VERSION_NUM >= 0x080900
608
5.79k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_GUESS_SCHEME);
609
5.79k
#endif
610
611
    // Mutate only the duplicate so the original handle's getter state remains
612
    // attributable to parsing the scenario URL rather than this lifecycle
613
    // probe's own append operation.
614
5.79k
    CURLU* duplicate = curl_url_dup(url_handle);
615
5.79k
    if (duplicate != nullptr) {
616
5.77k
      (void)curl_url_set(duplicate, CURLUPART_QUERY, input.c_str(), CURLU_APPENDQUERY | CURLU_URLENCODE);
617
5.77k
      ProbeUrlPart(duplicate, CURLUPART_URL, 0);
618
5.77k
      curl_url_cleanup(duplicate);
619
5.77k
    }
620
5.79k
    curl_url_cleanup(url_handle);
621
5.79k
  }
622
623
  // Exercise explicit binary lengths as well as the NUL-terminated API path.
624
  // The API policy caps input far below INT_MAX, making the signed conversion
625
  // and worst-case threefold escaping allocation deterministic.
626
5.79k
  const int input_length = static_cast<int>(input.size());
627
5.79k
  char* escaped = curl_easy_escape(easy_, input.data(), input_length);
628
5.79k
  if (escaped != nullptr) {
629
5.79k
    int decoded_length = 0;
630
5.79k
    char* decoded = curl_easy_unescape(easy_, escaped, 0, &decoded_length);
631
5.79k
    curl_free(decoded);
632
5.79k
    curl_free(escaped);
633
5.79k
  }
634
5.79k
  int decoded_length = 0;
635
5.79k
  char* decoded = curl_easy_unescape(easy_, input.data(), input_length, &decoded_length);
636
5.79k
  curl_free(decoded);
637
5.79k
}
638
639
/// Run correctly typed CURLINFO and response-header probes selected by the
640
/// plan. Call only after the transfer entrypoint has returned. The typed
641
/// descriptor table suppresses duplicates whose only effect would be charging
642
/// an iteration for the same immutable result. Header traversal remains
643
/// unconditional in the API lane because it exposes a separate public API and
644
/// is independently capped.
645
/// @param probe_upkeep True only while curl_easy_perform's internal multi
646
///        remains attached and can service curl_easy_upkeep safely.
647
5.79k
void ApiLifecycle::ProbeTransferResults(bool probe_upkeep) {
648
5.79k
  std::array<bool, kInfoDescriptorCount> seen{};
649
5.79k
  const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiInfoSelectors,
650
5.79k
                                                           static_cast<std::size_t>(plan_.easy_info_selectors_size()));
651
7.57k
  for (std::size_t index = 0; index < selector_count; ++index) {
652
1.78k
    const std::uint32_t selector = plan_.easy_info_selectors(static_cast<int>(index));
653
1.78k
    const std::size_t descriptor_index = selector % kInfoDescriptorCount;
654
1.78k
    if (!seen[descriptor_index]) {
655
1.49k
      ProbeInfoDescriptor(easy_, kInfoDescriptors[descriptor_index]);
656
1.49k
      seen[descriptor_index] = true;
657
1.49k
    }
658
1.78k
  }
659
660
5.79k
  struct curl_header* header = nullptr;
661
5.79k
  (void)curl_easy_header(easy_, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
662
5.79k
  header = nullptr;
663
8.69k
  for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
664
8.57k
    header = curl_easy_nextheader(easy_, kAllHeaderOrigins, -1, header);
665
8.57k
    if (header == nullptr) {
666
5.67k
      break;
667
5.67k
    }
668
8.57k
  }
669
670
  // curl_easy_perform retains an internal multi that upkeep expects. The
671
  // external multi paths destroy theirs before result probing, and current
672
  // debug builds deliberately reject upkeep on that detached handle state.
673
5.79k
  if (probe_upkeep) {
674
1.27k
    (void)curl_easy_upkeep(easy_);
675
1.27k
  }
676
677
  // Post-transfer pause calls deliberately cover the public API's rejected
678
  // inactive-handle path without changing the request that populated results.
679
5.79k
  (void)curl_easy_pause(easy_, CURLPAUSE_ALL);
680
5.79k
  (void)curl_easy_pause(easy_, CURLPAUSE_CONT);
681
5.79k
}
682
683
/// Duplicate, reset, and destroy a scratch easy handle while every pointer-
684
/// valued option copied from the source still has a live owner. The duplicate
685
/// inherits borrowed slists and callback userdata but not the source share.
686
/// Reset it immediately, then cleanup; performing it would reuse mock/request
687
/// cursors and test a harness artifact instead of libcurl's duplication lifecycle.
688
5.79k
void ApiLifecycle::ProbeEasyDuplication() {
689
5.79k
  if (!plan_.duplicate_easy()) {
690
5.37k
    return;
691
5.37k
  }
692
417
  CurlEasyPtr duplicate(curl_easy_duphandle(easy_));
693
417
  if (duplicate != nullptr) {
694
377
    curl_easy_reset(duplicate.get());
695
377
  }
696
417
}
697
698
}  // namespace proto_fuzzer