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/scenario_runner.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 ScenarioRunner::Run.
9
10
#include "proto_fuzzer/scenario_runner.h"
11
12
#include <curl/curl.h>
13
#include <curl/header.h>
14
15
#include <cstddef>
16
#include <memory>
17
#include <string>
18
19
#include "proto_fuzzer/api_lifecycle.h"
20
#include "proto_fuzzer/ftp_mock_server.h"
21
#include "proto_fuzzer/mock_server.h"
22
#include "proto_fuzzer/mock_server_base.h"
23
#include "proto_fuzzer/option_apply.h"
24
#include "proto_fuzzer/request_data.h"
25
#include "proto_fuzzer/telnet_mock_server.h"
26
#include "proto_fuzzer/tftp_mock_server.h"
27
#include "proto_fuzzer/websocket_mock_server.h"
28
29
#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
30
#include "proto_fuzzer/tls_mock_server.h"
31
#endif
32
33
namespace proto_fuzzer {
34
35
namespace {
36
37
/// @brief RAII wrapper for CURL* easy handles.
38
struct CurlEasyDeleter {
39
38.0k
  void operator()(CURL* h) const noexcept {
40
38.0k
    if (h) curl_easy_cleanup(h);
41
38.0k
  }
42
};
43
using CurlEasyPtr = std::unique_ptr<CURL, CurlEasyDeleter>;
44
45
/// @brief RAII wrapper for caller-owned curl_slist option data.
46
struct CurlSlistDeleter {
47
38.0k
  void operator()(curl_slist* list) const noexcept { curl_slist_free_all(list); }
48
};
49
using CurlSlistPtr = std::unique_ptr<curl_slist, CurlSlistDeleter>;
50
51
constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO;
52
constexpr std::size_t kMaxResultHeaders = 16;
53
54
/// Probe each public getinfo return family and the response-header API after
55
/// curl has settled the transfer. Applications commonly inspect these APIs,
56
/// but a harness that only drives I/O leaves their type dispatch and
57
/// post-transfer state unexecuted even when the corresponding parser ran.
58
/// The chosen values are handle-owned or scalar: notably CERTINFO exercises
59
/// the pointer/slist dispatch family without materialising a separately-owned
60
/// cookie/engine list. Header iteration is capped independently of response
61
/// size so this unconditional coverage cannot dominate a fuzz iteration.
62
31.0k
void ProbeTransferResults(CURL* easy) {
63
31.0k
  char* string_result = nullptr;
64
31.0k
  long long_result = 0;
65
31.0k
  double double_result = 0;
66
31.0k
  curl_off_t offset_result = 0;
67
31.0k
  curl_socket_t socket_result = CURL_SOCKET_BAD;
68
31.0k
  struct curl_certinfo* certinfo_result = nullptr;
69
70
31.0k
  (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result);
71
31.0k
  (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result);
72
31.0k
  (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result);
73
31.0k
  (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result);
74
31.0k
  (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result);
75
31.0k
  (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result);
76
77
31.0k
  struct curl_header* header = nullptr;
78
31.0k
  (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
79
31.0k
  header = nullptr;
80
65.2k
  for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
81
64.6k
    header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header);
82
64.6k
    if (header == nullptr) {
83
30.4k
      break;
84
30.4k
    }
85
64.6k
  }
86
31.0k
}
87
88
/// Map a Scheme enum to the URL scheme literal.
89
38.3k
const char* SchemePrefix(curl::fuzzer::proto::Scheme scheme) {
90
38.3k
  switch (scheme) {
91
24.2k
    case curl::fuzzer::proto::SCHEME_HTTP:
92
24.2k
      return "http";
93
4.00k
    case curl::fuzzer::proto::SCHEME_HTTPS:
94
4.00k
      return "https";
95
3.21k
    case curl::fuzzer::proto::SCHEME_WS:
96
3.21k
      return "ws";
97
3.79k
    case curl::fuzzer::proto::SCHEME_WSS:
98
3.79k
      return "wss";
99
3.06k
    case curl::fuzzer::proto::SCHEME_TELNET:
100
3.06k
      return "telnet";
101
0
    case curl::fuzzer::proto::SCHEME_FTP:
102
0
      return "ftp";
103
0
    case curl::fuzzer::proto::SCHEME_TFTP:
104
0
      return "tftp";
105
50
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
106
50
    default:
107
50
      return nullptr;
108
38.3k
  }
109
38.3k
}
110
111
/// Pick the peer implementation authorized by both protocol and target mode.
112
/// The compatibility target must keep treating HTTPS response bytes as raw TLS
113
/// records, while the dedicated HTTPS lane interprets them as decrypted HTTP.
114
/// Keeping that semantic boundary in the closed run-mode enum prevents a new
115
/// protobuf field from silently changing old OSS-Fuzz reproducers.
116
std::unique_ptr<MockServerBase> MakeMockServerForScenario(const curl::fuzzer::proto::Scenario& scenario,
117
38.0k
                                                          ScenarioRunMode mode) {
118
38.0k
  switch (scenario.scheme()) {
119
24.1k
    case curl::fuzzer::proto::SCHEME_HTTP:
120
24.1k
      return std::make_unique<MockServer>();
121
4.00k
    case curl::fuzzer::proto::SCHEME_HTTPS:
122
4.00k
#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
123
4.00k
      if (mode == ScenarioRunMode::kTlsCoverage) {
124
3.73k
        return std::make_unique<TlsMockServer>();
125
3.73k
      }
126
#else
127
      (void)mode;
128
#endif
129
267
      return std::make_unique<MockServer>();
130
3.18k
    case curl::fuzzer::proto::SCHEME_WS:
131
6.89k
    case curl::fuzzer::proto::SCHEME_WSS:
132
6.89k
      return std::make_unique<WebSocketMockServer>();
133
3.02k
    case curl::fuzzer::proto::SCHEME_TELNET:
134
3.02k
      return std::make_unique<TelnetMockServer>();
135
0
    case curl::fuzzer::proto::SCHEME_FTP:
136
      // New numeric enum values may already occur in the historical mixed
137
      // corpus as unknown fields. Only the fixed FTP profile may reinterpret
138
      // one as a live two-channel protocol exchange.
139
0
      if (mode == ScenarioRunMode::kFtpCoverage) {
140
0
        return std::make_unique<FtpMockServer>();
141
0
      }
142
0
      return nullptr;
143
0
    case curl::fuzzer::proto::SCHEME_TFTP:
144
      // TFTP changes the callback transport from a preconnected stream to a
145
      // real UDP endpoint, so compatibility inputs must not opt into it merely
146
      // because this build learned a new enum value.
147
0
      if (mode == ScenarioRunMode::kTftpCoverage) {
148
0
        return std::make_unique<TftpMockServer>();
149
0
      }
150
0
      return nullptr;
151
0
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
152
0
    default:
153
0
      return nullptr;
154
38.0k
  }
155
38.0k
}
156
157
}  // namespace
158
159
/// @class proto_fuzzer::ScenarioRunner
160
/// @brief Executes one Scenario end-to-end: applies options, picks a mock
161
///        server for the scheme, and hands off to the mock's DriveScenario.
162
///        Instances are cheap; create one per fuzz case so per-scenario state
163
///        is torn down cleanly.
164
165
/// Default-construct an empty runner. All state is set up inside Run().
166
38.3k
ScenarioRunner::ScenarioRunner() = default;
167
168
/// Default destructor; per-run state is local to Run() so nothing to tear
169
/// down at instance scope.
170
38.3k
ScenarioRunner::~ScenarioRunner() = default;
171
172
/// Implement the bounded orchestration contract documented on Run's public
173
/// declaration; keeping argument docs there avoids two drifting descriptions.
174
38.3k
int ScenarioRunner::Run(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) {
175
38.3k
  const char* prefix = SchemePrefix(scenario.scheme());
176
38.3k
  if (prefix == nullptr || scenario.host_path().empty()) {
177
308
    return 0;
178
308
  }
179
180
38.0k
  std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode);
181
38.0k
  if (!mock) {
182
0
    return 0;
183
0
  }
184
185
  // Declaration order is an ownership invariant: reverse destruction keeps
186
  // CONNECT_TO storage and share callback userdata alive through easy cleanup.
187
  // This matters for incomplete transfers, where an explicit share detach can
188
  // be rejected while easy cleanup can still release the reference safely.
189
38.0k
  std::unique_ptr<ApiLifecycle> api_lifecycle;
190
38.0k
  CurlSlistPtr connect_to;
191
38.0k
  CurlEasyPtr easy(curl_easy_init());
192
38.0k
  if (!easy) {
193
0
    return 0;
194
0
  }
195
196
38.0k
  std::string url = std::string(prefix) + "://" + scenario.host_path();
197
38.0k
  const auto configure_easy = [&] {
198
38.0k
    connect_to.reset(ApplyBaselineOptions(easy.get(), scenario.scheme()));
199
38.0k
    curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str());
200
38.0k
    mock->Install(easy.get());
201
202
    // Compatibility inputs deliberately bypass the mutating postprocessor,
203
    // so enforce the shared option prefix again at the runtime boundary. The
204
    // helper still ignores individual CURLcodes: the fuzzer stresses curl
205
    // rather than treating rejected combinations as harness failures.
206
38.0k
    (void)ApplyScenarioOptions(easy.get(), scenario);
207
38.0k
  };
208
38.0k
  configure_easy();
209
210
38.0k
  const curl::fuzzer::proto::ApiPlan* api_plan =
211
38.0k
      mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr;
212
38.0k
  if (api_plan != nullptr && api_plan->reset_easy()) {
213
    // Reset deliberately drops every pointer-valued option before its backing
214
    // list is freed. Reapplying the exact scenario then lets the transfer
215
    // populate post-reset state instead of turning reset coverage into a
216
    // guaranteed malformed request.
217
0
    curl_easy_reset(easy.get());
218
0
    connect_to.reset();
219
0
    configure_easy();
220
0
  }
221
222
38.0k
  if (api_plan != nullptr) {
223
0
    api_lifecycle = std::make_unique<ApiLifecycle>(easy.get(), *api_plan, url);
224
0
  }
225
226
38.0k
  {
227
    // HTTP headers, MIME bodies, TELNET options, and callback userdata are
228
    // pointer-valued state that libcurl does not copy. Keep their owner around
229
    // the entire multi-handle drive, then let it detach them while `easy` is
230
    // still valid. This inner scope is deliberate: easy.reset() below must
231
    // never run before the owner's destructor clears those options.
232
38.0k
    ScenarioRequestData request_data(easy.get(), scenario);
233
38.0k
    mock->ConfigureRequestData(&request_data);
234
38.0k
    const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode();
235
38.0k
    if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) {
236
0
      mock->DriveEasyScenario(easy.get(), scenario);
237
38.0k
    } else {
238
38.0k
      mock->DriveScenario(easy.get(), scenario, drive_mode == curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET,
239
38.0k
                          api_plan != nullptr && api_plan->wake_multi());
240
38.0k
    }
241
38.0k
    if (api_lifecycle != nullptr) {
242
0
      api_lifecycle->ProbeTransferResults(drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM);
243
0
      api_lifecycle->ProbeEasyDuplication();
244
38.0k
    } else if (mode != ScenarioRunMode::kFastProtocol) {
245
31.0k
      ProbeTransferResults(easy.get());
246
31.0k
    }
247
38.0k
  }
248
249
  // Easy cleanup is the reliable share-detach boundary even if the bounded
250
  // drive stopped with a connection attached. The lifecycle object—and thus
251
  // lock callback userdata—outlives it, then releases share-owned caches.
252
38.0k
  easy.reset();
253
38.0k
  connect_to.reset();
254
38.0k
  api_lifecycle.reset();
255
38.0k
  return 0;
256
38.0k
}
257
258
}  // namespace proto_fuzzer