curl-fuzzer C++ reference
Loading...
Searching...
No Matches
happy_eyeballs_mock_server.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#include <arpa/inet.h>
13#include <fcntl.h>
14#include <netinet/in.h>
15#include <sys/socket.h>
16#include <sys/time.h>
17#include <sys/types.h>
18#include <unistd.h>
19
20#include <algorithm>
21#include <cerrno>
22#include <cstddef>
23#include <cstdint>
24#include <cstring>
25#include <string>
26#include <utility>
27
29
30namespace proto_fuzzer {
31
32namespace {
33
38constexpr std::size_t kMaxConnectAttempts = 16;
39
42constexpr std::size_t kMaxAcceptedPeers = 12;
43constexpr int kMaxAcceptsPerCall = 8;
44
48constexpr int kFirstServeHostByte = 2;
49constexpr int kLastServeHostByte = 9;
50
55constexpr int kRaceBacklog = 1;
56constexpr int kRaceFillConnections = 2;
57
58constexpr std::size_t kReadBufferSize = 4096;
59
61void CloseFd(int* fd) {
62 if (*fd >= 0) {
63 close(*fd);
64 *fd = -1;
65 }
66}
67
71bool MakeNonBlocking(int fd, bool require_selectable) {
72 if (require_selectable && fd >= FD_SETSIZE) {
73 return false;
74 }
75 const int flags = fcntl(fd, F_GETFL, 0);
76 return flags >= 0 && fcntl(fd, F_SETFL, flags | O_NONBLOCK) >= 0;
77}
78
81std::size_t RawChunkCount(const curl::fuzzer::proto::Connection& script) {
82 return std::min<std::size_t>(scenario_limits::kMaxResponseChunks, script.on_readable_size());
83}
84
89struct sockaddr_in LoopbackAddress(int host_byte, std::uint16_t port) {
90 struct sockaddr_in addr;
91 std::memset(&addr, 0, sizeof(addr));
92 addr.sin_family = AF_INET;
93 addr.sin_addr.s_addr = htonl(0x7f000000U | static_cast<std::uint32_t>(host_byte));
94 addr.sin_port = port;
95 return addr;
96}
97
98} // namespace
99
102 : easy_(nullptr),
103 race_fd_(-1),
104 serve_fd_(-1),
105 listener_port_(0),
106 opened_socket_count_(0),
107 accepted_peer_count_(0),
108 scenario_(nullptr) {}
109
114 scenario_ = nullptr;
115 for (Peer& peer : peers_) {
116 ClosePeer(&peer);
117 }
118 peers_.clear();
119 CloseListeners();
120}
121
128 easy_ = easy;
129 setup_ok_ = OpenListeners();
130 if (easy_ != nullptr && setup_ok_) {
131 setup_ok_ = curl_easy_setopt(easy_, CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS, 0L) == CURLE_OK;
132 }
133}
134
141 // Any early return leaves setup_ok_ false, so RunLoop refuses to drive curl
142 // rather than falling through to the previous resolve.test mapping and
143 // connecting to an unrelated address (e.g. 127.0.0.1:80).
144 setup_ok_ = false;
145 if (easy_ == nullptr || listener_port_ <= 0 || serve_address_.empty()) {
146 return;
147 }
148
149 const std::string mapping = "resolve.test:" + std::to_string(listener_port_) + ":127.0.0.1," + serve_address_;
150
151 CurlSlistPtr resolve_list;
152 curl_slist* entry = curl_slist_append(nullptr, mapping.c_str());
153 if (entry == nullptr) {
154 return;
155 }
156 resolve_list.reset(entry);
157 if (curl_easy_setopt(easy_, CURLOPT_RESOLVE, resolve_list.get()) != CURLE_OK) {
158 return;
159 }
160 resolve_list_ = std::move(resolve_list);
161 if (curl_easy_setopt(easy_, CURLOPT_PORT, static_cast<long>(listener_port_)) != CURLE_OK) {
162 return;
163 }
164 setup_ok_ = true;
165}
166
174curl_socket_t HappyEyeballsMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
175 if (purpose != CURLSOCKTYPE_IPCXN || address == nullptr || opened_socket_count_ >= kMaxConnectAttempts) {
176 return CURL_SOCKET_BAD;
177 }
178 if (!IsOwnedDestination(address)) {
179 return CURL_SOCKET_BAD;
180 }
181
182 curl_socket_t fd = socket(address->family, address->socktype, address->protocol);
183 if (fd == CURL_SOCKET_BAD) {
184 return CURL_SOCKET_BAD;
185 }
186 if (!MakeNonBlocking(fd, true)) {
187 close(fd);
188 return CURL_SOCKET_BAD;
189 }
190 ++opened_socket_count_;
191 return fd;
192}
193
200 curlsocktype /*purpose*/) const {
201 return SocketSetupDisposition::kNeedsSetup;
202}
203
209bool HappyEyeballsMockServer::IsOwnedDestination(const struct curl_sockaddr* address) const {
210 if (address == nullptr || address->family != AF_INET || listener_port_ <= 0) {
211 return false;
212 }
213 const struct sockaddr_in* sin = reinterpret_cast<const struct sockaddr_in*>(&address->addr);
214 const std::uint32_t host = ntohl(sin->sin_addr.s_addr);
215 return (host & 0xff000000U) == 0x7f000000U && sin->sin_port == htons(static_cast<std::uint16_t>(listener_port_));
216}
217
224void HappyEyeballsMockServer::RunLoop(CURLM* multi, CURL* /*easy*/, const curl::fuzzer::proto::Scenario& scenario) {
225 if (!setup_ok_) {
226 // Listeners or the resolve mapping failed to install. Never perform: the
227 // only safe outcome is no connection attempt at all.
228 return;
229 }
230 scenario_ = &scenario;
231
232 int still_running = 1;
233 int idle_iterations = 0;
234 int drive_iterations = 0;
235 CURLMcode rc = CURLM_OK;
236
237 while (still_running && idle_iterations < kMaxIdleIterations && drive_iterations++ < kMaxDriveIterations) {
238 bool made_progress = AcceptPending();
239 made_progress = ServicePeers() || made_progress;
240
241 const int running_before = still_running;
242 rc = curl_multi_perform(multi, &still_running);
243 if (rc != CURLM_OK) {
244 break;
245 }
246 made_progress = still_running != running_before || made_progress;
247 made_progress = AcceptPending() || made_progress;
248 made_progress = ServicePeers() || made_progress;
249 if (!still_running) {
250 break;
251 }
252
253 if (made_progress) {
254 idle_iterations = 0;
255 continue;
256 }
257
258 CURLMcode wait_rc = CURLM_OK;
259 if (WaitOnMultiFdset(multi, &wait_rc) < 0 || wait_rc != CURLM_OK) {
260 break;
261 }
262 ++idle_iterations;
263 }
264
265 // Every Peer::script points into the caller-owned scenario. Clear those
266 // borrowings before returning even if a peer outlives the drive loop.
267 scenario_ = nullptr;
268 for (Peer& peer : peers_) {
269 ClosePeer(&peer);
270 }
271 peers_.clear();
272}
273
279bool HappyEyeballsMockServer::OpenListeners() {
280 CloseListeners();
281
282 const int race_fd = socket(AF_INET, SOCK_STREAM, 0);
283 if (race_fd < 0 || race_fd >= FD_SETSIZE) {
284 if (race_fd >= 0) {
285 close(race_fd);
286 }
287 return false;
288 }
289
290 const int reuse = 1;
291 (void)setsockopt(race_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
292
293 struct sockaddr_in race_addr = LoopbackAddress(1, 0);
294 if (bind(race_fd, reinterpret_cast<const struct sockaddr*>(&race_addr), sizeof(race_addr)) != 0 ||
295 listen(race_fd, kRaceBacklog) != 0) {
296 close(race_fd);
297 return false;
298 }
299
300 socklen_t addrlen = sizeof(race_addr);
301 if (getsockname(race_fd, reinterpret_cast<struct sockaddr*>(&race_addr), &addrlen) != 0 ||
302 !MakeNonBlocking(race_fd, true)) {
303 close(race_fd);
304 return false;
305 }
306 race_fd_ = race_fd;
307 listener_port_ = static_cast<int>(ntohs(race_addr.sin_port));
308 const std::uint16_t network_port = race_addr.sin_port;
309
310 for (int host_byte = kFirstServeHostByte; host_byte <= kLastServeHostByte; ++host_byte) {
311 const int serve_fd = socket(AF_INET, SOCK_STREAM, 0);
312 if (serve_fd < 0 || serve_fd >= FD_SETSIZE) {
313 if (serve_fd >= 0) {
314 close(serve_fd);
315 }
316 continue;
317 }
318 (void)setsockopt(serve_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
319
320 struct sockaddr_in serve_addr = LoopbackAddress(host_byte, network_port);
321 if (bind(serve_fd, reinterpret_cast<const struct sockaddr*>(&serve_addr), sizeof(serve_addr)) == 0 &&
322 listen(serve_fd, 8) == 0 && MakeNonBlocking(serve_fd, true)) {
323 serve_fd_ = serve_fd;
324 serve_address_ = "127.0.0." + std::to_string(host_byte);
325 break;
326 }
327 close(serve_fd);
328 }
329
330 if (serve_fd_ < 0) {
331 CloseListeners();
332 return false;
333 }
334
335 // Fill the race endpoint's accept queue. SO_SNDTIMEO bounds connect() on the
336 // loopback path, which normally completes immediately. These client halves
337 // stay open for the whole drive so the queue remains full.
338 struct timeval connect_timeout;
339 connect_timeout.tv_sec = 0;
340 connect_timeout.tv_usec = 10000;
341 const struct sockaddr_in race_target = LoopbackAddress(1, network_port);
342 for (int index = 0; index < kRaceFillConnections; ++index) {
343 const int fd = socket(AF_INET, SOCK_STREAM, 0);
344 if (fd < 0 || fd >= FD_SETSIZE) {
345 if (fd >= 0) {
346 close(fd);
347 }
348 continue;
349 }
350 (void)setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &connect_timeout, sizeof(connect_timeout));
351 int connect_result;
352 do {
353 connect_result = connect(fd, reinterpret_cast<const struct sockaddr*>(&race_target), sizeof(race_target));
354 } while (connect_result != 0 && errno == EINTR);
355 if (connect_result != 0) {
356 close(fd);
357 continue;
358 }
359 dummy_fds_.push_back(fd);
360 }
361
362 return true;
363}
364
366void HappyEyeballsMockServer::CloseListeners() {
367 for (int& fd : dummy_fds_) {
368 CloseFd(&fd);
369 }
370 dummy_fds_.clear();
371 CloseFd(&race_fd_);
372 CloseFd(&serve_fd_);
373 listener_port_ = 0;
374 serve_address_.clear();
375}
376
381bool HappyEyeballsMockServer::AcceptPending() {
382 bool accepted_any = false;
383
384 int accepted_this_call = 0;
385 while (accepted_this_call < kMaxAcceptsPerCall) {
386 const int fd = accept(serve_fd_, nullptr, nullptr);
387 if (fd < 0) {
388 if (errno == EINTR) {
389 continue;
390 }
391 break;
392 }
393 ++accepted_this_call;
394 accepted_any = true;
395 ++accepted_peer_count_;
396
397 if (!MakeNonBlocking(fd, true) || peers_.size() >= kMaxAcceptedPeers) {
398 close(fd);
399 continue;
400 }
401
402 const curl::fuzzer::proto::Connection* script = ScriptForAccept(accepted_peer_count_ - 1);
403 if (script == nullptr) {
404 close(fd);
405 continue;
406 }
407
408 Peer peer;
409 peer.fd = fd;
410 peer.script = script;
411 peer.pending = script->initial_response();
412 peers_.push_back(std::move(peer));
413 }
414 return accepted_any;
415}
416
420bool HappyEyeballsMockServer::ServicePeers() {
421 bool made_progress = false;
422
423 for (std::size_t index = 0; index < peers_.size();) {
424 Peer& peer = peers_[index];
425 bool keep_peer = peer.fd >= 0;
426
427 unsigned char scratch[kReadBufferSize];
428 while (keep_peer) {
429 const ssize_t count = recv(peer.fd, scratch, sizeof(scratch), 0);
430 if (count > 0) {
431 made_progress = true;
432 continue;
433 }
434 if (count == 0) {
435 keep_peer = false;
436 made_progress = true;
437 } else if (errno == EINTR) {
438 continue;
439 } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
440 keep_peer = false;
441 made_progress = true;
442 }
443 break;
444 }
445
446 while (keep_peer && peer.pending_offset >= peer.pending.size()) {
447 peer.pending.clear();
448 peer.pending_offset = 0;
449 if (peer.script != nullptr && peer.next_chunk < RawChunkCount(*peer.script)) {
450 peer.pending = peer.script->on_readable(static_cast<int>(peer.next_chunk++));
451 if (!peer.pending.empty()) {
452 break;
453 }
454 } else {
455 break;
456 }
457 }
458
459 while (keep_peer && peer.pending_offset < peer.pending.size()) {
460 const ssize_t count = send(peer.fd, peer.pending.data() + peer.pending_offset,
461 peer.pending.size() - peer.pending_offset, MSG_NOSIGNAL);
462 if (count > 0) {
463 peer.pending_offset += static_cast<std::size_t>(count);
464 made_progress = true;
465 } else if (count < 0 && errno == EINTR) {
466 continue;
467 } else if (count < 0 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
468 break;
469 } else {
470 keep_peer = false;
471 made_progress = true;
472 }
473 }
474
475 if (keep_peer && peer.pending_offset >= peer.pending.size() &&
476 (peer.script == nullptr || peer.next_chunk >= RawChunkCount(*peer.script)) && !peer.sent_final) {
477 (void)shutdown(peer.fd, SHUT_WR);
478 peer.sent_final = true;
479 made_progress = true;
480 }
481
482 if (!keep_peer) {
483 ClosePeer(&peer);
484 peers_.erase(peers_.begin() + static_cast<std::ptrdiff_t>(index));
485 continue;
486 }
487 ++index;
488 }
489 return made_progress;
490}
491
493void HappyEyeballsMockServer::ClosePeer(Peer* peer) {
494 if (peer != nullptr) {
495 CloseFd(&peer->fd);
496 }
497}
498
501const curl::fuzzer::proto::Connection* HappyEyeballsMockServer::ScriptForAccept(std::size_t index) const {
502 if (scenario_ == nullptr) {
503 return nullptr;
504 }
505 // Each race produces exactly one serving connection, so the accepted order
506 // maps one-to-one onto the primary script followed by the subsequent
507 // connections (mirroring MockServer::SetScripts). A redirect or HTTP/1.1
508 // retry must therefore receive subsequent_connections[0], not a repeat of
509 // the primary script.
510 if (index == 0) {
511 return &scenario_->connection();
512 }
513 const std::size_t subsequent_index = index - 1;
514 if (subsequent_index < static_cast<std::size_t>(scenario_->subsequent_connections_size()) &&
515 subsequent_index < scenario_limits::kMaxConnections - 1) {
516 return &scenario_->subsequent_connections(static_cast<int>(subsequent_index));
517 }
518 return nullptr;
519}
520
521} // namespace proto_fuzzer
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Run a bounded perform loop that also accepts real loopback connections and serves their response scri...
curl_socket_t HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr *address) override
Hand curl a fresh unconnected socket for every address its happy-eyeballs filter decides to race.
~HappyEyeballsMockServer() override
Release listeners and accepted loopback peers.
void Install(CURL *easy) override
Install the shared socket callbacks and remember the easy handle for the later resolve/port/timeout s...
HappyEyeballsMockServer()
Construct an idle peer with no listener or accepted connections.
SocketSetupDisposition GetSocketSetupDisposition(curl_socket_t curlfd, curlsocktype purpose) const override
Stream peers must follow curl's ordinary connect path; unlike the socketpair mocks,...
void ConfigureRequestData(ScenarioRequestData *request_data) override
Replace the resolver lane's single-address mapping with a two-address entry for the fixed canonical h...
virtual void Install(CURL *easy)
Install the common OPENSOCKETFUNCTION / OPENSOCKETDATA / SOCKOPTFUNCTION callbacks on 'easy'.
static int WaitOnMultiFdset(CURLM *multi, CURLMcode *rc)
Wait on curl's fdset with a short timeout.
static constexpr int kMaxDriveIterations
Hard operation budget for one scenario.
static constexpr int kMaxIdleIterations
Consecutive no-progress budget for ordinary scenarios.
Builds HTTP headers or TELNET options, MIME state, and upload callbacks whose storage or userdata lib...
std::unique_ptr< curl_slist, CurlSlistDeleter > CurlSlistPtr
Unique ownership of a caller-owned curl_slist.
Definition curl_raii.h:60
HappyEyeballsMockServer — a loopback peer that lets curl's happy-eyeballs connection filter race seve...
SocketSetupDisposition
Tell curl whether an application-provided socket still needs its normal connect/options setup or alre...
Shared complexity limits for structured scenarios.
constexpr std::size_t kMaxResponseChunks
Enough response boundaries to exercise incremental parsers while keeping per-perform delivery work fi...