13#include <netinet/in.h>
14#include <sys/socket.h>
27#include "proto_fuzzer/allocation_fault_injection.h"
31namespace proto_fuzzer {
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) {}
72 if (data ==
nullptr || size == 0) {
75 pending_control_bytes_.append(
reinterpret_cast<const char*
>(data), size);
83void FtpMockServer::DataChannel::ObserveIncomingData(
const unsigned char* data, std::size_t size) {
84 if (data ==
nullptr || size == 0) {
87 incoming_data.append(
reinterpret_cast<const char*
>(data), size);
118 CloseActiveDescriptors();
119 control_connection_.reset();
120 for (DataChannel& channel : data_channels_) {
121 channel.connection.reset();
122 channel.incoming_data.clear();
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;
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;
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;
159 for (DataChannel& channel : data_channels_) {
160 if (channel.active_fd >= 0) {
161 close(channel.active_fd);
162 channel.active_fd = -1;
165 pending_active_channel_ = kMaxDataChannels;
172void FtpMockServer::ApplyScriptBackpressure(MockConnection*
connection,
const curl::fuzzer::proto::Connection& script) {
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()));
185bool FtpMockServer::UsesActiveMode()
const {
186 if (scenario_ ==
nullptr) {
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();
206 if (purpose == CURLSOCKTYPE_ACCEPT || curlfd == active_listener_client_fd_) {
207 return SocketSetupDisposition::kNeedsSetup;
209 return SocketSetupDisposition::kAlreadyConnected;
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;
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;
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_));
231 if (active_listener_fd_ >= 0) {
232 close(active_listener_fd_);
233 active_listener_fd_ = -1;
235 active_listener_client_fd_ = CURL_SOCKET_BAD;
237 const int listener = socket(address->family, address->socktype, address->protocol);
239 return CURL_SOCKET_BAD;
241 active_listener_fd_ = dup(listener);
242 if (active_listener_fd_ < 0) {
244 return CURL_SOCKET_BAD;
246 active_listener_client_fd_ = listener;
247 (void)fcntl(active_listener_fd_, F_SETFD, FD_CLOEXEC);
260 if (scenario_ ==
nullptr || purpose != CURLSOCKTYPE_IPCXN) {
261 return CURL_SOCKET_BAD;
264 if (!control_opened_) {
266 if (!control_connection_ || !control_connection_->ok()) {
267 control_connection_.reset();
268 return CURL_SOCKET_BAD;
270 control_connection_->SetIncomingDataObserver(
this);
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;
282 control_opened_ =
true;
283 return control_connection_->take_client_fd();
286 if (UsesActiveMode()) {
293 return CURL_SOCKET_BAD;
295 return OpenActiveListener(address);
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;
304 DataChannel& channel = data_channels_[next_data_script_];
305 channel.script = &scenario_->subsequent_connections(
static_cast<int>(next_data_script_));
307 if (!channel.connection || !channel.connection->ok()) {
308 channel.connection.reset();
309 channel.script =
nullptr;
310 return CURL_SOCKET_BAD;
312 channel.connection->SetIncomingDataObserver(&channel);
314 ApplyScriptBackpressure(channel.connection.get(), *channel.script);
316 return channel.connection->take_client_fd();
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);
327 offset +=
static_cast<std::size_t
>(written);
330 if (written < 0 && errno == EINTR) {
340bool FtpMockServer::ReadActiveBytes(
int fd, std::string* output) {
341 bool made_progress =
false;
342 char buffer[16 * 1024];
344 const ssize_t amount = recv(fd, buffer,
sizeof(buffer), MSG_DONTWAIT);
346 output->append(buffer,
static_cast<std::size_t
>(amount));
347 made_progress =
true;
350 if (amount < 0 && errno == EINTR) {
353 return made_progress;
360void FtpMockServer::ConnectActiveDataChannel() {
361 if (active_listener_fd_ < 0 || pending_active_channel_ >= kMaxDataChannels) {
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) {
372 const int fd = socket(address.ss_family, SOCK_STREAM, IPPROTO_TCP);
376 if (connect(fd,
reinterpret_cast<struct sockaddr*
>(&address), address_length) != 0) {
380 const int flags = fcntl(fd, F_GETFL, 0);
382 (void)fcntl(fd, F_SETFL, flags | O_NONBLOCK);
384 (void)fcntl(fd, F_SETFD, FD_CLOEXEC);
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;
400void FtpMockServer::CapturePrefix(std::string_view source, std::size_t limit, std::string* destination) {
401 if (destination ==
nullptr || destination->size() >= limit) {
404 const std::size_t available = limit - destination->size();
405 destination->append(source.data(), std::min(source.size(), available));
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')) {
419 std::size_t end = begin;
420 while (end < command.size() && command[end] !=
' ' && command[end] !=
'\t' && command[end] !=
'\r' &&
421 command[end] !=
'\n') {
424 return command.substr(begin, end - begin);
433bool FtpMockServer::VerbEquals(std::string_view verb, std::string_view expected_uppercase) {
434 if (verb.size() != expected_uppercase.size()) {
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');
442 if (actual !=
static_cast<unsigned char>(expected_uppercase[index])) {
455FtpMockServer::TransferDirection FtpMockServer::DirectionForVerb(std::string_view verb) {
456 if (VerbEquals(verb,
"STOR") || VerbEquals(verb,
"APPE")) {
457 return TransferDirection::kUpload;
459 if (VerbEquals(verb,
"RETR") || VerbEquals(verb,
"LIST") || VerbEquals(verb,
"NLST") || VerbEquals(verb,
"MLSD")) {
460 return TransferDirection::kDownload;
462 return TransferDirection::kNone;
473bool FtpMockServer::IsConfiguredCustomDownload(std::string_view verb)
const {
474 if (scenario_ ==
nullptr || verb.empty()) {
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) {
483 if (option.value_case() != curl::fuzzer::proto::SetOption::kStringValue) {
486 return VerbEquals(verb, CommandVerb(option.string_value()));
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) {
503 const std::size_t line_size = newline - line_start + 1;
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');
513 line_start = newline + 1;
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) {
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] ==
' ';
541const std::string* FtpMockServer::NextControlReply() {
542 if (scenario_ ==
nullptr || next_control_reply_ >= control_reply_count_) {
545 return &scenario_->connection().on_readable(
static_cast<int>(next_control_reply_++));
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()));
566void FtpMockServer::QueueTransferCompletion() {
567 const std::string* response = NextControlReply();
568 if (response !=
nullptr && response->empty()) {
569 if (control_connection_ !=
nullptr) {
570 control_connection_->ShutdownWrite();
574 if (response !=
nullptr && !response->empty()) {
575 (void)WriteControlBytes(*response);
576 if (FirstReplyCode(*response) >= 0) {
579 if (TailNeedsOnlyNewline(*response)) {
580 (void)WriteControlBytes(
"\r\n");
583 if (response->back() !=
'\n') {
584 (void)WriteControlBytes(
"\r\n");
590 (void)WriteControlBytes(
"226 mock transfer complete\r\n");
599void FtpMockServer::PreloadDownload(DataChannel* channel) {
600 if (channel ==
nullptr || channel->script ==
nullptr || (channel->connection ==
nullptr && channel->active_fd < 0)) {
604 const std::string& initial = channel->script->initial_response();
605 const auto write_bytes = [channel](
const std::string& bytes) {
609 if (channel->connection !=
nullptr) {
610 return channel->connection->WriteAll(
reinterpret_cast<const unsigned char*
>(bytes.data()), bytes.size());
612 return WriteActiveBytes(channel->active_fd,
reinterpret_cast<const unsigned char*
>(bytes.data()), bytes.size());
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);
622 if (channel->connection !=
nullptr) {
623 channel->connection->ShutdownWrite();
625 (void)shutdown(channel->active_fd, SHUT_WR);
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) {
640 channel.transfer_started =
true;
641 channel.upload = direction == TransferDirection::kUpload;
642 if (!channel.upload) {
643 PreloadDownload(&channel);
657void FtpMockServer::HandleControlCommand(std::string_view command) {
658 const std::string* response = NextControlReply();
659 if (response ==
nullptr) {
663 const int response_code = FirstReplyCode(*response);
664 const bool response_written = WriteControlBytes(*response);
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();
671 if ((VerbEquals(verb,
"EPRT") || VerbEquals(verb,
"PORT")) && response_code >= 200 && response_code < 300) {
672 ConnectActiveDataChannel();
674 TransferDirection direction = DirectionForVerb(verb);
675 if (direction == TransferDirection::kNone && IsConfiguredCustomDownload(verb)) {
676 direction = TransferDirection::kDownload;
678 const bool starts_download =
679 direction == TransferDirection::kDownload && (response_code == 125 || response_code == 150);
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();
693bool FtpMockServer::ServiceControlConnection() {
694 if (control_connection_ ==
nullptr) {
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;
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) {
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;
717 pending_control_bytes_.erase(0, consumed);
722 const std::size_t flush_progress = control_connection_->DrainIncoming();
723 made_progress = made_progress || flush_progress != 0;
725 return made_progress;
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;
749 if (channel.upload && channel.active_fd >= 0) {
751 (void)ReadActiveBytes(channel.active_fd, &bytes);
752 if (!bytes.empty()) {
753 CapturePrefix(bytes, kMaxCapturedUploadBytes, &uploaded_data_);
754 made_progress =
true;
758 return made_progress;
766void FtpMockServer::FinishConnections(
bool completed) {
767 if (control_connection_ !=
nullptr) {
769 (void)WriteControlBytes(
"221 mock closing control connection\r\n");
771 control_connection_->ShutdownWrite();
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();
777 if (data_channels_[index].active_fd >= 0) {
778 (void)shutdown(data_channels_[index].active_fd, SHUT_WR);
781 if (active_listener_fd_ >= 0) {
782 close(active_listener_fd_);
783 active_listener_fd_ = -1;
785 active_listener_client_fd_ = CURL_SOCKET_BAD;
797 ResetForScenario(scenario);
799 int still_running = 1;
800 int idle_iterations = 0;
801 int drive_iterations = 0;
802 CURLMcode result = CURLM_OK;
804 const int running_before = still_running;
805 result = curl_multi_perform(multi, &still_running);
806 if (result != CURLM_OK) {
813 if (scenario.probe_multi_pollset() && still_running && control_opened_) {
815 ++active_pollset_probe_count_;
818 bool made_progress = still_running != running_before;
819 made_progress = ServiceControlConnection() || made_progress;
820 made_progress = ServiceDataConnections() || made_progress;
830 (void)ServiceControlConnection();
831 (void)ServiceDataConnections();
832 FinishConnections(still_running == 0 && result == CURLM_OK);
838#if defined(PROTO_FUZZER_OBSERVE_STATE)
842 (void)ftp_observation;
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.
MockConnection * connection()
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.