Coverage Report

Created: 2026-09-14 07:12

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