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/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 <sys/select.h>
15
#include <sys/socket.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
/// Tell curl whether the peer supplied an already-connected socketpair or a
28
/// real datagram socket that still needs protocol-owned setup. Returning the
29
/// socketpair answer unconditionally made the old TFTP harness skip the wrong
30
/// setup assumptions and eventually call IP operations on an AF_UNIX stream.
31
/// Accepted sockets are already established by curl itself, where the callback
32
/// contract treats any non-zero result as an error rather than as a shortcut.
33
147k
int SockOptTrampoline(void* /*clientp*/, curl_socket_t curlfd, curlsocktype purpose) {
34
147k
  if (purpose == CURLSOCKTYPE_ACCEPT) {
35
0
    return CURL_SOCKOPT_OK;
36
0
  }
37
38
147k
  int socket_type = 0;
39
147k
  socklen_t socket_type_size = sizeof(socket_type);
40
147k
  if (getsockopt(curlfd, SOL_SOCKET, SO_TYPE, &socket_type, &socket_type_size) != 0) {
41
0
    return CURL_SOCKOPT_ERROR;
42
0
  }
43
147k
  return socket_type == SOCK_DGRAM ? CURL_SOCKOPT_OK : CURL_SOCKOPT_ALREADY_CONNECTED;
44
147k
}
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
154k
curl_socket_t MockServerBaseOpenSocketTrampoline(void* clientp, curlsocktype purpose, struct curl_sockaddr* address) {
55
154k
  return static_cast<MockServerBase*>(clientp)->HandleOpenSocket(purpose, address);
56
154k
}
57
58
/// Default-construct an empty base instance with no connection.
59
MockServerBase::MockServerBase()
60
136k
    : connection_(nullptr), pending_recv_buf_bytes_(0), pending_drain_limit_(0), multi_socket_driver_(nullptr) {}
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
136k
MockServerBase::~MockServerBase() = default;
66
67
/// @return the owned MockConnection, or nullptr if one has not been opened.
68
680k
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
138k
void MockServerBase::Install(CURL* easy) {
73
138k
  curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, &MockServerBaseOpenSocketTrampoline);
74
138k
  curl_easy_setopt(easy, CURLOPT_OPENSOCKETDATA, this);
75
138k
  curl_easy_setopt(easy, CURLOPT_SOCKOPTFUNCTION, &SockOptTrampoline);
76
138k
}
77
78
/// Ordinary event-driven mocks need no upload-callback hook; their RunLoop
79
/// regains control after each perform and drains client traffic there.
80
132k
void MockServerBase::ConfigureRequestData(ScenarioRequestData* /*request_data*/) {}
81
82
/// Allocate a multi, attach 'easy', delegate to the subclass RunLoop, consume
83
/// its completion message, and clean up. Harness setup failures return a
84
/// stable sentinel; fuzzer callers may ignore it while unit tests can assert
85
/// the protocol result without adding another callback or global.
86
CURLcode MockServerBase::DriveScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool use_multi_socket,
87
134k
                                       bool wake_multi) {
88
  // Cache backpressure knobs so HandleOpenSocket can apply them the moment
89
  // connection_ exists. Both default to 0, which matches the legacy "drain
90
  // greedily, kernel-default buffers" behaviour exactly.
91
134k
  const auto& bp = scenario.connection().backpressure();
92
134k
  pending_recv_buf_bytes_ = static_cast<int>(bp.recv_buf_bytes());
93
134k
  pending_drain_limit_ = static_cast<std::size_t>(bp.drain_limit());
94
95
134k
  CurlMultiPtr multi(curl_multi_init());
96
134k
  if (multi == nullptr) {
97
0
    return CURLE_FAILED_INIT;
98
0
  }
99
100
134k
  CURLcode transfer_result = CURLE_FAILED_INIT;
101
102
  // Callback data must survive both remove_handle and multi_cleanup, since
103
  // either may emit CURL_POLL_REMOVE. Keeping it in this outer scope provides
104
  // that lifetime without allocating per-watch state.
105
134k
  MultiSocketDriver socket_driver;
106
134k
  if (use_multi_socket && socket_driver.Install(multi.get())) {
107
873
    multi_socket_driver_ = &socket_driver;
108
873
  }
109
134k
  if (curl_multi_add_handle(multi.get(), easy) == CURLM_OK) {
110
134k
    if (wake_multi) {
111
288
      if (multi_socket_driver_ != nullptr) {
112
130
        multi_socket_driver_->ProbeControlApis();
113
158
      } else {
114
158
        long timeout_ms = -1;
115
158
        (void)curl_multi_timeout(multi.get(), &timeout_ms);
116
158
        (void)curl_multi_wakeup(multi.get());
117
158
      }
118
288
    }
119
134k
    RunLoop(multi.get(), easy, scenario);
120
121
    // Completion messages are the multi API's only durable record of the
122
    // transfer result. Consume them while the easy handle is still attached:
123
    // otherwise every scenario systematically skips curl_multi_info_read's
124
    // result path and removal discards the opportunity. With one easy handle
125
    // attached, this drain has at most one completion message regardless of
126
    // fuzzed response size or redirect count.
127
134k
    int messages_remaining = 0;
128
134k
    CURLMsg* message = nullptr;
129
248k
    while ((message = curl_multi_info_read(multi.get(), &messages_remaining)) != nullptr) {
130
113k
      if (message->msg == CURLMSG_DONE && message->easy_handle == easy) {
131
113k
        transfer_result = message->data.result;
132
113k
      }
133
113k
    }
134
135
134k
    curl_multi_remove_handle(multi.get(), easy);
136
134k
  }
137
134k
  multi.reset();
138
134k
  multi_socket_driver_ = nullptr;
139
134k
  return transfer_result;
140
134k
}
141
142
/// Preserve a safe fallback for protocol mocks that require an outer driver
143
/// to make progress. The API policy currently forces HTTP, whose override can
144
/// preload its bounded response and call curl_easy_perform without a thread.
145
0
void MockServerBase::DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
146
0
  DriveScenario(easy, scenario);
147
0
}
148
149
/// Expose only the current callback state to protocol drive loops. Ownership
150
/// remains in DriveScenario so no subclass can accidentally shorten it.
151
96.2k
MultiSocketDriver* MockServerBase::multi_socket_driver() { return multi_socket_driver_; }
152
153
/// Hand the cached backpressure config to the connection. Safe to call when
154
/// connection_ is null (no-op) or when both knobs are 0 (ApplyBackpressure
155
/// itself is a no-op in that case).
156
17.5k
void MockServerBase::ApplyPendingBackpressure() {
157
17.5k
  if (connection_) {
158
17.5k
    connection_->ApplyBackpressure(pending_recv_buf_bytes_, pending_drain_limit_);
159
17.5k
  }
160
17.5k
}
161
162
/// Treat a non-default BackpressureConfig as an explicit request for the
163
/// slower, timed drive policy. Proto3 scalar defaults make this deterministic:
164
/// a present-but-empty message remains on the ordinary fast path.
165
/// @param scenario Scenario whose backpressure settings select the policy.
166
/// @return true when the scenario explicitly opted into socket backpressure.
167
25.1k
bool MockServerBase::UsesTimedDrive(const curl::fuzzer::proto::Scenario& scenario) {
168
25.1k
  const auto& bp = scenario.connection().backpressure();
169
25.1k
  return bp.recv_buf_bytes() != 0 || bp.drain_limit() != 0;
170
25.1k
}
171
172
/// Wait on curl's fdset with a short timeout. Returns select()'s result; on
173
/// error sets *rc to the corresponding CURLMcode.
174
282k
int MockServerBase::WaitOnMultiFdset(CURLM* multi, CURLMcode* rc) {
175
282k
  fd_set readfds;
176
282k
  fd_set writefds;
177
282k
  fd_set excfds;
178
282k
  FD_ZERO(&readfds);
179
282k
  FD_ZERO(&writefds);
180
282k
  FD_ZERO(&excfds);
181
282k
  int maxfd = -1;
182
282k
  *rc = curl_multi_fdset(multi, &readfds, &writefds, &excfds, &maxfd);
183
282k
  if (*rc != CURLM_OK) {
184
0
    return -1;
185
0
  }
186
282k
  if (maxfd < 0) {
187
0
    return 0;
188
0
  }
189
282k
  struct timeval timeout;
190
282k
  timeout.tv_sec = 0;
191
282k
  timeout.tv_usec = kSelectTimeoutUs;
192
282k
  return ::select(maxfd + 1, &readfds, &writefds, &excfds, &timeout);
193
282k
}
194
195
/// Exercise curl_multi_poll's pollset/filter traversal once without sleeping.
196
/// The result is deliberately ignored: this is an API/state probe, while the
197
/// protocol-specific perform loop remains the authority on transfer progress.
198
8.26k
void MockServerBase::ProbeMultiPollset(CURLM* multi) {
199
8.26k
  int numfds = 0;
200
8.26k
  (void)curl_multi_poll(multi, nullptr, 0, 0, &numfds);
201
8.26k
}
202
203
}  // namespace proto_fuzzer