Coverage Report

Created: 2026-10-28 07:17

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
3.82k
                              curl::fuzzer::proto::Connection* destination) {
50
3.82k
  std::size_t bytes_left = scenario_limits::kMaxTelnetResponseBytes;
51
5.54k
  const auto copy_fragment = [&bytes_left](const std::string& fragment, std::string* output) {
52
5.54k
    const std::size_t retained = std::min(fragment.size(), bytes_left);
53
5.54k
    output->assign(fragment.data(), retained);
54
5.54k
    bytes_left -= retained;
55
5.54k
    return retained == fragment.size();
56
5.54k
  };
57
58
3.82k
  if (!copy_fragment(source.initial_response(), destination->mutable_initial_response())) {
59
2
    return;
60
2
  }
61
62
3.82k
  const std::size_t chunk_count =
63
3.82k
      std::min<std::size_t>(scenario_limits::kMaxTelnetResponseChunks, source.on_readable_size());
64
5.54k
  for (std::size_t index = 0; index < chunk_count && bytes_left != 0; ++index) {
65
1.72k
    if (!copy_fragment(source.on_readable(static_cast<int>(index)), destination->add_on_readable())) {
66
2
      break;
67
2
    }
68
1.72k
  }
69
3.82k
}
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.05k
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.05k
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.05k
void TelnetMockServer::ConfigureRequestData(ScenarioRequestData* request_data) {
86
5.05k
  if (request_data != nullptr) {
87
5.05k
    request_data->SetBeforeUploadReadCallback(&DrainBeforeUploadRead, this);
88
5.05k
  }
89
5.05k
}
90
91
/// C-compatible UploadScriptState hook forwarding to DrainIncoming().
92
1.64k
void TelnetMockServer::DrainBeforeUploadRead(void* userdata) {
93
1.64k
  auto* server = static_cast<TelnetMockServer*>(userdata);
94
1.64k
  if (server != nullptr && server->connection_) {
95
1.64k
    (void)server->connection_->DrainIncoming();
96
1.64k
  }
97
1.64k
}
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
3.82k
curl_socket_t TelnetMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
112
3.82k
  (void)purpose;
113
3.82k
  (void)address;
114
3.82k
  if (scenario_ == nullptr || socket_opened_) {
115
0
    return CURL_SOCKET_BAD;
116
0
  }
117
3.82k
  socket_opened_ = true;
118
119
3.82k
  connection_ = std::make_unique<MockConnection>();
120
3.82k
  if (!connection_->ok()) {
121
0
    connection_.reset();
122
0
    return CURL_SOCKET_BAD;
123
0
  }
124
125
3.82k
  if (!connection_->EnsureClientSendBufferSize(kMinClientSendBufferBytes)) {
126
0
    connection_.reset();
127
0
    return CURL_SOCKET_BAD;
128
0
  }
129
130
5.49k
  const auto write_bytes = [this](const std::string& bytes) {
131
5.49k
    return bytes.empty() || connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size());
132
5.49k
  };
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
3.82k
  const auto& source = scenario_->connection();
138
3.82k
  curl::fuzzer::proto::Connection script;
139
3.82k
  CopyTelnetResponsePrefix(source, &script);
140
3.82k
  BoundTelnetResponse(&script);
141
3.82k
  bool complete = write_bytes(script.initial_response());
142
3.82k
  const std::size_t chunk_count = static_cast<std::size_t>(script.on_readable_size());
143
5.49k
  for (std::size_t index = 0; complete && index < chunk_count; ++index) {
144
1.67k
    complete = write_bytes(script.on_readable(static_cast<int>(index)));
145
1.67k
  }
146
3.82k
  (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
3.82k
  connection_->ShutdownWrite();
152
3.82k
  return connection_->take_client_fd();
153
3.82k
}
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
4.98k
void TelnetMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
163
4.98k
  (void)easy;
164
4.98k
  scenario_ = &scenario;
165
4.98k
  socket_opened_ = false;
166
4.98k
  connection_.reset();
167
168
4.98k
  int still_running = 1;
169
11.7k
  for (int iteration = 0; still_running && iteration < kMaxIdleIterations; ++iteration) {
170
6.80k
    const CURLMcode result = curl_multi_perform(multi, &still_running);
171
6.80k
    if (result != CURLM_OK) {
172
0
      break;
173
0
    }
174
6.80k
    if (connection_) {
175
3.82k
      (void)connection_->DrainIncoming();
176
3.82k
    }
177
6.80k
  }
178
179
4.98k
  scenario_ = nullptr;
180
4.98k
}
181
182
}  // namespace proto_fuzzer