/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 | 36.2k | 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 | 36.0k | void H2Runtime::PrepareMulti(CURLM* multi, bool accept_push) { |
39 | 36.0k | accept_push_ = accept_push; |
40 | 36.0k | accepted_push_count_ = 0; |
41 | 36.0k | pushed_body_bytes_ = 0; |
42 | 36.0k | (void)curl_multi_setopt(multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX); |
43 | 36.0k | (void)curl_multi_setopt(multi, CURLMOPT_PUSHFUNCTION, &H2Runtime::PushCallback); |
44 | 36.0k | (void)curl_multi_setopt(multi, CURLMOPT_PUSHDATA, this); |
45 | 36.0k | } |
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 | 35.4k | void H2Runtime::AttachConnection(MockConnection* connection) { |
51 | 35.4k | if (connection != nullptr) { |
52 | 35.4k | connection->SetIncomingDataObserver(&plan_driver_); |
53 | 35.4k | } |
54 | 35.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 | 19.7k | void H2Runtime::ResetPlan(const curl::fuzzer::proto::Http2Plan& plan, std::string scheme, std::string authority) { |
61 | 19.7k | plan_driver_.Reset(plan, std::move(scheme), std::move(authority)); |
62 | 19.7k | } |
63 | | |
64 | | /// Alternate curl and one bounded H2 plan boundary. Carrier observation and |
65 | | /// response-resume work run after each successful perform through the hook. |
66 | | /// Idle and operation limits remain selected by the owning driver. |
67 | | /// @param multi Caller-owned multi handle containing the transfer to drive. |
68 | | /// @param server Carrier exposing the currently active connection. |
69 | | /// @param max_idle_iterations Consecutive no-progress turns allowed. |
70 | | /// @param max_drive_iterations Total state-machine turns allowed. |
71 | | /// @param after_perform Carrier hook invoked after each successful perform. |
72 | | void H2Runtime::DrivePlan(CURLM* multi, MockServer& server, int max_idle_iterations, int max_drive_iterations, |
73 | 19.7k | const std::function<void()>& after_perform) { |
74 | 19.7k | int still_running = 1; |
75 | 19.7k | int idle_iterations = 0; |
76 | 19.7k | int drive_iterations = 0; |
77 | 385k | while (still_running && idle_iterations < max_idle_iterations && drive_iterations++ < max_drive_iterations) { |
78 | 374k | const int running_before = still_running; |
79 | 374k | if (curl_multi_perform(multi, &still_running) != CURLM_OK) { |
80 | 0 | break; |
81 | 0 | } |
82 | 374k | after_perform(); |
83 | 374k | if (!still_running) { |
84 | 8.33k | break; |
85 | 8.33k | } |
86 | | |
87 | 365k | bool made_progress = still_running != running_before; |
88 | 365k | if (MockConnection* connection = server.connection(); connection != nullptr) { |
89 | 365k | made_progress = connection->DrainIncoming() != 0 || made_progress; |
90 | 365k | std::string chunk; |
91 | 365k | if (plan_driver_.NextChunk(&chunk)) { |
92 | 260k | made_progress = true; |
93 | 260k | if (!chunk.empty()) { |
94 | 126k | (void)connection->WriteAll(reinterpret_cast<const unsigned char*>(chunk.data()), chunk.size()); |
95 | 126k | } |
96 | 260k | made_progress = connection->DrainIncoming() != 0 || made_progress; |
97 | 260k | } |
98 | 365k | } |
99 | 365k | if (made_progress) { |
100 | 274k | idle_iterations = 0; |
101 | 274k | } else { |
102 | 91.2k | ++idle_iterations; |
103 | 91.2k | } |
104 | 365k | } |
105 | 19.7k | } |
106 | | |
107 | | /// Probe upkeep while curl's connection cache and the peer are still live. |
108 | | /// @param easy Caller-owned easy handle whose upkeep API is probed. |
109 | | /// @param connection Peer to drain after upkeep, or nullptr when none opened. |
110 | 36.0k | void H2Runtime::FinishTransfer(CURL* easy, MockConnection* connection) { |
111 | 36.0k | upkeep_result_ = curl_easy_upkeep(easy); |
112 | 36.0k | if (connection != nullptr) { |
113 | 35.4k | (void)connection->DrainIncoming(); |
114 | 35.4k | } |
115 | | |
116 | 36.0k | #if defined(PROTO_FUZZER_OBSERVE_STATE) |
117 | | // The carrier still owns every callback and plan observation here. Retain |
118 | | // the completed state in profiling builds without changing the H2 exchange |
119 | | // or adding per-input work to ordinary fuzz targets. |
120 | 36.0k | const volatile std::size_t observation[] = {push_callback_count(), |
121 | 36.0k | push_header_count(), |
122 | 36.0k | saw_push_path(), |
123 | 36.0k | accepted_push_count(), |
124 | 36.0k | pushed_body_bytes(), |
125 | 36.0k | static_cast<std::size_t>(upkeep_result()), |
126 | 36.0k | observed_request_count(), |
127 | 36.0k | observed_client_settings_count(), |
128 | 36.0k | plan_driver_.has_pending_work()}; |
129 | 36.0k | (void)observation; |
130 | 36.0k | #endif |
131 | 36.0k | } |
132 | | |
133 | | /// @return Number of syntactically valid pushes offered to this runtime. |
134 | 36.0k | std::size_t H2Runtime::push_callback_count() const { return push_callback_count_; } |
135 | | |
136 | | /// @return Number of PUSH_PROMISE headers advertised to the push callback. |
137 | 36.0k | std::size_t H2Runtime::push_header_count() const { return push_header_count_; } |
138 | | |
139 | | /// @return True when the latest push callback found the promised :path. |
140 | 36.0k | bool H2Runtime::saw_push_path() const { return saw_push_path_; } |
141 | | |
142 | | /// @return Number of pushed transfers accepted in the most recent drive. |
143 | 36.0k | std::size_t H2Runtime::accepted_push_count() const { return accepted_push_count_; } |
144 | | |
145 | | /// @return Body bytes consumed from accepted pushes in the latest drive. |
146 | 36.0k | std::size_t H2Runtime::pushed_body_bytes() const { return pushed_body_bytes_; } |
147 | | |
148 | | /// @return Result of the most recent upkeep probe, or CURLE_FAILED_INIT. |
149 | 36.0k | CURLcode H2Runtime::upkeep_result() const { return upkeep_result_; } |
150 | | |
151 | | /// @return Number of request streams observed since the latest plan reset. |
152 | 36.0k | std::size_t H2Runtime::observed_request_count() const { return plan_driver_.observed_request_count(); } |
153 | | |
154 | | /// @return Non-ACK client SETTINGS frames observed since the latest plan reset. |
155 | 36.0k | std::size_t H2Runtime::observed_client_settings_count() const { return plan_driver_.observed_client_settings_count(); } |
156 | | |
157 | | int H2Runtime::PushCallback(CURL* /*parent*/, CURL* pushed, std::size_t header_count, struct curl_pushheaders* headers, |
158 | 8.36k | void* userdata) { |
159 | 8.36k | auto* self = static_cast<H2Runtime*>(userdata); |
160 | 8.36k | if (self == nullptr) { |
161 | 0 | return CURL_PUSH_DENY; |
162 | 0 | } |
163 | | |
164 | 8.36k | ++self->push_callback_count_; |
165 | 8.36k | self->push_header_count_ += header_count; |
166 | 8.36k | const std::size_t inspected = std::min(header_count, kMaxInspectedPushHeaders); |
167 | 49.6k | for (std::size_t index = 0; index < inspected; ++index) { |
168 | 41.2k | (void)curl_pushheader_bynum(headers, index); |
169 | 41.2k | } |
170 | 8.36k | self->saw_push_path_ = curl_pushheader_byname(headers, ":path") != nullptr; |
171 | 8.36k | (void)curl_pushheader_byname(headers, "x-fuzzer-missing"); |
172 | | |
173 | 8.36k | if (!self->accept_push_ || self->accepted_push_count_ >= kMaxAcceptedPushes) { |
174 | 6.26k | return CURL_PUSH_DENY; |
175 | 6.26k | } |
176 | | // The new pushed handle needs an application-owned sink before acceptance. |
177 | 2.09k | if (curl_easy_setopt(pushed, CURLOPT_WRITEFUNCTION, &H2Runtime::PushedWriteCallback) != CURLE_OK || |
178 | 2.09k | curl_easy_setopt(pushed, CURLOPT_WRITEDATA, self) != CURLE_OK) { |
179 | 0 | return CURL_PUSH_DENY; |
180 | 0 | } |
181 | 2.09k | ++self->accepted_push_count_; |
182 | 2.09k | return CURL_PUSH_OK; |
183 | 2.09k | } |
184 | | |
185 | 214 | std::size_t H2Runtime::PushedWriteCallback(char* /*contents*/, std::size_t size, std::size_t nmemb, void* userdata) { |
186 | 214 | auto* self = static_cast<H2Runtime*>(userdata); |
187 | 214 | const std::size_t maximum = std::numeric_limits<std::size_t>::max(); |
188 | 214 | if (self == nullptr || (size != 0 && nmemb > maximum / size)) { |
189 | 0 | return 0; |
190 | 0 | } |
191 | 214 | const std::size_t bytes = size * nmemb; |
192 | 214 | self->pushed_body_bytes_ += std::min(bytes, maximum - self->pushed_body_bytes_); |
193 | 214 | return bytes; |
194 | 214 | } |
195 | | |
196 | | } // namespace proto_fuzzer |