/src/curl_fuzzer/proto_fuzzer/tftp_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 bounded loopback UDP TFTP peer. |
9 | | |
10 | | #include "proto_fuzzer/tftp_mock_server.h" |
11 | | |
12 | | #include <fcntl.h> |
13 | | #include <sys/socket.h> |
14 | | #include <unistd.h> |
15 | | |
16 | | #include <algorithm> |
17 | | #include <array> |
18 | | #include <cerrno> |
19 | | #include <cstring> |
20 | | #include <utility> |
21 | | |
22 | | namespace proto_fuzzer { |
23 | | |
24 | | /// Start without live descriptors or borrowed scenario storage. RunLoop builds |
25 | | /// both immediately before the first curl_multi_perform that can observe them. |
26 | | TftpMockServer::TftpMockServer() |
27 | 0 | : response_datagrams_(), |
28 | 0 | response_datagram_count_(0), |
29 | 0 | next_response_datagram_(0), |
30 | 0 | request_fd_(-1), |
31 | 0 | transfer_fd_(-1), |
32 | 0 | client_address_(), |
33 | 0 | client_address_length_(0), |
34 | 0 | has_client_address_(false), |
35 | 0 | socket_opened_(false), |
36 | 0 | request_port_(0), |
37 | 0 | transfer_port_(0), |
38 | 0 | received_datagrams_() {} |
39 | | |
40 | | /// Close the two server endpoints after the base drive has removed curl's |
41 | | /// separately-owned client descriptor from its multi handle. |
42 | 0 | TftpMockServer::~TftpMockServer() { ResetPeer(); } |
43 | | |
44 | | /// Return observations rather than parsing them in the peer. Tests can assert |
45 | | /// exact RRQ/WRQ/DATA/ACK bytes while production fuzz iterations pay only the |
46 | | /// bounded copies already required to expose those observations. |
47 | 0 | const std::vector<TftpReceivedDatagram>& TftpMockServer::received_datagrams() const { return received_datagrams_; } |
48 | | |
49 | | /// Return the kernel-selected request port in host byte order. |
50 | 0 | std::uint16_t TftpMockServer::request_port() const { return request_port_; } |
51 | | |
52 | | /// Return the kernel-selected transfer port in host byte order. |
53 | 0 | std::uint16_t TftpMockServer::transfer_port() const { return transfer_port_; } |
54 | | |
55 | | /// Borrow only the response prefix the runtime can emit. The nonempty check on |
56 | | /// initial_response preserves proto3's absent/empty equivalence; repeated bytes |
57 | | /// retain presence, so an empty on_readable entry remains a real zero-length |
58 | | /// UDP datagram useful for curl's short-packet retry path. |
59 | 0 | void TftpMockServer::PrepareScript(const curl::fuzzer::proto::Connection& connection) { |
60 | 0 | response_datagrams_.fill(nullptr); |
61 | 0 | response_datagram_count_ = 0; |
62 | 0 | next_response_datagram_ = 0; |
63 | |
|
64 | 0 | if (!connection.initial_response().empty()) { |
65 | 0 | response_datagrams_[response_datagram_count_++] = &connection.initial_response(); |
66 | 0 | } |
67 | 0 | const std::size_t chunk_count = |
68 | 0 | std::min<std::size_t>(scenario_limits::kMaxResponseChunks, connection.on_readable_size()); |
69 | 0 | for (std::size_t index = 0; index < chunk_count; ++index) { |
70 | 0 | response_datagrams_[response_datagram_count_++] = &connection.on_readable(static_cast<int>(index)); |
71 | 0 | } |
72 | 0 | } |
73 | | |
74 | | /// Tear down only state owned by this mock. Curl takes ownership of the client |
75 | | /// descriptor as soon as HandleOpenSocket succeeds, so retaining or closing a |
76 | | /// duplicate here would create cross-owner lifetime bugs during multi cleanup. |
77 | 0 | void TftpMockServer::ResetPeer() { |
78 | 0 | if (request_fd_ >= 0) { |
79 | 0 | (void)::close(request_fd_); |
80 | 0 | request_fd_ = -1; |
81 | 0 | } |
82 | 0 | if (transfer_fd_ >= 0) { |
83 | 0 | (void)::close(transfer_fd_); |
84 | 0 | transfer_fd_ = -1; |
85 | 0 | } |
86 | 0 | std::memset(&client_address_, 0, sizeof(client_address_)); |
87 | 0 | client_address_length_ = 0; |
88 | 0 | has_client_address_ = false; |
89 | 0 | socket_opened_ = false; |
90 | 0 | request_port_ = 0; |
91 | 0 | transfer_port_ = 0; |
92 | 0 | } |
93 | | |
94 | | /// Establish both descriptor properties ourselves. Curl normally requests |
95 | | /// SOCK_CLOEXEC/SOCK_NONBLOCK from socket(), but an application callback is |
96 | | /// allowed to ignore those type flags and therefore must return a safe fd. |
97 | 0 | bool TftpMockServer::ConfigureSocket(int fd) { |
98 | 0 | if (fd < 0) { |
99 | 0 | return false; |
100 | 0 | } |
101 | 0 | const int descriptor_flags = ::fcntl(fd, F_GETFD, 0); |
102 | 0 | if (descriptor_flags < 0 || ::fcntl(fd, F_SETFD, descriptor_flags | FD_CLOEXEC) < 0) { |
103 | 0 | return false; |
104 | 0 | } |
105 | 0 | const int status_flags = ::fcntl(fd, F_GETFL, 0); |
106 | 0 | return status_flags >= 0 && ::fcntl(fd, F_SETFL, status_flags | O_NONBLOCK) == 0; |
107 | 0 | } |
108 | | |
109 | | /// Use ephemeral loopback ports so parallel fuzz workers cannot collide and no |
110 | | /// privilege is required for TFTP's conventional port 69. getsockname, rather |
111 | | /// than assumptions about bind(), is the authority on the chosen destination. |
112 | 0 | int TftpMockServer::OpenLoopbackSocket(struct sockaddr_in* bound_address) { |
113 | 0 | if (bound_address == nullptr) { |
114 | 0 | return -1; |
115 | 0 | } |
116 | 0 | const int fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
117 | 0 | if (fd < 0 || !ConfigureSocket(fd)) { |
118 | 0 | if (fd >= 0) { |
119 | 0 | (void)::close(fd); |
120 | 0 | } |
121 | 0 | return -1; |
122 | 0 | } |
123 | | |
124 | 0 | struct sockaddr_in requested = {}; |
125 | 0 | requested.sin_family = AF_INET; |
126 | 0 | requested.sin_port = htons(0); |
127 | 0 | requested.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
128 | 0 | if (::bind(fd, reinterpret_cast<const struct sockaddr*>(&requested), sizeof(requested)) != 0) { |
129 | 0 | (void)::close(fd); |
130 | 0 | return -1; |
131 | 0 | } |
132 | | |
133 | 0 | socklen_t length = sizeof(*bound_address); |
134 | 0 | std::memset(bound_address, 0, sizeof(*bound_address)); |
135 | 0 | if (::getsockname(fd, reinterpret_cast<struct sockaddr*>(bound_address), &length) != 0 || |
136 | 0 | length != sizeof(*bound_address) || bound_address->sin_family != AF_INET) { |
137 | 0 | (void)::close(fd); |
138 | 0 | return -1; |
139 | 0 | } |
140 | 0 | return fd; |
141 | 0 | } |
142 | | |
143 | | /// Curl explicitly permits CURLOPT_OPENSOCKETFUNCTION to replace its supplied |
144 | | /// destination. Replacing the metadata as well as sockaddr is essential: curl |
145 | | /// copies these values into its connection filter and TFTP later retrieves that |
146 | | /// copy for sendto(), independently of the returned descriptor's properties. |
147 | | bool TftpMockServer::RewriteDestination(struct curl_sockaddr* address, const struct sockaddr_in& destination) { |
148 | | if (address == nullptr || sizeof(destination) > sizeof(address->addr)) { |
149 | | return false; |
150 | | } |
151 | | address->family = AF_INET; |
152 | | address->socktype = SOCK_DGRAM; |
153 | | address->protocol = IPPROTO_UDP; |
154 | | address->addrlen = sizeof(destination); |
155 | | std::memset(&address->addr, 0, sizeof(address->addr)); |
156 | | std::memcpy(&address->addr, &destination, sizeof(destination)); |
157 | | return true; |
158 | | } |
159 | | |
160 | | /// Create both server transfer IDs before returning curl's client socket. The |
161 | | /// first response deliberately comes from a port different from the rewritten |
162 | | /// request destination, matching real TFTP and making curl's address-pinning |
163 | | /// transition observable through where its next ACK/DATA arrives. |
164 | 0 | curl_socket_t TftpMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) { |
165 | 0 | if (purpose != CURLSOCKTYPE_IPCXN || address == nullptr || socket_opened_) { |
166 | 0 | return CURL_SOCKET_BAD; |
167 | 0 | } |
168 | 0 | socket_opened_ = true; |
169 | |
|
170 | 0 | struct sockaddr_in request_address = {}; |
171 | 0 | struct sockaddr_in transfer_address = {}; |
172 | 0 | request_fd_ = OpenLoopbackSocket(&request_address); |
173 | 0 | transfer_fd_ = OpenLoopbackSocket(&transfer_address); |
174 | 0 | if (request_fd_ < 0 || transfer_fd_ < 0 || !RewriteDestination(address, request_address)) { |
175 | 0 | ResetPeer(); |
176 | 0 | return CURL_SOCKET_BAD; |
177 | 0 | } |
178 | | |
179 | 0 | request_port_ = ntohs(request_address.sin_port); |
180 | 0 | transfer_port_ = ntohs(transfer_address.sin_port); |
181 | |
|
182 | 0 | const int client_fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
183 | 0 | if (client_fd < 0 || !ConfigureSocket(client_fd)) { |
184 | 0 | if (client_fd >= 0) { |
185 | 0 | (void)::close(client_fd); |
186 | 0 | } |
187 | 0 | ResetPeer(); |
188 | 0 | return CURL_SOCKET_BAD; |
189 | 0 | } |
190 | 0 | return static_cast<curl_socket_t>(client_fd); |
191 | 0 | } |
192 | | |
193 | | /// Pin the first IPv4 client endpoint. Later packets are still captured, but |
194 | | /// they cannot redirect scripted responses; otherwise a mutation could make the |
195 | | /// harness accept source changes that curl's own TFTP implementation rejects. |
196 | 0 | void TftpMockServer::RememberClientAddress(const struct sockaddr_in& address, socklen_t length) { |
197 | 0 | if (has_client_address_ || length != sizeof(address) || address.sin_family != AF_INET) { |
198 | 0 | return; |
199 | 0 | } |
200 | 0 | client_address_ = address; |
201 | 0 | client_address_length_ = length; |
202 | 0 | has_client_address_ = true; |
203 | 0 | } |
204 | | |
205 | | /// Drain until EAGAIN so curl never experiences harness-created UDP receive |
206 | | /// backpressure. A 65,536-byte buffer covers the maximum IPv4 UDP payload, and |
207 | | /// recvfrom preserves even zero-length datagrams as one state-machine event. |
208 | 0 | std::size_t TftpMockServer::DrainSocket(int fd, TftpSocketRole role) { |
209 | 0 | if (fd < 0) { |
210 | 0 | return 0; |
211 | 0 | } |
212 | | |
213 | 0 | std::array<unsigned char, 65536> packet; |
214 | 0 | std::size_t count = 0; |
215 | 0 | while (true) { |
216 | 0 | struct sockaddr_in source = {}; |
217 | 0 | socklen_t source_length = sizeof(source); |
218 | 0 | const ssize_t received = |
219 | 0 | ::recvfrom(fd, packet.data(), packet.size(), 0, reinterpret_cast<struct sockaddr*>(&source), &source_length); |
220 | 0 | if (received < 0) { |
221 | 0 | if (errno == EINTR) { |
222 | 0 | continue; |
223 | 0 | } |
224 | 0 | break; |
225 | 0 | } |
226 | | |
227 | 0 | ++count; |
228 | 0 | RememberClientAddress(source, source_length); |
229 | 0 | if (received_datagrams_.size() < kMaxCapturedDatagrams) { |
230 | 0 | TftpReceivedDatagram observation; |
231 | 0 | observation.received_on = role; |
232 | 0 | observation.source_port = |
233 | 0 | source_length == sizeof(source) && source.sin_family == AF_INET ? ntohs(source.sin_port) : 0; |
234 | 0 | observation.bytes.assign(reinterpret_cast<const char*>(packet.data()), static_cast<std::size_t>(received)); |
235 | 0 | received_datagrams_.push_back(std::move(observation)); |
236 | 0 | } |
237 | 0 | } |
238 | 0 | return count; |
239 | 0 | } |
240 | | |
241 | | /// Drain the well-known request endpoint first because it is the only valid |
242 | | /// source of the initial client address. Once a response selects the transfer |
243 | | /// endpoint, draining both remains cheap and captures protocol mistakes without |
244 | | /// allowing either queue to survive into a later fuzz iteration. |
245 | 0 | std::size_t TftpMockServer::DrainClientDatagrams() { |
246 | 0 | return DrainSocket(request_fd_, TftpSocketRole::kRequest) + DrainSocket(transfer_fd_, TftpSocketRole::kTransfer); |
247 | 0 | } |
248 | | |
249 | | /// Consume exactly one script boundary per curl turn. Sending from the transfer |
250 | | /// endpoint rather than the request endpoint is what makes subsequent client |
251 | | /// traffic prove curl accepted the peer's new TFTP transfer ID. |
252 | 0 | bool TftpMockServer::SendNextDatagram() { |
253 | 0 | if (!has_client_address_ || transfer_fd_ < 0 || next_response_datagram_ >= response_datagram_count_) { |
254 | 0 | return false; |
255 | 0 | } |
256 | | |
257 | 0 | const std::string& datagram = *response_datagrams_[next_response_datagram_++]; |
258 | 0 | (void)::sendto(transfer_fd_, datagram.data(), datagram.size(), 0, |
259 | 0 | reinterpret_cast<const struct sockaddr*>(&client_address_), client_address_length_); |
260 | 0 | return true; |
261 | 0 | } |
262 | | |
263 | | /// Give curl one nonblocking state-machine turn, retain all client packets that |
264 | | /// turn produced, then release at most one peer packet. Completion gets one |
265 | | /// final drain so the terminal ACK remains observable. Incomplete scripts stop |
266 | | /// after a small idle prefix rather than waiting for TFTP's one-second retry |
267 | | /// clock, keeping mutation throughput independent of wall time. |
268 | 0 | void TftpMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
269 | 0 | (void)easy; |
270 | 0 | ResetPeer(); |
271 | 0 | PrepareScript(scenario.connection()); |
272 | 0 | received_datagrams_.clear(); |
273 | 0 | received_datagrams_.reserve(kMaxCapturedDatagrams); |
274 | |
|
275 | 0 | int still_running = 1; |
276 | 0 | int idle_iterations = 0; |
277 | 0 | for (int iteration = 0; iteration < kMaxDriveIterations; ++iteration) { |
278 | 0 | const CURLMcode result = curl_multi_perform(multi, &still_running); |
279 | 0 | if (result != CURLM_OK) { |
280 | 0 | break; |
281 | 0 | } |
282 | | |
283 | 0 | const bool received = DrainClientDatagrams() != 0; |
284 | 0 | const bool sent = still_running != 0 && received && SendNextDatagram(); |
285 | 0 | if (received || sent) { |
286 | 0 | idle_iterations = 0; |
287 | 0 | } else { |
288 | 0 | ++idle_iterations; |
289 | 0 | } |
290 | |
|
291 | 0 | if (still_running == 0 || idle_iterations >= kMaxIdleIterations) { |
292 | 0 | break; |
293 | 0 | } |
294 | 0 | } |
295 | 0 | } |
296 | | |
297 | | } // namespace proto_fuzzer |