14#include <sys/select.h>
15#include <sys/socket.h>
32namespace proto_fuzzer {
43constexpr int kMaxConnectOnlyIoIterations = 16;
44constexpr std::size_t kConnectOnlyBufferSize = 1024;
48bool FdFitsInFdSet(
int fd) {
return fd >= 0 && fd < FD_SETSIZE; }
61 if (socketpair(AF_UNIX, SOCK_STREAM, 0, fds) != 0) {
66 if (!FdFitsInFdSet(fds[0]) || !FdFitsInFdSet(fds[1])) {
73 int flags = fcntl(fds[0], F_GETFL, 0);
74 if (flags < 0 || fcntl(fds[0], F_SETFL, flags | O_NONBLOCK) < 0) {
90 : server_fd_(adopted_server_fd), client_fd_(-1), drain_limit_(0), incoming_data_observer_(nullptr) {
94 const int flags = fcntl(server_fd_, F_GETFL, 0);
95 if (!FdFitsInFdSet(server_fd_) || flags < 0 || fcntl(server_fd_, F_SETFL, flags | O_NONBLOCK) < 0) {
103 if (server_fd_ >= 0) {
106 if (client_fd_ >= 0) {
124 if (client_fd_ < 0) {
128 socklen_t length =
sizeof(size);
129 if (getsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &size, &length) != 0 || size <= 0) {
132 return static_cast<std::size_t
>(size);
145 if (client_fd_ < 0 || minimum >
static_cast<std::size_t
>(std::numeric_limits<int>::max())) {
148 const int requested =
static_cast<int>(minimum);
149 if (setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &requested,
sizeof(requested)) != 0) {
160 if (client_fd_ < 0) {
163 const int flags = fcntl(client_fd_, F_GETFL, 0);
164 if (flags < 0 || fcntl(client_fd_, F_SETFL, flags | O_NONBLOCK) < 0) {
168 static constexpr unsigned char kPadding[4096] = {};
171 const ssize_t written = ::send(client_fd_, kPadding,
sizeof(kPadding), MSG_NOSIGNAL);
175 full = written < 0 && (errno == EAGAIN || errno == EWOULDBLOCK);
178 if (fcntl(client_fd_, F_SETFL, flags) < 0) {
190 return static_cast<curl_socket_t
>(fd);
202 if (server_fd_ < 0) {
205 std::size_t written = 0;
206 while (written < size) {
207 ssize_t n = ::send(server_fd_, data + written, size - written, MSG_NOSIGNAL);
211 written +=
static_cast<std::size_t
>(n);
226 if (server_fd_ < 0) {
229 unsigned char scratch[4096];
230 std::size_t drained = 0;
231 while (drain_limit_ == 0 || drained < drain_limit_) {
232 std::size_t want =
sizeof(scratch);
233 if (drain_limit_ != 0) {
234 const std::size_t remaining = drain_limit_ - drained;
235 if (remaining < want) {
239 ssize_t n = ::read(server_fd_, scratch, want);
244 drained +=
static_cast<std::size_t
>(n);
259 if (incoming_data_observer_ !=
nullptr) {
260 incoming_data_observer_->ObserveIncomingData(data, size);
273 if (recv_buf_bytes > 0) {
274 if (server_fd_ >= 0) {
275 (void)setsockopt(server_fd_, SOL_SOCKET, SO_RCVBUF, &recv_buf_bytes,
sizeof(recv_buf_bytes));
277 if (client_fd_ >= 0) {
278 (void)setsockopt(client_fd_, SOL_SOCKET, SO_SNDBUF, &recv_buf_bytes,
sizeof(recv_buf_bytes));
281 drain_limit_ = drain_limit;
289 if (server_fd_ < 0 || out ==
nullptr) {
292 unsigned char scratch[4096];
294 ssize_t n = ::read(server_fd_, scratch,
sizeof(scratch));
298 out->append(
reinterpret_cast<const char*
>(scratch),
static_cast<std::size_t
>(n));
304 if (server_fd_ < 0) {
307 ::shutdown(server_fd_, SHUT_WR);
320 : MockServerBase(drive_policy),
323 active_script_(nullptr),
324 preload_all_chunks_(false),
325 keep_connections_open_(false) {}
345 const auto append_script = [
this](
const curl::fuzzer::proto::Connection&
connection) {
346 ConnectionScript& script = scripts_[script_count_++];
350 script.frame_chunk_count = std::min<std::size_t>(frame_budget,
connection.server_frames_size());
351 script.next_chunk = 0;
354 append_script(scenario.connection());
355 const std::size_t subsequent_count = std::min<std::size_t>(
357 for (std::size_t i = 0; i < subsequent_count; ++i) {
358 append_script(scenario.subsequent_connections(
static_cast<int>(i)));
370 return active_script_ !=
nullptr && active_script_->next_chunk < active_script_->chunk_count();
384 active_script_ =
nullptr;
386 previous_connections_.clear();
407 if (next_script_ >= script_count_) {
410 return CURL_SOCKET_BAD;
417 previous_connections_.push_back(std::move(
connection_));
420 active_script_ = &scripts_[next_script_++];
421 const curl::fuzzer::proto::Connection& script_connection = *active_script_->connection;
425 active_script_ =
nullptr;
426 return CURL_SOCKET_BAD;
432 const std::uint32_t int_max =
static_cast<std::uint32_t
>(std::numeric_limits<int>::max());
433 const auto& backpressure = script_connection.backpressure();
434 const int recv_buf_bytes =
static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max));
435 connection_->ApplyBackpressure(recv_buf_bytes,
static_cast<std::size_t
>(backpressure.drain_limit()));
437 const std::string& initial_response = script_connection.initial_response();
438 if (!initial_response.empty()) {
439 if (!
connection_->WriteAll(
reinterpret_cast<const unsigned char*
>(initial_response.data()),
440 initial_response.size())) {
442 active_script_ =
nullptr;
443 return CURL_SOCKET_BAD;
446 if (preload_all_chunks_) {
451 if (active_script_->chunk_count() == 0 && !keep_connections_open_) {
473 preload_all_chunks_ =
true;
474 (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 50L);
475 (void)curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 50L);
483 (void)curl_easy_setopt(easy, CURLOPT_CONNECT_ONLY, 0L);
485 preload_all_chunks_ =
false;
499 preload_all_chunks_ =
true;
500 (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 50L);
501 (void)curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 50L);
502 (void)curl_easy_setopt(easy, CURLOPT_CONNECT_ONLY, 1L);
504 preload_all_chunks_ =
false;
509 constexpr std::string_view kFallbackRequest =
"GET / HTTP/1.0\r\n\r\n";
510 const std::string& upload = scenario.upload().data();
511 const std::string_view source = upload.empty() ? kFallbackRequest : std::string_view(upload);
514 for (
int iteration = 0; stats.
sent_bytes < outgoing.size() && iteration < kMaxConnectOnlyIoIterations; ++iteration) {
515 std::size_t sent = 0;
517 curl_easy_send(easy, outgoing.data() + stats.
sent_bytes, outgoing.size() - stats.
sent_bytes, &sent);
525 std::array<unsigned char, kConnectOnlyBufferSize> incoming{};
526 for (
int iteration = 0; iteration < kMaxConnectOnlyIoIterations; ++iteration) {
527 std::size_t received = 0;
528 stats.
recv_result = curl_easy_recv(easy, incoming.data(), incoming.size(), &received);
531 (stats.
recv_result == CURLE_OK && received == 0)) {
546 const std::size_t chunk_index = active_script_->next_chunk++;
547 const auto& script_connection = *active_script_->connection;
548 if (chunk_index < active_script_->raw_chunk_count) {
549 const std::string& chunk = script_connection.on_readable(
static_cast<int>(chunk_index));
550 if (!chunk.empty()) {
551 connection_->WriteAll(
reinterpret_cast<const unsigned char*
>(chunk.data()), chunk.size());
558 const std::size_t frame_index = chunk_index - active_script_->raw_chunk_count;
559 const std::string chunk =
SerializeWebSocketFrame(script_connection.server_frames(
static_cast<int>(frame_index)));
560 if (!chunk.empty()) {
561 connection_->WriteAll(
reinterpret_cast<const unsigned char*
>(chunk.data()), chunk.size());
575std::size_t MockServer::DrainIncomingConnections() {
576 std::size_t drained = 0;
577 for (
const auto& previous : previous_connections_) {
578 drained += previous->DrainIncoming();
595 bool made_progress = DrainIncomingConnections() != 0;
599 return made_progress;
614void MockServer::RunSocketActionLoop(CURLM* multi, CURL* easy) {
616 if (driver ==
nullptr) {
620 int still_running = 1;
621 CURLMcode rc = driver->
Start(&still_running);
624 int idle_iterations = 0;
625 int drive_iterations = 0;
629 const MultiSocketDriver::DriveResult drive_result = driver->
DriveReady(&still_running);
630 rc = drive_result.
code;
633 made_progress = drive_result.made_progress || made_progress;
658 RunSocketActionLoop(multi, easy);
662 int still_running = 1;
663 int idle_iterations = 0;
664 int drive_iterations = 0;
665 bool pollset_probed =
false;
669 const bool timed_drive =
670 std::any_of(scripts_.begin(), scripts_.begin() + script_count_, [](
const ConnectionScript& script) {
671 const auto& backpressure = script.connection->backpressure();
672 return backpressure.recv_buf_bytes() != 0 || backpressure.drain_limit() != 0;
676 CURLMcode rc = CURLM_OK;
678 while (still_running && idle_iterations < idle_limit && drive_iterations++ <
kMaxDriveIterations) {
679 bool made_progress =
false;
680 const int running_before = still_running;
681 rc = curl_multi_perform(multi, &still_running);
682 if (rc != CURLM_OK) {
687 made_progress = still_running != running_before;
688 if (still_running && ((timed_drive && !pollset_probed) || repeated_pollset_probe)) {
695 pollset_probed =
true;
697 if (!still_running) {
718 if (rc != CURLM_OK) {
Receives bytes after any transport decoding.
bool EnsureClientSendBufferSize(std::size_t minimum)
Ensure curl's endpoint reports at least minimum bytes of send buffer, requesting a larger SO_SNDBUF w...
virtual bool WriteAll(const unsigned char *data, std::size_t size)
Write 'size' bytes from 'data' to the server fd, looping until the whole buffer is sent or a short/fa...
void ReadAvailable(std::string *out)
Non-blocking read: append whatever bytes are currently available on the server fd to 'out'.
virtual void ShutdownWrite()
Signal end-of-response to libcurl by half-closing the write side.
void NotifyIncomingData(const unsigned char *data, std::size_t size)
Forward one application-data fragment to the optional observer.
virtual std::size_t DrainIncoming()
Advance incoming transport work according to the configured per-call limit.
void SetIncomingDataObserver(IncomingDataObserver *observer)
Register a non-owning sink for bytes consumed from curl.
MockConnection()
Construct a non-blocking AF_UNIX/SOCK_STREAM socketpair.
std::size_t client_send_buffer_size() const
Query the capacity curl's endpoint reports for queued client writes while this object still owns that...
virtual ~MockConnection()
Close the server-side fd (and the client-side fd if it was never handed off via take_client_fd()).
bool FillClientSendBuffer()
Saturate curl's endpoint before ownership is transferred.
void ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit)
Apply deterministic backpressure knobs before traffic begins.
curl_socket_t take_client_fd()
Hand the client-side fd to libcurl.
std::unique_ptr< MockConnection > connection_
The per-scenario MockConnection, lazily created by HandleOpenSocket().
MockConnection * connection()
static int WaitOnMultiFdset(CURLM *multi, CURLMcode *rc)
Wait on curl's fdset with a short timeout.
static constexpr int kMaxTimedIdleIterations
Explicit backpressure scenarios get a larger idle budget and may use the short timed wait above.
static void ProbeMultiPollset(CURLM *multi)
Ask curl to construct and inspect its connection-filter pollset without waiting.
static constexpr int kMaxDriveIterations
Hard operation budget for one scenario.
void ResumeResponseIfRequested(CURL *easy)
Resume receive-side callback output at one bounded drive boundary when the dedicated API plan request...
static constexpr int kMaxIdleIterations
Consecutive no-progress budget for ordinary scenarios.
MultiSocketDriver * multi_socket_driver()
Expose the callback state installed for the current socket-action drive.
CURLcode DriveEasyScenario(CURL *easy, const curl::fuzzer::proto::Scenario &scenario, bool use_events=false) override
Preload all bounded response bytes from inside OPENSOCKETFUNCTION, where the mock still owns both soc...
bool has_more_chunks() const
void ResetConnections()
Release all current and retired socketpairs before resetting transport- specific state.
ConnectOnlyRunStats DriveConnectOnlyScenario(CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Establish only the HTTP transport, then pass bounded application bytes through curl's public direct-I...
std::size_t opened_connection_count() const
Report how many peer sockets curl opened during the current script run.
bool ServiceConnections()
Service one deterministic application event-loop turn by draining all available request bytes and rel...
~MockServer() override
Default destructor; current and previous MockConnections clean up their server-side descriptors only ...
void SetKeepConnectionsOpen(bool keep_open)
Keep completed response sockets writable instead of half-closing them.
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Seed the mock from the scenario, then drive the perform loop until curl is done or a deterministic op...
void SetScripts(const curl::fuzzer::proto::Scenario &scenario)
Build the complete borrowed script table before entering curl.
virtual std::unique_ptr< MockConnection > CreateConnection()
Construct the transport used for one HTTP exchange.
virtual void ObserveActiveTransfer(CURL *easy)
Observe curl while its connection filters are still attached.
virtual bool SupportsRepeatedPollsetProbe() const
Ordinary HTTP and compatibility peers retain the historical perform-only loop even if an append-only ...
MockServer(MultiDrivePolicy drive_policy=MultiDrivePolicy::kPerform)
Construct an idle MockServer with no scripted responses or open peers.
curl_socket_t HandleOpenSocket(curlsocktype purpose=CURLSOCKTYPE_IPCXN, struct curl_sockaddr *address=nullptr) override
Called by the OPENSOCKETFUNCTION trampoline in the base class.
bool DeliverNextChunk()
Push the next queued chunk.
Owns the socket and timer callback state required by libcurl's event-driven multi API.
CURLMcode Start(int *running_handles)
Start libcurl's socket state machine through the documented timeout pseudo-socket without waiting for...
DriveResult DriveReady(int *running_handles)
Run one bounded, non-blocking event-loop turn: poll the callback-provided watch set with a zero timeo...
CURLcode curl_easy_perform_ev(CURL *easy)
MockConnection and MockServer — the in-process peer that feeds canned response bytes to libcurl over ...
MultiDrivePolicy
Fixed policy for interpreting a scenario's multi-drive fields.
Bounded event-loop state for driving curl_multi_socket_action.
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...
constexpr std::size_t kMaxConnections
Four sockets cover redirect/auth flows without permitting mutation-sized connection chains to dominat...
constexpr std::size_t kMaxApiStringBytes
URL escaping can expand input threefold.
Results from the bounded CONNECT_ONLY public-API probe.
CURLcode connect_result
Result of establishing the connect-only transport.
CURLcode recv_result
Final result returned by curl_easy_recv.
std::size_t received_bytes
Total application bytes returned by curl_easy_recv.
std::size_t sent_bytes
Total application bytes accepted by curl_easy_send.
CURLcode send_result
Final result returned by curl_easy_send.
CURLMcode code
libcurl's result for the last socket action in the turn.
std::string SerializeWebSocketFrame(const curl::fuzzer::proto::WebSocketFrame &frame)
Serialise a proto WebSocketFrame into RFC 6455 wire bytes.
Serialize a proto WebSocketFrame message into RFC 6455 wire bytes.