Coverage Report

Created: 2026-10-15 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/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
796k
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
398k
MockConnection::MockConnection() : server_fd_(-1), client_fd_(-1), drain_limit_(0), incoming_data_observer_(nullptr) {
58
398k
  int fds[2];
59
60
398k
  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
398k
  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
398k
  int flags = fcntl(fds[0], F_GETFL, 0);
73
398k
  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
398k
  server_fd_ = fds[0];
81
398k
  client_fd_ = fds[1];
82
398k
}
83
84
/// Close the server-side fd (and the client-side fd if it was never handed off via take_client_fd()).
85
398k
MockConnection::~MockConnection() {
86
398k
  if (server_fd_ >= 0) {
87
398k
    close(server_fd_);
88
398k
  }
89
398k
  if (client_fd_ >= 0) {
90
0
    close(client_fd_);
91
0
  }
92
398k
}
93
94
/// @return true if the underlying socketpair was set up successfully.
95
1.98M
bool MockConnection::ok() const { return server_fd_ >= 0; }
96
97
/// @return the server-side fd (still owned by this MockConnection).
98
221k
int MockConnection::server_fd() const { return server_fd_; }
99
100
/// Query the capacity curl's endpoint reports for queued client writes while
101
/// this object still owns that endpoint. Synchronous protocol mocks use this
102
/// before transferring fd ownership because they cannot rely on the outer
103
/// driver to make room. A zero result covers both an invalid fd and a platform
104
/// query failure; callers need a verified capacity before proceeding.
105
/// @return SO_SNDBUF in bytes, or zero when it cannot be queried.
106
4.14k
std::size_t MockConnection::client_send_buffer_size() const {
107
4.14k
  if (client_fd_ < 0) {
108
0
    return 0;
109
0
  }
110
4.14k
  int size = 0;
111
4.14k
  socklen_t length = sizeof(size);
112
4.14k
  if (getsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &size, &length) != 0 || size <= 0) {
113
0
    return 0;
114
0
  }
115
4.14k
  return static_cast<std::size_t>(size);
116
4.14k
}
117
118
/// Ensure curl's endpoint reports at least `minimum` bytes of send buffer,
119
/// requesting a larger SO_SNDBUF when the platform default is smaller. Linux
120
/// may transform socket-buffer requests, so the post-set query establishes the
121
/// contract rather than assuming setsockopt accepted the exact value.
122
/// @param minimum Smallest acceptable reported capacity in bytes.
123
/// @return true when the queried postcondition holds.
124
4.14k
bool MockConnection::EnsureClientSendBufferSize(std::size_t minimum) {
125
4.14k
  if (client_send_buffer_size() >= minimum) {
126
4.14k
    return true;
127
4.14k
  }
128
0
  if (client_fd_ < 0 || minimum > static_cast<std::size_t>(std::numeric_limits<int>::max())) {
129
0
    return false;
130
0
  }
131
0
  const int requested = static_cast<int>(minimum);
132
0
  if (setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &requested, sizeof(requested)) != 0) {
133
0
    return false;
134
0
  }
135
0
  return client_send_buffer_size() >= minimum;
136
0
}
137
138
/// Hand the client-side fd to libcurl. After this call the caller owns the fd and the MockConnection will not close it
139
/// on destruction.
140
/// @return the client-side socket fd as a curl_socket_t.
141
398k
curl_socket_t MockConnection::take_client_fd() {
142
398k
  int fd = client_fd_;
143
398k
  client_fd_ = -1;
144
398k
  return static_cast<curl_socket_t>(fd);
145
398k
}
146
147
/// Write 'size' bytes from 'data' to the server fd, looping until the whole
148
/// buffer is sent or a short/failed write occurs. MSG_NOSIGNAL is a harness
149
/// invariant rather than a curl behavior choice: a response race may leave the
150
/// peer closed, and that must look like an ordinary failed mock write instead
151
/// of terminating the fuzz process with SIGPIPE.
152
/// @param data Buffer to send.
153
/// @param size Number of bytes in 'data'.
154
/// @return false on short or failed write (treat the connection as lost).
155
664k
bool MockConnection::WriteAll(const unsigned char* data, std::size_t size) {
156
664k
  if (server_fd_ < 0) {
157
0
    return false;
158
0
  }
159
664k
  std::size_t written = 0;
160
1.32M
  while (written < size) {
161
664k
    ssize_t n = ::send(server_fd_, data + written, size - written, MSG_NOSIGNAL);
162
664k
    if (n <= 0) {
163
2.50k
      return false;
164
2.50k
    }
165
662k
    written += static_cast<std::size_t>(n);
166
662k
  }
167
662k
  return true;
168
664k
}
169
170
/// Advance incoming transport work according to the configured per-call
171
/// limit. This plaintext implementation reports bytes consumed; layered
172
/// transports may also report handshake state changes so the bounded outer
173
/// loop does not mistake useful protocol progress for an idle connection.
174
/// When a backpressure drain limit has been applied (see ApplyBackpressure),
175
/// stop after drain_limit_ bytes so the kernel recv buffer stays near-full
176
/// and curl keeps seeing short writes. Otherwise drain until read() returns
177
/// 0/EAGAIN, matching legacy behaviour.
178
/// @return number of bytes consumed during this call.
179
2.41M
std::size_t MockConnection::DrainIncoming() {
180
2.41M
  if (server_fd_ < 0) {
181
0
    return 0;
182
0
  }
183
2.41M
  unsigned char scratch[4096];
184
2.41M
  std::size_t drained = 0;
185
2.98M
  while (drain_limit_ == 0 || drained < drain_limit_) {
186
2.85M
    std::size_t want = sizeof(scratch);
187
2.85M
    if (drain_limit_ != 0) {
188
224k
      const std::size_t remaining = drain_limit_ - drained;
189
224k
      if (remaining < want) {
190
213k
        want = remaining;
191
213k
      }
192
224k
    }
193
2.85M
    ssize_t n = ::read(server_fd_, scratch, want);
194
2.85M
    if (n <= 0) {
195
2.27M
      break;
196
2.27M
    }
197
576k
    NotifyIncomingData(scratch, static_cast<std::size_t>(n));
198
576k
    drained += static_cast<std::size_t>(n);
199
576k
  }
200
2.41M
  return drained;
201
2.41M
}
202
203
/// Register a non-owning sink for bytes consumed from curl. TLS transports
204
/// notify it after decryption, while plaintext transports report socket
205
/// bytes directly. The observer must outlive this connection.
206
/// @param observer Observer to notify, or nullptr to disable observation.
207
63.4k
void MockConnection::SetIncomingDataObserver(IncomingDataObserver* observer) { incoming_data_observer_ = observer; }
208
209
/// Forward one application-data fragment to the optional observer.
210
/// @param data First byte of the fragment.
211
/// @param size Number of bytes available at `data`.
212
1.15M
void MockConnection::NotifyIncomingData(const unsigned char* data, std::size_t size) {
213
1.15M
  if (incoming_data_observer_ != nullptr) {
214
311k
    incoming_data_observer_->ObserveIncomingData(data, size);
215
311k
  }
216
1.15M
}
217
218
/// Apply deterministic backpressure knobs before traffic begins. A positive
219
/// recv_buf_bytes sets SO_RCVBUF on the server fd and SO_SNDBUF on the client
220
/// fd, which this object still owns, to make curl short-write or see EAGAIN.
221
/// Linux socketpairs effectively use max(SNDBUF, RCVBUF*2) as pipe capacity,
222
/// so both settings are needed to produce short writes reliably. Cap each
223
/// DrainIncoming call at drain_limit bytes; zero leaves the drain unlimited.
224
/// @param recv_buf_bytes SO_RCVBUF size in bytes, or 0 to leave default.
225
/// @param drain_limit    Max bytes drained per DrainIncoming call, 0 for unlimited.
226
394k
void MockConnection::ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit) {
227
394k
  if (recv_buf_bytes > 0) {
228
19.0k
    if (server_fd_ >= 0) {
229
19.0k
      (void)setsockopt(server_fd_, SOL_SOCKET, SO_RCVBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
230
19.0k
    }
231
19.0k
    if (client_fd_ >= 0) {
232
19.0k
      (void)setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
233
19.0k
    }
234
19.0k
  }
235
394k
  drain_limit_ = drain_limit;
236
394k
}
237
238
/// Non-blocking read: append whatever bytes are currently available on the
239
/// server fd to 'out'. Used by the WS handshake path to collect curl's HTTP
240
/// Upgrade request without losing any bytes.
241
/// @param out Destination buffer; unchanged if no bytes are pending.
242
112k
void MockConnection::ReadAvailable(std::string* out) {
243
112k
  if (server_fd_ < 0 || out == nullptr) {
244
0
    return;
245
0
  }
246
112k
  unsigned char scratch[4096];
247
162k
  while (true) {
248
162k
    ssize_t n = ::read(server_fd_, scratch, sizeof(scratch));
249
162k
    if (n <= 0) {
250
112k
      break;
251
112k
    }
252
50.2k
    out->append(reinterpret_cast<const char*>(scratch), static_cast<std::size_t>(n));
253
50.2k
  }
254
112k
}
255
256
/// Signal end-of-response to libcurl by half-closing the write side.
257
229k
void MockConnection::ShutdownWrite() {
258
229k
  if (server_fd_ < 0) {
259
0
    return;
260
0
  }
261
229k
  ::shutdown(server_fd_, SHUT_WR);
262
229k
}
263
264
/// @class proto_fuzzer::MockServer
265
/// @brief Orchestrates a bounded sequence of mock HTTP exchanges: installs
266
/// socket callbacks on an easy handle, assigns a response script to each new
267
/// socket, and feeds queued chunks as libcurl reads them.
268
269
/// Construct an idle MockServer with no scripted responses or open peers.
270
/// DriveScenario() configures it from a Scenario before curl can open a socket;
271
/// the fixed policy decides whether that scenario's ApiPlan controls the loop.
272
/// @param drive_policy Multi interface loop selected for this server.
273
MockServer::MockServer(MultiDrivePolicy drive_policy)
274
294k
    : MockServerBase(drive_policy),
275
294k
      script_count_(0),
276
294k
      next_script_(0),
277
294k
      active_script_(nullptr),
278
294k
      preload_all_chunks_(false),
279
294k
      keep_connections_open_(false) {}
280
281
/// Default destructor; current and previous MockConnections clean up their
282
/// server-side descriptors only after curl has been removed from the multi.
283
294k
MockServer::~MockServer() = default;
284
285
/// Build the complete borrowed script table before entering curl. The primary
286
/// Connection always occupies slot zero for backwards compatibility; only
287
/// three subsequent pointers are retained so protobuf mutations cannot
288
/// allocate socketpairs or prolong redirects in proportion to repeated-field
289
/// size. The caller must keep the Scenario alive and unmodified until the
290
/// synchronous drive finishes. RunScenario provides that lifetime, making
291
/// borrowing safe while avoiding copies of response bytes before curl
292
/// requests the corresponding socket or chunk.
293
/// @param scenario Source of the primary and bounded follow-on scripts.
294
292k
void MockServer::SetScripts(const curl::fuzzer::proto::Scenario& scenario) {
295
292k
  ResetConnections();
296
292k
  script_count_ = 0;
297
292k
  next_script_ = 0;
298
299
373k
  const auto append_script = [this](const curl::fuzzer::proto::Connection& connection) {
300
373k
    ConnectionScript& script = scripts_[script_count_++];
301
373k
    script.connection = &connection;
302
373k
    script.raw_chunk_count = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, connection.on_readable_size());
303
373k
    const std::size_t frame_budget = scenario_limits::kMaxResponseChunks - script.raw_chunk_count;
304
373k
    script.frame_chunk_count = std::min<std::size_t>(frame_budget, connection.server_frames_size());
305
373k
    script.next_chunk = 0;
306
373k
  };
307
308
292k
  append_script(scenario.connection());
309
292k
  const std::size_t subsequent_count = std::min<std::size_t>(
310
292k
      scenario_limits::kMaxConnections - 1, static_cast<std::size_t>(scenario.subsequent_connections_size()));
311
373k
  for (std::size_t i = 0; i < subsequent_count; ++i) {
312
81.0k
    append_script(scenario.subsequent_connections(static_cast<int>(i)));
313
81.0k
  }
314
292k
}
315
316
/// Keep completed response sockets writable instead of half-closing them.
317
/// The multi-transfer lane uses this to let a queued easy handle reuse an
318
/// HTTP/1.1 connection; ordinary protocol drives retain close-on-completion.
319
/// @param keep_open Whether the peer should suppress its response-side FIN.
320
47.6k
void MockServer::SetKeepConnectionsOpen(bool keep_open) { keep_connections_open_ = keep_open; }
321
322
/// @return true if at least one on_readable chunk has not yet been sent.
323
2.66M
bool MockServer::has_more_chunks() const {
324
2.66M
  return active_script_ != nullptr && active_script_->next_chunk < active_script_->chunk_count();
325
2.66M
}
326
327
/// Construct the transport used for one HTTP exchange. HTTPS overrides this
328
/// factory with a TLS record layer while retaining the same bounded response
329
/// scripts and redirect lifetimes, without copying the HTTP state machine.
330
/// @return a new connection, whose ok() result is checked before use.
331
230k
std::unique_ptr<MockConnection> MockServer::CreateConnection() { return std::make_unique<MockConnection>(); }
332
333
/// Release all current and retired socketpairs before resetting transport-
334
/// specific state. Derived destructors call this while their borrowed state
335
/// is still alive. This is separate from SetScripts because a derived class
336
/// may need a stricter order than C++'s derived-member-before-base teardown.
337
426k
void MockServer::ResetConnections() {
338
426k
  active_script_ = nullptr;
339
426k
  connection_.reset();
340
426k
  previous_connections_.clear();
341
426k
}
342
343
/// Observe curl while its connection filters are still attached. Plain HTTP
344
/// has no transport-specific state to inspect; layered transports override
345
/// this instead of querying stale state after the multi has been dismantled.
346
/// @param easy Active easy handle, unused by the plaintext transport.
347
968k
void MockServer::ObserveActiveTransfer(CURL* /*easy*/) {}
348
349
/// Called by the OPENSOCKETFUNCTION trampoline in the base class. Creates the
350
/// MockConnection, writes initial_response into it, and returns the
351
/// client-side fd to hand to libcurl.
352
/// @param purpose Socket role requested by curl; ordinary stream mocks do not
353
///                need to distinguish outbound HTTP roles.
354
/// @param address Intended destination retained by curl; the socketpair is
355
///                already connected and therefore leaves it untouched.
356
/// @return the client-side fd to hand to libcurl, or CURL_SOCKET_BAD on
357
///         failure.
358
395k
curl_socket_t MockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
359
395k
  (void)purpose;
360
395k
  (void)address;
361
395k
  if (next_script_ >= script_count_) {
362
    // Refusing a fifth socket keeps redirect loops bounded even if curl's own
363
    // redirect limit is mutated upward or an authentication scheme retries.
364
42.0k
    return CURL_SOCKET_BAD;
365
42.0k
  }
366
367
353k
  if (connection_) {
368
    // libcurl owns the corresponding client fd, so retain the server half
369
    // until the easy handle leaves the multi instead of closing it at the
370
    // moment a redirect or retry opens its replacement.
371
72.5k
    previous_connections_.push_back(std::move(connection_));
372
72.5k
  }
373
374
353k
  active_script_ = &scripts_[next_script_++];
375
353k
  const curl::fuzzer::proto::Connection& script_connection = *active_script_->connection;
376
353k
  connection_ = CreateConnection();
377
353k
  if (!connection_ || !connection_->ok()) {
378
0
    connection_.reset();
379
0
    active_script_ = nullptr;
380
0
    return CURL_SOCKET_BAD;
381
0
  }
382
383
  // Target policies clamp/clear these values before execution. Saturating the
384
  // compatibility lane's raw uint32 avoids implementation-defined narrowing
385
  // while preserving its ability to request any representable socket size.
386
353k
  const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max());
387
353k
  const auto& backpressure = script_connection.backpressure();
388
353k
  const int recv_buf_bytes = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max));
389
353k
  connection_->ApplyBackpressure(recv_buf_bytes, static_cast<std::size_t>(backpressure.drain_limit()));
390
391
353k
  const std::string& initial_response = script_connection.initial_response();
392
353k
  if (!initial_response.empty()) {
393
159k
    if (!connection_->WriteAll(reinterpret_cast<const unsigned char*>(initial_response.data()),
394
159k
                               initial_response.size())) {
395
0
      connection_.reset();
396
0
      active_script_ = nullptr;
397
0
      return CURL_SOCKET_BAD;
398
0
    }
399
159k
  }
400
353k
  if (preload_all_chunks_) {
401
11.7k
    while (has_more_chunks()) {
402
9.55k
      (void)DeliverNextChunk();
403
9.55k
    }
404
2.19k
  }
405
353k
  if (active_script_->chunk_count() == 0 && !keep_connections_open_) {
406
180k
    connection_->ShutdownWrite();
407
180k
  }
408
353k
  return connection_->take_client_fd();
409
353k
}
410
411
/// Preload all bounded response bytes from inside OPENSOCKETFUNCTION, where
412
/// the mock still owns both socketpair ends, and half-close the peer when the
413
/// preload finishes. This avoids a helper thread and prevents
414
/// curl from waiting for the outer chunk-delivery loop. The total serialized
415
/// fuzz input is capped by libFuzzer's max_len. If a non-blocking preload fills
416
/// the local socket, curl observes only the successfully queued prefix and
417
/// the timeouts below bound the incomplete response. This drive has no
418
/// iteration budget of its own, so it reasserts those timeouts after scenario
419
/// setopts and clears the option that can disable them.
420
/// @param easy Configured easy handle whose open-socket callback targets this
421
///        mock.
422
/// @param scenario Bounded response script to preload before performing.
423
/// @param use_events Select curl's debug event-based easy entrypoint.
424
/// @return Transfer result reported by curl's selected easy entrypoint.
425
1.22k
CURLcode MockServer::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool use_events) {
426
1.22k
  SetScripts(scenario);
427
1.22k
  preload_all_chunks_ = true;
428
1.22k
  (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 50L);
429
1.22k
  (void)curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 50L);
430
  // CONNECT_ONLY makes curl's timeleft check report "no limit" for the whole
431
  // post-connect phase, so the timeouts above stop being enforced; value 2
432
  // additionally skips the connect-only shortcut and runs a complete transfer.
433
  // Clearing it is not a coverage loss: the multi-driven WebSocket lanes still
434
  // exercise CONNECT_ONLY under their own iteration budget. Zero rather than
435
  // one because CONNECT_ONLY=1 also disables the timeout and stays bounded only
436
  // through a curl-internal shortcut this harness should not depend on.
437
1.22k
  (void)curl_easy_setopt(easy, CURLOPT_CONNECT_ONLY, 0L);
438
1.22k
  const CURLcode result = use_events ? curl_easy_perform_ev(easy) : curl_easy_perform(easy);
439
1.22k
  preload_all_chunks_ = false;
440
1.22k
  return result;
441
1.22k
}
442
443
/// Establish only the HTTP transport, then pass bounded application bytes
444
/// through curl's public direct-I/O wrappers. Preloading makes receive
445
/// readiness deterministic without a helper thread; the socketpair peer stays
446
/// readable after ShutdownWrite and continues accepting the outgoing probe.
447
/// @param easy Configured easy handle whose transport this mock supplies.
448
/// @param scenario Source of the bounded response and direct-I/O probe bytes.
449
/// @return Results and byte counts from connect, send, and receive operations.
450
89
ConnectOnlyRunStats MockServer::DriveConnectOnlyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
451
89
  ConnectOnlyRunStats stats;
452
89
  SetScripts(scenario);
453
89
  preload_all_chunks_ = true;
454
89
  (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 50L);
455
89
  (void)curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 50L);
456
89
  (void)curl_easy_setopt(easy, CURLOPT_CONNECT_ONLY, 1L);
457
89
  stats.connect_result = curl_easy_perform(easy);
458
89
  preload_all_chunks_ = false;
459
89
  if (stats.connect_result != CURLE_OK || connection_ == nullptr) {
460
30
    return stats;
461
30
  }
462
463
59
  constexpr std::string_view kFallbackRequest = "GET / HTTP/1.0\r\n\r\n";
464
59
  const std::string& upload = scenario.upload().data();
465
59
  const std::string_view source = upload.empty() ? kFallbackRequest : std::string_view(upload);
466
59
  const std::string_view outgoing = source.substr(0, scenario_limits::kMaxApiStringBytes);
467
59
  stats.send_result = CURLE_OK;
468
114
  for (int iteration = 0; stats.sent_bytes < outgoing.size() && iteration < kMaxConnectOnlyIoIterations; ++iteration) {
469
59
    std::size_t sent = 0;
470
59
    stats.send_result =
471
59
        curl_easy_send(easy, outgoing.data() + stats.sent_bytes, outgoing.size() - stats.sent_bytes, &sent);
472
59
    stats.sent_bytes += sent;
473
59
    (void)connection_->DrainIncoming();
474
59
    if (stats.send_result != CURLE_OK && stats.send_result != CURLE_AGAIN) {
475
4
      break;
476
4
    }
477
59
  }
478
479
59
  std::array<unsigned char, kConnectOnlyBufferSize> incoming{};
480
221
  for (int iteration = 0; iteration < kMaxConnectOnlyIoIterations; ++iteration) {
481
220
    std::size_t received = 0;
482
220
    stats.recv_result = curl_easy_recv(easy, incoming.data(), incoming.size(), &received);
483
220
    stats.received_bytes += received;
484
220
    if ((stats.recv_result != CURLE_OK && stats.recv_result != CURLE_AGAIN) ||
485
216
        (stats.recv_result == CURLE_OK && received == 0)) {
486
58
      break;
487
58
    }
488
220
  }
489
59
  return stats;
490
89
}
491
492
/// Push the next queued chunk. Called by the drive loop when curl is ready
493
/// for more data.
494
/// @return true when a chunk was consumed from the script.
495
635k
bool MockServer::DeliverNextChunk() {
496
635k
  if (!connection_ || !has_more_chunks()) {
497
0
    return false;
498
0
  }
499
635k
  connection_->DrainIncoming();
500
635k
  const std::size_t chunk_index = active_script_->next_chunk++;
501
635k
  const auto& script_connection = *active_script_->connection;
502
635k
  if (chunk_index < active_script_->raw_chunk_count) {
503
501k
    const std::string& chunk = script_connection.on_readable(static_cast<int>(chunk_index));
504
501k
    if (!chunk.empty()) {
505
469k
      connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
506
469k
    }
507
501k
  } else {
508
    // HTTP scenarios historically accept structured WebSocket frames as raw
509
    // response bytes after every on_readable chunk. Serialize only the frame
510
    // curl is about to receive: follow-on scripts and capped suffix frames may
511
    // never be consumed, so eagerly materialising all of them wastes mutations.
512
134k
    const std::size_t frame_index = chunk_index - active_script_->raw_chunk_count;
513
134k
    const std::string chunk = SerializeWebSocketFrame(script_connection.server_frames(static_cast<int>(frame_index)));
514
134k
    if (!chunk.empty()) {
515
134k
      connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
516
134k
    }
517
134k
  }
518
635k
  if (!has_more_chunks() && !keep_connections_open_) {
519
147k
    connection_->ShutdownWrite();
520
147k
  }
521
635k
  return true;
522
635k
}
523
524
/// Drain every live mock peer. Previous connections are normally quiescent,
525
/// but curl can finish sending a request body or close one after it has begun
526
/// resolving/opening the redirect target. Servicing both sides makes that
527
/// overlap deterministic without conflating their response scripts.
528
/// @return total bytes drained during this call.
529
1.38M
std::size_t MockServer::DrainIncomingConnections() {
530
1.38M
  std::size_t drained = 0;
531
1.38M
  for (const auto& previous : previous_connections_) {
532
565k
    drained += previous->DrainIncoming();
533
565k
  }
534
1.38M
  if (connection_) {
535
1.37M
    drained += connection_->DrainIncoming();
536
1.37M
  }
537
1.38M
  return drained;
538
1.38M
}
539
540
/// Service one deterministic application event-loop turn by draining all
541
/// available request bytes and releasing at most one response chunk. The
542
/// shared-multi driver owns the outer loop and calls this directly; ordinary
543
/// perform and socket-action drives use the same peer servicing. Draining
544
/// first creates request-side space before a response can provoke another
545
/// write, and releasing only one chunk preserves the scenario's mutation-
546
/// controlled parser boundaries.
547
/// @return true when any request or response byte advanced.
548
1.38M
bool MockServer::ServiceConnections() {
549
1.38M
  bool made_progress = DrainIncomingConnections() != 0;
550
1.38M
  if (has_more_chunks()) {
551
626k
    made_progress = DeliverNextChunk() || made_progress;
552
626k
  }
553
1.38M
  return made_progress;
554
1.38M
}
555
556
/// Report how many peer sockets curl opened during the current script run.
557
/// @return Number of response scripts assigned to live or retired sockets.
558
22.6k
std::size_t MockServer::opened_connection_count() const { return next_script_; }
559
560
/// Run a zero-wait application event loop around curl_multi_socket_action
561
/// using callback state owned by MockServerBase::DriveScenario. Positive
562
/// timers are deliberately not slept: the API lane clears timing controls
563
/// and covers event-driven dispatch, while the timing lane remains
564
/// responsible for clock-dependent behavior. Return after curl finishes or
565
/// a deterministic idle/operation cap wins.
566
/// @param multi Multi handle containing `easy`.
567
/// @param easy The easy handle attached to the mock.
568
504
void MockServer::RunSocketActionLoop(CURLM* multi, CURL* easy) {
569
504
  MultiSocketDriver* driver = multi_socket_driver();
570
504
  if (driver == nullptr) {
571
0
    return;
572
0
  }
573
574
504
  int still_running = 1;
575
504
  CURLMcode rc = driver->Start(&still_running);
576
504
  ObserveActiveTransfer(easy);
577
504
  ResumeResponseIfRequested(easy);
578
504
  int idle_iterations = 0;
579
504
  int drive_iterations = 0;
580
19.1k
  while (rc == CURLM_OK && still_running && idle_iterations < kMaxIdleIterations &&
581
18.6k
         drive_iterations++ < kMaxDriveIterations) {
582
18.6k
    bool made_progress = ServiceConnections();
583
18.6k
    const MultiSocketDriver::DriveResult drive_result = driver->DriveReady(&still_running);
584
18.6k
    rc = drive_result.code;
585
18.6k
    ObserveActiveTransfer(easy);
586
18.6k
    ResumeResponseIfRequested(easy);
587
18.6k
    made_progress = drive_result.made_progress || made_progress;
588
18.6k
    if (made_progress) {
589
18.4k
      idle_iterations = 0;
590
18.4k
    } else {
591
152
      ++idle_iterations;
592
152
    }
593
18.6k
  }
594
504
}
595
596
/// Seed the mock from the scenario, then drive the perform loop until curl is
597
/// done or a deterministic operation/idle budget is hit. Ordinary scenarios
598
/// never wait; explicit backpressure scenarios may use short select() waits.
599
/// @param multi    caller-owned multi; 'easy' is already added.
600
/// @param easy     the curl easy handle attached to this mock.
601
/// @param scenario source of the initial_response and on_readable chunks.
602
249k
void MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
603
249k
  SetScripts(scenario);
604
605
249k
  if (multi_socket_driver() != nullptr) {
606
504
    RunSocketActionLoop(multi, easy);
607
504
    return;
608
504
  }
609
610
248k
  int still_running = 1;
611
248k
  int idle_iterations = 0;
612
248k
  int drive_iterations = 0;
613
248k
  bool pollset_probed = false;
614
  // Each newly opened socket can carry its own pressure settings. Inspect only
615
  // the borrowed, runtime-bounded scripts: ignored repeated protobuf entries must
616
  // not opt a fast transfer into timed waits.
617
248k
  const bool timed_drive =
618
308k
      std::any_of(scripts_.begin(), scripts_.begin() + script_count_, [](const ConnectionScript& script) {
619
308k
        const auto& backpressure = script.connection->backpressure();
620
308k
        return backpressure.recv_buf_bytes() != 0 || backpressure.drain_limit() != 0;
621
308k
      });
622
248k
  const int idle_limit = timed_drive ? kMaxTimedIdleIterations : kMaxIdleIterations;
623
248k
  CURLMcode rc = CURLM_OK;
624
625
1.44M
  while (still_running && idle_iterations < idle_limit && drive_iterations++ < kMaxDriveIterations) {
626
1.42M
    bool made_progress = false;
627
1.42M
    const int running_before = still_running;
628
1.42M
    rc = curl_multi_perform(multi, &still_running);
629
1.42M
    if (rc != CURLM_OK) {
630
0
      break;
631
0
    }
632
1.42M
    ObserveActiveTransfer(easy);
633
1.42M
    ResumeResponseIfRequested(easy);
634
1.42M
    made_progress = still_running != running_before;
635
1.42M
    if (timed_drive && still_running && !pollset_probed) {
636
      // Probe only after curl has built the socket/filter chain. A zero-timeout
637
      // poll adds no wall-clock wait; restricting it to the timing lane keeps
638
      // the fixed HTTP target free of a per-input polling syscall.
639
7.50k
      ProbeMultiPollset(multi);
640
7.50k
      pollset_probed = true;
641
7.50k
    }
642
1.42M
    if (!still_running) {
643
228k
      break;
644
228k
    }
645
646
    // Always drain whatever curl has written. Under backpressure the kernel
647
    // recv buffer would otherwise stay full — curl short-writes, the mock
648
    // never consumes, and the transfer wedges until the drive budget. With
649
    // drain_limit set this still honours the per-tick byte budget.
650
1.19M
    made_progress = ServiceConnections() || made_progress;
651
652
1.19M
    if (made_progress) {
653
907k
      idle_iterations = 0;
654
907k
      continue;
655
907k
    }
656
657
    // Ordinary socketpair scenarios never sleep: repeated no-progress
658
    // performs are enough to settle curl's local state machine. Only an
659
    // explicit BackpressureConfig opts into short waits so timeout-related
660
    // behaviour remains fuzzable without taxing every corpus entry.
661
291k
    if (timed_drive) {
662
70.0k
      (void)WaitOnMultiFdset(multi, &rc);
663
70.0k
      if (rc != CURLM_OK) {
664
0
        break;
665
0
      }
666
70.0k
    }
667
291k
    ++idle_iterations;
668
291k
  }
669
248k
}
670
671
}  // namespace proto_fuzzer