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_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
443k
short PollEventsForInterest(int interest) {
24
443k
  short events = 0;
25
443k
  if ((interest & CURL_POLL_IN) != 0) {
26
437k
    events |= POLLIN;
27
437k
  }
28
443k
  if ((interest & CURL_POLL_OUT) != 0) {
29
115k
    events |= POLLOUT;
30
115k
  }
31
443k
  return events;
32
443k
}
33
34
/// Translate an observed poll result into curl_multi_socket_action flags.
35
297k
int CurlEventsForPollResult(short events) {
36
297k
  int result = 0;
37
297k
  if ((events & (POLLIN | POLLHUP)) != 0) {
38
    // A stream hangup remains readable until curl consumes EOF.
39
39.0k
    result |= CURL_CSELECT_IN;
40
39.0k
  }
41
297k
  if ((events & POLLOUT) != 0) {
42
113k
    result |= CURL_CSELECT_OUT;
43
113k
  }
44
297k
  if ((events & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
45
0
    result |= CURL_CSELECT_ERR;
46
0
  }
47
297k
  return result;
48
297k
}
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
345k
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
345k
MultiSocketDriver::~MultiSocketDriver() = default;
59
60
/// Install socket/timer callbacks on a caller-owned multi before adding an
61
/// easy handle. curl may announce a timer during curl_multi_add_handle, so
62
/// installing later would miss the event that starts an otherwise idle
63
/// socket-action application.
64
/// @param multi Multi handle that outlives this driver.
65
/// @return true when every callback option was accepted.
66
6.25k
bool MultiSocketDriver::Install(CURLM* multi) {
67
6.25k
  multi_ = multi;
68
6.25k
  if (multi_ == nullptr) {
69
0
    return false;
70
0
  }
71
6.25k
  return curl_multi_setopt(multi_, CURLMOPT_SOCKETFUNCTION, &MultiSocketDriver::SocketCallback) == CURLM_OK &&
72
6.25k
         curl_multi_setopt(multi_, CURLMOPT_SOCKETDATA, this) == CURLM_OK &&
73
6.25k
         curl_multi_setopt(multi_, CURLMOPT_TIMERFUNCTION, &MultiSocketDriver::TimerCallback) == CURLM_OK &&
74
6.25k
         curl_multi_setopt(multi_, CURLMOPT_TIMERDATA, this) == CURLM_OK;
75
6.25k
}
76
77
/// Start libcurl's socket state machine through the documented timeout
78
/// pseudo-socket without waiting for a real timer. This creates the first
79
/// socket and gives the callback its initial watch without a wall-clock
80
/// dependency.
81
/// @param running_handles Receives the number of unfinished transfers.
82
/// @return Result from curl_multi_socket_action.
83
6.25k
CURLMcode MultiSocketDriver::Start(int* running_handles) {
84
6.25k
  if (multi_ == nullptr) {
85
0
    return CURLM_BAD_HANDLE;
86
0
  }
87
6.25k
  timer_pending_ = false;
88
6.25k
  return curl_multi_socket_action(multi_, CURL_SOCKET_TIMEOUT, 0, running_handles);
89
6.25k
}
90
91
/// Run one bounded, non-blocking event-loop turn: poll the callback-provided
92
/// watch set with a zero timeout and report readiness back to libcurl.
93
/// The snapshot is intentional: a socket action may synchronously remove
94
/// the current watch or install another one, so the callback-owned table
95
/// must not be iterated as a live container across that call. Service a
96
/// deferred zero timer after socket callbacks, never recursively from the
97
/// timer callback.
98
/// @param running_handles In/out unfinished-transfer count.
99
/// @return Result code and whether observable driver state advanced.
100
218k
MultiSocketDriver::DriveResult MultiSocketDriver::DriveReady(int* running_handles) {
101
218k
  DriveResult result;
102
218k
  if (multi_ == nullptr) {
103
0
    result.code = CURLM_BAD_HANDLE;
104
0
    return result;
105
0
  }
106
107
218k
  std::array<struct pollfd, kMaxWatches> poll_fds{};
108
218k
  std::size_t poll_count = 0;
109
3.49M
  for (const Watch& watch : watches_) {
110
3.49M
    if (!watch.active) {
111
3.05M
      continue;
112
3.05M
    }
113
443k
    poll_fds[poll_count].fd = watch.socket;
114
443k
    poll_fds[poll_count].events = PollEventsForInterest(watch.interest);
115
443k
    ++poll_count;
116
443k
  }
117
118
218k
  const std::uint64_t generation_before = generation_;
119
218k
  const int running_before = running_handles == nullptr ? 0 : *running_handles;
120
218k
  bool action_dispatched = false;
121
218k
  if (poll_count != 0 && ::poll(poll_fds.data(), static_cast<nfds_t>(poll_count), 0) > 0) {
122
297k
    for (std::size_t index = 0; index < poll_count; ++index) {
123
297k
      if (running_handles != nullptr && *running_handles == 0) {
124
101
        break;
125
101
      }
126
      // An earlier action can synchronously remove or repurpose any later fd
127
      // in the snapshot. Revalidate it against the callback-owned table so a
128
      // stale readiness notification never reaches curl under a new meaning.
129
297k
      Watch* current = FindWatch(poll_fds[index].fd);
130
297k
      if (current == nullptr) {
131
0
        continue;
132
0
      }
133
297k
      const int events = CurlEventsForPollResult(poll_fds[index].revents);
134
297k
      if (events == 0) {
135
151k
        continue;
136
151k
      }
137
146k
      result.code = curl_multi_socket_action(multi_, poll_fds[index].fd, events, running_handles);
138
146k
      action_dispatched = true;
139
      // Process at most one snapshot entry. Its callbacks can repurpose an fd
140
      // with the same numeric value and interest, which no post-hoc lookup can
141
      // distinguish; the outer loop rebuilds readiness from fresh watches.
142
146k
      break;
143
297k
    }
144
146k
  }
145
146
218k
  if (result.code == CURLM_OK && timer_pending_ && timeout_ms_ == 0) {
147
    // Clear first: the action may synchronously install another zero timer,
148
    // which belongs to the next outer loop turn rather than recursive work.
149
997
    timer_pending_ = false;
150
997
    result.code = curl_multi_socket_action(multi_, CURL_SOCKET_TIMEOUT, 0, running_handles);
151
997
    action_dispatched = true;
152
997
  }
153
154
218k
  const int running_after = running_handles == nullptr ? 0 : *running_handles;
155
  // A successful action can consume buffered protocol bytes without changing
156
  // either callbacks or handle count, so dispatch itself is observable
157
  // progress. The outer fixed operation cap still bounds permanently-ready
158
  // sockets.
159
218k
  result.made_progress = action_dispatched || generation_ != generation_before || running_after != running_before;
160
218k
  return result;
161
218k
}
162
163
/// Exercise the wakeup and timeout-query APIs from a valid live-multi state.
164
/// A zero-timeout query is informational; wakeup is also non-blocking when
165
/// no other thread is in a poll call, which is the deterministic behavior
166
/// this lane needs. Neither call waits or changes the operation budget.
167
80
void MultiSocketDriver::ProbeControlApis() {
168
80
  if (multi_ == nullptr) {
169
0
    return;
170
0
  }
171
80
  long timeout_ms = -1;
172
80
  (void)curl_multi_timeout(multi_, &timeout_ms);
173
80
  (void)curl_multi_wakeup(multi_);
174
80
}
175
176
/// Trampoline registered as CURLMOPT_SOCKETFUNCTION. Route the C callback
177
/// into state whose lifetime is owned by DriveScenario.
178
int MultiSocketDriver::SocketCallback(CURL* /*easy*/, curl_socket_t socket, int what, void* user_data,
179
30.5k
                                      void* socket_data) {
180
30.5k
  return static_cast<MultiSocketDriver*>(user_data)->UpdateSocket(socket, what, socket_data);
181
30.5k
}
182
183
/// Trampoline registered as CURLMOPT_TIMERFUNCTION. Record work for deferred
184
/// processing only: invoking curl_multi_socket_action here, including for
185
/// a zero timer, would recursively re-enter libcurl's callback API.
186
43.9k
int MultiSocketDriver::TimerCallback(CURLM* /*multi*/, long timeout_ms, void* user_data) {
187
43.9k
  return static_cast<MultiSocketDriver*>(user_data)->UpdateTimer(timeout_ms);
188
43.9k
}
189
190
/// Apply one socket callback transition to the fixed watch table, maintaining
191
/// a stable association for every observed fd. `socket_data` is used only as
192
/// a consistency hint: libcurl owns it and may legitimately pass null for
193
/// the first notification, while our fd lookup remains authoritative.
194
30.5k
int MultiSocketDriver::UpdateSocket(curl_socket_t socket, int what, void* socket_data) {
195
30.5k
  Watch* watch = FindWatch(socket);
196
30.5k
  if (what == CURL_POLL_REMOVE) {
197
14.6k
    if (watch != nullptr) {
198
14.6k
      (void)curl_multi_assign(multi_, socket, nullptr);
199
14.6k
      watch->active = false;
200
14.6k
      watch->socket = CURL_SOCKET_BAD;
201
14.6k
      watch->interest = CURL_POLL_NONE;
202
14.6k
      ++generation_;
203
14.6k
    }
204
14.6k
    return 0;
205
14.6k
  }
206
207
15.9k
  if (watch == nullptr) {
208
14.6k
    watch = FindFreeWatch();
209
14.6k
    if (watch == nullptr) {
210
0
      return 0;
211
0
    }
212
14.6k
    watch->socket = socket;
213
14.6k
    watch->active = true;
214
14.6k
    (void)curl_multi_assign(multi_, socket, watch);
215
14.6k
    ++generation_;
216
14.6k
  } else if (socket_data != nullptr && socket_data != watch) {
217
    // Reassert the stable association if an unusual transition supplied a
218
    // different application pointer. Never dereference foreign socket_data.
219
0
    (void)curl_multi_assign(multi_, socket, watch);
220
0
  }
221
222
15.9k
  if (watch->interest != what) {
223
15.9k
    watch->interest = what;
224
15.9k
    ++generation_;
225
15.9k
  }
226
15.9k
  return 0;
227
15.9k
}
228
229
/// Record libcurl's latest timer request for deferred processing. Only
230
/// meaningful timer transitions count as progress: repeated identical
231
/// callbacks could otherwise consume the full operation budget instead
232
/// of the much smaller idle budget.
233
43.9k
int MultiSocketDriver::UpdateTimer(long timeout_ms) {
234
43.9k
  if (!timer_pending_ || timeout_ms_ != timeout_ms) {
235
29.2k
    ++generation_;
236
29.2k
  }
237
43.9k
  timeout_ms_ = timeout_ms;
238
43.9k
  timer_pending_ = timeout_ms >= 0;
239
43.9k
  return 0;
240
43.9k
}
241
242
/// Find an active watch by fd without allocating or depending on fd
243
/// magnitude; socket descriptors are not safe array indexes.
244
328k
MultiSocketDriver::Watch* MultiSocketDriver::FindWatch(curl_socket_t socket) {
245
728k
  for (Watch& watch : watches_) {
246
728k
    if (watch.active && watch.socket == socket) {
247
313k
      return &watch;
248
313k
    }
249
728k
  }
250
14.6k
  return nullptr;
251
328k
}
252
253
/// Find the first inactive stable slot for a newly observed fd. Exhaustion
254
/// is harmless: curl keeps owning the socket and the deterministic idle
255
/// budget ends the fuzz case.
256
14.6k
MultiSocketDriver::Watch* MultiSocketDriver::FindFreeWatch() {
257
23.4k
  for (Watch& watch : watches_) {
258
23.4k
    if (!watch.active) {
259
14.6k
      return &watch;
260
14.6k
    }
261
23.4k
  }
262
0
  return nullptr;
263
14.6k
}
264
265
}  // namespace proto_fuzzer