Close QUIC connections if a RESET_STREAM frame is received with a too-small final byte offset value. Sends the IETF error code FINAL_SIZE_ERROR in this case, as per: https://datatracker.ietf.org/doc/html/rfc9000#name-stream-final-size Protected by quic_reloadable_flag_quic_close_connection_on_underflow. PiperOrigin-RevId: 881650756
diff --git a/quiche/common/quiche_feature_flags_list.h b/quiche/common/quiche_feature_flags_list.h index 7fb61fb..6c9ad32 100755 --- a/quiche/common/quiche_feature_flags_list.h +++ b/quiche/common/quiche_feature_flags_list.h
@@ -19,6 +19,7 @@ QUICHE_FLAG(bool, quiche_reloadable_flag_quic_block_until_settings_received_copt, true, true, "If enabled and a BSUS connection is received, blocks server connections until SETTINGS frame is received.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_clear_body_manager_along_with_sequencer, false, false, "If true, QuicSpdyStream::StopReading always clears BodyManager along with the SequenceBuffer.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_client_check_blockage_before_on_can_write, false, false, "If true, quic clients will only call OnCanWrite() upon write events if the writer is unblocked.") +QUICHE_FLAG(bool, quiche_reloadable_flag_quic_close_connection_on_underflow, true, true, "If true, close QUIC connections if a RESET_STREAM frame is received with a too-small final byte offset value.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_close_on_idle_timeout, false, false, "If true, closes the connection if it has exceeded the idle timeout when deciding whether to open a stream.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_conservative_bursts, false, false, "If true, set burst token to 2 in cwnd bootstrapping experiment.") QUICHE_FLAG(bool, quiche_reloadable_flag_quic_conservative_cwnd_and_pacing_gains, false, false, "If true, uses conservative cwnd gain and pacing gain when cwnd gets bootstrapped.")
diff --git a/quiche/quic/core/quic_error_codes.cc b/quiche/quic/core/quic_error_codes.cc index 3353f46..e738773 100644 --- a/quiche/quic/core/quic_error_codes.cc +++ b/quiche/quic/core/quic_error_codes.cc
@@ -138,6 +138,7 @@ RETURN_STRING_LITERAL(QUIC_FLOW_CONTROL_RECEIVED_TOO_MUCH_DATA); RETURN_STRING_LITERAL(QUIC_FLOW_CONTROL_SENT_TOO_MUCH_DATA); RETURN_STRING_LITERAL(QUIC_FLOW_CONTROL_INVALID_WINDOW); + RETURN_STRING_LITERAL(QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED); RETURN_STRING_LITERAL(QUIC_CONNECTION_IP_POOLED); RETURN_STRING_LITERAL(QUIC_PROOF_INVALID); RETURN_STRING_LITERAL(QUIC_CRYPTO_DUPLICATE_TAG); @@ -453,6 +454,8 @@ return {true, static_cast<uint64_t>(INTERNAL_ERROR)}; case QUIC_FLOW_CONTROL_INVALID_WINDOW: return {true, static_cast<uint64_t>(FLOW_CONTROL_ERROR)}; + case QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED: + return {true, static_cast<uint64_t>(FINAL_SIZE_ERROR)}; case QUIC_CONNECTION_IP_POOLED: return {true, static_cast<uint64_t>(INTERNAL_ERROR)}; case QUIC_TOO_MANY_OUTSTANDING_SENT_PACKETS:
diff --git a/quiche/quic/core/quic_error_codes.h b/quiche/quic/core/quic_error_codes.h index 415e79f..9409892 100644 --- a/quiche/quic/core/quic_error_codes.h +++ b/quiche/quic/core/quic_error_codes.h
@@ -229,6 +229,9 @@ QUIC_FLOW_CONTROL_SENT_TOO_MUCH_DATA = 63, // The peer received an invalid flow control window. QUIC_FLOW_CONTROL_INVALID_WINDOW = 64, + // The peer sent a RESET_STREAM or STREAM frame indicating a change in the + // final size for the stream. + QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED = 221, // The connection has been IP pooled into an existing connection. QUIC_CONNECTION_IP_POOLED = 62, // The connection has too many outstanding sent packets. @@ -644,7 +647,7 @@ QUIC_CLIENT_LOST_NETWORK_ACCESS = 215, // No error. Used as bound while iterating. - QUIC_LAST_ERROR = 221, + QUIC_LAST_ERROR = 222, }; // QuicErrorCodes is encoded as four octets on-the-wire when doing Google QUIC, // or a varint62 when doing IETF QUIC. Ensure that its value does not exceed
diff --git a/quiche/quic/core/quic_session.cc b/quiche/quic/core/quic_session.cc index bfb3e54..15c790f 100644 --- a/quiche/quic/core/quic_session.cc +++ b/quiche/quic/core/quic_session.cc
@@ -1329,6 +1329,15 @@ QUIC_DVLOG(1) << ENDPOINT << "Received final byte offset " << final_byte_offset << " for stream " << stream_id; + if (GetQuicReloadableFlag(quic_close_connection_on_underflow)) { + QUIC_RELOADABLE_FLAG_COUNT(quic_close_connection_on_underflow); + if (final_byte_offset < it->second) { + connection_->CloseConnection( + QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED, "Invalid final byte offset", + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET); + return; + } + } QuicByteCount offset_diff = final_byte_offset - it->second; if (flow_controller_.UpdateHighestReceivedOffset( flow_controller_.highest_received_byte_offset() + offset_diff)) {
diff --git a/quiche/quic/core/quic_session_test.cc b/quiche/quic/core/quic_session_test.cc index 35e490f..e77ec26 100644 --- a/quiche/quic/core/quic_session_test.cc +++ b/quiche/quic/core/quic_session_test.cc
@@ -225,6 +225,7 @@ TestStream(PendingStream* pending, QuicSession* session) : QuicStream(pending, session, /*is_static=*/false) {} + using QuicStream::AddBytesConsumed; using QuicStream::CloseWriteSide; using QuicStream::WriteMemSlices; @@ -3628,6 +3629,64 @@ EXPECT_FALSE(session_.IsStreamFlowControlBlocked()); } +TEST_P(QuicSessionTestServer, FlowControlFinalByteUnderflow) { + CompleteHandshake(); + + // Server creates an outgoing bidirectional stream. + TestStream* stream = session_.CreateOutgoingBidirectionalStream(); + ASSERT_NE(stream, nullptr); + const QuicStreamId stream_id = stream->id(); + const QuicStreamOffset kDataSize = 8000; + + // Peer sends 8000 bytes of data (no FIN) on this stream. + std::string data(kDataSize, 'a'); + QuicStreamFrame data_frame(stream_id, /*fin=*/false, /*offset=*/0, + absl::string_view(data)); + session_.OnStreamFrame(data_frame); + + EXPECT_EQ(session_.flow_controller()->highest_received_byte_offset(), + kDataSize); + EXPECT_EQ(stream->highest_received_byte_offset(), kDataSize); + + // Consume all data at stream level so connection bytes_consumed advances. + stream->AddBytesConsumed(kDataSize); + + // Server locally resets the stream. Since no FIN was received, + // the stream goes into locally_closed_streams_highest_offset_ + // with offset = 8000. + EXPECT_CALL(*connection_, SendControlFrame(_)).Times(AtLeast(1)); + EXPECT_CALL(*connection_, OnStreamReset(stream_id, _)); + stream->Reset(QUIC_STREAM_CANCELLED); + + // Peer sends RESET_STREAM with an invalid final_size = 0, which should + // trigger the connection to be closed. + QuicRstStreamFrame malicious_rst( + /*control_frame_id=*/kInvalidControlFrameId, stream_id, + QUIC_STREAM_CANCELLED, + /*bytes_written=*/0); + if (GetQuicReloadableFlag(quic_close_connection_on_underflow)) { + EXPECT_CALL( + *connection_, + CloseConnection(QUIC_FLOW_CONTROL_FINAL_SIZE_CHANGED, + "Invalid final byte offset", + ConnectionCloseBehavior::SEND_CONNECTION_CLOSE_PACKET)); + } else { + EXPECT_CALL(*connection_, CloseConnection(_, _, _)).Times(0); + } + session_.OnRstStream(malicious_rst); + + EXPECT_EQ(session_.flow_controller()->highest_received_byte_offset(), + kDataSize); + const QuicByteCount bytes_consumed_after = + session_.flow_controller()->bytes_consumed(); + + if (GetQuicReloadableFlag(quic_close_connection_on_underflow)) { + EXPECT_EQ(bytes_consumed_after, kDataSize); + } else { + EXPECT_NE(bytes_consumed_after, kDataSize); + } +} + } // namespace } // namespace test } // namespace quic