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/dict_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 command-aware in-process DICT peer.
9
10
#include "proto_fuzzer/dict_mock_server.h"
11
12
#include <cstdlib>
13
#include <string>
14
#include <string_view>
15
16
#include "proto_fuzzer/mock_server.h"
17
#include "proto_fuzzer/scenario_limits.h"
18
19
namespace proto_fuzzer {
20
21
namespace {
22
23
// curl builds and sends the complete DICT request synchronously before the
24
// outer peer loop can drain it. RFC 2229 escaping can double every retained
25
// path byte; fixed command/version text fits comfortably in the remaining
26
// headroom. Rejecting a surprising platform socket configuration is safer
27
// than entering curl's blocking POLLOUT wait without an operation deadline.
28
constexpr std::size_t kMinClientSendBufferBytes = 64 * 1024;
29
constexpr std::size_t kMaxBufferedClientBytes = 2 * scenario_limits::kMaxDictHostPathBytes + 4 * 1024;
30
static_assert(kMaxBufferedClientBytes < kMinClientSendBufferBytes,
31
              "DICT mutation cap exceeds the verified client send buffer");
32
33
/// Temporarily replace one process environment value and restore the caller's
34
/// exact state on scope exit. libFuzzer drives inputs serially, so the scope
35
/// around one synchronous DICT drive also scopes curl's debug socket setting
36
/// to the connection-filter construction that consumes it.
37
class ScopedEnvironmentOverride {
38
 public:
39
3.06k
  ScopedEnvironmentOverride(const char* name, const char* value) : name_(name) {
40
3.06k
    const char* previous = std::getenv(name);
41
3.06k
    if (previous != nullptr) {
42
0
      had_previous_ = true;
43
0
      previous_ = previous;
44
0
    }
45
3.06k
    active_ = setenv(name, value, 1) == 0;
46
3.06k
  }
47
48
3.06k
  ~ScopedEnvironmentOverride() {
49
3.06k
    if (!active_) {
50
0
      return;
51
0
    }
52
3.06k
    if (had_previous_) {
53
0
      (void)setenv(name_.c_str(), previous_.c_str(), 1);
54
3.06k
    } else {
55
3.06k
      (void)unsetenv(name_.c_str());
56
3.06k
    }
57
3.06k
  }
58
59
  ScopedEnvironmentOverride(const ScopedEnvironmentOverride&) = delete;
60
  ScopedEnvironmentOverride& operator=(const ScopedEnvironmentOverride&) = delete;
61
62
 private:
63
  std::string name_;
64
  std::string previous_;
65
  bool had_previous_ = false;
66
  bool active_ = false;
67
};
68
69
}  // namespace
70
71
/// Begin without a borrowed protobuf or open socketpair. RunLoop borrows a
72
/// Scenario only for the synchronous DriveScenario call in which socket
73
/// callbacks can run.
74
3.21k
DictMockServer::DictMockServer() : reply_sent_(false), write_closed_(false) {}
75
76
/// Release the server-owned socketpair half after curl has returned its client
77
/// descriptor. The out-of-line destructor keeps MockConnection's complete type
78
/// local to this translation unit.
79
3.21k
DictMockServer::~DictMockServer() = default;
80
81
/// Remove the operation deadline so a partial DICT request write exercises
82
/// sendf()'s explicit no-timeout fallback. The bounded drive and writable
83
/// socketpair keep this path independent of wall-clock timing.
84
/// @param easy DICT easy handle configured before the connection is opened.
85
3.17k
void DictMockServer::Install(CURL* easy) {
86
3.17k
  MockServerBase::Install(easy);
87
3.17k
  (void)curl_easy_setopt(easy, CURLOPT_TIMEOUT_MS, 0L);
88
3.17k
}
89
90
/// Construct the ordinary plaintext transport only when its client endpoint
91
/// can queue the complete bounded DICT command. sendf() owns the thread until
92
/// that command is written, so the outer peer cannot create space meanwhile.
93
/// @return A verified socketpair, or nullptr on a platform that cannot provide
94
///         the required capacity.
95
2.54k
std::unique_ptr<MockConnection> DictMockServer::CreateConnection() {
96
2.54k
  std::unique_ptr<MockConnection> connection = PingpongMockServer::CreateConnection();
97
2.54k
  if (connection == nullptr || !connection->ok() ||
98
2.54k
      !connection->EnsureClientSendBufferSize(kMinClientSendBufferBytes)) {
99
0
    return nullptr;
100
0
  }
101
2.54k
  return connection;
102
2.54k
}
103
104
/// Split each sufficiently large socket write while curl constructs and uses
105
/// this DICT connection. curl's debug socket filter snapshots the environment
106
/// during its first perform; restoring it before DriveScenario tears down the
107
/// multi does not change that connection, and prevents a DICT input from
108
/// perturbing later non-DICT inputs in a mixed process.
109
/// @param multi Multi handle containing the DICT easy handle.
110
/// @param easy Easy handle already installed on this mock.
111
/// @param scenario DICT script borrowed for the synchronous drive.
112
3.06k
void DictMockServer::RunLoop(CURLM* multi, CURL* easy, const curl::fuzzer::proto::Scenario& scenario) {
113
3.06k
  ScopedEnvironmentOverride partial_writes("CURL_DBG_SOCK_WPARTIAL", "50");
114
3.06k
  PingpongMockServer::RunLoop(multi, easy, scenario);
115
3.06k
}
116
117
/// Reset DICT's response bookkeeping for a new drive. The shared base has
118
/// already cleared the transport, reply cursor, transcript, upload capture,
119
/// and any in-flight body phase.
120
3.06k
void DictMockServer::ResetProtocolState() {
121
3.06k
  reply_sent_ = false;
122
3.06k
  write_closed_ = false;
123
3.06k
}
124
125
/// Queue end-of-response and half-close exactly once per drive.
126
3.79k
void DictMockServer::FinishResponse() {
127
3.79k
  if (write_closed_) {
128
1.33k
    return;
129
1.33k
  }
130
2.46k
  write_closed_ = true;
131
2.46k
  if (connection_ != nullptr) {
132
2.46k
    connection_->ShutdownWrite();
133
2.46k
  }
134
2.46k
}
135
136
/// Answer the first lookup command in the request and ignore CLIENT, which is
137
/// a bare identification line. curl normally writes CLIENT, the lookup, and
138
/// QUIT together and then waits for the response body; QUIT is therefore only
139
/// answered when it arrives without a preceding lookup command (for example a
140
/// URL with no path).
141
/// @param line One newline-terminated command from curl.
142
7.39k
void DictMockServer::HandleLine(std::string_view line) {
143
7.39k
  const std::string_view verb = FirstToken(line);
144
7.39k
  if (verb.empty()) {
145
1.12k
    return;
146
1.12k
  }
147
148
6.26k
  if (TokenEquals(verb, "CLIENT")) {
149
2.46k
    return;
150
2.46k
  }
151
152
3.79k
  if (reply_sent_ || TokenEquals(verb, "QUIT")) {
153
2.46k
    if (!reply_sent_) {
154
1.12k
      (void)WriteNextReply();
155
1.12k
      reply_sent_ = true;
156
1.12k
    }
157
2.46k
    FinishResponse();
158
2.46k
    return;
159
2.46k
  }
160
161
  // The first lookup command consumes the scripted body. A script without a
162
  // reply still closes the response so curl cannot wait for a body forever.
163
1.33k
  (void)WriteNextReply();
164
1.33k
  reply_sent_ = true;
165
1.33k
  FinishResponse();
166
1.33k
}
167
168
}  // namespace proto_fuzzer