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/mock_server_base.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 MockServerBase — shared trampolines, shared
9
///        select() helper, DriveScenario multi-handle RAII, and the scheme
10
///        classifier that produces the right subclass for a Scenario.
11
12
#include "proto_fuzzer/mock_server_base.h"
13
14
#include <curl/multi.h>
15
#include <sys/select.h>
16
17
#include "proto_fuzzer/curl_raii.h"
18
#include "proto_fuzzer/mock_server.h"
19
#include "proto_fuzzer/multi_socket_driver.h"
20
#include "proto_fuzzer/scenario_limits.h"
21
22
namespace proto_fuzzer {
23
24
namespace {
25
26
constexpr long kSelectTimeoutUs = 1000;  // 1 ms; explicit timing cases only.
27
28
/// Enumerate the attached handles despite the scenario's bounded allocation
29
/// failures. The parent remains attached, so a null result means the array
30
/// allocation failed and is safe to retry. One final attempt follows the
31
/// largest possible run of injected failures.
32
/// @param multi Live multi handle whose attached easies are required.
33
/// @return Caller-owned, null-terminated handle array, or nullptr after all
34
///         bounded attempts fail.
35
322k
CURL** GetHandlesForCleanup(CURLM* multi) {
36
322k
  constexpr std::size_t kMaxAttempts = scenario_limits::kMaxAllocationFailures + 1;
37
322k
  for (std::size_t attempt = 0; attempt < kMaxAttempts; ++attempt) {
38
322k
    CURL** handles = curl_multi_get_handles(multi);
39
322k
    if (handles != nullptr) {
40
322k
      return handles;
41
322k
    }
42
322k
  }
43
0
  return nullptr;
44
322k
}
45
46
}  // namespace
47
48
/// @brief C trampoline for CURLOPT_OPENSOCKETFUNCTION. Declared at namespace
49
///        scope so it can be a friend of MockServerBase.
50
/// @param clientp Pointer to the MockServerBase instance.
51
/// @param purpose The socket role curl is asking the mock to provide.
52
/// @param address Curl's mutable description of the intended destination.
53
/// @return The client-side socket fd as a curl_socket_t.
54
447k
curl_socket_t MockServerBaseOpenSocketTrampoline(void* clientp, curlsocktype purpose, struct curl_sockaddr* address) {
55
447k
  return static_cast<MockServerBase*>(clientp)->HandleOpenSocket(purpose, address);
56
447k
}
57
58
/// Translate explicit peer transport metadata into curl's sockopt callback
59
/// contract. The descriptor is used only as an opaque identity; sandbox
60
/// policy must not decide whether an otherwise valid in-process transport can
61
/// run.
62
/// @param clientp Pointer to the MockServerBase instance.
63
/// @param curlfd Descriptor returned by the open-socket callback.
64
/// @param purpose Socket role assigned by curl.
65
/// @return CURL_SOCKOPT_ALREADY_CONNECTED for prepared stream peers, otherwise
66
///         CURL_SOCKOPT_OK.
67
404k
int MockServerBaseSockOptTrampoline(void* clientp, curl_socket_t curlfd, curlsocktype purpose) {
68
404k
  const auto disposition = static_cast<MockServerBase*>(clientp)->GetSocketSetupDisposition(curlfd, purpose);
69
404k
  return disposition == SocketSetupDisposition::kAlreadyConnected ? CURL_SOCKOPT_ALREADY_CONNECTED : CURL_SOCKOPT_OK;
70
404k
}
71
72
/// Construct an empty base instance with a fixed multi-drive policy.
73
/// @param drive_policy Whether to perform, use socket actions, or honor the
74
///        containing Scenario's ApiPlan.
75
MockServerBase::MockServerBase(MultiDrivePolicy drive_policy)
76
348k
    : connection_(nullptr),
77
348k
      pending_recv_buf_bytes_(0),
78
348k
      pending_drain_limit_(0),
79
348k
      multi_socket_driver_(nullptr),
80
348k
      additional_handle_cleanup_count_(0),
81
348k
      resume_response_(false),
82
348k
      drive_policy_(drive_policy) {}
83
84
/// Out-of-line destructor so MockConnection can stay forward-declared in the
85
/// base header (its complete type is only needed where unique_ptr is
86
/// instantiated for destruction).
87
348k
MockServerBase::~MockServerBase() = default;
88
89
/// @return the owned MockConnection, or nullptr if one has not been opened.
90
1.59M
MockConnection* MockServerBase::connection() { return connection_.get(); }
91
92
/// Install the common OPENSOCKETFUNCTION / OPENSOCKETDATA / SOCKOPTFUNCTION
93
/// callbacks on 'easy'. All subclasses share the trampolines, which route
94
/// back into this instance through HandleOpenSocket. Subclasses may override
95
/// to layer additional protocol-specific setopts, such as a WRITEFUNCTION
96
/// that exercises protocol APIs from inside a curl callback.
97
/// @param easy The curl easy handle to configure.
98
395k
void MockServerBase::Install(CURL* easy) {
99
395k
  curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, &MockServerBaseOpenSocketTrampoline);
100
395k
  curl_easy_setopt(easy, CURLOPT_OPENSOCKETDATA, this);
101
395k
  curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, &MockServerBaseSockOptTrampoline);
102
395k
  curl_easy_setopt(easy, CURLOPT_SOCKOPTDATA, this);
103
395k
}
104
105
/// Describe the socket returned by HandleOpenSocket without issuing native
106
/// descriptor queries. Stream peers return connected socketpairs; accepted
107
/// sockets and datagram peers require curl's ordinary setup.
108
/// @param curlfd Descriptor returned by HandleOpenSocket.
109
/// @param purpose Role curl assigned to the descriptor.
110
/// @return Whether curl must perform its normal socket setup.
111
385k
SocketSetupDisposition MockServerBase::GetSocketSetupDisposition(curl_socket_t /*curlfd*/, curlsocktype purpose) const {
112
  // curl itself accepted CURLSOCKTYPE_ACCEPT descriptors, so they must follow
113
  // its normal post-accept option path. Every IPCXN socket returned by the
114
  // ordinary stream mocks is one end of an already-connected socketpair.
115
385k
  return purpose == CURLSOCKTYPE_ACCEPT ? SocketSetupDisposition::kNeedsSetup
116
385k
                                        : SocketSetupDisposition::kAlreadyConnected;
117
385k
}
118
119
/// Bind protocol-specific work to the request-data callbacks after their
120
/// per-scenario state has been constructed. Ordinary HTTP and WebSocket
121
/// mocks regain control in their outer perform loops and drain client
122
/// traffic there. Most mocks therefore need no hook; TELNET uses this
123
/// upload-callback boundary to drain client replies while curl owns the
124
/// thread.
125
/// @param request_data Callback state that outlives the subsequent drive.
126
387k
void MockServerBase::ConfigureRequestData(ScenarioRequestData* /*request_data*/) {}
127
128
/// Run 'scenario' to completion on 'easy': allocate a multi, attach 'easy',
129
/// delegate protocol-specific work to RunLoop, consume its completion
130
/// message, and clean up. Read the result before removing the only easy
131
/// handle so every protocol runner exercises the public multi-result path.
132
/// Harness setup failures return a stable sentinel; fuzzer callers may
133
/// ignore it while unit tests can assert the protocol result without adding
134
/// another callback or global.
135
/// @param easy     curl easy handle already Install()ed on this mock.
136
/// @param scenario the Scenario proto to drive.
137
/// @return the completed transfer's CURLcode, or CURLE_FAILED_INIT when the
138
/// bounded drive could not produce a completion message.
139
323k
CURLcode MockServerBase::DriveScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
140
323k
  const curl::fuzzer::proto::ApiPlan* api_plan =
141
323k
      drive_policy_ == MultiDrivePolicy::kFromApiPlan && scenario.has_api_plan() ? &scenario.api_plan() : nullptr;
142
323k
  const bool use_multi_socket =
143
323k
      drive_policy_ == MultiDrivePolicy::kSocketAction ||
144
318k
      (api_plan != nullptr && api_plan->drive_mode() == curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET);
145
146
  // Cache backpressure knobs so HandleOpenSocket can apply them the moment
147
  // connection_ exists. Both default to 0, which matches the legacy "drain
148
  // greedily, kernel-default buffers" behaviour exactly.
149
323k
  const auto& bp = scenario.connection().backpressure();
150
323k
  pending_recv_buf_bytes_ = static_cast<int>(bp.recv_buf_bytes());
151
323k
  pending_drain_limit_ = static_cast<std::size_t>(bp.drain_limit());
152
323k
  additional_handle_cleanup_count_ = 0;
153
323k
  resume_response_ = api_plan != nullptr && api_plan->pause_response_once();
154
155
323k
  CurlMultiPtr multi(curl_multi_init());
156
323k
  if (multi == nullptr) {
157
871
    resume_response_ = false;
158
871
    return CURLE_FAILED_INIT;
159
871
  }
160
161
322k
  CURLcode transfer_result = CURLE_FAILED_INIT;
162
163
  // Callback data must survive both remove_handle and multi_cleanup, since
164
  // either may emit CURL_POLL_REMOVE. Keeping it in this outer scope provides
165
  // that lifetime without allocating per-watch state.
166
322k
  MultiSocketDriver socket_driver;
167
322k
  if (use_multi_socket && socket_driver.Install(multi.get())) {
168
5.11k
    multi_socket_driver_ = &socket_driver;
169
5.11k
  }
170
322k
  if (curl_multi_add_handle(multi.get(), easy) == CURLM_OK) {
171
322k
    if (multi_observer_) {
172
4.45k
      multi_observer_(multi.get());
173
4.45k
    }
174
322k
    if (api_plan != nullptr && api_plan->wake_multi()) {
175
54
      if (multi_socket_driver_ != nullptr) {
176
20
        multi_socket_driver_->ProbeControlApis();
177
34
      } else {
178
34
        long timeout_ms = -1;
179
34
        (void)curl_multi_timeout(multi.get(), &timeout_ms);
180
34
        (void)curl_multi_wakeup(multi.get());
181
34
      }
182
54
    }
183
322k
    RunLoop(multi.get(), easy, scenario);
184
185
    // Completion messages are the multi API's only durable record of the
186
    // transfer result. Consume them while all easy handles are still attached:
187
    // otherwise every scenario systematically skips curl_multi_info_read's
188
    // result path and removal discards the opportunity. An accepted HTTP/2
189
    // push can contribute one additional completion message; only the caller's
190
    // parent determines DriveScenario's result.
191
322k
    int messages_remaining = 0;
192
322k
    CURLMsg* message = nullptr;
193
589k
    while ((message = curl_multi_info_read(multi.get(), &messages_remaining)) != nullptr) {
194
267k
      if (message->msg == CURLMSG_DONE && message->easy_handle == easy) {
195
266k
        transfer_result = message->data.result;
196
266k
      }
197
267k
    }
198
199
    // CURL_PUSH_OK transfers ownership of each automatically-added easy to
200
    // the application. Enumerate attached handles only after draining their
201
    // completion messages, then honor the public remove-before-cleanup
202
    // lifecycle. The original `easy` remains caller-owned.
203
322k
    CURL** handles = GetHandlesForCleanup(multi.get());
204
322k
    if (handles != nullptr) {
205
646k
      for (std::size_t index = 0; handles[index] != nullptr; ++index) {
206
324k
        CURL* handle = handles[index];
207
324k
        if (handle != easy && curl_multi_remove_handle(multi.get(), handle) == CURLM_OK) {
208
2.09k
          curl_easy_cleanup(handle);
209
2.09k
          ++additional_handle_cleanup_count_;
210
2.09k
        }
211
324k
      }
212
322k
      curl_free(handles);
213
322k
    }
214
215
322k
    curl_multi_remove_handle(multi.get(), easy);
216
322k
  }
217
  // Socket-action callbacks borrow the stack driver above, so that path must
218
  // clean its multi before this function returns. Perform-based protocol mocks
219
  // may retain the detached multi when their fixed lifecycle requires it.
220
322k
  if (use_multi_socket) {
221
5.11k
    multi.reset();
222
317k
  } else {
223
317k
    HandleDetachedMulti(std::move(multi));
224
317k
  }
225
322k
  multi_socket_driver_ = nullptr;
226
322k
  resume_response_ = false;
227
322k
  return transfer_result;
228
323k
}
229
230
/// Dispose of or retain the multi after its easy handle has been removed.
231
/// The default destroys it before DriveScenario returns. A protocol mock
232
/// may retain it as member state when its fixed lifecycle requires easy
233
/// cleanup to happen first.
234
/// @param multi Detached multi handle and its live connection cache.
235
281k
void MockServerBase::HandleDetachedMulti(CurlMultiPtr /*multi*/) {}
236
237
/// Run through the public easy entrypoint when a protocol mock can preload
238
/// all peer work before curl takes control. The base preserves the ordinary
239
/// multi-drive fallback for protocols that require an outer driver to make
240
/// progress. The API policy currently forces HTTP, whose override preloads
241
/// its bounded response and calls curl_easy_perform without a thread.
242
/// @param easy curl easy handle already Install()ed on this mock.
243
/// @param scenario Scenario whose response the mock must prepare.
244
/// @param use_events Select curl's debug event-based easy entrypoint when
245
///        the concrete mock supports it.
246
/// @return Transfer result reported by the selected easy or fallback drive.
247
CURLcode MockServerBase::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario,
248
0
                                           bool /*use_events*/) {
249
0
  return DriveScenario(easy, scenario);
250
0
}
251
252
/// Establish a CONNECT_ONLY transport, then perform bounded direct I/O.
253
/// HTTP overrides this; other protocol mocks retain a safe fallback.
254
/// @param easy curl easy handle already Install()ed on this mock.
255
/// @param scenario Scenario supplying response and direct-I/O bytes.
256
/// @return Results and byte counts from connect, send, and receive probes.
257
ConnectOnlyRunStats MockServerBase::DriveConnectOnlyScenario(CURL* easy,
258
0
                                                             const curl::fuzzer::proto::Scenario& scenario) {
259
0
  ConnectOnlyRunStats stats;
260
0
  stats.connect_result = DriveScenario(easy, scenario);
261
0
  return stats;
262
0
}
263
264
/// Expose the callback state installed for the current socket-action drive.
265
/// HTTP's RunLoop uses this instead of reading the proto directly, so only
266
/// the dedicated API binary can opt into lifecycle work and compatibility
267
/// inputs containing the new field retain their old behavior. Ownership
268
/// remains in DriveScenario so no subclass can shorten the driver's lifetime.
269
/// @return active driver, or nullptr for the ordinary perform path.
270
254k
MultiSocketDriver* MockServerBase::multi_socket_driver() { return multi_socket_driver_; }
271
272
/// @return extra easy handles removed and cleaned after the latest drive.
273
/// Server push is currently the only path that can add one behind the
274
/// caller's back; exposing the count to subclasses keeps ownership in this
275
/// multi-owning base while allowing focused lifecycle assertions.
276
0
std::size_t MockServerBase::additional_handle_cleanup_count() const { return additional_handle_cleanup_count_; }
277
278
/// Resume receive-side callback output at one bounded drive boundary when
279
/// the dedicated API plan requested it. Calling CONT before the callback
280
/// pauses is harmless; repeating it ensures a later response chunk cannot
281
/// leave the transfer suspended until timeout.
282
/// @param easy Active easy handle whose receive callbacks may be paused.
283
1.82M
void MockServerBase::ResumeResponseIfRequested(CURL* easy) {
284
1.82M
  if (resume_response_) {
285
3.57k
    (void)curl_easy_pause(easy, CURLPAUSE_CONT);
286
3.57k
  }
287
1.82M
}
288
289
/// Apply the BackpressureConfig cached by DriveScenario to the new
290
/// connection_. Subclasses call this at the end of HandleOpenSocket, before
291
/// returning the client fd, so SO_RCVBUF takes effect before traffic flows.
292
/// Safe to call when connection_ is null or both knobs are zero: those cases
293
/// are no-ops.
294
27.9k
void MockServerBase::ApplyPendingBackpressure() {
295
27.9k
  if (connection_) {
296
27.9k
    connection_->ApplyBackpressure(pending_recv_buf_bytes_, pending_drain_limit_);
297
27.9k
  }
298
27.9k
}
299
300
/// Treat a non-default BackpressureConfig as an explicit request for the
301
/// slower, timed drive policy. Proto3 scalar defaults make this deterministic:
302
/// a present-but-empty message remains on the ordinary fast path.
303
/// @param scenario Scenario whose backpressure settings select the policy.
304
/// @return true when the scenario explicitly opted into socket backpressure.
305
32.3k
bool MockServerBase::UsesTimedDrive(const curl::fuzzer::proto::Scenario& scenario) {
306
32.3k
  const auto& bp = scenario.connection().backpressure();
307
32.3k
  return bp.recv_buf_bytes() != 0 || bp.drain_limit() != 0;
308
32.3k
}
309
310
/// Wait on curl's fdset with a short timeout. Drive loops call this only for
311
/// scenarios that explicitly request backpressure/timing behaviour; ordinary
312
/// scenarios run without wall-clock sleeps.
313
/// @param multi The multi handle whose fdset to poll.
314
/// @param rc    Out parameter: set to the CURLMcode on error.
315
/// @return select()'s result, or -1 on curl_multi_fdset failure.
316
468k
int MockServerBase::WaitOnMultiFdset(CURLM* multi, CURLMcode* rc) {
317
468k
  fd_set readfds;
318
468k
  fd_set writefds;
319
468k
  fd_set excfds;
320
468k
  FD_ZERO(&readfds);
321
468k
  FD_ZERO(&writefds);
322
468k
  FD_ZERO(&excfds);
323
468k
  int maxfd = -1;
324
468k
  *rc = curl_multi_fdset(multi, &readfds, &writefds, &excfds, &maxfd);
325
468k
  if (*rc != CURLM_OK) {
326
0
    return -1;
327
0
  }
328
468k
  if (maxfd < 0) {
329
0
    return 0;
330
0
  }
331
468k
  struct timeval timeout;
332
468k
  timeout.tv_sec = 0;
333
468k
  timeout.tv_usec = kSelectTimeoutUs;
334
468k
  return ::select(maxfd + 1, &readfds, &writefds, &excfds, &timeout);
335
468k
}
336
337
/// Ask curl to construct and inspect its connection-filter pollset without
338
/// waiting. A perform-only harness can complete local socketpair transfers
339
/// while skipping the public multi-poll path used by event-driven
340
/// applications. Timing scenarios make one zero-timeout probe to retain
341
/// that coverage without taxing the fixed fast lanes. The result is ignored:
342
/// the protocol-specific perform loop determines transfer progress.
343
/// @param multi The active multi handle after at least one perform call.
344
11.2k
void MockServerBase::ProbeMultiPollset(CURLM* multi) {
345
11.2k
  int numfds = 0;
346
11.2k
  (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds);
347
11.2k
}
348
349
}  // namespace proto_fuzzer