Commit 9539a15da5 for openssl.org

commit 9539a15da51bc57efe28df79edc84fd3f8b11a28
Author: Alexandr Nedvedicky <sashan@openssl.org>
Date:   Wed Sep 9 11:09:19 2026 +0200

    Enforce final size for streams.

    When FIN stream frame is received at particular offset,
    then the FIN's offset must be the largest offset kept
    in stream reassembly buffer.

    Receiving stream frame with offset larger than FIN frame
    must be reported as protocol violation and connection
    must be closed.

    In order to report protocol violation error the stream
    reassembly module must have a reference to QUIC_CHANNEL
    object where particular stream belongs to. The refernce
    to channel is part of QUIC_RSTREAM_QPARAM structure.
    This change makes stream quality parameter mandatory.

    Co-authored-by: Jakub Zelenka <bukka@php.net>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Merge-date: Sat Sep 26 11:35:33 2026
    Merged-from: https://github.com/openssl/openssl/pull/32038

diff --git a/include/internal/quic_stream.h b/include/internal/quic_stream.h
index 2d0d7d9475..1c0fd3f86e 100644
--- a/include/internal/quic_stream.h
+++ b/include/internal/quic_stream.h
@@ -408,7 +408,7 @@ size_t ossl_quic_rstream_get_chunk_count(QUIC_RSTREAM *qrs);
  */
 size_t ossl_quic_rstream_get_range_count(QUIC_RSTREAM *qrs);

-QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void);
+QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(QUIC_CHANNEL *ch);
 void ossl_quic_rstream_qparm_destroy(QUIC_RSTREAM_QPARM *rsqp);
 #endif

diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index 66b90c5876..cb96efb925 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -308,12 +308,11 @@ static int ch_init(QUIC_CHANNEL *ch)
             goto err;
     }

-    ch->rsqp = ossl_quic_rstream_qparm_new();
+    ch->rsqp = ossl_quic_rstream_qparm_new(ch);
     if (ch->rsqp == NULL)
         goto err;

     for (pn_space = QUIC_PN_SPACE_INITIAL; pn_space < QUIC_PN_SPACE_NUM; ++pn_space) {
-        /* no quality control for crypto stream. */
         ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL, ch->rsqp);
         if (ch->crypto_recv[pn_space] == NULL)
             goto err;
diff --git a/ssl/quic/quic_strm_reas.c b/ssl/quic/quic_strm_reas.c
index 08dfb1d531..1746001959 100644
--- a/ssl/quic/quic_strm_reas.c
+++ b/ssl/quic/quic_strm_reas.c
@@ -12,6 +12,7 @@
 #include "internal/quic_stream.h"
 #include "internal/quic_strm_reas.h"
 #include "internal/list.h"
+#include "internal/quic_channel.h"

 #if !defined(NDEBUG) && defined(WITH_STRM_REAS_DEBUG)
 #include <stdio.h>
@@ -61,6 +62,7 @@ struct stream_chunk_t {
 struct quic_rstream_qparm_st {
     size_t rsqp_pkt_overhead_treshold;
     size_t rsqp_pkt_overhead_sz;
+    QUIC_CHANNEL *rsqp_ch;
 };

 #define sc_data sc_data_u.u_data
@@ -104,27 +106,25 @@ OSSL_RBT_GENERATE(srange, stream_range_t, sr_rbe, srange_cmp);

 static void rsqp_add_overhead(QUIC_RSTREAM_QPARM *rsqp, size_t sc_overhead)
 {
-    if (rsqp != NULL)
-        rsqp->rsqp_pkt_overhead_sz += sc_overhead;
+    rsqp->rsqp_pkt_overhead_sz += sc_overhead;
 }

 static void rsqp_sub_overhead(QUIC_RSTREAM_QPARM *rsqp, size_t sc_overhead)
 {
-    if (rsqp != NULL)
-        rsqp->rsqp_pkt_overhead_sz -= sc_overhead;
+    rsqp->rsqp_pkt_overhead_sz -= sc_overhead;
 }

 /*
-  * Cleansing (SSL_OP_CLEANSE_PLAINTEXT) must write through the const
-  * data pointers received from ossl_sframe_set_insert(), which may
-  * point into a shared packet buffer. That is safe: each chunk
-  * references the disjoint payload slice of its own frame and a
-  * processed packet is kept alive only by the chunks stored on it,
-  * so nobody else reads the wiped bytes.
-  *
-  * The const should eventually be dropped from the prototypes
-  * instead; until then deconst() is used.
-  */
+ * Cleansing (SSL_OP_CLEANSE_PLAINTEXT) must write through the const
+ * data pointers received from ossl_sframe_set_insert(), which may
+ * point into a shared packet buffer. That is safe: each chunk
+ * references the disjoint payload slice of its own frame and a
+ * processed packet is kept alive only by the chunks stored on it,
+ * so nobody else reads the wiped bytes.
+ *
+ * The const should eventually be dropped from the prototypes
+ * instead; until then deconst() is used.
+ */
 static unsigned char *deconst(const unsigned char *data)
 {
     union {
@@ -216,10 +216,9 @@ static int srange_cmp(const struct stream_range_t *a_sr,
 }

 static int keep_schunk_data_on_packet(SFRAME_SET *fs, OSSL_QRX_PKT *pkt,
-    UINT_RANGE *r)
+    size_t overhead)
 {
-    if (fs->rsqp != NULL
-        && fs->rsqp->rsqp_pkt_overhead_sz >= fs->rsqp->rsqp_pkt_overhead_treshold)
+    if ((fs->rsqp->rsqp_pkt_overhead_sz + overhead) >= fs->rsqp->rsqp_pkt_overhead_treshold)
         return 0;

     return 1;
@@ -242,26 +241,20 @@ static struct stream_chunk_t *new_schunk(SFRAME_SET *fs, OSSL_QRX_PKT *pkt,
     rsize = r->end - r->start;
     assert(rsize <= pkt->datagram_len);
     overhead = UINT64_TO_SIZE_T(pkt->datagram_len - rsize);
-    rsqp_add_overhead(fs->rsqp, overhead);

-    if (keep_schunk_data_on_packet(fs, pkt, r) == 1) {
+    if (keep_schunk_data_on_packet(fs, pkt, overhead) == 1) {
         sc->sc_st = ST_TYPE_PKT;
         sc->sc_pkt = pkt;
         ossl_qrx_pkt_up_ref(pkt);
+        rsqp_add_overhead(fs->rsqp, overhead);
         sc->sc_data = data;
         sc->sc_range = *r;
-        if (fs->rsqp != NULL)
-            DEBUG_PRINT(stderr,
-                "%s sc: %p sc overhead: %d pkt_buf_overhead_sz: %zu -> %zu\n",
-                OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(pkt, sc),
-                fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(pkt, sc),
-                fs->rsqp->rsqp_pkt_overhead_sz);
+        DEBUG_PRINT(stderr,
+            "%s sc: %p sc overhead: %llu pkt_buf_overhead_sz: %llu -> %zu\n",
+            OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(pkt, sc),
+            fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(pkt, sc),
+            fs->rsqp->rsqp_pkt_overhead_sz);
     } else {
-        /*
-         * Only data which stay on packet must be accounted as overhead.
-         */
-        rsqp_sub_overhead(fs->rsqp, overhead);
-
         if (rsize <= DIRECT_STORAGE_SZ) {
             DEBUG_PRINT(stderr, "%s ST_TYPE_DIRECT sc: %p %llu\n", OPENSSL_FUNC,
                 (void *)sc, rsize);
@@ -301,14 +294,12 @@ static void destroy_schunk(SFRAME_SET *fs, struct stream_chunk_t *sc)

     switch (sc->sc_st) {
     case ST_TYPE_PKT:
-        assert(fs->rsqp == NULL
-            || fs->rsqp->rsqp_pkt_overhead_sz >= SCHUNK_OVERHEAD(sc->sc_pkt, sc));
-        if (fs->rsqp != NULL)
-            DEBUG_PRINT(stderr,
-                "%s sc: %p sc overhead: %d pkt_buf_overhead_sz: %zu -> %zu\n",
-                OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(sc->sc_pkt, sc),
-                fs->rsqp->rsqp_pkt_overhead_sz,
-                fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(sc->sc_pkt, sc));
+        assert(fs->rsqp->rsqp_pkt_overhead_sz >= SCHUNK_OVERHEAD(sc->sc_pkt, sc));
+        DEBUG_PRINT(stderr,
+            "%s sc: %p sc overhead: %llu pkt_buf_overhead_sz: %zu -> %llu\n",
+            OPENSSL_FUNC, (void *)sc, SCHUNK_OVERHEAD(sc->sc_pkt, sc),
+            fs->rsqp->rsqp_pkt_overhead_sz,
+            fs->rsqp->rsqp_pkt_overhead_sz - SCHUNK_OVERHEAD(sc->sc_pkt, sc));
         rsqp_sub_overhead(fs->rsqp,
             UINT64_TO_SIZE_T(SCHUNK_OVERHEAD(sc->sc_pkt, sc)));
         ossl_qrx_pkt_release(sc->sc_pkt);
@@ -374,6 +365,8 @@ static struct stream_range_t *create_range(SFRAME_SET *fs,

 void ossl_sframe_set_init(SFRAME_SET *fs, QUIC_RSTREAM_QPARM *rsqp)
 {
+    assert(rsqp != NULL);
+
     memset(fs, 0, sizeof(*fs));
     OSSL_RBT_INIT(srange, &fs->ranges);
     fs->rsqp = rsqp;
@@ -403,7 +396,7 @@ static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt,
      */
     rsize = r->end - r->start;
     if (r->start < sr->sr_range.start && r->end > sr->sr_range.end
-        && rsize > DIRECT_STORAGE_SZ && ossl_list_sc_num(&sr->sr_chunks) > 1)
+        && ossl_list_sc_num(&sr->sr_chunks) > 1)
         return 0;

     head_sc = ossl_list_sc_head(&sr->sr_chunks);
@@ -782,11 +775,10 @@ static int chop_range(SFRAME_SET *fs, struct stream_range_t *sr,

     if (sc->sc_st == ST_TYPE_PKT) {
         rsqp_add_overhead(fs->rsqp, unused_sz);
-        if (fs->rsqp != NULL)
-            DEBUG_PRINT(stderr, "%s sc: %p unused_sz: %zu %zu -> %zu\n",
-                OPENSSL_FUNC, (void *)sc, unused_sz,
-                fs->rsqp->rsqp_pkt_overhead_sz - unused_sz,
-                fs->rsqp->rsqp_pkt_overhead_sz);
+        DEBUG_PRINT(stderr, "%s sc: %p unused_sz: %zu %zu -> %zu\n",
+            OPENSSL_FUNC, (void *)sc, unused_sz,
+            fs->rsqp->rsqp_pkt_overhead_sz - unused_sz,
+            fs->rsqp->rsqp_pkt_overhead_sz);
     }

     return 1;
@@ -866,6 +858,7 @@ static struct stream_range_t *append_range(SFRAME_SET *fs,

 /*
  * receives a chunk of data from stream frame.
+ * note there is a tri-state return value:
  */
 int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt,
     const unsigned char *data, int fin)
@@ -876,28 +869,47 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt,
     struct stream_chunk_t *sc = NULL;
     struct stream_range_t key_sr = { 0 };

+    assert(r->start <= r->end);
+
     /*
-     * receive the FIN frame if FIN frame. If FIN was not seen yet,
-     * then record FIN's offset (r->end). If FIN was received then
-     * verify FIN's offset match, error out on mismatch.
+     * receive the FIN frame. If FIN was not seen yet, then record
+     * FIN's offset (r->end). If FIN was received then verify FIN's
+     * offset match, error out on mismatch.
      */
     if (fin != 0) {
         if (fs->fin == 0) {
+            sr = OSSL_RBT_MAX(srange, &fs->ranges);
+            if (r->end < fs->offset
+                || (sr != NULL && sr->sr_range.end > r->end)) {
+                ossl_quic_channel_raise_protocol_error(fs->rsqp->rsqp_ch,
+                    OSSL_QUIC_ERR_FINAL_SIZE_ERROR,
+                    OSSL_QUIC_FRAME_TYPE_STREAM_FIN,
+                    "stream final size error");
+                return 0;
+            }
             fs->fin = 1;
             fs->fin_off = r->end;
         } else if (fs->fin_off != r->end) {
+            ossl_quic_channel_raise_protocol_error(fs->rsqp->rsqp_ch,
+                OSSL_QUIC_ERR_FINAL_SIZE_ERROR,
+                OSSL_QUIC_FRAME_TYPE_STREAM_FIN,
+                "stream final size error");
             return 0;
         }
     }

     /*
-     * discard any data past FIN offset (of FIN offset is set).
+     * reject any data at or past the FIN offset (if FIN offset is set).
      */
     if (fs->fin != 0) {
-        if (fs->fin_off < r->end)
-            r->end = fs->fin_off; /* truncate bytes beyond FIN */
-        if (fs->fin_off < r->start)
+        if (fs->fin_off < r->end) {
+            ossl_quic_channel_raise_protocol_error(fs->rsqp->rsqp_ch,
+                OSSL_QUIC_ERR_FINAL_SIZE_ERROR,
+                (fin == 0) ? OSSL_QUIC_FRAME_TYPE_STREAM
+                           : OSSL_QUIC_FRAME_TYPE_STREAM_FIN,
+                "stream final size error");
             return 0;
+        }
     }

     if (r->end <= fs->offset) {
@@ -1286,11 +1298,10 @@ int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t new_offset)

         if (sc->sc_st == ST_TYPE_PKT) {
             rsqp_add_overhead(fs->rsqp, unused_sz);
-            if (fs->rsqp != NULL)
-                DEBUG_PRINT(stderr, "%s sc: %p unused_sz: %zu %zu -> %zu\n",
-                    OPENSSL_FUNC, (void *)sc, unused_sz,
-                    fs->rsqp->rsqp_pkt_overhead_sz - unused_sz,
-                    fs->rsqp->rsqp_pkt_overhead_sz);
+            DEBUG_PRINT(stderr, "%s sc: %p unused_sz: %zu %zu -> %zu\n",
+                OPENSSL_FUNC, (void *)sc, unused_sz,
+                fs->rsqp->rsqp_pkt_overhead_sz - unused_sz,
+                fs->rsqp->rsqp_pkt_overhead_sz);
         }
     }

@@ -1311,7 +1322,7 @@ int ossl_sframe_set_avail(SFRAME_SET *fs, uint64_t *avail, int *fin)
     return 1;
 }

-QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void)
+QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(QUIC_CHANNEL *ch)
 {
     QUIC_RSTREAM_QPARM *rsqp;

@@ -1319,6 +1330,7 @@ QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void)
     if (rsqp != NULL) {
         rsqp->rsqp_pkt_overhead_treshold = PKT_BUFFER_OVERHEAD_TRESHOLD;
         rsqp->rsqp_pkt_overhead_sz = 0;
+        rsqp->rsqp_ch = ch;
     }

     return rsqp;
diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c
index 468a096439..1d75f0611b 100644
--- a/test/quic_stream_test.c
+++ b/test/quic_stream_test.c
@@ -12,6 +12,7 @@
 #include "internal/quic_record_rx.h"
 #include "internal/quic_stream.h"
 #include "../ssl/quic/quic_record_rx_local.h"
+#include "../ssl/quic/quic_channel_local.h"
 #include "internal/nelem.h"
 #include "testutil.h"

@@ -411,6 +412,7 @@ static int test_rstream_random(int idx)
     unsigned char *bulk_data = NULL;
     unsigned char *read_buf = NULL;
     QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     OSSL_QRX_PKT **pkts = NULL;
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     size_t i, read_off, queued_min, queued_max, num_pkts = 0;
@@ -424,7 +426,8 @@ static int test_rstream_random(int idx)
     if (!TEST_ptr(bulk_data = OPENSSL_malloc(data_size))
         || !TEST_ptr(read_buf = OPENSSL_malloc(data_size))
         || !TEST_ptr(pkts = OPENSSL_zalloc(sizeof(*pkts) * max_pkts))
-        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())
+        || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
         || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;

@@ -532,6 +535,9 @@ static int test_rstream_random(int idx)
             goto err;
     }

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ret = 1;

 err:
@@ -548,6 +554,7 @@ err:
     ossl_quic_rstream_qparm_destroy(rsqp);
     OPENSSL_free(bulk_data);
     OPENSSL_free(read_buf);
+    ossl_quic_channel_free(ch);
     return ret;
 }

@@ -558,6 +565,7 @@ err:
 static int test_rstream_pkt(void)
 {
     QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     OSSL_QRX_PKT *pkt_a = NULL, *pkt_b = NULL, *pkt_c = NULL;
     unsigned char pdata[64], cbuf[64], buf[64];
@@ -571,7 +579,8 @@ static int test_rstream_pkt(void)
     if (!TEST_ptr(pkt_a = pkt_test_new(1200))
         || !TEST_ptr(pkt_b = pkt_test_new(1200))
         || !TEST_ptr(pkt_c = pkt_test_new(1200))
-        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())
+        || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
         || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;

@@ -661,6 +670,9 @@ static int test_rstream_pkt(void)
         if (!TEST_uchar_eq(cbuf[i], i >= 8 && i < 56 ? 0 : 0xAA))
             goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ret = 1;

 err:
@@ -669,6 +681,7 @@ err:
     pkt_test_free(pkt_a);
     pkt_test_free(pkt_b);
     pkt_test_free(pkt_c);
+    ossl_quic_channel_free(ch);
     return ret;
 }

@@ -683,6 +696,7 @@ static int test_rstream_pkt_overhead(void)
     QUIC_RSTREAM *rstream = NULL;
     OSSL_QRX_PKT **pkt = NULL;
     QUIC_RSTREAM_QPARM *rsqp = NULL;
+    QUIC_CHANNEL *ch = NULL;
     unsigned char *data = NULL, *buf = NULL;
     const size_t framesz = 8;
     const size_t nframes = 4096; /* far past a 64 KiB overhead limit */
@@ -694,7 +708,8 @@ static int test_rstream_pkt_overhead(void)
     if (!TEST_ptr(data = OPENSSL_malloc(total))
         || !TEST_ptr(buf = OPENSSL_malloc(total))
         || !TEST_ptr(pkt = OPENSSL_zalloc(nframes * sizeof(*pkt)))
-        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())
+        || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
         || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;

@@ -736,6 +751,9 @@ static int test_rstream_pkt_overhead(void)
         if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
             goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ret = 1;

 err:
@@ -747,6 +765,7 @@ err:
     OPENSSL_free(pkt);
     OPENSSL_free(data);
     OPENSSL_free(buf);
+    ossl_quic_channel_free(ch);
     return ret;
 }

@@ -764,6 +783,7 @@ static int test_rstream_reorder(int idx)
 {
     unsigned char *data = NULL, *buf = NULL, *arena = NULL, *ap;
     QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     OSSL_QRX_PKT **pkts = NULL;
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     const size_t data_size = 4096;
@@ -779,7 +799,8 @@ static int test_rstream_reorder(int idx)
         || !TEST_ptr(arena = OPENSSL_malloc(3 * data_size))
         || !TEST_ptr(order = OPENSSL_malloc(nframes * sizeof(*order)))
         || !TEST_ptr(pkts = OPENSSL_zalloc(2 * nframes * sizeof(*pkts)))
-        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())
+        || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
         || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;

@@ -849,6 +870,9 @@ static int test_rstream_reorder(int idx)
         || !TEST_mem_eq(buf, got, data, data_size))
         goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ret = 1;

 err:
@@ -866,6 +890,99 @@ err:
     OPENSSL_free(arena);
     OPENSSL_free(data);
     OPENSSL_free(buf);
+    ossl_quic_channel_free(ch);
+    return ret;
+}
+
+/*
+ * A short retransmit which starts below an existing range and ends past its
+ * direct storage tail chunk is small enough to slip past the full overlap
+ * guard in try_dstorage(), whose append path then treats every byte below
+ * the tail chunk end as duplicate. The genuinely new bytes below the range
+ * start must not be dropped when the insert reports success, otherwise the
+ * frame is acked, never retransmitted and the gap in the stream is permanent.
+ */
+static int test_rstream_dstorage_two_sided_overlap(void)
+{
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    OSSL_QRX_PKT *pkts[59] = { NULL };
+    unsigned char pdata[64], buf[64], fill = 0xFF;
+    size_t num_pkts = 0, readbytes = 0, avail = 0, i;
+    int fin = 0;
+    int ret = 0;
+
+    for (i = 0; i < sizeof(pdata); ++i)
+        pdata[i] = (unsigned char)(0x40 + i);
+
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
+        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
+        goto err;
+
+    for (i = 0; i < OSSL_NELEM(pkts); ++i)
+        if (!TEST_ptr(pkts[num_pkts++] = pkt_test_new(1200)))
+            goto err;
+
+    /* a packet backed chunk [10, 11) while below the overhead limit */
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[0], 10,
+            pdata + 10, 1, 0))
+        || !TEST_size_t_eq(pkt_test_refcount(pkts[0]), 2))
+        goto err;
+
+    /*
+     * disjoint 1-byte frames on 1200 byte datagrams, each accounting
+     * 1199 bytes of overhead, take the stream past the 64kB overhead
+     * limit so that short chunks switch to direct storage
+     */
+    for (i = 0; i < 55; ++i)
+        if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[1 + i],
+                100 + 2 * i, &fill, 1, 0)))
+            goto err;
+
+    /*
+     * [11, 12) goes to the direct storage of a new tail chunk appended
+     * to the range, holding no reference to its packet
+     */
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[56], 11,
+            pdata + 11, 1, 0))
+        || !TEST_size_t_eq(pkt_test_refcount(pkts[56]), 1))
+        goto err;
+
+    /*
+     * a 6 byte retransmit [8, 14) starts below the range [10, 12) and
+     * ends past its direct storage tail chunk, the bytes [8, 10) are
+     * new and must be kept
+     */
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[57], 8,
+            pdata + 8, 6, 0)))
+        goto err;
+
+    /* deliver the head [0, 8) so everything up to 14 is contiguous */
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[58], 0,
+            pdata, 8, 0)))
+        goto err;
+
+    if (!TEST_true(ossl_quic_rstream_available(rstream, &avail, &fin))
+        || !TEST_size_t_eq(avail, 14)
+        || !TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf),
+            &readbytes, &fin))
+        || !TEST_size_t_eq(readbytes, 14)
+        || !TEST_mem_eq(buf, readbytes, pdata, 14))
+        goto err;
+
+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
+    ret = 1;
+
+err:
+    ossl_quic_rstream_free(rstream);
+    ossl_quic_rstream_qparm_destroy(rsqp);
+    for (i = 0; i < num_pkts; ++i)
+        pkt_test_free(pkts[i]);
+    ossl_quic_channel_free(ch);
     return ret;
 }

@@ -889,16 +1006,20 @@ static int test_rstream_chunk_partial_overlap(void)
     OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     TEST_STREAM_CHUNK_T *tsc;
-    QUIC_RSTREAM *rstream;
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     size_t readbytes;
     unsigned int i;
-    unsigned int send_order[7];
+    unsigned int send_order[OSSL_NELEM(tsc_buf)];
     int fin = 0;
     int ok = 0;

-    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
         return 0;

+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
     rstream = ossl_quic_rstream_new(NULL, NULL, rsqp);
     if (!TEST_ptr(rstream))
         goto err;
@@ -1069,6 +1190,9 @@ static int test_rstream_chunk_partial_overlap(void)
         if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
             goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ok = 1;
 err:
     for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
@@ -1076,6 +1200,7 @@ err:

     ossl_quic_rstream_free(rstream);
     ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);

     return ok;
 }
@@ -1095,16 +1220,20 @@ static int test_rstream_chunk_full_overlap(void)
     OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     TEST_STREAM_CHUNK_T *tsc;
-    QUIC_RSTREAM *rstream;
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     size_t readbytes;
     unsigned int i;
-    unsigned int send_order[5];
+    unsigned int send_order[OSSL_NELEM(tsc_buf)];
     int fin;
     int ok = 0;

-    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
         return 0;

+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
     rstream = ossl_quic_rstream_new(NULL, NULL, rsqp);
     if (!TEST_ptr(rstream))
         goto err;
@@ -1205,6 +1334,9 @@ static int test_rstream_chunk_full_overlap(void)
         if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
             goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ok = 1;
 err:
     for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
@@ -1212,6 +1344,7 @@ err:

     ossl_quic_rstream_free(rstream);
     ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);

     return ok;
 }
@@ -1224,15 +1357,19 @@ static int test_rstream_range_overlap(void)
     OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     TEST_STREAM_CHUNK_T *tsc;
-    QUIC_RSTREAM *rstream;
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     size_t readbytes;
     unsigned int i;
     int fin;
     int ok = 0;

-    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
         return 0;

+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
     rstream = ossl_quic_rstream_new(NULL, NULL, rsqp);
     if (!TEST_ptr(rstream))
         goto err;
@@ -1396,6 +1533,9 @@ static int test_rstream_range_overlap(void)
         if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
             goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ok = 1;
 err:
     for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
@@ -1403,6 +1543,7 @@ err:

     ossl_quic_rstream_free(rstream);
     ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);

     return ok;
 }
@@ -1415,10 +1556,11 @@ static int test_rstream_prepend_byte_chunks(void)
     OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     TEST_STREAM_CHUNK_T *tsc;
-    QUIC_RSTREAM *rstream;
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     size_t readbytes;
     unsigned int i;
-    unsigned int send_order[6];
+    unsigned int send_order[OSSL_NELEM(tsc_buf)];
     int fin;
     int ok = 0;

@@ -1427,9 +1569,12 @@ static int test_rstream_prepend_byte_chunks(void)
         return 1;
     }

-    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
         return 0;

+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
     rstream = ossl_quic_rstream_new(NULL, NULL, rsqp);
     if (!TEST_ptr(rstream))
         goto err;
@@ -1542,6 +1687,9 @@ static int test_rstream_prepend_byte_chunks(void)
         if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
             goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ok = 1;
 err:
     for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
@@ -1549,6 +1697,7 @@ err:

     ossl_quic_rstream_free(rstream);
     ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);

     return ok;
 }
@@ -1561,10 +1710,11 @@ static int test_rstream_append_byte_chunks(void)
     OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     TEST_STREAM_CHUNK_T *tsc;
-    QUIC_RSTREAM *rstream;
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     size_t readbytes;
     unsigned int i;
-    unsigned int send_order[6];
+    unsigned int send_order[OSSL_NELEM(tsc_buf)];
     int fin;
     int ok = 0;

@@ -1573,9 +1723,12 @@ static int test_rstream_append_byte_chunks(void)
         return 1;
     }

-    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
         return 0;

+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
     rstream = ossl_quic_rstream_new(NULL, NULL, rsqp);
     if (!TEST_ptr(rstream))
         goto err;
@@ -1688,6 +1841,9 @@ static int test_rstream_append_byte_chunks(void)
         if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
             goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
     ok = 1;
 err:
     for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
@@ -1695,6 +1851,7 @@ err:

     ossl_quic_rstream_free(rstream);
     ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);

     return ok;
 }
@@ -1707,10 +1864,11 @@ static int test_rstream_mix_chunks(void)
     OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
     QUIC_RSTREAM_QPARM *rsqp = NULL;
     TEST_STREAM_CHUNK_T *tsc;
-    QUIC_RSTREAM *rstream;
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
     size_t readbytes;
     unsigned int i;
-    unsigned int send_order[7];
+    unsigned int send_order[OSSL_NELEM(tsc_buf)];
     int fin;
     int ok = 0;

@@ -1719,9 +1877,12 @@ static int test_rstream_mix_chunks(void)
         return 1;
     }

-    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
         return 0;

+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
     rstream = ossl_quic_rstream_new(NULL, NULL, rsqp);
     if (!TEST_ptr(rstream))
         goto err;
@@ -1860,13 +2021,270 @@ static int test_rstream_mix_chunks(void)
         if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
             goto err;

+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
+    ok = 1;
+err:
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
+        pkt_test_free(pkt[i]);
+
+    ossl_quic_rstream_free(rstream);
+    ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);
+
+    return ok;
+}
+
+static int test_final_size_violation_fin_first(void)
+{
+    unsigned char data[4096];
+    unsigned char read_buf[4096];
+    TEST_STREAM_CHUNK_T tsc_buf[3];
+    OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    TEST_STREAM_CHUNK_T *tsc;
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
+    unsigned int i;
+    unsigned int send_order[OSSL_NELEM(tsc_buf)];
+    int ok = 0;
+
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
+        return 0;
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
+    rstream = ossl_quic_rstream_new(NULL, NULL, rsqp);
+    if (!TEST_ptr(rstream))
+        goto err;
+
+    for (i = 0; i < sizeof(data); i++)
+        data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)];
+
+    memset(tsc_buf, 0, sizeof(tsc_buf));
+    memset(read_buf, 0, sizeof(read_buf));
+
+    /*
+     * we start with 4-byte nibble which at offset 8.
+     */
+    tsc = &tsc_buf[0];
+    tsc->tsc_data = &data[10];
+    tsc->tsc_off = 10;
+    tsc->tsc_len = 10;
+    tsc->tsc_fin = 1;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[0] = 0;
+
+    tsc = &tsc_buf[1];
+    tsc->tsc_data = &data[5];
+    tsc->tsc_off = 5;
+    tsc->tsc_len = 5;
+    tsc->tsc_fin = 0;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 2;
+    send_order[1] = 1;
+
+    tsc = &tsc_buf[2];
+    tsc->tsc_data = &data[50];
+    tsc->tsc_off = 20;
+    tsc->tsc_len = 10;
+    tsc->tsc_fin = 0;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 2;
+    send_order[2] = 2;
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf) - 1; i++) {
+        pkt[i] = pkt_test_new(1200);
+        if (!TEST_ptr(pkt[i]))
+            goto err;
+        tsc = &tsc_buf[send_order[i]];
+        if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i],
+                tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin)))
+            goto err;
+
+        /*
+         * check our assumptions about about reassemble process internals.
+         */
+        if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream),
+                tsc->tsc_ranges_exp)) {
+            TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+            goto err;
+        }
+
+        if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream),
+                tsc->tsc_chunks_exp)) {
+            TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+            goto err;
+        }
+    }
+
+    pkt[i] = pkt_test_new(1200);
+    if (!TEST_ptr(pkt[i]))
+        goto err;
+    tsc = &tsc_buf[send_order[i]];
+    if (!TEST_false(ossl_quic_rstream_queue_data(rstream, pkt[i],
+            tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin)))
+        goto err;
+
+    if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream),
+            tsc->tsc_ranges_exp)) {
+        TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+        goto err;
+    }
+
+    if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream),
+            tsc->tsc_chunks_exp)) {
+        TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+        goto err;
+    }
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf) - 1; i++)
+        if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 2))
+            goto err;
+
+    if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
+        goto err;
+
+    if (!TEST_int_eq(ch->protocol_error, 1))
+        goto err;
+
     ok = 1;
 err:
+    /*
+     * the data from rstream has not been consumed,
+     * references to packets are still retained there.
+     * therefore we need to free rstream before freeing
+     * pkckets.
+     */
+    ossl_quic_rstream_free(rstream);
+
     for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
         pkt_test_free(pkt[i]);

+    ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);
+
+    return ok;
+}
+
+static int test_final_size_violation_data_first(void)
+{
+    unsigned char data[4096];
+    unsigned char read_buf[4096];
+    TEST_STREAM_CHUNK_T tsc_buf[3];
+    OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    TEST_STREAM_CHUNK_T *tsc;
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
+    unsigned int i;
+    int ok = 0;
+
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
+        return 0;
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
+    rstream = ossl_quic_rstream_new(NULL, NULL, rsqp);
+    if (!TEST_ptr(rstream))
+        goto err;
+
+    for (i = 0; i < sizeof(data); i++)
+        data[i] = FILL_PATTERN[i % (sizeof(FILL_PATTERN) - 1)];
+
+    memset(tsc_buf, 0, sizeof(tsc_buf));
+    memset(read_buf, 0, sizeof(read_buf));
+
+    /*
+     * we start with 4-byte nibble which at offset 8.
+     */
+    tsc = &tsc_buf[0];
+    tsc->tsc_data = &data[10];
+    tsc->tsc_off = 10;
+    tsc->tsc_len = 10;
+    tsc->tsc_fin = 0;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+
+    tsc = &tsc_buf[1];
+    tsc->tsc_data = &data[5];
+    tsc->tsc_off = 5;
+    tsc->tsc_len = 5;
+    tsc->tsc_fin = 1;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+
+    pkt[0] = pkt_test_new(1200);
+    if (!TEST_ptr(pkt[0]))
+        goto err;
+    tsc = &tsc_buf[0];
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[0],
+            tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin)))
+        goto err;
+
+    /*
+     * check our assumptions about about reassemble process internals.
+     */
+    if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream),
+            tsc->tsc_ranges_exp)) {
+        TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+        goto err;
+    }
+
+    if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream),
+            tsc->tsc_chunks_exp)) {
+        TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+        goto err;
+    }
+
+    pkt[1] = pkt_test_new(1200);
+    if (!TEST_ptr(pkt[1]))
+        goto err;
+    tsc = &tsc_buf[1];
+    if (!TEST_false(ossl_quic_rstream_queue_data(rstream, pkt[1],
+            tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, tsc->tsc_fin)))
+        goto err;
+
+    if (!TEST_size_t_eq(ossl_quic_rstream_get_range_count(rstream),
+            tsc->tsc_ranges_exp)) {
+        TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+        goto err;
+    }
+
+    if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream),
+            tsc->tsc_chunks_exp)) {
+        TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+        goto err;
+    }
+
+    if (!TEST_size_t_eq(pkt_test_refcount(pkt[0]), 2))
+        goto err;
+
+    if (!TEST_size_t_eq(pkt_test_refcount(pkt[1]), 1))
+        goto err;
+
+    if (!TEST_int_eq(ch->protocol_error, 1))
+        goto err;
+
+    ok = 1;
+err:
+    /*
+     * the data from rstream has not been consumed,
+     * references to packets are still retained there.
+     * therefore we need to free rstream before freeing
+     * pkckets.
+     */
     ossl_quic_rstream_free(rstream);
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
+        pkt_test_free(pkt[i]);
+
     ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);

     return ok;
 }
@@ -1879,12 +2297,15 @@ int setup_tests(void)
     ADD_TEST(test_rstream_pkt);
     ADD_TEST(test_rstream_pkt_overhead);
     ADD_ALL_TESTS(test_rstream_reorder, 40);
+    ADD_TEST(test_rstream_dstorage_two_sided_overlap);
     ADD_TEST(test_rstream_chunk_partial_overlap);
     ADD_TEST(test_rstream_chunk_full_overlap);
     ADD_TEST(test_rstream_range_overlap);
     ADD_TEST(test_rstream_prepend_byte_chunks);
     ADD_TEST(test_rstream_append_byte_chunks);
     ADD_TEST(test_rstream_mix_chunks);
+    ADD_TEST(test_final_size_violation_fin_first);
+    ADD_TEST(test_final_size_violation_data_first);

     return 1;
 }