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