curl-fuzzer C++ reference
Loading...
Searching...
No Matches
multi_transfer_runner.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#define CURL_ALLOW_OLD_MULTI_SOCKET
13#include <curl/curl.h>
14#include <curl/multi.h>
15
16#include <algorithm>
17#include <array>
18#include <cstddef>
19#include <cstdio>
20#include <memory>
21#include <string>
22
30
31namespace proto_fuzzer {
32
33namespace {
34
36struct TransferState {
37 TransferSession session;
38 bool attached = false;
39};
40
41constexpr int kMaxDriveIterations = 512;
42constexpr int kMaxIdleIterations = 8;
43constexpr curl_off_t kStateProbeReceiveRate = 1;
44constexpr long kStateProbeTimeoutMs = 20;
45
46struct FileCloser {
47 void operator()(FILE* file) const {
48 if (file != nullptr) {
49 std::fclose(file);
50 }
51 }
52};
53
54using FilePtr = std::unique_ptr<FILE, FileCloser>;
55
59bool ApplyStateProbeOptions(CURL* easy, curl::fuzzer::proto::MultiStateProbeProfile profile, FILE* progress_sink) {
60 if (profile != curl::fuzzer::proto::MULTI_STATE_PROBE_PROGRESS_RATE_LIMIT || progress_sink == nullptr) {
61 return false;
62 }
63 return curl_easy_setopt(easy, CURLOPT_STDERR, progress_sink) == CURLE_OK &&
64 curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kStateProbeTimeoutMs) == CURLE_OK &&
65 curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 0L) == CURLE_OK &&
66 curl_easy_setopt(easy, CURLOPT_MAX_RECV_SPEED_LARGE, kStateProbeReceiveRate) == CURLE_OK;
67}
68
69std::size_t RuntimeTransferCount(const curl::fuzzer::proto::MultiPlan& plan) {
71 std::min(static_cast<std::size_t>(plan.transfer_count()), scenario_limits::kMaxMultiTransfers));
72}
73
74void RecordNotification(CURLM*, unsigned int notification, CURL*, void* userdata) {
75 auto* stats = static_cast<MultiTransferRunStats*>(userdata);
76 if (stats == nullptr) {
77 return;
78 }
79 if (notification == CURLMNOTIFY_INFO_READ) {
80 ++stats->info_read_notifications;
81 } else if (notification == CURLMNOTIFY_EASY_DONE) {
82 ++stats->easy_done_notifications;
83 }
84}
85
89class MultiTransferDriver {
90 public:
91 MultiTransferDriver(CURLM* multi, std::array<TransferState, scenario_limits::kMaxMultiTransfers>& transfers,
92 std::size_t transfer_count, MultiTransferRunStats& stats)
93 : multi_(multi), transfers_(transfers), transfer_count_(transfer_count), stats_(stats) {}
94
98 bool DrainCompletionMessages() {
99 bool consumed = false;
100 int messages_remaining = 0;
101 CURLMsg* message = nullptr;
102 while ((message = curl_multi_info_read(multi_, &messages_remaining)) != nullptr) {
103 consumed = true;
104 if (message->msg != CURLMSG_DONE) {
105 continue;
106 }
107 for (std::size_t index = 0; index < transfer_count_; ++index) {
108 TransferState& transfer = transfers_[index];
109 if (transfer.session.easy() == message->easy_handle) {
110 ++stats_.completion_messages;
111 break;
112 }
113 }
114 }
115 return consumed;
116 }
117
121 bool ApplyAction(const curl::fuzzer::proto::MultiAction& action) {
122 if (transfer_count_ == 0) {
123 return false;
124 }
125 TransferState& transfer = transfers_[static_cast<std::size_t>(action.transfer_selector()) % transfer_count_];
126 CURL* easy = transfer.session.easy();
127 if (easy == nullptr) {
128 return false;
129 }
130
131 switch (action.kind()) {
132 case curl::fuzzer::proto::MULTI_ACTION_PAUSE_RECV:
133 return curl_easy_pause(easy, CURLPAUSE_RECV) == CURLE_OK;
134 case curl::fuzzer::proto::MULTI_ACTION_PAUSE_SEND:
135 return curl_easy_pause(easy, CURLPAUSE_SEND) == CURLE_OK;
136 case curl::fuzzer::proto::MULTI_ACTION_PAUSE_ALL:
137 return curl_easy_pause(easy, CURLPAUSE_ALL) == CURLE_OK;
138 case curl::fuzzer::proto::MULTI_ACTION_RESUME:
139 return curl_easy_pause(easy, CURLPAUSE_CONT) == CURLE_OK;
140 case curl::fuzzer::proto::MULTI_ACTION_REMOVE:
141 if (transfer.attached && curl_multi_remove_handle(multi_, easy) == CURLM_OK) {
142 transfer.attached = false;
143 return true;
144 }
145 return false;
146 case curl::fuzzer::proto::MULTI_ACTION_READD:
147 if (!transfer.attached && curl_multi_add_handle(multi_, easy) == CURLM_OK) {
148 transfer.attached = true;
149 return true;
150 }
151 return false;
152 case curl::fuzzer::proto::MULTI_ACTION_NONE:
153 default:
154 return false;
155 }
156 }
157
158 private:
159 CURLM* multi_;
160 std::array<TransferState, scenario_limits::kMaxMultiTransfers>& transfers_;
161 const std::size_t transfer_count_;
162 MultiTransferRunStats& stats_;
163};
164
167void ProbeMultiWaitApis(CURLM* multi) {
168 int numfds = 0;
169 (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds);
170 (void)curl_multi_wait(multi, nullptr, 0, 0, &numfds);
171
172 unsigned int waitfd_count = 0;
173 (void)curl_multi_waitfds(multi, nullptr, 0, &waitfd_count);
174 std::array<curl_waitfd, scenario_limits::kMaxMultiTransfers * 2> waitfds{};
175 (void)curl_multi_waitfds(multi, waitfds.data(), static_cast<unsigned int>(waitfds.size()), &waitfd_count);
176
177 fd_set readfds;
178 fd_set writefds;
179 fd_set exceptfds;
180 FD_ZERO(&readfds);
181 FD_ZERO(&writefds);
182 FD_ZERO(&exceptfds);
183 int maxfd = -1;
184 (void)curl_multi_fdset(multi, &readfds, &writefds, &exceptfds, &maxfd);
185 long timeout_ms = -1;
186 (void)curl_multi_timeout(multi, &timeout_ms);
187
188 CURL** handles = curl_multi_get_handles(multi);
189 curl_free(handles);
190
191 constexpr CURLMinfo_offt kOffsetInfo[] = {
192 CURLMINFO_XFERS_CURRENT, CURLMINFO_XFERS_RUNNING, CURLMINFO_XFERS_PENDING,
193 CURLMINFO_XFERS_DONE, CURLMINFO_XFERS_ADDED,
194 };
195 for (const CURLMinfo_offt info : kOffsetInfo) {
196 curl_off_t value = 0;
197 (void)curl_multi_get_offt(multi, info, &value);
198 }
199}
200
205void ProbeLegacyMultiSocketApis(CURLM* multi) {
206 int running_handles = 0;
207#if defined(__GNUC__)
208#pragma GCC diagnostic push
209#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
210#endif
211 (void)curl_multi_socket(multi, CURL_SOCKET_TIMEOUT, &running_handles);
212 (void)curl_multi_socket_all(multi, &running_handles);
213#if defined(__GNUC__)
214#pragma GCC diagnostic pop
215#endif
216}
217
218} // namespace
219
226MultiTransferRunStats RunMultiTransferScenario(const curl::fuzzer::proto::Scenario& scenario) {
228 const auto& plan = scenario.multi_plan();
229 const std::size_t transfer_count = RuntimeTransferCount(plan);
230 stats.configured_handles = transfer_count;
231
232 FilePtr progress_sink;
233 if (plan.state_probe_profile() == curl::fuzzer::proto::MULTI_STATE_PROBE_PROGRESS_RATE_LIMIT) {
234 progress_sink.reset(std::tmpfile());
235 }
236
237 // The peer and socket callback state must outlive multi cleanup, which can
238 // synchronously announce socket removals and close cached connections.
239 MockServer mock;
240 mock.SetScripts(scenario);
241 mock.SetKeepConnectionsOpen(plan.keep_connections_open());
242 MultiSocketDriver socket_driver;
243 CurlMultiPtr multi(curl_multi_init());
244 if (!multi) {
245 return stats;
246 }
247
248 const long max_host_connections =
249 static_cast<long>(std::min<std::size_t>(plan.max_host_connections(), transfer_count));
250 const long max_total_connections =
251 static_cast<long>(std::min<std::size_t>(plan.max_total_connections(), transfer_count));
252 const long cache_size =
253 static_cast<long>(std::min<std::size_t>(plan.connection_cache_size(), scenario_limits::kMaxMultiTransfers * 2));
254 (void)curl_multi_setopt(multi.get(), CURLMOPT_MAX_HOST_CONNECTIONS, max_host_connections);
255 (void)curl_multi_setopt(multi.get(), CURLMOPT_MAX_TOTAL_CONNECTIONS, max_total_connections);
256 (void)curl_multi_setopt(multi.get(), CURLMOPT_MAXCONNECTS, cache_size);
257 (void)curl_multi_setopt(multi.get(), CURLMOPT_PIPELINING,
258 plan.multiplex() ? static_cast<long>(CURLPIPE_MULTIPLEX) : 0L);
259 (void)curl_multi_setopt(multi.get(), CURLMOPT_NOTIFYFUNCTION, &RecordNotification);
260 (void)curl_multi_setopt(multi.get(), CURLMOPT_NOTIFYDATA, &stats);
261 (void)curl_multi_notify_enable(multi.get(), CURLMNOTIFY_INFO_READ);
262 (void)curl_multi_notify_enable(multi.get(), CURLMNOTIFY_EASY_DONE);
263 (void)curl_multi_notify_disable(multi.get(), CURLMNOTIFY_INFO_READ);
264 (void)curl_multi_notify_enable(multi.get(), CURLMNOTIFY_INFO_READ);
265 (void)curl_multi_notify_disable(multi.get(), CURLMNOTIFY_EASY_DONE);
266 (void)curl_multi_notify_enable(multi.get(), CURLMNOTIFY_EASY_DONE);
267
268 const bool socket_mode = plan.drive_mode() == curl::fuzzer::proto::MULTI_DRIVE_SOCKET;
269 const bool socket_driver_installed = socket_mode && socket_driver.Install(multi.get());
270 std::array<TransferState, scenario_limits::kMaxMultiTransfers> transfers;
271 const std::string url = "http://" + scenario.host_path();
272 for (std::size_t index = 0; index < transfer_count; ++index) {
273 TransferState& transfer = transfers[index];
274 if (!transfer.session.Initialize()) {
275 continue;
276 }
277 transfer.session.ApplyBaseline(scenario);
278 (void)curl_easy_setopt(transfer.session.easy(), CURLOPT_URL, url.c_str());
279 mock.Install(transfer.session.easy());
280 (void)ApplyScenarioOptions(transfer.session.easy(), scenario);
281 if (ApplyStateProbeOptions(transfer.session.easy(), plan.state_probe_profile(), progress_sink.get())) {
282 ++stats.state_probe_handles;
283 }
284 mock.ConfigureRequestData(transfer.session.InstallRequestData(scenario));
285 if (curl_multi_add_handle(multi.get(), transfer.session.easy()) == CURLM_OK) {
286 transfer.attached = true;
287 ++stats.added_handles;
288 }
289 }
290
291 if (stats.added_handles == 0) {
292 multi.reset();
293 return stats;
294 }
295
296 MultiTransferDriver driver(multi.get(), transfers, transfer_count, stats);
297 ProbeLegacyMultiSocketApis(multi.get());
298
299 const std::size_t action_count = std::min<std::size_t>(plan.actions_size(), scenario_limits::kMaxMultiActions);
300 std::size_t next_action = 0;
301 bool initial_action_changed = false;
302 if (next_action < action_count) {
303 initial_action_changed = driver.ApplyAction(plan.actions(static_cast<int>(next_action++)));
304 ++stats.actions_consumed;
305 }
306
307 int running_handles = 0;
308 CURLMcode result = socket_driver_installed ? socket_driver.Start(&running_handles)
309 : curl_multi_perform(multi.get(), &running_handles);
310 bool completion_progress = driver.DrainCompletionMessages();
311 ProbeMultiWaitApis(multi.get());
312 if (plan.wake_multi()) {
313 if (socket_driver_installed) {
314 socket_driver.ProbeControlApis();
315 } else {
316 (void)curl_multi_wakeup(multi.get());
317 }
318 }
319
320 int idle_iterations = initial_action_changed || completion_progress ? 0 : 1;
321 for (int iteration = 0; result == CURLM_OK && iteration < kMaxDriveIterations; ++iteration) {
322 if (running_handles == 0 && next_action >= action_count) {
323 break;
324 }
325
326 bool made_progress = mock.ServiceConnections();
327 if (next_action < action_count) {
328 made_progress = driver.ApplyAction(plan.actions(static_cast<int>(next_action++))) || made_progress;
329 ++stats.actions_consumed;
330 // Consuming an ordered no-op is still harness progress: continue to the
331 // next action instead of letting an idle transport hide its suffix.
332 made_progress = true;
333 }
334
335 const int running_before = running_handles;
336 if (socket_driver_installed) {
337 const MultiSocketDriver::DriveResult drive = socket_driver.DriveReady(&running_handles);
338 result = drive.code;
339 made_progress = drive.made_progress || made_progress;
340 } else {
341 result = curl_multi_perform(multi.get(), &running_handles);
342 made_progress = running_handles != running_before || made_progress;
343 }
344 made_progress = driver.DrainCompletionMessages() || made_progress;
345
346 if (made_progress) {
347 idle_iterations = 0;
348 } else if (++idle_iterations >= kMaxIdleIterations) {
349 break;
350 }
351 }
352
353 (void)driver.DrainCompletionMessages();
355 for (std::size_t index = 0; index < transfer_count; ++index) {
356 TransferState& transfer = transfers[index];
357 if (transfer.session.easy() == nullptr) {
358 continue;
359 }
360 (void)curl_easy_pause(transfer.session.easy(), CURLPAUSE_CONT);
361 if (transfer.attached) {
362 (void)curl_multi_remove_handle(multi.get(), transfer.session.easy());
363 transfer.attached = false;
364 }
365 }
366
367 // Destroy the multi while both its socket callback storage and mock peer are
368 // live, then detach request-data pointers before cleaning each easy handle.
369 multi.reset();
370 stats.watch_exhausted = socket_driver.watch_exhausted();
371 if (stats.watch_exhausted) {
372 std::fputs("curl-fuzzer: socket watch capacity exhausted\n", stderr);
373 }
374 for (std::size_t index = 0; index < transfer_count; ++index) {
375 transfers[index].session.Close();
376 }
377 return stats;
378}
379
380} // namespace proto_fuzzer
virtual void Install(CURL *easy)
Install the common OPENSOCKETFUNCTION / OPENSOCKETDATA / SOCKOPTFUNCTION callbacks on 'easy'.
virtual void ConfigureRequestData(ScenarioRequestData *request_data)
Bind protocol-specific work to the request-data callbacks after their per-scenario state has been con...
Orchestrates a bounded sequence of mock HTTP exchanges: installs socket callbacks on an easy handle,...
Definition mock_server.h:72
std::size_t opened_connection_count() const
Report how many peer sockets curl opened during the current script run.
bool ServiceConnections()
Service one deterministic application event-loop turn by draining all available request bytes and rel...
void SetKeepConnectionsOpen(bool keep_open)
Keep completed response sockets writable instead of half-closing them.
void SetScripts(const curl::fuzzer::proto::Scenario &scenario)
Build the complete borrowed script table before entering curl.
Owns the socket and timer callback state required by libcurl's event-driven multi API.
CURLMcode Start(int *running_handles)
Start libcurl's socket state machine through the documented timeout pseudo-socket without waiting for...
bool Install(CURLM *multi)
Install socket/timer callbacks on a caller-owned multi before adding an easy handle.
void ProbeControlApis()
Exercise the wakeup and timeout-query APIs from a valid live-multi state.
DriveResult DriveReady(int *running_handles)
Run one bounded, non-blocking event-loop turn: poll the callback-provided watch set with a zero timeo...
Common ownership types for independently cleaned-up curl handles.
std::unique_ptr< CURLM, CurlMultiDeleter > CurlMultiPtr
Unique ownership of a CURL multi handle.
Definition curl_raii.h:58
MockConnection and MockServer — the in-process peer that feeds canned response bytes to libcurl over ...
Bounded event-loop state for driving curl_multi_socket_action.
MultiTransferRunStats RunMultiTransferScenario(const curl::fuzzer::proto::Scenario &scenario)
Drive two to four easy handles through one CURLM and one bounded in-process HTTP peer.
Bounded shared-multi orchestration for concurrent HTTP transfers.
std::size_t ApplyScenarioOptions(CURL *easy, const curl::fuzzer::proto::Scenario &scenario)
Apply the bounded runtime-visible prefix of Scenario.options.
Translates Scenario SetOption messages into curl_easy_setopt calls, plus the fixed baseline setopts t...
Owns pointer-valued HTTP headers, TELNET options, MIME state, and upload callback state derived from ...
Shared complexity limits for structured scenarios.
constexpr std::size_t kMinMultiTransfers
Two handles are the minimum that can enter shared-multi scheduling, while four align with the mock's ...
constexpr std::size_t kMaxMultiActions
Ordered lifecycle transitions remain useful in pairs (pause/resume and remove/re-add); sixteen permit...
constexpr std::size_t kMaxMultiTransfers
Maximum easy handles configured in one shared-multi fuzz iteration.
Result from one non-blocking event-loop turn.
bool made_progress
True when an action ran or callback/handle state changed.
CURLMcode code
libcurl's result for the last socket action in the turn.
Observable lifecycle counts returned to focused unit tests.
std::size_t added_handles
Easy handles successfully added to the shared multi handle.
std::size_t state_probe_handles
Easy handles that received the complete closed state-probe setup.
std::size_t opened_connections
Peer sockets opened while serving the concurrent transfers.
std::size_t configured_handles
Easy handles requested after runtime bounds are applied.
std::size_t actions_consumed
Ordered plan actions consumed, including intentional no-ops.
bool watch_exhausted
Socket readiness could not be represented in the fixed watch table.
Owns an easy handle and the backing storage retained by its options.