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