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.h
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 MockConnection and MockServer — the in-process peer that feeds
9
///        canned response bytes to libcurl over a socketpair.
10
11
#ifndef PROTO_FUZZER_MOCK_SERVER_H_
12
#define PROTO_FUZZER_MOCK_SERVER_H_
13
14
#include <curl/curl.h>
15
16
#include <array>
17
#include <cstddef>
18
#include <memory>
19
#include <string>
20
#include <vector>
21
22
#include "curl_fuzzer.pb.h"
23
#include "proto_fuzzer/mock_server_base.h"
24
#include "proto_fuzzer/scenario_limits.h"
25
26
namespace proto_fuzzer {
27
28
class MockConnection {
29
 public:
30
  MockConnection();
31
  virtual ~MockConnection();
32
33
  MockConnection(const MockConnection&) = delete;
34
  MockConnection& operator=(const MockConnection&) = delete;
35
36
  virtual bool ok() const;
37
  curl_socket_t take_client_fd();
38
  int server_fd() const;
39
40
  /// Return the capacity curl's endpoint reports for queued client writes.
41
  /// Protocol mocks use this before transferring fd ownership when their
42
  /// synchronous send path cannot rely on the outer driver to make room.
43
  /// @return SO_SNDBUF in bytes, or zero when it cannot be queried.
44
  std::size_t client_send_buffer_size() const;
45
46
  /// Ensure curl's endpoint reports at least `minimum` bytes of send buffer,
47
  /// requesting a larger SO_SNDBUF when the platform default is smaller.
48
  /// @param minimum Smallest acceptable reported capacity in bytes.
49
  /// @return true when the queried postcondition holds.
50
  bool EnsureClientSendBufferSize(std::size_t minimum);
51
52
  virtual bool WriteAll(const unsigned char* data, std::size_t size);
53
  /// Advance incoming transport work according to the configured per-call
54
  /// limit. Plaintext connections report bytes consumed; layered transports
55
  /// may also report handshake state changes so the bounded outer loop does
56
  /// not mistake useful protocol progress for an idle connection.
57
  /// @return amount of transport progress made during this call.
58
  virtual std::size_t DrainIncoming();
59
  void ReadAvailable(std::string* out);
60
  virtual void ShutdownWrite();
61
62
  /// Apply deterministic backpressure knobs. Set SO_RCVBUF on the server
63
  /// side (if recv_buf_bytes > 0) to cap how much curl can write before it
64
  /// short-writes / EAGAINs, and cap DrainIncoming()'s per-call byte budget
65
  /// (0 = unlimited). Must be called before any traffic for the recv_buf
66
  /// setting to matter.
67
  /// @param recv_buf_bytes SO_RCVBUF size in bytes, or 0 to leave default.
68
  /// @param drain_limit    Max bytes drained per DrainIncoming call, 0 for unlimited.
69
  void ApplyBackpressure(int recv_buf_bytes, std::size_t drain_limit);
70
71
 private:
72
  int server_fd_;
73
  int client_fd_;
74
  std::size_t drain_limit_;
75
};
76
77
/// @class proto_fuzzer::MockServer
78
/// @brief HTTP in-process mock peer. Assigns one bounded response script to
79
///        each socket curl opens, allowing redirects and authentication
80
///        retries to progress without permitting an unbounded connection
81
///        graph. WebSocketMockServer keeps its separate single-socket model.
82
class MockServer : public MockServerBase {
83
 public:
84
  MockServer();
85
  ~MockServer() override;
86
87
  /// Borrow the primary and bounded follow-on HTTP scripts from `scenario`.
88
  /// The caller must keep the scenario alive and unmodified until the current
89
  /// synchronous drive has finished. RunScenario already provides exactly
90
  /// that lifetime, so retaining pointers avoids copying response bytes before
91
  /// curl has even requested the corresponding socket or chunk.
92
  void SetScripts(const curl::fuzzer::proto::Scenario& scenario);
93
94
  /// Keep completed response sockets writable instead of half-closing them.
95
  /// The multi-transfer lane uses this to let a queued easy handle reuse an
96
  /// HTTP/1.1 connection; ordinary protocol drives retain close-on-completion.
97
  /// @param keep_open Whether the peer should suppress its response-side FIN.
98
  void SetKeepConnectionsOpen(bool keep_open);
99
100
  /// Preload the bounded HTTP response, half-close the peer, and invoke
101
  /// curl_easy_perform. This avoids a helper thread while guaranteeing that
102
  /// curl never waits for the outer chunk-delivery loop.
103
  void DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario, bool use_events = false) override;
104
105
  /// Exercise direct send/receive APIs after a bounded CONNECT_ONLY setup.
106
  ConnectOnlyRunStats DriveConnectOnlyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) override;
107
108
  /// Deliver one queued response chunk.
109
  /// @return true when a chunk was consumed from the script.
110
  bool DeliverNextChunk();
111
  bool has_more_chunks() const;
112
113
  /// Service one deterministic application event-loop turn by draining all
114
  /// request bytes currently available and releasing at most one response
115
  /// chunk. Exposed for the shared-multi driver, which owns the outer loop.
116
  /// @return true when any request or response byte advanced.
117
  bool ServiceConnections();
118
119
  /// Report how many peer sockets curl opened during the current script run.
120
  /// @return Number of response scripts assigned to live or retired sockets.
121
  std::size_t opened_connection_count() const;
122
123
 protected:
124
  /// Construct the transport used for one HTTP exchange. HTTPS overrides this
125
  /// factory with a TLS record layer while retaining the same bounded response
126
  /// scripting and redirect lifetimes as plaintext HTTP.
127
  /// @return a new connection, whose ok() result is checked before use.
128
  virtual std::unique_ptr<MockConnection> CreateConnection();
129
130
  /// Release every current and retired connection before resetting transport-
131
  /// specific state. Derived servers call this from their destructors when
132
  /// connection objects borrow state owned by the derived class.
133
  void ResetConnections();
134
135
  /// Observe curl while its connection filters are still attached. The
136
  /// default HTTP peer has no transport-specific state to inspect; layered
137
  /// transports override this instead of querying stale state after the
138
  /// multi handle has been dismantled.
139
  virtual void ObserveActiveTransfer(CURL* easy);
140
141
  curl_socket_t HandleOpenSocket(curlsocktype purpose = CURLSOCKTYPE_IPCXN,
142
                                 struct curl_sockaddr* address = nullptr) override;
143
  void RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) override;
144
145
 private:
146
  /// One socket's borrowed response configuration plus its delivery cursor.
147
  /// The fixed script array is fully populated before curl runs, so both this
148
  /// object and its pointer into the caller-owned Scenario remain stable across
149
  /// callbacks. Counts record the exact runtime-visible prefix: raw chunks come
150
  /// first and structured frames consume only the remaining shared budget.
151
  struct ConnectionScript {
152
    const curl::fuzzer::proto::Connection* connection = nullptr;
153
    std::size_t raw_chunk_count = 0;
154
    std::size_t frame_chunk_count = 0;
155
    std::size_t next_chunk = 0;
156
157
    /// @return Number of raw and structured chunks visible to the runtime.
158
2.52M
    std::size_t chunk_count() const { return raw_chunk_count + frame_chunk_count; }
159
  };
160
161
  /// Drain client request bytes from both the current socket and sockets curl
162
  /// has moved past. Keeping old peers responsive prevents a late write/close
163
  /// on a redirect source connection from stalling the new exchange.
164
  /// @return total bytes drained during this call.
165
  std::size_t DrainIncomingConnections();
166
167
  /// Drive the HTTP exchange through curl_multi_socket_action using the
168
  /// callback state owned by MockServerBase::DriveScenario.
169
  /// @param multi Multi handle containing `easy`.
170
  /// @return after curl finishes or a deterministic idle/operation cap wins.
171
  void RunSocketActionLoop(CURLM* multi, CURL* easy);
172
173
  std::array<ConnectionScript, scenario_limits::kMaxConnections> scripts_;
174
  std::size_t script_count_;
175
  std::size_t next_script_;
176
  ConnectionScript* active_script_;
177
178
  /// True only while HandleOpenSocket is preparing a synchronous easy drive.
179
  /// Every response chunk must be queued before the callback returns because
180
  /// curl_easy_perform does not yield control to the mock.
181
  bool preload_all_chunks_;
182
183
  /// Suppress response-side half-close after the final scripted chunk. This
184
  /// is false for every historical target and enabled only by MultiPlan.
185
  bool keep_connections_open_;
186
187
  /// Old server halves must outlive their active role: libcurl owns the client
188
  /// fds and may close or briefly revisit them after opening the next socket.
189
  /// Destroying a MockConnection at that boundary would turn valid lifecycle
190
  /// traffic into harness-generated ECONNRESET/SIGPIPE behavior.
191
  std::vector<std::unique_ptr<MockConnection>> previous_connections_;
192
};
193
194
}  // namespace proto_fuzzer
195
196
#endif  // PROTO_FUZZER_MOCK_SERVER_H_