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/mock_server.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 MockConnection and MockServer.
9
10
#include "proto_fuzzer/mock_server.h"
11
12
#include <fcntl.h>
13
#include <string.h>
14
#include <sys/select.h>
15
#include <sys/socket.h>
16
#include <sys/types.h>
17
#include <unistd.h>
18
19
#include <algorithm>
20
#include <cstddef>
21
#include <cstdint>
22
#include <limits>
23
#include <string>
24
#include <vector>
25
26
#include "proto_fuzzer/multi_socket_driver.h"
27
#include "proto_fuzzer/scenario_limits.h"
28
#include "proto_fuzzer/ws_frame.h"
29
30
namespace proto_fuzzer {
31
32
namespace {
33
34
// fd_set can only represent file descriptors < FD_SETSIZE. Reject any pair that couldn't participate in select()
35
// without memory corruption.
36
76.4k
bool FdFitsInFdSet(int fd) { return fd >= 0 && fd < FD_SETSIZE; }
37
38
}  // namespace
39
40
/// @class proto_fuzzer::MockConnection
41
/// @brief Owns one half of a socketpair used to feed canned responses to libcurl. The destructor closes the server-side
42
/// fd; the client-side fd is handed to libcurl via CURLOPT_OPENSOCKETFUNCTION and becomes curl's to close.
43
44
/// Construct a non-blocking AF_UNIX/SOCK_STREAM socketpair. Both fds are validated to fit inside FD_SETSIZE; on any
45
/// failure ok() returns false and the instance is unusable.
46
38.2k
MockConnection::MockConnection() : server_fd_(-1), client_fd_(-1), drain_limit_(0) {
47
38.2k
  int fds[2];
48
49
38.2k
  if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) {
50
0
    return;
51
0
  }
52
53
  // The fds must be small enough to fit in an fd_set for select(). If not, close them and fail the constructor.
54
38.2k
  if (!FdFitsInFdSet(fds[0]) || !FdFitsInFdSet(fds[1])) {
55
0
    close(fds[0]);
56
0
    close(fds[1]);
57
0
    return;
58
0
  }
59
60
  // Set the server-side fd non-blocking so we can write to it without risk of hanging the fuzzer.
61
38.2k
  int flags = fcntl(fds[0], F_GETFL, 0);
62
38.2k
  if (flags < 0 || fcntl(fds[0], F_SETFL, flags | O_NONBLOCK) < 0) {
63
0
    close(fds[0]);
64
0
    close(fds[1]);
65
0
    return;
66
0
  }
67
68
  // Success: store the file descriptors.
69
38.2k
  server_fd_ = fds[0];
70
38.2k
  client_fd_ = fds[1];
71
38.2k
}
72
73
/// Close the server-side fd (and the client-side fd if it was never handed off via take_client_fd()).
74
38.2k
MockConnection::~MockConnection() {
75
38.2k
  if (server_fd_ >= 0) {
76
38.2k
    close(server_fd_);
77
38.2k
  }
78
38.2k
  if (client_fd_ >= 0) {
79
0
    close(client_fd_);
80
0
  }
81
38.2k
}
82
83
/// @return true if the underlying socketpair was set up successfully.
84
49.7k
bool MockConnection::ok() const { return server_fd_ >= 0; }
85
86
/// @return the server-side fd (still owned by this MockConnection).
87
7.08k
int MockConnection::server_fd() const { return server_fd_; }
88
89
/// Query the client endpoint while this object still owns it. A zero result is
90
/// deliberately ambiguous between an invalid fd and a platform query failure:
91
/// callers need only distinguish a verified capacity from every unsafe case.
92
2.45k
std::size_t MockConnection::client_send_buffer_size() const {
93
2.45k
  if (client_fd_ < 0) {
94
0
    return 0;
95
0
  }
96
2.45k
  int size = 0;
97
2.45k
  socklen_t length = sizeof(size);
98
2.45k
  if (getsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &size, &length) != 0 || size <= 0) {
99
0
    return 0;
100
0
  }
101
2.45k
  return static_cast<std::size_t>(size);
102
2.45k
}
103
104
/// Establish and verify the capacity required by a synchronous protocol
105
/// driver. Linux may transform socket-buffer requests, so the post-set query
106
/// is the contract rather than assuming setsockopt accepted the exact value.
107
2.45k
bool MockConnection::EnsureClientSendBufferSize(std::size_t minimum) {
108
2.45k
  if (client_send_buffer_size() >= minimum) {
109
2.45k
    return true;
110
2.45k
  }
111
0
  if (client_fd_ < 0 || minimum > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
112
0
    return false;
113
0
  }
114
0
  const int requested = static_cast<int>(minimum);
115
0
  if (setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &requested, sizeof(requested)) != 0) {
116
0
    return false;
117
0
  }
118
0
  return client_send_buffer_size() >= minimum;
119
0
}
120
121
/// Hand the client-side fd to libcurl. After this call the caller owns the fd and the MockConnection will not close it
122
/// on destruction.
123
/// @return the client-side socket fd as a curl_socket_t.
124
38.2k
curl_socket_t MockConnection::take_client_fd() {
125
38.2k
  int fd = client_fd_;
126
38.2k
  client_fd_ = -1;
127
38.2k
  return static_cast<curl_socket_t>(fd);
128
38.2k
}
129
130
/// Write 'size' bytes from 'data' to the server fd, looping until the whole
131
/// buffer is sent or a short/failed write occurs. MSG_NOSIGNAL is a harness
132
/// invariant rather than a curl behavior choice: a response race may leave the
133
/// peer closed, and that must look like an ordinary failed mock write instead
134
/// of terminating the fuzz process with SIGPIPE.
135
/// @param data Buffer to send.
136
/// @param size Number of bytes in 'data'.
137
/// @return false on short or failed write (treat the connection as lost).
138
76.5k
bool MockConnection::WriteAll(const unsigned char* data, std::size_t size) {
139
76.5k
  if (server_fd_ < 0) {
140
0
    return false;
141
0
  }
142
76.5k
  std::size_t written = 0;
143
152k
  while (written < size) {
144
76.5k
    ssize_t n = ::send(server_fd_, data + written, size - written, MSG_NOSIGNAL);
145
76.5k
    if (n <= 0) {
146
92
      return false;
147
92
    }
148
76.4k
    written += static_cast<std::size_t>(n);
149
76.4k
  }
150
76.4k
  return true;
151
76.5k
}
152
153
/// Drain bytes curl has written. When a backpressure drain limit has been
154
/// applied (see ApplyBackpressure), stops after drain_limit_ bytes so the
155
/// kernel recv buffer stays near-full and curl keeps seeing short writes.
156
/// Otherwise drains until read() returns 0/EAGAIN, matching legacy behaviour.
157
/// @return number of bytes consumed during this call.
158
301k
std::size_t MockConnection::DrainIncoming() {
159
301k
  if (server_fd_ < 0) {
160
0
    return 0;
161
0
  }
162
301k
  unsigned char scratch[4096];
163
301k
  std::size_t drained = 0;
164
391k
  while (drain_limit_ == 0 || drained < drain_limit_) {
165
337k
    std::size_t want = sizeof(scratch);
166
337k
    if (drain_limit_ != 0) {
167
102k
      const std::size_t remaining = drain_limit_ - drained;
168
102k
      if (remaining < want) {
169
91.5k
        want = remaining;
170
91.5k
      }
171
102k
    }
172
337k
    ssize_t n = ::read(server_fd_, scratch, want);
173
337k
    if (n <= 0) {
174
247k
      break;
175
247k
    }
176
90.1k
    drained += static_cast<std::size_t>(n);
177
90.1k
  }
178
301k
  return drained;
179
301k
}
180
181
/// Tighten both halves of the socketpair buffer and/or cap DrainIncoming's
182
/// per-call byte budget. SO_RCVBUF on the server fd caps how much curl can
183
/// push into the pipe; SO_SNDBUF on the client fd (which curl will soon own
184
/// but hasn't yet, so we can still tune it) caps how much curl's send() can
185
/// buffer before short-writing. Linux socketpairs effectively use max(SNDBUF,
186
/// RCVBUF*2) as pipe capacity, so we need both to see short writes reliably.
187
/// See header docs.
188
35.7k
void MockConnection::ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit) {
189
35.7k
  if (recv_buf_bytes > 0) {
190
4.77k
    if (server_fd_ >= 0) {
191
4.77k
      (void)setsockopt(server_fd_, SOL_SOCKET, SO_RCVBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
192
4.77k
    }
193
4.77k
    if (client_fd_ >= 0) {
194
4.77k
      (void)setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
195
4.77k
    }
196
4.77k
  }
197
35.7k
  drain_limit_ = drain_limit;
198
35.7k
}
199
200
/// Non-blocking read: append whatever bytes are currently available on the
201
/// server fd to 'out'. Used by the WS handshake path to collect curl's HTTP
202
/// Upgrade request without losing any bytes.
203
/// @param out Destination buffer; unchanged if no bytes are pending.
204
22.2k
void MockConnection::ReadAvailable(std::string* out) {
205
22.2k
  if (server_fd_ < 0 || out == nullptr) {
206
0
    return;
207
0
  }
208
22.2k
  unsigned char scratch[4096];
209
27.4k
  while (true) {
210
27.4k
    ssize_t n = ::read(server_fd_, scratch, sizeof(scratch));
211
27.4k
    if (n <= 0) {
212
22.2k
      break;
213
22.2k
    }
214
5.27k
    out->append(reinterpret_cast<const char*>(scratch), static_cast<std::size_t>(n));
215
5.27k
  }
216
22.2k
}
217
218
/// Signal end-of-response to libcurl by half-closing the write side.
219
27.1k
void MockConnection::ShutdownWrite() {
220
27.1k
  if (server_fd_ < 0) {
221
0
    return;
222
0
  }
223
27.1k
  ::shutdown(server_fd_, SHUT_WR);
224
27.1k
}
225
226
/// @class proto_fuzzer::MockServer
227
/// @brief Orchestrates a bounded sequence of mock HTTP exchanges: installs
228
/// socket callbacks on an easy handle, assigns a response script to each new
229
/// socket, and feeds queued chunks as libcurl reads them.
230
231
/// Construct an idle MockServer with no scripted responses or open peers.
232
/// DriveScenario() configures it from a Scenario before curl can open a socket.
233
28.1k
MockServer::MockServer() : script_count_(0), next_script_(0), active_script_(nullptr), preload_all_chunks_(false) {}
234
235
/// Default destructor; current and previous MockConnections clean up their
236
/// server-side descriptors only after curl has been removed from the multi.
237
28.1k
MockServer::~MockServer() = default;
238
239
/// Build the complete borrowed script table before entering curl. The primary
240
/// Connection always occupies slot zero for backwards compatibility; only
241
/// three subsequent pointers are retained so protobuf mutations cannot
242
/// allocate socketpairs or prolong redirects in proportion to repeated-field
243
/// size. The Scenario passed by ScenarioRunner outlives this synchronous drive,
244
/// which makes borrowing safe while avoiding response-byte copies.
245
/// @param scenario Source of the primary and bounded follow-on scripts.
246
28.1k
void MockServer::SetScripts(const curl::fuzzer::proto::Scenario& scenario) {
247
28.1k
  ResetConnections();
248
28.1k
  script_count_ = 0;
249
28.1k
  next_script_ = 0;
250
251
35.0k
  const auto append_script = [this](const curl::fuzzer::proto::Connection& connection) {
252
35.0k
    ConnectionScript& script = scripts_[script_count_++];
253
35.0k
    script.connection = &connection;
254
35.0k
    script.raw_chunk_count = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, connection.on_readable_size());
255
35.0k
    const std::size_t frame_budget = scenario_limits::kMaxResponseChunks - script.raw_chunk_count;
256
35.0k
    script.frame_chunk_count = std::min<std::size_t>(frame_budget, connection.server_frames_size());
257
35.0k
    script.next_chunk = 0;
258
35.0k
  };
259
260
28.1k
  append_script(scenario.connection());
261
28.1k
  const std::size_t subsequent_count = std::min<std::size_t>(
262
28.1k
      scenario_limits::kMaxConnections - 1, static_cast<std::size_t>(scenario.subsequent_connections_size()));
263
35.0k
  for (std::size_t i = 0; i < subsequent_count; ++i) {
264
6.95k
    append_script(scenario.subsequent_connections(static_cast<int>(i)));
265
6.95k
  }
266
28.1k
}
267
268
/// @return true if at least one on_readable chunk has not yet been sent.
269
206k
bool MockServer::has_more_chunks() const {
270
206k
  return active_script_ != nullptr && active_script_->next_chunk < active_script_->chunk_count();
271
206k
}
272
273
/// Keep plaintext transport construction behind a virtual boundary so the
274
/// HTTPS lane can add TLS without copying the HTTP script state machine.
275
27.4k
std::unique_ptr<MockConnection> MockServer::CreateConnection() { return std::make_unique<MockConnection>(); }
276
277
/// Release all socketpairs while the dynamic transport type is still alive.
278
/// This is separate from SetScripts because a derived destructor may need to
279
/// enforce a stricter order than C++'s derived-member-before-base teardown.
280
31.8k
void MockServer::ResetConnections() {
281
31.8k
  active_script_ = nullptr;
282
31.8k
  connection_.reset();
283
31.8k
  previous_connections_.clear();
284
31.8k
}
285
286
/// Plain HTTP has no connection-filter result that must be observed live.
287
/// @param easy Active easy handle, unused by the plaintext transport.
288
122k
void MockServer::ObserveActiveTransfer(CURL* /*easy*/) {}
289
290
/// Called by the OPENSOCKETFUNCTION trampoline in the base class. Creates the
291
/// MockConnection, writes initial_response into it, and returns the
292
/// client-side fd to hand to libcurl.
293
/// @param purpose Socket role requested by curl; ordinary stream mocks do not
294
///                need to distinguish outbound HTTP roles.
295
/// @param address Intended destination retained by curl; the socketpair is
296
///                already connected and therefore leaves it untouched.
297
/// @return the client-side fd to hand to libcurl, or CURL_SOCKET_BAD on
298
///         failure.
299
33.1k
curl_socket_t MockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
300
33.1k
  (void)purpose;
301
33.1k
  (void)address;
302
33.1k
  if (next_script_ >= script_count_) {
303
    // Refusing a fifth socket keeps redirect loops bounded even if curl's own
304
    // redirect limit is mutated upward or an authentication scheme retries.
305
2.03k
    return CURL_SOCKET_BAD;
306
2.03k
  }
307
308
31.0k
  if (connection_) {
309
    // libcurl owns the corresponding client fd, so retain the server half
310
    // until the easy handle leaves the multi instead of closing it at the
311
    // moment a redirect or retry opens its replacement.
312
4.47k
    previous_connections_.push_back(std::move(connection_));
313
4.47k
  }
314
315
31.0k
  active_script_ = &scripts_[next_script_++];
316
31.0k
  const curl::fuzzer::proto::Connection& script_connection = *active_script_->connection;
317
31.0k
  connection_ = CreateConnection();
318
31.0k
  if (!connection_ || !connection_->ok()) {
319
0
    connection_.reset();
320
0
    active_script_ = nullptr;
321
0
    return CURL_SOCKET_BAD;
322
0
  }
323
324
  // Target policies clamp/clear these values before execution. Saturating the
325
  // compatibility lane's raw uint32 avoids implementation-defined narrowing
326
  // while preserving its ability to request any representable socket size.
327
31.0k
  const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max());
328
31.0k
  const auto& backpressure = script_connection.backpressure();
329
31.0k
  const int recv_buf_bytes = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max));
330
31.0k
  connection_->ApplyBackpressure(recv_buf_bytes, static_cast<std::size_t>(backpressure.drain_limit()));
331
332
31.0k
  const std::string& initial_response = script_connection.initial_response();
333
31.0k
  if (!initial_response.empty()) {
334
18.6k
    if (!connection_->WriteAll(reinterpret_cast<const unsigned char*>(initial_response.data()),
335
18.6k
                               initial_response.size())) {
336
0
      connection_.reset();
337
0
      active_script_ = nullptr;
338
0
      return CURL_SOCKET_BAD;
339
0
    }
340
18.6k
  }
341
31.0k
  if (preload_all_chunks_) {
342
0
    while (has_more_chunks()) {
343
0
      (void)DeliverNextChunk();
344
0
    }
345
0
  }
346
31.0k
  if (active_script_->chunk_count() == 0) {
347
16.2k
    connection_->ShutdownWrite();
348
16.2k
  }
349
31.0k
  return connection_->take_client_fd();
350
31.0k
}
351
352
/// Preload all bounded response bytes from inside OPENSOCKETFUNCTION, where
353
/// the mock still owns both socketpair ends. The total serialized fuzz input
354
/// is capped by libFuzzer's max_len. If a non-blocking preload fills the local
355
/// socket, curl observes only the successfully queued prefix and the short
356
/// timeout below bounds the incomplete response. Reassert that timeout after
357
/// scenario setopts so a mutated blocking option cannot turn this synchronous
358
/// coverage path into a hung worker.
359
/// @param easy Configured easy handle whose open-socket callback targets this
360
///        mock.
361
/// @param scenario Bounded response script to preload before performing.
362
0
void MockServer::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
363
0
  SetScripts(scenario);
364
0
  preload_all_chunks_ = true;
365
0
  (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 50L);
366
0
  (void)curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 50L);
367
0
  (void)curl_easy_perform(easy);
368
0
  preload_all_chunks_ = false;
369
0
}
370
371
/// Push the next queued chunk. Called by the drive loop when curl is ready
372
/// for more data.
373
/// @return true when a chunk was consumed from the script.
374
50.5k
bool MockServer::DeliverNextChunk() {
375
50.5k
  if (!connection_ || !has_more_chunks()) {
376
0
    return false;
377
0
  }
378
50.5k
  connection_->DrainIncoming();
379
50.5k
  const std::size_t chunk_index = active_script_->next_chunk++;
380
50.5k
  const auto& script_connection = *active_script_->connection;
381
50.5k
  if (chunk_index < active_script_->raw_chunk_count) {
382
41.9k
    const std::string& chunk = script_connection.on_readable(static_cast<int>(chunk_index));
383
41.9k
    if (!chunk.empty()) {
384
39.8k
      connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
385
39.8k
    }
386
41.9k
  } else {
387
    // HTTP scenarios historically accept structured WebSocket frames as raw
388
    // response bytes after every on_readable chunk. Serialize only the frame
389
    // curl is about to receive: follow-on scripts and capped suffix frames may
390
    // never be consumed, so eagerly materialising all of them wastes mutations.
391
8.61k
    const std::size_t frame_index = chunk_index - active_script_->raw_chunk_count;
392
8.61k
    const std::string chunk = SerializeWebSocketFrame(script_connection.server_frames(static_cast<int>(frame_index)));
393
8.61k
    if (!chunk.empty()) {
394
8.61k
      connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
395
8.61k
    }
396
8.61k
  }
397
50.5k
  if (!has_more_chunks()) {
398
11.1k
    connection_->ShutdownWrite();
399
11.1k
  }
400
50.5k
  return true;
401
50.5k
}
402
403
/// Drain every live mock peer. Previous connections are normally quiescent,
404
/// but curl can finish sending a request body or close one after it has begun
405
/// resolving/opening the redirect target. Servicing both sides makes that
406
/// overlap deterministic without conflating their response scripts.
407
105k
std::size_t MockServer::DrainIncomingConnections() {
408
105k
  std::size_t drained = 0;
409
105k
  for (const auto& previous : previous_connections_) {
410
25.1k
    drained += previous->DrainIncoming();
411
25.1k
  }
412
105k
  if (connection_) {
413
105k
    drained += connection_->DrainIncoming();
414
105k
  }
415
105k
  return drained;
416
105k
}
417
418
/// Keep peer servicing identical across the perform and socket-action APIs.
419
/// Draining first creates request-side space before a response can provoke
420
/// another write, and releasing only one chunk preserves the scenario's
421
/// mutation-controlled parser boundaries.
422
105k
bool MockServer::ServiceConnections() {
423
105k
  bool made_progress = DrainIncomingConnections() != 0;
424
105k
  if (has_more_chunks()) {
425
50.5k
    made_progress = DeliverNextChunk() || made_progress;
426
50.5k
  }
427
105k
  return made_progress;
428
105k
}
429
430
/// Run a zero-wait application event loop around curl_multi_socket_action.
431
/// Positive timers are deliberately not slept: the API lane clears timing
432
/// controls and exists to cover event-driven dispatch, while the dedicated
433
/// timing lane remains responsible for clock-dependent behavior.
434
0
void MockServer::RunSocketActionLoop(CURLM* multi, CURL* easy) {
435
0
  MultiSocketDriver* driver = multi_socket_driver();
436
0
  if (driver == nullptr) {
437
0
    return;
438
0
  }
439
440
0
  int still_running = 1;
441
0
  CURLMcode rc = driver->Start(&still_running);
442
0
  ObserveActiveTransfer(easy);
443
0
  int idle_iterations = 0;
444
0
  int drive_iterations = 0;
445
0
  while (rc == CURLM_OK && still_running && idle_iterations < kMaxIdleIterations &&
446
0
         drive_iterations++ < kMaxDriveIterations) {
447
0
    bool made_progress = ServiceConnections();
448
0
    const MultiSocketDriver::DriveResult drive_result = driver->DriveReady(&still_running);
449
0
    rc = drive_result.code;
450
0
    ObserveActiveTransfer(easy);
451
0
    made_progress = drive_result.made_progress || made_progress;
452
0
    if (made_progress) {
453
0
      idle_iterations = 0;
454
0
    } else {
455
0
      ++idle_iterations;
456
0
    }
457
0
  }
458
0
}
459
460
/// Seed the mock from the scenario, then drive the perform loop until curl is
461
/// done or a deterministic operation/idle budget is hit. Ordinary scenarios
462
/// never wait; explicit backpressure scenarios may use short select() waits.
463
/// @param multi    caller-owned multi; 'easy' is already added.
464
/// @param easy     the curl easy handle attached to this mock.
465
/// @param scenario source of the initial_response and on_readable chunks.
466
28.1k
void MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
467
28.1k
  SetScripts(scenario);
468
469
28.1k
  if (multi_socket_driver() != nullptr) {
470
0
    RunSocketActionLoop(multi, easy);
471
0
    return;
472
0
  }
473
474
28.1k
  int still_running = 1;
475
28.1k
  int idle_iterations = 0;
476
28.1k
  int drive_iterations = 0;
477
28.1k
  bool pollset_probed = false;
478
  // Each newly opened socket can carry its own pressure settings. Inspect only
479
  // the borrowed, runtime-bounded scripts: ignored repeated protobuf entries must
480
  // not opt a fast transfer into timed waits.
481
28.1k
  const bool timed_drive =
482
33.5k
      std::any_of(scripts_.begin(), scripts_.begin() + script_count_, [](const ConnectionScript& script) {
483
33.5k
        const auto& backpressure = script.connection->backpressure();
484
33.5k
        return backpressure.recv_buf_bytes() != 0 || backpressure.drain_limit() != 0;
485
33.5k
      });
486
28.1k
  const int idle_limit = timed_drive ? kMaxTimedIdleIterations : kMaxIdleIterations;
487
28.1k
  CURLMcode rc = CURLM_OK;
488
489
133k
  while (still_running && idle_iterations < idle_limit && drive_iterations++ < kMaxDriveIterations) {
490
133k
    bool made_progress = false;
491
133k
    const int running_before = still_running;
492
133k
    rc = curl_multi_perform(multi, &still_running);
493
133k
    if (rc != CURLM_OK) {
494
0
      break;
495
0
    }
496
133k
    ObserveActiveTransfer(easy);
497
133k
    made_progress = still_running != running_before;
498
133k
    if (timed_drive && still_running && !pollset_probed) {
499
      // Probe only after curl has built the socket/filter chain. A zero-timeout
500
      // poll adds no wall-clock wait; restricting it to the timing lane keeps
501
      // the fixed HTTP target free of a per-input polling syscall.
502
2.08k
      ProbeMultiPollset(multi);
503
2.08k
      pollset_probed = true;
504
2.08k
    }
505
133k
    if (!still_running) {
506
28.0k
      break;
507
28.0k
    }
508
509
    // Always drain whatever curl has written. Under backpressure the kernel
510
    // recv buffer would otherwise stay full — curl short-writes, the mock
511
    // never consumes, and the transfer wedges until the drive budget. With
512
    // drain_limit set this still honours the per-tick byte budget.
513
105k
    made_progress = ServiceConnections() || made_progress;
514
515
105k
    if (made_progress) {
516
88.8k
      idle_iterations = 0;
517
88.8k
      continue;
518
88.8k
    }
519
520
    // Ordinary socketpair scenarios never sleep: repeated no-progress
521
    // performs are enough to settle curl's local state machine. Only an
522
    // explicit BackpressureConfig opts into short waits so timeout-related
523
    // behaviour remains fuzzable without taxing every corpus entry.
524
16.9k
    if (timed_drive) {
525
16.6k
      (void)WaitOnMultiFdset(multi, &rc);
526
16.6k
      if (rc != CURLM_OK) {
527
0
        break;
528
0
      }
529
16.6k
    }
530
16.9k
    ++idle_iterations;
531
16.9k
  }
532
28.1k
}
533
534
}  // namespace proto_fuzzer