Coverage Report

Created: 2026-09-01 07:00

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