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/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
2.45k
                              curl::fuzzer::proto::Connection* destination) {
50
2.45k
  std::size_t bytes_left = scenario_limits::kMaxTelnetResponseBytes;
51
4.41k
  const auto copy_fragment = [&bytes_left](const std::string& fragment, std::string* output) {
52
4.41k
    const std::size_t retained = std::min(fragment.size(), bytes_left);
53
4.41k
    output->assign(fragment.data(), retained);
54
4.41k
    bytes_left -= retained;
55
4.41k
    return retained == fragment.size();
56
4.41k
  };
57
58
2.45k
  if (!copy_fragment(source.initial_response(), destination->mutable_initial_response())) {
59
4
    return;
60
4
  }
61
62
2.45k
  const std::size_t chunk_count =
63
2.45k
      std::min<std::size_t>(scenario_limits::kMaxTelnetResponseChunks, source.on_readable_size());
64
4.40k
  for (std::size_t index = 0; index < chunk_count && bytes_left != 0; ++index) {
65
1.95k
    if (!copy_fragment(source.on_readable(static_cast<int>(index)), destination->add_on_readable())) {
66
6
      break;
67
6
    }
68
1.95k
  }
69
2.45k
}
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
3.02k
TelnetMockServer::TelnetMockServer() : scenario_(nullptr), socket_opened_(false) {}
76
77
/// The base owns and closes the server half after curl releases its client fd.
78
3.02k
TelnetMockServer::~TelnetMockServer() = default;
79
80
3.02k
void TelnetMockServer::ConfigureRequestData(ScenarioRequestData* request_data) {
81
3.02k
  if (request_data != nullptr) {
82
3.02k
    request_data->SetBeforeUploadReadCallback(&DrainBeforeUploadRead, this);
83
3.02k
  }
84
3.02k
}
85
86
1.29k
void TelnetMockServer::DrainBeforeUploadRead(void* userdata) {
87
1.29k
  auto* server = static_cast<TelnetMockServer*>(userdata);
88
1.29k
  if (server != nullptr && server->connection_) {
89
1.29k
    (void)server->connection_->DrainIncoming();
90
1.29k
  }
91
1.29k
}
92
93
/// Create one socketpair, preload every raw response fragment curl can observe,
94
/// then half-close the peer before returning the client fd. TELNET's transfer
95
/// function does not yield to the outer multi loop while it polls, so deferred
96
/// delivery would turn an ordinary mutation into a timeout. WriteAll uses a
97
/// non-blocking server fd; if a compatibility input exceeds the socket buffer,
98
/// the retained prefix is still useful and the half-close still guarantees
99
/// termination.
100
/// @param purpose Socket role requested by curl; TELNET uses one outbound
101
///                stream and does not need role-specific setup.
102
/// @param address Intended destination retained by curl; the already-connected
103
///                socketpair does not need to rewrite it.
104
/// @return The preloaded client socket, or CURL_SOCKET_BAD on setup failure.
105
2.45k
curl_socket_t TelnetMockServer::HandleOpenSocket(curlsocktype purpose, struct curl_sockaddr* address) {
106
2.45k
  (void)purpose;
107
2.45k
  (void)address;
108
2.45k
  if (scenario_ == nullptr || socket_opened_) {
109
0
    return CURL_SOCKET_BAD;
110
0
  }
111
2.45k
  socket_opened_ = true;
112
113
2.45k
  connection_ = std::make_unique<MockConnection>();
114
2.45k
  if (!connection_->ok()) {
115
0
    connection_.reset();
116
0
    return CURL_SOCKET_BAD;
117
0
  }
118
119
2.45k
  if (!connection_->EnsureClientSendBufferSize(kMinClientSendBufferBytes)) {
120
0
    connection_.reset();
121
0
    return CURL_SOCKET_BAD;
122
0
  }
123
124
4.33k
  const auto write_bytes = [this](const std::string& bytes) {
125
4.33k
    return bytes.empty() || connection_->WriteAll(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size());
126
4.33k
  };
127
128
  // Compatibility corpus entries bypass the LPM postprocessor. Copy only the
129
  // raw prefix TELNET can consume, then normalize it so direct replay gets the
130
  // same byte/control budgets without traversing HTTP/WS-only fields.
131
2.45k
  const auto& source = scenario_->connection();
132
2.45k
  curl::fuzzer::proto::Connection script;
133
2.45k
  CopyTelnetResponsePrefix(source, &script);
134
2.45k
  BoundTelnetResponse(&script);
135
2.45k
  bool complete = write_bytes(script.initial_response());
136
2.45k
  const std::size_t chunk_count = static_cast<std::size_t>(script.on_readable_size());
137
4.33k
  for (std::size_t index = 0; complete && index < chunk_count; ++index) {
138
1.87k
    complete = write_bytes(script.on_readable(static_cast<int>(index)));
139
1.87k
  }
140
2.45k
  (void)complete;
141
142
  // Do not apply BackpressureConfig here. curl's TELNET driver sends from
143
  // inside its blocking protocol loop; tightening the socket would invalidate
144
  // the minimum-capacity invariant used by the pre-read drain.
145
2.45k
  connection_->ShutdownWrite();
146
2.45k
  return connection_->take_client_fd();
147
2.45k
}
148
149
/// Drive a small number of non-waiting multi transitions. The first call that
150
/// enters telnet_do() sees a preloaded, half-closed peer and therefore returns
151
/// without relying on the outer loop to deliver bytes. Subsequent calls only
152
/// settle curl's completion bookkeeping.
153
/// @param multi Multi handle whose sole easy handle runs the TELNET transfer.
154
/// @param easy Easy handle already installed on this mock.
155
/// @param scenario Bounded script to preload when curl opens its socket.
156
3.02k
void TelnetMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
157
3.02k
  (void)easy;
158
3.02k
  scenario_ = &scenario;
159
3.02k
  socket_opened_ = false;
160
3.02k
  connection_.reset();
161
162
3.02k
  int still_running = 1;
163
6.05k
  for (int iteration = 0; still_running && iteration < kMaxIdleIterations; ++iteration) {
164
3.02k
    const CURLMcode result = curl_multi_perform(multi, &still_running);
165
3.02k
    if (result != CURLM_OK) {
166
0
      break;
167
0
    }
168
3.02k
    if (connection_) {
169
2.45k
      (void)connection_->DrainIncoming();
170
2.45k
    }
171
3.02k
  }
172
173
3.02k
  scenario_ = nullptr;
174
3.02k
}
175
176
}  // namespace proto_fuzzer