Coverage Report

Created: 2026-10-15 06:25

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