Coverage Report

Created: 2026-09-14 07:12

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
21
namespace proto_fuzzer {
22
23
namespace {
24
25
constexpr long kSelectTimeoutUs = 1000;  // 1 ms; explicit timing cases only.
26
27
}  // namespace
28
29
/// @brief C trampoline for CURLOPT_OPENSOCKETFUNCTION. Declared at namespace
30
///        scope so it can be a friend of MockServerBase.
31
/// @param clientp Pointer to the MockServerBase instance.
32
/// @param purpose The socket role curl is asking the mock to provide.
33
/// @param address Curl's mutable description of the intended destination.
34
/// @return The client-side socket fd as a curl_socket_t.
35
376k
curl_socket_t MockServerBaseOpenSocketTrampoline(void* clientp, curlsocktype purpose, struct curl_sockaddr* address) {
36
376k
  return static_cast<MockServerBase*>(clientp)->HandleOpenSocket(purpose, address);
37
376k
}
38
39
/// Translate explicit peer transport metadata into curl's sockopt callback
40
/// contract. The descriptor is used only as an opaque identity; sandbox
41
/// policy must not decide whether an otherwise valid in-process transport can
42
/// run.
43
/// @param clientp Pointer to the MockServerBase instance.
44
/// @param curlfd Descriptor returned by the open-socket callback.
45
/// @param purpose Socket role assigned by curl.
46
/// @return CURL_SOCKOPT_ALREADY_CONNECTED for prepared stream peers, otherwise
47
///         CURL_SOCKOPT_OK.
48
341k
int MockServerBaseSockOptTrampoline(void* clientp, curl_socket_t curlfd, curlsocktype purpose) {
49
341k
  const auto disposition = static_cast<MockServerBase*>(clientp)->GetSocketSetupDisposition(curlfd, purpose);
50
341k
  return disposition == SocketSetupDisposition::kAlreadyConnected ? CURL_SOCKOPT_ALREADY_CONNECTED : CURL_SOCKOPT_OK;
51
341k
}
52
53
/// Default-construct an empty base instance with no connection.
54
MockServerBase::MockServerBase()
55
285k
    : connection_(nullptr),
56
285k
      pending_recv_buf_bytes_(0),
57
285k
      pending_drain_limit_(0),
58
285k
      multi_socket_driver_(nullptr),
59
285k
      additional_handle_cleanup_count_(0),
60
285k
      resume_response_(false) {}
61
62
/// Out-of-line destructor so MockConnection can stay forward-declared in the
63
/// base header (its complete type is only needed where unique_ptr is
64
/// instantiated for destruction).
65
285k
MockServerBase::~MockServerBase() = default;
66
67
/// @return the owned MockConnection, or nullptr if one has not been opened.
68
936k
MockConnection* MockServerBase::connection() { return connection_.get(); }
69
70
/// Install the common socket-callback trio. All subclasses share the same
71
/// trampoline; dispatch to the subclass happens through HandleOpenSocket().
72
327k
void MockServerBase::Install(CURL* easy) {
73
327k
  curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, &MockServerBaseOpenSocketTrampoline);
74
327k
  curl_easy_setopt(easy, CURLOPT_OPENSOCKETDATA, this);
75
327k
  curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, &MockServerBaseSockOptTrampoline);
76
327k
  curl_easy_setopt(easy, CURLOPT_SOCKOPTDATA, this);
77
327k
}
78
79
318k
SocketSetupDisposition MockServerBase::GetSocketSetupDisposition(curl_socket_t /*curlfd*/, curlsocktype purpose) const {
80
  // curl itself accepted CURLSOCKTYPE_ACCEPT descriptors, so they must follow
81
  // its normal post-accept option path. Every IPCXN socket returned by the
82
  // ordinary stream mocks is one end of an already-connected socketpair.
83
318k
  return purpose == CURLSOCKTYPE_ACCEPT ? SocketSetupDisposition::kNeedsSetup
84
318k
                                        : SocketSetupDisposition::kAlreadyConnected;
85
318k
}
86
87
/// Ordinary event-driven mocks need no upload-callback hook; their RunLoop
88
/// regains control after each perform and drains client traffic there.
89
321k
void MockServerBase::ConfigureRequestData(ScenarioRequestData* /*request_data*/) {}
90
91
/// Allocate a multi, attach 'easy', delegate to the subclass RunLoop, consume
92
/// its completion message, and clean up. Harness setup failures return a
93
/// stable sentinel; fuzzer callers may ignore it while unit tests can assert
94
/// the protocol result without adding another callback or global.
95
CURLcode MockServerBase::DriveScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool use_multi_socket,
96
264k
                                       bool wake_multi, bool resume_response) {
97
  // Cache backpressure knobs so HandleOpenSocket can apply them the moment
98
  // connection_ exists. Both default to 0, which matches the legacy "drain
99
  // greedily, kernel-default buffers" behaviour exactly.
100
264k
  const auto& bp = scenario.connection().backpressure();
101
264k
  pending_recv_buf_bytes_ = static_cast<int>(bp.recv_buf_bytes());
102
264k
  pending_drain_limit_ = static_cast<std::size_t>(bp.drain_limit());
103
264k
  additional_handle_cleanup_count_ = 0;
104
264k
  resume_response_ = resume_response;
105
106
264k
  CurlMultiPtr multi(curl_multi_init());
107
264k
  if (multi == nullptr) {
108
0
    resume_response_ = false;
109
0
    return CURLE_FAILED_INIT;
110
0
  }
111
112
264k
  CURLcode transfer_result = CURLE_FAILED_INIT;
113
114
  // Callback data must survive both remove_handle and multi_cleanup, since
115
  // either may emit CURL_POLL_REMOVE. Keeping it in this outer scope provides
116
  // that lifetime without allocating per-watch state.
117
264k
  MultiSocketDriver socket_driver;
118
264k
  if (use_multi_socket && socket_driver.Install(multi.get())) {
119
7.54k
    multi_socket_driver_ = &socket_driver;
120
7.54k
  }
121
264k
  if (curl_multi_add_handle(multi.get(), easy) == CURLM_OK) {
122
264k
    if (wake_multi) {
123
86
      if (multi_socket_driver_ != nullptr) {
124
30
        multi_socket_driver_->ProbeControlApis();
125
56
      } else {
126
56
        long timeout_ms = -1;
127
56
        (void)curl_multi_timeout(multi.get(), &timeout_ms);
128
56
        (void)curl_multi_wakeup(multi.get());
129
56
      }
130
86
    }
131
264k
    RunLoop(multi.get(), easy, scenario);
132
133
    // Completion messages are the multi API's only durable record of the
134
    // transfer result. Consume them while all easy handles are still attached:
135
    // otherwise every scenario systematically skips curl_multi_info_read's
136
    // result path and removal discards the opportunity. An accepted HTTP/2
137
    // push can contribute one additional completion message; only the caller's
138
    // parent determines DriveScenario's result.
139
264k
    int messages_remaining = 0;
140
264k
    CURLMsg* message = nullptr;
141
487k
    while ((message = curl_multi_info_read(multi.get(), &messages_remaining)) != nullptr) {
142
223k
      if (message->msg == CURLMSG_DONE && message->easy_handle == easy) {
143
223k
        transfer_result = message->data.result;
144
223k
      }
145
223k
    }
146
147
    // CURL_PUSH_OK transfers ownership of each automatically-added easy to
148
    // the application. Enumerate attached handles only after draining their
149
    // completion messages, then honor the public remove-before-cleanup
150
    // lifecycle. The original `easy` remains caller-owned.
151
264k
    CURL** handles = curl_multi_get_handles(multi.get());
152
264k
    if (handles != nullptr) {
153
528k
      for (std::size_t index = 0; handles[index] != nullptr; ++index) {
154
264k
        CURL* handle = handles[index];
155
264k
        if (handle != easy && curl_multi_remove_handle(multi.get(), handle) == CURLM_OK) {
156
134
          curl_easy_cleanup(handle);
157
134
          ++additional_handle_cleanup_count_;
158
134
        }
159
264k
      }
160
264k
      curl_free(handles);
161
264k
    }
162
163
264k
    curl_multi_remove_handle(multi.get(), easy);
164
264k
  }
165
264k
  multi.reset();
166
264k
  multi_socket_driver_ = nullptr;
167
264k
  resume_response_ = false;
168
264k
  return transfer_result;
169
264k
}
170
171
/// Preserve a safe fallback for protocol mocks that require an outer driver
172
/// to make progress. The API policy currently forces HTTP, whose override can
173
/// preload its bounded response and call curl_easy_perform without a thread.
174
0
void MockServerBase::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool /*use_events*/) {
175
0
  DriveScenario(easy, scenario);
176
0
}
177
178
ConnectOnlyRunStats MockServerBase::DriveConnectOnlyScenario(CURL* easy,
179
0
                                                             const curl::fuzzer::proto::Scenario& scenario) {
180
0
  ConnectOnlyRunStats stats;
181
0
  stats.connect_result = DriveScenario(easy, scenario);
182
0
  return stats;
183
0
}
184
185
/// Expose only the current callback state to protocol drive loops. Ownership
186
/// remains in DriveScenario so no subclass can accidentally shorten it.
187
218k
MultiSocketDriver* MockServerBase::multi_socket_driver() { return multi_socket_driver_; }
188
189
0
std::size_t MockServerBase::additional_handle_cleanup_count() const { return additional_handle_cleanup_count_; }
190
191
1.16M
void MockServerBase::ResumeResponseIfRequested(CURL* easy) {
192
1.16M
  if (resume_response_) {
193
1.58k
    (void)curl_easy_pause(easy, CURLPAUSE_CONT);
194
1.58k
  }
195
1.16M
}
196
197
/// Hand the cached backpressure config to the connection. Safe to call when
198
/// connection_ is null (no-op) or when both knobs are 0 (ApplyBackpressure
199
/// itself is a no-op in that case).
200
22.3k
void MockServerBase::ApplyPendingBackpressure() {
201
22.3k
  if (connection_) {
202
22.3k
    connection_->ApplyBackpressure(pending_recv_buf_bytes_, pending_drain_limit_);
203
22.3k
  }
204
22.3k
}
205
206
/// Treat a non-default BackpressureConfig as an explicit request for the
207
/// slower, timed drive policy. Proto3 scalar defaults make this deterministic:
208
/// a present-but-empty message remains on the ordinary fast path.
209
/// @param scenario Scenario whose backpressure settings select the policy.
210
/// @return true when the scenario explicitly opted into socket backpressure.
211
29.8k
bool MockServerBase::UsesTimedDrive(const curl::fuzzer::proto::Scenario& scenario) {
212
29.8k
  const auto& bp = scenario.connection().backpressure();
213
29.8k
  return bp.recv_buf_bytes() != 0 || bp.drain_limit() != 0;
214
29.8k
}
215
216
/// Wait on curl's fdset with a short timeout. Returns select()'s result; on
217
/// error sets *rc to the corresponding CURLMcode.
218
361k
int MockServerBase::WaitOnMultiFdset(CURLM* multi, CURLMcode* rc) {
219
361k
  fd_set readfds;
220
361k
  fd_set writefds;
221
361k
  fd_set excfds;
222
361k
  FD_ZERO(&readfds);
223
361k
  FD_ZERO(&writefds);
224
361k
  FD_ZERO(&excfds);
225
361k
  int maxfd = -1;
226
361k
  *rc = curl_multi_fdset(multi, &readfds, &writefds, &excfds, &maxfd);
227
361k
  if (*rc != CURLM_OK) {
228
0
    return -1;
229
0
  }
230
361k
  if (maxfd < 0) {
231
0
    return 0;
232
0
  }
233
361k
  struct timeval timeout;
234
361k
  timeout.tv_sec = 0;
235
361k
  timeout.tv_usec = kSelectTimeoutUs;
236
361k
  return ::select(maxfd + 1, &readfds, &writefds, &excfds, &timeout);
237
361k
}
238
239
/// Exercise curl_multi_poll's pollset/filter traversal once without sleeping.
240
/// The result is deliberately ignored: this is an API/state probe, while the
241
/// protocol-specific perform loop remains the authority on transfer progress.
242
11.0k
void MockServerBase::ProbeMultiPollset(CURLM* multi) {
243
11.0k
  int numfds = 0;
244
11.0k
  (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds);
245
11.0k
}
246
247
}  // namespace proto_fuzzer