Coverage Report

Created: 2026-08-31 06:49

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/mock_server.h"
20
#include "proto_fuzzer/mock_server_base.h"
21
#include "proto_fuzzer/option_apply.h"
22
#include "proto_fuzzer/request_data.h"
23
#include "proto_fuzzer/websocket_mock_server.h"
24
25
namespace proto_fuzzer {
26
27
namespace {
28
29
/// @brief RAII wrapper for CURL* easy handles.
30
struct CurlEasyDeleter {
31
10.1k
  void operator()(CURL* h) const noexcept {
32
10.1k
    if (h) curl_easy_cleanup(h);
33
10.1k
  }
34
};
35
using CurlEasyPtr = std::unique_ptr<CURL, CurlEasyDeleter>;
36
37
constexpr unsigned int kAllHeaderOrigins = CURLH_HEADER | CURLH_TRAILER | CURLH_CONNECT | CURLH_1XX | CURLH_PSEUDO;
38
constexpr std::size_t kMaxResultHeaders = 16;
39
40
/// Probe each public getinfo return family and the response-header API after
41
/// curl has settled the transfer. Applications commonly inspect these APIs,
42
/// but a harness that only drives I/O leaves their type dispatch and
43
/// post-transfer state unexecuted even when the corresponding parser ran.
44
/// The chosen values are handle-owned or scalar: notably CERTINFO exercises
45
/// the pointer/slist dispatch family without materialising a separately-owned
46
/// cookie/engine list. Header iteration is capped independently of response
47
/// size so this unconditional coverage cannot dominate a fuzz iteration.
48
10.1k
void ProbeTransferResults(CURL* easy) {
49
10.1k
  char* string_result = nullptr;
50
10.1k
  long long_result = 0;
51
10.1k
  double double_result = 0;
52
10.1k
  curl_off_t offset_result = 0;
53
10.1k
  curl_socket_t socket_result = CURL_SOCKET_BAD;
54
10.1k
  struct curl_certinfo* certinfo_result = nullptr;
55
56
10.1k
  (void)curl_easy_getinfo(easy, CURLINFO_EFFECTIVE_URL, &string_result);
57
10.1k
  (void)curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &long_result);
58
10.1k
  (void)curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &double_result);
59
10.1k
  (void)curl_easy_getinfo(easy, CURLINFO_SIZE_DOWNLOAD_T, &offset_result);
60
10.1k
  (void)curl_easy_getinfo(easy, CURLINFO_ACTIVESOCKET, &socket_result);
61
10.1k
  (void)curl_easy_getinfo(easy, CURLINFO_CERTINFO, &certinfo_result);
62
63
10.1k
  struct curl_header* header = nullptr;
64
10.1k
  (void)curl_easy_header(easy, "Content-Type", 0, kAllHeaderOrigins, -1, &header);
65
10.1k
  header = nullptr;
66
29.6k
  for (std::size_t index = 0; index < kMaxResultHeaders; ++index) {
67
29.0k
    header = curl_easy_nextheader(easy, kAllHeaderOrigins, -1, header);
68
29.0k
    if (header == nullptr) {
69
9.60k
      break;
70
9.60k
    }
71
29.0k
  }
72
10.1k
}
73
74
/// Map a Scheme enum to the URL scheme literal.
75
10.1k
const char* SchemePrefix(curl::fuzzer::proto::Scheme scheme) {
76
10.1k
  switch (scheme) {
77
7.79k
    case curl::fuzzer::proto::SCHEME_HTTP:
78
7.79k
      return "http";
79
140
    case curl::fuzzer::proto::SCHEME_HTTPS:
80
140
      return "https";
81
1.71k
    case curl::fuzzer::proto::SCHEME_WS:
82
1.71k
      return "ws";
83
501
    case curl::fuzzer::proto::SCHEME_WSS:
84
501
      return "wss";
85
46
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
86
46
    default:
87
46
      return nullptr;
88
10.1k
  }
89
10.1k
}
90
91
/// Pick the MockServerBase subclass to use for 'scenario'. The scheme is the
92
/// sole classifier today: WS / WSS → WebSocketMockServer, HTTP / HTTPS →
93
/// MockServer. Returns nullptr for unsupported / unspecified schemes so the
94
/// runner can skip the scenario cleanly.
95
10.1k
std::unique_ptr<MockServerBase> MakeMockServerForScenario(const curl::fuzzer::proto::Scenario& scenario) {
96
10.1k
  switch (scenario.scheme()) {
97
7.79k
    case curl::fuzzer::proto::SCHEME_HTTP:
98
7.93k
    case curl::fuzzer::proto::SCHEME_HTTPS:
99
7.93k
      return std::make_unique<MockServer>();
100
1.71k
    case curl::fuzzer::proto::SCHEME_WS:
101
2.21k
    case curl::fuzzer::proto::SCHEME_WSS:
102
2.21k
      return std::make_unique<WebSocketMockServer>();
103
0
    case curl::fuzzer::proto::SCHEME_UNSPECIFIED:
104
0
    default:
105
0
      return nullptr;
106
10.1k
  }
107
10.1k
}
108
109
}  // namespace
110
111
/// @class proto_fuzzer::ScenarioRunner
112
/// @brief Executes one Scenario end-to-end: applies options, picks a mock
113
///        server for the scheme, and hands off to the mock's DriveScenario.
114
///        Instances are cheap; create one per fuzz case so per-scenario state
115
///        is torn down cleanly.
116
117
/// Default-construct an empty runner. All state is set up inside Run().
118
10.1k
ScenarioRunner::ScenarioRunner() = default;
119
120
/// Default destructor; per-run state is local to Run() so nothing to tear
121
/// down at instance scope.
122
10.1k
ScenarioRunner::~ScenarioRunner() = default;
123
124
/// Run the scenario. Classifies the scheme to pick a MockServer subclass,
125
/// applies baseline + per-option setopt calls, builds the URL from
126
/// scenario.scheme + scenario.host_path, and drives the transfer via the
127
/// mock's own DriveScenario.
128
/// @param scenario The Scenario describing the curl operations to perform.
129
/// @param probe_transfer_results Whether to exercise post-transfer easy-handle
130
///        result APIs. The multi driver always consumes curl_multi_info_read;
131
///        this flag controls only the separate getinfo/header probes here.
132
/// @return 0 on normal completion (including curl errors that aren't harness
133
///         failures). The libFuzzer entrypoint doesn't care about the return
134
///         value; it's there for tests.
135
10.1k
int ScenarioRunner::Run(const curl::fuzzer::proto::Scenario& scenario, bool probe_transfer_results) {
136
10.1k
  const char* prefix = SchemePrefix(scenario.scheme());
137
10.1k
  if (prefix == nullptr || scenario.host_path().empty()) {
138
50
    return 0;
139
50
  }
140
141
10.1k
  std::unique_ptr<MockServerBase> mock = MakeMockServerForScenario(scenario);
142
10.1k
  if (!mock) {
143
0
    return 0;
144
0
  }
145
146
10.1k
  CurlEasyPtr easy(curl_easy_init());
147
10.1k
  if (!easy) {
148
0
    return 0;
149
0
  }
150
151
10.1k
  struct curl_slist* connect_to = ApplyBaselineOptions(easy.get());
152
153
10.1k
  std::string url = std::string(prefix) + "://" + scenario.host_path();
154
10.1k
  curl_easy_setopt(easy.get(), CURLOPT_URL, url.c_str());
155
156
10.1k
  mock->Install(easy.get());
157
158
  // Compatibility inputs deliberately bypass the mutating postprocessor, so
159
  // enforce the shared option prefix again at the runtime boundary. The helper
160
  // still ignores individual CURLcodes: the fuzzer stresses curl rather than
161
  // treating rejected option combinations as harness failures.
162
10.1k
  (void)ApplyScenarioOptions(easy.get(), scenario);
163
164
10.1k
  {
165
    // HTTP headers and MIME bodies are pointer-valued options that libcurl
166
    // does not copy. Keep their owner around the entire multi-handle drive,
167
    // then let it detach them while `easy` is still valid. This inner scope is
168
    // deliberate: easy.reset() below must never run before the owner's
169
    // destructor tries to clear those options.
170
10.1k
    ScenarioRequestData request_data(easy.get(), scenario);
171
10.1k
    mock->DriveScenario(easy.get(), scenario);
172
10.1k
    if (probe_transfer_results) {
173
10.1k
      ProbeTransferResults(easy.get());
174
10.1k
    }
175
10.1k
  }
176
177
10.1k
  easy.reset();
178
10.1k
  curl_slist_free_all(connect_to);
179
10.1k
  return 0;
180
10.1k
}
181
182
}  // namespace proto_fuzzer