Stream ID wire range is capped by a 32-bit implementation limit
Summary
quiche supports IETF QUIC stream IDs and stream counts only within a 32-bit internal range. That implementation choice is not itself forbidden, because an endpoint can advertise stream limits. The inconsistency is that quiche applies the 32-bit limit while parsing wire-format varint62 stream IDs/counts, so some RFC-valid encoded values are rejected before the implementation can apply the correct stream-limit or frame-specific error semantics.
Standard Requirement
- RFC 9000 Section 2.1, Stream Types and Identifiers
- RFC 9000 Section 4.6, Controlling Concurrency
- RFC 9000 Section 19.8, STREAM Frames
- RFC 9000 Section 19.11, MAX_STREAMS Frames
- Link: https://www.rfc-editor.org/rfc/rfc9000.html
RFC 9000 Section 2.1 states:
Streams are identified within a connection by a numeric value, referred to as the stream ID. A stream ID is a 62-bit integer (0 to 2^62-1) that is unique for all streams on a connection. Stream IDs are encoded as variable-length integers; see Section 16. A QUIC endpoint MUST NOT reuse a stream ID within a connection.
RFC 9000 Section 4.6 also requires a specific error when a peer opens too many streams:
An endpoint MUST terminate a connection with an error of type STREAM_LIMIT_ERROR if a peer opens more streams than was permitted.
RFC 9000 Section 19.11 permits MAX_STREAMS values up to the point where the resulting stream ID would still fit in the 62-bit stream-ID space. Values greater than 2^60 are the standard-defined invalid range for MAX_STREAMS, not values merely greater than 2^32-1.
Relevant Source Code
Source: quiche-main/quiche-main/quiche/quic/core/quic_types.h:36-44
36: // IMPORTANT: IETF QUIC defines stream IDs and stream counts as being unsigned
37: // 62-bit numbers. However, we have decided to only support up to 2^32-1 streams
38: // in order to reduce the size of data structures such as QuicStreamFrame
39: // and QuicTransmissionInfo, as that allows them to fit in cache lines and has
40: // visible perfomance impact.
41: using QuicStreamId = uint32_t;
42:
43: // Count of stream IDs. Used in MAX_STREAMS and STREAMS_BLOCKED frames.
44: using QuicStreamCount = QuicStreamId;
Source: quiche-main/quiche-main/quiche/quic/core/quic_constants.h:257-264
257: // The max value that can be encoded using IETF Var Ints.
258: inline constexpr uint64_t kMaxIetfVarInt = UINT64_C(0x3fffffffffffffff);
259:
260: // The maximum stream id value that is supported - (2^32)-1
261: inline constexpr QuicStreamId kMaxQuicStreamId = 0xffffffff;
262:
263: // The maximum value that can be stored in a 32-bit QuicStreamCount.
264: inline constexpr QuicStreamCount kMaxQuicStreamCount = 0xffffffff;
Source: quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:6402-6417
6402: bool QuicFramer::ReadUint32FromVarint62(QuicDataReader* reader,
6403: QuicIetfFrameType type,
6404: QuicStreamId* id) {
6405: uint64_t temp_uint64;
6406: if (!reader->ReadVarInt62(&temp_uint64)) {
6407: set_detailed_error("Unable to read " + QuicIetfFrameTypeString(type) +
6408: " frame stream id/count.");
6409: return false;
6410: }
6411: if (temp_uint64 > kMaxQuicStreamId) {
6412: set_detailed_error("Stream id/count of " + QuicIetfFrameTypeString(type) +
6413: "frame is too large.");
6414: return false;
6415: }
6416: *id = static_cast<uint32_t>(temp_uint64);
6417: return true;
Source: quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:3341-3347
3341: bool QuicFramer::ProcessIetfStreamFrame(QuicDataReader* reader,
3342: uint8_t frame_type,
3343: QuicStreamFrame* frame) {
3344: // Read stream id from the frame. It's always present.
3345: if (!ReadUint32FromVarint62(reader, IETF_STREAM, &frame->stream_id)) {
3346: return false;
3347: }
Source: quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:2894-2898
2894: if (IS_IETF_STREAM_FRAME(frame_type)) {
2895: QuicStreamFrame frame;
2896: if (!ProcessIetfStreamFrame(reader, frame_type, &frame)) {
2897: return RaiseError(QUIC_INVALID_STREAM_DATA);
2898: }
Source: quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:368-369
368: case QUIC_INVALID_STREAM_DATA:
369: return {true, static_cast<uint64_t>(FRAME_ENCODING_ERROR)};
Source: quiche-main/quiche-main/quiche/quic/core/quic_stream_id_manager.cc:191-200
191: if (incoming_stream_count_ + stream_count_increment >
192: incoming_advertised_max_streams_) {
193: QUIC_DLOG(INFO) << ENDPOINT
194: << "Failed to create a new incoming stream with id:"
195: << stream_id << ", reaching MAX_STREAMS limit: "
196: << incoming_advertised_max_streams_ << ".";
197: *error_details = absl::StrCat("Stream id ", stream_id,
198: " would exceed stream count limit ",
199: incoming_advertised_max_streams_);
200: return false;
Implementation Behavior
quiche reads IETF QUIC stream IDs and stream counts using ReadVarInt62, so the wire decoder initially accepts the RFC varint62 format. Immediately after that, ReadUint32FromVarint62 rejects any value greater than kMaxQuicStreamId (0xffffffff) and returns a framer parse failure.
For a STREAM frame, this failure becomes QUIC_INVALID_STREAM_DATA, which maps to IETF FRAME_ENCODING_ERROR. This happens before the parsed stream ID can reach QuicStreamIdManager::MaybeIncreaseLargestPeerStreamId, where stream-count-limit enforcement is performed.
The code therefore conflates two different concepts:
- RFC wire-format validity: stream IDs are 62-bit varints.
- quiche implementation capacity: stream IDs/counts are stored in
uint32_t.
Inconsistency Reason
The RFC does not require an implementation to practically open every possible stream ID up to 2^62-1; endpoints control concurrency through transport parameters and MAX_STREAMS. However, a stream ID greater than 0xffffffff is still a syntactically valid QUIC stream ID if it is within the 62-bit varint range.
When such a stream ID represents more streams than the endpoint permitted, RFC 9000 requires the connection to be closed with STREAM_LIMIT_ERROR. quiche instead rejects the value in the framer because it exceeds a 32-bit internal representation limit, causing a parse/frame-data error path such as QUIC_INVALID_STREAM_DATA and ultimately FRAME_ENCODING_ERROR.
This is why the finding is a confirmed partial implementation: quiche supports the lower 32-bit subset of IETF QUIC stream IDs/counts, but it applies that implementation limit too early as a wire-format parsing limit.
Impact
Peers that send RFC-valid 62-bit stream IDs or stream counts above quiche's 32-bit internal limit can be rejected with frame/parsing-oriented errors instead of the standard's stream-limit semantics. This can produce incorrect transport error codes and makes quiche's accepted wire-level stream-ID range smaller than RFC 9000 defines.
Fix Direction
Separate wire parsing from implementation-limit enforcement.
The framer should preserve 62-bit stream IDs/counts long enough to distinguish:
- malformed varint encoding,
- standard-invalid values, such as
MAX_STREAMS > 2^60,
- valid stream IDs that exceed the endpoint's advertised stream limit,
- values that are valid on the wire but unsupported by the implementation.
For STREAM frames, a valid 62-bit stream ID that opens more streams than permitted should reach the stream-limit validation path and produce STREAM_LIMIT_ERROR, not FRAME_ENCODING_ERROR.
Stream ID wire range is capped by a 32-bit implementation limit
Summary
quiche supports IETF QUIC stream IDs and stream counts only within a 32-bit internal range. That implementation choice is not itself forbidden, because an endpoint can advertise stream limits. The inconsistency is that quiche applies the 32-bit limit while parsing wire-format varint62 stream IDs/counts, so some RFC-valid encoded values are rejected before the implementation can apply the correct stream-limit or frame-specific error semantics.
Standard Requirement
RFC 9000 Section 2.1 states:
RFC 9000 Section 4.6 also requires a specific error when a peer opens too many streams:
RFC 9000 Section 19.11 permits
MAX_STREAMSvalues up to the point where the resulting stream ID would still fit in the 62-bit stream-ID space. Values greater than2^60are the standard-defined invalid range forMAX_STREAMS, not values merely greater than2^32-1.Relevant Source Code
Source:
quiche-main/quiche-main/quiche/quic/core/quic_types.h:36-44Source:
quiche-main/quiche-main/quiche/quic/core/quic_constants.h:257-264Source:
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:6402-6417Source:
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:3341-3347Source:
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:2894-2898Source:
quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:368-369Source:
quiche-main/quiche-main/quiche/quic/core/quic_stream_id_manager.cc:191-200Implementation Behavior
quiche reads IETF QUIC stream IDs and stream counts using
ReadVarInt62, so the wire decoder initially accepts the RFC varint62 format. Immediately after that,ReadUint32FromVarint62rejects any value greater thankMaxQuicStreamId(0xffffffff) and returns a framer parse failure.For a
STREAMframe, this failure becomesQUIC_INVALID_STREAM_DATA, which maps to IETFFRAME_ENCODING_ERROR. This happens before the parsed stream ID can reachQuicStreamIdManager::MaybeIncreaseLargestPeerStreamId, where stream-count-limit enforcement is performed.The code therefore conflates two different concepts:
uint32_t.Inconsistency Reason
The RFC does not require an implementation to practically open every possible stream ID up to
2^62-1; endpoints control concurrency through transport parameters andMAX_STREAMS. However, a stream ID greater than0xffffffffis still a syntactically valid QUIC stream ID if it is within the 62-bit varint range.When such a stream ID represents more streams than the endpoint permitted, RFC 9000 requires the connection to be closed with
STREAM_LIMIT_ERROR. quiche instead rejects the value in the framer because it exceeds a 32-bit internal representation limit, causing a parse/frame-data error path such asQUIC_INVALID_STREAM_DATAand ultimatelyFRAME_ENCODING_ERROR.This is why the finding is a confirmed partial implementation: quiche supports the lower 32-bit subset of IETF QUIC stream IDs/counts, but it applies that implementation limit too early as a wire-format parsing limit.
Impact
Peers that send RFC-valid 62-bit stream IDs or stream counts above quiche's 32-bit internal limit can be rejected with frame/parsing-oriented errors instead of the standard's stream-limit semantics. This can produce incorrect transport error codes and makes quiche's accepted wire-level stream-ID range smaller than RFC 9000 defines.
Fix Direction
Separate wire parsing from implementation-limit enforcement.
The framer should preserve 62-bit stream IDs/counts long enough to distinguish:
MAX_STREAMS > 2^60,For
STREAMframes, a valid 62-bit stream ID that opens more streams than permitted should reach the stream-limit validation path and produceSTREAM_LIMIT_ERROR, notFRAME_ENCODING_ERROR.