/src/curl_fuzzer/proto_fuzzer/ftp_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 FTP peer. |
9 | | |
10 | | #include "proto_fuzzer/ftp_mock_server.h" |
11 | | |
12 | | #include <algorithm> |
13 | | #include <cstddef> |
14 | | #include <cstdint> |
15 | | #include <limits> |
16 | | #include <memory> |
17 | | #include <string> |
18 | | #include <string_view> |
19 | | |
20 | | #include "proto_fuzzer/mock_server.h" |
21 | | #include "proto_fuzzer/scenario_limits.h" |
22 | | |
23 | | namespace proto_fuzzer { |
24 | | |
25 | | /// Begin without a borrowed protobuf or any open socketpairs. Keeping all |
26 | | /// session state in the object avoids process globals that could leak one |
27 | | /// libFuzzer iteration's protocol position into the next. |
28 | | FtpMockServer::FtpMockServer() |
29 | 0 | : scenario_(nullptr), |
30 | 0 | next_data_script_(0), |
31 | 0 | control_reply_count_(0), |
32 | 0 | next_control_reply_(0), |
33 | 0 | control_opened_(false) {} |
34 | | |
35 | | /// Loopback peers are ordinary unique_ptr-owned transports; the out-of-line |
36 | | /// destructor also permits MockConnection to remain forward-declared |
37 | | /// in the header. |
38 | 0 | FtpMockServer::~FtpMockServer() = default; |
39 | | |
40 | | /// Return the bounded command capture accumulated by the most recent drive. |
41 | 0 | const std::string& FtpMockServer::control_transcript() const { return control_transcript_; } |
42 | | |
43 | | /// Return the bounded upload capture accumulated by the most recent drive. |
44 | 0 | const std::string& FtpMockServer::uploaded_data() const { return uploaded_data_; } |
45 | | |
46 | | /// Report how many passive peers curl actually requested, rather than how many |
47 | | /// scripts happened to be present in the protobuf. |
48 | 0 | std::size_t FtpMockServer::opened_data_connection_count() const { return next_data_script_; } |
49 | | |
50 | | /// Clear descriptor and parser state before borrowing the next Scenario. A |
51 | | /// FtpMockServer can be reused serially, but no connection or response cursor |
52 | | /// is meaningful across easy-handle drives. |
53 | 0 | void FtpMockServer::ResetForScenario(const curl::fuzzer::proto::Scenario& scenario) { |
54 | 0 | control_connection_.reset(); |
55 | 0 | for (DataChannel& channel : data_channels_) { |
56 | 0 | channel.connection.reset(); |
57 | 0 | channel.script = nullptr; |
58 | 0 | channel.transfer_started = false; |
59 | 0 | channel.upload = false; |
60 | 0 | } |
61 | |
|
62 | 0 | scenario_ = &scenario; |
63 | 0 | next_data_script_ = 0; |
64 | 0 | control_reply_count_ = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, |
65 | 0 | static_cast<std::size_t>(scenario.connection().on_readable_size())); |
66 | 0 | next_control_reply_ = 0; |
67 | 0 | pending_control_bytes_.clear(); |
68 | 0 | control_transcript_.clear(); |
69 | 0 | uploaded_data_.clear(); |
70 | 0 | control_opened_ = false; |
71 | 0 | } |
72 | | |
73 | | /// Clamp a compatibility input's raw protobuf socket size before crossing the |
74 | | /// platform int boundary. Target policies normally clear FTP backpressure, |
75 | | /// but direct corpus replay must retain deterministic, well-defined behavior. |
76 | 0 | void FtpMockServer::ApplyScriptBackpressure(MockConnection* connection, const curl::fuzzer::proto::Connection& script) { |
77 | 0 | if (connection == nullptr) { |
78 | 0 | return; |
79 | 0 | } |
80 | | |
81 | 0 | const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max()); |
82 | 0 | const auto& backpressure = script.backpressure(); |
83 | 0 | const int receive_buffer = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max)); |
84 | 0 | connection->ApplyBackpressure(receive_buffer, static_cast<std::size_t>(backpressure.drain_limit())); |
85 | 0 | } |
86 | | |
87 | | /// Allocate one control socket followed by a bounded sequence of passive data |
88 | | /// sockets. Data is intentionally not preloaded here: curl opens EPSV/PASV's |
89 | | /// socket before it has sent the command that determines whether the stream is |
90 | | /// a listing, download, or upload. |
91 | | /// @param purpose Socket role requested by curl; active-mode accepts are |
92 | | /// deliberately unsupported. |
93 | | /// @param address Original IP destination retained by curl for FTP's passive |
94 | | /// host selection; the connected socketpair need not alter it. |
95 | | /// @return An already-connected client fd, or CURL_SOCKET_BAD when the bounded |
96 | | /// control/data script has no matching peer. |
97 | 0 | curl_socket_t FtpMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) { |
98 | 0 | (void)address; |
99 | | // Passive control/data sockets are ordinary outbound connections. Reject an |
100 | | // active-mode accept request rather than handing curl a connected socketpair |
101 | | // whose semantics cannot model listen/accept or a server callback. |
102 | 0 | if (scenario_ == nullptr || purpose != CURLSOCKTYPE_IPCXN) { |
103 | 0 | return CURL_SOCKET_BAD; |
104 | 0 | } |
105 | | |
106 | 0 | if (!control_opened_) { |
107 | 0 | control_connection_ = std::make_unique<MockConnection>(); |
108 | 0 | if (!control_connection_->ok()) { |
109 | 0 | control_connection_.reset(); |
110 | 0 | return CURL_SOCKET_BAD; |
111 | 0 | } |
112 | | |
113 | 0 | ApplyScriptBackpressure(control_connection_.get(), scenario_->connection()); |
114 | 0 | const std::string& greeting = scenario_->connection().initial_response(); |
115 | 0 | if (!greeting.empty() && |
116 | 0 | !control_connection_->WriteAll(reinterpret_cast<const unsigned char*>(greeting.data()), greeting.size())) { |
117 | 0 | control_connection_.reset(); |
118 | 0 | return CURL_SOCKET_BAD; |
119 | 0 | } |
120 | | // Do not consume the unique control slot until its peer is usable. Curl |
121 | | // may retry a failed application socket callback, and that retry must not |
122 | | // be mistaken for the first passive data connection. |
123 | 0 | control_opened_ = true; |
124 | 0 | return control_connection_->take_client_fd(); |
125 | 0 | } |
126 | | |
127 | 0 | const std::size_t available_scripts = |
128 | 0 | std::min<std::size_t>(kMaxDataChannels, static_cast<std::size_t>(scenario_->subsequent_connections_size())); |
129 | 0 | if (next_data_script_ >= available_scripts) { |
130 | 0 | return CURL_SOCKET_BAD; |
131 | 0 | } |
132 | | |
133 | 0 | DataChannel& channel = data_channels_[next_data_script_]; |
134 | 0 | channel.script = &scenario_->subsequent_connections(static_cast<int>(next_data_script_)); |
135 | 0 | channel.connection = std::make_unique<MockConnection>(); |
136 | 0 | if (!channel.connection->ok()) { |
137 | 0 | channel.connection.reset(); |
138 | 0 | channel.script = nullptr; |
139 | 0 | return CURL_SOCKET_BAD; |
140 | 0 | } |
141 | | |
142 | 0 | ApplyScriptBackpressure(channel.connection.get(), *channel.script); |
143 | 0 | ++next_data_script_; |
144 | 0 | return channel.connection->take_client_fd(); |
145 | 0 | } |
146 | | |
147 | | /// Retain only a bounded prefix for assertions. Parsing and draining continue |
148 | | /// against the full transport bytes, so reaching the cap cannot change curl's |
149 | | /// behavior or manufacture socket backpressure. |
150 | 0 | void FtpMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) { |
151 | 0 | if (destination == nullptr || destination->size() >= limit) { |
152 | 0 | return; |
153 | 0 | } |
154 | 0 | const std::size_t available = limit - destination->size(); |
155 | 0 | destination->append(source.data(), std::min(source.size(), available)); |
156 | 0 | } |
157 | | |
158 | | /// Remove FTP's optional leading horizontal whitespace and return just the |
159 | | /// verb. Arguments remain untouched because only curl, not the mock harness, |
160 | | /// should interpret fuzzed paths and offsets. |
161 | 0 | std::string_view FtpMockServer::CommandVerb(std::string_view command) { |
162 | 0 | std::size_t begin = 0; |
163 | 0 | while (begin < command.size() && (command[begin] == ' ' || command[begin] == '\t')) { |
164 | 0 | ++begin; |
165 | 0 | } |
166 | |
|
167 | 0 | std::size_t end = begin; |
168 | 0 | while (end < command.size() && command[end] != ' ' && command[end] != '\t' && command[end] != '\r' && |
169 | 0 | command[end] != '\n') { |
170 | 0 | ++end; |
171 | 0 | } |
172 | 0 | return command.substr(begin, end - begin); |
173 | 0 | } |
174 | | |
175 | | /// Fold only ASCII lowercase command letters. FTP verbs are ASCII tokens, so |
176 | | /// locale-aware case conversion would add state and undefined signed-char |
177 | | /// behavior without accepting anything curl can legitimately send. |
178 | 0 | bool FtpMockServer::VerbEquals(std::string_view verb, std::string_view expected_uppercase) { |
179 | 0 | if (verb.size() != expected_uppercase.size()) { |
180 | 0 | return false; |
181 | 0 | } |
182 | 0 | for (std::size_t index = 0; index < verb.size(); ++index) { |
183 | 0 | unsigned char actual = static_cast<unsigned char>(verb[index]); |
184 | 0 | if (actual >= 'a' && actual <= 'z') { |
185 | 0 | actual = static_cast<unsigned char>(actual - 'a' + 'A'); |
186 | 0 | } |
187 | 0 | if (actual != static_cast<unsigned char>(expected_uppercase[index])) { |
188 | 0 | return false; |
189 | 0 | } |
190 | 0 | } |
191 | 0 | return true; |
192 | 0 | } |
193 | | |
194 | | /// Recognize curl's built-in data commands plus MLSD, the useful structured |
195 | | /// listing custom request. PRET deliberately remains control-only even though |
196 | | /// its argument can contain RETR/STOR: no data transfer has begun at that |
197 | | /// point. |
198 | 0 | FtpMockServer::TransferDirection FtpMockServer::DirectionForVerb(std::string_view verb) { |
199 | 0 | if (VerbEquals(verb, "STOR") || VerbEquals(verb, "APPE")) { |
200 | 0 | return TransferDirection::kUpload; |
201 | 0 | } |
202 | 0 | if (VerbEquals(verb, "RETR") || VerbEquals(verb, "LIST") || VerbEquals(verb, "NLST") || VerbEquals(verb, "MLSD")) { |
203 | 0 | return TransferDirection::kDownload; |
204 | 0 | } |
205 | 0 | return TransferDirection::kNone; |
206 | 0 | } |
207 | | |
208 | | /// Scan backwards because repeated setopt calls are legal and libcurl keeps |
209 | | /// the last value. Comparing only the verb preserves arbitrary custom |
210 | | /// arguments while still avoiding false positives on preparatory FTP commands |
211 | | /// that happen to receive a fuzzed 125/150 reply after EPSV opened a socket. |
212 | 0 | bool FtpMockServer::IsConfiguredCustomDownload(std::string_view verb) const { |
213 | 0 | if (scenario_ == nullptr || verb.empty()) { |
214 | 0 | return false; |
215 | 0 | } |
216 | | |
217 | 0 | for (int index = scenario_->options_size() - 1; index >= 0; --index) { |
218 | 0 | const auto& option = scenario_->options(index); |
219 | 0 | if (option.option_id() != curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST) { |
220 | 0 | continue; |
221 | 0 | } |
222 | 0 | if (option.value_case() != curl::fuzzer::proto::SetOption::kStringValue) { |
223 | 0 | return false; |
224 | 0 | } |
225 | 0 | return VerbEquals(verb, CommandVerb(option.string_value())); |
226 | 0 | } |
227 | 0 | return false; |
228 | 0 | } |
229 | | |
230 | | /// Scan complete lines because a legal FTP response fragment may start with |
231 | | /// one or more informational lines. curl ends a response at the first line |
232 | | /// whose first three bytes are digits and whose fourth byte is a space. |
233 | 0 | int FtpMockServer::FirstReplyCode(std::string_view response) { |
234 | 0 | std::size_t line_start = 0; |
235 | 0 | while (line_start < response.size()) { |
236 | 0 | const std::size_t newline = response.find('\n', line_start); |
237 | 0 | if (newline == std::string_view::npos) { |
238 | 0 | return -1; |
239 | 0 | } |
240 | 0 | const std::size_t line_size = newline - line_start + 1; |
241 | 0 | if (line_size > 3) { |
242 | 0 | const unsigned char first = static_cast<unsigned char>(response[line_start]); |
243 | 0 | const unsigned char second = static_cast<unsigned char>(response[line_start + 1]); |
244 | 0 | const unsigned char third = static_cast<unsigned char>(response[line_start + 2]); |
245 | 0 | if (first >= '0' && first <= '9' && second >= '0' && second <= '9' && third >= '0' && third <= '9' && |
246 | 0 | response[line_start + 3] == ' ') { |
247 | 0 | return 100 * (first - '0') + 10 * (second - '0') + (third - '0'); |
248 | 0 | } |
249 | 0 | } |
250 | 0 | line_start = newline + 1; |
251 | 0 | } |
252 | 0 | return -1; |
253 | 0 | } |
254 | | |
255 | | /// A raw completion such as "226 done" needs only its missing newline. In |
256 | | /// that common mutation, appending a second synthetic reply would leave bytes |
257 | | /// in curl's control cache and misalign a later wildcard transfer. |
258 | 0 | bool FtpMockServer::TailNeedsOnlyNewline(std::string_view response) { |
259 | 0 | const std::size_t last_newline = response.rfind('\n'); |
260 | 0 | const std::size_t tail = last_newline == std::string_view::npos ? 0 : last_newline + 1; |
261 | 0 | if (response.size() - tail <= 3) { |
262 | 0 | return false; |
263 | 0 | } |
264 | 0 | const unsigned char first = static_cast<unsigned char>(response[tail]); |
265 | 0 | const unsigned char second = static_cast<unsigned char>(response[tail + 1]); |
266 | 0 | const unsigned char third = static_cast<unsigned char>(response[tail + 2]); |
267 | 0 | return first >= '0' && first <= '9' && second >= '0' && second <= '9' && third >= '0' && third <= '9' && |
268 | 0 | response[tail + 3] == ' '; |
269 | 0 | } |
270 | | |
271 | | /// Return the next bounded primary response. Empty strings still advance the |
272 | | /// cursor: they are meaningful truncation mutations for ordinary command |
273 | | /// states and explicitly select control EOF at transfer completion. |
274 | 0 | const std::string* FtpMockServer::NextControlReply() { |
275 | 0 | if (scenario_ == nullptr || next_control_reply_ >= control_reply_count_) { |
276 | 0 | return nullptr; |
277 | 0 | } |
278 | 0 | return &scenario_->connection().on_readable(static_cast<int>(next_control_reply_++)); |
279 | 0 | } |
280 | | |
281 | | /// Keep failed peer writes local to the mock. Response cursors must still |
282 | | /// advance after curl closes early, otherwise a replacement socket could see |
283 | | /// a reply intended for an earlier command. |
284 | 0 | bool FtpMockServer::WriteControlBytes(std::string_view bytes) { |
285 | 0 | return bytes.empty() || |
286 | 0 | (control_connection_ != nullptr && |
287 | 0 | control_connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size())); |
288 | 0 | } |
289 | | |
290 | | /// Queue a scenario-provided final reply and make it guaranteed non-blocking. |
291 | | /// ftp_done_control_reply() calls getftpresponse() synchronously after data |
292 | | /// EOF; if the fuzzed fragment lacks a terminating numeric line, append the |
293 | | /// smallest completion needed so curl returns to the harness immediately. An |
294 | | /// explicitly empty repeated value instead half-closes the peer: this retains |
295 | | /// deterministic liveness while making curl's missing-completion error path |
296 | | /// directly seedable. |
297 | 0 | void FtpMockServer::QueueTransferCompletion() { |
298 | 0 | const std::string* response = NextControlReply(); |
299 | 0 | if (response != nullptr && response->empty()) { |
300 | 0 | if (control_connection_ != nullptr) { |
301 | 0 | control_connection_->ShutdownWrite(); |
302 | 0 | } |
303 | 0 | return; |
304 | 0 | } |
305 | 0 | if (response != nullptr && !response->empty()) { |
306 | 0 | (void)WriteControlBytes(*response); |
307 | 0 | if (FirstReplyCode(*response) >= 0) { |
308 | 0 | return; |
309 | 0 | } |
310 | 0 | if (TailNeedsOnlyNewline(*response)) { |
311 | 0 | (void)WriteControlBytes("\r\n"); |
312 | 0 | return; |
313 | 0 | } |
314 | 0 | if (response->back() != '\n') { |
315 | 0 | (void)WriteControlBytes("\r\n"); |
316 | 0 | } |
317 | 0 | } |
318 | | |
319 | | // This line is a liveness guard, not a forced successful outcome: any |
320 | | // complete fuzzed final response above remains the reply curl consumes. |
321 | 0 | (void)WriteControlBytes("226 mock transfer complete\r\n"); |
322 | 0 | } |
323 | | |
324 | | /// Send every runtime-visible data fragment before half-closing the peer. The |
325 | | /// serialized fuzz input is already length-bounded; sending the complete |
326 | | /// prefix lets curl drain its data state without event-loop sleeps while each |
327 | | /// protobuf fragment still affects byte content and parser behavior. |
328 | 0 | void FtpMockServer::PreloadDownload(DataChannel* channel) { |
329 | 0 | if (channel == nullptr || channel->connection == nullptr || channel->script == nullptr) { |
330 | 0 | return; |
331 | 0 | } |
332 | | |
333 | 0 | const std::string& initial = channel->script->initial_response(); |
334 | 0 | bool complete = initial.empty() || |
335 | 0 | channel->connection->WriteAll(reinterpret_cast<const unsigned char*>(initial.data()), initial.size()); |
336 | 0 | const std::size_t chunk_count = std::min<std::size_t>(scenario_limits::kMaxResponseChunks, |
337 | 0 | static_cast<std::size_t>(channel->script->on_readable_size())); |
338 | 0 | for (std::size_t index = 0; complete && index < chunk_count; ++index) { |
339 | 0 | const std::string& chunk = channel->script->on_readable(static_cast<int>(index)); |
340 | 0 | complete = chunk.empty() || |
341 | 0 | channel->connection->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
342 | 0 | } |
343 | 0 | (void)complete; |
344 | 0 | channel->connection->ShutdownWrite(); |
345 | 0 | } |
346 | | |
347 | | /// Pair the next transfer command with the oldest passive socket that EPSV or |
348 | | /// PASV opened but no command has used. FTP serializes data transfers, so this |
349 | | /// simple cursor is sufficient and avoids interpreting fuzzed port numbers. |
350 | 0 | void FtpMockServer::StartNextTransfer(TransferDirection direction) { |
351 | 0 | for (std::size_t index = 0; index < next_data_script_; ++index) { |
352 | 0 | DataChannel& channel = data_channels_[index]; |
353 | 0 | if (channel.connection == nullptr || channel.transfer_started) { |
354 | 0 | continue; |
355 | 0 | } |
356 | | |
357 | 0 | channel.transfer_started = true; |
358 | 0 | channel.upload = direction == TransferDirection::kUpload; |
359 | 0 | if (!channel.upload) { |
360 | 0 | PreloadDownload(&channel); |
361 | 0 | } |
362 | | // Upload peers keep their write half open until cleanup. A real FTP server |
363 | | // does not send an early FIN while receiving a file, and a readiness |
364 | | // backend could otherwise report that FIN alongside POLLOUT before curl |
365 | | // has delivered the upload callback's bytes. |
366 | 0 | return; |
367 | 0 | } |
368 | 0 | } |
369 | | |
370 | | /// Advance one reply for every complete command. Accepted transfer commands |
371 | | /// additionally consume their final reply now, because waiting until data EOF |
372 | | /// would be too late to escape curl's blocking completion read. |
373 | 0 | void FtpMockServer::HandleControlCommand(std::string_view command) { |
374 | 0 | const std::string* response = NextControlReply(); |
375 | 0 | if (response == nullptr) { |
376 | 0 | return; |
377 | 0 | } |
378 | | |
379 | 0 | const int response_code = FirstReplyCode(*response); |
380 | 0 | (void)WriteControlBytes(*response); |
381 | |
|
382 | 0 | const std::string_view verb = CommandVerb(command); |
383 | 0 | TransferDirection direction = DirectionForVerb(verb); |
384 | 0 | if (direction == TransferDirection::kNone && IsConfiguredCustomDownload(verb)) { |
385 | 0 | direction = TransferDirection::kDownload; |
386 | 0 | } |
387 | 0 | const bool starts_download = |
388 | 0 | direction == TransferDirection::kDownload && (response_code == 125 || response_code == 150); |
389 | | // curl's STOR handler accepts every response below 400 before initiating |
390 | | // the upload, even though 125/150 are the conventional successful replies. |
391 | 0 | const bool starts_upload = direction == TransferDirection::kUpload && response_code >= 100 && response_code < 400; |
392 | 0 | if (starts_download || starts_upload) { |
393 | 0 | StartNextTransfer(direction); |
394 | 0 | QueueTransferCompletion(); |
395 | 0 | } |
396 | 0 | } |
397 | | |
398 | | /// Read without waiting, retain partial final commands, and process all full |
399 | | /// lines already emitted by curl. The number of replies is bounded even if a |
400 | | /// malformed command stream contains many newlines. |
401 | 0 | bool FtpMockServer::ServiceControlConnection() { |
402 | 0 | if (control_connection_ == nullptr) { |
403 | 0 | return false; |
404 | 0 | } |
405 | | |
406 | 0 | const std::size_t size_before_read = pending_control_bytes_.size(); |
407 | 0 | control_connection_->ReadAvailable(&pending_control_bytes_); |
408 | 0 | bool made_progress = pending_control_bytes_.size() != size_before_read; |
409 | |
|
410 | 0 | std::size_t consumed = 0; |
411 | 0 | while (consumed < pending_control_bytes_.size()) { |
412 | 0 | const std::size_t newline = pending_control_bytes_.find('\n', consumed); |
413 | 0 | if (newline == std::string::npos) { |
414 | 0 | break; |
415 | 0 | } |
416 | 0 | const std::size_t command_size = newline - consumed + 1; |
417 | 0 | const std::string_view command(pending_control_bytes_.data() + consumed, command_size); |
418 | 0 | CapturePrefix(command, kMaxCapturedControlBytes, &control_transcript_); |
419 | 0 | HandleControlCommand(command); |
420 | 0 | consumed += command_size; |
421 | 0 | made_progress = true; |
422 | 0 | } |
423 | |
|
424 | 0 | if (consumed != 0) { |
425 | 0 | pending_control_bytes_.erase(0, consumed); |
426 | 0 | } |
427 | 0 | return made_progress; |
428 | 0 | } |
429 | | |
430 | | /// Drain uploads into a temporary buffer so data beyond the observable cap is |
431 | | /// still removed from the socket. This keeps curl's progress independent of |
432 | | /// how much a unit test chooses to retain. |
433 | 0 | bool FtpMockServer::ServiceUploadConnections() { |
434 | 0 | bool made_progress = false; |
435 | 0 | for (std::size_t index = 0; index < next_data_script_; ++index) { |
436 | 0 | DataChannel& channel = data_channels_[index]; |
437 | 0 | if (!channel.upload || channel.connection == nullptr) { |
438 | 0 | continue; |
439 | 0 | } |
440 | | |
441 | 0 | std::string bytes; |
442 | 0 | channel.connection->ReadAvailable(&bytes); |
443 | 0 | if (!bytes.empty()) { |
444 | 0 | CapturePrefix(bytes, kMaxCapturedUploadBytes, &uploaded_data_); |
445 | 0 | made_progress = true; |
446 | 0 | } |
447 | 0 | } |
448 | 0 | return made_progress; |
449 | 0 | } |
450 | | |
451 | | /// Prepare cleanup while the mock can still write to curl's control socket. |
452 | | /// A completed easy remains in multi's connection cache until cleanup, where |
453 | | /// FTP sends QUIT and performs a blocking read. Preloading 221 handles that |
454 | | /// path; an unfinished drive instead gets EOF so cleanup fails fast. |
455 | 0 | void FtpMockServer::FinishConnections(bool completed) { |
456 | 0 | if (control_connection_ != nullptr) { |
457 | 0 | if (completed) { |
458 | 0 | (void)WriteControlBytes("221 mock closing control connection\r\n"); |
459 | 0 | } |
460 | 0 | control_connection_->ShutdownWrite(); |
461 | 0 | } |
462 | 0 | for (std::size_t index = 0; index < next_data_script_; ++index) { |
463 | 0 | if (data_channels_[index].connection != nullptr) { |
464 | 0 | data_channels_[index].connection->ShutdownWrite(); |
465 | 0 | } |
466 | 0 | } |
467 | 0 | } |
468 | | |
469 | | /// Alternate curl transitions with immediate peer service until the transfer |
470 | | /// completes or deterministic idle/operation caps win. No select(), poll(), |
471 | | /// or sleep is needed: every transport is a local socketpair, and malformed |
472 | | /// scripts are terminated by half-close after the bounded loop. |
473 | 0 | void FtpMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
474 | 0 | (void)easy; |
475 | 0 | ResetForScenario(scenario); |
476 | |
|
477 | 0 | int still_running = 1; |
478 | 0 | int idle_iterations = 0; |
479 | 0 | int drive_iterations = 0; |
480 | 0 | CURLMcode result = CURLM_OK; |
481 | 0 | while (still_running && idle_iterations < kMaxIdleIterations && drive_iterations++ < kMaxDriveIterations) { |
482 | 0 | const int running_before = still_running; |
483 | 0 | result = curl_multi_perform(multi, &still_running); |
484 | 0 | if (result != CURLM_OK) { |
485 | 0 | break; |
486 | 0 | } |
487 | | |
488 | 0 | bool made_progress = still_running != running_before; |
489 | 0 | made_progress = ServiceControlConnection() || made_progress; |
490 | 0 | made_progress = ServiceUploadConnections() || made_progress; |
491 | 0 | if (made_progress) { |
492 | 0 | idle_iterations = 0; |
493 | 0 | } else { |
494 | 0 | ++idle_iterations; |
495 | 0 | } |
496 | 0 | } |
497 | | |
498 | | // The final perform may have emitted upload or control bytes immediately |
499 | | // before marking the transfer done. Drain/capture them before cleanup. |
500 | 0 | (void)ServiceControlConnection(); |
501 | 0 | (void)ServiceUploadConnections(); |
502 | 0 | FinishConnections(still_running == 0 && result == CURLM_OK); |
503 | 0 | scenario_ = nullptr; |
504 | 0 | } |
505 | | |
506 | | } // namespace proto_fuzzer |