Coverage Report

Created: 2026-10-28 07:17

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