/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 |