Coverage Report

Created: 2026-09-01 07:00

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
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