Skip to content

Stream Limit Violation Uses the Wrong Transport Error #130

@LiD0209

Description

@LiD0209

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:

  1. QuicStreamIdManager::MaybeIncreaseLargestPeerStreamId() detects that the peer-created stream count would exceed the advertised limit.
  2. The function returns false with an error detail such as Stream id 400 would exceed stream count limit 100.
  3. QuicSession::MaybeIncreaseLargestPeerStreamId() closes the connection with internal error QUIC_INVALID_STREAM_ID.
  4. QuicConnectionCloseFrame has no explicit IETF transport error override, so it maps the internal error through QuicErrorCodeToTransportErrorCode().
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions