Coverage Report

Created: 2026-09-14 07:12

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.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