/src/curl_fuzzer/proto_fuzzer/h2_runtime.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 Shared HTTP/2 origin behavior independent of the transport carrier. |
9 | | |
10 | | #include "proto_fuzzer/h2_runtime.h" |
11 | | |
12 | | #include <curl/multi.h> |
13 | | |
14 | | #include <algorithm> |
15 | | #include <limits> |
16 | | #include <utility> |
17 | | |
18 | | #include "proto_fuzzer/mock_server.h" |
19 | | |
20 | | namespace proto_fuzzer { |
21 | | |
22 | | namespace { |
23 | | |
24 | | // Bound header inspection independently of curl's PUSH_PROMISE limits. |
25 | | constexpr std::size_t kMaxInspectedPushHeaders = 16; |
26 | | // One extra easy handle exercises accepted push without multiplying work. |
27 | | constexpr std::size_t kMaxAcceptedPushes = 1; |
28 | | |
29 | | } // namespace |
30 | | |
31 | | /// Construct detached protocol state without observing a connection. |
32 | 27.5k | H2Runtime::H2Runtime() = default; |
33 | | |
34 | | /// Install the bounded push policy before curl emits its initial SETTINGS. |
35 | | /// The runtime must outlive the multi and any accepted pushed handles. |
36 | | /// @param multi Caller-owned multi handle receiving the push callbacks. |
37 | | /// @param accept_push Whether to accept one push and reject further pushes. |
38 | 27.3k | void H2Runtime::PrepareMulti(CURLM* multi, bool accept_push) { |
39 | 27.3k | accept_push_ = accept_push; |
40 | 27.3k | accepted_push_count_ = 0; |
41 | 27.3k | pushed_body_bytes_ = 0; |
42 | 27.3k | (void)curl_multi_setopt(multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX); |
43 | 27.3k | (void)curl_multi_setopt(multi, CURLMOPT_PUSHFUNCTION, &H2Runtime::PushCallback); |
44 | 27.3k | (void)curl_multi_setopt(multi, CURLMOPT_PUSHDATA, this); |
45 | 27.3k | } |
46 | | |
47 | | /// Observe application bytes after the carrier has decoded its transport. |
48 | | /// A null connection is harmless; an attached connection borrows this runtime. |
49 | | /// @param connection Transport to observe, or nullptr when creation failed. |
50 | 26.4k | void H2Runtime::AttachConnection(MockConnection* connection) { |
51 | 26.4k | if (connection != nullptr) { |
52 | 26.4k | connection->SetIncomingDataObserver(&plan_driver_); |
53 | 26.4k | } |
54 | 26.4k | } |
55 | | |
56 | | /// Borrow a structured plan for the ensuing synchronous drive. |
57 | | /// @param plan Stable plan that remains alive and unmodified during the drive. |
58 | | /// @param scheme Scheme used in generated push-request pseudo-headers. |
59 | | /// @param authority Authority used in generated push-request pseudo-headers. |
60 | 16.0k | void H2Runtime::ResetPlan(const curl::fuzzer::proto::Http2Plan& plan, std::string scheme, std::string authority) { |
61 | 16.0k | plan_driver_.Reset(plan, std::move(scheme), std::move(authority)); |
62 | 16.0k | stream_weight_after_headers_ = plan.stream_weight_after_headers(); |
63 | 16.0k | post_headers_transition_applied_ = false; |
64 | 16.0k | stream_weight_setopt_result_ = CURLE_FAILED_INIT; |
65 | 16.0k | stream_weight_egress_result_ = CURLE_FAILED_INIT; |
66 | 16.0k | } |
67 | | |
68 | | /// Apply the plan's one state transition only after a real request stream has |
69 | | /// been observed. Changing the public option at this point makes curl compare |
70 | | /// the desired weight against the live stream's effective weight. |
71 | | /// @param easy Active easy handle whose live stream weight is changed. |
72 | 245k | void H2Runtime::ApplyPostHeadersTransition(CURL* easy) { |
73 | 245k | if (stream_weight_after_headers_ == 0 || post_headers_transition_applied_ || |
74 | 244k | plan_driver_.observed_request_count() == 0) { |
75 | 244k | return; |
76 | 244k | } |
77 | 1.30k | post_headers_transition_applied_ = true; |
78 | 1.30k | stream_weight_setopt_result_ = |
79 | 1.30k | curl_easy_setopt(easy, CURLOPT_STREAM_WEIGHT, static_cast<long>(stream_weight_after_headers_)); |
80 | | // Unpausing an already-running H2 stream is a documented no-op at the |
81 | | // transfer layer, but it deliberately asks the live filter to flush its |
82 | | // updated flow/priority state. This public call provides a deterministic |
83 | | // egress boundary without sleeping or depending on socket readiness. |
84 | 1.30k | stream_weight_egress_result_ = curl_easy_pause(easy, CURLPAUSE_CONT); |
85 | 1.30k | } |
86 | | |
87 | | /// Alternate curl and one bounded H2 plan boundary. Carrier observation and |
88 | | /// response-resume work run after each successful perform through the hook. |
89 | | /// Idle and operation limits remain selected by the owning driver. |
90 | | /// @param multi Caller-owned multi handle containing the transfer to drive. |
91 | | /// @param server Carrier exposing the currently active connection. |
92 | | /// @param max_idle_iterations Consecutive no-progress turns allowed. |
93 | | /// @param max_drive_iterations Total state-machine turns allowed. |
94 | | /// @param after_perform Carrier hook invoked after each successful perform. |
95 | | void H2Runtime::DrivePlan(CURLM* multi, MockServer& server, int max_idle_iterations, int max_drive_iterations, |
96 | 16.0k | const std::function<void()>& after_perform) { |
97 | 16.0k | int still_running = 1; |
98 | 16.0k | int idle_iterations = 0; |
99 | 16.0k | int drive_iterations = 0; |
100 | 253k | while (still_running && idle_iterations < max_idle_iterations && drive_iterations++ < max_drive_iterations) { |
101 | 245k | const int running_before = still_running; |
102 | 245k | if (curl_multi_perform(multi, &still_running) != CURLM_OK) { |
103 | 0 | break; |
104 | 0 | } |
105 | 245k | after_perform(); |
106 | 245k | if (!still_running) { |
107 | 7.92k | break; |
108 | 7.92k | } |
109 | | |
110 | 237k | bool made_progress = still_running != running_before; |
111 | 237k | if (MockConnection* connection = server.connection(); connection != nullptr) { |
112 | 235k | made_progress = connection->DrainIncoming() != 0 || made_progress; |
113 | 235k | std::string chunk; |
114 | 235k | if (plan_driver_.NextChunk(&chunk)) { |
115 | 155k | made_progress = true; |
116 | 155k | if (!chunk.empty()) { |
117 | 80.7k | (void)connection->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
118 | 80.7k | } |
119 | 155k | made_progress = connection->DrainIncoming() != 0 || made_progress; |
120 | 155k | } |
121 | 235k | } |
122 | 237k | if (made_progress) { |
123 | 172k | idle_iterations = 0; |
124 | 172k | } else { |
125 | 65.2k | ++idle_iterations; |
126 | 65.2k | } |
127 | 237k | } |
128 | 16.0k | } |
129 | | |
130 | | /// Probe upkeep while curl's connection cache and the peer are still live. |
131 | | /// @param easy Caller-owned easy handle whose upkeep API is probed. |
132 | | /// @param connection Peer to drain after upkeep, or nullptr when none opened. |
133 | 27.3k | void H2Runtime::FinishTransfer(CURL* easy, MockConnection* connection) { |
134 | 27.3k | upkeep_result_ = curl_easy_upkeep(easy); |
135 | 27.3k | if (connection != nullptr) { |
136 | 26.4k | (void)connection->DrainIncoming(); |
137 | 26.4k | } |
138 | | |
139 | 27.3k | #if defined(PROTO_FUZZER_OBSERVE_STATE) |
140 | | // The carrier still owns every callback and plan observation here. Retain |
141 | | // the completed state in profiling builds without changing the H2 exchange |
142 | | // or adding per-input work to ordinary fuzz targets. |
143 | 27.3k | const volatile std::size_t observation[] = {push_callback_count(), |
144 | 27.3k | push_header_count(), |
145 | 27.3k | saw_push_path(), |
146 | 27.3k | accepted_push_count(), |
147 | 27.3k | pushed_body_bytes(), |
148 | 27.3k | static_cast<std::size_t>(upkeep_result()), |
149 | 27.3k | observed_request_count(), |
150 | 27.3k | observed_client_settings_count(), |
151 | 27.3k | plan_driver_.has_pending_work()}; |
152 | 27.3k | (void)observation; |
153 | 27.3k | #endif |
154 | 27.3k | } |
155 | | |
156 | | /// @return Number of syntactically valid pushes offered to this runtime. |
157 | 27.3k | std::size_t H2Runtime::push_callback_count() const { return push_callback_count_; } |
158 | | |
159 | | /// @return Number of PUSH_PROMISE headers advertised to the push callback. |
160 | 27.3k | std::size_t H2Runtime::push_header_count() const { return push_header_count_; } |
161 | | |
162 | | /// @return True when the latest push callback found the promised :path. |
163 | 27.3k | bool H2Runtime::saw_push_path() const { return saw_push_path_; } |
164 | | |
165 | | /// @return Number of pushed transfers accepted in the most recent drive. |
166 | 27.3k | std::size_t H2Runtime::accepted_push_count() const { return accepted_push_count_; } |
167 | | |
168 | | /// @return Body bytes consumed from accepted pushes in the latest drive. |
169 | 27.3k | std::size_t H2Runtime::pushed_body_bytes() const { return pushed_body_bytes_; } |
170 | | |
171 | | /// @return Result of the most recent upkeep probe, or CURLE_FAILED_INIT. |
172 | 27.3k | CURLcode H2Runtime::upkeep_result() const { return upkeep_result_; } |
173 | | |
174 | | /// @return Number of request streams observed since the latest plan reset. |
175 | 27.3k | std::size_t H2Runtime::observed_request_count() const { return plan_driver_.observed_request_count(); } |
176 | | |
177 | | /// @return Non-ACK client SETTINGS frames observed since the latest plan reset. |
178 | 27.3k | std::size_t H2Runtime::observed_client_settings_count() const { return plan_driver_.observed_client_settings_count(); } |
179 | | |
180 | | /// @return PRIORITY frames observed in curl's client byte stream. |
181 | 0 | std::size_t H2Runtime::observed_client_priority_count() const { return plan_driver_.observed_client_priority_count(); } |
182 | | |
183 | | /// @return Whether request observation triggered the configured transition. |
184 | 0 | bool H2Runtime::post_headers_transition_applied() const { return post_headers_transition_applied_; } |
185 | | |
186 | | /// @return Result of the live CURLOPT_STREAM_WEIGHT update. |
187 | 0 | CURLcode H2Runtime::stream_weight_setopt_result() const { return stream_weight_setopt_result_; } |
188 | | |
189 | | /// @return Result of the public resume used as the deterministic egress edge. |
190 | 0 | CURLcode H2Runtime::stream_weight_egress_result() const { return stream_weight_egress_result_; } |
191 | | |
192 | | int H2Runtime::PushCallback(CURL* /*parent*/, CURL* pushed, std::size_t header_count, struct curl_pushheaders* headers, |
193 | 8.48k | void* userdata) { |
194 | 8.48k | auto* self = static_cast<H2Runtime*>(userdata); |
195 | 8.48k | if (self == nullptr) { |
196 | 0 | return CURL_PUSH_DENY; |
197 | 0 | } |
198 | | |
199 | 8.48k | ++self->push_callback_count_; |
200 | 8.48k | self->push_header_count_ += header_count; |
201 | 8.48k | const std::size_t inspected = std::min(header_count, kMaxInspectedPushHeaders); |
202 | 55.5k | for (std::size_t index = 0; index < inspected; ++index) { |
203 | 47.0k | (void)curl_pushheader_bynum(headers, index); |
204 | 47.0k | } |
205 | 8.48k | self->saw_push_path_ = curl_pushheader_byname(headers, ":path") != nullptr; |
206 | 8.48k | (void)curl_pushheader_byname(headers, "x-fuzzer-missing"); |
207 | | |
208 | 8.48k | if (!self->accept_push_ || self->accepted_push_count_ >= kMaxAcceptedPushes) { |
209 | 6.63k | return CURL_PUSH_DENY; |
210 | 6.63k | } |
211 | | // The new pushed handle needs an application-owned sink before acceptance. |
212 | 1.84k | if (curl_easy_setopt(pushed, CURLOPT_WRITEFUNCTION, &H2Runtime::PushedWriteCallback) != CURLE_OK || |
213 | 1.84k | curl_easy_setopt(pushed, CURLOPT_WRITEDATA, self) != CURLE_OK) { |
214 | 0 | return CURL_PUSH_DENY; |
215 | 0 | } |
216 | 1.84k | ++self->accepted_push_count_; |
217 | 1.84k | return CURL_PUSH_OK; |
218 | 1.84k | } |
219 | | |
220 | 164 | std::size_t H2Runtime::PushedWriteCallback(char* /*contents*/, std::size_t size, std::size_t nmemb, void* userdata) { |
221 | 164 | auto* self = static_cast<H2Runtime*>(userdata); |
222 | 164 | const std::size_t maximum = std::numeric_limits<std::size_t>::max(); |
223 | 164 | if (self == nullptr || (size != 0 && nmemb > maximum / size)) { |
224 | 0 | return 0; |
225 | 0 | } |
226 | 164 | const std::size_t bytes = size * nmemb; |
227 | 164 | self->pushed_body_bytes_ += std::min(bytes, maximum - self->pushed_body_bytes_); |
228 | 164 | return bytes; |
229 | 164 | } |
230 | | |
231 | | } // namespace proto_fuzzer |