Stream Limit Violation Uses the Wrong Transport Error
Summary
quiche detects peer-created stream IDs that exceed the stream limit it has advertised, and it closes the connection. However, the close path uses the internal error QUIC_INVALID_STREAM_ID, which is translated to the IETF transport error PROTOCOL_VIOLATION.
RFC 9000 requires this specific condition to be treated as a connection error of type STREAM_LIMIT_ERROR. Therefore, the enforcement behavior exists, but the wire-level transport error code is not compliant.
Standard Requirement
Official standard: RFC 9000 Section 4.6, Controlling Concurrency
Endpoints MUST NOT exceed the limit set by their peer. An endpoint that receives a frame with a stream ID exceeding the limit it has sent MUST treat this as a connection error of type STREAM_LIMIT_ERROR; see Section 11 for details on error handling.
RFC 9000 Section 20.1 defines STREAM_LIMIT_ERROR as QUIC transport error code 0x04.
The requirement is not just to reject the frame or close the connection. For a frame whose stream ID exceeds the stream limit sent by the receiver, the receiver must use the STREAM_LIMIT_ERROR transport error.
Relevant Source Code
The stream ID manager checks peer-created stream IDs against the stream limit that quiche has advertised to the peer:
Source: quiche-main/quiche-main/quiche/quic/core/quic_stream_id_manager.cc:191-200
if (incoming_stream_count_ + stream_count_increment >
incoming_advertised_max_streams_) {
QUIC_DLOG(INFO) << ENDPOINT
<< "Failed to create a new incoming stream with id:"
<< stream_id << ", reaching MAX_STREAMS limit: "
<< incoming_advertised_max_streams_ << ".";
*error_details = absl::StrCat("Stream id ", stream_id,
" would exceed stream count limit ",
incoming_advertised_max_streams_);
return false;
}
incoming_advertised_max_streams_ is the stream limit quiche sends to the peer in MAX_STREAMS frames:
Source: quiche-main/quiche-main/quiche/quic/core/quic_stream_id_manager.cc:117-121
void QuicStreamIdManager::SendMaxStreamsFrame() {
QUIC_BUG_IF(quic_bug_12413_2,
incoming_advertised_max_streams_ >= incoming_actual_max_streams_);
incoming_advertised_max_streams_ = incoming_actual_max_streams_;
delegate_->SendMaxStreams(incoming_advertised_max_streams_, unidirectional_);
}
Source: quiche-main/quiche-main/quiche/quic/core/quic_session.cc:1222-1229
void QuicSession::SendMaxStreams(QuicStreamCount stream_count,
bool unidirectional) {
if (!is_configured_) {
QUIC_BUG(quic_bug_10866_5)
<< "Try to send max streams before config negotiated.";
return;
}
control_frame_manager_.WriteOrBufferMaxStreams(stream_count, unidirectional);
}
When the stream-limit check fails, the IETF QUIC session closes the connection with QUIC_INVALID_STREAM_ID:
Source: quiche-main/quiche-main/quiche/quic/core/quic_session.cc:2296-2307
bool QuicSession::MaybeIncreaseLargestPeerStreamId(
const QuicStreamId stream_id) {
if (VersionIsIetfQuic(transport_version())) {
std::string error_details;
if (ietf_streamid_manager_.MaybeIncreaseLargestPeerStreamId(
stream_id, &error_details)) {
return true;
}
connection()->CloseConnection(
QUIC_INVALID_STREAM_ID, error_details,
ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
return false;
}
The three-argument CloseConnection overload does not provide an explicit IETF transport error override:
Source: quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:4769-4773
void QuicConnection::CloseConnection(
QuicErrorCode error, const std::string& details,
ConnectionCloseBehavior connection_close_behavior) {
CloseConnection(error, NO_IETF_QUIC_ERROR, details,
connection_close_behavior);
}
The connection-close frame therefore uses the default internal-to-IETF error mapping:
Source: quiche-main/quiche-main/quiche/quic/core/frames/quic_connection_close_frame.cc:28-34
QuicErrorCodeToIetfMapping mapping =
QuicErrorCodeToTransportErrorCode(error_code);
if (ietf_error != NO_IETF_QUIC_ERROR) {
wire_error_code = ietf_error;
} else {
wire_error_code = mapping.error_code;
}
QUIC_INVALID_STREAM_ID maps to PROTOCOL_VIOLATION, not STREAM_LIMIT_ERROR:
Source: quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:410-411
case QUIC_INVALID_STREAM_ID:
return {true, static_cast<uint64_t>(PROTOCOL_VIOLATION)};
Implementation Behavior
For IETF QUIC, receiving a frame that attempts to create a peer-initiated stream beyond incoming_advertised_max_streams_ follows this path:
QuicStreamIdManager::MaybeIncreaseLargestPeerStreamId() detects that the peer-created stream count would exceed the advertised limit.
- The function returns
false with an error detail such as Stream id 400 would exceed stream count limit 100.
QuicSession::MaybeIncreaseLargestPeerStreamId() closes the connection with internal error QUIC_INVALID_STREAM_ID.
QuicConnectionCloseFrame has no explicit IETF transport error override, so it maps the internal error through QuicErrorCodeToTransportErrorCode().
- The wire transport error becomes
PROTOCOL_VIOLATION, because QUIC_INVALID_STREAM_ID maps to PROTOCOL_VIOLATION.
The source tree does define the IETF enum value STREAM_LIMIT_ERROR, but no reviewed close path for this stream-limit violation passes STREAM_LIMIT_ERROR as the explicit IETF transport error.
Inconsistency Reason
The standard requires a specific transport error: STREAM_LIMIT_ERROR.
The implementation detects the violation and closes the connection, but it uses QUIC_INVALID_STREAM_ID. In the IETF QUIC close-frame path, that internal error is translated to PROTOCOL_VIOLATION.
Because the emitted IETF transport error is PROTOCOL_VIOLATION instead of STREAM_LIMIT_ERROR, the behavior is inconsistent with RFC 9000 Section 4.6 for this specific stream-limit violation.
Test Evidence
The existing unit tests also encode the current internal error behavior. For a stream ID above the advertised limit, NewStreamIdAboveLimit expects CloseConnection(QUIC_INVALID_STREAM_ID, ...):
Source: quiche-main/quiche-main/quiche/quic/core/quic_session_test.cc:2956-2987
// Close the connection if the id exceeds the limit.
TEST_P(QuicSessionTestServer, NewStreamIdAboveLimit) {
if (!VersionIsIetfQuic(transport_version())) {
return;
}
QuicStreamId bidirectional_stream_id = StreamCountToId(
QuicSessionPeer::ietf_streamid_manager(&session_)
->advertised_max_incoming_bidirectional_streams() +
1,
Perspective::IS_CLIENT, /*bidirectional=*/true);
QuicStreamFrame bidirectional_stream_frame(bidirectional_stream_id, false, 0,
"Random String");
EXPECT_CALL(
*connection_,
CloseConnection(QUIC_INVALID_STREAM_ID,
"Stream id 400 would exceed stream count limit 100", _));
session_.OnStreamFrame(bidirectional_stream_frame);
This confirms that the tested behavior is connection close with QUIC_INVALID_STREAM_ID; the test does not assert STREAM_LIMIT_ERROR.
Additional static search found no CloseConnection(... STREAM_LIMIT_ERROR ...), OnStreamError(... STREAM_LIMIT_ERROR ...), or SendConnectionClosePacket(... STREAM_LIMIT_ERROR ...) override for this stream-limit violation path.
Impact
Peers receive a QUIC transport close, so the connection is terminated. However, the peer observes the wrong IETF transport error code. This can reduce interoperability diagnostics, break conformance tests that expect STREAM_LIMIT_ERROR, and make stream-limit violations indistinguishable from generic protocol violations on the wire.
Fix Direction
When MaybeIncreaseLargestPeerStreamId() fails because the peer-created stream ID exceeds the advertised stream limit, the IETF QUIC close path should send STREAM_LIMIT_ERROR as the explicit transport error.
One direct fix direction is to change the IETF branch in QuicSession::MaybeIncreaseLargestPeerStreamId() to call the four-argument close overload with an explicit IETF error:
connection()->CloseConnection(
QUIC_INVALID_STREAM_ID, STREAM_LIMIT_ERROR, error_details,
ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
Alternatively, quiche could introduce a dedicated internal error for stream-limit violations and map it to STREAM_LIMIT_ERROR, but the fix must preserve the specific RFC 9000 wire error code for this condition.
Stream Limit Violation Uses the Wrong Transport Error
Summary
quiche detects peer-created stream IDs that exceed the stream limit it has advertised, and it closes the connection. However, the close path uses the internal error
QUIC_INVALID_STREAM_ID, which is translated to the IETF transport errorPROTOCOL_VIOLATION.RFC 9000 requires this specific condition to be treated as a connection error of type
STREAM_LIMIT_ERROR. Therefore, the enforcement behavior exists, but the wire-level transport error code is not compliant.Standard Requirement
Official standard: RFC 9000 Section 4.6, Controlling Concurrency
RFC 9000 Section 20.1 defines
STREAM_LIMIT_ERRORas QUIC transport error code0x04.The requirement is not just to reject the frame or close the connection. For a frame whose stream ID exceeds the stream limit sent by the receiver, the receiver must use the
STREAM_LIMIT_ERRORtransport error.Relevant Source Code
The stream ID manager checks peer-created stream IDs against the stream limit that quiche has advertised to the peer:
Source:
quiche-main/quiche-main/quiche/quic/core/quic_stream_id_manager.cc:191-200incoming_advertised_max_streams_is the stream limit quiche sends to the peer inMAX_STREAMSframes:Source:
quiche-main/quiche-main/quiche/quic/core/quic_stream_id_manager.cc:117-121Source:
quiche-main/quiche-main/quiche/quic/core/quic_session.cc:1222-1229When the stream-limit check fails, the IETF QUIC session closes the connection with
QUIC_INVALID_STREAM_ID:Source:
quiche-main/quiche-main/quiche/quic/core/quic_session.cc:2296-2307The three-argument
CloseConnectionoverload does not provide an explicit IETF transport error override:Source:
quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:4769-4773The connection-close frame therefore uses the default internal-to-IETF error mapping:
Source:
quiche-main/quiche-main/quiche/quic/core/frames/quic_connection_close_frame.cc:28-34QuicErrorCodeToIetfMapping mapping = QuicErrorCodeToTransportErrorCode(error_code); if (ietf_error != NO_IETF_QUIC_ERROR) { wire_error_code = ietf_error; } else { wire_error_code = mapping.error_code; }QUIC_INVALID_STREAM_IDmaps toPROTOCOL_VIOLATION, notSTREAM_LIMIT_ERROR:Source:
quiche-main/quiche-main/quiche/quic/core/quic_error_codes.cc:410-411Implementation Behavior
For IETF QUIC, receiving a frame that attempts to create a peer-initiated stream beyond
incoming_advertised_max_streams_follows this path:QuicStreamIdManager::MaybeIncreaseLargestPeerStreamId()detects that the peer-created stream count would exceed the advertised limit.falsewith an error detail such asStream id 400 would exceed stream count limit 100.QuicSession::MaybeIncreaseLargestPeerStreamId()closes the connection with internal errorQUIC_INVALID_STREAM_ID.QuicConnectionCloseFramehas no explicit IETF transport error override, so it maps the internal error throughQuicErrorCodeToTransportErrorCode().PROTOCOL_VIOLATION, becauseQUIC_INVALID_STREAM_IDmaps toPROTOCOL_VIOLATION.The source tree does define the IETF enum value
STREAM_LIMIT_ERROR, but no reviewed close path for this stream-limit violation passesSTREAM_LIMIT_ERRORas the explicit IETF transport error.Inconsistency Reason
The standard requires a specific transport error:
STREAM_LIMIT_ERROR.The implementation detects the violation and closes the connection, but it uses
QUIC_INVALID_STREAM_ID. In the IETF QUIC close-frame path, that internal error is translated toPROTOCOL_VIOLATION.Because the emitted IETF transport error is
PROTOCOL_VIOLATIONinstead ofSTREAM_LIMIT_ERROR, the behavior is inconsistent with RFC 9000 Section 4.6 for this specific stream-limit violation.Test Evidence
The existing unit tests also encode the current internal error behavior. For a stream ID above the advertised limit,
NewStreamIdAboveLimitexpectsCloseConnection(QUIC_INVALID_STREAM_ID, ...):Source:
quiche-main/quiche-main/quiche/quic/core/quic_session_test.cc:2956-2987This confirms that the tested behavior is connection close with
QUIC_INVALID_STREAM_ID; the test does not assertSTREAM_LIMIT_ERROR.Additional static search found no
CloseConnection(... STREAM_LIMIT_ERROR ...),OnStreamError(... STREAM_LIMIT_ERROR ...), orSendConnectionClosePacket(... STREAM_LIMIT_ERROR ...)override for this stream-limit violation path.Impact
Peers receive a QUIC transport close, so the connection is terminated. However, the peer observes the wrong IETF transport error code. This can reduce interoperability diagnostics, break conformance tests that expect
STREAM_LIMIT_ERROR, and make stream-limit violations indistinguishable from generic protocol violations on the wire.Fix Direction
When
MaybeIncreaseLargestPeerStreamId()fails because the peer-created stream ID exceeds the advertised stream limit, the IETF QUIC close path should sendSTREAM_LIMIT_ERRORas the explicit transport error.One direct fix direction is to change the IETF branch in
QuicSession::MaybeIncreaseLargestPeerStreamId()to call the four-argument close overload with an explicit IETF error:connection()->CloseConnection( QUIC_INVALID_STREAM_ID, STREAM_LIMIT_ERROR, error_details, ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);Alternatively, quiche could introduce a dedicated internal error for stream-limit violations and map it to
STREAM_LIMIT_ERROR, but the fix must preserve the specific RFC 9000 wire error code for this condition.