/src/curl_fuzzer/proto_fuzzer/transfer_session.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 Ordered easy-handle and retained-option resource ownership. |
9 | | |
10 | | #include "proto_fuzzer/transfer_session.h" |
11 | | |
12 | | #include <algorithm> |
13 | | #include <cstddef> |
14 | | #include <cstdint> |
15 | | #include <utility> |
16 | | |
17 | | #include "proto_fuzzer/api_lifecycle.h" |
18 | | #include "proto_fuzzer/bounded_anonymous_input_file.h" |
19 | | #include "proto_fuzzer/option_apply.h" |
20 | | #include "proto_fuzzer/request_data.h" |
21 | | #include "proto_fuzzer/scenario_limits.h" |
22 | | |
23 | | namespace proto_fuzzer { |
24 | | namespace { |
25 | | |
26 | | constexpr char kDevNullPath[] = "/dev/null"; |
27 | | constexpr char kAltSvcOrigin[] = "altsvc-origin.test"; |
28 | | constexpr char kAltSvcLoopbackResolve[] = "*:80:127.0.1.127"; |
29 | | |
30 | | /// Fail closed before any resolver backend starts ambient DNS work. |
31 | 23 | int AbortUnexpectedAltSvcResolve(void*, void*, void*) { return 1; } |
32 | | |
33 | 1.53k | bool HasCanonicalAltSvcAuthority(const std::string& host_path) { |
34 | 1.53k | const std::size_t host_size = sizeof(kAltSvcOrigin) - 1; |
35 | 1.53k | return host_path.compare(0, host_size, kAltSvcOrigin) == 0 && |
36 | 1.53k | (host_path.size() == host_size || host_path[host_size] == '/' || host_path[host_size] == '?' || |
37 | 17 | host_path[host_size] == '#'); |
38 | 1.53k | } |
39 | | |
40 | | /// Repeat fixed-target bounds for direct runtime and compatibility callers. |
41 | | void PrepareInputFile(const std::string& contents, std::size_t* remaining_bytes, BoundedAnonymousInputFile* input_file, |
42 | 183k | bool guard_netrc = false) { |
43 | 183k | const std::size_t size = std::min(contents.size(), std::min(scenario_limits::kMaxFileInputBytes, *remaining_bytes)); |
44 | 183k | if (size == 0) { |
45 | 174k | return; |
46 | 174k | } |
47 | 8.72k | if (guard_netrc) { |
48 | | // Retain a parser-neutral newline when curl's loader drops comment lines. |
49 | | // Charge only protobuf bytes to the shared budget, as before. |
50 | 2.14k | std::string guarded_contents(1, '\n'); |
51 | 2.14k | guarded_contents.append(contents.data(), size); |
52 | 2.14k | if (input_file->Write(reinterpret_cast<const std::uint8_t*>(guarded_contents.data()), guarded_contents.size())) { |
53 | 2.14k | *remaining_bytes -= size; |
54 | 2.14k | } |
55 | 6.58k | } else if (input_file->Write(reinterpret_cast<const std::uint8_t*>(contents.data()), size)) { |
56 | 6.58k | *remaining_bytes -= size; |
57 | 6.58k | } |
58 | 8.72k | } |
59 | | |
60 | | } // namespace |
61 | | |
62 | | /// Files are allocated only for parser-input lanes and survive easy cleanup. |
63 | | struct TransferSession::ParserInputs { |
64 | | BoundedAnonymousInputFile cookie{scenario_limits::kMaxFileInputBytes}; |
65 | | BoundedAnonymousInputFile altsvc{scenario_limits::kMaxFileInputBytes}; |
66 | | BoundedAnonymousInputFile hsts{scenario_limits::kMaxFileInputBytes}; |
67 | | BoundedAnonymousInputFile netrc{scenario_limits::kMaxFileInputBytes + 1}; |
68 | | BoundedAnonymousInputFile crl{scenario_limits::kMaxFileInputBytes}; |
69 | | }; |
70 | | |
71 | 416k | TransferSession::TransferSession() = default; |
72 | 416k | TransferSession::~TransferSession() { Close(); } |
73 | | |
74 | | /// Allocate the easy lazily so unused concurrent-transfer slots stay empty. |
75 | | /// @return Whether the session has a usable easy handle. |
76 | 394k | bool TransferSession::Initialize() { |
77 | 394k | if (!easy_) { |
78 | 394k | easy_.reset(curl_easy_init()); |
79 | 394k | } |
80 | 394k | return easy_ != nullptr; |
81 | 394k | } |
82 | | |
83 | | /// Borrow the owned easy; callers must not clean it themselves. |
84 | | /// @return Easy handle, or nullptr before successful initialization. |
85 | 965k | CURL* TransferSession::easy() const { return easy_.get(); } |
86 | | |
87 | | /// Apply baseline options and retain their CONNECT_TO storage. |
88 | | /// @param scheme Protocol serviced by the installed peer. |
89 | | /// @param trace_ids Whether to enable bounded trace-ID diagnostics. |
90 | 395k | void TransferSession::ApplyBaseline(curl::fuzzer::proto::Scheme scheme, bool trace_ids) { |
91 | 395k | if (easy_) { |
92 | | // Apply the new pointer before releasing the previous list. |
93 | 395k | connect_to_.reset(ApplyBaselineOptions(easy_.get(), scheme, trace_ids)); |
94 | 395k | } |
95 | 395k | } |
96 | | |
97 | | /// Reset pre-transfer configuration while preserving prepared parser files. |
98 | | /// @return False if request or API callback state has already been installed. |
99 | 1.89k | bool TransferSession::ResetConfiguration() { |
100 | 1.89k | if (!easy_ || request_data_ || api_lifecycle_) { |
101 | 0 | return false; |
102 | 0 | } |
103 | 1.89k | curl_easy_reset(easy_.get()); |
104 | 1.89k | connect_to_.reset(); |
105 | 1.89k | altsvc_resolve_.reset(); |
106 | 1.89k | return true; |
107 | 1.89k | } |
108 | | |
109 | | /// Copy bounded parser inputs into lazily allocated anonymous files. |
110 | | /// @param scenario Source of parser-file bytes; not retained. |
111 | | /// @param mode Select deep HTTP or TLS parser inputs. |
112 | | /// @return False after easy initialization, when paths may be retained. |
113 | 325k | bool TransferSession::PrepareInputFiles(const curl::fuzzer::proto::Scenario& scenario, ScenarioRunMode mode) { |
114 | 325k | if (easy_) { |
115 | 0 | return false; |
116 | 0 | } |
117 | 325k | if (mode != ScenarioRunMode::kDeepHttpCoverage && mode != ScenarioRunMode::kTlsCoverage) { |
118 | 232k | return true; |
119 | 232k | } |
120 | 93.3k | parser_inputs_ = std::make_unique<ParserInputs>(); |
121 | 93.3k | std::size_t remaining_bytes = scenario_limits::kMaxFileInputTotalBytes; |
122 | 93.3k | if (mode == ScenarioRunMode::kDeepHttpCoverage) { |
123 | 30.1k | PrepareInputFile(scenario.cookie_file(), &remaining_bytes, &parser_inputs_->cookie); |
124 | 30.1k | PrepareInputFile(scenario.altsvc_file(), &remaining_bytes, &parser_inputs_->altsvc); |
125 | 30.1k | PrepareInputFile(scenario.hsts_file(), &remaining_bytes, &parser_inputs_->hsts); |
126 | 30.1k | PrepareInputFile(scenario.netrc_file(), &remaining_bytes, &parser_inputs_->netrc, true); |
127 | 63.2k | } else { |
128 | 63.2k | PrepareInputFile(scenario.crl_file(), &remaining_bytes, &parser_inputs_->crl); |
129 | 63.2k | } |
130 | 93.3k | return true; |
131 | 325k | } |
132 | | |
133 | | /// Apply prepared parser paths after baseline and peer installation. |
134 | 326k | void TransferSession::ApplyInputFiles() { |
135 | 326k | if (!easy_ || !parser_inputs_) { |
136 | 233k | return; |
137 | 233k | } |
138 | 93.1k | if (const char* path = parser_inputs_->cookie.path()) { |
139 | 2.35k | (void)curl_easy_setopt(easy_.get(), CURLOPT_COOKIEFILE, path); |
140 | 2.35k | } |
141 | | // Alt-Svc/HSTS use one option for input and cleanup output. Load first, |
142 | | // then restore the fixed sink; both loaders retain earlier file entries. |
143 | 93.1k | if (const char* path = parser_inputs_->altsvc.path()) { |
144 | 1.53k | (void)curl_easy_setopt(easy_.get(), CURLOPT_ALTSVC, path); |
145 | 1.53k | (void)curl_easy_setopt(easy_.get(), CURLOPT_ALTSVC, kDevNullPath); |
146 | 1.53k | } |
147 | 93.1k | if (const char* path = parser_inputs_->hsts.path()) { |
148 | 1.65k | (void)curl_easy_setopt(easy_.get(), CURLOPT_HSTS, path); |
149 | 1.65k | (void)curl_easy_setopt(easy_.get(), CURLOPT_HSTS, kDevNullPath); |
150 | 1.65k | } |
151 | 93.1k | if (const char* path = parser_inputs_->netrc.path()) { |
152 | 2.14k | (void)curl_easy_setopt(easy_.get(), CURLOPT_NETRC_FILE, path); |
153 | 2.14k | (void)curl_easy_setopt(easy_.get(), CURLOPT_NETRC, CURL_NETRC_REQUIRED); |
154 | 2.14k | } |
155 | 93.1k | if (const char* path = parser_inputs_->crl.path()) { |
156 | 1.01k | (void)curl_easy_setopt(easy_.get(), CURLOPT_CRLFILE, path); |
157 | 1.01k | } |
158 | 93.1k | } |
159 | | |
160 | | /// Enable isolated Alt-Svc routing for the canonical deep-HTTP authority. |
161 | | /// @param host_path Scenario URL authority/path used by this transfer. |
162 | 324k | void TransferSession::ConfigureAltSvcRouting(const std::string& host_path) { |
163 | 324k | if (!easy_ || !parser_inputs_ || parser_inputs_->altsvc.path() == nullptr || |
164 | 322k | !HasCanonicalAltSvcAuthority(host_path)) { |
165 | 322k | return; |
166 | 322k | } |
167 | 1.53k | CurlSlistPtr resolve(curl_slist_append(nullptr, kAltSvcLoopbackResolve)); |
168 | 1.53k | if (!resolve || curl_easy_setopt(easy_.get(), CURLOPT_RESOLVE, resolve.get()) != CURLE_OK) { |
169 | 4 | return; |
170 | 4 | } |
171 | | // Adopt only after installation; failures leave the previous list alive. |
172 | 1.53k | altsvc_resolve_ = std::move(resolve); |
173 | 1.53k | if (curl_easy_setopt(easy_.get(), CURLOPT_RESOLVER_START_FUNCTION, &AbortUnexpectedAltSvcResolve) == CURLE_OK) { |
174 | | // CONNECT_TO takes precedence over Alt-Svc. Only detach it when the |
175 | | // wildcard host cache and fail-closed resolver callback are installed. |
176 | 1.53k | (void)curl_easy_setopt(easy_.get(), CURLOPT_CONNECT_TO, nullptr); |
177 | 1.53k | } |
178 | 1.53k | } |
179 | | |
180 | | /// Install owned request attachments. The Scenario remains borrowed. |
181 | | /// @param scenario Live, unmodified source until session teardown. |
182 | | /// @param apply_resolve_entries Install resolver-lane mappings. |
183 | | /// @return Request owner, or nullptr without an easy or if already installed. |
184 | | ScenarioRequestData* TransferSession::InstallRequestData(const curl::fuzzer::proto::Scenario& scenario, |
185 | 393k | bool apply_resolve_entries) { |
186 | 393k | if (!easy_ || request_data_) { |
187 | 0 | return nullptr; |
188 | 0 | } |
189 | 393k | request_data_.emplace(easy_.get(), scenario, apply_resolve_entries); |
190 | 393k | return &*request_data_; |
191 | 393k | } |
192 | | |
193 | | /// Install API/share state that survives easy cleanup. |
194 | | /// @param plan Live plan until session teardown. |
195 | | /// @param url URL used only during lifecycle construction. |
196 | | /// @return Lifecycle owner, or nullptr if absent/already installed. |
197 | 5.79k | ApiLifecycle* TransferSession::InstallApiLifecycle(const curl::fuzzer::proto::ApiPlan& plan, std::string_view url) { |
198 | 5.79k | if (!easy_ || api_lifecycle_) { |
199 | 0 | return nullptr; |
200 | 0 | } |
201 | 5.79k | api_lifecycle_ = std::make_unique<ApiLifecycle>(easy_.get(), plan, url); |
202 | 5.79k | return api_lifecycle_.get(); |
203 | 5.79k | } |
204 | | |
205 | | /// Release all resources in their required order. Safe to call repeatedly. |
206 | 485k | void TransferSession::Close() { |
207 | 485k | request_data_.reset(); |
208 | 485k | easy_.reset(); |
209 | 485k | api_lifecycle_.reset(); |
210 | 485k | altsvc_resolve_.reset(); |
211 | 485k | connect_to_.reset(); |
212 | 485k | parser_inputs_.reset(); |
213 | 485k | } |
214 | | |
215 | | } // namespace proto_fuzzer |