/src/curl_fuzzer/proto_fuzzer/dict_mock_server.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 the command-aware in-process DICT peer. |
9 | | |
10 | | #include "proto_fuzzer/dict_mock_server.h" |
11 | | |
12 | | #include <cstdlib> |
13 | | #include <string> |
14 | | #include <string_view> |
15 | | |
16 | | #include "proto_fuzzer/mock_server.h" |
17 | | #include "proto_fuzzer/scenario_limits.h" |
18 | | |
19 | | namespace proto_fuzzer { |
20 | | |
21 | | namespace { |
22 | | |
23 | | // curl builds and sends the complete DICT request synchronously before the |
24 | | // outer peer loop can drain it. RFC 2229 escaping can double every retained |
25 | | // path byte; fixed command/version text fits comfortably in the remaining |
26 | | // headroom. Rejecting a surprising platform socket configuration is safer |
27 | | // than entering curl's blocking POLLOUT wait without an operation deadline. |
28 | | constexpr std::size_t kMinClientSendBufferBytes = 64 * 1024; |
29 | | constexpr std::size_t kMaxBufferedClientBytes = 2 * scenario_limits::kMaxDictHostPathBytes + 4 * 1024; |
30 | | static_assert(kMaxBufferedClientBytes < kMinClientSendBufferBytes, |
31 | | "DICT mutation cap exceeds the verified client send buffer"); |
32 | | |
33 | | /// Temporarily replace one process environment value and restore the caller's |
34 | | /// exact state on scope exit. libFuzzer drives inputs serially, so the scope |
35 | | /// around one synchronous DICT drive also scopes curl's debug socket setting |
36 | | /// to the connection-filter construction that consumes it. |
37 | | class ScopedEnvironmentOverride { |
38 | | public: |
39 | 3.06k | ScopedEnvironmentOverride(const char* name, const char* value) : name_(name) { |
40 | 3.06k | const char* previous = std::getenv(name); |
41 | 3.06k | if (previous != nullptr) { |
42 | 0 | had_previous_ = true; |
43 | 0 | previous_ = previous; |
44 | 0 | } |
45 | 3.06k | active_ = setenv(name, value, 1) == 0; |
46 | 3.06k | } |
47 | | |
48 | 3.06k | ~ScopedEnvironmentOverride() { |
49 | 3.06k | if (!active_) { |
50 | 0 | return; |
51 | 0 | } |
52 | 3.06k | if (had_previous_) { |
53 | 0 | (void)setenv(name_.c_str(), previous_.c_str(), 1); |
54 | 3.06k | } else { |
55 | 3.06k | (void)unsetenv(name_.c_str()); |
56 | 3.06k | } |
57 | 3.06k | } |
58 | | |
59 | | ScopedEnvironmentOverride(const ScopedEnvironmentOverride&) = delete; |
60 | | ScopedEnvironmentOverride& operator=(const ScopedEnvironmentOverride&) = delete; |
61 | | |
62 | | private: |
63 | | std::string name_; |
64 | | std::string previous_; |
65 | | bool had_previous_ = false; |
66 | | bool active_ = false; |
67 | | }; |
68 | | |
69 | | } // namespace |
70 | | |
71 | | /// Begin without a borrowed protobuf or open socketpair. RunLoop borrows a |
72 | | /// Scenario only for the synchronous DriveScenario call in which socket |
73 | | /// callbacks can run. |
74 | 3.21k | DictMockServer::DictMockServer() : reply_sent_(false), write_closed_(false) {} |
75 | | |
76 | | /// Release the server-owned socketpair half after curl has returned its client |
77 | | /// descriptor. The out-of-line destructor keeps MockConnection's complete type |
78 | | /// local to this translation unit. |
79 | 3.21k | DictMockServer::~DictMockServer() = default; |
80 | | |
81 | | /// Remove the operation deadline so a partial DICT request write exercises |
82 | | /// sendf()'s explicit no-timeout fallback. The bounded drive and writable |
83 | | /// socketpair keep this path independent of wall-clock timing. |
84 | | /// @param easy DICT easy handle configured before the connection is opened. |
85 | 3.17k | void DictMockServer::Install(CURL* easy) { |
86 | 3.17k | MockServerBase::Install(easy); |
87 | 3.17k | (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 0L); |
88 | 3.17k | } |
89 | | |
90 | | /// Construct the ordinary plaintext transport only when its client endpoint |
91 | | /// can queue the complete bounded DICT command. sendf() owns the thread until |
92 | | /// that command is written, so the outer peer cannot create space meanwhile. |
93 | | /// @return A verified socketpair, or nullptr on a platform that cannot provide |
94 | | /// the required capacity. |
95 | 2.54k | std::unique_ptr<MockConnection> DictMockServer::CreateConnection() { |
96 | 2.54k | std::unique_ptr<MockConnection> connection = PingpongMockServer::CreateConnection(); |
97 | 2.54k | if (connection == nullptr || !connection->ok() || |
98 | 2.54k | !connection->EnsureClientSendBufferSize(kMinClientSendBufferBytes)) { |
99 | 0 | return nullptr; |
100 | 0 | } |
101 | 2.54k | return connection; |
102 | 2.54k | } |
103 | | |
104 | | /// Split each sufficiently large socket write while curl constructs and uses |
105 | | /// this DICT connection. curl's debug socket filter snapshots the environment |
106 | | /// during its first perform; restoring it before DriveScenario tears down the |
107 | | /// multi does not change that connection, and prevents a DICT input from |
108 | | /// perturbing later non-DICT inputs in a mixed process. |
109 | | /// @param multi Multi handle containing the DICT easy handle. |
110 | | /// @param easy Easy handle already installed on this mock. |
111 | | /// @param scenario DICT script borrowed for the synchronous drive. |
112 | 3.06k | void DictMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
113 | 3.06k | ScopedEnvironmentOverride partial_writes("CURL_DBG_SOCK_WPARTIAL", "50"); |
114 | 3.06k | PingpongMockServer::RunLoop(multi, easy, scenario); |
115 | 3.06k | } |
116 | | |
117 | | /// Reset DICT's response bookkeeping for a new drive. The shared base has |
118 | | /// already cleared the transport, reply cursor, transcript, upload capture, |
119 | | /// and any in-flight body phase. |
120 | 3.06k | void DictMockServer::ResetProtocolState() { |
121 | 3.06k | reply_sent_ = false; |
122 | 3.06k | write_closed_ = false; |
123 | 3.06k | } |
124 | | |
125 | | /// Queue end-of-response and half-close exactly once per drive. |
126 | 3.79k | void DictMockServer::FinishResponse() { |
127 | 3.79k | if (write_closed_) { |
128 | 1.33k | return; |
129 | 1.33k | } |
130 | 2.46k | write_closed_ = true; |
131 | 2.46k | if (connection_ != nullptr) { |
132 | 2.46k | connection_->ShutdownWrite(); |
133 | 2.46k | } |
134 | 2.46k | } |
135 | | |
136 | | /// Answer the first lookup command in the request and ignore CLIENT, which is |
137 | | /// a bare identification line. curl normally writes CLIENT, the lookup, and |
138 | | /// QUIT together and then waits for the response body; QUIT is therefore only |
139 | | /// answered when it arrives without a preceding lookup command (for example a |
140 | | /// URL with no path). |
141 | | /// @param line One newline-terminated command from curl. |
142 | 7.39k | void DictMockServer::HandleLine(std::string_view line) { |
143 | 7.39k | const std::string_view verb = FirstToken(line); |
144 | 7.39k | if (verb.empty()) { |
145 | 1.12k | return; |
146 | 1.12k | } |
147 | | |
148 | 6.26k | if (TokenEquals(verb, "CLIENT")) { |
149 | 2.46k | return; |
150 | 2.46k | } |
151 | | |
152 | 3.79k | if (reply_sent_ || TokenEquals(verb, "QUIT")) { |
153 | 2.46k | if (!reply_sent_) { |
154 | 1.12k | (void)WriteNextReply(); |
155 | 1.12k | reply_sent_ = true; |
156 | 1.12k | } |
157 | 2.46k | FinishResponse(); |
158 | 2.46k | return; |
159 | 2.46k | } |
160 | | |
161 | | // The first lookup command consumes the scripted body. A script without a |
162 | | // reply still closes the response so curl cannot wait for a body forever. |
163 | 1.33k | (void)WriteNextReply(); |
164 | 1.33k | reply_sent_ = true; |
165 | 1.33k | FinishResponse(); |
166 | 1.33k | } |
167 | | |
168 | | } // namespace proto_fuzzer |