Coverage Report

Created: 2026-10-28 07:17

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
173k
void ProbeUrlPart(CURLU* url, CURLUPart part, unsigned int flags) {
47
173k
  char* result = nullptr;
48
173k
  if (curl_url_get(url, part, &result, flags) == CURLUE_OK) {
49
85.1k
    curl_free(result);
50
85.1k
  }
51
173k
}
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
586
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
133
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.00k
bool ProbePublicApiOnce(CURL* easy, CURLM* multi, ReentrantProbeKind kind) {
242
3.00k
  switch (kind) {
243
216
    case ReentrantProbeKind::kEasySetopt:
244
216
      return RecursiveRejected(curl_easy_setopt(easy, CURLOPT_REFERER, "reentrant.test"));
245
83
    case ReentrantProbeKind::kEasyGetinfo: {
246
83
      long response_code = 0;
247
83
      return RecursiveRejected(curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code));
248
0
    }
249
234
    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
234
      struct curl_header* header = nullptr;
253
234
      (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
254
234
      return false;
255
0
    }
256
1.96k
    case ReentrantProbeKind::kEasyDuphandle: {
257
      // duphandle is reentrant; duplicate, reset, and release immediately so
258
      // the copied option pointers never outlive this callback.
259
1.96k
      CURL* duplicate = curl_easy_duphandle(easy);
260
1.96k
      if (duplicate != nullptr) {
261
1.81k
        curl_easy_reset(duplicate);
262
1.81k
        curl_easy_cleanup(duplicate);
263
1.81k
      }
264
1.96k
      return false;
265
0
    }
266
83
    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
83
      curl_easy_reset(easy);
270
83
      return false;
271
59
    case ReentrantProbeKind::kEasyPerform:
272
59
      return RecursiveRejected(curl_easy_perform(easy));
273
58
    case ReentrantProbeKind::kEasyUpkeep:
274
58
      return RecursiveRejected(curl_easy_upkeep(easy));
275
32
    case ReentrantProbeKind::kEasySend: {
276
32
      char byte = 'x';
277
32
      std::size_t sent = 0;
278
32
      return RecursiveRejected(curl_easy_send(easy, &byte, 1, &sent));
279
0
    }
280
78
    case ReentrantProbeKind::kEasyRecv: {
281
78
      char byte = 0;
282
78
      std::size_t received = 0;
283
78
      return RecursiveRejected(curl_easy_recv(easy, &byte, 1, &received));
284
0
    }
285
60
    case ReentrantProbeKind::kEasyPause:
286
60
      return RecursiveRejected(curl_easy_pause(easy, CURLPAUSE_CONT));
287
58
    case ReentrantProbeKind::kMultiPerform: {
288
58
      int running = 0;
289
58
      return RecursiveRejected(curl_multi_perform(multi, &running));
290
0
    }
291
19
    case ReentrantProbeKind::kMultiSetopt:
292
19
      return RecursiveRejected(curl_multi_setopt(multi, CURLMOPT_MAXCONNECTS, 0L));
293
41
    case ReentrantProbeKind::kMultiAddHandle:
294
41
      return RecursiveRejected(curl_multi_add_handle(multi, easy));
295
15
    case ReentrantProbeKind::kMultiWait: {
296
15
      int numfds = 0;
297
15
      return RecursiveRejected(curl_multi_wait(multi, nullptr, 0, 0, &numfds));
298
0
    }
299
3.00k
  }
300
0
  return false;
301
3.00k
}
302
303
/// Call one CURLINFO descriptor with storage matching its encoded type.
304
2.30k
void ProbeInfoDescriptor(CURL* easy, const InfoDescriptor& descriptor) {
305
2.30k
  switch (descriptor.result_type) {
306
841
    case InfoResultType::kString: {
307
841
      char* result = nullptr;
308
841
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
309
841
      return;
310
0
    }
311
578
    case InfoResultType::kLong: {
312
578
      long result = 0;
313
578
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
314
578
      return;
315
0
    }
316
164
    case InfoResultType::kDouble: {
317
164
      double result = 0;
318
164
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
319
164
      return;
320
0
    }
321
495
    case InfoResultType::kOffset: {
322
495
      curl_off_t result = 0;
323
495
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
324
495
      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
27
    case InfoResultType::kCertificateInfo: {
332
27
      struct curl_certinfo* result = nullptr;
333
27
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
334
27
      return;
335
0
    }
336
39
    case InfoResultType::kTlsSessionInfo: {
337
39
      struct curl_tlssessioninfo* result = nullptr;
338
39
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
339
39
      return;
340
0
    }
341
61
    case InfoResultType::kOwnedSlist: {
342
61
      struct curl_slist* result = nullptr;
343
61
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
344
61
      curl_slist_free_all(result);
345
61
      return;
346
0
    }
347
41
    case InfoResultType::kUnknown: {
348
41
      void* result = nullptr;
349
41
      (void)curl_easy_getinfo(easy, descriptor.info, &result);
350
41
      return;
351
0
    }
352
2.30k
  }
353
2.30k
}
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.59k
void ProbeKnownErrorStringsOnce() {
359
5.59k
  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.59k
  (void)probed;
376
5.59k
}
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.59k
void ProbeEasyOptionMetadataOnce() {
383
5.59k
  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.59k
  (void)probed;
396
5.59k
}
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.59k
    : easy_(easy),
411
5.59k
      plan_(plan),
412
5.59k
      share_(nullptr),
413
5.59k
      active_multi_(nullptr),
414
5.59k
      guessed_scheme_url_(false),
415
5.59k
      version_info_probed_(false),
416
5.59k
      https_proxy_feature_present_(false) {
417
5.59k
  ProbeKnownErrorStringsOnce();
418
5.59k
  ProbeEasyOptionMetadataOnce();
419
5.59k
  if (plan_.probe_version_info()) {
420
675
    const curl_version_info_data* info = curl_version_info(CURLVERSION_NOW);
421
675
    version_info_probed_ = info != nullptr;
422
675
    if (info != nullptr) {
423
675
      https_proxy_feature_present_ = (info->features & CURL_VERSION_HTTPS_PROXY) != 0;
424
      // Feature names are null-terminated public data. Keep even a malformed
425
      // provider response from turning this observation into an unbounded walk.
426
675
      if (info->feature_names != nullptr) {
427
13.5k
        for (std::size_t index = 0; index < 64 && info->feature_names[index] != nullptr; ++index) {
428
12.8k
          (void)std::string_view(info->feature_names[index]);
429
12.8k
        }
430
675
      }
431
675
    }
432
675
  }
433
5.59k
  ProbeUrlAndEscaping(url);
434
5.59k
  response_callback_state_.owner = this;
435
5.59k
  const bool wants_response_callback = plan_.pause_response_once() || plan_.reentrant_probe_selectors_size() > 0;
436
5.59k
  if (wants_response_callback) {
437
1.50k
    response_callback_state_.pause_once = plan_.pause_response_once();
438
    // Install userdata first: if the callback setopt were ever rejected, the
439
    // baseline sink safely ignores the non-null pointer. The opposite order
440
    // could dispatch this callback with libcurl's default FILE* userdata.
441
1.50k
    (void)curl_easy_setopt(easy_, CURLOPT_WRITEDATA, &response_callback_state_);
442
1.50k
    (void)curl_easy_setopt(easy_, CURLOPT_WRITEFUNCTION, &ApiLifecycle::ResponseWrite);
443
1.50k
  }
444
5.59k
  if (plan_.attach_share()) {
445
1.05k
    ConfigureShare();
446
1.05k
  }
447
5.59k
}
448
449
/// RunScenario keeps this owner alive through easy cleanup, which releases
450
/// even an incomplete connection's share reference before CleanupShare runs.
451
5.59k
ApiLifecycle::~ApiLifecycle() { CleanupShare(); }
452
453
/// Pause the first non-empty body delivery, then accept its replay. Curl replays
454
/// the same bytes when the event loop calls curl_easy_pause(CURLPAUSE_CONT), at
455
/// which point accepting the full count exercises its callback-output buffering
456
/// path.
457
3.08k
std::size_t ApiLifecycle::ResponseWrite(char* /*contents*/, std::size_t size, std::size_t nmemb, void* user_data) {
458
3.08k
  auto* state = static_cast<ResponseCallbackState*>(user_data);
459
3.08k
  if (state == nullptr || (size != 0 && nmemb > std::numeric_limits<std::size_t>::max() / size)) {
460
0
    return 0;
461
0
  }
462
3.08k
  const std::size_t bytes = size * nmemb;
463
3.08k
  if (bytes != 0 && state->pause_once && !state->pause_returned) {
464
249
    state->pause_returned = true;
465
249
    return CURL_WRITEFUNC_PAUSE;
466
249
  }
467
2.83k
  if (bytes > std::numeric_limits<std::size_t>::max() - state->bytes_received) {
468
0
    return 0;
469
0
  }
470
2.83k
  state->bytes_received += bytes;
471
2.83k
  if (bytes != 0 && state->owner != nullptr && !state->probes_done) {
472
501
    state->probes_done = true;
473
501
    state->owner->RunReentrantProbes();
474
501
  }
475
2.83k
  return bytes;
476
2.83k
}
477
478
/// @return whether the API-plan response callback returned PAUSE once.
479
5.59k
bool ApiLifecycle::response_pause_returned() const { return response_callback_state_.pause_returned; }
480
481
/// @return response bytes accepted after the paused chunk was replayed.
482
5.59k
std::size_t ApiLifecycle::response_bytes_received() const { return response_callback_state_.bytes_received; }
483
484
/// Hand the driving multi handle to the reentrancy probes so the multi-entry
485
/// probes can target the live object. Called before the drive loop starts;
486
/// null in easy-only drives, where multi probes are skipped.
487
/// @param multi Live multi handle driving the transfer, or null in easy-only
488
///        drives where multi probes are skipped.
489
4.89k
void ApiLifecycle::SetActiveMulti(CURLM* multi) { active_multi_ = multi; }
490
491
/// @return number of public API probes fired from the response callback.
492
5.59k
std::size_t ApiLifecycle::reentrant_probes_run() const { return response_callback_state_.probes_run; }
493
494
/// @return number of probes libcurl rejected with a recursive-API-call code.
495
5.59k
std::size_t ApiLifecycle::reentrant_recursive_rejections() const {
496
5.59k
  return response_callback_state_.recursive_rejections;
497
5.59k
}
498
499
/// Fire each selected public API call once, in scenario order, from inside the
500
/// first non-empty response callback. Multi probes are skipped when the drive
501
/// exposes no multi handle. Record how many probes ran and how many libcurl
502
/// rejected as recursive so tests can prove the guard branch was actually taken.
503
501
void ApiLifecycle::RunReentrantProbes() {
504
501
  const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxApiReentrantSelectors,
505
501
                                                  static_cast<std::size_t>(plan_.reentrant_probe_selectors_size()));
506
3.54k
  for (std::size_t index = 0; index < count; ++index) {
507
3.04k
    const auto selector = static_cast<std::size_t>(plan_.reentrant_probe_selectors(static_cast<int>(index)));
508
3.04k
    const ReentrantProbe& probe = kReentrantProbes[selector % kReentrantProbeCount];
509
3.04k
    if (probe.needs_multi && active_multi_ == nullptr) {
510
39
      continue;
511
39
    }
512
3.00k
    ++response_callback_state_.probes_run;
513
3.00k
    if (ProbePublicApiOnce(easy_, active_multi_, probe.kind)) {
514
360
      ++response_callback_state_.recursive_rejections;
515
360
    }
516
3.00k
  }
517
501
}
518
519
/// Count one share lock callback without introducing synchronization into the
520
/// fuzzer's single-threaded lifecycle. The state is owned by this lifecycle and
521
/// remains valid until after the final share cleanup callback.
522
6.55k
void ApiLifecycle::ShareLock(CURL* /*easy*/, curl_lock_data /*data*/, curl_lock_access /*access*/, void* user_data) {
523
6.55k
  auto* state = static_cast<ShareCallbackState*>(user_data);
524
6.55k
  ++state->locks;
525
6.55k
}
526
527
/// Count the matching unlock callback using the same live userdata, without
528
/// recursively entering any libcurl API.
529
6.55k
void ApiLifecycle::ShareUnlock(CURL* /*easy*/, curl_lock_data /*data*/, void* user_data) {
530
6.55k
  auto* state = static_cast<ShareCallbackState*>(user_data);
531
6.55k
  ++state->unlocks;
532
6.55k
}
533
534
/// Install and attach a share according to bounded typed selectors. Configure
535
/// cache domains before attachment, when SHARE/UNSHARE transitions are valid.
536
/// Once attached, probe mutable userdata plus the cleanup API's safe
537
/// CURLSHE_IN_USE refusal without destroying the referenced handle.
538
1.05k
void ApiLifecycle::ConfigureShare() {
539
1.05k
  share_ = curl_share_init();
540
1.05k
  if (share_ == nullptr) {
541
9
    return;
542
9
  }
543
544
  // Install userdata before exposing either callback. curl_share_setopt does
545
  // not invoke them itself, but every later easy/share operation must observe
546
  // a fully formed callback tuple if curl begins using the configured domains.
547
1.04k
  (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
548
1.04k
  (void)curl_share_setopt(share_, CURLSHOPT_LOCKFUNC, &ApiLifecycle::ShareLock);
549
1.04k
  (void)curl_share_setopt(share_, CURLSHOPT_UNLOCKFUNC, &ApiLifecycle::ShareUnlock);
550
551
1.04k
  const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiShareDataSelectors,
552
1.04k
                                                           static_cast<std::size_t>(plan_.share_data_selectors_size()));
553
2.79k
  for (std::size_t index = 0; index < selector_count; ++index) {
554
1.74k
    const std::uint32_t selector = plan_.share_data_selectors(static_cast<int>(index));
555
1.74k
    const curl_lock_data data = kShareData[selector % kShareDataCount];
556
1.74k
    if (curl_share_setopt(share_, CURLSHOPT_SHARE, data) == CURLSHE_OK) {
557
      // Exercise the reversible transition before any transfer can populate
558
      // the selected cache, then leave the domain enabled. In particular,
559
      // unsharing CONNECT after use makes current curl stop treating its
560
      // connection pool as cleanup-owned, so doing teardown in that order
561
      // would manufacture a deterministic leak in the harness.
562
1.37k
      (void)curl_share_setopt(share_, CURLSHOPT_UNSHARE, data);
563
1.37k
      (void)curl_share_setopt(share_, CURLSHOPT_SHARE, data);
564
1.37k
    }
565
1.74k
  }
566
567
1.04k
  if (curl_easy_setopt(easy_, CURLOPT_SHARE, share_) == CURLE_OK) {
568
    // USERDATA remains mutable while attached; cleanup is the complementary
569
    // ownership check and returns CURLSHE_IN_USE without destroying the share.
570
1.04k
    (void)curl_share_setopt(share_, CURLSHOPT_USERDATA, &share_callback_state_);
571
1.04k
    (void)curl_share_cleanup(share_);
572
1.04k
  }
573
1.04k
}
574
575
/// Destroy the share after easy cleanup has released its final reference.
576
/// Explicitly detaching first is not equivalent: curl rejects that setopt while
577
/// an incomplete transfer still has a connection, but easy cleanup always drops
578
/// the reference. Keep successful shared domains configured so curl can identify
579
/// and release caches populated during the transfer.
580
5.59k
void ApiLifecycle::CleanupShare() {
581
5.59k
  if (share_ == nullptr) {
582
4.55k
    return;
583
4.55k
  }
584
1.04k
  if (curl_share_cleanup(share_) == CURLSHE_OK) {
585
1.04k
    share_ = nullptr;
586
1.04k
  }
587
1.04k
}
588
589
/// Exercise URL parsing, typed part retrieval, duplication, and escaping
590
/// against bytes already selected by the bounded scenario. A valid fallback URL
591
/// keeps getter success paths reachable even when a mutation makes the complete
592
/// URL unparsable; the rejected parse still executes first, so this does not
593
/// hide malformed-input branches.
594
5.59k
void ApiLifecycle::ProbeUrlAndEscaping(std::string_view url) {
595
5.59k
  const std::size_t bounded_size = std::min(url.size(), scenario_limits::kMaxApiStringBytes);
596
5.59k
  const std::string input(url.substr(0, bounded_size));
597
598
5.59k
  CURLU* url_handle = curl_url();
599
5.59k
  if (url_handle != nullptr) {
600
5.59k
    if (plan_.guess_scheme_url()) {
601
703
      constexpr char kGuessInput[] = "ftp.example.test/path";
602
703
      if (curl_url_set(url_handle, CURLUPART_URL, kGuessInput, CURLU_GUESS_SCHEME) == CURLUE_OK) {
603
695
        char* scheme = nullptr;
604
695
        if (curl_url_get(url_handle, CURLUPART_SCHEME, &scheme, 0) == CURLUE_OK) {
605
694
          guessed_scheme_url_ = scheme != nullptr && std::string_view(scheme) == "ftp";
606
694
          curl_free(scheme);
607
694
        }
608
695
      }
609
703
      curl_url_cleanup(url_handle);
610
703
      url_handle = curl_url();
611
703
    }
612
5.59k
  }
613
5.59k
  if (url_handle != nullptr) {
614
5.58k
    const unsigned int parse_flags = CURLU_ALLOW_SPACE | CURLU_NON_SUPPORT_SCHEME;
615
5.58k
    if (curl_url_set(url_handle, CURLUPART_URL, input.c_str(), parse_flags) != CURLUE_OK) {
616
933
      (void)curl_url_set(url_handle, CURLUPART_SCHEME, "http", 0);
617
933
      (void)curl_url_set(url_handle, CURLUPART_HOST, "api.test", 0);
618
933
      (void)curl_url_set(url_handle, CURLUPART_PATH, input.c_str(), CURLU_URLENCODE);
619
933
    }
620
621
    // Zero and URLDECODE take distinct getter paths for most components;
622
    // unsupported combinations are documented errors rather than unsafe raw
623
    // varargs, so traversing the full typed part table is intentional.
624
61.4k
    for (const CURLUPart part : kUrlParts) {
625
61.4k
      ProbeUrlPart(url_handle, part, 0);
626
61.4k
      ProbeUrlPart(url_handle, part, CURLU_URLDECODE);
627
61.4k
    }
628
5.58k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_DEFAULT_PORT);
629
5.58k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_DEFAULT_PORT);
630
5.58k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_URLENCODE);
631
5.58k
#if LIBCURL_VERSION_NUM >= 0x075800
632
5.58k
    ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNYCODE);
633
5.58k
#endif
634
5.58k
#if LIBCURL_VERSION_NUM >= 0x080300
635
5.58k
    ProbeUrlPart(url_handle, CURLUPART_HOST, CURLU_PUNY2IDN);
636
5.58k
#endif
637
5.58k
#if LIBCURL_VERSION_NUM >= 0x080800
638
5.58k
    ProbeUrlPart(url_handle, CURLUPART_QUERY, CURLU_GET_EMPTY);
639
5.58k
    ProbeUrlPart(url_handle, CURLUPART_FRAGMENT, CURLU_GET_EMPTY);
640
5.58k
#endif
641
5.58k
#if LIBCURL_VERSION_NUM >= 0x080900
642
5.58k
    ProbeUrlPart(url_handle, CURLUPART_URL, CURLU_NO_GUESS_SCHEME);
643
5.58k
#endif
644
645
    // Mutate only the duplicate so the original handle's getter state remains
646
    // attributable to parsing the scenario URL rather than this lifecycle
647
    // probe's own append operation.
648
5.58k
    CURLU* duplicate = curl_url_dup(url_handle);
649
5.58k
    if (duplicate != nullptr) {
650
5.56k
      (void)curl_url_set(duplicate, CURLUPART_QUERY, input.c_str(), CURLU_APPENDQUERY | CURLU_URLENCODE);
651
5.56k
      ProbeUrlPart(duplicate, CURLUPART_URL, 0);
652
5.56k
      curl_url_cleanup(duplicate);
653
5.56k
    }
654
5.58k
    curl_url_cleanup(url_handle);
655
5.58k
  }
656
657
  // Exercise explicit binary lengths as well as the NUL-terminated API path.
658
  // The API policy caps input far below INT_MAX, making the signed conversion
659
  // and worst-case threefold escaping allocation deterministic.
660
5.59k
  const int input_length = static_cast<int>(input.size());
661
5.59k
  char* escaped = curl_easy_escape(easy_, input.data(), input_length);
662
5.59k
  if (escaped != nullptr) {
663
5.58k
    int decoded_length = 0;
664
5.58k
    char* decoded = curl_easy_unescape(easy_, escaped, 0, &decoded_length);
665
5.58k
    curl_free(decoded);
666
5.58k
    curl_free(escaped);
667
5.58k
  }
668
5.59k
  int decoded_length = 0;
669
5.59k
  char* decoded = curl_easy_unescape(easy_, input.data(), input_length, &decoded_length);
670
5.59k
  curl_free(decoded);
671
5.59k
}
672
673
/// @return True when the opt-in public URL probe inferred FTP successfully.
674
0
bool ApiLifecycle::guessed_scheme_url() const { return guessed_scheme_url_; }
675
676
/// @return True when the opt-in version descriptor query returned data.
677
0
bool ApiLifecycle::version_info_probed() const { return version_info_probed_; }
678
679
/// @return Public HTTPS-proxy feature state observed by the version query.
680
0
bool ApiLifecycle::https_proxy_feature_present() const { return https_proxy_feature_present_; }
681
682
/// Run correctly typed CURLINFO and response-header probes selected by the
683
/// plan. Call only after the transfer entrypoint has returned. The typed
684
/// descriptor table suppresses duplicates whose only effect would be charging
685
/// an iteration for the same immutable result. Header traversal remains
686
/// unconditional in the API lane because it exposes a separate public API and
687
/// is independently capped.
688
/// @param probe_upkeep True only while curl_easy_perform's internal multi
689
///        remains attached and can service curl_easy_upkeep safely.
690
5.59k
void ApiLifecycle::ProbeTransferResults(bool probe_upkeep) {
691
5.59k
  std::array<bool, kInfoDescriptorCount> seen{};
692
5.59k
  const std::size_t selector_count = std::min<std::size_t>(scenario_limits::kMaxApiInfoSelectors,
693
5.59k
                                                           static_cast<std::size_t>(plan_.easy_info_selectors_size()));
694
8.50k
  for (std::size_t index = 0; index < selector_count; ++index) {
695
2.90k
    const std::uint32_t selector = plan_.easy_info_selectors(static_cast<int>(index));
696
2.90k
    const std::size_t descriptor_index = selector % kInfoDescriptorCount;
697
2.90k
    if (!seen[descriptor_index]) {
698
2.30k
      ProbeInfoDescriptor(easy_, kInfoDescriptors[descriptor_index]);
699
2.30k
      seen[descriptor_index] = true;
700
2.30k
    }
701
2.90k
  }
702
703
5.59k
  struct curl_header* header = nullptr;
704
5.59k
  (void)curl_easy_header(easy_, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
705
5.59k
  header = nullptr;
706
12.4k
  for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
707
12.0k
    header = curl_easy_nextheader(easy_, kAllHeaderOrigins, -1, header);
708
12.0k
    if (header == nullptr) {
709
5.27k
      break;
710
5.27k
    }
711
12.0k
  }
712
713
  // curl_easy_perform retains an internal multi that upkeep expects. The
714
  // external multi paths destroy theirs before result probing, and current
715
  // debug builds deliberately reject upkeep on that detached handle state.
716
5.59k
  if (probe_upkeep) {
717
1.00k
    (void)curl_easy_upkeep(easy_);
718
1.00k
  }
719
720
  // Post-transfer pause calls deliberately cover the public API's rejected
721
  // inactive-handle path without changing the request that populated results.
722
5.59k
  (void)curl_easy_pause(easy_, CURLPAUSE_ALL);
723
5.59k
  (void)curl_easy_pause(easy_, CURLPAUSE_CONT);
724
5.59k
}
725
726
/// Duplicate, reset, and destroy a scratch easy handle while every pointer-
727
/// valued option copied from the source still has a live owner. The duplicate
728
/// inherits borrowed slists and callback userdata but not the source share.
729
/// Reset it immediately, then cleanup; performing it would reuse mock/request
730
/// cursors and test a harness artifact instead of libcurl's duplication lifecycle.
731
5.59k
void ApiLifecycle::ProbeEasyDuplication() {
732
5.59k
  if (!plan_.duplicate_easy()) {
733
4.57k
    return;
734
4.57k
  }
735
1.02k
  CurlEasyPtr duplicate(curl_easy_duphandle(easy_));
736
1.02k
  if (duplicate != nullptr) {
737
980
    curl_easy_reset(duplicate.get());
738
980
  }
739
1.02k
}
740
741
}  // namespace proto_fuzzer