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.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. ScenarioRunner 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
  /// Preload the bounded HTTP response, half-close the peer, and invoke
95
  /// curl_easy_perform. This avoids a helper thread while guaranteeing that
96
  /// curl never waits for the outer chunk-delivery loop.
97
  void DriveEasyScenario(CURL* easy, const curl::fuzzer::proto::Scenario& scenario) override;
98
99
  /// Deliver one queued response chunk.
100
  /// @return true when a chunk was consumed from the script.
101
  bool DeliverNextChunk();
102
  bool has_more_chunks() const;
103
104
 protected:
105
  /// Construct the transport used for one HTTP exchange. HTTPS overrides this
106
  /// factory with a TLS record layer while retaining the same bounded response
107
  /// scripting and redirect lifetimes as plaintext HTTP.
108
  /// @return a new connection, whose ok() result is checked before use.
109
  virtual std::unique_ptr<MockConnection> CreateConnection();
110
111
  /// Release every current and retired connection before resetting transport-
112
  /// specific state. Derived servers call this from their destructors when
113
  /// connection objects borrow state owned by the derived class.
114
  void ResetConnections();
115
116
  /// Observe curl while its connection filters are still attached. The
117
  /// default HTTP peer has no transport-specific state to inspect; layered
118
  /// transports override this instead of querying stale state after the
119
  /// multi handle has been dismantled.
120
  virtual void ObserveActiveTransfer(CURL* easy);
121
122
  curl_socket_t HandleOpenSocket(curlsocktype purpose = CURLSOCKTYPE_IPCXN,
123
                                 struct curl_sockaddr* address = nullptr) override;
124
  void RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) override;
125
126
 private:
127
  /// One socket's borrowed response configuration plus its delivery cursor.
128
  /// The fixed script array is fully populated before curl runs, so both this
129
  /// object and its pointer into the caller-owned Scenario remain stable across
130
  /// callbacks. Counts record the exact runtime-visible prefix: raw chunks come
131
  /// first and structured frames consume only the remaining shared budget.
132
  struct ConnectionScript {
133
    const curl::fuzzer::proto::Connection* connection = nullptr;
134
    std::size_t raw_chunk_count = 0;
135
    std::size_t frame_chunk_count = 0;
136
    std::size_t next_chunk = 0;
137
138
    /// @return Number of raw and structured chunks visible to the runtime.
139
237k
    std::size_t chunk_count() const { return raw_chunk_count + frame_chunk_count; }
140
  };
141
142
  /// Drain client request bytes from both the current socket and sockets curl
143
  /// has moved past. Keeping old peers responsive prevents a late write/close
144
  /// on a redirect source connection from stalling the new exchange.
145
  /// @return total bytes drained during this call.
146
  std::size_t DrainIncomingConnections();
147
148
  /// Service one deterministic application event-loop turn by draining all
149
  /// request bytes currently available and releasing at most one response
150
  /// chunk. Both multi APIs use this ordering so selecting socket_action
151
  /// changes only how readiness reaches curl, not the mock protocol script.
152
  /// @return true when any request or response byte advanced.
153
  bool ServiceConnections();
154
155
  /// Drive the HTTP exchange through curl_multi_socket_action using the
156
  /// callback state owned by MockServerBase::DriveScenario.
157
  /// @param multi Multi handle containing `easy`.
158
  /// @return after curl finishes or a deterministic idle/operation cap wins.
159
  void RunSocketActionLoop(CURLM* multi, CURL* easy);
160
161
  std::array<ConnectionScript, scenario_limits::kMaxConnections> scripts_;
162
  std::size_t script_count_;
163
  std::size_t next_script_;
164
  ConnectionScript* active_script_;
165
166
  /// True only while HandleOpenSocket is preparing a synchronous easy drive.
167
  /// Every response chunk must be queued before the callback returns because
168
  /// curl_easy_perform does not yield control to the mock.
169
  bool preload_all_chunks_;
170
171
  /// Old server halves must outlive their active role: libcurl owns the client
172
  /// fds and may close or briefly revisit them after opening the next socket.
173
  /// Destroying a MockConnection at that boundary would turn valid lifecycle
174
  /// traffic into harness-generated ECONNRESET/SIGPIPE behavior.
175
  std::vector<std::unique_ptr<MockConnection>> previous_connections_;
176
};
177
178
}  // namespace proto_fuzzer
179
180
#endif  // PROTO_FUZZER_MOCK_SERVER_H_