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