Coverage Report

Created: 2026-10-28 07:17

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/curl_fuzzer/proto_fuzzer/multi_transfer_runner.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 concurrent easy-handle runner.
9
10
#include "proto_fuzzer/multi_transfer_runner.h"
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
23
#include "proto_fuzzer/curl_raii.h"
24
#include "proto_fuzzer/mock_server.h"
25
#include "proto_fuzzer/multi_socket_driver.h"
26
#include "proto_fuzzer/option_apply.h"
27
#include "proto_fuzzer/request_data.h"
28
#include "proto_fuzzer/scenario_limits.h"
29
#include "proto_fuzzer/transfer_session.h"
30
31
namespace proto_fuzzer {
32
33
namespace {
34
35
/// Track shared-multi membership separately from each transfer's resources.
36
struct TransferState {
37
  TransferSession session;
38
  bool attached = false;
39
};
40
41
constexpr int kMaxDriveIterations = 512;
42
constexpr int kMaxIdleIterations = 8;
43
constexpr curl_off_t kStateProbeReceiveRate = 1;
44
constexpr long kStateProbeTimeoutMs = 20;
45
46
struct FileCloser {
47
1.56k
  void operator()(FILE* file) const {
48
1.56k
    if (file != nullptr) {
49
1.56k
      std::fclose(file);
50
1.56k
    }
51
1.56k
  }
52
};
53
54
using FilePtr = std::unique_ptr<FILE, FileCloser>;
55
56
/// Install the one intentionally slow transfer shape without exposing its
57
/// values through generic SetOption mutations. The anonymous stream is owned
58
/// by RunMultiTransferScenario and outlives every configured easy handle.
59
37.7k
bool ApplyStateProbeOptions(CURL* easy, curl::fuzzer::proto::MultiStateProbeProfile profile, FILE* progress_sink) {
60
37.7k
  if (profile != curl::fuzzer::proto::MULTI_STATE_PROBE_PROGRESS_RATE_LIMIT || progress_sink == nullptr) {
61
32.2k
    return false;
62
32.2k
  }
63
5.45k
  return curl_easy_setopt(easy, CURLOPT_STDERR, progress_sink) == CURLE_OK &&
64
5.45k
         curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, kStateProbeTimeoutMs) == CURLE_OK &&
65
5.45k
         curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 0L) == CURLE_OK &&
66
5.45k
         curl_easy_setopt(easy, CURLOPT_MAX_RECV_SPEED_LARGE, kStateProbeReceiveRate) == CURLE_OK;
67
37.7k
}
68
69
12.1k
std::size_t RuntimeTransferCount(const curl::fuzzer::proto::MultiPlan& plan) {
70
12.1k
  return std::max(scenario_limits::kMinMultiTransfers,
71
12.1k
                  std::min(static_cast<std::size_t>(plan.transfer_count()), scenario_limits::kMaxMultiTransfers));
72
12.1k
}
73
74
51.1k
void RecordNotification(CURLM*, unsigned int notification, CURL*, void* userdata) {
75
51.1k
  auto* stats = static_cast<MultiTransferRunStats*>(userdata);
76
51.1k
  if (stats == nullptr) {
77
0
    return;
78
0
  }
79
51.1k
  if (notification == CURLMNOTIFY_INFO_READ) {
80
16.6k
    ++stats->info_read_notifications;
81
34.5k
  } else if (notification == CURLMNOTIFY_EASY_DONE) {
82
34.5k
    ++stats->easy_done_notifications;
83
34.5k
  }
84
51.1k
}
85
86
/// Consume every currently queued completion before handle removal can discard
87
/// it. Re-added handles may complete more than once, so count messages rather
88
/// than only distinct easy pointers.
89
bool DrainCompletionMessages(CURLM* multi, std::array<TransferState, scenario_limits::kMaxMultiTransfers>* transfers,
90
181k
                             std::size_t transfer_count, MultiTransferRunStats* stats) {
91
181k
  bool consumed = false;
92
181k
  int messages_remaining = 0;
93
181k
  CURLMsg* message = nullptr;
94
215k
  while ((message = curl_multi_info_read(multi, &messages_remaining)) != nullptr) {
95
34.1k
    consumed = true;
96
34.1k
    if (message->msg != CURLMSG_DONE) {
97
0
      continue;
98
0
    }
99
75.3k
    for (std::size_t index = 0; index < transfer_count; ++index) {
100
75.3k
      TransferState& transfer = (*transfers)[index];
101
75.3k
      if (transfer.session.easy() == message->easy_handle) {
102
34.1k
        ++stats->completion_messages;
103
34.1k
        break;
104
34.1k
      }
105
75.3k
    }
106
34.1k
  }
107
181k
  return consumed;
108
181k
}
109
110
/// Apply one schema-safe lifecycle transition. Return true only when libcurl
111
/// accepted a state change; consuming an ineffective action is tracked
112
/// separately so later ordered actions remain reachable.
113
bool ApplyAction(const curl::fuzzer::proto::MultiAction& action, CURLM* multi,
114
                 std::array<TransferState, scenario_limits::kMaxMultiTransfers>* transfers,
115
21.0k
                 std::size_t transfer_count) {
116
21.0k
  if (transfer_count == 0) {
117
0
    return false;
118
0
  }
119
21.0k
  TransferState& transfer = (*transfers)[static_cast<std::size_t>(action.transfer_selector()) % transfer_count];
120
21.0k
  CURL* easy = transfer.session.easy();
121
21.0k
  if (easy == nullptr) {
122
159
    return false;
123
159
  }
124
125
20.8k
  switch (action.kind()) {
126
1.53k
    case curl::fuzzer::proto::MULTI_ACTION_PAUSE_RECV:
127
1.53k
      return curl_easy_pause(easy, CURLPAUSE_RECV) == CURLE_OK;
128
1.10k
    case curl::fuzzer::proto::MULTI_ACTION_PAUSE_SEND:
129
1.10k
      return curl_easy_pause(easy, CURLPAUSE_SEND) == CURLE_OK;
130
808
    case curl::fuzzer::proto::MULTI_ACTION_PAUSE_ALL:
131
808
      return curl_easy_pause(easy, CURLPAUSE_ALL) == CURLE_OK;
132
597
    case curl::fuzzer::proto::MULTI_ACTION_RESUME:
133
597
      return curl_easy_pause(easy, CURLPAUSE_CONT) == CURLE_OK;
134
6.27k
    case curl::fuzzer::proto::MULTI_ACTION_REMOVE:
135
6.27k
      if (transfer.attached && curl_multi_remove_handle(multi, easy) == CURLM_OK) {
136
5.41k
        transfer.attached = false;
137
5.41k
        return true;
138
5.41k
      }
139
861
      return false;
140
6.42k
    case curl::fuzzer::proto::MULTI_ACTION_READD:
141
6.42k
      if (!transfer.attached && curl_multi_add_handle(multi, easy) == CURLM_OK) {
142
4.59k
        transfer.attached = true;
143
4.59k
        return true;
144
4.59k
      }
145
1.83k
      return false;
146
4.11k
    case curl::fuzzer::proto::MULTI_ACTION_NONE:
147
4.11k
    default:
148
4.11k
      return false;
149
20.8k
  }
150
20.8k
}
151
152
/// Cover the public wait/poll/fdset queries from a valid shared-multi state
153
/// without introducing wall-clock delay.
154
12.1k
void ProbeMultiWaitApis(CURLM* multi) {
155
12.1k
  int numfds = 0;
156
12.1k
  (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds);
157
12.1k
  (void)curl_multi_wait(multi, nullptr, 0, 0, &numfds);
158
159
12.1k
  unsigned int waitfd_count = 0;
160
12.1k
  (void)curl_multi_waitfds(multi, nullptr, 0, &waitfd_count);
161
12.1k
  std::array<curl_waitfd, scenario_limits::kMaxMultiTransfers * 2> waitfds{};
162
12.1k
  (void)curl_multi_waitfds(multi, waitfds.data(), static_cast<unsigned int>(waitfds.size()), &waitfd_count);
163
164
12.1k
  fd_set readfds;
165
12.1k
  fd_set writefds;
166
12.1k
  fd_set exceptfds;
167
12.1k
  FD_ZERO(&readfds);
168
12.1k
  FD_ZERO(&writefds);
169
12.1k
  FD_ZERO(&exceptfds);
170
12.1k
  int maxfd = -1;
171
12.1k
  (void)curl_multi_fdset(multi, &readfds, &writefds, &exceptfds, &maxfd);
172
12.1k
  long timeout_ms = -1;
173
12.1k
  (void)curl_multi_timeout(multi, &timeout_ms);
174
175
12.1k
  CURL** handles = curl_multi_get_handles(multi);
176
12.1k
  curl_free(handles);
177
178
12.1k
  constexpr CURLMinfo_offt kOffsetInfo[] = {
179
12.1k
      CURLMINFO_XFERS_CURRENT, CURLMINFO_XFERS_RUNNING, CURLMINFO_XFERS_PENDING,
180
12.1k
      CURLMINFO_XFERS_DONE,    CURLMINFO_XFERS_ADDED,
181
12.1k
  };
182
60.6k
  for (const CURLMinfo_offt info : kOffsetInfo) {
183
60.6k
    curl_off_t value = 0;
184
60.6k
    (void)curl_multi_get_offt(multi, info, &value);
185
60.6k
  }
186
12.1k
}
187
188
/// Exercise the two deprecated public entrypoints as real symbols rather than
189
/// curl_multi_socket's compatibility macro. Their output is deliberately not
190
/// used to select the primary runner: either call may advance a transfer, and
191
/// the normal bounded loop consumes whatever state remains.
192
12.1k
void ProbeLegacyMultiSocketApis(CURLM* multi) {
193
12.1k
  int running_handles = 0;
194
12.1k
#if defined(__GNUC__)
195
12.1k
#pragma GCC diagnostic push
196
12.1k
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
197
12.1k
#endif
198
12.1k
  (void)curl_multi_socket(multi, CURL_SOCKET_TIMEOUT, &running_handles);
199
12.1k
  (void)curl_multi_socket_all(multi, &running_handles);
200
12.1k
#if defined(__GNUC__)
201
12.1k
#pragma GCC diagnostic pop
202
12.1k
#endif
203
12.1k
}
204
205
}  // namespace
206
207
/// Drive two to four easy handles through one CURLM and one bounded in-process
208
/// HTTP peer. Runtime bounds repeat the mutation policy's limits so direct
209
/// unit-test and standalone calls cannot make work proportional to unchecked
210
/// input.
211
/// @param scenario HTTP transfer shape and concurrent lifecycle plan.
212
/// @return Bounded setup, action, and completion counts.
213
12.1k
MultiTransferRunStats RunMultiTransferScenario(const curl::fuzzer::proto::Scenario& scenario) {
214
12.1k
  MultiTransferRunStats stats;
215
12.1k
  const auto& plan = scenario.multi_plan();
216
12.1k
  const std::size_t transfer_count = RuntimeTransferCount(plan);
217
12.1k
  stats.configured_handles = transfer_count;
218
219
12.1k
  FilePtr progress_sink;
220
12.1k
  if (plan.state_probe_profile() == curl::fuzzer::proto::MULTI_STATE_PROBE_PROGRESS_RATE_LIMIT) {
221
1.56k
    progress_sink.reset(std::tmpfile());
222
1.56k
  }
223
224
  // The peer and socket callback state must outlive multi cleanup, which can
225
  // synchronously announce socket removals and close cached connections.
226
12.1k
  MockServer mock;
227
12.1k
  mock.SetScripts(scenario);
228
12.1k
  mock.SetKeepConnectionsOpen(plan.keep_connections_open());
229
12.1k
  MultiSocketDriver socket_driver;
230
12.1k
  CurlMultiPtr multi(curl_multi_init());
231
12.1k
  if (!multi) {
232
58
    return stats;
233
58
  }
234
235
12.1k
  const long max_host_connections =
236
12.1k
      static_cast<long>(std::min<std::size_t>(plan.max_host_connections(), transfer_count));
237
12.1k
  const long max_total_connections =
238
12.1k
      static_cast<long>(std::min<std::size_t>(plan.max_total_connections(), transfer_count));
239
12.1k
  const long cache_size =
240
12.1k
      static_cast<long>(std::min<std::size_t>(plan.connection_cache_size(), scenario_limits::kMaxMultiTransfers * 2));
241
12.1k
  (void)curl_multi_setopt(multi.get(), CURLMOPT_MAX_HOST_CONNECTIONS, max_host_connections);
242
12.1k
  (void)curl_multi_setopt(multi.get(), CURLMOPT_MAX_TOTAL_CONNECTIONS, max_total_connections);
243
12.1k
  (void)curl_multi_setopt(multi.get(), CURLMOPT_MAXCONNECTS, cache_size);
244
12.1k
  (void)curl_multi_setopt(multi.get(), CURLMOPT_PIPELINING,
245
12.1k
                          plan.multiplex() ? static_cast<long>(CURLPIPE_MULTIPLEX) : 0L);
246
12.1k
  (void)curl_multi_setopt(multi.get(), CURLMOPT_NOTIFYFUNCTION, &RecordNotification);
247
12.1k
  (void)curl_multi_setopt(multi.get(), CURLMOPT_NOTIFYDATA, &stats);
248
12.1k
  (void)curl_multi_notify_enable(multi.get(), CURLMNOTIFY_INFO_READ);
249
12.1k
  (void)curl_multi_notify_enable(multi.get(), CURLMNOTIFY_EASY_DONE);
250
12.1k
  (void)curl_multi_notify_disable(multi.get(), CURLMNOTIFY_INFO_READ);
251
12.1k
  (void)curl_multi_notify_enable(multi.get(), CURLMNOTIFY_INFO_READ);
252
12.1k
  (void)curl_multi_notify_disable(multi.get(), CURLMNOTIFY_EASY_DONE);
253
12.1k
  (void)curl_multi_notify_enable(multi.get(), CURLMNOTIFY_EASY_DONE);
254
255
12.1k
  const bool socket_mode = plan.drive_mode() == curl::fuzzer::proto::MULTI_DRIVE_SOCKET;
256
12.1k
  const bool socket_driver_installed = socket_mode && socket_driver.Install(multi.get());
257
12.1k
  std::array<TransferState, scenario_limits::kMaxMultiTransfers> transfers;
258
12.1k
  const std::string url = "http://" + scenario.host_path();
259
50.4k
  for (std::size_t index = 0; index < transfer_count; ++index) {
260
38.3k
    TransferState& transfer = transfers[index];
261
38.3k
    if (!transfer.session.Initialize()) {
262
605
      continue;
263
605
    }
264
37.7k
    transfer.session.ApplyBaseline(scenario);
265
37.7k
    (void)curl_easy_setopt(transfer.session.easy(), CURLOPT_URL, url.c_str());
266
37.7k
    mock.Install(transfer.session.easy());
267
37.7k
    (void)ApplyScenarioOptions(transfer.session.easy(), scenario);
268
37.7k
    if (ApplyStateProbeOptions(transfer.session.easy(), plan.state_probe_profile(), progress_sink.get())) {
269
5.45k
      ++stats.state_probe_handles;
270
5.45k
    }
271
37.7k
    mock.ConfigureRequestData(transfer.session.InstallRequestData(scenario));
272
37.7k
    if (curl_multi_add_handle(multi.get(), transfer.session.easy()) == CURLM_OK) {
273
37.7k
      transfer.attached = true;
274
37.7k
      ++stats.added_handles;
275
37.7k
    }
276
37.7k
  }
277
278
12.1k
  if (stats.added_handles == 0) {
279
2
    multi.reset();
280
2
    return stats;
281
2
  }
282
283
12.1k
  ProbeLegacyMultiSocketApis(multi.get());
284
285
12.1k
  const std::size_t action_count = std::min<std::size_t>(plan.actions_size(), scenario_limits::kMaxMultiActions);
286
12.1k
  std::size_t next_action = 0;
287
12.1k
  bool initial_action_changed = false;
288
12.1k
  if (next_action < action_count) {
289
3.06k
    initial_action_changed =
290
3.06k
        ApplyAction(plan.actions(static_cast<int>(next_action++)), multi.get(), &transfers, transfer_count);
291
3.06k
    ++stats.actions_consumed;
292
3.06k
  }
293
294
12.1k
  int running_handles = 0;
295
12.1k
  CURLMcode result = socket_driver_installed ? socket_driver.Start(&running_handles)
296
12.1k
                                             : curl_multi_perform(multi.get(), &running_handles);
297
12.1k
  bool completion_progress = DrainCompletionMessages(multi.get(), &transfers, transfer_count, &stats);
298
12.1k
  ProbeMultiWaitApis(multi.get());
299
12.1k
  if (plan.wake_multi()) {
300
842
    if (socket_driver_installed) {
301
411
      socket_driver.ProbeControlApis();
302
431
    } else {
303
431
      (void)curl_multi_wakeup(multi.get());
304
431
    }
305
842
  }
306
307
12.1k
  int idle_iterations = initial_action_changed || completion_progress ? 0 : 1;
308
167k
  for (int iteration = 0; result == CURLM_OK && iteration < kMaxDriveIterations; ++iteration) {
309
166k
    if (running_handles == 0 && next_action >= action_count) {
310
9.55k
      break;
311
9.55k
    }
312
313
157k
    bool made_progress = mock.ServiceConnections();
314
157k
    if (next_action < action_count) {
315
17.9k
      made_progress =
316
17.9k
          ApplyAction(plan.actions(static_cast<int>(next_action++)), multi.get(), &transfers, transfer_count) ||
317
7.20k
          made_progress;
318
17.9k
      ++stats.actions_consumed;
319
      // Consuming an ordered no-op is still harness progress: continue to the
320
      // next action instead of letting an idle transport hide its suffix.
321
17.9k
      made_progress = true;
322
17.9k
    }
323
324
157k
    const int running_before = running_handles;
325
157k
    if (socket_driver_installed) {
326
121k
      const MultiSocketDriver::DriveResult drive = socket_driver.DriveReady(&running_handles);
327
121k
      result = drive.code;
328
121k
      made_progress = drive.made_progress || made_progress;
329
121k
    } else {
330
36.3k
      result = curl_multi_perform(multi.get(), &running_handles);
331
36.3k
      made_progress = running_handles != running_before || made_progress;
332
36.3k
    }
333
157k
    made_progress = DrainCompletionMessages(multi.get(), &transfers, transfer_count, &stats) || made_progress;
334
335
157k
    if (made_progress) {
336
138k
      idle_iterations = 0;
337
138k
    } else if (++idle_iterations >= kMaxIdleIterations) {
338
2.37k
      break;
339
2.37k
    }
340
157k
  }
341
342
12.1k
  (void)DrainCompletionMessages(multi.get(), &transfers, transfer_count, &stats);
343
12.1k
  stats.opened_connections = mock.opened_connection_count();
344
50.4k
  for (std::size_t index = 0; index < transfer_count; ++index) {
345
38.3k
    TransferState& transfer = transfers[index];
346
38.3k
    if (transfer.session.easy() == nullptr) {
347
601
      continue;
348
601
    }
349
37.7k
    (void)curl_easy_pause(transfer.session.easy(), CURLPAUSE_CONT);
350
37.7k
    if (transfer.attached) {
351
36.9k
      (void)curl_multi_remove_handle(multi.get(), transfer.session.easy());
352
36.9k
      transfer.attached = false;
353
36.9k
    }
354
37.7k
  }
355
356
  // Destroy the multi while both its socket callback storage and mock peer are
357
  // live, then detach request-data pointers before cleaning each easy handle.
358
12.1k
  multi.reset();
359
50.4k
  for (std::size_t index = 0; index < transfer_count; ++index) {
360
38.3k
    transfers[index].session.Close();
361
38.3k
  }
362
12.1k
  return stats;
363
12.1k
}
364
365
}  // namespace proto_fuzzer