STREAM_DATA_BLOCKED on send-only streams is not rejected
Summary
quiche parses and dispatches received STREAM_DATA_BLOCKED frames, but it does not reject a STREAM_DATA_BLOCKED frame that names a send-only stream. RFC 9000 requires the receiver to terminate the connection with STREAM_STATE_ERROR in that case.
This is a confirmed protocol compliance gap, not a false positive. The receive path reaches QuicSession::OnBlockedFrame(), where the frame is only counted and logged. No stream-direction check is performed.
Standard Requirement
Relevant normative text:
An endpoint that receives a STREAM_DATA_BLOCKED frame for a send-only stream MUST terminate the connection with error STREAM_STATE_ERROR.
In protocol terms, a STREAM_DATA_BLOCKED frame is valid only for a stream on which the sender can send data. If the receiver sees this frame for a stream that is send-only from the receiver's own perspective, the peer is claiming to be blocked on a stream where it is not allowed to send data. The connection must therefore be closed with STREAM_STATE_ERROR.
In quiche's local stream-type terminology, this condition corresponds to a received STREAM_DATA_BLOCKED frame whose stream id resolves to WRITE_UNIDIRECTIONAL for the local endpoint.
Relevant Source Code
Frame parsing
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:6274
bool QuicFramer::ProcessStreamDataBlockedFrame(QuicDataReader* reader,
QuicBlockedFrame* frame) {
if (!ReadUint32FromVarint62(reader, IETF_STREAM_DATA_BLOCKED,
&frame->stream_id)) {
return false;
}
if (!reader->ReadVarInt62(&frame->offset)) {
set_detailed_error("Can not read stream blocked offset.");
return false;
}
return true;
}
The framer only extracts stream_id and offset. It does not validate whether the stream id denotes a send-only stream.
Connection-level dispatch
quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:2333
bool QuicConnection::OnBlockedFrame(const QuicBlockedFrame& frame) {
QUIC_BUG_IF(quic_bug_12714_17, !connected_)
<< "Processing BLOCKED frame when connection is closed. Received packet "
"info: "
<< last_received_packet_info_;
if (!UpdatePacketContent(BLOCKED_FRAME)) {
return false;
}
if (debug_visitor_ != nullptr) {
debug_visitor_->OnBlockedFrame(frame);
}
QUIC_DLOG(INFO) << ENDPOINT
<< "BLOCKED_FRAME received for stream: " << frame.stream_id;
MaybeUpdateAckTimeout();
visitor_->OnBlockedFrame(frame);
stats_.blocked_frames_received++;
return connected_;
}
The connection layer treats the frame as a generic blocked frame and passes it to the session visitor. It does not close the connection.
Session-level handling
quiche-main/quiche-main/quiche/quic/core/quic_session.cc:751
void QuicSession::OnBlockedFrame(const QuicBlockedFrame& frame) {
if (frame.stream_id == QuicUtils::GetInvalidStreamId(transport_version())) {
QUIC_CODE_COUNT(quic_data_blocked_frame_received);
} else {
QUIC_CODE_COUNT(quic_stream_data_blocked_frame_received);
}
// TODO(rjshade): Compare our flow control receive windows for specified
// streams: if we have a large window then maybe something
// had gone wrong with the flow control accounting.
QUIC_DLOG(INFO) << ENDPOINT << "Received BLOCKED frame with stream id: "
<< frame.stream_id << ", offset: " << frame.offset;
}
This is the relevant receive-side endpoint behavior. It records counters and logs the frame, but it does not call QuicUtils::GetStreamType(), does not check for WRITE_UNIDIRECTIONAL, and does not call CloseConnection().
Stream-type utility exists
quiche-main/quiche-main/quiche/quic/core/quic_utils.cc:380
StreamType QuicUtils::GetStreamType(QuicStreamId id, Perspective perspective,
bool peer_initiated,
ParsedQuicVersion version) {
QUICHE_DCHECK(version.IsIetfQuic());
if (IsBidirectionalStreamId(id, version)) {
return BIDIRECTIONAL;
}
if (peer_initiated) {
if (perspective == Perspective::IS_SERVER) {
QUICHE_DCHECK_EQ(2u, id % 4);
} else {
QUICHE_DCHECK_EQ(Perspective::IS_CLIENT, perspective);
QUICHE_DCHECK_EQ(3u, id % 4);
}
return READ_UNIDIRECTIONAL;
}
if (perspective == Perspective::IS_SERVER) {
QUICHE_DCHECK_EQ(3u, id % 4);
} else {
QUICHE_DCHECK_EQ(Perspective::IS_CLIENT, perspective);
QUICHE_DCHECK_EQ(2u, id % 4);
}
return WRITE_UNIDIRECTIONAL;
}
quiche has the helper needed to classify stream direction, but OnBlockedFrame() does not use it.
Comparable direction checks exist for other frames
quiche-main/quiche-main/quiche/quic/core/quic_session.cc:727
if (VersionIsIetfQuic(transport_version()) &&
QuicUtils::GetStreamType(stream_id, perspective(),
IsIncomingStream(stream_id),
version()) == READ_UNIDIRECTIONAL) {
connection()->CloseConnection(
QUIC_WINDOW_UPDATE_RECEIVED_ON_READ_UNIDIRECTIONAL_STREAM,
"WindowUpdateFrame received on READ_UNIDIRECTIONAL stream.",
ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
return;
}
This shows that quiche already performs stream-direction validation for similar stream-flow-control frame handling. The missing check is specific to received STREAM_DATA_BLOCKED.
Implementation Behavior
The receive path for STREAM_DATA_BLOCKED is:
QuicFramer::ProcessStreamDataBlockedFrame() parses stream_id and offset.
QuicConnection::OnBlockedFrame() dispatches the frame to the session visitor.
QuicSession::OnBlockedFrame() increments a counter and logs the frame.
- The connection remains open unless another unrelated error occurs.
No code path was found that rejects STREAM_DATA_BLOCKED based on the target stream being send-only. Searches for OnBlockedFrame(), STREAM_DATA_BLOCKED, WRITE_UNIDIRECTIONAL, READ_UNIDIRECTIONAL, and GetStreamType() show no receive-side branch that closes the connection for this condition.
Inconsistency Reason
RFC 9000 requires connection termination with STREAM_STATE_ERROR when a received STREAM_DATA_BLOCKED frame targets a send-only stream.
quiche does not implement that validation. It accepts the frame through the parser and dispatch path, then only records telemetry in QuicSession::OnBlockedFrame(). Since no stream-direction check or close path exists, a peer can send this invalid frame without triggering the required STREAM_STATE_ERROR.
The earlier wording "send-only/read-unidirectional" should be read carefully. In quiche's local endpoint terminology, a stream that is send-only for the receiver is represented as WRITE_UNIDIRECTIONAL. That wording issue does not change the finding, because OnBlockedFrame() checks neither WRITE_UNIDIRECTIONAL nor READ_UNIDIRECTIONAL.
Impact
A peer can send a STREAM_DATA_BLOCKED frame for a stream where it is not allowed to send data. Instead of closing the connection with STREAM_STATE_ERROR as required by RFC 9000, quiche logs the frame and continues. This weakens protocol error enforcement and can produce behavior that differs from other QUIC implementations that enforce the mandatory close.
Fix Direction
Add a receive-side validation branch in QuicSession::OnBlockedFrame() for IETF QUIC stream-specific blocked frames.
The intended shape is:
if (VersionIsIetfQuic(transport_version()) &&
frame.stream_id != QuicUtils::GetInvalidStreamId(transport_version()) &&
QuicUtils::GetStreamType(frame.stream_id, perspective(),
IsIncomingStream(frame.stream_id),
version()) == WRITE_UNIDIRECTIONAL) {
connection()->CloseConnection(
QUIC_DATA_RECEIVED_ON_WRITE_UNIDIRECTIONAL_STREAM,
"STREAM_DATA_BLOCKED received on send-only stream.",
ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET);
return;
}
The internal error code should map to IETF STREAM_STATE_ERROR. QUIC_DATA_RECEIVED_ON_WRITE_UNIDIRECTIONAL_STREAM already maps to STREAM_STATE_ERROR, but the final choice should follow quiche's error-code conventions for frame-direction violations.
STREAM_DATA_BLOCKED on send-only streams is not rejected
Summary
quiche parses and dispatches received
STREAM_DATA_BLOCKEDframes, but it does not reject aSTREAM_DATA_BLOCKEDframe that names a send-only stream. RFC 9000 requires the receiver to terminate the connection withSTREAM_STATE_ERRORin that case.This is a confirmed protocol compliance gap, not a false positive. The receive path reaches
QuicSession::OnBlockedFrame(), where the frame is only counted and logged. No stream-direction check is performed.Standard Requirement
Relevant normative text:
In protocol terms, a
STREAM_DATA_BLOCKEDframe is valid only for a stream on which the sender can send data. If the receiver sees this frame for a stream that is send-only from the receiver's own perspective, the peer is claiming to be blocked on a stream where it is not allowed to send data. The connection must therefore be closed withSTREAM_STATE_ERROR.In quiche's local stream-type terminology, this condition corresponds to a received
STREAM_DATA_BLOCKEDframe whose stream id resolves toWRITE_UNIDIRECTIONALfor the local endpoint.Relevant Source Code
Frame parsing
quiche-main/quiche-main/quiche/quic/core/quic_framer.cc:6274The framer only extracts
stream_idandoffset. It does not validate whether the stream id denotes a send-only stream.Connection-level dispatch
quiche-main/quiche-main/quiche/quic/core/quic_connection.cc:2333The connection layer treats the frame as a generic blocked frame and passes it to the session visitor. It does not close the connection.
Session-level handling
quiche-main/quiche-main/quiche/quic/core/quic_session.cc:751This is the relevant receive-side endpoint behavior. It records counters and logs the frame, but it does not call
QuicUtils::GetStreamType(), does not check forWRITE_UNIDIRECTIONAL, and does not callCloseConnection().Stream-type utility exists
quiche-main/quiche-main/quiche/quic/core/quic_utils.cc:380quiche has the helper needed to classify stream direction, but
OnBlockedFrame()does not use it.Comparable direction checks exist for other frames
quiche-main/quiche-main/quiche/quic/core/quic_session.cc:727This shows that quiche already performs stream-direction validation for similar stream-flow-control frame handling. The missing check is specific to received
STREAM_DATA_BLOCKED.Implementation Behavior
The receive path for
STREAM_DATA_BLOCKEDis:QuicFramer::ProcessStreamDataBlockedFrame()parsesstream_idandoffset.QuicConnection::OnBlockedFrame()dispatches the frame to the session visitor.QuicSession::OnBlockedFrame()increments a counter and logs the frame.No code path was found that rejects
STREAM_DATA_BLOCKEDbased on the target stream being send-only. Searches forOnBlockedFrame(),STREAM_DATA_BLOCKED,WRITE_UNIDIRECTIONAL,READ_UNIDIRECTIONAL, andGetStreamType()show no receive-side branch that closes the connection for this condition.Inconsistency Reason
RFC 9000 requires connection termination with
STREAM_STATE_ERRORwhen a receivedSTREAM_DATA_BLOCKEDframe targets a send-only stream.quiche does not implement that validation. It accepts the frame through the parser and dispatch path, then only records telemetry in
QuicSession::OnBlockedFrame(). Since no stream-direction check or close path exists, a peer can send this invalid frame without triggering the requiredSTREAM_STATE_ERROR.The earlier wording "send-only/read-unidirectional" should be read carefully. In quiche's local endpoint terminology, a stream that is send-only for the receiver is represented as
WRITE_UNIDIRECTIONAL. That wording issue does not change the finding, becauseOnBlockedFrame()checks neitherWRITE_UNIDIRECTIONALnorREAD_UNIDIRECTIONAL.Impact
A peer can send a
STREAM_DATA_BLOCKEDframe for a stream where it is not allowed to send data. Instead of closing the connection withSTREAM_STATE_ERRORas required by RFC 9000, quiche logs the frame and continues. This weakens protocol error enforcement and can produce behavior that differs from other QUIC implementations that enforce the mandatory close.Fix Direction
Add a receive-side validation branch in
QuicSession::OnBlockedFrame()for IETF QUIC stream-specific blocked frames.The intended shape is:
The internal error code should map to IETF
STREAM_STATE_ERROR.QUIC_DATA_RECEIVED_ON_WRITE_UNIDIRECTIONAL_STREAMalready maps toSTREAM_STATE_ERROR, but the final choice should follow quiche's error-code conventions for frame-direction violations.