/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 <sys/select.h> |
15 | | #include <sys/socket.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 | | /// Tell curl whether the peer supplied an already-connected socketpair or a |
28 | | /// real datagram socket that still needs protocol-owned setup. Returning the |
29 | | /// socketpair answer unconditionally made the old TFTP harness skip the wrong |
30 | | /// setup assumptions and eventually call IP operations on an AF_UNIX stream. |
31 | | /// Accepted sockets are already established by curl itself, where the callback |
32 | | /// contract treats any non-zero result as an error rather than as a shortcut. |
33 | 147k | int SockOptTrampoline(void* /*clientp*/, curl_socket_t curlfd, curlsocktype purpose) { |
34 | 147k | if (purpose == CURLSOCKTYPE_ACCEPT) { |
35 | 0 | return CURL_SOCKOPT_OK; |
36 | 0 | } |
37 | | |
38 | 147k | int socket_type = 0; |
39 | 147k | socklen_t socket_type_size = sizeof(socket_type); |
40 | 147k | if (getsockopt(curlfd, SOL_SOCKET, SO_TYPE, &socket_type, &socket_type_size) != 0) { |
41 | 0 | return CURL_SOCKOPT_ERROR; |
42 | 0 | } |
43 | 147k | return socket_type == SOCK_DGRAM ? CURL_SOCKOPT_OK : CURL_SOCKOPT_ALREADY_CONNECTED; |
44 | 147k | } |
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 | 154k | curl_socket_t MockServerBaseOpenSocketTrampoline(void* clientp, curlsocktype purpose, struct curl_sockaddr* address) { |
55 | 154k | return static_cast<MockServerBase*>(clientp)->HandleOpenSocket(purpose, address); |
56 | 154k | } |
57 | | |
58 | | /// Default-construct an empty base instance with no connection. |
59 | | MockServerBase::MockServerBase() |
60 | 136k | : connection_(nullptr), pending_recv_buf_bytes_(0), pending_drain_limit_(0), multi_socket_driver_(nullptr) {} |
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 | 136k | MockServerBase::~MockServerBase() = default; |
66 | | |
67 | | /// @return the owned MockConnection, or nullptr if one has not been opened. |
68 | 680k | 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 | 138k | void MockServerBase::Install(CURL* easy) { |
73 | 138k | curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, &MockServerBaseOpenSocketTrampoline); |
74 | 138k | curl_easy_setopt(easy, CURLOPT_OPENSOCKETDATA, this); |
75 | 138k | curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, &SockOptTrampoline); |
76 | 138k | } |
77 | | |
78 | | /// Ordinary event-driven mocks need no upload-callback hook; their RunLoop |
79 | | /// regains control after each perform and drains client traffic there. |
80 | 132k | void MockServerBase::ConfigureRequestData(ScenarioRequestData* /*request_data*/) {} |
81 | | |
82 | | /// Allocate a multi, attach 'easy', delegate to the subclass RunLoop, consume |
83 | | /// its completion message, and clean up. Harness setup failures return a |
84 | | /// stable sentinel; fuzzer callers may ignore it while unit tests can assert |
85 | | /// the protocol result without adding another callback or global. |
86 | | CURLcode MockServerBase::DriveScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool use_multi_socket, |
87 | 134k | bool wake_multi) { |
88 | | // Cache backpressure knobs so HandleOpenSocket can apply them the moment |
89 | | // connection_ exists. Both default to 0, which matches the legacy "drain |
90 | | // greedily, kernel-default buffers" behaviour exactly. |
91 | 134k | const auto& bp = scenario.connection().backpressure(); |
92 | 134k | pending_recv_buf_bytes_ = static_cast<int>(bp.recv_buf_bytes()); |
93 | 134k | pending_drain_limit_ = static_cast<std::size_t>(bp.drain_limit()); |
94 | | |
95 | 134k | CurlMultiPtr multi(curl_multi_init()); |
96 | 134k | if (multi == nullptr) { |
97 | 0 | return CURLE_FAILED_INIT; |
98 | 0 | } |
99 | | |
100 | 134k | CURLcode transfer_result = CURLE_FAILED_INIT; |
101 | | |
102 | | // Callback data must survive both remove_handle and multi_cleanup, since |
103 | | // either may emit CURL_POLL_REMOVE. Keeping it in this outer scope provides |
104 | | // that lifetime without allocating per-watch state. |
105 | 134k | MultiSocketDriver socket_driver; |
106 | 134k | if (use_multi_socket && socket_driver.Install(multi.get())) { |
107 | 873 | multi_socket_driver_ = &socket_driver; |
108 | 873 | } |
109 | 134k | if (curl_multi_add_handle(multi.get(), easy) == CURLM_OK) { |
110 | 134k | if (wake_multi) { |
111 | 288 | if (multi_socket_driver_ != nullptr) { |
112 | 130 | multi_socket_driver_->ProbeControlApis(); |
113 | 158 | } else { |
114 | 158 | long timeout_ms = -1; |
115 | 158 | (void)curl_multi_timeout(multi.get(), &timeout_ms); |
116 | 158 | (void)curl_multi_wakeup(multi.get()); |
117 | 158 | } |
118 | 288 | } |
119 | 134k | RunLoop(multi.get(), easy, scenario); |
120 | | |
121 | | // Completion messages are the multi API's only durable record of the |
122 | | // transfer result. Consume them while the easy handle is still attached: |
123 | | // otherwise every scenario systematically skips curl_multi_info_read's |
124 | | // result path and removal discards the opportunity. With one easy handle |
125 | | // attached, this drain has at most one completion message regardless of |
126 | | // fuzzed response size or redirect count. |
127 | 134k | int messages_remaining = 0; |
128 | 134k | CURLMsg* message = nullptr; |
129 | 248k | while ((message = curl_multi_info_read(multi.get(), &messages_remaining)) != nullptr) { |
130 | 113k | if (message->msg == CURLMSG_DONE && message->easy_handle == easy) { |
131 | 113k | transfer_result = message->data.result; |
132 | 113k | } |
133 | 113k | } |
134 | | |
135 | 134k | curl_multi_remove_handle(multi.get(), easy); |
136 | 134k | } |
137 | 134k | multi.reset(); |
138 | 134k | multi_socket_driver_ = nullptr; |
139 | 134k | return transfer_result; |
140 | 134k | } |
141 | | |
142 | | /// Preserve a safe fallback for protocol mocks that require an outer driver |
143 | | /// to make progress. The API policy currently forces HTTP, whose override can |
144 | | /// preload its bounded response and call curl_easy_perform without a thread. |
145 | 0 | void MockServerBase::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
146 | 0 | DriveScenario(easy, scenario); |
147 | 0 | } |
148 | | |
149 | | /// Expose only the current callback state to protocol drive loops. Ownership |
150 | | /// remains in DriveScenario so no subclass can accidentally shorten it. |
151 | 96.2k | MultiSocketDriver* MockServerBase::multi_socket_driver() { return multi_socket_driver_; } |
152 | | |
153 | | /// Hand the cached backpressure config to the connection. Safe to call when |
154 | | /// connection_ is null (no-op) or when both knobs are 0 (ApplyBackpressure |
155 | | /// itself is a no-op in that case). |
156 | 17.5k | void MockServerBase::ApplyPendingBackpressure() { |
157 | 17.5k | if (connection_) { |
158 | 17.5k | connection_->ApplyBackpressure(pending_recv_buf_bytes_, pending_drain_limit_); |
159 | 17.5k | } |
160 | 17.5k | } |
161 | | |
162 | | /// Treat a non-default BackpressureConfig as an explicit request for the |
163 | | /// slower, timed drive policy. Proto3 scalar defaults make this deterministic: |
164 | | /// a present-but-empty message remains on the ordinary fast path. |
165 | | /// @param scenario Scenario whose backpressure settings select the policy. |
166 | | /// @return true when the scenario explicitly opted into socket backpressure. |
167 | 25.1k | bool MockServerBase::UsesTimedDrive(const curl::fuzzer::proto::Scenario& scenario) { |
168 | 25.1k | const auto& bp = scenario.connection().backpressure(); |
169 | 25.1k | return bp.recv_buf_bytes() != 0 || bp.drain_limit() != 0; |
170 | 25.1k | } |
171 | | |
172 | | /// Wait on curl's fdset with a short timeout. Returns select()'s result; on |
173 | | /// error sets *rc to the corresponding CURLMcode. |
174 | 282k | int MockServerBase::WaitOnMultiFdset(CURLM* multi, CURLMcode* rc) { |
175 | 282k | fd_set readfds; |
176 | 282k | fd_set writefds; |
177 | 282k | fd_set excfds; |
178 | 282k | FD_ZERO(&readfds); |
179 | 282k | FD_ZERO(&writefds); |
180 | 282k | FD_ZERO(&excfds); |
181 | 282k | int maxfd = -1; |
182 | 282k | *rc = curl_multi_fdset(multi, &readfds, &writefds, &excfds, &maxfd); |
183 | 282k | if (*rc != CURLM_OK) { |
184 | 0 | return -1; |
185 | 0 | } |
186 | 282k | if (maxfd < 0) { |
187 | 0 | return 0; |
188 | 0 | } |
189 | 282k | struct timeval timeout; |
190 | 282k | timeout.tv_sec = 0; |
191 | 282k | timeout.tv_usec = kSelectTimeoutUs; |
192 | 282k | return ::select(maxfd + 1, &readfds, &writefds, &excfds, &timeout); |
193 | 282k | } |
194 | | |
195 | | /// Exercise curl_multi_poll's pollset/filter traversal once without sleeping. |
196 | | /// The result is deliberately ignored: this is an API/state probe, while the |
197 | | /// protocol-specific perform loop remains the authority on transfer progress. |
198 | 8.26k | void MockServerBase::ProbeMultiPollset(CURLM* multi) { |
199 | 8.26k | int numfds = 0; |
200 | 8.26k | (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds); |
201 | 8.26k | } |
202 | | |
203 | | } // namespace proto_fuzzer |