Coverage Report

Created: 2026-09-04 07:25

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/curl_raii.h"
21
#include "proto_fuzzer/ftp_mock_server.h"
22
#include "proto_fuzzer/mock_server.h"
23
#include "proto_fuzzer/mock_server_base.h"
24
#include "proto_fuzzer/multi_transfer_runner.h"
25
#include "proto_fuzzer/option_apply.h"
26
#include "proto_fuzzer/request_data.h"
27
#include "proto_fuzzer/telnet_mock_server.h"
28
#include "proto_fuzzer/tftp_mock_server.h"
29
#include "proto_fuzzer/websocket_mock_server.h"
30
31
#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
32
#include "proto_fuzzer/h2_proxy_mock_server.h"
33
#include "proto_fuzzer/tls_mock_server.h"
34
#endif
35
36
namespace proto_fuzzer {
37
38
namespace {
39
40
constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO;
41
constexpr std::size_t kMaxResultHeaders = 16;
42
43
/// Probe each public getinfo return family and the response-header API after
44
/// curl has settled the transfer. Applications commonly inspect these APIs,
45
/// but a harness that only drives I/O leaves their type dispatch and
46
/// post-transfer state unexecuted even when the corresponding parser ran.
47
/// The chosen values are handle-owned or scalar: notably CERTINFO exercises
48
/// the pointer/slist dispatch family without materialising a separately-owned
49
/// cookie/engine list. Header iteration is capped independently of response
50
/// size so this unconditional coverage cannot dominate a fuzz iteration.
51
112k
void ProbeTransferResults(CURL* easy) {
52
112k
  char* string_result = nullptr;
53
112k
  long long_result = 0;
54
112k
  double double_result = 0;
55
112k
  curl_off_t offset_result = 0;
56
112k
  curl_socket_t socket_result = CURL_SOCKET_BAD;
57
112k
  struct curl_certinfo* certinfo_result = nullptr;
58
59
112k
  (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result);
60
112k
  (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result);
61
112k
  (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result);
62
112k
  (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result);
63
112k
  (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result);
64
112k
  (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result);
65
66
112k
  struct curl_header* header = nullptr;
67
112k
  (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
68
112k
  header = nullptr;
69
224k
  for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
70
222k
    header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header);
71
222k
    if (header == nullptr) {
72
110k
      break;
73
110k
    }
74
222k
  }
75
112k
}
76
77
/// Map a Scheme enum to the URL scheme literal.
78
136k
const char* SchemePrefix(curl::fuzzer::proto::Scheme scheme) {
79
136k
  switch (scheme) {
80
78.0k
    case curl::fuzzer::proto::SCHEME_HTTP:
81
78.0k
      return "http";
82
18.7k
    case curl::fuzzer::proto::SCHEME_HTTPS:
83
18.7k
      return "https";
84
16.3k
    case curl::fuzzer::proto::SCHEME_WS:
85
16.3k
      return "ws";
86
9.45k
    case curl::fuzzer::proto::SCHEME_WSS:
87
9.45k
      return "wss";
88
5.08k
    case curl::fuzzer::proto::SCHEME_TELNET:
89
5.08k
      return "telnet";
90
7.19k
    case curl::fuzzer::proto::SCHEME_FTP:
91
7.19k
      return "ftp";
92
2.01k
    case curl::fuzzer::proto::SCHEME_TFTP:
93
2.01k
      return "tftp";
94
60
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
95
60
    default:
96
60
      return nullptr;
97
136k
  }
98
136k
}
99
100
/// Pick the peer implementation authorized by both protocol and target mode.
101
/// The compatibility target must keep treating HTTPS response bytes as raw TLS
102
/// records, while the dedicated HTTPS lane interprets them as decrypted HTTP.
103
/// Keeping that semantic boundary in the closed run-mode enum prevents a new
104
/// protobuf field from silently changing old OSS-Fuzz reproducers.
105
std::unique_ptr<MockServerBase> MakeMockServerForScenario(const curl::fuzzer::proto::Scenario& scenario,
106
135k
                                                          ScenarioRunMode mode) {
107
135k
  if (mode == ScenarioRunMode::kH2ProxyCoverage) {
108
5.54k
#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
109
5.54k
    return std::make_unique<H2ProxyMockServer>();
110
#else
111
    // MemorySanitizer builds deliberately omit OpenSSL. Keep the target
112
    // binary available to OSS-Fuzz, but do not pretend a plaintext mock can
113
    // negotiate the ALPN gate required to enter cf-h2-proxy.
114
    return nullptr;
115
#endif
116
5.54k
  }
117
118
130k
  switch (scenario.scheme()) {
119
71.9k
    case curl::fuzzer::proto::SCHEME_HTTP:
120
71.9k
      return std::make_unique<MockServer>();
121
18.7k
    case curl::fuzzer::proto::SCHEME_HTTPS:
122
18.7k
#if defined(PROTO_FUZZER_HAS_TLS_MOCK_SERVER)
123
18.7k
      if (mode == ScenarioRunMode::kTlsCoverage) {
124
17.8k
        return std::make_unique<TlsMockServer>(scenario.tls_certificate_chain());
125
17.8k
      }
126
#else
127
      (void)mode;
128
#endif
129
961
      return std::make_unique<MockServer>();
130
16.0k
    case curl::fuzzer::proto::SCHEME_WS:
131
25.1k
    case curl::fuzzer::proto::SCHEME_WSS:
132
25.1k
      return std::make_unique<WebSocketMockServer>();
133
4.87k
    case curl::fuzzer::proto::SCHEME_TELNET:
134
4.87k
      return std::make_unique<TelnetMockServer>();
135
7.19k
    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
7.19k
      if (mode == ScenarioRunMode::kFtpCoverage) {
140
7.19k
        return std::make_unique<FtpMockServer>();
141
7.19k
      }
142
2
      return nullptr;
143
2.01k
    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
2.01k
      if (mode == ScenarioRunMode::kTftpCoverage) {
148
2.01k
        return std::make_unique<TftpMockServer>();
149
2.01k
      }
150
1
      return nullptr;
151
0
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
152
0
    default:
153
0
      return nullptr;
154
130k
  }
155
130k
}
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
137k
ScenarioRunner::ScenarioRunner() = default;
167
168
/// Default destructor; per-run state is local to Run() so nothing to tear
169
/// down at instance scope.
170
137k
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
137k
int ScenarioRunner::Run(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) {
175
137k
  if (mode == ScenarioRunMode::kMultiTransfer) {
176
502
    (void)MultiTransferRunner().Run(scenario);
177
502
    return 0;
178
502
  }
179
180
136k
  const char* prefix = SchemePrefix(scenario.scheme());
181
136k
  if (prefix == nullptr || scenario.host_path().empty()) {
182
1.44k
    return 0;
183
1.44k
  }
184
185
135k
  std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario, mode);
186
135k
  if (!mock) {
187
3
    return 0;
188
3
  }
189
190
  // Declaration order is an ownership invariant: reverse destruction keeps
191
  // CONNECT_TO storage and share callback userdata alive through easy cleanup.
192
  // This matters for incomplete transfers, where an explicit share detach can
193
  // be rejected while easy cleanup can still release the reference safely.
194
135k
  std::unique_ptr<ApiLifecycle> api_lifecycle;
195
135k
  CurlSlistPtr connect_to;
196
135k
  CurlEasyPtr easy(curl_easy_init());
197
135k
  if (!easy) {
198
0
    return 0;
199
0
  }
200
201
135k
  std::string url = std::string(prefix) + "://" + scenario.host_path();
202
137k
  const auto configure_easy = [&] {
203
137k
    connect_to.reset(ApplyBaselineOptions(easy.get(), scenario.scheme()));
204
137k
    curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str());
205
137k
    mock->Install(easy.get());
206
207
    // Compatibility inputs deliberately bypass the mutating postprocessor,
208
    // so enforce the shared option prefix again at the runtime boundary. The
209
    // helper still ignores individual CURLcodes: the fuzzer stresses curl
210
    // rather than treating rejected combinations as harness failures.
211
137k
    (void)ApplyScenarioOptions(easy.get(), scenario);
212
137k
  };
213
135k
  configure_easy();
214
215
135k
  const curl::fuzzer::proto::ApiPlan* api_plan =
216
135k
      mode == ScenarioRunMode::kApiLifecycle && scenario.has_api_plan() ? &scenario.api_plan() : nullptr;
217
135k
  if (api_plan != nullptr && api_plan->reset_easy()) {
218
    // Reset deliberately drops every pointer-valued option before its backing
219
    // list is freed. Reapplying the exact scenario then lets the transfer
220
    // populate post-reset state instead of turning reset coverage into a
221
    // guaranteed malformed request.
222
1.71k
    curl_easy_reset(easy.get());
223
1.71k
    connect_to.reset();
224
1.71k
    configure_easy();
225
1.71k
  }
226
227
135k
  if (api_plan != nullptr) {
228
4.82k
    api_lifecycle = std::make_unique<ApiLifecycle>(easy.get(), *api_plan, url);
229
4.82k
  }
230
231
135k
  {
232
    // HTTP headers, MIME bodies, TELNET options, and callback userdata are
233
    // pointer-valued state that libcurl does not copy. Keep their owner around
234
    // the entire multi-handle drive, then let it detach them while `easy` is
235
    // still valid. This inner scope is deliberate: easy.reset() below must
236
    // never run before the owner's destructor clears those options.
237
135k
    ScenarioRequestData request_data(easy.get(), scenario);
238
135k
    mock->ConfigureRequestData(&request_data);
239
135k
    const auto drive_mode = api_plan == nullptr ? curl::fuzzer::proto::API_DRIVE_MULTI_PERFORM : api_plan->drive_mode();
240
135k
    if (drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM) {
241
945
      mock->DriveEasyScenario(easy.get(), scenario);
242
134k
    } else {
243
134k
      mock->DriveScenario(easy.get(), scenario, drive_mode == curl::fuzzer::proto::API_DRIVE_MULTI_SOCKET,
244
134k
                          api_plan != nullptr && api_plan->wake_multi());
245
134k
    }
246
135k
    if (api_lifecycle != nullptr) {
247
4.82k
      api_lifecycle->ProbeTransferResults(drive_mode == curl::fuzzer::proto::API_DRIVE_EASY_PERFORM);
248
4.82k
      api_lifecycle->ProbeEasyDuplication();
249
130k
    } else if (mode != ScenarioRunMode::kFastProtocol) {
250
112k
      ProbeTransferResults(easy.get());
251
112k
    }
252
135k
  }
253
254
  // Easy cleanup is the reliable share-detach boundary even if the bounded
255
  // drive stopped with a connection attached. The lifecycle object—and thus
256
  // lock callback userdata—outlives it, then releases share-owned caches.
257
135k
  easy.reset();
258
135k
  connect_to.reset();
259
135k
  api_lifecycle.reset();
260
135k
  return 0;
261
135k
}
262
263
}  // namespace proto_fuzzer