/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 | 2.50k | : response_datagrams_(), |
28 | 2.50k | response_datagram_count_(0), |
29 | 2.50k | next_response_datagram_(0), |
30 | 2.50k | request_fd_(-1), |
31 | 2.50k | transfer_fd_(-1), |
32 | 2.50k | client_address_(), |
33 | 2.50k | client_address_length_(0), |
34 | 2.50k | has_client_address_(false), |
35 | 2.50k | socket_opened_(false), |
36 | 2.50k | request_port_(0), |
37 | 2.50k | transfer_port_(0), |
38 | 2.50k | 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 | 2.50k | TftpMockServer::~TftpMockServer() { ResetPeer(); } |
43 | | |
44 | | /// Return bounded observations from the most recent run rather than parsing |
45 | | /// them in the peer. Tests can assert exact RRQ/WRQ/DATA/ACK bytes while production |
46 | | /// fuzz iterations pay only the bounded copies needed for those observations. |
47 | | /// @return Borrowed observations that remain valid until the next drive or |
48 | | /// destruction of this server. |
49 | 2.47k | const std::vector<TftpReceivedDatagram>& TftpMockServer::received_datagrams() const { return received_datagrams_; } |
50 | | |
51 | | /// Expose the kernel-selected request endpoint for transfer-ID assertions, |
52 | | /// not for routing. |
53 | | /// @return request UDP port in host byte order, or zero before socket setup. |
54 | 2.47k | std::uint16_t TftpMockServer::request_port() const { return request_port_; } |
55 | | |
56 | | /// Expose the kernel-selected response endpoint for transfer-ID assertions, |
57 | | /// not for routing. |
58 | | /// @return transfer UDP port in host byte order, or zero before socket setup. |
59 | 2.47k | std::uint16_t TftpMockServer::transfer_port() const { return transfer_port_; } |
60 | | |
61 | | /// TFTP returns an unconnected IPv4 datagram descriptor, so curl must still |
62 | | /// bind/configure it before the first sendto. |
63 | | /// @param curlfd Descriptor returned by HandleOpenSocket. |
64 | | /// @param purpose Role curl assigned to the descriptor. |
65 | | /// @return Whether curl must perform its normal socket setup. |
66 | | SocketSetupDisposition TftpMockServer::GetSocketSetupDisposition(curl_socket_t /*curlfd*/, |
67 | 2.01k | curlsocktype /*purpose*/) const { |
68 | 2.01k | return SocketSetupDisposition::kNeedsSetup; |
69 | 2.01k | } |
70 | | |
71 | | /// Build the borrowed response-pointer table before curl can open a socket, |
72 | | /// using only the prefix the runtime can emit. The nonempty check on |
73 | | /// initial_response preserves proto3's absent/empty equivalence; repeated bytes |
74 | | /// retain presence, so an empty on_readable entry remains a real zero-length |
75 | | /// UDP datagram useful for curl's short-packet retry path. |
76 | | /// @param connection Primary connection whose packet boundaries are retained. |
77 | 2.39k | void TftpMockServer::PrepareScript(const curl::fuzzer::proto::Connection& connection) { |
78 | 2.39k | response_datagrams_.fill(nullptr); |
79 | 2.39k | response_datagram_count_ = 0; |
80 | 2.39k | next_response_datagram_ = 0; |
81 | | |
82 | 2.39k | if (!connection.initial_response().empty()) { |
83 | 33 | response_datagrams_[response_datagram_count_++] = &connection.initial_response(); |
84 | 33 | } |
85 | 2.39k | const std::size_t chunk_count = |
86 | 2.39k | std::min<std::size_t>(scenario_limits::kMaxResponseChunks, connection.on_readable_size()); |
87 | 2.98k | for (std::size_t index = 0; index < chunk_count; ++index) { |
88 | 591 | response_datagrams_[response_datagram_count_++] = &connection.on_readable(static_cast<int>(index)); |
89 | 591 | } |
90 | 2.39k | } |
91 | | |
92 | | /// Close only server-owned sockets and clear address/routing state. Curl takes |
93 | | /// ownership of the client descriptor as soon as HandleOpenSocket succeeds, so |
94 | | /// retaining or closing a duplicate here would create lifetime bugs during multi |
95 | | /// cleanup. |
96 | 4.89k | void TftpMockServer::ResetPeer() { |
97 | 4.89k | if (request_fd_ >= 0) { |
98 | 2.01k | (void)::close(request_fd_); |
99 | 2.01k | request_fd_ = -1; |
100 | 2.01k | } |
101 | 4.89k | if (transfer_fd_ >= 0) { |
102 | 2.01k | (void)::close(transfer_fd_); |
103 | 2.01k | transfer_fd_ = -1; |
104 | 2.01k | } |
105 | 4.89k | std::memset(&client_address_, 0, sizeof(client_address_)); |
106 | 4.89k | client_address_length_ = 0; |
107 | 4.89k | has_client_address_ = false; |
108 | 4.89k | socket_opened_ = false; |
109 | 4.89k | request_port_ = 0; |
110 | 4.89k | transfer_port_ = 0; |
111 | 4.89k | } |
112 | | |
113 | | /// Establish both descriptor properties ourselves. Curl normally requests |
114 | | /// SOCK_CLOEXEC/SOCK_NONBLOCK from socket(), but an application callback is |
115 | | /// allowed to ignore those type flags and therefore must return a safe fd. |
116 | | /// @param fd Descriptor to configure. |
117 | | /// @return true only when both descriptor invariants were established. |
118 | 6.05k | bool TftpMockServer::ConfigureSocket(int fd) { |
119 | 6.05k | if (fd < 0) { |
120 | 0 | return false; |
121 | 0 | } |
122 | 6.05k | const int descriptor_flags = ::fcntl(fd, F_GETFD, 0); |
123 | 6.05k | if (descriptor_flags < 0 || ::fcntl(fd, F_SETFD, descriptor_flags | FD_CLOEXEC) < 0) { |
124 | 0 | return false; |
125 | 0 | } |
126 | 6.05k | const int status_flags = ::fcntl(fd, F_GETFL, 0); |
127 | 6.05k | return status_flags >= 0 && ::fcntl(fd, F_SETFL, status_flags | O_NONBLOCK) == 0; |
128 | 6.05k | } |
129 | | |
130 | | /// Bind one IPv4 UDP endpoint to an ephemeral loopback port so parallel fuzz |
131 | | /// workers cannot collide and no privilege is required for TFTP's conventional |
132 | | /// port 69. getsockname, rather than assumptions about bind(), determines the |
133 | | /// chosen destination. |
134 | | /// @param bound_address Receives the exact address selected by the kernel. |
135 | | /// @return owned descriptor, or -1 after closing any failed descriptor. |
136 | 4.03k | int TftpMockServer::OpenLoopbackSocket(struct sockaddr_in* bound_address) { |
137 | 4.03k | if (bound_address == nullptr) { |
138 | 0 | return -1; |
139 | 0 | } |
140 | 4.03k | const int fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
141 | 4.03k | if (fd < 0 || !ConfigureSocket(fd)) { |
142 | 0 | if (fd >= 0) { |
143 | 0 | (void)::close(fd); |
144 | 0 | } |
145 | 0 | return -1; |
146 | 0 | } |
147 | | |
148 | 4.03k | struct sockaddr_in requested = {}; |
149 | 4.03k | requested.sin_family = AF_INET; |
150 | 4.03k | requested.sin_port = htons(0); |
151 | 4.03k | requested.sin_addr.s_addr = htonl(INADDR_LOOPBACK); |
152 | 4.03k | if (::bind(fd, reinterpret_cast<const struct sockaddr*>(&requested), sizeof(requested)) != 0) { |
153 | 0 | (void)::close(fd); |
154 | 0 | return -1; |
155 | 0 | } |
156 | | |
157 | 4.03k | socklen_t length = sizeof(*bound_address); |
158 | 4.03k | std::memset(bound_address, 0, sizeof(*bound_address)); |
159 | 4.03k | if (::getsockname(fd, reinterpret_cast<struct sockaddr*>(bound_address), &length) != 0 || |
160 | 4.03k | length != sizeof(*bound_address) || bound_address->sin_family != AF_INET) { |
161 | 0 | (void)::close(fd); |
162 | 0 | return -1; |
163 | 0 | } |
164 | 4.03k | return fd; |
165 | 4.03k | } |
166 | | |
167 | | /// Curl explicitly permits CURLOPT_OPENSOCKETFUNCTION to replace its supplied |
168 | | /// destination. Replacing the metadata as well as sockaddr is essential: curl |
169 | | /// copies these values into its connection filter and TFTP later retrieves that |
170 | | /// copy for sendto(), independently of the returned descriptor's properties. |
171 | | /// Rewriting every field prevents a mutated URL from selecting a real endpoint. |
172 | | /// @param address Mutable callback address to rewrite. |
173 | | /// @param destination Bound request endpoint to install. |
174 | | /// @return true when the public address storage can represent IPv4 safely. |
175 | 2.01k | bool TftpMockServer::RewriteDestination(struct curl_sockaddr* address, const struct sockaddr_in& destination) { |
176 | 2.01k | if (address == nullptr || sizeof(destination) > sizeof(address->addr)) { |
177 | 0 | return false; |
178 | 0 | } |
179 | 2.01k | address->family = AF_INET; |
180 | 2.01k | address->socktype = SOCK_DGRAM; |
181 | 2.01k | address->protocol = IPPROTO_UDP; |
182 | 2.01k | address->addrlen = sizeof(destination); |
183 | 2.01k | std::memset(&address->addr, 0, sizeof(address->addr)); |
184 | 2.01k | std::memcpy(&address->addr, &destination, sizeof(destination)); |
185 | 2.01k | return true; |
186 | 2.01k | } |
187 | | |
188 | | /// Create both server transfer IDs before returning curl's client socket. The |
189 | | /// first response deliberately comes from a port different from the rewritten |
190 | | /// request destination, matching real TFTP and making curl's address-pinning |
191 | | /// transition observable through where its next ACK/DATA arrives. |
192 | | /// Curl owns the client descriptor after a successful return. |
193 | | /// @param purpose Socket purpose supplied by curl's open-socket callback. |
194 | | /// @param address Mutable destination storage supplied by curl. |
195 | | /// @return client UDP descriptor, or CURL_SOCKET_BAD on setup failure. |
196 | 2.01k | curl_socket_t TftpMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) { |
197 | 2.01k | if (purpose != CURLSOCKTYPE_IPCXN || address == nullptr || socket_opened_) { |
198 | 0 | return CURL_SOCKET_BAD; |
199 | 0 | } |
200 | 2.01k | socket_opened_ = true; |
201 | | |
202 | 2.01k | struct sockaddr_in request_address = {}; |
203 | 2.01k | struct sockaddr_in transfer_address = {}; |
204 | 2.01k | request_fd_ = OpenLoopbackSocket(&request_address); |
205 | 2.01k | transfer_fd_ = OpenLoopbackSocket(&transfer_address); |
206 | 2.01k | if (request_fd_ < 0 || transfer_fd_ < 0 || !RewriteDestination(address, request_address)) { |
207 | 0 | ResetPeer(); |
208 | 0 | return CURL_SOCKET_BAD; |
209 | 0 | } |
210 | | |
211 | 2.01k | request_port_ = ntohs(request_address.sin_port); |
212 | 2.01k | transfer_port_ = ntohs(transfer_address.sin_port); |
213 | | |
214 | 2.01k | const int client_fd = ::socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); |
215 | 2.01k | if (client_fd < 0 || !ConfigureSocket(client_fd)) { |
216 | 0 | if (client_fd >= 0) { |
217 | 0 | (void)::close(client_fd); |
218 | 0 | } |
219 | 0 | ResetPeer(); |
220 | 0 | return CURL_SOCKET_BAD; |
221 | 0 | } |
222 | 2.01k | return static_cast<curl_socket_t>(client_fd); |
223 | 2.01k | } |
224 | | |
225 | | /// Pin the first IPv4 client endpoint. Later packets are still captured, but |
226 | | /// they cannot redirect scripted responses; otherwise a mutation could make the |
227 | | /// harness accept source changes that curl's own TFTP implementation rejects. |
228 | | /// @param address Source address returned by recvfrom. |
229 | | /// @param length Number of valid bytes in address. |
230 | 1.61k | void TftpMockServer::RememberClientAddress(const struct sockaddr_in& address, socklen_t length) { |
231 | 1.61k | if (has_client_address_ || length != sizeof(address) || address.sin_family != AF_INET) { |
232 | 200 | return; |
233 | 200 | } |
234 | 1.41k | client_address_ = address; |
235 | 1.41k | client_address_length_ = length; |
236 | 1.41k | has_client_address_ = true; |
237 | 1.41k | } |
238 | | |
239 | | /// Drain until EAGAIN so curl never experiences harness-created UDP receive |
240 | | /// backpressure. A 65,536-byte buffer covers the maximum IPv4 UDP payload, and |
241 | | /// recvfrom preserves even zero-length datagrams as one state-machine event. |
242 | | /// @param fd Nonblocking server descriptor to read. |
243 | | /// @param role Which endpoint the descriptor represents. |
244 | | /// @return number of datagrams consumed, including zero-length datagrams. |
245 | 28.4k | std::size_t TftpMockServer::DrainSocket(int fd, TftpSocketRole role) { |
246 | 28.4k | if (fd < 0) { |
247 | 1.93k | return 0; |
248 | 1.93k | } |
249 | | |
250 | 26.5k | std::array<unsigned char, 65536> packet; |
251 | 26.5k | std::size_t count = 0; |
252 | 28.1k | while (true) { |
253 | 28.1k | struct sockaddr_in source = {}; |
254 | 28.1k | socklen_t source_length = sizeof(source); |
255 | 28.1k | const ssize_t received = |
256 | 28.1k | ::recvfrom(fd, packet.data(), packet.size(), 0, reinterpret_cast<struct sockaddr*>(&source), &source_length); |
257 | 28.1k | if (received < 0) { |
258 | 26.5k | if (errno == EINTR) { |
259 | 0 | continue; |
260 | 0 | } |
261 | 26.5k | break; |
262 | 26.5k | } |
263 | | |
264 | 1.61k | ++count; |
265 | 1.61k | RememberClientAddress(source, source_length); |
266 | 1.61k | if (received_datagrams_.size() < kMaxCapturedDatagrams) { |
267 | 1.61k | TftpReceivedDatagram observation; |
268 | 1.61k | observation.received_on = role; |
269 | 1.61k | observation.source_port = |
270 | 1.61k | source_length == sizeof(source) && source.sin_family == AF_INET ? ntohs(source.sin_port) : 0; |
271 | 1.61k | observation.bytes.assign(reinterpret_cast<const char*>(packet.data()), static_cast<std::size_t>(received)); |
272 | 1.61k | received_datagrams_.push_back(std::move(observation)); |
273 | 1.61k | } |
274 | 1.61k | } |
275 | 26.5k | return count; |
276 | 28.4k | } |
277 | | |
278 | | /// Drain the request endpoint first because it is the only valid source of the |
279 | | /// initial client address. Once a response selects the transfer endpoint, |
280 | | /// draining both captures protocol mistakes and prevents uploads or |
281 | | /// retransmissions from filling either kernel queue between curl turns. |
282 | | /// @return total number of client datagrams consumed. |
283 | 14.2k | std::size_t TftpMockServer::DrainClientDatagrams() { |
284 | 14.2k | return DrainSocket(request_fd_, TftpSocketRole::kRequest) + DrainSocket(transfer_fd_, TftpSocketRole::kTransfer); |
285 | 14.2k | } |
286 | | |
287 | | /// Consume exactly one script boundary per curl turn. UDP sends are atomic, so |
288 | | /// an error still consumes the entry instead of retrying in a hot loop. Sending |
289 | | /// from the transfer endpoint makes subsequent client traffic prove curl accepted |
290 | | /// the peer's new TFTP transfer ID. |
291 | | /// @return true when a script entry was consumed, regardless of send result. |
292 | 1.60k | bool TftpMockServer::SendNextDatagram() { |
293 | 1.60k | if (!has_client_address_ || transfer_fd_ < 0 || next_response_datagram_ >= response_datagram_count_) { |
294 | 1.37k | return false; |
295 | 1.37k | } |
296 | | |
297 | 228 | const std::string& datagram = *response_datagrams_[next_response_datagram_++]; |
298 | 228 | (void)::sendto(transfer_fd_, datagram.data(), datagram.size(), 0, |
299 | 228 | reinterpret_cast<const struct sockaddr*>(&client_address_), client_address_length_); |
300 | 228 | return true; |
301 | 1.60k | } |
302 | | |
303 | | /// Give curl one nonblocking state-machine turn, retain all client packets that |
304 | | /// turn produced, then release at most one peer packet. Completion gets one |
305 | | /// final drain so the terminal ACK remains observable. Incomplete scripts stop |
306 | | /// after a small idle prefix rather than waiting for TFTP's one-second retry |
307 | | /// clock, keeping mutation throughput independent of wall time. |
308 | | /// @param multi Multi handle containing the scenario's easy handle. |
309 | | /// @param easy Easy handle being driven; all work is reached through multi. |
310 | | /// @param scenario Scenario whose primary Connection supplies UDP packets. |
311 | 2.39k | void TftpMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) { |
312 | 2.39k | (void)easy; |
313 | 2.39k | ResetPeer(); |
314 | 2.39k | PrepareScript(scenario.connection()); |
315 | 2.39k | received_datagrams_.clear(); |
316 | 2.39k | received_datagrams_.reserve(kMaxCapturedDatagrams); |
317 | | |
318 | 2.39k | int still_running = 1; |
319 | 2.39k | int idle_iterations = 0; |
320 | 14.2k | for (int iteration = 0; iteration < kMaxDriveIterations; ++iteration) { |
321 | 14.2k | const CURLMcode result = curl_multi_perform(multi, &still_running); |
322 | 14.2k | if (result != CURLM_OK) { |
323 | 0 | break; |
324 | 0 | } |
325 | | |
326 | 14.2k | const bool received = DrainClientDatagrams() != 0; |
327 | 14.2k | const bool sent = still_running != 0 && received && SendNextDatagram(); |
328 | 14.2k | if (received || sent) { |
329 | 1.61k | idle_iterations = 0; |
330 | 12.6k | } else { |
331 | 12.6k | ++idle_iterations; |
332 | 12.6k | } |
333 | | |
334 | 14.2k | if (still_running == 0 || idle_iterations >= kMaxIdleIterations) { |
335 | 2.39k | break; |
336 | 2.39k | } |
337 | 14.2k | } |
338 | 2.39k | } |
339 | | |
340 | | } // namespace proto_fuzzer |