Coverage Report

Created: 2026-09-01 07:00

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