Coverage Report

Created: 2026-09-04 07: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
27.5k
short PollEventsForInterest(int interest) {
24
27.5k
  short events = 0;
25
27.5k
  if ((interest & CURL_POLL_IN) != 0) {
26
25.8k
    events |= POLLIN;
27
25.8k
  }
28
27.5k
  if ((interest & CURL_POLL_OUT) != 0) {
29
8.95k
    events |= POLLOUT;
30
8.95k
  }
31
27.5k
  return events;
32
27.5k
}
33
34
/// Translate an observed poll result into curl_multi_socket_action flags.
35
25.3k
int CurlEventsForPollResult(short events) {
36
25.3k
  int result = 0;
37
25.3k
  if ((events & (POLLIN | POLLHUP)) != 0) {
38
    // A stream hangup remains readable until curl consumes EOF.
39
5.28k
    result |= CURL_CSELECT_IN;
40
5.28k
  }
41
25.3k
  if ((events & POLLOUT) != 0) {
42
8.17k
    result |= CURL_CSELECT_OUT;
43
8.17k
  }
44
25.3k
  if ((events & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
45
0
    result |= CURL_CSELECT_ERR;
46
0
  }
47
25.3k
  return result;
48
25.3k
}
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
135k
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
135k
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
1.05k
bool MultiSocketDriver::Install(CURLM* multi) {
64
1.05k
  multi_ = multi;
65
1.05k
  if (multi_ == nullptr) {
66
0
    return false;
67
0
  }
68
1.05k
  return curl_multi_setopt(multi_, CURLMOPT_SOCKETFUNCTION, &MultiSocketDriver::SocketCallback) == CURLM_OK &&
69
1.05k
         curl_multi_setopt(multi_, CURLMOPT_SOCKETDATA, this) == CURLM_OK &&
70
1.05k
         curl_multi_setopt(multi_, CURLMOPT_TIMERFUNCTION, &MultiSocketDriver::TimerCallback) == CURLM_OK &&
71
1.05k
         curl_multi_setopt(multi_, CURLMOPT_TIMERDATA, this) == CURLM_OK;
72
1.05k
}
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
1.05k
CURLMcode MultiSocketDriver::Start(int* running_handles) {
78
1.05k
  if (multi_ == nullptr) {
79
0
    return CURLM_BAD_HANDLE;
80
0
  }
81
1.05k
  timer_pending_ = false;
82
1.05k
  return curl_multi_socket_action(multi_, CURL_SOCKET_TIMEOUT, 0, running_handles);
83
1.05k
}
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
13.7k
MultiSocketDriver::DriveResult MultiSocketDriver::DriveReady(int* running_handles) {
90
13.7k
  DriveResult result;
91
13.7k
  if (multi_ == nullptr) {
92
0
    result.code = CURLM_BAD_HANDLE;
93
0
    return result;
94
0
  }
95
96
13.7k
  std::array<struct pollfd, kMaxWatches> poll_fds{};
97
13.7k
  std::size_t poll_count = 0;
98
220k
  for (const Watch& watch : watches_) {
99
220k
    if (!watch.active) {
100
192k
      continue;
101
192k
    }
102
27.5k
    poll_fds[poll_count].fd = watch.socket;
103
27.5k
    poll_fds[poll_count].events = PollEventsForInterest(watch.interest);
104
27.5k
    ++poll_count;
105
27.5k
  }
106
107
13.7k
  const std::uint64_t generation_before = generation_;
108
13.7k
  const int running_before = running_handles == nullptr ? 0 : *running_handles;
109
13.7k
  bool action_dispatched = false;
110
13.7k
  if (poll_count != 0 && ::poll(poll_fds.data(), static_cast<nfds_t>(poll_count), 0) > 0) {
111
25.3k
    for (std::size_t index = 0; index < poll_count; ++index) {
112
25.3k
      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
25.3k
      Watch* current = FindWatch(poll_fds[index].fd);
119
25.3k
      if (current == nullptr) {
120
0
        continue;
121
0
      }
122
25.3k
      const int events = CurlEventsForPollResult(poll_fds[index].revents);
123
25.3k
      if (events == 0) {
124
12.7k
        continue;
125
12.7k
      }
126
12.6k
      result.code = curl_multi_socket_action(multi_, poll_fds[index].fd, events, running_handles);
127
12.6k
      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
12.6k
      break;
132
25.3k
    }
133
12.6k
  }
134
135
13.7k
  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
164
    timer_pending_ = false;
139
164
    result.code = curl_multi_socket_action(multi_, CURL_SOCKET_TIMEOUT, 0, running_handles);
140
164
    action_dispatched = true;
141
164
  }
142
143
13.7k
  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
13.7k
  result.made_progress = action_dispatched || generation_ != generation_before || running_after != running_before;
149
13.7k
  return result;
150
13.7k
}
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
204
void MultiSocketDriver::ProbeControlApis() {
156
204
  if (multi_ == nullptr) {
157
0
    return;
158
0
  }
159
204
  long timeout_ms = -1;
160
204
  (void)curl_multi_timeout(multi_, &timeout_ms);
161
204
  (void)curl_multi_wakeup(multi_);
162
204
}
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
5.76k
                                      void* socket_data) {
167
5.76k
  return static_cast<MultiSocketDriver*>(user_data)->UpdateSocket(socket, what, socket_data);
168
5.76k
}
169
170
/// Defer timer processing so a zero timer cannot recursively call back into
171
/// curl_multi_socket_action from inside libcurl.
172
4.61k
int MultiSocketDriver::TimerCallback(CURLM* /*multi*/, long timeout_ms, void* user_data) {
173
4.61k
  return static_cast<MultiSocketDriver*>(user_data)->UpdateTimer(timeout_ms);
174
4.61k
}
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
5.76k
int MultiSocketDriver::UpdateSocket(curl_socket_t socket, int what, void* socket_data) {
180
5.76k
  Watch* watch = FindWatch(socket);
181
5.76k
  if (what == CURL_POLL_REMOVE) {
182
2.75k
    if (watch != nullptr) {
183
2.75k
      (void)curl_multi_assign(multi_, socket, nullptr);
184
2.75k
      watch->active = false;
185
2.75k
      watch->socket = CURL_SOCKET_BAD;
186
2.75k
      watch->interest = CURL_POLL_NONE;
187
2.75k
      ++generation_;
188
2.75k
    }
189
2.75k
    return 0;
190
2.75k
  }
191
192
3.00k
  if (watch == nullptr) {
193
2.75k
    watch = FindFreeWatch();
194
2.75k
    if (watch == nullptr) {
195
0
      return 0;
196
0
    }
197
2.75k
    watch->socket = socket;
198
2.75k
    watch->active = true;
199
2.75k
    (void)curl_multi_assign(multi_, socket, watch);
200
2.75k
    ++generation_;
201
2.75k
  } 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
3.00k
  if (watch->interest != what) {
208
3.00k
    watch->interest = what;
209
3.00k
    ++generation_;
210
3.00k
  }
211
3.00k
  return 0;
212
3.00k
}
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
4.61k
int MultiSocketDriver::UpdateTimer(long timeout_ms) {
218
4.61k
  if (!timer_pending_ || timeout_ms_ != timeout_ms) {
219
3.39k
    ++generation_;
220
3.39k
  }
221
4.61k
  timeout_ms_ = timeout_ms;
222
4.61k
  timer_pending_ = timeout_ms >= 0;
223
4.61k
  return 0;
224
4.61k
}
225
226
/// Locate an existing association without allocating or depending on fd
227
/// magnitude (socket descriptors are not safe array indexes).
228
31.1k
MultiSocketDriver::Watch* MultiSocketDriver::FindWatch(curl_socket_t socket) {
229
87.5k
  for (Watch& watch : watches_) {
230
87.5k
    if (watch.active && watch.socket == socket) {
231
28.3k
      return &watch;
232
28.3k
    }
233
87.5k
  }
234
2.75k
  return nullptr;
235
31.1k
}
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
2.75k
MultiSocketDriver::Watch* MultiSocketDriver::FindFreeWatch() {
240
4.52k
  for (Watch& watch : watches_) {
241
4.52k
    if (!watch.active) {
242
2.75k
      return &watch;
243
2.75k
    }
244
4.52k
  }
245
0
  return nullptr;
246
2.75k
}
247
248
}  // namespace proto_fuzzer