Coverage Report

Created: 2026-07-16 06:34

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 <string>
22
#include <utility>
23
#include <vector>
24
25
#include "proto_fuzzer/ws_frame.h"
26
27
namespace proto_fuzzer {
28
29
namespace {
30
31
// fd_set can only represent file descriptors < FD_SETSIZE. Reject any pair that couldn't participate in select()
32
// without memory corruption.
33
17.6k
bool FdFitsInFdSet(int fd) { return fd >= 0 && fd < FD_SETSIZE; }
34
35
// Cap per-scenario response chunks so a mutator that creates thousands of
36
// tiny on_readable entries can't dominate runtime.
37
constexpr std::size_t kMaxResponseChunks = 16;
38
39
/// Combine the scenario's raw on_readable strings with any serialised
40
/// WebSocket frames into a single ordered chunk list, capped at
41
/// kMaxResponseChunks. Historical behaviour: HTTP scenarios can carry
42
/// server_frames too; the fuzzer just feeds those bytes to curl.
43
7.34k
std::vector<std::string> BuildChunkList(const curl::fuzzer::proto::Connection& conn) {
44
7.34k
  std::vector<std::string> chunks;
45
7.34k
  chunks.reserve(kMaxResponseChunks);
46
7.34k
  const std::size_t raw_budget = std::min<std::size_t>(kMaxResponseChunks, conn.on_readable_size());
47
18.5k
  for (std::size_t i = 0; i < raw_budget; ++i) {
48
11.2k
    chunks.emplace_back(conn.on_readable(i));
49
11.2k
  }
50
7.34k
  const std::size_t frame_budget = kMaxResponseChunks - chunks.size();
51
7.34k
  const std::size_t frame_count = std::min<std::size_t>(frame_budget, conn.server_frames_size());
52
12.6k
  for (std::size_t i = 0; i < frame_count; ++i) {
53
5.32k
    chunks.emplace_back(SerializeWebSocketFrame(conn.server_frames(static_cast<int>(i))));
54
5.32k
  }
55
7.34k
  return chunks;
56
7.34k
}
57
58
}  // namespace
59
60
/// @class proto_fuzzer::MockConnection
61
/// @brief Owns one half of a socketpair used to feed canned responses to libcurl. The destructor closes the server-side
62
/// fd; the client-side fd is handed to libcurl via CURLOPT_OPENSOCKETFUNCTION and becomes curl's to close.
63
64
/// Construct a non-blocking AF_UNIX/SOCK_STREAM socketpair. Both fds are validated to fit inside FD_SETSIZE; on any
65
/// failure ok() returns false and the instance is unusable.
66
8.81k
MockConnection::MockConnection() : server_fd_(-1), client_fd_(-1), drain_limit_(0) {
67
8.81k
  int fds[2];
68
69
8.81k
  if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) {
70
0
    return;
71
0
  }
72
73
  // The fds must be small enough to fit in an fd_set for select(). If not, close them and fail the constructor.
74
8.81k
  if (!FdFitsInFdSet(fds[0]) || !FdFitsInFdSet(fds[1])) {
75
0
    close(fds[0]);
76
0
    close(fds[1]);
77
0
    return;
78
0
  }
79
80
  // Set the server-side fd non-blocking so we can write to it without risk of hanging the fuzzer.
81
8.81k
  int flags = fcntl(fds[0], F_GETFL, 0);
82
8.81k
  if (flags < 0 || fcntl(fds[0], F_SETFL, flags | O_NONBLOCK) < 0) {
83
0
    close(fds[0]);
84
0
    close(fds[1]);
85
0
    return;
86
0
  }
87
88
  // Success: store the file descriptors.
89
8.81k
  server_fd_ = fds[0];
90
8.81k
  client_fd_ = fds[1];
91
8.81k
}
92
93
/// Close the server-side fd (and the client-side fd if it was never handed off via take_client_fd()).
94
8.81k
MockConnection::~MockConnection() {
95
8.81k
  if (server_fd_ >= 0) {
96
8.81k
    close(server_fd_);
97
8.81k
  }
98
8.81k
  if (client_fd_ >= 0) {
99
0
    close(client_fd_);
100
0
  }
101
8.81k
}
102
103
/// @return true if the underlying socketpair was set up successfully.
104
8.81k
bool MockConnection::ok() const { return server_fd_ >= 0; }
105
106
/// @return the server-side fd (still owned by this MockConnection).
107
0
int MockConnection::server_fd() const { return server_fd_; }
108
109
/// Hand the client-side fd to libcurl. After this call the caller owns the fd and the MockConnection will not close it
110
/// on destruction.
111
/// @return the client-side socket fd as a curl_socket_t.
112
8.81k
curl_socket_t MockConnection::take_client_fd() {
113
8.81k
  int fd = client_fd_;
114
8.81k
  client_fd_ = -1;
115
8.81k
  return static_cast<curl_socket_t>(fd);
116
8.81k
}
117
118
/// Write 'size' bytes from 'data' to the server fd, looping until the whole buffer is sent or a short/failed write
119
/// occurs.
120
/// @param data Buffer to send.
121
/// @param size Number of bytes in 'data'.
122
/// @return false on short or failed write (treat the connection as lost).
123
24.1k
bool MockConnection::WriteAll(const unsigned char* data, std::size_t size) {
124
24.1k
  if (server_fd_ < 0) {
125
0
    return false;
126
0
  }
127
24.1k
  std::size_t written = 0;
128
48.3k
  while (written < size) {
129
24.1k
    ssize_t n = ::write(server_fd_, data + written, size - written);
130
24.1k
    if (n <= 0) {
131
0
      return false;
132
0
    }
133
24.1k
    written += static_cast<std::size_t>(n);
134
24.1k
  }
135
24.1k
  return true;
136
24.1k
}
137
138
/// Drain bytes curl has written. When a backpressure drain limit has been
139
/// applied (see ApplyBackpressure), stops after drain_limit_ bytes so the
140
/// kernel recv buffer stays near-full and curl keeps seeing short writes.
141
/// Otherwise drains until read() returns 0/EAGAIN, matching legacy behaviour.
142
46.5k
void MockConnection::DrainIncoming() {
143
46.5k
  if (server_fd_ < 0) {
144
0
    return;
145
0
  }
146
46.5k
  unsigned char scratch[4096];
147
46.5k
  std::size_t drained = 0;
148
67.2k
  while (drain_limit_ == 0 || drained < drain_limit_) {
149
55.4k
    std::size_t want = sizeof(scratch);
150
55.4k
    if (drain_limit_ != 0) {
151
18.7k
      const std::size_t remaining = drain_limit_ - drained;
152
18.7k
      if (remaining < want) {
153
15.2k
        want = remaining;
154
15.2k
      }
155
18.7k
    }
156
55.4k
    ssize_t n = ::read(server_fd_, scratch, want);
157
55.4k
    if (n <= 0) {
158
34.7k
      break;
159
34.7k
    }
160
20.7k
    drained += static_cast<std::size_t>(n);
161
20.7k
  }
162
46.5k
}
163
164
/// Tighten both halves of the socketpair buffer and/or cap DrainIncoming's
165
/// per-call byte budget. SO_RCVBUF on the server fd caps how much curl can
166
/// push into the pipe; SO_SNDBUF on the client fd (which curl will soon own
167
/// but hasn't yet, so we can still tune it) caps how much curl's send() can
168
/// buffer before short-writing. Linux socketpairs effectively use max(SNDBUF,
169
/// RCVBUF*2) as pipe capacity, so we need both to see short writes reliably.
170
/// See header docs.
171
8.81k
void MockConnection::ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit) {
172
8.81k
  if (recv_buf_bytes > 0) {
173
764
    if (server_fd_ >= 0) {
174
764
      (void)setsockopt(server_fd_, SOL_SOCKET, SO_RCVBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
175
764
    }
176
764
    if (client_fd_ >= 0) {
177
764
      (void)setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
178
764
    }
179
764
  }
180
8.81k
  drain_limit_ = drain_limit;
181
8.81k
}
182
183
/// Non-blocking read: append whatever bytes are currently available on the
184
/// server fd to 'out'. Used by the WS handshake path to collect curl's HTTP
185
/// Upgrade request without losing any bytes.
186
/// @param out Destination buffer; unchanged if no bytes are pending.
187
7.70k
void MockConnection::ReadAvailable(std::string* out) {
188
7.70k
  if (server_fd_ < 0 || out == nullptr) {
189
0
    return;
190
0
  }
191
7.70k
  unsigned char scratch[4096];
192
9.71k
  while (true) {
193
9.71k
    ssize_t n = ::read(server_fd_, scratch, sizeof(scratch));
194
9.71k
    if (n <= 0) {
195
7.70k
      break;
196
7.70k
    }
197
2.01k
    out->append(reinterpret_cast<const char*>(scratch), static_cast<std::size_t>(n));
198
2.01k
  }
199
7.70k
}
200
201
/// Signal end-of-response to libcurl by half-closing the write side.
202
6.80k
void MockConnection::ShutdownWrite() {
203
6.80k
  if (server_fd_ < 0) {
204
0
    return;
205
0
  }
206
6.80k
  ::shutdown(server_fd_, SHUT_WR);
207
6.80k
}
208
209
/// @class proto_fuzzer::MockServer
210
/// @brief Orchestrates a single mock HTTP exchange: installs the socket callbacks on an easy handle, then feeds queued
211
/// responses as libcurl reads them.
212
213
/// Construct an idle MockServer with no scripted responses. Install() on the
214
/// base class and DriveScenario() configure it from a Scenario proto.
215
7.34k
MockServer::MockServer() : next_chunk_(0), initial_sent_(false) {}
216
217
/// Default destructor; the owned MockConnection (if any) cleans up its socketpair.
218
7.34k
MockServer::~MockServer() = default;
219
220
/// Queue bytes to emit. initial_response is written synchronously in the
221
/// OPENSOCKETFUNCTION callback (HandleOpenSocket); on_readable entries are
222
/// written one-at-a-time when libcurl makes the fd readable.
223
/// @param initial_response Bytes written immediately on connection open.
224
/// @param on_readable      Additional chunks delivered one per iteration.
225
7.34k
void MockServer::SetScript(std::string initial_response, std::vector<std::string> on_readable) {
226
7.34k
  initial_response_ = std::move(initial_response);
227
7.34k
  on_readable_ = std::move(on_readable);
228
7.34k
  next_chunk_ = 0;
229
7.34k
  initial_sent_ = false;
230
7.34k
}
231
232
/// @return true if at least one on_readable chunk has not yet been sent.
233
26.0k
bool MockServer::has_more_chunks() const { return next_chunk_ < on_readable_.size(); }
234
235
/// Called by the OPENSOCKETFUNCTION trampoline in the base class. Creates the
236
/// MockConnection, writes initial_response into it, and returns the
237
/// client-side fd to hand to libcurl.
238
/// @return the client-side fd to hand to libcurl, or CURL_SOCKET_BAD on
239
///         failure.
240
7.71k
curl_socket_t MockServer::HandleOpenSocket() {
241
7.71k
  if (connection_) {
242
    // This mock supports exactly one connection per scenario.
243
764
    return CURL_SOCKET_BAD;
244
764
  }
245
6.94k
  connection_ = std::make_unique<MockConnection>();
246
6.94k
  if (!connection_->ok()) {
247
0
    connection_.reset();
248
0
    return CURL_SOCKET_BAD;
249
0
  }
250
6.94k
  ApplyPendingBackpressure();
251
6.94k
  if (!initial_response_.empty()) {
252
4.70k
    if (!connection_->WriteAll(reinterpret_cast<const unsigned char*>(initial_response_.data()),
253
4.70k
                               initial_response_.size())) {
254
0
      connection_.reset();
255
0
      return CURL_SOCKET_BAD;
256
0
    }
257
4.70k
  }
258
6.94k
  initial_sent_ = true;
259
6.94k
  if (on_readable_.empty()) {
260
2.95k
    connection_->ShutdownWrite();
261
2.95k
  }
262
6.94k
  return connection_->take_client_fd();
263
6.94k
}
264
265
/// Push the next queued chunk. Called by the drive loop when curl is ready
266
/// for more data. No-op if the queue is empty or no connection is open.
267
14.9k
void MockServer::DeliverNextChunk() {
268
14.9k
  if (!connection_ || next_chunk_ >= on_readable_.size()) {
269
0
    return;
270
0
  }
271
14.9k
  connection_->DrainIncoming();
272
14.9k
  const std::string& chunk = on_readable_[next_chunk_++];
273
14.9k
  if (!chunk.empty()) {
274
14.0k
    connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
275
14.0k
  }
276
14.9k
  if (next_chunk_ >= on_readable_.size()) {
277
3.28k
    connection_->ShutdownWrite();
278
3.28k
  }
279
14.9k
}
280
281
/// Seed the mock from the scenario, then drive the perform loop until curl is
282
/// done or the idle-iteration cap is hit. Bounded by select() timeouts so a
283
/// misbehaving scenario cannot spin forever.
284
/// @param multi    caller-owned multi; 'easy' is already added.
285
/// @param easy     the curl easy handle attached to this mock.
286
/// @param scenario source of the initial_response and on_readable chunks.
287
7.34k
void MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
288
7.34k
  (void)easy;
289
7.34k
  const auto& conn = scenario.connection();
290
7.34k
  SetScript(conn.initial_response(), BuildChunkList(conn));
291
292
7.34k
  int still_running = 1;
293
7.34k
  int idle_iterations = 0;
294
7.34k
  CURLMcode rc = CURLM_OK;
295
296
33.4k
  while (still_running && idle_iterations < kMaxIdleIterations) {
297
33.4k
    rc = curl_multi_perform(multi, &still_running);
298
33.4k
    if (rc != CURLM_OK) {
299
0
      break;
300
0
    }
301
33.4k
    if (!still_running) {
302
7.33k
      break;
303
7.33k
    }
304
305
26.0k
    int ready = WaitOnMultiFdset(multi, &rc);
306
26.0k
    if (rc != CURLM_OK) {
307
0
      break;
308
0
    }
309
310
    // Always drain whatever curl has written. Under backpressure the kernel
311
    // recv buffer would otherwise stay full — curl short-writes, the mock
312
    // never consumes, and the transfer wedges until kMaxIdleIterations. With
313
    // drain_limit set this still honours the per-tick byte budget.
314
26.0k
    if (connection_) {
315
26.0k
      connection_->DrainIncoming();
316
26.0k
    }
317
26.0k
    if (has_more_chunks()) {
318
14.9k
      DeliverNextChunk();
319
14.9k
      idle_iterations = 0;
320
14.9k
    } else if (ready == 0) {
321
11.1k
      ++idle_iterations;
322
11.1k
    } else {
323
14
      idle_iterations = 0;
324
14
    }
325
26.0k
  }
326
7.34k
}
327
328
}  // namespace proto_fuzzer