From 269cdf7806dbab3b69ac99b3f843ad502ba5f6ef Mon Sep 17 00:00:00 2001 From: Roman Arutyunyan Date: Tue, 28 Jul 2026 18:34:36 +0400 Subject: [PATCH] QUIC: always validate stream final size As per RFC 9000, Section 4.5: Once a final size for a stream is known, it cannot change. If a RESET_STREAM or STREAM frame is received indicating a change in the final size for the stream, an endpoint SHOULD respond with an error of type FINAL_SIZE_ERROR. A receiver SHOULD treat receipt of data at or beyond the final size as an error of type FINAL_SIZE_ERROR, even after a stream is closed. Previously, the stream final size was validated only after other checks that could return earlier and skip validation: the receive state check and, for STREAM frames, the check for data below the current receive offset. As a result, a STREAM or RESET_STREAM frame carrying a final size error was silently ignored once the receiving part of the stream had reached a state in which the frame would otherwise be discarded, or when a STREAM frame did not advance the receive offset. Now the final size is validated first, so a FINAL_SIZE_ERROR is generated in these cases as well. --- src/event/quic/ngx_event_quic_streams.c | 49 ++++++++++++------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/event/quic/ngx_event_quic_streams.c b/src/event/quic/ngx_event_quic_streams.c index bc2b4014b..cb885a013 100644 --- a/src/event/quic/ngx_event_quic_streams.c +++ b/src/event/quic/ngx_event_quic_streams.c @@ -1260,6 +1260,19 @@ ngx_quic_handle_stream_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, return NGX_OK; } + if (qs->recv_final_size != (uint64_t) -1 + && (qs->recv_final_size < last + || (qs->recv_final_size > last && f->fin))) + { + qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR; + return NGX_ERROR; + } + + if (qs->recv_last > last && f->fin) { + qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR; + return NGX_ERROR; + } + if (qs->recv_state != NGX_QUIC_STREAM_RECV_RECV && qs->recv_state != NGX_QUIC_STREAM_RECV_SIZE_KNOWN) { @@ -1270,27 +1283,11 @@ ngx_quic_handle_stream_frame(ngx_connection_t *c, ngx_quic_header_t *pkt, return NGX_ERROR; } - if (qs->recv_final_size != (uint64_t) -1 && last > qs->recv_final_size) { - qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR; - return NGX_ERROR; - } - if (last < qs->recv_offset) { return NGX_OK; } if (f->fin) { - if (qs->recv_final_size != (uint64_t) -1 && qs->recv_final_size != last) - { - qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR; - return NGX_ERROR; - } - - if (qs->recv_last > last) { - qc->error = NGX_QUIC_ERR_FINAL_SIZE_ERROR; - return NGX_ERROR; - } - qs->recv_final_size = last; qs->recv_state = NGX_QUIC_STREAM_RECV_SIZE_KNOWN; } @@ -1475,16 +1472,6 @@ ngx_quic_handle_reset_stream_frame(ngx_connection_t *c, return NGX_OK; } - if (qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_RECVD - || qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_READ) - { - return NGX_OK; - } - - if (ngx_quic_control_flow(qs, f->final_size) != NGX_OK) { - return NGX_ERROR; - } - if (qs->recv_final_size != (uint64_t) -1 && qs->recv_final_size != f->final_size) { @@ -1497,6 +1484,16 @@ ngx_quic_handle_reset_stream_frame(ngx_connection_t *c, return NGX_ERROR; } + if (qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_RECVD + || qs->recv_state == NGX_QUIC_STREAM_RECV_RESET_READ) + { + return NGX_OK; + } + + if (ngx_quic_control_flow(qs, f->final_size) != NGX_OK) { + return NGX_ERROR; + } + qs->recv_final_size = f->final_size; qs->recv_state = NGX_QUIC_STREAM_RECV_RESET_RECVD;