curl-fuzzer C++ reference
Loading...
Searching...
No Matches
ftp_mock_server.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#include <fcntl.h>
13#include <netinet/in.h>
14#include <sys/socket.h>
15#include <unistd.h>
16
17#include <algorithm>
18#include <cerrno>
19#include <cstddef>
20#include <cstdint>
21#include <cstring>
22#include <limits>
23#include <memory>
24#include <string>
25#include <string_view>
26
27#include "proto_fuzzer/allocation_fault_injection.h"
30
31namespace proto_fuzzer {
32
38 : scenario_(nullptr),
39 next_data_script_(0),
40 active_listener_fd_(-1),
41 active_listener_client_fd_(CURL_SOCKET_BAD),
42 pending_active_channel_(kMaxDataChannels),
43 control_reply_count_(0),
44 next_control_reply_(0),
45 active_pollset_probe_count_(0),
46 control_opened_(false) {}
47
53
58std::unique_ptr<MockConnection> FtpMockServer::CreateConnection() { return std::make_unique<MockConnection>(); }
59
64bool FtpMockServer::SupportsActiveMode() const { return true; }
65
71void FtpMockServer::ObserveIncomingData(const unsigned char* data, std::size_t size) {
72 if (data == nullptr || size == 0) {
73 return;
74 }
75 pending_control_bytes_.append(reinterpret_cast<const char*>(data), size);
76}
77
83void FtpMockServer::DataChannel::ObserveIncomingData(const unsigned char* data, std::size_t size) {
84 if (data == nullptr || size == 0) {
85 return;
86 }
87 incoming_data.append(reinterpret_cast<const char*>(data), size);
88}
89
94const std::string& FtpMockServer::control_transcript() const { return control_transcript_; }
95
99const std::string& FtpMockServer::uploaded_data() const { return uploaded_data_; }
100
105std::size_t FtpMockServer::opened_data_connection_count() const { return next_data_script_; }
106
112std::size_t FtpMockServer::active_pollset_probe_count() const { return active_pollset_probe_count_; }
113
118 CloseActiveDescriptors();
119 control_connection_.reset();
120 for (DataChannel& channel : data_channels_) {
121 channel.connection.reset();
122 channel.incoming_data.clear();
123 }
124}
125
129void FtpMockServer::ResetForScenario(const curl::fuzzer::proto::Scenario& scenario) {
131 for (DataChannel& channel : data_channels_) {
132 channel.script = nullptr;
133 channel.transfer_started = false;
134 channel.upload = false;
135 }
136
137 scenario_ = &scenario;
138 next_data_script_ = 0;
139 pending_active_channel_ = kMaxDataChannels;
140 control_reply_count_ = std::min<std::size_t>(scenario_limits::kMaxResponseChunks,
141 static_cast<std::size_t>(scenario.connection().on_readable_size()));
142 next_control_reply_ = 0;
143 pending_control_bytes_.clear();
144 control_transcript_.clear();
145 uploaded_data_.clear();
146 active_pollset_probe_count_ = 0;
147 control_opened_ = false;
148}
149
153void FtpMockServer::CloseActiveDescriptors() {
154 active_listener_client_fd_ = CURL_SOCKET_BAD;
155 if (active_listener_fd_ >= 0) {
156 close(active_listener_fd_);
157 active_listener_fd_ = -1;
158 }
159 for (DataChannel& channel : data_channels_) {
160 if (channel.active_fd >= 0) {
161 close(channel.active_fd);
162 channel.active_fd = -1;
163 }
164 }
165 pending_active_channel_ = kMaxDataChannels;
166}
167
172void FtpMockServer::ApplyScriptBackpressure(MockConnection* connection, const curl::fuzzer::proto::Connection& script) {
173 if (connection == nullptr) {
174 return;
175 }
176
177 const std::uint32_t int_max = static_cast<std::uint32_t>(std::numeric_limits<int>::max());
178 const auto& backpressure = script.backpressure();
179 const int receive_buffer = static_cast<int>(std::min(backpressure.recv_buf_bytes(), int_max));
180 connection->ApplyBackpressure(receive_buffer, static_cast<std::size_t>(backpressure.drain_limit()));
181}
182
185bool FtpMockServer::UsesActiveMode() const {
186 if (scenario_ == nullptr) {
187 return false;
188 }
189 for (int index = scenario_->options_size() - 1; index >= 0; --index) {
190 const auto& option = scenario_->options(index);
191 if (option.option_id() == curl::fuzzer::proto::CURLOPT_FTPPORT) {
192 return option.value_case() == curl::fuzzer::proto::SetOption::kStringValue && !option.string_value().empty();
193 }
194 }
195 return false;
196}
197
205SocketSetupDisposition FtpMockServer::GetSocketSetupDisposition(curl_socket_t curlfd, curlsocktype purpose) const {
206 if (purpose == CURLSOCKTYPE_ACCEPT || curlfd == active_listener_client_fd_) {
207 return SocketSetupDisposition::kNeedsSetup;
208 }
209 return SocketSetupDisposition::kAlreadyConnected;
210}
211
215curl_socket_t FtpMockServer::OpenActiveListener(struct curl_sockaddr* address) {
216 if (address == nullptr || (address->family != AF_INET && address->family != AF_INET6)) {
217 return CURL_SOCKET_BAD;
218 }
219
220 if (pending_active_channel_ == kMaxDataChannels) {
221 const std::size_t available_scripts =
222 std::min<std::size_t>(kMaxDataChannels, static_cast<std::size_t>(scenario_->subsequent_connections_size()));
223 if (next_data_script_ >= available_scripts) {
224 return CURL_SOCKET_BAD;
225 }
226 pending_active_channel_ = next_data_script_++;
227 DataChannel& channel = data_channels_[pending_active_channel_];
228 channel.script = &scenario_->subsequent_connections(static_cast<int>(pending_active_channel_));
229 }
230
231 if (active_listener_fd_ >= 0) {
232 close(active_listener_fd_);
233 active_listener_fd_ = -1;
234 }
235 active_listener_client_fd_ = CURL_SOCKET_BAD;
236
237 const int listener = socket(address->family, address->socktype, address->protocol);
238 if (listener < 0) {
239 return CURL_SOCKET_BAD;
240 }
241 active_listener_fd_ = dup(listener);
242 if (active_listener_fd_ < 0) {
243 close(listener);
244 return CURL_SOCKET_BAD;
245 }
246 active_listener_client_fd_ = listener;
247 (void)fcntl(active_listener_fd_, F_SETFD, FD_CLOEXEC);
248 return listener;
249}
250
259curl_socket_t FtpMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
260 if (scenario_ == nullptr || purpose != CURLSOCKTYPE_IPCXN) {
261 return CURL_SOCKET_BAD;
262 }
263
264 if (!control_opened_) {
265 control_connection_ = CreateConnection();
266 if (!control_connection_ || !control_connection_->ok()) {
267 control_connection_.reset();
268 return CURL_SOCKET_BAD;
269 }
270 control_connection_->SetIncomingDataObserver(this);
271
272 ApplyScriptBackpressure(control_connection_.get(), scenario_->connection());
273 const std::string& greeting = scenario_->connection().initial_response();
274 if (!greeting.empty() &&
275 !control_connection_->WriteAll(reinterpret_cast<const unsigned char*>(greeting.data()), greeting.size())) {
276 control_connection_.reset();
277 return CURL_SOCKET_BAD;
278 }
279 // Do not consume the unique control slot until its peer is usable. Curl
280 // may retry a failed application socket callback, and that retry must not
281 // be mistaken for the first passive data connection.
282 control_opened_ = true;
283 return control_connection_->take_client_fd();
284 }
285
286 if (UsesActiveMode()) {
287 // A TLS peer owns only wrapped socketpairs, so it cannot hand curl a real
288 // listener that its data TLS server could later answer. Reject the request
289 // immediately: the target policy already removes CURLOPT_FTPPORT, and this
290 // guard keeps direct callers on a bounded failure instead of an unusable
291 // plaintext data channel.
292 if (!SupportsActiveMode()) {
293 return CURL_SOCKET_BAD;
294 }
295 return OpenActiveListener(address);
296 }
297
298 const std::size_t available_scripts =
299 std::min<std::size_t>(kMaxDataChannels, static_cast<std::size_t>(scenario_->subsequent_connections_size()));
300 if (next_data_script_ >= available_scripts) {
301 return CURL_SOCKET_BAD;
302 }
303
304 DataChannel& channel = data_channels_[next_data_script_];
305 channel.script = &scenario_->subsequent_connections(static_cast<int>(next_data_script_));
306 channel.connection = CreateConnection();
307 if (!channel.connection || !channel.connection->ok()) {
308 channel.connection.reset();
309 channel.script = nullptr;
310 return CURL_SOCKET_BAD;
311 }
312 channel.connection->SetIncomingDataObserver(&channel);
313
314 ApplyScriptBackpressure(channel.connection.get(), *channel.script);
315 ++next_data_script_;
316 return channel.connection->take_client_fd();
317}
318
322bool FtpMockServer::WriteActiveBytes(int fd, const unsigned char* data, std::size_t size) {
323 std::size_t offset = 0;
324 while (offset < size) {
325 const ssize_t written = send(fd, data + offset, size - offset, MSG_NOSIGNAL | MSG_DONTWAIT);
326 if (written > 0) {
327 offset += static_cast<std::size_t>(written);
328 continue;
329 }
330 if (written < 0 && errno == EINTR) {
331 continue;
332 }
333 return false;
334 }
335 return true;
336}
337
340bool FtpMockServer::ReadActiveBytes(int fd, std::string* output) {
341 bool made_progress = false;
342 char buffer[16 * 1024];
343 for (;;) {
344 const ssize_t amount = recv(fd, buffer, sizeof(buffer), MSG_DONTWAIT);
345 if (amount > 0) {
346 output->append(buffer, static_cast<std::size_t>(amount));
347 made_progress = true;
348 continue;
349 }
350 if (amount < 0 && errno == EINTR) {
351 continue;
352 }
353 return made_progress;
354 }
355}
356
360void FtpMockServer::ConnectActiveDataChannel() {
361 if (active_listener_fd_ < 0 || pending_active_channel_ >= kMaxDataChannels) {
362 return;
363 }
364
365 struct sockaddr_storage address;
366 std::memset(&address, 0, sizeof(address));
367 socklen_t address_length = sizeof(address);
368 if (getsockname(active_listener_fd_, reinterpret_cast<struct sockaddr*>(&address), &address_length) != 0) {
369 return;
370 }
371
372 const int fd = socket(address.ss_family, SOCK_STREAM, IPPROTO_TCP);
373 if (fd < 0) {
374 return;
375 }
376 if (connect(fd, reinterpret_cast<struct sockaddr*>(&address), address_length) != 0) {
377 close(fd);
378 return;
379 }
380 const int flags = fcntl(fd, F_GETFL, 0);
381 if (flags >= 0) {
382 (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
383 }
384 (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
385
386 DataChannel& channel = data_channels_[pending_active_channel_];
387 channel.active_fd = fd;
388 close(active_listener_fd_);
389 active_listener_fd_ = -1;
390 active_listener_client_fd_ = CURL_SOCKET_BAD;
391 pending_active_channel_ = kMaxDataChannels;
392}
393
400void FtpMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) {
401 if (destination == nullptr || destination->size() >= limit) {
402 return;
403 }
404 const std::size_t available = limit - destination->size();
405 destination->append(source.data(), std::min(source.size(), available));
406}
407
413std::string_view FtpMockServer::CommandVerb(std::string_view command) {
414 std::size_t begin = 0;
415 while (begin < command.size() && (command[begin] == ' ' || command[begin] == '\t')) {
416 ++begin;
417 }
418
419 std::size_t end = begin;
420 while (end < command.size() && command[end] != ' ' && command[end] != '\t' && command[end] != '\r' &&
421 command[end] != '\n') {
422 ++end;
423 }
424 return command.substr(begin, end - begin);
425}
426
433bool FtpMockServer::VerbEquals(std::string_view verb, std::string_view expected_uppercase) {
434 if (verb.size() != expected_uppercase.size()) {
435 return false;
436 }
437 for (std::size_t index = 0; index < verb.size(); ++index) {
438 unsigned char actual = static_cast<unsigned char>(verb[index]);
439 if (actual >= 'a' && actual <= 'z') {
440 actual = static_cast<unsigned char>(actual - 'a' + 'A');
441 }
442 if (actual != static_cast<unsigned char>(expected_uppercase[index])) {
443 return false;
444 }
445 }
446 return true;
447}
448
455FtpMockServer::TransferDirection FtpMockServer::DirectionForVerb(std::string_view verb) {
456 if (VerbEquals(verb, "STOR") || VerbEquals(verb, "APPE")) {
457 return TransferDirection::kUpload;
458 }
459 if (VerbEquals(verb, "RETR") || VerbEquals(verb, "LIST") || VerbEquals(verb, "NLST") || VerbEquals(verb, "MLSD")) {
460 return TransferDirection::kDownload;
461 }
462 return TransferDirection::kNone;
463}
464
473bool FtpMockServer::IsConfiguredCustomDownload(std::string_view verb) const {
474 if (scenario_ == nullptr || verb.empty()) {
475 return false;
476 }
477
478 for (int index = scenario_->options_size() - 1; index >= 0; --index) {
479 const auto& option = scenario_->options(index);
480 if (option.option_id() != curl::fuzzer::proto::CURLOPT_CUSTOMREQUEST) {
481 continue;
482 }
483 if (option.value_case() != curl::fuzzer::proto::SetOption::kStringValue) {
484 return false;
485 }
486 return VerbEquals(verb, CommandVerb(option.string_value()));
487 }
488 return false;
489}
490
496int FtpMockServer::FirstReplyCode(std::string_view response) {
497 std::size_t line_start = 0;
498 while (line_start < response.size()) {
499 const std::size_t newline = response.find('\n', line_start);
500 if (newline == std::string_view::npos) {
501 return -1;
502 }
503 const std::size_t line_size = newline - line_start + 1;
504 if (line_size > 3) {
505 const unsigned char first = static_cast<unsigned char>(response[line_start]);
506 const unsigned char second = static_cast<unsigned char>(response[line_start + 1]);
507 const unsigned char third = static_cast<unsigned char>(response[line_start + 2]);
508 if (first >= '0' && first <= '9' && second >= '0' && second <= '9' && third >= '0' && third <= '9' &&
509 response[line_start + 3] == ' ') {
510 return 100 * (first - '0') + 10 * (second - '0') + (third - '0');
511 }
512 }
513 line_start = newline + 1;
514 }
515 return -1;
516}
517
523bool FtpMockServer::TailNeedsOnlyNewline(std::string_view response) {
524 const std::size_t last_newline = response.rfind('\n');
525 const std::size_t tail = last_newline == std::string_view::npos ? 0 : last_newline + 1;
526 if (response.size() - tail <= 3) {
527 return false;
528 }
529 const unsigned char first = static_cast<unsigned char>(response[tail]);
530 const unsigned char second = static_cast<unsigned char>(response[tail + 1]);
531 const unsigned char third = static_cast<unsigned char>(response[tail + 2]);
532 return first >= '0' && first <= '9' && second >= '0' && second <= '9' && third >= '0' && third <= '9' &&
533 response[tail + 3] == ' ';
534}
535
541const std::string* FtpMockServer::NextControlReply() {
542 if (scenario_ == nullptr || next_control_reply_ >= control_reply_count_) {
543 return nullptr;
544 }
545 return &scenario_->connection().on_readable(static_cast<int>(next_control_reply_++));
546}
547
553bool FtpMockServer::WriteControlBytes(std::string_view bytes) {
554 return bytes.empty() ||
555 (control_connection_ != nullptr &&
556 control_connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size()));
557}
558
566void FtpMockServer::QueueTransferCompletion() {
567 const std::string* response = NextControlReply();
568 if (response != nullptr && response->empty()) {
569 if (control_connection_ != nullptr) {
570 control_connection_->ShutdownWrite();
571 }
572 return;
573 }
574 if (response != nullptr && !response->empty()) {
575 (void)WriteControlBytes(*response);
576 if (FirstReplyCode(*response) >= 0) {
577 return;
578 }
579 if (TailNeedsOnlyNewline(*response)) {
580 (void)WriteControlBytes("\r\n");
581 return;
582 }
583 if (response->back() != '\n') {
584 (void)WriteControlBytes("\r\n");
585 }
586 }
587
588 // This line is a liveness guard, not a forced successful outcome: any
589 // complete fuzzed final response above remains the reply curl consumes.
590 (void)WriteControlBytes("226 mock transfer complete\r\n");
591}
592
599void FtpMockServer::PreloadDownload(DataChannel* channel) {
600 if (channel == nullptr || channel->script == nullptr || (channel->connection == nullptr && channel->active_fd < 0)) {
601 return;
602 }
603
604 const std::string& initial = channel->script->initial_response();
605 const auto write_bytes = [channel](const std::string& bytes) {
606 if (bytes.empty()) {
607 return true;
608 }
609 if (channel->connection != nullptr) {
610 return channel->connection->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size());
611 }
612 return WriteActiveBytes(channel->active_fd, reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size());
613 };
614 bool complete = write_bytes(initial);
615 const std::size_t chunk_count = std::min<std::size_t>(scenario_limits::kMaxResponseChunks,
616 static_cast<std::size_t>(channel->script->on_readable_size()));
617 for (std::size_t index = 0; complete && index < chunk_count; ++index) {
618 const std::string& chunk = channel->script->on_readable(static_cast<int>(index));
619 complete = write_bytes(chunk);
620 }
621 (void)complete;
622 if (channel->connection != nullptr) {
623 channel->connection->ShutdownWrite();
624 } else {
625 (void)shutdown(channel->active_fd, SHUT_WR);
626 }
627}
628
633void FtpMockServer::StartNextTransfer(TransferDirection direction) {
634 for (std::size_t index = 0; index < next_data_script_; ++index) {
635 DataChannel& channel = data_channels_[index];
636 if ((channel.connection == nullptr && channel.active_fd < 0) || channel.transfer_started) {
637 continue;
638 }
639
640 channel.transfer_started = true;
641 channel.upload = direction == TransferDirection::kUpload;
642 if (!channel.upload) {
643 PreloadDownload(&channel);
644 }
645 // Upload peers keep their write half open until cleanup. A real FTP server
646 // does not send an early FIN while receiving a file, and a readiness
647 // backend could otherwise report that FIN alongside POLLOUT before curl
648 // has delivered the upload callback's bytes.
649 return;
650 }
651}
652
657void FtpMockServer::HandleControlCommand(std::string_view command) {
658 const std::string* response = NextControlReply();
659 if (response == nullptr) {
660 return;
661 }
662
663 const int response_code = FirstReplyCode(*response);
664 const bool response_written = WriteControlBytes(*response);
665
666 const std::string_view verb = CommandVerb(command);
667 if (response_written && scenario_->ftp_auth_tls_filter_allocation_failure() && VerbEquals(verb, "AUTH") &&
668 (response_code == 234 || response_code == 334)) {
669 allocation_fault_injection::ArmNextAllocationFailure();
670 }
671 if ((VerbEquals(verb, "EPRT") || VerbEquals(verb, "PORT")) && response_code >= 200 && response_code < 300) {
672 ConnectActiveDataChannel();
673 }
674 TransferDirection direction = DirectionForVerb(verb);
675 if (direction == TransferDirection::kNone && IsConfiguredCustomDownload(verb)) {
676 direction = TransferDirection::kDownload;
677 }
678 const bool starts_download =
679 direction == TransferDirection::kDownload && (response_code == 125 || response_code == 150);
680 // curl's STOR handler accepts every response below 400 before initiating
681 // the upload, even though 125/150 are the conventional successful replies.
682 const bool starts_upload = direction == TransferDirection::kUpload && response_code >= 100 && response_code < 400;
683 if (starts_download || starts_upload) {
684 StartNextTransfer(direction);
685 QueueTransferCompletion();
686 }
687}
688
693bool FtpMockServer::ServiceControlConnection() {
694 if (control_connection_ == nullptr) {
695 return false;
696 }
697
698 const std::size_t size_before_read = pending_control_bytes_.size();
699 const std::size_t transport_progress = control_connection_->DrainIncoming();
700 bool made_progress = pending_control_bytes_.size() != size_before_read || transport_progress != 0;
701
702 std::size_t consumed = 0;
703 while (consumed < pending_control_bytes_.size()) {
704 const std::size_t newline = pending_control_bytes_.find('\n', consumed);
705 if (newline == std::string::npos) {
706 break;
707 }
708 const std::size_t command_size = newline - consumed + 1;
709 const std::string_view command(pending_control_bytes_.data() + consumed, command_size);
710 CapturePrefix(command, kMaxCapturedControlBytes, &control_transcript_);
711 HandleControlCommand(command);
712 consumed += command_size;
713 made_progress = true;
714 }
715
716 if (consumed != 0) {
717 pending_control_bytes_.erase(0, consumed);
718 // Replies queued while handling a command must reach the wire before the
719 // next curl transition. FTP performs its final transfer-reply read inside
720 // curl_multi_perform, so a deferred TLS record would turn a completed
721 // download or upload into a control-response timeout.
722 const std::size_t flush_progress = control_connection_->DrainIncoming();
723 made_progress = made_progress || flush_progress != 0;
724 }
725 return made_progress;
726}
727
734bool FtpMockServer::ServiceDataConnections() {
735 bool made_progress = false;
736 for (std::size_t index = 0; index < next_data_script_; ++index) {
737 DataChannel& channel = data_channels_[index];
738 if (channel.connection != nullptr) {
739 const std::size_t transport_progress = channel.connection->DrainIncoming();
740 made_progress = made_progress || transport_progress != 0;
741 if (channel.upload && !channel.incoming_data.empty()) {
742 CapturePrefix(channel.incoming_data, kMaxCapturedUploadBytes, &uploaded_data_);
743 channel.incoming_data.clear();
744 made_progress = true;
745 }
746 continue;
747 }
748
749 if (channel.upload && channel.active_fd >= 0) {
750 std::string bytes;
751 (void)ReadActiveBytes(channel.active_fd, &bytes);
752 if (!bytes.empty()) {
753 CapturePrefix(bytes, kMaxCapturedUploadBytes, &uploaded_data_);
754 made_progress = true;
755 }
756 }
757 }
758 return made_progress;
759}
760
766void FtpMockServer::FinishConnections(bool completed) {
767 if (control_connection_ != nullptr) {
768 if (completed) {
769 (void)WriteControlBytes("221 mock closing control connection\r\n");
770 }
771 control_connection_->ShutdownWrite();
772 }
773 for (std::size_t index = 0; index < next_data_script_; ++index) {
774 if (data_channels_[index].connection != nullptr) {
775 data_channels_[index].connection->ShutdownWrite();
776 }
777 if (data_channels_[index].active_fd >= 0) {
778 (void)shutdown(data_channels_[index].active_fd, SHUT_WR);
779 }
780 }
781 if (active_listener_fd_ >= 0) {
782 close(active_listener_fd_);
783 active_listener_fd_ = -1;
784 }
785 active_listener_client_fd_ = CURL_SOCKET_BAD;
786}
787
795void FtpMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
796 (void)easy;
797 ResetForScenario(scenario);
798
799 int still_running = 1;
800 int idle_iterations = 0;
801 int drive_iterations = 0;
802 CURLMcode result = CURLM_OK;
803 while (still_running && idle_iterations < kMaxIdleIterations && drive_iterations++ < kMaxDriveIterations) {
804 const int running_before = still_running;
805 result = curl_multi_perform(multi, &still_running);
806 if (result != CURLM_OK) {
807 break;
808 }
809
810 // Pollset construction is opt-in coverage work. Waiting until the socket
811 // callback has run and the transfer remains active reaches FTP's protocol
812 // and DO_MORE pollsets without adding a syscall to the default hot loop.
813 if (scenario.probe_multi_pollset() && still_running && control_opened_) {
814 ProbeMultiPollset(multi);
815 ++active_pollset_probe_count_;
816 }
817
818 bool made_progress = still_running != running_before;
819 made_progress = ServiceControlConnection() || made_progress;
820 made_progress = ServiceDataConnections() || made_progress;
821 if (made_progress) {
822 idle_iterations = 0;
823 } else {
824 ++idle_iterations;
825 }
826 }
827
828 // The final perform may have emitted upload or control bytes immediately
829 // before marking the transfer done. Drain/capture them before cleanup.
830 (void)ServiceControlConnection();
831 (void)ServiceDataConnections();
832 FinishConnections(still_running == 0 && result == CURLM_OK);
833 scenario_ = nullptr;
834}
835
838#if defined(PROTO_FUZZER_OBSERVE_STATE)
840 const volatile std::size_t ftp_observation[] = {control_transcript().size(), uploaded_data().size(),
842 (void)ftp_observation;
843#endif
844}
845
846} // namespace proto_fuzzer
void RunLoop(CURLM *multi, CURL *easy, const curl::fuzzer::proto::Scenario &scenario) override
Alternate curl transitions with immediate peer service until the transfer completes or deterministic ...
virtual bool SupportsActiveMode() const
Plain FTP can create and publish a real loopback listener for PORT/EPRT.
virtual std::unique_ptr< MockConnection > CreateConnection()
Create the default plaintext transport for the control and passive data connections.
~FtpMockServer() override
Release server-owned descriptors and socketpair peers after curl has returned its client fds.
SocketSetupDisposition GetSocketSetupDisposition(curl_socket_t curlfd, curlsocktype purpose) const override
Control/passive sockets are connected AF_UNIX pairs, while active FTP needs curl to bind and listen o...
curl_socket_t HandleOpenSocket(curlsocktype purpose=CURLSOCKTYPE_IPCXN, struct curl_sockaddr *address=nullptr) override
Allocate one control socket followed by a bounded sequence of passive data sockets,...
const std::string & uploaded_data() const
Expose a bounded upload prefix from the most recent drive while continuing to drain all client bytes,...
std::size_t active_pollset_probe_count() const
Report how many requested zero-timeout multi-poll probes ran during the most recent drive.
void ObserveState() override
Retain protocol outcomes in coverage and introspector builds.
void ObserveIncomingData(const unsigned char *data, std::size_t size) override
Retain decoded client bytes for the control parser.
const std::string & control_transcript() const
Expose the bounded command capture from the most recent drive so tests can verify FTP states without ...
std::size_t opened_data_connection_count() const
Report how many bounded data scripts curl assigned to passive or active transfer sockets,...
void ReleaseConnections()
Close every server-owned transport before the borrowed scenario or a subclass transport disappears.
FtpMockServer()
Begin without a borrowed protobuf or open socketpairs.
static void ProbeMultiPollset(CURLM *multi)
Ask curl to construct and inspect its connection-filter pollset without waiting.
virtual void ObserveState()
Default no-op for peers without protocol-specific observations.
static constexpr int kMaxDriveIterations
Hard operation budget for one scenario.
static constexpr int kMaxIdleIterations
Consecutive no-progress budget for ordinary scenarios.
Command-aware in-process peer for curl's FTP state machine.
MockConnection and MockServer — the in-process peer that feeds canned response bytes to libcurl over ...
SocketSetupDisposition
Tell curl whether an application-provided socket still needs its normal connect/options setup or alre...
Shared complexity limits for structured scenarios.