Coverage Report

Created: 2026-10-22 06:56

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
326k
CURL** GetHandlesForCleanup(CURLM* multi) {
34
326k
  allocation_fault_injection::ScopedFailureSuppression suppress_failures;
35
326k
  return curl_multi_get_handles(multi);
36
326k
}
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
448k
curl_socket_t MockServerBaseOpenSocketTrampoline(void* clientp, curlsocktype purpose, struct curl_sockaddr* address) {
47
448k
  return static_cast<MockServerBase*>(clientp)->HandleOpenSocket(purpose, address);
48
448k
}
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
409k
int MockServerBaseSockOptTrampoline(void* clientp, curl_socket_t curlfd, curlsocktype purpose) {
60
409k
  const auto disposition = static_cast<MockServerBase*>(clientp)->GetSocketSetupDisposition(curlfd, purpose);
61
409k
  return disposition == SocketSetupDisposition::kAlreadyConnected ? CURL_SOCKOPT_ALREADY_CONNECTED : CURL_SOCKOPT_OK;
62
409k
}
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
353k
    : connection_(nullptr),
69
353k
      pending_recv_buf_bytes_(0),
70
353k
      pending_drain_limit_(0),
71
353k
      multi_socket_driver_(nullptr),
72
353k
      additional_handle_cleanup_count_(0),
73
353k
      resume_response_(false),
74
353k
      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
353k
MockServerBase::~MockServerBase() = default;
80
81
/// @return the owned MockConnection, or nullptr if one has not been opened.
82
1.63M
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
400k
void MockServerBase::Install(CURL* easy) {
91
400k
  curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, &MockServerBaseOpenSocketTrampoline);
92
400k
  curl_easy_setopt(easy, CURLOPT_OPENSOCKETDATA, this);
93
400k
  curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, &MockServerBaseSockOptTrampoline);
94
400k
  curl_easy_setopt(easy, CURLOPT_SOCKOPTDATA, this);
95
400k
}
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
390k
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
390k
  return purpose == CURLSOCKTYPE_ACCEPT ? SocketSetupDisposition::kNeedsSetup
108
390k
                                        : SocketSetupDisposition::kAlreadyConnected;
109
390k
}
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
393k
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
328k
CURLcode MockServerBase::DriveScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
132
328k
  const curl::fuzzer::proto::ApiPlan* api_plan =
133
328k
      drive_policy_ == MultiDrivePolicy::kFromApiPlan && scenario.has_api_plan() ? &scenario.api_plan() : nullptr;
134
328k
  const bool use_multi_socket =
135
328k
      drive_policy_ == MultiDrivePolicy::kSocketAction ||
136
323k
      (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
328k
  const auto& bp = scenario.connection().backpressure();
142
328k
  pending_recv_buf_bytes_ = static_cast<int>(bp.recv_buf_bytes());
143
328k
  pending_drain_limit_ = static_cast<std::size_t>(bp.drain_limit());
144
328k
  additional_handle_cleanup_count_ = 0;
145
328k
  resume_response_ = api_plan != nullptr && api_plan->pause_response_once();
146
147
328k
  CurlMultiPtr multi(curl_multi_init());
148
328k
  if (multi == nullptr) {
149
1.36k
    resume_response_ = false;
150
1.36k
    return CURLE_FAILED_INIT;
151
1.36k
  }
152
153
326k
  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
326k
  MultiSocketDriver socket_driver;
159
326k
  if (use_multi_socket && socket_driver.Install(multi.get())) {
160
4.94k
    multi_socket_driver_ = &socket_driver;
161
4.94k
  }
162
326k
  if (curl_multi_add_handle(multi.get(), easy) == CURLM_OK) {
163
326k
    if (multi_observer_) {
164
4.60k
      multi_observer_(multi.get());
165
4.60k
    }
166
326k
    if (api_plan != nullptr && api_plan->wake_multi()) {
167
64
      if (multi_socket_driver_ != nullptr) {
168
20
        multi_socket_driver_->ProbeControlApis();
169
44
      } else {
170
44
        long timeout_ms = -1;
171
44
        (void)curl_multi_timeout(multi.get(), &timeout_ms);
172
44
        (void)curl_multi_wakeup(multi.get());
173
44
      }
174
64
    }
175
326k
    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
326k
    int messages_remaining = 0;
184
326k
    CURLMsg* message = nullptr;
185
598k
    while ((message = curl_multi_info_read(multi.get(), &messages_remaining)) != nullptr) {
186
271k
      if (message->msg == CURLMSG_DONE && message->easy_handle == easy) {
187
271k
        transfer_result = message->data.result;
188
271k
      }
189
271k
    }
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
326k
    CURL** handles = GetHandlesForCleanup(multi.get());
196
326k
    if (handles != nullptr) {
197
655k
      for (std::size_t index = 0; handles[index] != nullptr; ++index) {
198
328k
        CURL* handle = handles[index];
199
328k
        if (handle != easy && curl_multi_remove_handle(multi.get(), handle) == CURLM_OK) {
200
2.06k
          curl_easy_cleanup(handle);
201
2.06k
          ++additional_handle_cleanup_count_;
202
2.06k
        }
203
328k
      }
204
326k
      curl_free(handles);
205
326k
    }
206
207
326k
    curl_multi_remove_handle(multi.get(), easy);
208
326k
  }
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
326k
  if (use_multi_socket) {
213
4.94k
    multi.reset();
214
321k
  } else {
215
321k
    HandleDetachedMulti(std::move(multi));
216
321k
  }
217
326k
  multi_socket_driver_ = nullptr;
218
326k
  resume_response_ = false;
219
326k
  return transfer_result;
220
328k
}
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
285k
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
258k
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.78M
void MockServerBase::ResumeResponseIfRequested(CURL* easy) {
276
1.78M
  if (resume_response_) {
277
3.64k
    (void)curl_easy_pause(easy, CURLPAUSE_CONT);
278
3.64k
  }
279
1.78M
}
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
28.3k
void MockServerBase::ApplyPendingBackpressure() {
287
28.3k
  if (connection_) {
288
28.3k
    connection_->ApplyBackpressure(pending_recv_buf_bytes_, pending_drain_limit_);
289
28.3k
  }
290
28.3k
}
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
32.6k
bool MockServerBase::UsesTimedDrive(const curl::fuzzer::proto::Scenario& scenario) {
298
32.6k
  const auto& bp = scenario.connection().backpressure();
299
32.6k
  return bp.recv_buf_bytes() != 0 || bp.drain_limit() != 0;
300
32.6k
}
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
470k
int MockServerBase::WaitOnMultiFdset(CURLM* multi, CURLMcode* rc) {
309
470k
  fd_set readfds;
310
470k
  fd_set writefds;
311
470k
  fd_set excfds;
312
470k
  FD_ZERO(&readfds);
313
470k
  FD_ZERO(&writefds);
314
470k
  FD_ZERO(&excfds);
315
470k
  int maxfd = -1;
316
470k
  *rc = curl_multi_fdset(multi, &readfds, &writefds, &excfds, &maxfd);
317
470k
  if (*rc != CURLM_OK) {
318
0
    return -1;
319
0
  }
320
470k
  if (maxfd < 0) {
321
0
    return 0;
322
0
  }
323
470k
  struct timeval timeout;
324
470k
  timeout.tv_sec = 0;
325
470k
  timeout.tv_usec = kSelectTimeoutUs;
326
470k
  return ::select(maxfd + 1, &readfds, &writefds, &excfds, &timeout);
327
470k
}
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. Timing scenarios make one zero-timeout probe to retain
333
/// that coverage without taxing the fixed fast lanes. 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
12.4k
void MockServerBase::ProbeMultiPollset(CURLM* multi) {
337
12.4k
  int numfds = 0;
338
12.4k
  (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds);
339
12.4k
}
340
341
}  // namespace proto_fuzzer