/src/curl_fuzzer/proto_fuzzer/multi_socket_driver.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 the bounded curl_multi_socket_action driver. |
9 | | |
10 | | #include "proto_fuzzer/multi_socket_driver.h" |
11 | | |
12 | | #include <poll.h> |
13 | | |
14 | | #include <array> |
15 | | #include <cstddef> |
16 | | #include <cstdint> |
17 | | |
18 | | namespace proto_fuzzer { |
19 | | |
20 | | namespace { |
21 | | |
22 | | /// Translate libcurl's read/write interest into poll(2) events. |
23 | 0 | short PollEventsForInterest(int interest) { |
24 | 0 | short events = 0; |
25 | 0 | if ((interest & CURL_POLL_IN) != 0) { |
26 | 0 | events |= POLLIN; |
27 | 0 | } |
28 | 0 | if ((interest & CURL_POLL_OUT) != 0) { |
29 | 0 | events |= POLLOUT; |
30 | 0 | } |
31 | 0 | return events; |
32 | 0 | } |
33 | | |
34 | | /// Translate an observed poll result into curl_multi_socket_action flags. |
35 | 0 | int CurlEventsForPollResult(short events) { |
36 | 0 | int result = 0; |
37 | 0 | if ((events & (POLLIN | POLLHUP)) != 0) { |
38 | | // A stream hangup remains readable until curl consumes EOF. |
39 | 0 | result |= CURL_CSELECT_IN; |
40 | 0 | } |
41 | 0 | if ((events & POLLOUT) != 0) { |
42 | 0 | result |= CURL_CSELECT_OUT; |
43 | 0 | } |
44 | 0 | if ((events & (POLLERR | POLLHUP | POLLNVAL)) != 0) { |
45 | 0 | result |= CURL_CSELECT_ERR; |
46 | 0 | } |
47 | 0 | return result; |
48 | 0 | } |
49 | | |
50 | | } // namespace |
51 | | |
52 | | /// Construct detached callback state. Install() supplies the multi only after |
53 | | /// all watch storage has reached its final address. |
54 | 38.0k | MultiSocketDriver::MultiSocketDriver() : multi_(nullptr), timeout_ms_(-1), timer_pending_(false), generation_(0) {} |
55 | | |
56 | | /// The owner deliberately destroys this after curl_multi_cleanup, so there is |
57 | | /// no callback deregistration or libcurl access left for the destructor. |
58 | 38.0k | MultiSocketDriver::~MultiSocketDriver() = default; |
59 | | |
60 | | /// Register all callbacks before an easy handle is added. curl may announce a |
61 | | /// timer during curl_multi_add_handle, so installing later would miss the |
62 | | /// event that starts an otherwise idle socket-action application. |
63 | 0 | bool MultiSocketDriver::Install(CURLM* multi) { |
64 | 0 | multi_ = multi; |
65 | 0 | if (multi_ == nullptr) { |
66 | 0 | return false; |
67 | 0 | } |
68 | 0 | return curl_multi_setopt(multi_, CURLMOPT_SOCKETFUNCTION, &MultiSocketDriver::SocketCallback) == CURLM_OK && |
69 | 0 | curl_multi_setopt(multi_, CURLMOPT_SOCKETDATA, this) == CURLM_OK && |
70 | 0 | curl_multi_setopt(multi_, CURLMOPT_TIMERFUNCTION, &MultiSocketDriver::TimerCallback) == CURLM_OK && |
71 | 0 | curl_multi_setopt(multi_, CURLMOPT_TIMERDATA, this) == CURLM_OK; |
72 | 0 | } |
73 | | |
74 | | /// Kick the state machine through the documented timeout pseudo-socket. This |
75 | | /// creates the first real socket and therefore gives the callback its initial |
76 | | /// watch without introducing a wall-clock dependency. |
77 | 0 | CURLMcode MultiSocketDriver::Start(int* running_handles) { |
78 | 0 | if (multi_ == nullptr) { |
79 | 0 | return CURLM_BAD_HANDLE; |
80 | 0 | } |
81 | 0 | timer_pending_ = false; |
82 | 0 | return curl_multi_socket_action(multi_, CURL_SOCKET_TIMEOUT, 0, running_handles); |
83 | 0 | } |
84 | | |
85 | | /// Run one bounded, non-blocking application event-loop turn. The snapshot is |
86 | | /// intentional: a socket action may synchronously remove the current watch or |
87 | | /// install another one, so the callback-owned table must not be iterated as a |
88 | | /// live container across that call. |
89 | 0 | MultiSocketDriver::DriveResult MultiSocketDriver::DriveReady(int* running_handles) { |
90 | 0 | DriveResult result; |
91 | 0 | if (multi_ == nullptr) { |
92 | 0 | result.code = CURLM_BAD_HANDLE; |
93 | 0 | return result; |
94 | 0 | } |
95 | | |
96 | 0 | std::array<struct pollfd, kMaxWatches> poll_fds{}; |
97 | 0 | std::size_t poll_count = 0; |
98 | 0 | for (const Watch& watch : watches_) { |
99 | 0 | if (!watch.active) { |
100 | 0 | continue; |
101 | 0 | } |
102 | 0 | poll_fds[poll_count].fd = watch.socket; |
103 | 0 | poll_fds[poll_count].events = PollEventsForInterest(watch.interest); |
104 | 0 | ++poll_count; |
105 | 0 | } |
106 | |
|
107 | 0 | const std::uint64_t generation_before = generation_; |
108 | 0 | const int running_before = running_handles == nullptr ? 0 : *running_handles; |
109 | 0 | bool action_dispatched = false; |
110 | 0 | if (poll_count != 0 && ::poll(poll_fds.data(), static_cast<nfds_t>(poll_count), 0) > 0) { |
111 | 0 | for (std::size_t index = 0; index < poll_count; ++index) { |
112 | 0 | if (running_handles != nullptr && *running_handles == 0) { |
113 | 0 | break; |
114 | 0 | } |
115 | | // An earlier action can synchronously remove or repurpose any later fd |
116 | | // in the snapshot. Revalidate it against the callback-owned table so a |
117 | | // stale readiness notification never reaches curl under a new meaning. |
118 | 0 | Watch* current = FindWatch(poll_fds[index].fd); |
119 | 0 | if (current == nullptr) { |
120 | 0 | continue; |
121 | 0 | } |
122 | 0 | const int events = CurlEventsForPollResult(poll_fds[index].revents); |
123 | 0 | if (events == 0) { |
124 | 0 | continue; |
125 | 0 | } |
126 | 0 | result.code = curl_multi_socket_action(multi_, poll_fds[index].fd, events, running_handles); |
127 | 0 | action_dispatched = true; |
128 | | // Process at most one snapshot entry. Its callbacks can repurpose an fd |
129 | | // with the same numeric value and interest, which no post-hoc lookup can |
130 | | // distinguish; the outer loop rebuilds readiness from fresh watches. |
131 | 0 | break; |
132 | 0 | } |
133 | 0 | } |
134 | |
|
135 | 0 | if (result.code == CURLM_OK && timer_pending_ && timeout_ms_ == 0) { |
136 | | // Clear first: the action may synchronously install another zero timer, |
137 | | // which belongs to the next outer loop turn rather than recursive work. |
138 | 0 | timer_pending_ = false; |
139 | 0 | result.code = curl_multi_socket_action(multi_, CURL_SOCKET_TIMEOUT, 0, running_handles); |
140 | 0 | action_dispatched = true; |
141 | 0 | } |
142 | |
|
143 | 0 | const int running_after = running_handles == nullptr ? 0 : *running_handles; |
144 | | // A successful action can consume buffered protocol bytes without changing |
145 | | // either callbacks or handle count, so dispatch itself is observable |
146 | | // progress. The outer fixed operation cap still bounds permanently-ready |
147 | | // sockets. |
148 | 0 | result.made_progress = action_dispatched || generation_ != generation_before || running_after != running_before; |
149 | 0 | return result; |
150 | 0 | } |
151 | | |
152 | | /// Touch the control APIs from a valid live-multi state. A zero-timeout query |
153 | | /// is informational; wakeup is also non-blocking when no other thread is in a |
154 | | /// poll call, which is exactly the deterministic behavior this lane needs. |
155 | 0 | void MultiSocketDriver::ProbeControlApis() { |
156 | 0 | if (multi_ == nullptr) { |
157 | 0 | return; |
158 | 0 | } |
159 | 0 | long timeout_ms = -1; |
160 | 0 | (void)curl_multi_timeout(multi_, &timeout_ms); |
161 | 0 | (void)curl_multi_wakeup(multi_); |
162 | 0 | } |
163 | | |
164 | | /// Route the C callback into state whose lifetime is owned by DriveScenario. |
165 | | int MultiSocketDriver::SocketCallback(CURL* /*easy*/, curl_socket_t socket, int what, void* user_data, |
166 | 0 | void* socket_data) { |
167 | 0 | return static_cast<MultiSocketDriver*>(user_data)->UpdateSocket(socket, what, socket_data); |
168 | 0 | } |
169 | | |
170 | | /// Defer timer processing so a zero timer cannot recursively call back into |
171 | | /// curl_multi_socket_action from inside libcurl. |
172 | 0 | int MultiSocketDriver::TimerCallback(CURLM* /*multi*/, long timeout_ms, void* user_data) { |
173 | 0 | return static_cast<MultiSocketDriver*>(user_data)->UpdateTimer(timeout_ms); |
174 | 0 | } |
175 | | |
176 | | /// Maintain a stable association for every observed fd. `socket_data` is used |
177 | | /// only as a consistency hint: libcurl owns it and may legitimately pass null |
178 | | /// for the first notification, while our fd lookup remains authoritative. |
179 | 0 | int MultiSocketDriver::UpdateSocket(curl_socket_t socket, int what, void* socket_data) { |
180 | 0 | Watch* watch = FindWatch(socket); |
181 | 0 | if (what == CURL_POLL_REMOVE) { |
182 | 0 | if (watch != nullptr) { |
183 | 0 | (void)curl_multi_assign(multi_, socket, nullptr); |
184 | 0 | watch->active = false; |
185 | 0 | watch->socket = CURL_SOCKET_BAD; |
186 | 0 | watch->interest = CURL_POLL_NONE; |
187 | 0 | ++generation_; |
188 | 0 | } |
189 | 0 | return 0; |
190 | 0 | } |
191 | | |
192 | 0 | if (watch == nullptr) { |
193 | 0 | watch = FindFreeWatch(); |
194 | 0 | if (watch == nullptr) { |
195 | 0 | return 0; |
196 | 0 | } |
197 | 0 | watch->socket = socket; |
198 | 0 | watch->active = true; |
199 | 0 | (void)curl_multi_assign(multi_, socket, watch); |
200 | 0 | ++generation_; |
201 | 0 | } else if (socket_data != nullptr && socket_data != watch) { |
202 | | // Reassert the stable association if an unusual transition supplied a |
203 | | // different application pointer. Never dereference foreign socket_data. |
204 | 0 | (void)curl_multi_assign(multi_, socket, watch); |
205 | 0 | } |
206 | | |
207 | 0 | if (watch->interest != what) { |
208 | 0 | watch->interest = what; |
209 | 0 | ++generation_; |
210 | 0 | } |
211 | 0 | return 0; |
212 | 0 | } |
213 | | |
214 | | /// Record only meaningful timer transitions. Repeated identical callbacks do |
215 | | /// not count as progress, otherwise an unproductive transfer could consume |
216 | | /// the full operation budget instead of the much smaller idle budget. |
217 | 0 | int MultiSocketDriver::UpdateTimer(long timeout_ms) { |
218 | 0 | if (!timer_pending_ || timeout_ms_ != timeout_ms) { |
219 | 0 | ++generation_; |
220 | 0 | } |
221 | 0 | timeout_ms_ = timeout_ms; |
222 | 0 | timer_pending_ = timeout_ms >= 0; |
223 | 0 | return 0; |
224 | 0 | } |
225 | | |
226 | | /// Locate an existing association without allocating or depending on fd |
227 | | /// magnitude (socket descriptors are not safe array indexes). |
228 | 0 | MultiSocketDriver::Watch* MultiSocketDriver::FindWatch(curl_socket_t socket) { |
229 | 0 | for (Watch& watch : watches_) { |
230 | 0 | if (watch.active && watch.socket == socket) { |
231 | 0 | return &watch; |
232 | 0 | } |
233 | 0 | } |
234 | 0 | return nullptr; |
235 | 0 | } |
236 | | |
237 | | /// Return the first inactive stable slot. Exhaustion is harmless: curl keeps |
238 | | /// owning the socket and the deterministic idle budget ends the fuzz case. |
239 | 0 | MultiSocketDriver::Watch* MultiSocketDriver::FindFreeWatch() { |
240 | 0 | for (Watch& watch : watches_) { |
241 | 0 | if (!watch.active) { |
242 | 0 | return &watch; |
243 | 0 | } |
244 | 0 | } |
245 | 0 | return nullptr; |
246 | 0 | } |
247 | | |
248 | | } // namespace proto_fuzzer |