12#include <nghttp2/nghttp2.h>
25namespace proto_fuzzer {
29constexpr std::string_view kClientPreface =
"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
30constexpr std::size_t kFrameHeaderSize = 9;
31constexpr std::size_t kMaxFramePayload = 16384;
33constexpr std::uint8_t kDataFrame = 0;
34constexpr std::uint8_t kHeadersFrame = 1;
35constexpr std::uint8_t kPriorityFrame = 2;
36constexpr std::uint8_t kRstStreamFrame = 3;
37constexpr std::uint8_t kSettingsFrame = 4;
38constexpr std::uint8_t kPushPromiseFrame = 5;
39constexpr std::uint8_t kPingFrame = 6;
40constexpr std::uint8_t kGoawayFrame = 7;
41constexpr std::uint8_t kWindowUpdateFrame = 8;
42constexpr std::uint8_t kContinuationFrame = 9;
44constexpr std::uint8_t kFlagAck = 0x1;
45constexpr std::uint8_t kFlagEndStream = 0x1;
46constexpr std::uint8_t kFlagEndHeaders = 0x4;
48void AppendUint32(std::string* output, std::uint32_t value) {
49 output->push_back(
static_cast<char>((value >> 24U) & 0xffU));
50 output->push_back(
static_cast<char>((value >> 16U) & 0xffU));
51 output->push_back(
static_cast<char>((value >> 8U) & 0xffU));
52 output->push_back(
static_cast<char>(value & 0xffU));
55std::uint32_t ReadUint32(
const char* input) {
56 const auto* bytes =
reinterpret_cast<const unsigned char*
>(input);
57 return (
static_cast<std::uint32_t
>(bytes[0]) << 24U) | (
static_cast<std::uint32_t
>(bytes[1]) << 16U) |
58 (
static_cast<std::uint32_t
>(bytes[2]) << 8U) |
static_cast<std::uint32_t
>(bytes[3]);
61void AppendFrame(std::string* output, std::uint32_t declared_length, std::uint8_t type, std::uint8_t flags,
62 std::uint32_t stream_id, std::string_view payload) {
63 const std::uint32_t length = declared_length & 0x00ffffffU;
64 output->push_back(
static_cast<char>((length >> 16U) & 0xffU));
65 output->push_back(
static_cast<char>((length >> 8U) & 0xffU));
66 output->push_back(
static_cast<char>(length & 0xffU));
67 output->push_back(
static_cast<char>(type));
68 output->push_back(
static_cast<char>(flags));
69 AppendUint32(output, stream_id & 0x7fffffffU);
70 output->append(payload.data(), payload.size());
73std::uint32_t CanonicalStatus(std::uint32_t status) {
74 if (status >= 100U && status <= 599U) {
77 return status == 0U ? 200U : 100U + (status % 500U);
80std::string BoundedString(
const std::string& input, std::size_t limit) {
return input.substr(0, limit); }
82std::string CanonicalHeaderName(
const std::string& input) {
87 for (
char&
byte : name) {
88 const unsigned char value =
static_cast<unsigned char>(byte);
89 const bool alpha = (value >=
'A' && value <=
'Z') || (value >=
'a' && value <=
'z');
90 const bool digit = value >=
'0' && value <=
'9';
91 const bool punctuation = value ==
'!' || value ==
'#' || value ==
'$' || value ==
'%' || value ==
'&' ||
92 value ==
'\'' || value ==
'*' || value ==
'+' || value ==
'-' || value ==
'.' ||
93 value ==
'^' || value ==
'_' || value ==
'`' || value ==
'|' || value ==
'~';
94 if (value >=
'A' && value <=
'Z') {
95 byte =
static_cast<char>(value -
'A' +
'a');
96 }
else if (!alpha && !digit && !punctuation) {
103std::string CanonicalHeaderValue(
const std::string& input) {
105 for (
char&
byte : value) {
106 const unsigned char character =
static_cast<unsigned char>(byte);
107 if ((character < 0x20U && character !=
'\t') || character == 0x7fU) {
116H2PlanDriver::H2PlanDriver()
120 remaining_yields_(0),
121 initial_settings_sent_(false),
122 client_settings_ack_sent_(false),
123 expect_client_preface_(true),
124 observed_client_settings_(0),
125 observed_client_priority_(0) {
126 (void)nghttp2_hd_deflate_new(&deflater_, 4096);
129H2PlanDriver::~H2PlanDriver() { nghttp2_hd_deflate_del(deflater_); }
135void H2PlanDriver::Reset(
const curl::fuzzer::proto::Http2Plan& plan, std::string scheme, std::string authority) {
138 remaining_yields_ = 0;
139 initial_settings_sent_ =
false;
140 client_settings_ack_sent_ =
false;
141 expect_client_preface_ =
true;
142 observed_client_settings_ = 0;
143 observed_client_priority_ = 0;
144 client_bytes_.clear();
145 request_streams_.clear();
146 pushed_streams_.clear();
147 client_streams_.clear();
148 scheme_ = std::move(scheme);
149 authority_ = std::move(authority);
150 nghttp2_hd_deflate_del(deflater_);
152 (void)nghttp2_hd_deflate_new(&deflater_, 4096);
159 if (data ==
nullptr || size == 0) {
164 client_bytes_.clear();
165 expect_client_preface_ =
false;
168 client_bytes_.append(
reinterpret_cast<const char*
>(data), size);
172void H2PlanDriver::ParseClientFrames() {
173 if (expect_client_preface_) {
174 if (client_bytes_.size() < kClientPreface.size()) {
177 if (std::string_view(client_bytes_).substr(0, kClientPreface.size()) == kClientPreface) {
178 client_bytes_.erase(0, kClientPreface.size());
180 expect_client_preface_ =
false;
183 std::size_t consumed = 0;
184 while (client_bytes_.size() - consumed >= kFrameHeaderSize) {
185 const auto* header =
reinterpret_cast<const unsigned char*
>(client_bytes_.data() + consumed);
186 const std::size_t payload_size = (
static_cast<std::size_t
>(header[0]) << 16U) |
187 (
static_cast<std::size_t
>(header[1]) << 8U) |
static_cast<std::size_t
>(header[2]);
188 if (payload_size > scenario_limits::kMaxHttp2ObservedBytes ||
189 client_bytes_.size() - consumed < kFrameHeaderSize + payload_size) {
192 const std::uint8_t type = header[3];
193 const std::uint8_t flags = header[4];
194 const std::uint32_t stream_id = ReadUint32(client_bytes_.data() + consumed + 5) & 0x7fffffffU;
195 RecordClientFrame(type, flags, stream_id);
196 consumed += kFrameHeaderSize + payload_size;
198 client_bytes_.erase(0, consumed);
201void H2PlanDriver::RecordClientFrame(std::uint8_t type, std::uint8_t flags, std::uint32_t stream_id) {
202 if (type == kSettingsFrame && stream_id == 0 && (flags & kFlagAck) == 0) {
203 ++observed_client_settings_;
206 if (stream_id == 0) {
210 if (type == kPriorityFrame) {
211 ++observed_client_priority_;
214 auto& stream = client_streams_[stream_id];
215 if (type == kHeadersFrame) {
217 if ((stream_id & 1U) != 0 &&
218 std::find(request_streams_.begin(), request_streams_.end(), stream_id) == request_streams_.end()) {
219 request_streams_.push_back(stream_id);
221 }
else if (type == kDataFrame) {
224 if ((type == kHeadersFrame || type == kDataFrame) && (flags & kFlagEndStream) != 0) {
229std::uint32_t H2PlanDriver::ResolveStream(
const curl::fuzzer::proto::Http2StreamRef& stream,
230 bool allow_connection)
const {
231 using StreamRef = curl::fuzzer::proto::Http2StreamRef;
232 switch (stream.target_case()) {
233 case StreamRef::kConnection:
234 return allow_connection ? 0U : 1U;
235 case StreamRef::kRequestIndex: {
236 const std::size_t index =
static_cast<std::size_t
>(stream.request_index());
237 if (index < request_streams_.size()) {
238 return request_streams_[index];
240 return 1U + 2U *
static_cast<std::uint32_t
>(index % 0x3fffffffU);
242 case StreamRef::kPushIndex: {
243 const std::size_t index =
static_cast<std::size_t
>(stream.push_index());
244 if (index < pushed_streams_.size()) {
245 return pushed_streams_[index];
247 return 2U + 2U *
static_cast<std::uint32_t
>(index % 0x3fffffffU);
249 case StreamRef::kExplicitId:
250 return stream.explicit_id() & 0x7fffffffU;
251 case StreamRef::kLatestRequest:
252 case StreamRef::TARGET_NOT_SET:
254 return request_streams_.empty() ? 1U : request_streams_.back();
258std::uint32_t H2PlanDriver::ResolvePromisedStream(
const curl::fuzzer::proto::Http2StreamRef& stream) {
259 std::uint32_t stream_id = 0;
260 if (stream.target_case() == curl::fuzzer::proto::Http2StreamRef::TARGET_NOT_SET ||
261 stream.target_case() == curl::fuzzer::proto::Http2StreamRef::kLatestRequest ||
262 stream.target_case() == curl::fuzzer::proto::Http2StreamRef::kConnection) {
263 stream_id = 2U + 2U *
static_cast<std::uint32_t
>(pushed_streams_.size());
265 stream_id = ResolveStream(stream,
false);
267 if (std::find(pushed_streams_.begin(), pushed_streams_.end(), stream_id) == pushed_streams_.end()) {
268 pushed_streams_.push_back(stream_id);
273bool H2PlanDriver::WaitSatisfied(
const curl::fuzzer::proto::Http2Wait& wait)
const {
274 const std::size_t required = std::max<std::size_t>(1, std::min<std::size_t>(wait.count(), 16));
275 if (wait.event() == curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS) {
276 return observed_client_settings_ >= required;
278 const std::uint32_t stream_id = ResolveStream(wait.stream(),
false);
279 const auto found = client_streams_.find(stream_id);
280 if (found == client_streams_.end()) {
283 switch (wait.event()) {
284 case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_HEADERS:
285 return found->second.headers >= required;
286 case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_DATA:
287 return found->second.data >= required;
288 case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_END_STREAM:
289 return found->second.end_stream >= required;
290 case curl::fuzzer::proto::HTTP2_CLIENT_EVENT_SETTINGS:
296bool H2PlanDriver::EncodeHeaders(
const std::vector<std::pair<std::string, std::string>>& headers, std::string* output) {
297 if (deflater_ ==
nullptr || output ==
nullptr) {
300 std::vector<nghttp2_nv> name_values;
301 name_values.reserve(headers.size());
302 for (
const auto& header : headers) {
304 nv.name =
reinterpret_cast<std::uint8_t*
>(
const_cast<char*
>(header.first.data()));
305 nv.value =
reinterpret_cast<std::uint8_t*
>(
const_cast<char*
>(header.second.data()));
306 nv.namelen = header.first.size();
307 nv.valuelen = header.second.size();
308 nv.flags = NGHTTP2_NV_FLAG_NONE;
309 name_values.push_back(nv);
311 const std::size_t bound = nghttp2_hd_deflate_bound(deflater_, name_values.data(), name_values.size());
312 if (bound > scenario_limits::kMaxHttp2HeaderBytes * 2U) {
315 output->resize(bound);
316 const nghttp2_ssize encoded = nghttp2_hd_deflate_hd2(deflater_,
reinterpret_cast<std::uint8_t*
>(output->data()),
317 output->size(), name_values.data(), name_values.size());
322 output->resize(
static_cast<std::size_t
>(encoded));
326bool H2PlanDriver::SerializeAction(
const curl::fuzzer::proto::Http2Action& action, std::string* output) {
327 using Action = curl::fuzzer::proto::Http2Action;
329 switch (action.action_case()) {
330 case Action::kSettings: {
332 const auto& settings = action.settings();
333 if (!settings.ack()) {
334 const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxHttp2Settings,
335 static_cast<std::size_t
>(settings.entries_size()));
336 payload.reserve(count * 6U);
337 for (std::size_t index = 0; index < count; ++index) {
338 const auto& entry = settings.entries(
static_cast<int>(index));
339 const std::uint16_t identifier =
static_cast<std::uint16_t
>(entry.identifier());
340 payload.push_back(
static_cast<char>((identifier >> 8U) & 0xffU));
341 payload.push_back(
static_cast<char>(identifier & 0xffU));
342 AppendUint32(&payload, entry.value());
345 AppendFrame(output, payload.size(), kSettingsFrame, settings.ack() ? kFlagAck : 0, 0, payload);
348 case Action::kHeaders: {
349 const auto& headers = action.headers();
350 std::vector<std::pair<std::string, std::string>> fields;
351 const std::size_t count =
352 std::min<std::size_t>(scenario_limits::kMaxHttp2Headers,
static_cast<std::size_t
>(headers.fields_size()));
353 fields.reserve(count + 1U);
354 if (!headers.trailers()) {
355 fields.emplace_back(
":status", std::to_string(CanonicalStatus(headers.status_code())));
357 std::size_t bytes = 0;
358 for (std::size_t index = 0; index < count && bytes < scenario_limits::kMaxHttp2HeaderBytes; ++index) {
359 const auto& field = headers.fields(
static_cast<int>(index));
360 std::string name = CanonicalHeaderName(field.name());
361 std::string value = CanonicalHeaderValue(field.value());
362 const std::size_t remaining = scenario_limits::kMaxHttp2HeaderBytes - bytes;
363 if (name.size() + value.size() > remaining) {
364 value.resize(remaining > name.size() ? remaining - name.size() : 0);
365 name.resize(std::min(name.size(), remaining));
367 bytes += name.size() + value.size();
368 fields.emplace_back(std::move(name), std::move(value));
371 if (!EncodeHeaders(fields, &block)) {
374 const std::uint32_t stream_id = ResolveStream(headers.stream(),
false);
375 std::size_t offset = 0;
377 const std::size_t chunk_size = std::min(kMaxFramePayload, block.size() - offset);
378 const bool last = offset + chunk_size == block.size();
379 std::uint8_t flags = last ? kFlagEndHeaders : 0;
380 if (offset == 0 && headers.end_stream()) {
381 flags |= kFlagEndStream;
383 AppendFrame(output, chunk_size, offset == 0 ? kHeadersFrame : kContinuationFrame, flags, stream_id,
384 std::string_view(block).substr(offset, chunk_size));
385 offset += chunk_size;
386 }
while (offset < block.size());
389 case Action::kData: {
390 const auto& data = action.data();
391 const std::string payload = BoundedString(data.data(), scenario_limits::kMaxHttp2DataBytes);
392 const std::uint32_t stream_id = ResolveStream(data.stream(),
false);
393 std::size_t offset = 0;
395 const std::size_t chunk_size = std::min(kMaxFramePayload, payload.size() - offset);
396 const bool last = offset + chunk_size == payload.size();
397 AppendFrame(output, chunk_size, kDataFrame, last && data.end_stream() ? kFlagEndStream : 0, stream_id,
398 std::string_view(payload).substr(offset, chunk_size));
399 offset += chunk_size;
400 }
while (offset < payload.size());
403 case Action::kWindowUpdate: {
404 const auto& update = action.window_update();
406 AppendUint32(&payload, std::max(1U, update.increment() & 0x7fffffffU));
407 AppendFrame(output, payload.size(), kWindowUpdateFrame, 0, ResolveStream(update.stream(),
true), payload);
410 case Action::kRstStream: {
411 const auto& reset = action.rst_stream();
413 AppendUint32(&payload, reset.error_code());
414 AppendFrame(output, payload.size(), kRstStreamFrame, 0, ResolveStream(reset.stream(),
false), payload);
417 case Action::kPing: {
418 std::array<char, 8> opaque{};
419 const std::string& source = action.ping().opaque_data();
420 std::copy_n(source.begin(), std::min(source.size(), opaque.size()), opaque.begin());
421 AppendFrame(output, opaque.size(), kPingFrame, action.ping().ack() ? kFlagAck : 0, 0,
422 std::string_view(opaque.data(), opaque.size()));
425 case Action::kGoaway: {
426 const auto& goaway = action.goaway();
428 AppendUint32(&payload, ResolveStream(goaway.last_stream(),
true));
429 AppendUint32(&payload, goaway.error_code());
430 payload.append(BoundedString(goaway.debug_data(), scenario_limits::kMaxHttp2RawFrameBytes));
431 AppendFrame(output, payload.size(), kGoawayFrame, 0, 0, payload);
434 case Action::kPushPromise: {
435 const auto& promise = action.push_promise();
436 std::vector<std::pair<std::string, std::string>> fields = {
437 {
":method",
"GET"}, {
":scheme", scheme_}, {
":authority", authority_}, {
":path",
"/push"}};
438 const std::size_t count = std::min<std::size_t>(scenario_limits::kMaxHttp2Headers,
439 static_cast<std::size_t
>(promise.request_headers_size()));
440 for (std::size_t index = 0; index < count; ++index) {
441 const auto& field = promise.request_headers(
static_cast<int>(index));
442 fields.emplace_back(CanonicalHeaderName(field.name()), CanonicalHeaderValue(field.value()));
445 if (!EncodeHeaders(fields, &block)) {
448 const std::uint32_t parent = ResolveStream(promise.parent_stream(),
false);
449 const std::uint32_t promised = ResolvePromisedStream(promise.promised_stream());
450 std::string first_payload;
451 AppendUint32(&first_payload, promised);
452 const std::size_t first_size = std::min(kMaxFramePayload - 4U, block.size());
453 first_payload.append(block.data(), first_size);
454 AppendFrame(output, first_payload.size(), kPushPromiseFrame, first_size == block.size() ? kFlagEndHeaders : 0,
455 parent, first_payload);
456 std::size_t offset = first_size;
457 while (offset < block.size()) {
458 const std::size_t chunk_size = std::min(kMaxFramePayload, block.size() - offset);
459 offset += chunk_size;
460 AppendFrame(output, chunk_size, kContinuationFrame, offset == block.size() ? kFlagEndHeaders : 0, parent,
461 std::string_view(block).substr(offset - chunk_size, chunk_size));
465 case Action::kWireFrame: {
466 const auto& frame = action.wire_frame();
467 const std::string payload = BoundedString(frame.payload(), scenario_limits::kMaxHttp2RawFrameBytes);
468 const std::uint32_t declared = frame.use_declared_length() ? frame.declared_length() : payload.size();
469 AppendFrame(output, declared,
static_cast<std::uint8_t
>(frame.type()),
static_cast<std::uint8_t
>(frame.flags()),
470 frame.stream_id(), payload);
473 case Action::kRawBytes:
474 *output = BoundedString(action.raw_bytes(), scenario_limits::kMaxHttp2RawFrameBytes);
477 case Action::kYieldTurns:
478 case Action::ACTION_NOT_SET:
490 if (output ==
nullptr || plan_ ==
nullptr) {
494 if (!initial_settings_sent_) {
495 curl::fuzzer::proto::Http2Action initial;
496 *initial.mutable_settings() = plan_->initial_settings();
497 initial.mutable_settings()->set_ack(
false);
498 (void)SerializeAction(initial, output);
499 initial_settings_sent_ =
true;
502 if (!client_settings_ack_sent_) {
503 if (observed_client_settings_ == 0) {
506 AppendFrame(output, 0, kSettingsFrame, kFlagAck, 0, {});
507 client_settings_ack_sent_ =
true;
511 const std::size_t action_count =
513 if (next_action_ >= action_count) {
516 const auto& action = plan_->actions(
static_cast<int>(next_action_));
517 if (action.action_case() == curl::fuzzer::proto::Http2Action::kWait) {
518 if (!WaitSatisfied(action.wait())) {
524 if (action.action_case() == curl::fuzzer::proto::Http2Action::kYieldTurns) {
525 if (remaining_yields_ == 0) {
527 if (remaining_yields_ == 0) {
533 if (remaining_yields_ == 0) {
539 (void)SerializeAction(action, output);
546 if (plan_ ==
nullptr || !initial_settings_sent_ || !client_settings_ack_sent_) {
547 return plan_ !=
nullptr;
549 return next_action_ <
bool has_pending_work() const
std::size_t observed_request_count() const
void ObserveIncomingData(const unsigned char *data, std::size_t size) override
Parse client frame headers after transport decoding.
std::size_t observed_client_priority_count() const
bool NextChunk(std::string *output)
Advance one scripted boundary.
std::size_t observed_client_settings_count() const
void Reset(const curl::fuzzer::proto::Http2Plan &plan, std::string scheme="https", std::string authority="tls.test")
Reset HPACK and stream state for one connection.
Stateful serializer for structured HTTP/2 peer actions.
Shared complexity limits for structured scenarios.
constexpr std::size_t kMaxHttp2Actions
Ordered H2 work can express startup, barriers, informational responses, flow-control changes,...
constexpr std::size_t kMaxHttp2YieldTurns
Sixteen turns preserve meaningful scheduler boundaries without letting a single scalar consume the co...
constexpr std::size_t kMaxHttp2ObservedBytes
Client tracking only needs frame headers and one maximum ordinary payload.
constexpr std::size_t kMaxHttp3HeaderNameBytes
Header names are tokens rather than general metadata.
constexpr std::size_t kMaxMetadataBytes
Metadata reaches NUL-terminated APIs, where giant suffixes add no coverage.