/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 <vector> |
25 | | |
26 | | #include "proto_fuzzer/scenario_limits.h" |
27 | | #include "proto_fuzzer/ws_frame.h" |
28 | | |
29 | | namespace proto_fuzzer { |
30 | | |
31 | | namespace { |
32 | | |
33 | | // fd_set can only represent file descriptors < FD_SETSIZE. Reject any pair that couldn't participate in select() |
34 | | // without memory corruption. |
35 | 19.3k | bool FdFitsInFdSet(int fd) { return fd >= 0 && fd < FD_SETSIZE; } |
36 | | |
37 | | } // namespace |
38 | | |
39 | | /// @class proto_fuzzer::MockConnection |
40 | | /// @brief Owns one half of a socketpair used to feed canned responses to libcurl. The destructor closes the server-side |
41 | | /// fd; the client-side fd is handed to libcurl via CURLOPT_OPENSOCKETFUNCTION and becomes curl's to close. |
42 | | |
43 | | /// Construct a non-blocking AF_UNIX/SOCK_STREAM socketpair. Both fds are validated to fit inside FD_SETSIZE; on any |
44 | | /// failure ok() returns false and the instance is unusable. |
45 | 9.66k | MockConnection::MockConnection() : server_fd_(-1), client_fd_(-1), drain_limit_(0) { |
46 | 9.66k | int fds[2]; |
47 | | |
48 | 9.66k | if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) { |
49 | 0 | return; |
50 | 0 | } |
51 | | |
52 | | // The fds must be small enough to fit in an fd_set for select(). If not, close them and fail the constructor. |
53 | 9.66k | if (!FdFitsInFdSet(fds[0]) || !FdFitsInFdSet(fds[1])) { |
54 | 0 | close(fds[0]); |
55 | 0 | close(fds[1]); |
56 | 0 | return; |
57 | 0 | } |
58 | | |
59 | | // Set the server-side fd non-blocking so we can write to it without risk of hanging the fuzzer. |
60 | 9.66k | int flags = fcntl(fds[0], F_GETFL, 0); |
61 | 9.66k | if (flags < 0 || fcntl(fds[0], F_SETFL, flags | O_NONBLOCK) < 0) { |
62 | 0 | close(fds[0]); |
63 | 0 | close(fds[1]); |
64 | 0 | return; |
65 | 0 | } |
66 | | |
67 | | // Success: store the file descriptors. |
68 | 9.66k | server_fd_ = fds[0]; |
69 | 9.66k | client_fd_ = fds[1]; |
70 | 9.66k | } |
71 | | |
72 | | /// Close the server-side fd (and the client-side fd if it was never handed off via take_client_fd()). |
73 | 9.66k | MockConnection::~MockConnection() { |
74 | 9.66k | if (server_fd_ >= 0) { |
75 | 9.66k | close(server_fd_); |
76 | 9.66k | } |
77 | 9.66k | if (client_fd_ >= 0) { |
78 | 0 | close(client_fd_); |
79 | 0 | } |
80 | 9.66k | } |
81 | | |
82 | | /// @return true if the underlying socketpair was set up successfully. |
83 | 9.66k | bool MockConnection::ok() const { return server_fd_ >= 0; } |
84 | | |
85 | | /// @return the server-side fd (still owned by this MockConnection). |
86 | 0 | int MockConnection::server_fd() const { return server_fd_; } |
87 | | |
88 | | /// Hand the client-side fd to libcurl. After this call the caller owns the fd and the MockConnection will not close it |
89 | | /// on destruction. |
90 | | /// @return the client-side socket fd as a curl_socket_t. |
91 | 9.66k | curl_socket_t MockConnection::take_client_fd() { |
92 | 9.66k | int fd = client_fd_; |
93 | 9.66k | client_fd_ = -1; |
94 | 9.66k | return static_cast<curl_socket_t>(fd); |
95 | 9.66k | } |
96 | | |
97 | | /// Write 'size' bytes from 'data' to the server fd, looping until the whole |
98 | | /// buffer is sent or a short/failed write occurs. MSG_NOSIGNAL is a harness |
99 | | /// invariant rather than a curl behavior choice: a response race may leave the |
100 | | /// peer closed, and that must look like an ordinary failed mock write instead |
101 | | /// of terminating the fuzz process with SIGPIPE. |
102 | | /// @param data Buffer to send. |
103 | | /// @param size Number of bytes in 'data'. |
104 | | /// @return false on short or failed write (treat the connection as lost). |
105 | 26.5k | bool MockConnection::WriteAll(const unsigned char* data, std::size_t size) { |
106 | 26.5k | if (server_fd_ < 0) { |
107 | 0 | return false; |
108 | 0 | } |
109 | 26.5k | std::size_t written = 0; |
110 | 53.1k | while (written < size) { |
111 | 26.5k | ssize_t n = ::send(server_fd_, data + written, size - written, MSG_NOSIGNAL); |
112 | 26.5k | if (n <= 0) { |
113 | 0 | return false; |
114 | 0 | } |
115 | 26.5k | written += static_cast<std::size_t>(n); |
116 | 26.5k | } |
117 | 26.5k | return true; |
118 | 26.5k | } |
119 | | |
120 | | /// Drain bytes curl has written. When a backpressure drain limit has been |
121 | | /// applied (see ApplyBackpressure), stops after drain_limit_ bytes so the |
122 | | /// kernel recv buffer stays near-full and curl keeps seeing short writes. |
123 | | /// Otherwise drains until read() returns 0/EAGAIN, matching legacy behaviour. |
124 | | /// @return number of bytes consumed during this call. |
125 | 66.0k | std::size_t MockConnection::DrainIncoming() { |
126 | 66.0k | if (server_fd_ < 0) { |
127 | 0 | return 0; |
128 | 0 | } |
129 | 66.0k | unsigned char scratch[4096]; |
130 | 66.0k | std::size_t drained = 0; |
131 | 88.6k | while (drain_limit_ == 0 || drained < drain_limit_) { |
132 | 77.3k | std::size_t want = sizeof(scratch); |
133 | 77.3k | if (drain_limit_ != 0) { |
134 | 21.4k | const std::size_t remaining = drain_limit_ - drained; |
135 | 21.4k | if (remaining < want) { |
136 | 19.1k | want = remaining; |
137 | 19.1k | } |
138 | 21.4k | } |
139 | 77.3k | ssize_t n = ::read(server_fd_, scratch, want); |
140 | 77.3k | if (n <= 0) { |
141 | 54.7k | break; |
142 | 54.7k | } |
143 | 22.6k | drained += static_cast<std::size_t>(n); |
144 | 22.6k | } |
145 | 66.0k | return drained; |
146 | 66.0k | } |
147 | | |
148 | | /// Tighten both halves of the socketpair buffer and/or cap DrainIncoming's |
149 | | /// per-call byte budget. SO_RCVBUF on the server fd caps how much curl can |
150 | | /// push into the pipe; SO_SNDBUF on the client fd (which curl will soon own |
151 | | /// but hasn't yet, so we can still tune it) caps how much curl's send() can |
152 | | /// buffer before short-writing. Linux socketpairs effectively use max(SNDBUF, |
153 | | /// RCVBUF*2) as pipe capacity, so we need both to see short writes reliably. |
154 | | /// See header docs. |
155 | 9.66k | void MockConnection::ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit) { |
156 | 9.66k | if (recv_buf_bytes > 0) { |
157 | 685 | if (server_fd_ >= 0) { |
158 | 685 | (void)setsockopt(server_fd_, SOL_SOCKET, SO_RCVBUF, &recv_buf_bytes, sizeof(recv_buf_bytes)); |
159 | 685 | } |
160 | 685 | if (client_fd_ >= 0) { |
161 | 685 | (void)setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &recv_buf_bytes, sizeof(recv_buf_bytes)); |
162 | 685 | } |
163 | 685 | } |
164 | 9.66k | drain_limit_ = drain_limit; |
165 | 9.66k | } |
166 | | |
167 | | /// Non-blocking read: append whatever bytes are currently available on the |
168 | | /// server fd to 'out'. Used by the WS handshake path to collect curl's HTTP |
169 | | /// Upgrade request without losing any bytes. |
170 | | /// @param out Destination buffer; unchanged if no bytes are pending. |
171 | 5.53k | void MockConnection::ReadAvailable(std::string* out) { |
172 | 5.53k | if (server_fd_ < 0 || out == nullptr) { |
173 | 0 | return; |
174 | 0 | } |
175 | 5.53k | unsigned char scratch[4096]; |
176 | 8.10k | while (true) { |
177 | 8.10k | ssize_t n = ::read(server_fd_, scratch, sizeof(scratch)); |
178 | 8.10k | if (n <= 0) { |
179 | 5.53k | break; |
180 | 5.53k | } |
181 | 2.57k | out->append(reinterpret_cast<const char*>(scratch), static_cast<std::size_t>(n)); |
182 | 2.57k | } |
183 | 5.53k | } |
184 | | |
185 | | /// Signal end-of-response to libcurl by half-closing the write side. |
186 | 7.71k | void MockConnection::ShutdownWrite() { |
187 | 7.71k | if (server_fd_ < 0) { |
188 | 0 | return; |
189 | 0 | } |
190 | 7.71k | ::shutdown(server_fd_, SHUT_WR); |
191 | 7.71k | } |
192 | | |
193 | | /// @class proto_fuzzer::MockServer |
194 | | /// @brief Orchestrates a bounded sequence of mock HTTP exchanges: installs |
195 | | /// socket callbacks on an easy handle, assigns a response script to each new |
196 | | /// socket, and feeds queued chunks as libcurl reads them. |
197 | | |
198 | | /// Construct an idle MockServer with no scripted responses or open peers. |
199 | | /// DriveScenario() configures it from a Scenario before curl can open a socket. |
200 | 7.93k | MockServer::MockServer() : script_count_(0), next_script_(0), active_script_(nullptr) {} |
201 | | |
202 | | /// Default destructor; current and previous MockConnections clean up their |
203 | | /// server-side descriptors only after curl has been removed from the multi. |
204 | 7.93k | MockServer::~MockServer() = default; |
205 | | |
206 | | /// Build the complete borrowed script table before entering curl. The primary |
207 | | /// Connection always occupies slot zero for backwards compatibility; only |
208 | | /// three subsequent pointers are retained so protobuf mutations cannot |
209 | | /// allocate socketpairs or prolong redirects in proportion to repeated-field |
210 | | /// size. The Scenario passed by ScenarioRunner outlives this synchronous drive, |
211 | | /// which makes borrowing safe while avoiding response-byte copies. |
212 | | /// @param scenario Source of the primary and bounded follow-on scripts. |
213 | 7.93k | void MockServer::SetScripts(const curl::fuzzer::proto::Scenario& scenario) { |
214 | 7.93k | script_count_ = 0; |
215 | 7.93k | next_script_ = 0; |
216 | 7.93k | active_script_ = nullptr; |
217 | 7.93k | connection_.reset(); |
218 | 7.93k | previous_connections_.clear(); |
219 | | |
220 | 7.93k | const auto append_script = [this](const curl::fuzzer::proto::Connection& connection) { |
221 | 7.93k | ConnectionScript& script = scripts_[script_count_++]; |
222 | 7.93k | script.connection = &connection; |
223 | 7.93k | script.raw_chunk_count = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, connection.on_readable_size()); |
224 | 7.93k | const std::size_t frame_budget = scenario_limits::kMaxResponseChunks - script.raw_chunk_count; |
225 | 7.93k | script.frame_chunk_count = std::min<std::size_t>(frame_budget, connection.server_frames_size()); |
226 | 7.93k | script.next_chunk = 0; |
227 | 7.93k | }; |
228 | | |
229 | 7.93k | append_script(scenario.connection()); |
230 | 7.93k | const std::size_t subsequent_count = std::min<std::size_t>( |
231 | 7.93k | scenario_limits::kMaxConnections - 1, static_cast<std::size_t>(scenario.subsequent_connections_size())); |
232 | 7.93k | for (std::size_t i = 0; i < subsequent_count; ++i) { |
233 | 0 | append_script(scenario.subsequent_connections(static_cast<int>(i))); |
234 | 0 | } |
235 | 7.93k | } |
236 | | |
237 | | /// @return true if at least one on_readable chunk has not yet been sent. |
238 | 65.3k | bool MockServer::has_more_chunks() const { |
239 | 65.3k | return active_script_ != nullptr && active_script_->next_chunk < active_script_->chunk_count(); |
240 | 65.3k | } |
241 | | |
242 | | /// Called by the OPENSOCKETFUNCTION trampoline in the base class. Creates the |
243 | | /// MockConnection, writes initial_response into it, and returns the |
244 | | /// client-side fd to hand to libcurl. |
245 | | /// @return the client-side fd to hand to libcurl, or CURL_SOCKET_BAD on |
246 | | /// failure. |
247 | 8.64k | curl_socket_t MockServer::HandleOpenSocket() { |
248 | 8.64k | if (next_script_ >= script_count_) { |
249 | | // Refusing a fifth socket keeps redirect loops bounded even if curl's own |
250 | | // redirect limit is mutated upward or an authentication scheme retries. |
251 | 912 | return CURL_SOCKET_BAD; |
252 | 912 | } |
253 | | |
254 | 7.72k | if (connection_) { |
255 | | // libcurl owns the corresponding client fd, so retain the server half |
256 | | // until the easy handle leaves the multi instead of closing it at the |
257 | | // moment a redirect or retry opens its replacement. |
258 | 0 | previous_connections_.push_back(std::move(connection_)); |
259 | 0 | } |
260 | | |
261 | 7.72k | active_script_ = &scripts_[next_script_++]; |
262 | 7.72k | const curl::fuzzer::proto::Connection& script_connection = *active_script_->connection; |
263 | 7.72k | connection_ = std::make_unique<MockConnection>(); |
264 | 7.72k | if (!connection_->ok()) { |
265 | 0 | connection_.reset(); |
266 | 0 | active_script_ = nullptr; |
267 | 0 | return CURL_SOCKET_BAD; |
268 | 0 | } |
269 | | |
270 | | // Target policies clamp/clear these values before execution. Saturating the |
271 | | // compatibility lane's raw uint32 avoids implementation-defined narrowing |
272 | | // while preserving its ability to request any representable socket size. |
273 | 7.72k | const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max()); |
274 | 7.72k | const auto& backpressure = script_connection.backpressure(); |
275 | 7.72k | const int recv_buf_bytes = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max)); |
276 | 7.72k | connection_->ApplyBackpressure(recv_buf_bytes, static_cast<std::size_t>(backpressure.drain_limit())); |
277 | | |
278 | 7.72k | const std::string& initial_response = script_connection.initial_response(); |
279 | 7.72k | if (!initial_response.empty()) { |
280 | 5.27k | if (!connection_->WriteAll(reinterpret_cast<const unsigned char*>(initial_response.data()), |
281 | 5.27k | initial_response.size())) { |
282 | 0 | connection_.reset(); |
283 | 0 | active_script_ = nullptr; |
284 | 0 | return CURL_SOCKET_BAD; |
285 | 0 | } |
286 | 5.27k | } |
287 | 7.72k | if (active_script_->chunk_count() == 0) { |
288 | 3.38k | connection_->ShutdownWrite(); |
289 | 3.38k | } |
290 | 7.72k | return connection_->take_client_fd(); |
291 | 7.72k | } |
292 | | |
293 | | /// Push the next queued chunk. Called by the drive loop when curl is ready |
294 | | /// for more data. |
295 | | /// @return true when a chunk was consumed from the script. |
296 | 16.7k | bool MockServer::DeliverNextChunk() { |
297 | 16.7k | if (!connection_ || !has_more_chunks()) { |
298 | 0 | return false; |
299 | 0 | } |
300 | 16.7k | connection_->DrainIncoming(); |
301 | 16.7k | const std::size_t chunk_index = active_script_->next_chunk++; |
302 | 16.7k | const auto& script_connection = *active_script_->connection; |
303 | 16.7k | if (chunk_index < active_script_->raw_chunk_count) { |
304 | 11.7k | const std::string& chunk = script_connection.on_readable(static_cast<int>(chunk_index)); |
305 | 11.7k | if (!chunk.empty()) { |
306 | 11.0k | connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
307 | 11.0k | } |
308 | 11.7k | } else { |
309 | | // HTTP scenarios historically accept structured WebSocket frames as raw |
310 | | // response bytes after every on_readable chunk. Serialize only the frame |
311 | | // curl is about to receive: follow-on scripts and capped suffix frames may |
312 | | // never be consumed, so eagerly materialising all of them wastes mutations. |
313 | 4.97k | const std::size_t frame_index = chunk_index - active_script_->raw_chunk_count; |
314 | 4.97k | const std::string chunk = SerializeWebSocketFrame(script_connection.server_frames(static_cast<int>(frame_index))); |
315 | 4.97k | if (!chunk.empty()) { |
316 | 4.97k | connection_->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
317 | 4.97k | } |
318 | 4.97k | } |
319 | 16.7k | if (!has_more_chunks()) { |
320 | 3.80k | connection_->ShutdownWrite(); |
321 | 3.80k | } |
322 | 16.7k | return true; |
323 | 16.7k | } |
324 | | |
325 | | /// Drain every live mock peer. Previous connections are normally quiescent, |
326 | | /// but curl can finish sending a request body or close one after it has begun |
327 | | /// resolving/opening the redirect target. Servicing both sides makes that |
328 | | /// overlap deterministic without conflating their response scripts. |
329 | 31.8k | std::size_t MockServer::DrainIncomingConnections() { |
330 | 31.8k | std::size_t drained = 0; |
331 | 31.8k | for (const auto& previous : previous_connections_) { |
332 | 0 | drained += previous->DrainIncoming(); |
333 | 0 | } |
334 | 31.8k | if (connection_) { |
335 | 31.8k | drained += connection_->DrainIncoming(); |
336 | 31.8k | } |
337 | 31.8k | return drained; |
338 | 31.8k | } |
339 | | |
340 | | /// Seed the mock from the scenario, then drive the perform loop until curl is |
341 | | /// done or a deterministic operation/idle budget is hit. Ordinary scenarios |
342 | | /// never wait; explicit backpressure scenarios may use short select() waits. |
343 | | /// @param multi caller-owned multi; 'easy' is already added. |
344 | | /// @param easy the curl easy handle attached to this mock. |
345 | | /// @param scenario source of the initial_response and on_readable chunks. |
346 | 7.93k | void MockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
347 | 7.93k | (void)easy; |
348 | 7.93k | SetScripts(scenario); |
349 | | |
350 | 7.93k | int still_running = 1; |
351 | 7.93k | int idle_iterations = 0; |
352 | 7.93k | int drive_iterations = 0; |
353 | 7.93k | bool pollset_probed = false; |
354 | | // Each newly opened socket can carry its own pressure settings. Inspect only |
355 | | // the borrowed, runtime-bounded scripts: ignored repeated protobuf entries must |
356 | | // not opt a fast transfer into timed waits. |
357 | 7.93k | const bool timed_drive = |
358 | 7.93k | std::any_of(scripts_.begin(), scripts_.begin() + script_count_, [](const ConnectionScript& script) { |
359 | 7.93k | const auto& backpressure = script.connection->backpressure(); |
360 | 7.93k | return backpressure.recv_buf_bytes() != 0 || backpressure.drain_limit() != 0; |
361 | 7.93k | }); |
362 | 7.93k | const int idle_limit = timed_drive ? kMaxTimedIdleIterations : kMaxIdleIterations; |
363 | 7.93k | CURLMcode rc = CURLM_OK; |
364 | | |
365 | 39.7k | while (still_running && idle_iterations < idle_limit && drive_iterations++ < kMaxDriveIterations) { |
366 | 39.7k | bool made_progress = false; |
367 | 39.7k | const int running_before = still_running; |
368 | 39.7k | rc = curl_multi_perform(multi, &still_running); |
369 | 39.7k | if (rc != CURLM_OK) { |
370 | 0 | break; |
371 | 0 | } |
372 | 39.7k | made_progress = still_running != running_before; |
373 | 39.7k | if (timed_drive && still_running && !pollset_probed) { |
374 | | // Probe only after curl has built the socket/filter chain. A zero-timeout |
375 | | // poll adds no wall-clock wait; restricting it to the timing lane keeps |
376 | | // the fixed HTTP target free of a per-input polling syscall. |
377 | 627 | ProbeMultiPollset(multi); |
378 | 627 | pollset_probed = true; |
379 | 627 | } |
380 | 39.7k | if (!still_running) { |
381 | 7.90k | break; |
382 | 7.90k | } |
383 | | |
384 | | // Always drain whatever curl has written. Under backpressure the kernel |
385 | | // recv buffer would otherwise stay full — curl short-writes, the mock |
386 | | // never consumes, and the transfer wedges until the drive budget. With |
387 | | // drain_limit set this still honours the per-tick byte budget. |
388 | 31.8k | made_progress = DrainIncomingConnections() != 0 || made_progress; |
389 | 31.8k | if (has_more_chunks()) { |
390 | 16.7k | made_progress = DeliverNextChunk() || made_progress; |
391 | 16.7k | } |
392 | | |
393 | 31.8k | if (made_progress) { |
394 | 24.3k | idle_iterations = 0; |
395 | 24.3k | continue; |
396 | 24.3k | } |
397 | | |
398 | | // Ordinary socketpair scenarios never sleep: repeated no-progress |
399 | | // performs are enough to settle curl's local state machine. Only an |
400 | | // explicit BackpressureConfig opts into short waits so timeout-related |
401 | | // behaviour remains fuzzable without taxing every corpus entry. |
402 | 7.53k | if (timed_drive) { |
403 | 7.47k | (void)WaitOnMultiFdset(multi, &rc); |
404 | 7.47k | if (rc != CURLM_OK) { |
405 | 0 | break; |
406 | 0 | } |
407 | 7.47k | } |
408 | 7.53k | ++idle_iterations; |
409 | 7.53k | } |
410 | 7.93k | } |
411 | | |
412 | | } // namespace proto_fuzzer |