/src/curl_fuzzer/proto_fuzzer/mock_server_base.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 MockServerBase — shared trampolines, shared |
9 | | /// select() helper, DriveScenario multi-handle RAII, and the scheme |
10 | | /// classifier that produces the right subclass for a Scenario. |
11 | | |
12 | | #include "proto_fuzzer/mock_server_base.h" |
13 | | |
14 | | #include <curl/multi.h> |
15 | | #include <sys/select.h> |
16 | | |
17 | | #include "proto_fuzzer/curl_raii.h" |
18 | | #include "proto_fuzzer/mock_server.h" |
19 | | #include "proto_fuzzer/multi_socket_driver.h" |
20 | | #include "proto_fuzzer/scenario_limits.h" |
21 | | |
22 | | namespace proto_fuzzer { |
23 | | |
24 | | namespace { |
25 | | |
26 | | constexpr long kSelectTimeoutUs = 1000; // 1 ms; explicit timing cases only. |
27 | | |
28 | | /// Enumerate the attached handles despite the scenario's bounded allocation |
29 | | /// failures. The parent remains attached, so a null result means the array |
30 | | /// allocation failed and is safe to retry. One final attempt follows the |
31 | | /// largest possible run of injected failures. |
32 | | /// @param multi Live multi handle whose attached easies are required. |
33 | | /// @return Caller-owned, null-terminated handle array, or nullptr after all |
34 | | /// bounded attempts fail. |
35 | 322k | CURL** GetHandlesForCleanup(CURLM* multi) { |
36 | 322k | constexpr std::size_t kMaxAttempts = scenario_limits::kMaxAllocationFailures + 1; |
37 | 322k | for (std::size_t attempt = 0; attempt < kMaxAttempts; ++attempt) { |
38 | 322k | CURL** handles = curl_multi_get_handles(multi); |
39 | 322k | if (handles != nullptr) { |
40 | 322k | return handles; |
41 | 322k | } |
42 | 322k | } |
43 | 0 | return nullptr; |
44 | 322k | } |
45 | | |
46 | | } // namespace |
47 | | |
48 | | /// @brief C trampoline for CURLOPT_OPENSOCKETFUNCTION. Declared at namespace |
49 | | /// scope so it can be a friend of MockServerBase. |
50 | | /// @param clientp Pointer to the MockServerBase instance. |
51 | | /// @param purpose The socket role curl is asking the mock to provide. |
52 | | /// @param address Curl's mutable description of the intended destination. |
53 | | /// @return The client-side socket fd as a curl_socket_t. |
54 | 447k | curl_socket_t MockServerBaseOpenSocketTrampoline(void* clientp, curlsocktype purpose, struct curl_sockaddr* address) { |
55 | 447k | return static_cast<MockServerBase*>(clientp)->HandleOpenSocket(purpose, address); |
56 | 447k | } |
57 | | |
58 | | /// Translate explicit peer transport metadata into curl's sockopt callback |
59 | | /// contract. The descriptor is used only as an opaque identity; sandbox |
60 | | /// policy must not decide whether an otherwise valid in-process transport can |
61 | | /// run. |
62 | | /// @param clientp Pointer to the MockServerBase instance. |
63 | | /// @param curlfd Descriptor returned by the open-socket callback. |
64 | | /// @param purpose Socket role assigned by curl. |
65 | | /// @return CURL_SOCKOPT_ALREADY_CONNECTED for prepared stream peers, otherwise |
66 | | /// CURL_SOCKOPT_OK. |
67 | 404k | int MockServerBaseSockOptTrampoline(void* clientp, curl_socket_t curlfd, curlsocktype purpose) { |
68 | 404k | const auto disposition = static_cast<MockServerBase*>(clientp)->GetSocketSetupDisposition(curlfd, purpose); |
69 | 404k | return disposition == SocketSetupDisposition::kAlreadyConnected ? CURL_SOCKOPT_ALREADY_CONNECTED : CURL_SOCKOPT_OK; |
70 | 404k | } |
71 | | |
72 | | /// Construct an empty base instance with a fixed multi-drive policy. |
73 | | /// @param drive_policy Whether to perform, use socket actions, or honor the |
74 | | /// containing Scenario's ApiPlan. |
75 | | MockServerBase::MockServerBase(MultiDrivePolicy drive_policy) |
76 | 348k | : connection_(nullptr), |
77 | 348k | pending_recv_buf_bytes_(0), |
78 | 348k | pending_drain_limit_(0), |
79 | 348k | multi_socket_driver_(nullptr), |
80 | 348k | additional_handle_cleanup_count_(0), |
81 | 348k | resume_response_(false), |
82 | 348k | drive_policy_(drive_policy) {} |
83 | | |
84 | | /// Out-of-line destructor so MockConnection can stay forward-declared in the |
85 | | /// base header (its complete type is only needed where unique_ptr is |
86 | | /// instantiated for destruction). |
87 | 348k | MockServerBase::~MockServerBase() = default; |
88 | | |
89 | | /// @return the owned MockConnection, or nullptr if one has not been opened. |
90 | 1.59M | MockConnection* MockServerBase::connection() { return connection_.get(); } |
91 | | |
92 | | /// Install the common OPENSOCKETFUNCTION / OPENSOCKETDATA / SOCKOPTFUNCTION |
93 | | /// callbacks on 'easy'. All subclasses share the trampolines, which route |
94 | | /// back into this instance through HandleOpenSocket. Subclasses may override |
95 | | /// to layer additional protocol-specific setopts, such as a WRITEFUNCTION |
96 | | /// that exercises protocol APIs from inside a curl callback. |
97 | | /// @param easy The curl easy handle to configure. |
98 | 395k | void MockServerBase::Install(CURL* easy) { |
99 | 395k | curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, &MockServerBaseOpenSocketTrampoline); |
100 | 395k | curl_easy_setopt(easy, CURLOPT_OPENSOCKETDATA, this); |
101 | 395k | curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, &MockServerBaseSockOptTrampoline); |
102 | 395k | curl_easy_setopt(easy, CURLOPT_SOCKOPTDATA, this); |
103 | 395k | } |
104 | | |
105 | | /// Describe the socket returned by HandleOpenSocket without issuing native |
106 | | /// descriptor queries. Stream peers return connected socketpairs; accepted |
107 | | /// sockets and datagram peers require curl's ordinary setup. |
108 | | /// @param curlfd Descriptor returned by HandleOpenSocket. |
109 | | /// @param purpose Role curl assigned to the descriptor. |
110 | | /// @return Whether curl must perform its normal socket setup. |
111 | 385k | SocketSetupDisposition MockServerBase::GetSocketSetupDisposition(curl_socket_t /*curlfd*/, curlsocktype purpose) const { |
112 | | // curl itself accepted CURLSOCKTYPE_ACCEPT descriptors, so they must follow |
113 | | // its normal post-accept option path. Every IPCXN socket returned by the |
114 | | // ordinary stream mocks is one end of an already-connected socketpair. |
115 | 385k | return purpose == CURLSOCKTYPE_ACCEPT ? SocketSetupDisposition::kNeedsSetup |
116 | 385k | : SocketSetupDisposition::kAlreadyConnected; |
117 | 385k | } |
118 | | |
119 | | /// Bind protocol-specific work to the request-data callbacks after their |
120 | | /// per-scenario state has been constructed. Ordinary HTTP and WebSocket |
121 | | /// mocks regain control in their outer perform loops and drain client |
122 | | /// traffic there. Most mocks therefore need no hook; TELNET uses this |
123 | | /// upload-callback boundary to drain client replies while curl owns the |
124 | | /// thread. |
125 | | /// @param request_data Callback state that outlives the subsequent drive. |
126 | 387k | void MockServerBase::ConfigureRequestData(ScenarioRequestData* /*request_data*/) {} |
127 | | |
128 | | /// Run 'scenario' to completion on 'easy': allocate a multi, attach 'easy', |
129 | | /// delegate protocol-specific work to RunLoop, consume its completion |
130 | | /// message, and clean up. Read the result before removing the only easy |
131 | | /// handle so every protocol runner exercises the public multi-result path. |
132 | | /// Harness setup failures return a stable sentinel; fuzzer callers may |
133 | | /// ignore it while unit tests can assert the protocol result without adding |
134 | | /// another callback or global. |
135 | | /// @param easy curl easy handle already Install()ed on this mock. |
136 | | /// @param scenario the Scenario proto to drive. |
137 | | /// @return the completed transfer's CURLcode, or CURLE_FAILED_INIT when the |
138 | | /// bounded drive could not produce a completion message. |
139 | 323k | CURLcode MockServerBase::DriveScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
140 | 323k | const curl::fuzzer::proto::ApiPlan* api_plan = |
141 | 323k | drive_policy_ == MultiDrivePolicy::kFromApiPlan && scenario.has_api_plan() ? &scenario.api_plan() : nullptr; |
142 | 323k | const bool use_multi_socket = |
143 | 323k | drive_policy_ == MultiDrivePolicy::kSocketAction || |
144 | 318k | (api_plan != nullptr && api_plan->drive_mode() == curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET); |
145 | | |
146 | | // Cache backpressure knobs so HandleOpenSocket can apply them the moment |
147 | | // connection_ exists. Both default to 0, which matches the legacy "drain |
148 | | // greedily, kernel-default buffers" behaviour exactly. |
149 | 323k | const auto& bp = scenario.connection().backpressure(); |
150 | 323k | pending_recv_buf_bytes_ = static_cast<int>(bp.recv_buf_bytes()); |
151 | 323k | pending_drain_limit_ = static_cast<std::size_t>(bp.drain_limit()); |
152 | 323k | additional_handle_cleanup_count_ = 0; |
153 | 323k | resume_response_ = api_plan != nullptr && api_plan->pause_response_once(); |
154 | | |
155 | 323k | CurlMultiPtr multi(curl_multi_init()); |
156 | 323k | if (multi == nullptr) { |
157 | 871 | resume_response_ = false; |
158 | 871 | return CURLE_FAILED_INIT; |
159 | 871 | } |
160 | | |
161 | 322k | CURLcode transfer_result = CURLE_FAILED_INIT; |
162 | | |
163 | | // Callback data must survive both remove_handle and multi_cleanup, since |
164 | | // either may emit CURL_POLL_REMOVE. Keeping it in this outer scope provides |
165 | | // that lifetime without allocating per-watch state. |
166 | 322k | MultiSocketDriver socket_driver; |
167 | 322k | if (use_multi_socket && socket_driver.Install(multi.get())) { |
168 | 5.11k | multi_socket_driver_ = &socket_driver; |
169 | 5.11k | } |
170 | 322k | if (curl_multi_add_handle(multi.get(), easy) == CURLM_OK) { |
171 | 322k | if (multi_observer_) { |
172 | 4.45k | multi_observer_(multi.get()); |
173 | 4.45k | } |
174 | 322k | if (api_plan != nullptr && api_plan->wake_multi()) { |
175 | 54 | if (multi_socket_driver_ != nullptr) { |
176 | 20 | multi_socket_driver_->ProbeControlApis(); |
177 | 34 | } else { |
178 | 34 | long timeout_ms = -1; |
179 | 34 | (void)curl_multi_timeout(multi.get(), &timeout_ms); |
180 | 34 | (void)curl_multi_wakeup(multi.get()); |
181 | 34 | } |
182 | 54 | } |
183 | 322k | RunLoop(multi.get(), easy, scenario); |
184 | | |
185 | | // Completion messages are the multi API's only durable record of the |
186 | | // transfer result. Consume them while all easy handles are still attached: |
187 | | // otherwise every scenario systematically skips curl_multi_info_read's |
188 | | // result path and removal discards the opportunity. An accepted HTTP/2 |
189 | | // push can contribute one additional completion message; only the caller's |
190 | | // parent determines DriveScenario's result. |
191 | 322k | int messages_remaining = 0; |
192 | 322k | CURLMsg* message = nullptr; |
193 | 589k | while ((message = curl_multi_info_read(multi.get(), &messages_remaining)) != nullptr) { |
194 | 267k | if (message->msg == CURLMSG_DONE && message->easy_handle == easy) { |
195 | 266k | transfer_result = message->data.result; |
196 | 266k | } |
197 | 267k | } |
198 | | |
199 | | // CURL_PUSH_OK transfers ownership of each automatically-added easy to |
200 | | // the application. Enumerate attached handles only after draining their |
201 | | // completion messages, then honor the public remove-before-cleanup |
202 | | // lifecycle. The original `easy` remains caller-owned. |
203 | 322k | CURL** handles = GetHandlesForCleanup(multi.get()); |
204 | 322k | if (handles != nullptr) { |
205 | 646k | for (std::size_t index = 0; handles[index] != nullptr; ++index) { |
206 | 324k | CURL* handle = handles[index]; |
207 | 324k | if (handle != easy && curl_multi_remove_handle(multi.get(), handle) == CURLM_OK) { |
208 | 2.09k | curl_easy_cleanup(handle); |
209 | 2.09k | ++additional_handle_cleanup_count_; |
210 | 2.09k | } |
211 | 324k | } |
212 | 322k | curl_free(handles); |
213 | 322k | } |
214 | | |
215 | 322k | curl_multi_remove_handle(multi.get(), easy); |
216 | 322k | } |
217 | | // Socket-action callbacks borrow the stack driver above, so that path must |
218 | | // clean its multi before this function returns. Perform-based protocol mocks |
219 | | // may retain the detached multi when their fixed lifecycle requires it. |
220 | 322k | if (use_multi_socket) { |
221 | 5.11k | multi.reset(); |
222 | 317k | } else { |
223 | 317k | HandleDetachedMulti(std::move(multi)); |
224 | 317k | } |
225 | 322k | multi_socket_driver_ = nullptr; |
226 | 322k | resume_response_ = false; |
227 | 322k | return transfer_result; |
228 | 323k | } |
229 | | |
230 | | /// Dispose of or retain the multi after its easy handle has been removed. |
231 | | /// The default destroys it before DriveScenario returns. A protocol mock |
232 | | /// may retain it as member state when its fixed lifecycle requires easy |
233 | | /// cleanup to happen first. |
234 | | /// @param multi Detached multi handle and its live connection cache. |
235 | 281k | void MockServerBase::HandleDetachedMulti(CurlMultiPtr /*multi*/) {} |
236 | | |
237 | | /// Run through the public easy entrypoint when a protocol mock can preload |
238 | | /// all peer work before curl takes control. The base preserves the ordinary |
239 | | /// multi-drive fallback for protocols that require an outer driver to make |
240 | | /// progress. The API policy currently forces HTTP, whose override preloads |
241 | | /// its bounded response and calls curl_easy_perform without a thread. |
242 | | /// @param easy curl easy handle already Install()ed on this mock. |
243 | | /// @param scenario Scenario whose response the mock must prepare. |
244 | | /// @param use_events Select curl's debug event-based easy entrypoint when |
245 | | /// the concrete mock supports it. |
246 | | /// @return Transfer result reported by the selected easy or fallback drive. |
247 | | CURLcode MockServerBase::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, |
248 | 0 | bool /*use_events*/) { |
249 | 0 | return DriveScenario(easy, scenario); |
250 | 0 | } |
251 | | |
252 | | /// Establish a CONNECT_ONLY transport, then perform bounded direct I/O. |
253 | | /// HTTP overrides this; other protocol mocks retain a safe fallback. |
254 | | /// @param easy curl easy handle already Install()ed on this mock. |
255 | | /// @param scenario Scenario supplying response and direct-I/O bytes. |
256 | | /// @return Results and byte counts from connect, send, and receive probes. |
257 | | ConnectOnlyRunStats MockServerBase::DriveConnectOnlyScenario(CURL* easy, |
258 | 0 | const curl::fuzzer::proto::Scenario& scenario) { |
259 | 0 | ConnectOnlyRunStats stats; |
260 | 0 | stats.connect_result = DriveScenario(easy, scenario); |
261 | 0 | return stats; |
262 | 0 | } |
263 | | |
264 | | /// Expose the callback state installed for the current socket-action drive. |
265 | | /// HTTP's RunLoop uses this instead of reading the proto directly, so only |
266 | | /// the dedicated API binary can opt into lifecycle work and compatibility |
267 | | /// inputs containing the new field retain their old behavior. Ownership |
268 | | /// remains in DriveScenario so no subclass can shorten the driver's lifetime. |
269 | | /// @return active driver, or nullptr for the ordinary perform path. |
270 | 254k | MultiSocketDriver* MockServerBase::multi_socket_driver() { return multi_socket_driver_; } |
271 | | |
272 | | /// @return extra easy handles removed and cleaned after the latest drive. |
273 | | /// Server push is currently the only path that can add one behind the |
274 | | /// caller's back; exposing the count to subclasses keeps ownership in this |
275 | | /// multi-owning base while allowing focused lifecycle assertions. |
276 | 0 | std::size_t MockServerBase::additional_handle_cleanup_count() const { return additional_handle_cleanup_count_; } |
277 | | |
278 | | /// Resume receive-side callback output at one bounded drive boundary when |
279 | | /// the dedicated API plan requested it. Calling CONT before the callback |
280 | | /// pauses is harmless; repeating it ensures a later response chunk cannot |
281 | | /// leave the transfer suspended until timeout. |
282 | | /// @param easy Active easy handle whose receive callbacks may be paused. |
283 | 1.82M | void MockServerBase::ResumeResponseIfRequested(CURL* easy) { |
284 | 1.82M | if (resume_response_) { |
285 | 3.57k | (void)curl_easy_pause(easy, CURLPAUSE_CONT); |
286 | 3.57k | } |
287 | 1.82M | } |
288 | | |
289 | | /// Apply the BackpressureConfig cached by DriveScenario to the new |
290 | | /// connection_. Subclasses call this at the end of HandleOpenSocket, before |
291 | | /// returning the client fd, so SO_RCVBUF takes effect before traffic flows. |
292 | | /// Safe to call when connection_ is null or both knobs are zero: those cases |
293 | | /// are no-ops. |
294 | 27.9k | void MockServerBase::ApplyPendingBackpressure() { |
295 | 27.9k | if (connection_) { |
296 | 27.9k | connection_->ApplyBackpressure(pending_recv_buf_bytes_, pending_drain_limit_); |
297 | 27.9k | } |
298 | 27.9k | } |
299 | | |
300 | | /// Treat a non-default BackpressureConfig as an explicit request for the |
301 | | /// slower, timed drive policy. Proto3 scalar defaults make this deterministic: |
302 | | /// a present-but-empty message remains on the ordinary fast path. |
303 | | /// @param scenario Scenario whose backpressure settings select the policy. |
304 | | /// @return true when the scenario explicitly opted into socket backpressure. |
305 | 32.3k | bool MockServerBase::UsesTimedDrive(const curl::fuzzer::proto::Scenario& scenario) { |
306 | 32.3k | const auto& bp = scenario.connection().backpressure(); |
307 | 32.3k | return bp.recv_buf_bytes() != 0 || bp.drain_limit() != 0; |
308 | 32.3k | } |
309 | | |
310 | | /// Wait on curl's fdset with a short timeout. Drive loops call this only for |
311 | | /// scenarios that explicitly request backpressure/timing behaviour; ordinary |
312 | | /// scenarios run without wall-clock sleeps. |
313 | | /// @param multi The multi handle whose fdset to poll. |
314 | | /// @param rc Out parameter: set to the CURLMcode on error. |
315 | | /// @return select()'s result, or -1 on curl_multi_fdset failure. |
316 | 468k | int MockServerBase::WaitOnMultiFdset(CURLM* multi, CURLMcode* rc) { |
317 | 468k | fd_set readfds; |
318 | 468k | fd_set writefds; |
319 | 468k | fd_set excfds; |
320 | 468k | FD_ZERO(&readfds); |
321 | 468k | FD_ZERO(&writefds); |
322 | 468k | FD_ZERO(&excfds); |
323 | 468k | int maxfd = -1; |
324 | 468k | *rc = curl_multi_fdset(multi, &readfds, &writefds, &excfds, &maxfd); |
325 | 468k | if (*rc != CURLM_OK) { |
326 | 0 | return -1; |
327 | 0 | } |
328 | 468k | if (maxfd < 0) { |
329 | 0 | return 0; |
330 | 0 | } |
331 | 468k | struct timeval timeout; |
332 | 468k | timeout.tv_sec = 0; |
333 | 468k | timeout.tv_usec = kSelectTimeoutUs; |
334 | 468k | return ::select(maxfd + 1, &readfds, &writefds, &excfds, &timeout); |
335 | 468k | } |
336 | | |
337 | | /// Ask curl to construct and inspect its connection-filter pollset without |
338 | | /// waiting. A perform-only harness can complete local socketpair transfers |
339 | | /// while skipping the public multi-poll path used by event-driven |
340 | | /// applications. Timing scenarios make one zero-timeout probe to retain |
341 | | /// that coverage without taxing the fixed fast lanes. The result is ignored: |
342 | | /// the protocol-specific perform loop determines transfer progress. |
343 | | /// @param multi The active multi handle after at least one perform call. |
344 | 11.2k | void MockServerBase::ProbeMultiPollset(CURLM* multi) { |
345 | 11.2k | int numfds = 0; |
346 | 11.2k | (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds); |
347 | 11.2k | } |
348 | | |
349 | | } // namespace proto_fuzzer |