/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 |