Coverage Report

Created: 2026-04-28 07:09

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
5.94k
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
2.23k
std::vector<std::string> BuildChunkList(const curl::fuzzer::proto::Connection& conn) {
44
2.23k
  std::vector<std::string> chunks;
45
2.23k
  chunks.reserve(kMaxResponseChunks);
46
2.23k
  const std::size_t raw_budget = std::min<std::size_t>(kMaxResponseChunks, conn.on_readable_size());
47
4.11k
  for (std::size_t i = 0; i < raw_budget; ++i) {
48
1.88k
    chunks.emplace_back(conn.on_readable(i));
49
1.88k
  }
50
2.23k
  const std::size_t frame_budget = kMaxResponseChunks - chunks.size();
51
2.23k
  const std::size_t frame_count = std::min<std::size_t>(frame_budget, conn.server_frames_size());
52
3.41k
  for (std::size_t i = 0; i < frame_count; ++i) {
53
1.18k
    chunks.emplace_back(SerializeWebSocketFrame(conn.server_frames(static_cast<int>(i))));
54
1.18k
  }
55
2.23k
  return chunks;
56
2.23k
}
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
2.97k
MockConnection::MockConnection() : server_fd_(-1), client_fd_(-1), drain_limit_(0) {
67
2.97k
  int fds[2];
68
69
2.97k
  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
2.97k
  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
2.97k
  int flags = fcntl(fds[0], F_GETFL, 0);
82
2.97k
  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
2.97k
  server_fd_ = fds[0];
90
2.97k
  client_fd_ = fds[1];
91
2.97k
}
92
93
/// Close the server-side fd (and the client-side fd if it was never handed off via take_client_fd()).
94
2.97k
MockConnection::~MockConnection() {
95
2.97k
  if (server_fd_ >= 0) {
96
2.97k
    close(server_fd_);
97
2.97k
  }
98
2.97k
  if (client_fd_ >= 0) {
99
0
    close(client_fd_);
100
0
  }
101
2.97k
}
102
103
/// @return true if the underlying socketpair was set up successfully.
104
2.97k
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
2.97k
curl_socket_t MockConnection::take_client_fd() {
113
2.97k
  int fd = client_fd_;
114
2.97k
  client_fd_ = -1;
115
2.97k
  return static_cast<curl_socket_t>(fd);
116
2.97k
}
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
5.67k
bool MockConnection::WriteAll(const unsigned char* data, std::size_t size) {
124
5.67k
  if (server_fd_ < 0) {
125
0
    return false;
126
0
  }
127
5.67k
  std::size_t written = 0;
128
11.3k
  while (written < size) {
129
5.67k
    ssize_t n = ::write(server_fd_, data + written, size - written);
130
5.67k
    if (n <= 0) {
131
0
      return false;
132
0
    }
133
5.67k
    written += static_cast<std::size_t>(n);
134
5.67k
  }
135
5.67k
  return true;
136
5.67k
}
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
6.55k
void MockConnection::DrainIncoming() {
143
6.55k
  if (server_fd_ < 0) {
144
0
    return;
145
0
  }
146
6.55k
  unsigned char scratch[4096];
147
6.55k
  std::size_t drained = 0;
148
8.12k
  while (drain_limit_ == 0 || drained < drain_limit_) {
149
8.01k
    std::size_t want = sizeof(scratch);
150
8.01k
    if (drain_limit_ != 0) {
151
207
      const std::size_t remaining = drain_limit_ - drained;
152
207
      if (remaining < want) {
153
207
        want = remaining;
154
207
      }
155
207
    }
156
8.01k
    ssize_t n = ::read(server_fd_, scratch, want);
157
8.01k
    if (n <= 0) {
158
6.45k
      break;
159
6.45k
    }
160
1.56k
    drained += static_cast<std::size_t>(n);
161
1.56k
  }
162
6.55k
}
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
2.97k
void MockConnection::ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit) {
172
2.97k
  if (recv_buf_bytes > 0) {
173
4
    if (server_fd_ >= 0) {
174
4
      (void)setsockopt(server_fd_, SOL_SOCKET, SO_RCVBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
175
4
    }
176
4
    if (client_fd_ >= 0) {
177
4
      (void)setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &recv_buf_bytes, sizeof(recv_buf_bytes));
178
4
    }
179
4
  }
180
2.97k
  drain_limit_ = drain_limit;
181
2.97k
}
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
3.74k
void MockConnection::ReadAvailable(std::string* out) {
188
3.74k
  if (server_fd_ < 0 || out == nullptr) {
189
0
    return;
190
0
  }
191
3.74k
  unsigned char scratch[4096];
192
4.61k
  while (true) {
193
4.61k
    ssize_t n = ::read(server_fd_, scratch, sizeof(scratch));
194
4.61k
    if (n <= 0) {
195
3.74k
      break;
196
3.74k
    }
197
868
    out->append(reinterpret_cast<const char*>(scratch), static_cast<std::size_t>(n));
198
868
  }
199
3.74k
}
200
201
/// Signal end-of-response to libcurl by half-closing the write side.
202
1.79k
void MockConnection::ShutdownWrite() {
203
1.79k
  if (server_fd_ < 0) {
204
0
    return;
205
0
  }
206
1.79k
  ::shutdown(server_fd_, SHUT_WR);
207
1.79k
}
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
2.23k
MockServer::MockServer() : next_chunk_(0), initial_sent_(false) {}
216
217
/// Default destructor; the owned MockConnection (if any) cleans up its socketpair.
218
2.23k
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
2.23k
void MockServer::SetScript(std::string initial_response, std::vector<std::string> on_readable) {
226
2.23k
  initial_response_ = std::move(initial_response);
227
2.23k
  on_readable_ = std::move(on_readable);
228
2.23k
  next_chunk_ = 0;
229
2.23k
  initial_sent_ = false;
230
2.23k
}
231
232
/// @return true if at least one on_readable chunk has not yet been sent.
233
2.86k
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
2.34k
curl_socket_t MockServer::HandleOpenSocket() {
241
2.34k
  if (connection_) {
242
    // This mock supports exactly one connection per scenario.
243
280
    return CURL_SOCKET_BAD;
244
280
  }
245
2.06k
  connection_ = std::make_unique<MockConnection>();
246
2.06k
  if (!connection_->ok()) {
247
0
    connection_.reset();
248
0
    return CURL_SOCKET_BAD;
249
0
  }
250
2.06k
  ApplyPendingBackpressure();
251
2.06k
  if (!initial_response_.empty()) {
252
1.47k
    if (!connection_->WriteAll(reinterpret_cast<const unsigned char*>(initial_response_.data()),
253
1.47k
                               initial_response_.size())) {
254
0
      connection_.reset();
255
0
      return CURL_SOCKET_BAD;
256
0
    }
257
1.47k
  }
258
2.06k
  initial_sent_ = true;
259
2.06k
  if (on_readable_.empty()) {
260
880
    connection_->ShutdownWrite();
261
880
  }
262
2.06k
  return connection_->take_client_fd();
263
2.06k
}
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
2.16k
void MockServer::DeliverNextChunk() {
268
2.16k
  if (!connection_ || next_chunk_ >= on_readable_.size()) {
269
0
    return;
270
0
  }
271
2.16k
  connection_->DrainIncoming();
272
2.16k
  const std::string& chunk = on_readable_[next_chunk_++];
273
2.16k
  if (!chunk.empty()) {
274
2.04k
    connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size());
275
2.04k
  }
276
2.16k
  if (next_chunk_ >= on_readable_.size()) {
277
733
    connection_->ShutdownWrite();
278
733
  }
279
2.16k
}
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
2.23k
void MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
288
2.23k
  (void)easy;
289
2.23k
  const auto& conn = scenario.connection();
290
2.23k
  SetScript(conn.initial_response(), BuildChunkList(conn));
291
292
2.23k
  int still_running = 1;
293
2.23k
  int idle_iterations = 0;
294
2.23k
  CURLMcode rc = CURLM_OK;
295
296
5.09k
  while (still_running && idle_iterations < kMaxIdleIterations) {
297
5.09k
    rc = curl_multi_perform(multi, &still_running);
298
5.09k
    if (rc != CURLM_OK) {
299
0
      break;
300
0
    }
301
5.09k
    if (!still_running) {
302
2.23k
      break;
303
2.23k
    }
304
305
2.86k
    int ready = WaitOnMultiFdset(multi, &rc);
306
2.86k
    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
2.86k
    if (connection_) {
315
2.86k
      connection_->DrainIncoming();
316
2.86k
    }
317
2.86k
    if (has_more_chunks()) {
318
2.16k
      DeliverNextChunk();
319
2.16k
      idle_iterations = 0;
320
2.16k
    } else if (ready == 0) {
321
694
      ++idle_iterations;
322
694
    } else {
323
5
      idle_iterations = 0;
324
5
    }
325
2.86k
  }
326
2.23k
}
327
328
}  // namespace proto_fuzzer