curl-fuzzer C++ reference
Loading...
Searching...
No Matches
h2_plan.cc
Go to the documentation of this file.
1/*
2 * Copyright (C) Max Dymond, <cmeister2@gmail.com>, et al.
3 *
4 * SPDX-License-Identifier: curl
5 */
6
9
11
12#include <nghttp2/nghttp2.h>
13
14#include <algorithm>
15#include <array>
16#include <cstddef>
17#include <cstdint>
18#include <string>
19#include <string_view>
20#include <utility>
21#include <vector>
22
24
25namespace proto_fuzzer {
26
27namespace {
28
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;
32
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;
43
44constexpr std::uint8_t kFlagAck = 0x1;
45constexpr std::uint8_t kFlagEndStream = 0x1;
46constexpr std::uint8_t kFlagEndHeaders = 0x4;
47
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));
53}
54
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]);
59}
60
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());
71}
72
73std::uint32_t CanonicalStatus(std::uint32_t status) {
74 if (status >= 100U && status <= 599U) {
75 return status;
76 }
77 return status == 0U ? 200U : 100U + (status % 500U);
78}
79
80std::string BoundedString(const std::string& input, std::size_t limit) { return input.substr(0, limit); }
81
82std::string CanonicalHeaderName(const std::string& input) {
83 std::string name = BoundedString(input, scenario_limits::kMaxHttp3HeaderNameBytes);
84 if (name.empty()) {
85 return "x-fuzz";
86 }
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) {
97 byte = '-';
98 }
99 }
100 return name;
101}
102
103std::string CanonicalHeaderValue(const std::string& input) {
104 std::string value = BoundedString(input, scenario_limits::kMaxMetadataBytes);
105 for (char& byte : value) {
106 const unsigned char character = static_cast<unsigned char>(byte);
107 if ((character < 0x20U && character != '\t') || character == 0x7fU) {
108 byte = ' ';
109 }
110 }
111 return value;
112}
113
114} // namespace
115
116H2PlanDriver::H2PlanDriver()
117 : plan_(nullptr),
118 deflater_(nullptr),
119 next_action_(0),
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);
127}
128
129H2PlanDriver::~H2PlanDriver() { nghttp2_hd_deflate_del(deflater_); }
130
135void H2PlanDriver::Reset(const curl::fuzzer::proto::Http2Plan& plan, std::string scheme, std::string authority) {
136 plan_ = &plan;
137 next_action_ = 0;
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_);
151 deflater_ = nullptr;
152 (void)nghttp2_hd_deflate_new(&deflater_, 4096);
153}
154
158void H2PlanDriver::ObserveIncomingData(const unsigned char* data, std::size_t size) {
159 if (data == nullptr || size == 0) {
160 return;
161 }
163 client_bytes_.size() > scenario_limits::kMaxHttp2ObservedBytes - size) {
164 client_bytes_.clear();
165 expect_client_preface_ = false;
166 return;
167 }
168 client_bytes_.append(reinterpret_cast<const char*>(data), size);
169 ParseClientFrames();
170}
171
172void H2PlanDriver::ParseClientFrames() {
173 if (expect_client_preface_) {
174 if (client_bytes_.size() < kClientPreface.size()) {
175 return;
176 }
177 if (std::string_view(client_bytes_).substr(0, kClientPreface.size()) == kClientPreface) {
178 client_bytes_.erase(0, kClientPreface.size());
179 }
180 expect_client_preface_ = false;
181 }
182
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) {
190 break;
191 }
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;
197 }
198 client_bytes_.erase(0, consumed);
199}
200
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_;
204 return;
205 }
206 if (stream_id == 0) {
207 return;
208 }
209
210 if (type == kPriorityFrame) {
211 ++observed_client_priority_;
212 }
213
214 auto& stream = client_streams_[stream_id];
215 if (type == kHeadersFrame) {
216 ++stream.headers;
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);
220 }
221 } else if (type == kDataFrame) {
222 ++stream.data;
223 }
224 if ((type == kHeadersFrame || type == kDataFrame) && (flags & kFlagEndStream) != 0) {
225 ++stream.end_stream;
226 }
227}
228
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];
239 }
240 return 1U + 2U * static_cast<std::uint32_t>(index % 0x3fffffffU);
241 }
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];
246 }
247 return 2U + 2U * static_cast<std::uint32_t>(index % 0x3fffffffU);
248 }
249 case StreamRef::kExplicitId:
250 return stream.explicit_id() & 0x7fffffffU;
251 case StreamRef::kLatestRequest:
252 case StreamRef::TARGET_NOT_SET:
253 default:
254 return request_streams_.empty() ? 1U : request_streams_.back();
255 }
256}
257
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());
264 } else {
265 stream_id = ResolveStream(stream, false);
266 }
267 if (std::find(pushed_streams_.begin(), pushed_streams_.end(), stream_id) == pushed_streams_.end()) {
268 pushed_streams_.push_back(stream_id);
269 }
270 return stream_id;
271}
272
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;
277 }
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()) {
281 return false;
282 }
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:
291 default:
292 return false;
293 }
294}
295
296bool H2PlanDriver::EncodeHeaders(const std::vector<std::pair<std::string, std::string>>& headers, std::string* output) {
297 if (deflater_ == nullptr || output == nullptr) {
298 return false;
299 }
300 std::vector<nghttp2_nv> name_values;
301 name_values.reserve(headers.size());
302 for (const auto& header : headers) {
303 nghttp2_nv nv{};
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);
310 }
311 const std::size_t bound = nghttp2_hd_deflate_bound(deflater_, name_values.data(), name_values.size());
312 if (bound > scenario_limits::kMaxHttp2HeaderBytes * 2U) {
313 return false;
314 }
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());
318 if (encoded < 0) {
319 output->clear();
320 return false;
321 }
322 output->resize(static_cast<std::size_t>(encoded));
323 return true;
324}
325
326bool H2PlanDriver::SerializeAction(const curl::fuzzer::proto::Http2Action& action, std::string* output) {
327 using Action = curl::fuzzer::proto::Http2Action;
328 output->clear();
329 switch (action.action_case()) {
330 case Action::kSettings: {
331 std::string payload;
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());
343 }
344 }
345 AppendFrame(output, payload.size(), kSettingsFrame, settings.ack() ? kFlagAck : 0, 0, payload);
346 return true;
347 }
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())));
356 }
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));
366 }
367 bytes += name.size() + value.size();
368 fields.emplace_back(std::move(name), std::move(value));
369 }
370 std::string block;
371 if (!EncodeHeaders(fields, &block)) {
372 return false;
373 }
374 const std::uint32_t stream_id = ResolveStream(headers.stream(), false);
375 std::size_t offset = 0;
376 do {
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;
382 }
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());
387 return true;
388 }
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;
394 do {
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());
401 return true;
402 }
403 case Action::kWindowUpdate: {
404 const auto& update = action.window_update();
405 std::string payload;
406 AppendUint32(&payload, std::max(1U, update.increment() & 0x7fffffffU));
407 AppendFrame(output, payload.size(), kWindowUpdateFrame, 0, ResolveStream(update.stream(), true), payload);
408 return true;
409 }
410 case Action::kRstStream: {
411 const auto& reset = action.rst_stream();
412 std::string payload;
413 AppendUint32(&payload, reset.error_code());
414 AppendFrame(output, payload.size(), kRstStreamFrame, 0, ResolveStream(reset.stream(), false), payload);
415 return true;
416 }
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()));
423 return true;
424 }
425 case Action::kGoaway: {
426 const auto& goaway = action.goaway();
427 std::string payload;
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);
432 return true;
433 }
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()));
443 }
444 std::string block;
445 if (!EncodeHeaders(fields, &block)) {
446 return false;
447 }
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));
462 }
463 return true;
464 }
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);
471 return true;
472 }
473 case Action::kRawBytes:
474 *output = BoundedString(action.raw_bytes(), scenario_limits::kMaxHttp2RawFrameBytes);
475 return true;
476 case Action::kWait:
477 case Action::kYieldTurns:
478 case Action::ACTION_NOT_SET:
479 default:
480 return false;
481 }
482}
483
489bool H2PlanDriver::NextChunk(std::string* output) {
490 if (output == nullptr || plan_ == nullptr) {
491 return false;
492 }
493 output->clear();
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;
500 return true;
501 }
502 if (!client_settings_ack_sent_) {
503 if (observed_client_settings_ == 0) {
504 return false;
505 }
506 AppendFrame(output, 0, kSettingsFrame, kFlagAck, 0, {});
507 client_settings_ack_sent_ = true;
508 return true;
509 }
510
511 const std::size_t action_count =
512 std::min<std::size_t>(scenario_limits::kMaxHttp2Actions, static_cast<std::size_t>(plan_->actions_size()));
513 if (next_action_ >= action_count) {
514 return false;
515 }
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())) {
519 return false;
520 }
521 ++next_action_;
522 return true;
523 }
524 if (action.action_case() == curl::fuzzer::proto::Http2Action::kYieldTurns) {
525 if (remaining_yields_ == 0) {
526 remaining_yields_ = std::min<std::size_t>(scenario_limits::kMaxHttp2YieldTurns, action.yield_turns());
527 if (remaining_yields_ == 0) {
528 ++next_action_;
529 return true;
530 }
531 }
532 --remaining_yields_;
533 if (remaining_yields_ == 0) {
534 ++next_action_;
535 }
536 return true;
537 }
538
539 (void)SerializeAction(action, output);
540 ++next_action_;
541 return true;
542}
543
546 if (plan_ == nullptr || !initial_settings_sent_ || !client_settings_ack_sent_) {
547 return plan_ != nullptr;
548 }
549 return next_action_ <
550 std::min<std::size_t>(scenario_limits::kMaxHttp2Actions, static_cast<std::size_t>(plan_->actions_size()));
551}
552
554std::size_t H2PlanDriver::observed_request_count() const { return request_streams_.size(); }
555
557std::size_t H2PlanDriver::observed_client_settings_count() const { return observed_client_settings_; }
558
560std::size_t H2PlanDriver::observed_client_priority_count() const { return observed_client_priority_; }
561
562} // namespace proto_fuzzer
bool has_pending_work() const
Definition h2_plan.cc:545
std::size_t observed_request_count() const
Definition h2_plan.cc:554
void ObserveIncomingData(const unsigned char *data, std::size_t size) override
Parse client frame headers after transport decoding.
Definition h2_plan.cc:158
std::size_t observed_client_priority_count() const
Definition h2_plan.cc:560
bool NextChunk(std::string *output)
Advance one scripted boundary.
Definition h2_plan.cc:489
std::size_t observed_client_settings_count() const
Definition h2_plan.cc:557
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.
Definition h2_plan.cc:135
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.