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/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
460k
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
230k
MockConnection::MockConnection() : server_fd_(-1), client_fd_(-1), drain_limit_(0), incoming_data_observer_(nullptr) {
58
230k
  int fds[2];
59
60
230k
  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
230k
  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
230k
  int flags = fcntl(fds[0], F_GETFL, 0);
73
230k
  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
230k
  server_fd_ = fds[0];
81
230k
  client_fd_ = fds[1];
82
230k
}
83
84
/// Close the server-side fd (and the client-side fd if it was never handed off via take_client_fd()).
85
230k
MockConnection::~MockConnection() {
86
230k
  if (server_fd_ >= 0) {
87
230k
    close(server_fd_);
88
230k
  }
89
230k
  if (client_fd_ >= 0) {
90
11
    close(client_fd_);
91
11
  }
92
230k
}
93
94
/// @return true if the underlying socketpair was set up successfully.
95
1.16M
bool MockConnection::ok() const { return server_fd_ >= 0; }
96
97
/// @return the server-side fd (still owned by this MockConnection).
98
119k
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
6.36k
std::size_t MockConnection::client_send_buffer_size() const {
107
6.36k
  if (client_fd_ < 0) {
108
0
    return 0;
109
0
  }
110
6.36k
  int size = 0;
111
6.36k
  socklen_t length = sizeof(size);
112
6.36k
  if (getsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &size, &length) != 0 || size <= 0) {
113
0
    return 0;
114
0
  }
115
6.36k
  return static_cast<std::size_t>(size);
116
6.36k
}
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
6.36k
bool MockConnection::EnsureClientSendBufferSize(std::size_t minimum) {
125
6.36k
  if (client_send_buffer_size() >= minimum) {
126
6.36k
    return true;
127
6.36k
  }
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
230k
curl_socket_t MockConnection::take_client_fd() {
142
230k
  int fd = client_fd_;
143
230k
  client_fd_ = -1;
144
230k
  return static_cast<curl_socket_t>(fd);
145
230k
}
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
391k
bool MockConnection::WriteAll(const unsigned char* data, std::size_t size) {
156
391k
  if (server_fd_ < 0) {
157
0
    return false;
158
0
  }
159
391k
  std::size_t written = 0;
160
778k
  while (written < size) {
161
391k
    ssize_t n = ::send(server_fd_, data + written, size - written, MSG_NOSIGNAL);
162
391k
    if (n <= 0) {
163
4.15k
      return false;
164
4.15k
    }
165
387k
    written += static_cast<std::size_t>(n);
166
387k
  }
167
387k
  return true;
168
391k
}
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
1.62M
std::size_t MockConnection::DrainIncoming() {
180
1.62M
  if (server_fd_ < 0) {
181
0
    return 0;
182
0
  }
183
1.62M
  unsigned char scratch[4096];
184
1.62M
  std::size_t drained = 0;
185
2.01M
  while (drain_limit_ == 0 || drained < drain_limit_) {
186
1.90M
    std::size_t want = sizeof(scratch);
187
1.90M
    if (drain_limit_ != 0) {
188
190k
      const std::size_t remaining = drain_limit_ - drained;
189
190k
      if (remaining < want) {
190
181k
        want = remaining;
191
181k
      }
192
190k
    }
193
1.90M
    ssize_t n = ::read(server_fd_, scratch, want);
194
1.90M
    if (n <= 0) {
195
1.50M
      break;
196
1.50M
    }
197
398k
    NotifyIncomingData(scratch, static_cast<std::size_t>(n));
198
398k
    drained += static_cast<std::size_t>(n);
199
398k
  }
200
1.62M
  return drained;
201
1.62M
}
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
101k
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
730k
void MockConnection::NotifyIncomingData(const unsigned char* data, std::size_t size) {
213
730k
  if (incoming_data_observer_ != nullptr) {
214
443k
    incoming_data_observer_->ObserveIncomingData(data, size);
215
443k
  }
216
730k
}
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
226k
void MockConnection::ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit) {
227
226k
  if (recv_buf_bytes > 0) {
228
9.68k
    if (server_fd_ >= 0) {
229
9.68k
      (void)setsockopt(server_fd_, SOL_SOCKET, SO_RCVBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
230
9.68k
    }
231
9.68k
    if (client_fd_ >= 0) {
232
9.68k
      (void)setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
233
9.68k
    }
234
9.68k
  }
235
226k
  drain_limit_ = drain_limit;
236
226k
}
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
15.8k
void MockConnection::ReadAvailable(std::string* out) {
243
15.8k
  if (server_fd_ < 0 || out == nullptr) {
244
0
    return;
245
0
  }
246
15.8k
  unsigned char scratch[4096];
247
27.1k
  while (true) {
248
27.1k
    ssize_t n = ::read(server_fd_, scratch, sizeof(scratch));
249
27.1k
    if (n <= 0) {
250
15.8k
      break;
251
15.8k
    }
252
11.2k
    out->append(reinterpret_cast<const char*>(scratch), static_cast<std::size_t>(n));
253
11.2k
  }
254
15.8k
}
255
256
/// Signal end-of-response to libcurl by half-closing the write side.
257
122k
void MockConnection::ShutdownWrite() {
258
122k
  if (server_fd_ < 0) {
259
0
    return;
260
0
  }
261
122k
  ::shutdown(server_fd_, SHUT_WR);
262
122k
}
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
165k
    : MockServerBase(drive_policy),
275
165k
      script_count_(0),
276
165k
      next_script_(0),
277
165k
      active_script_(nullptr),
278
165k
      preload_all_chunks_(false),
279
165k
      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
165k
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
164k
void MockServer::SetScripts(const curl::fuzzer::proto::Scenario& scenario) {
295
164k
  ResetConnections();
296
164k
  script_count_ = 0;
297
164k
  next_script_ = 0;
298
299
197k
  const auto append_script = [this](const curl::fuzzer::proto::Connection& connection) {
300
197k
    ConnectionScript& script = scripts_[script_count_++];
301
197k
    script.connection = &connection;
302
197k
    script.raw_chunk_count = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, connection.on_readable_size());
303
197k
    const std::size_t frame_budget = scenario_limits::kMaxResponseChunks - script.raw_chunk_count;
304
197k
    script.frame_chunk_count = std::min<std::size_t>(frame_budget, connection.server_frames_size());
305
197k
    script.next_chunk = 0;
306
197k
  };
307
308
164k
  append_script(scenario.connection());
309
164k
  const std::size_t subsequent_count = std::min<std::size_t>(
310
164k
      scenario_limits::kMaxConnections - 1, static_cast<std::size_t>(scenario.subsequent_connections_size()));
311
197k
  for (std::size_t i = 0; i < subsequent_count; ++i) {
312
32.6k
    append_script(scenario.subsequent_connections(static_cast<int>(i)));
313
32.6k
  }
314
164k
}
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
31.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
1.42M
bool MockServer::has_more_chunks() const {
324
1.42M
  return active_script_ != nullptr && active_script_->next_chunk < active_script_->chunk_count();
325
1.42M
}
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
106k
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
246k
void MockServer::ResetConnections() {
338
246k
  active_script_ = nullptr;
339
246k
  connection_.reset();
340
246k
  previous_connections_.clear();
341
246k
}
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
614k
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
188k
curl_socket_t MockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
359
188k
  (void)purpose;
360
188k
  (void)address;
361
188k
  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
16.0k
    return CURL_SOCKET_BAD;
365
16.0k
  }
366
367
172k
  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
18.5k
    previous_connections_.push_back(std::move(connection_));
372
18.5k
  }
373
374
172k
  active_script_ = &scripts_[next_script_++];
375
172k
  const curl::fuzzer::proto::Connection& script_connection = *active_script_->connection;
376
172k
  connection_ = CreateConnection();
377
172k
  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
172k
  const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max());
387
172k
  const auto& backpressure = script_connection.backpressure();
388
172k
  const int recv_buf_bytes = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max));
389
172k
  connection_->ApplyBackpressure(recv_buf_bytes, static_cast<std::size_t>(backpressure.drain_limit()));
390
391
172k
  const std::string& initial_response = script_connection.initial_response();
392
172k
  if (!initial_response.empty()) {
393
69.8k
    if (!connection_->WriteAll(reinterpret_cast<const unsigned char*>(initial_response.data()),
394
69.8k
                               initial_response.size())) {
395
0
      connection_.reset();
396
0
      active_script_ = nullptr;
397
0
      return CURL_SOCKET_BAD;
398
0
    }
399
69.8k
  }
400
172k
  if (preload_all_chunks_) {
401
9.44k
    while (has_more_chunks()) {
402
7.88k
      (void)DeliverNextChunk();
403
7.88k
    }
404
1.56k
  }
405
172k
  if (active_script_->chunk_count() == 0 && !keep_connections_open_) {
406
87.0k
    connection_->ShutdownWrite();
407
87.0k
  }
408
172k
  return connection_->take_client_fd();
409
172k
}
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
830
CURLcode MockServer::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool use_events) {
426
830
  SetScripts(scenario);
427
830
  preload_all_chunks_ = true;
428
830
  (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 50L);
429
830
  (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
830
  (void)curl_easy_setopt(easy, CURLOPT_CONNECT_ONLY, 0L);
438
830
  const CURLcode result = use_events ? curl_easy_perform_ev(easy) : curl_easy_perform(easy);
439
830
  preload_all_chunks_ = false;
440
830
  return result;
441
830
}
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
197
ConnectOnlyRunStats MockServer::DriveConnectOnlyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
451
197
  ConnectOnlyRunStats stats;
452
197
  SetScripts(scenario);
453
197
  preload_all_chunks_ = true;
454
197
  (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 50L);
455
197
  (void)curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 50L);
456
197
  (void)curl_easy_setopt(easy, CURLOPT_CONNECT_ONLY, 1L);
457
197
  stats.connect_result = curl_easy_perform(easy);
458
197
  preload_all_chunks_ = false;
459
197
  if (stats.connect_result != CURLE_OK || connection_ == nullptr) {
460
46
    return stats;
461
46
  }
462
463
151
  constexpr std::string_view kFallbackRequest = "GET / HTTP/1.0\r\n\r\n";
464
151
  const std::string& upload = scenario.upload().data();
465
151
  const std::string_view source = upload.empty() ? kFallbackRequest : std::string_view(upload);
466
151
  const std::string_view outgoing = source.substr(0, scenario_limits::kMaxApiStringBytes);
467
151
  stats.send_result = CURLE_OK;
468
269
  for (int iteration = 0; stats.sent_bytes < outgoing.size() && iteration < kMaxConnectOnlyIoIterations; ++iteration) {
469
151
    std::size_t sent = 0;
470
151
    stats.send_result =
471
151
        curl_easy_send(easy, outgoing.data() + stats.sent_bytes, outgoing.size() - stats.sent_bytes, &sent);
472
151
    stats.sent_bytes += sent;
473
151
    (void)connection_->DrainIncoming();
474
151
    if (stats.send_result != CURLE_OK && stats.send_result != CURLE_AGAIN) {
475
33
      break;
476
33
    }
477
151
  }
478
479
151
  std::array<unsigned char, kConnectOnlyBufferSize> incoming{};
480
327
  for (int iteration = 0; iteration < kMaxConnectOnlyIoIterations; ++iteration) {
481
326
    std::size_t received = 0;
482
326
    stats.recv_result = curl_easy_recv(easy, incoming.data(), incoming.size(), &received);
483
326
    stats.received_bytes += received;
484
326
    if ((stats.recv_result != CURLE_OK && stats.recv_result != CURLE_AGAIN) ||
485
247
        (stats.recv_result == CURLE_OK && received == 0)) {
486
150
      break;
487
150
    }
488
326
  }
489
151
  return stats;
490
197
}
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
296k
bool MockServer::DeliverNextChunk() {
496
296k
  if (!connection_ || !has_more_chunks()) {
497
0
    return false;
498
0
  }
499
296k
  connection_->DrainIncoming();
500
296k
  const std::size_t chunk_index = active_script_->next_chunk++;
501
296k
  const auto& script_connection = *active_script_->connection;
502
296k
  if (chunk_index < active_script_->raw_chunk_count) {
503
256k
    const std::string& chunk = script_connection.on_readable(static_cast<int>(chunk_index));
504
256k
    if (!chunk.empty()) {
505
239k
      connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
506
239k
    }
507
256k
  } 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
39.6k
    const std::size_t frame_index = chunk_index - active_script_->raw_chunk_count;
513
39.6k
    const std::string chunk = SerializeWebSocketFrame(script_connection.server_frames(static_cast<int>(frame_index)));
514
39.6k
    if (!chunk.empty()) {
515
39.6k
      connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
516
39.6k
    }
517
39.6k
  }
518
296k
  if (!has_more_chunks() && !keep_connections_open_) {
519
56.6k
    connection_->ShutdownWrite();
520
56.6k
  }
521
296k
  return true;
522
296k
}
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
819k
std::size_t MockServer::DrainIncomingConnections() {
530
819k
  std::size_t drained = 0;
531
819k
  for (const auto& previous : previous_connections_) {
532
190k
    drained += previous->DrainIncoming();
533
190k
  }
534
819k
  if (connection_) {
535
805k
    drained += connection_->DrainIncoming();
536
805k
  }
537
819k
  return drained;
538
819k
}
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
819k
bool MockServer::ServiceConnections() {
549
819k
  bool made_progress = DrainIncomingConnections() != 0;
550
819k
  if (has_more_chunks()) {
551
288k
    made_progress = DeliverNextChunk() || made_progress;
552
288k
  }
553
819k
  return made_progress;
554
819k
}
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
12.1k
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
596
void MockServer::RunSocketActionLoop(CURLM* multi, CURL* easy) {
569
596
  MultiSocketDriver* driver = multi_socket_driver();
570
596
  if (driver == nullptr) {
571
0
    return;
572
0
  }
573
574
596
  int still_running = 1;
575
596
  CURLMcode rc = driver->Start(&still_running);
576
596
  ObserveActiveTransfer(easy);
577
596
  ResumeResponseIfRequested(easy);
578
596
  int idle_iterations = 0;
579
596
  int drive_iterations = 0;
580
7.03k
  while (rc == CURLM_OK && still_running && idle_iterations < kMaxIdleIterations &&
581
6.44k
         drive_iterations++ < kMaxDriveIterations) {
582
6.44k
    bool made_progress = ServiceConnections();
583
6.44k
    const MultiSocketDriver::DriveResult drive_result = driver->DriveReady(&still_running);
584
6.44k
    rc = drive_result.code;
585
6.44k
    ObserveActiveTransfer(easy);
586
6.44k
    ResumeResponseIfRequested(easy);
587
6.44k
    made_progress = drive_result.made_progress || made_progress;
588
6.44k
    if (made_progress) {
589
6.26k
      idle_iterations = 0;
590
6.26k
    } else {
591
176
      ++idle_iterations;
592
176
    }
593
6.44k
  }
594
596
}
595
596
/// Ordinary HTTP and compatibility peers retain the historical perform-only
597
/// loop even if an append-only scenario field is present. Protocol subclasses
598
/// opt in only when their peer can service every repeated poll turn.
599
/// @return False for the generic mock server.
600
317
bool MockServer::SupportsRepeatedPollsetProbe() const { return false; }
601
602
/// Seed the mock from the scenario, then drive the perform loop until curl is
603
/// done or a deterministic operation/idle budget is hit. Ordinary scenarios
604
/// never wait; explicit backpressure scenarios may use short select() waits.
605
/// @param multi    caller-owned multi; 'easy' is already added.
606
/// @param easy     the curl easy handle attached to this mock.
607
/// @param scenario source of the initial_response and on_readable chunks.
608
135k
void MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
609
135k
  SetScripts(scenario);
610
611
135k
  if (multi_socket_driver() != nullptr) {
612
596
    RunSocketActionLoop(multi, easy);
613
596
    return;
614
596
  }
615
616
134k
  int still_running = 1;
617
134k
  int idle_iterations = 0;
618
134k
  int drive_iterations = 0;
619
134k
  bool pollset_probed = false;
620
  // Each newly opened socket can carry its own pressure settings. Inspect only
621
  // the borrowed, runtime-bounded scripts: ignored repeated protobuf entries must
622
  // not opt a fast transfer into timed waits.
623
134k
  const bool timed_drive =
624
151k
      std::any_of(scripts_.begin(), scripts_.begin() + script_count_, [](const ConnectionScript& script) {
625
151k
        const auto& backpressure = script.connection->backpressure();
626
151k
        return backpressure.recv_buf_bytes() != 0 || backpressure.drain_limit() != 0;
627
151k
      });
628
134k
  const int idle_limit = timed_drive ? kMaxTimedIdleIterations : kMaxIdleIterations;
629
134k
  const bool repeated_pollset_probe = scenario.probe_multi_pollset() && SupportsRepeatedPollsetProbe();
630
134k
  CURLMcode rc = CURLM_OK;
631
632
790k
  while (still_running && idle_iterations < idle_limit && drive_iterations++ < kMaxDriveIterations) {
633
776k
    bool made_progress = false;
634
776k
    const int running_before = still_running;
635
776k
    rc = curl_multi_perform(multi, &still_running);
636
776k
    if (rc != CURLM_OK) {
637
0
      break;
638
0
    }
639
776k
    ObserveActiveTransfer(easy);
640
776k
    ResumeResponseIfRequested(easy);
641
776k
    made_progress = still_running != running_before;
642
776k
    if (still_running && ((timed_drive && !pollset_probed) || repeated_pollset_probe)) {
643
      // Probe only after curl has built the socket/filter chain. Timing cases
644
      // need one zero-timeout poll; an explicit protocol opt-in probes each
645
      // bounded perform turn so filters installed after a SOCKS handshake
646
      // (notably HAProxy) are observed while active. Default fast paths remain
647
      // free of a per-input polling syscall.
648
23.4k
      ProbeMultiPollset(multi);
649
23.4k
      pollset_probed = true;
650
23.4k
    }
651
776k
    if (!still_running) {
652
120k
      break;
653
120k
    }
654
655
    // Always drain whatever curl has written. Under backpressure the kernel
656
    // recv buffer would otherwise stay full — curl short-writes, the mock
657
    // never consumes, and the transfer wedges until the drive budget. With
658
    // drain_limit set this still honours the per-tick byte budget.
659
655k
    made_progress = ServiceConnections() || made_progress;
660
661
655k
    if (made_progress) {
662
455k
      idle_iterations = 0;
663
455k
      continue;
664
455k
    }
665
666
    // Ordinary socketpair scenarios never sleep: repeated no-progress
667
    // performs are enough to settle curl's local state machine. Only an
668
    // explicit BackpressureConfig opts into short waits so timeout-related
669
    // behaviour remains fuzzable without taxing every corpus entry.
670
199k
    if (timed_drive) {
671
62.9k
      (void)WaitOnMultiFdset(multi, &rc);
672
62.9k
      if (rc != CURLM_OK) {
673
0
        break;
674
0
      }
675
62.9k
    }
676
199k
    ++idle_iterations;
677
199k
  }
678
134k
}
679
680
}  // namespace proto_fuzzer