Coverage Report

Created: 2026-10-15 06:25

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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