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