/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 | | |
21 | | namespace proto_fuzzer { |
22 | | |
23 | | namespace { |
24 | | |
25 | | constexpr long kSelectTimeoutUs = 1000; // 1 ms; explicit timing cases only. |
26 | | |
27 | | } // namespace |
28 | | |
29 | | /// @brief C trampoline for CURLOPT_OPENSOCKETFUNCTION. Declared at namespace |
30 | | /// scope so it can be a friend of MockServerBase. |
31 | | /// @param clientp Pointer to the MockServerBase instance. |
32 | | /// @param purpose The socket role curl is asking the mock to provide. |
33 | | /// @param address Curl's mutable description of the intended destination. |
34 | | /// @return The client-side socket fd as a curl_socket_t. |
35 | 376k | curl_socket_t MockServerBaseOpenSocketTrampoline(void* clientp, curlsocktype purpose, struct curl_sockaddr* address) { |
36 | 376k | return static_cast<MockServerBase*>(clientp)->HandleOpenSocket(purpose, address); |
37 | 376k | } |
38 | | |
39 | | /// Translate explicit peer transport metadata into curl's sockopt callback |
40 | | /// contract. The descriptor is used only as an opaque identity; sandbox |
41 | | /// policy must not decide whether an otherwise valid in-process transport can |
42 | | /// run. |
43 | | /// @param clientp Pointer to the MockServerBase instance. |
44 | | /// @param curlfd Descriptor returned by the open-socket callback. |
45 | | /// @param purpose Socket role assigned by curl. |
46 | | /// @return CURL_SOCKOPT_ALREADY_CONNECTED for prepared stream peers, otherwise |
47 | | /// CURL_SOCKOPT_OK. |
48 | 341k | int MockServerBaseSockOptTrampoline(void* clientp, curl_socket_t curlfd, curlsocktype purpose) { |
49 | 341k | const auto disposition = static_cast<MockServerBase*>(clientp)->GetSocketSetupDisposition(curlfd, purpose); |
50 | 341k | return disposition == SocketSetupDisposition::kAlreadyConnected ? CURL_SOCKOPT_ALREADY_CONNECTED : CURL_SOCKOPT_OK; |
51 | 341k | } |
52 | | |
53 | | /// Default-construct an empty base instance with no connection. |
54 | | MockServerBase::MockServerBase() |
55 | 285k | : connection_(nullptr), |
56 | 285k | pending_recv_buf_bytes_(0), |
57 | 285k | pending_drain_limit_(0), |
58 | 285k | multi_socket_driver_(nullptr), |
59 | 285k | additional_handle_cleanup_count_(0), |
60 | 285k | resume_response_(false) {} |
61 | | |
62 | | /// Out-of-line destructor so MockConnection can stay forward-declared in the |
63 | | /// base header (its complete type is only needed where unique_ptr is |
64 | | /// instantiated for destruction). |
65 | 285k | MockServerBase::~MockServerBase() = default; |
66 | | |
67 | | /// @return the owned MockConnection, or nullptr if one has not been opened. |
68 | 936k | MockConnection* MockServerBase::connection() { return connection_.get(); } |
69 | | |
70 | | /// Install the common socket-callback trio. All subclasses share the same |
71 | | /// trampoline; dispatch to the subclass happens through HandleOpenSocket(). |
72 | 327k | void MockServerBase::Install(CURL* easy) { |
73 | 327k | curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, &MockServerBaseOpenSocketTrampoline); |
74 | 327k | curl_easy_setopt(easy, CURLOPT_OPENSOCKETDATA, this); |
75 | 327k | curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, &MockServerBaseSockOptTrampoline); |
76 | 327k | curl_easy_setopt(easy, CURLOPT_SOCKOPTDATA, this); |
77 | 327k | } |
78 | | |
79 | 318k | SocketSetupDisposition MockServerBase::GetSocketSetupDisposition(curl_socket_t /*curlfd*/, curlsocktype purpose) const { |
80 | | // curl itself accepted CURLSOCKTYPE_ACCEPT descriptors, so they must follow |
81 | | // its normal post-accept option path. Every IPCXN socket returned by the |
82 | | // ordinary stream mocks is one end of an already-connected socketpair. |
83 | 318k | return purpose == CURLSOCKTYPE_ACCEPT ? SocketSetupDisposition::kNeedsSetup |
84 | 318k | : SocketSetupDisposition::kAlreadyConnected; |
85 | 318k | } |
86 | | |
87 | | /// Ordinary event-driven mocks need no upload-callback hook; their RunLoop |
88 | | /// regains control after each perform and drains client traffic there. |
89 | 321k | void MockServerBase::ConfigureRequestData(ScenarioRequestData* /*request_data*/) {} |
90 | | |
91 | | /// Allocate a multi, attach 'easy', delegate to the subclass RunLoop, consume |
92 | | /// its completion message, and clean up. Harness setup failures return a |
93 | | /// stable sentinel; fuzzer callers may ignore it while unit tests can assert |
94 | | /// the protocol result without adding another callback or global. |
95 | | CURLcode MockServerBase::DriveScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool use_multi_socket, |
96 | 264k | bool wake_multi, bool resume_response) { |
97 | | // Cache backpressure knobs so HandleOpenSocket can apply them the moment |
98 | | // connection_ exists. Both default to 0, which matches the legacy "drain |
99 | | // greedily, kernel-default buffers" behaviour exactly. |
100 | 264k | const auto& bp = scenario.connection().backpressure(); |
101 | 264k | pending_recv_buf_bytes_ = static_cast<int>(bp.recv_buf_bytes()); |
102 | 264k | pending_drain_limit_ = static_cast<std::size_t>(bp.drain_limit()); |
103 | 264k | additional_handle_cleanup_count_ = 0; |
104 | 264k | resume_response_ = resume_response; |
105 | | |
106 | 264k | CurlMultiPtr multi(curl_multi_init()); |
107 | 264k | if (multi == nullptr) { |
108 | 0 | resume_response_ = false; |
109 | 0 | return CURLE_FAILED_INIT; |
110 | 0 | } |
111 | | |
112 | 264k | CURLcode transfer_result = CURLE_FAILED_INIT; |
113 | | |
114 | | // Callback data must survive both remove_handle and multi_cleanup, since |
115 | | // either may emit CURL_POLL_REMOVE. Keeping it in this outer scope provides |
116 | | // that lifetime without allocating per-watch state. |
117 | 264k | MultiSocketDriver socket_driver; |
118 | 264k | if (use_multi_socket && socket_driver.Install(multi.get())) { |
119 | 7.54k | multi_socket_driver_ = &socket_driver; |
120 | 7.54k | } |
121 | 264k | if (curl_multi_add_handle(multi.get(), easy) == CURLM_OK) { |
122 | 264k | if (wake_multi) { |
123 | 86 | if (multi_socket_driver_ != nullptr) { |
124 | 30 | multi_socket_driver_->ProbeControlApis(); |
125 | 56 | } else { |
126 | 56 | long timeout_ms = -1; |
127 | 56 | (void)curl_multi_timeout(multi.get(), &timeout_ms); |
128 | 56 | (void)curl_multi_wakeup(multi.get()); |
129 | 56 | } |
130 | 86 | } |
131 | 264k | RunLoop(multi.get(), easy, scenario); |
132 | | |
133 | | // Completion messages are the multi API's only durable record of the |
134 | | // transfer result. Consume them while all easy handles are still attached: |
135 | | // otherwise every scenario systematically skips curl_multi_info_read's |
136 | | // result path and removal discards the opportunity. An accepted HTTP/2 |
137 | | // push can contribute one additional completion message; only the caller's |
138 | | // parent determines DriveScenario's result. |
139 | 264k | int messages_remaining = 0; |
140 | 264k | CURLMsg* message = nullptr; |
141 | 487k | while ((message = curl_multi_info_read(multi.get(), &messages_remaining)) != nullptr) { |
142 | 223k | if (message->msg == CURLMSG_DONE && message->easy_handle == easy) { |
143 | 223k | transfer_result = message->data.result; |
144 | 223k | } |
145 | 223k | } |
146 | | |
147 | | // CURL_PUSH_OK transfers ownership of each automatically-added easy to |
148 | | // the application. Enumerate attached handles only after draining their |
149 | | // completion messages, then honor the public remove-before-cleanup |
150 | | // lifecycle. The original `easy` remains caller-owned. |
151 | 264k | CURL** handles = curl_multi_get_handles(multi.get()); |
152 | 264k | if (handles != nullptr) { |
153 | 528k | for (std::size_t index = 0; handles[index] != nullptr; ++index) { |
154 | 264k | CURL* handle = handles[index]; |
155 | 264k | if (handle != easy && curl_multi_remove_handle(multi.get(), handle) == CURLM_OK) { |
156 | 134 | curl_easy_cleanup(handle); |
157 | 134 | ++additional_handle_cleanup_count_; |
158 | 134 | } |
159 | 264k | } |
160 | 264k | curl_free(handles); |
161 | 264k | } |
162 | | |
163 | 264k | curl_multi_remove_handle(multi.get(), easy); |
164 | 264k | } |
165 | 264k | multi.reset(); |
166 | 264k | multi_socket_driver_ = nullptr; |
167 | 264k | resume_response_ = false; |
168 | 264k | return transfer_result; |
169 | 264k | } |
170 | | |
171 | | /// Preserve a safe fallback for protocol mocks that require an outer driver |
172 | | /// to make progress. The API policy currently forces HTTP, whose override can |
173 | | /// preload its bounded response and call curl_easy_perform without a thread. |
174 | 0 | void MockServerBase::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool /*use_events*/) { |
175 | 0 | DriveScenario(easy, scenario); |
176 | 0 | } |
177 | | |
178 | | ConnectOnlyRunStats MockServerBase::DriveConnectOnlyScenario(CURL* easy, |
179 | 0 | const curl::fuzzer::proto::Scenario& scenario) { |
180 | 0 | ConnectOnlyRunStats stats; |
181 | 0 | stats.connect_result = DriveScenario(easy, scenario); |
182 | 0 | return stats; |
183 | 0 | } |
184 | | |
185 | | /// Expose only the current callback state to protocol drive loops. Ownership |
186 | | /// remains in DriveScenario so no subclass can accidentally shorten it. |
187 | 218k | MultiSocketDriver* MockServerBase::multi_socket_driver() { return multi_socket_driver_; } |
188 | | |
189 | 0 | std::size_t MockServerBase::additional_handle_cleanup_count() const { return additional_handle_cleanup_count_; } |
190 | | |
191 | 1.16M | void MockServerBase::ResumeResponseIfRequested(CURL* easy) { |
192 | 1.16M | if (resume_response_) { |
193 | 1.58k | (void)curl_easy_pause(easy, CURLPAUSE_CONT); |
194 | 1.58k | } |
195 | 1.16M | } |
196 | | |
197 | | /// Hand the cached backpressure config to the connection. Safe to call when |
198 | | /// connection_ is null (no-op) or when both knobs are 0 (ApplyBackpressure |
199 | | /// itself is a no-op in that case). |
200 | 22.3k | void MockServerBase::ApplyPendingBackpressure() { |
201 | 22.3k | if (connection_) { |
202 | 22.3k | connection_->ApplyBackpressure(pending_recv_buf_bytes_, pending_drain_limit_); |
203 | 22.3k | } |
204 | 22.3k | } |
205 | | |
206 | | /// Treat a non-default BackpressureConfig as an explicit request for the |
207 | | /// slower, timed drive policy. Proto3 scalar defaults make this deterministic: |
208 | | /// a present-but-empty message remains on the ordinary fast path. |
209 | | /// @param scenario Scenario whose backpressure settings select the policy. |
210 | | /// @return true when the scenario explicitly opted into socket backpressure. |
211 | 29.8k | bool MockServerBase::UsesTimedDrive(const curl::fuzzer::proto::Scenario& scenario) { |
212 | 29.8k | const auto& bp = scenario.connection().backpressure(); |
213 | 29.8k | return bp.recv_buf_bytes() != 0 || bp.drain_limit() != 0; |
214 | 29.8k | } |
215 | | |
216 | | /// Wait on curl's fdset with a short timeout. Returns select()'s result; on |
217 | | /// error sets *rc to the corresponding CURLMcode. |
218 | 361k | int MockServerBase::WaitOnMultiFdset(CURLM* multi, CURLMcode* rc) { |
219 | 361k | fd_set readfds; |
220 | 361k | fd_set writefds; |
221 | 361k | fd_set excfds; |
222 | 361k | FD_ZERO(&readfds); |
223 | 361k | FD_ZERO(&writefds); |
224 | 361k | FD_ZERO(&excfds); |
225 | 361k | int maxfd = -1; |
226 | 361k | *rc = curl_multi_fdset(multi, &readfds, &writefds, &excfds, &maxfd); |
227 | 361k | if (*rc != CURLM_OK) { |
228 | 0 | return -1; |
229 | 0 | } |
230 | 361k | if (maxfd < 0) { |
231 | 0 | return 0; |
232 | 0 | } |
233 | 361k | struct timeval timeout; |
234 | 361k | timeout.tv_sec = 0; |
235 | 361k | timeout.tv_usec = kSelectTimeoutUs; |
236 | 361k | return ::select(maxfd + 1, &readfds, &writefds, &excfds, &timeout); |
237 | 361k | } |
238 | | |
239 | | /// Exercise curl_multi_poll's pollset/filter traversal once without sleeping. |
240 | | /// The result is deliberately ignored: this is an API/state probe, while the |
241 | | /// protocol-specific perform loop remains the authority on transfer progress. |
242 | 11.0k | void MockServerBase::ProbeMultiPollset(CURLM* multi) { |
243 | 11.0k | int numfds = 0; |
244 | 11.0k | (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds); |
245 | 11.0k | } |
246 | | |
247 | | } // namespace proto_fuzzer |