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/telnet_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 preloaded TELNET mock peer.
9
10
#include "proto_fuzzer/telnet_mock_server.h"
11
12
#include <algorithm>
13
#include <cstddef>
14
#include <memory>
15
#include <string>
16
17
#include "proto_fuzzer/mock_server.h"
18
#include "proto_fuzzer/request_data.h"
19
#include "proto_fuzzer/scenario_limits.h"
20
#include "proto_fuzzer/telnet_scenario.h"
21
22
namespace proto_fuzzer {
23
24
namespace {
25
26
// BoundTelnetResponse limits worst-case negotiation output to well below this
27
// floor. Rejecting a surprising platform socket configuration is preferable
28
// to letting curl's blocking POLLOUT path become a fuzzer timeout.
29
constexpr std::size_t kMinClientSendBufferBytes = 64 * 1024;
30
31
// curl currently builds each TTYPE, XDISPLOC, or NEW_ENV reply in a fixed
32
// 2048-byte buffer. Charging that whole buffer to every retained IAC is more
33
// conservative than curl's protocol grammar, while doubled upload bytes and a
34
// generous allowance for initial/small negotiation frames cover the remaining
35
// writes that can accumulate before the pre-read drain runs.
36
constexpr std::size_t kCurlTelnetSuboptionReplyBytes = 2048;
37
constexpr std::size_t kSmallNegotiationHeadroomBytes = 4 * 1024;
38
constexpr std::size_t kMaxBufferedClientBytes =
39
    scenario_limits::kMaxTelnetControlBytes * kCurlTelnetSuboptionReplyBytes +
40
    2 * scenario_limits::kMaxTelnetUploadBytes + kSmallNegotiationHeadroomBytes;
41
static_assert(kMaxBufferedClientBytes <= kMinClientSendBufferBytes,
42
              "TELNET mutation caps exceed the verified client send buffer");
43
44
/// Copy only the byte prefix the runtime can preload before applying the
45
/// control-byte budget. Compatibility inputs bypass the LPM postprocessor, so
46
/// copying each complete protobuf field and trimming afterwards would briefly
47
/// duplicate mutation-sized response strings that curl can never observe.
48
void CopyTelnetResponsePrefix(const curl::fuzzer::proto::Connection& source,
49
4.14k
                              curl::fuzzer::proto::Connection* destination) {
50
4.14k
  std::size_t bytes_left = scenario_limits::kMaxTelnetResponseBytes;
51
5.72k
  const auto copy_fragment = [&bytes_left](const std::string& fragment, std::string* output) {
52
5.72k
    const std::size_t retained = std::min(fragment.size(), bytes_left);
53
5.72k
    output->assign(fragment.data(), retained);
54
5.72k
    bytes_left -= retained;
55
5.72k
    return retained == fragment.size();
56
5.72k
  };
57
58
4.14k
  if (!copy_fragment(source.initial_response(), destination->mutable_initial_response())) {
59
4
    return;
60
4
  }
61
62
4.14k
  const std::size_t chunk_count =
63
4.14k
      std::min<std::size_t>(scenario_limits::kMaxTelnetResponseChunks, source.on_readable_size());
64
5.71k
  for (std::size_t index = 0; index < chunk_count && bytes_left != 0; ++index) {
65
1.57k
    if (!copy_fragment(source.on_readable(static_cast<int>(index)), destination->add_on_readable())) {
66
2
      break;
67
2
    }
68
1.57k
  }
69
4.14k
}
70
71
}  // namespace
72
73
/// Start without a borrowed scenario. RunLoop installs the pointer only for
74
/// the synchronous multi drive in which curl may invoke HandleOpenSocket.
75
5.43k
TelnetMockServer::TelnetMockServer() : scenario_(nullptr), socket_opened_(false) {}
76
77
/// The base owns and closes the server half after curl releases its client fd.
78
5.43k
TelnetMockServer::~TelnetMockServer() = default;
79
80
/// Attach the synchronous peer drain to TELNET's upload callback. curl's
81
/// TELNET driver does not yield to RunLoop between network parsing and
82
/// callback writes, so this is the only cheap point at which the harness can
83
/// keep the outbound socket empty.
84
/// @param request_data Callback state retained through the transfer.
85
5.42k
void TelnetMockServer::ConfigureRequestData(ScenarioRequestData* request_data) {
86
5.42k
  if (request_data != nullptr) {
87
5.42k
    request_data->SetBeforeUploadReadCallback(&DrainBeforeUploadRead, this);
88
5.42k
  }
89
5.42k
}
90
91
/// C-compatible UploadScriptState hook forwarding to DrainIncoming().
92
1.75k
void TelnetMockServer::DrainBeforeUploadRead(void* userdata) {
93
1.75k
  auto* server = static_cast<TelnetMockServer*>(userdata);
94
1.75k
  if (server != nullptr && server->connection_) {
95
1.75k
    (void)server->connection_->DrainIncoming();
96
1.75k
  }
97
1.75k
}
98
99
/// Create one socketpair, preload every raw response fragment curl can observe,
100
/// then half-close the peer before returning the client fd. TELNET's transfer
101
/// function does not yield to the outer multi loop while it polls, so deferred
102
/// delivery would turn an ordinary mutation into a timeout. WriteAll uses a
103
/// non-blocking server fd; if a compatibility input exceeds the socket buffer,
104
/// the retained prefix is still useful and the half-close still guarantees
105
/// termination.
106
/// @param purpose Socket role requested by curl; TELNET uses one outbound
107
///                stream and does not need role-specific setup.
108
/// @param address Intended destination retained by curl; the already-connected
109
///                socketpair does not need to rewrite it.
110
/// @return The preloaded client socket, or CURL_SOCKET_BAD on setup failure.
111
4.14k
curl_socket_t TelnetMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
112
4.14k
  (void)purpose;
113
4.14k
  (void)address;
114
4.14k
  if (scenario_ == nullptr || socket_opened_) {
115
0
    return CURL_SOCKET_BAD;
116
0
  }
117
4.14k
  socket_opened_ = true;
118
119
4.14k
  connection_ = std::make_unique<MockConnection>();
120
4.14k
  if (!connection_->ok()) {
121
0
    connection_.reset();
122
0
    return CURL_SOCKET_BAD;
123
0
  }
124
125
4.14k
  if (!connection_->EnsureClientSendBufferSize(kMinClientSendBufferBytes)) {
126
0
    connection_.reset();
127
0
    return CURL_SOCKET_BAD;
128
0
  }
129
130
5.67k
  const auto write_bytes = [this](const std::string& bytes) {
131
5.67k
    return bytes.empty() || connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size());
132
5.67k
  };
133
134
  // Compatibility corpus entries bypass the LPM postprocessor. Copy only the
135
  // raw prefix TELNET can consume, then normalize it so direct replay gets the
136
  // same byte/control budgets without traversing HTTP/WS-only fields.
137
4.14k
  const auto& source = scenario_->connection();
138
4.14k
  curl::fuzzer::proto::Connection script;
139
4.14k
  CopyTelnetResponsePrefix(source, &script);
140
4.14k
  BoundTelnetResponse(&script);
141
4.14k
  bool complete = write_bytes(script.initial_response());
142
4.14k
  const std::size_t chunk_count = static_cast<std::size_t>(script.on_readable_size());
143
5.67k
  for (std::size_t index = 0; complete && index < chunk_count; ++index) {
144
1.53k
    complete = write_bytes(script.on_readable(static_cast<int>(index)));
145
1.53k
  }
146
4.14k
  (void)complete;
147
148
  // Do not apply BackpressureConfig here. curl's TELNET driver sends from
149
  // inside its blocking protocol loop; tightening the socket would invalidate
150
  // the minimum-capacity invariant used by the pre-read drain.
151
4.14k
  connection_->ShutdownWrite();
152
4.14k
  return connection_->take_client_fd();
153
4.14k
}
154
155
/// Drive a small number of non-waiting multi transitions. The first call that
156
/// enters telnet_do() sees a preloaded, half-closed peer and therefore returns
157
/// without relying on the outer loop to deliver bytes. Subsequent calls only
158
/// settle curl's completion bookkeeping.
159
/// @param multi Multi handle whose sole easy handle runs the TELNET transfer.
160
/// @param easy Easy handle already installed on this mock.
161
/// @param scenario Bounded script to preload when curl opens its socket.
162
5.38k
void TelnetMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
163
5.38k
  (void)easy;
164
5.38k
  scenario_ = &scenario;
165
5.38k
  socket_opened_ = false;
166
5.38k
  connection_.reset();
167
168
5.38k
  int still_running = 1;
169
12.3k
  for (int iteration = 0; still_running && iteration < kMaxIdleIterations; ++iteration) {
170
6.97k
    const CURLMcode result = curl_multi_perform(multi, &still_running);
171
6.97k
    if (result != CURLM_OK) {
172
0
      break;
173
0
    }
174
6.97k
    if (connection_) {
175
4.14k
      (void)connection_->DrainIncoming();
176
4.14k
    }
177
6.97k
  }
178
179
5.38k
  scenario_ = nullptr;
180
5.38k
}
181
182
}  // namespace proto_fuzzer