Commit 1121081b2a for openssl.org

commit 1121081b2a4667a77c641e9a4d00257d4c8342b6
Author: Alexandr Nedvedicky <sashan@openssl.org>
Date:   Mon Jul 27 09:54:00 2026 +0200

    Limit packet buffer overhead to ~64kB per stream.

    There is currently no limit on how many bytes of packet
    buffers each stream can hold in memory. To monitor and limit
    the size of packet buffers used by stream the change accounts
    so called stream chunk overhead which is a delta between
    actual datagram size of packet delivering the particular
    stream chunk and chunk itself. As soon as the chunk overhead
    exceeds ~64kB for all stream chunks kept in receive buffer,
    the newly received chunks will be moved from packet buffer
    to stream buffer.

    The packet buffer overhead limit is held in newly introduced
    structure QUIC_RSTREAM_QPARAM (RX stream quality parameter).
    If new additional quality parameters are added, then those
    should be part of QUIC_RSTREAM_QPARAM structure.

    this changeset introduces QUIC_RSTREAM_QPARAM

    the the rsqp (reead stream quality parameter) enables
    channel to control quality of RX stream. quality currently
    means the packet buffer overhead. more parameters
    may be added later.

    Fixes: CVE-2026-54873
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Merge-date: Sat Sep 26 11:35:32 2026
    Merged-from: https://github.com/openssl/openssl/pull/32038

diff --git a/include/internal/quic_predef.h b/include/internal/quic_predef.h
index 7d666c431a..485b07d5bf 100644
--- a/include/internal/quic_predef.h
+++ b/include/internal/quic_predef.h
@@ -43,6 +43,7 @@ typedef struct quic_conn_st QUIC_CONNECTION;
 typedef struct quic_xso_st QUIC_XSO;
 typedef struct quic_listener_st QUIC_LISTENER;
 typedef struct quic_domain_st QUIC_DOMAIN;
+typedef struct quic_rstream_qparm_st QUIC_RSTREAM_QPARM;

 #endif

diff --git a/include/internal/quic_stream.h b/include/internal/quic_stream.h
index b9431831d0..2d0d7d9475 100644
--- a/include/internal/quic_stream.h
+++ b/include/internal/quic_stream.h
@@ -320,7 +320,7 @@ void ossl_quic_sstream_set_cleanse(QUIC_SSTREAM *qss, int cleanse);
  * is read by application. `statm` is queried for current rtt.
  */
 QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc,
-    OSSL_STATM *statm);
+    OSSL_STATM *statm, QUIC_RSTREAM_QPARM *rsqp);

 /*
  * Frees a QUIC_RSTREAM and any associated storage.
@@ -407,6 +407,9 @@ size_t ossl_quic_rstream_get_chunk_count(QUIC_RSTREAM *qrs);
  * returns the number of stream ranges kept in rstream
  */
 size_t ossl_quic_rstream_get_range_count(QUIC_RSTREAM *qrs);
+
+QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void);
+void ossl_quic_rstream_qparm_destroy(QUIC_RSTREAM_QPARM *rsqp);
 #endif

 #endif
diff --git a/include/internal/quic_strm_reas.h b/include/internal/quic_strm_reas.h
index 1d1af12c74..a2d958a7ad 100644
--- a/include/internal/quic_strm_reas.h
+++ b/include/internal/quic_strm_reas.h
@@ -31,12 +31,13 @@ typedef struct sframe_set_t {
     /* Cleanse data on release? */
     int cleanse;
     int move_buffers;
+    QUIC_RSTREAM_QPARM *rsqp;
 } SFRAME_SET;

 /*
  * Initializes the stream frame list fs.
  */
-void ossl_sframe_set_init(SFRAME_SET *fs);
+void ossl_sframe_set_init(SFRAME_SET *fs, QUIC_RSTREAM_QPARM *rsqp);

 /*
  * Destroys the stream frame list fs releasing any data
diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index 73065fd7e0..66b90c5876 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -308,8 +308,13 @@ static int ch_init(QUIC_CHANNEL *ch)
             goto err;
     }

+    ch->rsqp = ossl_quic_rstream_qparm_new();
+    if (ch->rsqp == NULL)
+        goto err;
+
     for (pn_space = QUIC_PN_SPACE_INITIAL; pn_space < QUIC_PN_SPACE_NUM; ++pn_space) {
-        ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL);
+        /* 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;
     }
@@ -409,6 +414,9 @@ static void ch_cleanup(QUIC_CHANNEL *ch)
         ch->crypto_recv[pn_space] = NULL;
     }

+    ossl_quic_rstream_qparm_destroy(ch->rsqp);
+    ch->rsqp = NULL;
+
     ossl_qrx_pkt_release(ch->qrx_pkt);
     ch->qrx_pkt = NULL;

@@ -3838,7 +3846,7 @@ static int ch_init_new_stream(QUIC_CHANNEL *ch, QUIC_STREAM *qs,
             goto err;

     if (can_recv)
-        if ((qs->rstream = ossl_quic_rstream_new(NULL, NULL)) == NULL)
+        if ((qs->rstream = ossl_quic_rstream_new(NULL, NULL, ch->rsqp)) == NULL)
             goto err;

     /* TXFC */
diff --git a/ssl/quic/quic_channel_local.h b/ssl/quic/quic_channel_local.h
index 7475f623c9..2d55f95f2c 100644
--- a/ssl/quic/quic_channel_local.h
+++ b/ssl/quic/quic_channel_local.h
@@ -525,6 +525,12 @@ struct quic_channel_st {

     /* Title for qlog purposes. We own this copy. */
     char *qlog_title;
+
+    /*
+     * RX stream quality parameter.
+     */
+    QUIC_RSTREAM_QPARM *rsqp;
+
     /*
      * number of path responses waiting to be dispatched
      * from control frame queue (CFQ)
diff --git a/ssl/quic/quic_rstream.c b/ssl/quic/quic_rstream.c
index a2dc038e4f..1ac496858b 100644
--- a/ssl/quic/quic_rstream.c
+++ b/ssl/quic/quic_rstream.c
@@ -28,14 +28,14 @@ struct quic_rstream_st {
 #endif

 QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc,
-    OSSL_STATM *statm)
+    OSSL_STATM *statm, QUIC_RSTREAM_QPARM *rsqp)
 {
     QUIC_RSTREAM *ret = OPENSSL_zalloc(sizeof(*ret));

     if (ret == NULL)
         return NULL;

-    ossl_sframe_set_init(&ret->fs);
+    ossl_sframe_set_init(&ret->fs, rsqp);
     ret->rxfc = rxfc;
     ret->statm = statm;
     return ret;
diff --git a/ssl/quic/quic_strm_reas.c b/ssl/quic/quic_strm_reas.c
index 34f399db1d..08dfb1d531 100644
--- a/ssl/quic/quic_strm_reas.c
+++ b/ssl/quic/quic_strm_reas.c
@@ -9,6 +9,7 @@

 #include "internal/uint_set.h"
 #include "internal/common.h"
+#include "internal/quic_stream.h"
 #include "internal/quic_strm_reas.h"
 #include "internal/list.h"

@@ -21,6 +22,13 @@

 #define DIRECT_STORAGE_SZ (2 * sizeof(void *))

+/*
+ * Maximal allocation overhead in packet buffers is ~64kB for
+ * connection. If ~64kB limit is exceeded, then the newly received
+ * chunks are moved from the packet to the stream buffer.
+ */
+#define PKT_BUFFER_OVERHEAD_TRESHOLD (65535)
+
 /*
  * storage type indicates where stream data bytes
  * are stored.
@@ -50,6 +58,11 @@ struct stream_chunk_t {
     } sc_storage_u;
 };

+struct quic_rstream_qparm_st {
+    size_t rsqp_pkt_overhead_treshold;
+    size_t rsqp_pkt_overhead_sz;
+};
+
 #define sc_data sc_data_u.u_data
 #define sc_data_w sc_data_u.u_data_w

@@ -61,6 +74,7 @@ DEFINE_LIST_OF(sc, struct stream_chunk_t);

 #define SCHUNK_SIZE(_sc) ((_sc)->sc_range.end - (_sc)->sc_range.start)
 #define SRANGE_SIZE(_sr) ((_sr)->sr_range.end - (_sr)->sr_range.start)
+#define SCHUNK_OVERHEAD(_pkt, _sc) ((_pkt)->datagram_len - SCHUNK_SIZE(_sc))

 /*
  * Stream range keeps list of continuous stream chunks. The range
@@ -88,6 +102,18 @@ OSSL_RBT_GENERATE(srange, stream_range_t, sr_rbe, srange_cmp);

 #define UINT64_TO_SIZE_T(_x) ((size_t)(((_x) > SIZE_MAX) ? SIZE_MAX : (_x)))

+static void rsqp_add_overhead(QUIC_RSTREAM_QPARM *rsqp, size_t sc_overhead)
+{
+    if (rsqp != NULL)
+        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;
+}
+
 /*
   * Cleansing (SSL_OP_CLEANSE_PLAINTEXT) must write through the const
   * data pointers received from ossl_sframe_set_insert(), which may
@@ -192,27 +218,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)
 {
-    /*
-     * the function decides whether stream data should be moved
-     * from packet buffer to stream buffer or if data can stay
-     * at packet buffer.
-     *
-     * Keeping the data at packet saves yet another buffer
-     * allocation at heap (+ data transfer). On the other hand
-     * it opens door to malicious peer to force stack to use more
-     * memory than necessary.
-     *
-     * The function here should asses a current stream quality:
-     *   how many stream chunks are there
-     *   the time elapsed since the arrival of earlier chunk
-     *   the time elapsed since the application consumed the data
-     *   the size of the chunk compared with the whole packet size
-     *   the size of chunk with respect to DIRECT_STORAGE_SZ
-     *   ...
-     * the code to collect those parameters is still missing, once
-     * this gap will be filled this function will be able to
-     * make the decision.
-     */
+    if (fs->rsqp != NULL
+        && fs->rsqp->rsqp_pkt_overhead_sz >= fs->rsqp->rsqp_pkt_overhead_treshold)
+        return 0;

     return 1;
 }
@@ -222,6 +230,7 @@ static struct stream_chunk_t *new_schunk(SFRAME_SET *fs, OSSL_QRX_PKT *pkt,
 {
     struct stream_chunk_t *sc;
     uint64_t rsize;
+    size_t overhead;

     if (pkt == NULL)
         return NULL;
@@ -230,14 +239,29 @@ static struct stream_chunk_t *new_schunk(SFRAME_SET *fs, OSSL_QRX_PKT *pkt,
     if (sc == NULL)
         return NULL;

+    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) {
         sc->sc_st = ST_TYPE_PKT;
         sc->sc_pkt = pkt;
         ossl_qrx_pkt_up_ref(pkt);
         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);
     } else {
-        rsize = r->end - r->start;
+        /*
+         * 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);
@@ -277,6 +301,16 @@ 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));
+        rsqp_sub_overhead(fs->rsqp,
+            UINT64_TO_SIZE_T(SCHUNK_OVERHEAD(sc->sc_pkt, sc)));
         ossl_qrx_pkt_release(sc->sc_pkt);
         break;
     case ST_TYPE_HEAP:
@@ -338,10 +372,11 @@ static struct stream_range_t *create_range(SFRAME_SET *fs,
     return sr;
 }

-void ossl_sframe_set_init(SFRAME_SET *fs)
+void ossl_sframe_set_init(SFRAME_SET *fs, QUIC_RSTREAM_QPARM *rsqp)
 {
     memset(fs, 0, sizeof(*fs));
     OSSL_RBT_INIT(srange, &fs->ranges);
+    fs->rsqp = rsqp;
 }

 static uint64_t get_sc_dstorage_sz(struct stream_chunk_t *sc)
@@ -715,6 +750,7 @@ static int chop_range(SFRAME_SET *fs, struct stream_range_t *sr,
     uint64_t new_end)
 {
     struct stream_chunk_t *sc;
+    size_t unused_sz;

     assert(sr->sr_range.end >= new_end);

@@ -744,6 +780,15 @@ static int chop_range(SFRAME_SET *fs, struct stream_range_t *sr,
     sc->sc_range.end = new_end;
     sr->sr_range.end = new_end;

+    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);
+    }
+
     return 1;
 }

@@ -764,7 +809,8 @@ static struct stream_range_t *merge_ranges(SFRAME_SET *fs,
      * sub_sr and super_sr are equal ranges (sets)  super_sr
      * sub_sr is subset of super_sr (super_sr includes sub_sr).
      */
-    assert(super_sr->sr_range.start <= sub_sr->sr_range.start && super_sr->sr_range.end >= sub_sr->sr_range.end);
+    assert(super_sr->sr_range.start <= sub_sr->sr_range.start
+        && super_sr->sr_range.end >= sub_sr->sr_range.end);

     DEBUG_PRINT(stderr, "%s super: %p [ %llu, %llu ], sub: %p [ %llu, %llu]\n",
         OPENSSL_FUNC, (void *)super_sr, super_sr->sr_range.start,
@@ -953,7 +999,7 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt,
             (void *)sr, sr->sr_range.start, sr->sr_range.end);

         /*
-         * sandwich, append, prepend can still be improved to handle
+         * Following calls can still be improved to handle
          * chunks with direct storage better, but I don't think it's
          * worth the effort. out of order short data chunks (less
          * than DIRECT_STORAGE_SZ) should be considered exceptional.
@@ -1006,23 +1052,27 @@ int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt,
                 adjacent_sr->sr_range.end);
             fs->stream_ranges--;

-            if (sr->sr_range.start <= adjacent_sr->sr_range.start && sr->sr_range.end >= adjacent_sr->sr_range.end) {
+            if (sr->sr_range.start <= adjacent_sr->sr_range.start
+                && sr->sr_range.end >= adjacent_sr->sr_range.end) {
                 /*
                  *  adjacent_sr subset of sr
                  */
                 joined_sr = merge_ranges(fs, sr, adjacent_sr);
-            } else if (sr->sr_range.start >= adjacent_sr->sr_range.start && sr->sr_range.end <= adjacent_sr->sr_range.end) {
+            } else if (sr->sr_range.start >= adjacent_sr->sr_range.start
+                && sr->sr_range.end <= adjacent_sr->sr_range.end) {
                 /*
                  *  sr subset of adjacent_sr
                  */
                 joined_sr = merge_ranges(fs, adjacent_sr, sr);
-            } else if (sr->sr_range.start < adjacent_sr->sr_range.start && sr->sr_range.end >= adjacent_sr->sr_range.start) {
+            } else if (sr->sr_range.start < adjacent_sr->sr_range.start
+                && sr->sr_range.end >= adjacent_sr->sr_range.start) {
                 /*
                  * adjacent_sr follows sr
                  */
                 assert(sr->sr_range.end < adjacent_sr->sr_range.end);
                 joined_sr = append_range(fs, sr, adjacent_sr);
-            } else if (sr->sr_range.start <= adjacent_sr->sr_range.end && sr->sr_range.end > adjacent_sr->sr_range.end) {
+            } else if (sr->sr_range.start <= adjacent_sr->sr_range.end
+                && sr->sr_range.end > adjacent_sr->sr_range.end) {
                 /*
                  *  sr follows adjacent_sr
                  */
@@ -1185,6 +1235,7 @@ int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t new_offset)
 {
     struct stream_range_t *sr = OSSL_RBT_MIN(srange, &fs->ranges);
     struct stream_chunk_t *sc, *save_sc;
+    size_t unused_sz;

     if (new_offset == fs->offset)
         return 1;
@@ -1232,6 +1283,15 @@ int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t new_offset)
         sr->sr_range.start = new_offset;
         DEBUG_PRINT(stderr, "[ %lli, %llu ]\n",
             sr->sr_range.start, sr->sr_range.end);
+
+        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);
+        }
     }

     return 1;
@@ -1250,3 +1310,24 @@ int ossl_sframe_set_avail(SFRAME_SET *fs, uint64_t *avail, int *fin)
     *fin = (fs->fin && fs->offset + *avail == fs->fin_off) ? 1 : 0;
     return 1;
 }
+
+QUIC_RSTREAM_QPARM *ossl_quic_rstream_qparm_new(void)
+{
+    QUIC_RSTREAM_QPARM *rsqp;
+
+    rsqp = OPENSSL_malloc(sizeof(QUIC_RSTREAM_QPARM));
+    if (rsqp != NULL) {
+        rsqp->rsqp_pkt_overhead_treshold = PKT_BUFFER_OVERHEAD_TRESHOLD;
+        rsqp->rsqp_pkt_overhead_sz = 0;
+    }
+
+    return rsqp;
+}
+
+void ossl_quic_rstream_qparm_destroy(QUIC_RSTREAM_QPARM *rsqp)
+{
+    if (rsqp != NULL) {
+        assert(rsqp->rsqp_pkt_overhead_sz == 0);
+        OPENSSL_free(rsqp);
+    }
+}
diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c
index f9584aae42..468a096439 100644
--- a/test/quic_stream_test.c
+++ b/test/quic_stream_test.c
@@ -6,10 +6,13 @@
  * in the file LICENSE in the source distribution or at
  * https://www.openssl.org/source/license.html
  */
+#include <string.h>
+
 #include "internal/packet.h"
 #include "internal/quic_record_rx.h"
 #include "internal/quic_stream.h"
 #include "../ssl/quic/quic_record_rx_local.h"
+#include "internal/nelem.h"
 #include "testutil.h"

 /*
@@ -403,116 +406,13 @@ static int test_single_copy_read(QUIC_RSTREAM *qrs,
     return 1;
 }

-static const unsigned char simple_data[] = "Hello world! And thank you for all the fish!";
-
-static int test_rstream_simple(int idx)
-{
-    QUIC_RSTREAM *rstream = NULL;
-    OSSL_QRX_PKT *pkt[8] = { NULL };
-    int ret = 0;
-    unsigned char buf[sizeof(simple_data)];
-    size_t readbytes = 0, avail = 0, i;
-    int fin = 0;
-    int use_sc = idx % 2;
-    int (*read_fn)(QUIC_RSTREAM *, unsigned char *, size_t, size_t *,
-        int *)
-        = use_sc ? test_single_copy_read
-                 : ossl_quic_rstream_read;
-
-    /* every frame arrives in a packet, as it does in production */
-    for (i = 0; i < OSSL_NELEM(pkt); ++i)
-        if (!TEST_ptr(pkt[i] = pkt_test_new(1200)))
-            goto err;
-
-    if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL)))
-        goto err;
-
-    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[0], 5,
-            simple_data + 5, 10, 0))
-        || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[1],
-            sizeof(simple_data) - 1,
-            simple_data + sizeof(simple_data) - 1,
-            1, 1))
-        || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf),
-            &readbytes, &fin))
-        || !TEST_false(fin)
-        || !TEST_size_t_eq(readbytes, 0)
-        || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[2],
-            sizeof(simple_data) - 10,
-            simple_data + sizeof(simple_data) - 10,
-            10, 1))
-        || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[3], 0,
-            simple_data, 1, 0))
-        || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf),
-            &readbytes, &fin))
-        || !TEST_false(fin)
-        || !TEST_size_t_eq(readbytes, 1)
-        || !TEST_mem_eq(buf, 1, simple_data, 1)
-        || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[4],
-            0, simple_data,
-            10, 0))
-        || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[5],
-            sizeof(simple_data),
-            NULL,
-            0, 1))
-        || !TEST_true(ossl_quic_rstream_peek(rstream, buf, sizeof(buf),
-            &readbytes, &fin))
-        || !TEST_false(fin)
-        || !TEST_size_t_eq(readbytes, 15)
-        || !TEST_mem_eq(buf, 15, simple_data, 15)
-        || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[6],
-            15,
-            simple_data + 15,
-            sizeof(simple_data) - 15, 1))
-        || !TEST_true(ossl_quic_rstream_available(rstream, &avail, &fin))
-        || !TEST_true(fin)
-        || !TEST_size_t_eq(avail, sizeof(simple_data))
-        || !TEST_true(read_fn(rstream, buf, 2, &readbytes, &fin))
-        || !TEST_false(fin)
-        || !TEST_size_t_eq(readbytes, 2)
-        || !TEST_mem_eq(buf, 2, simple_data, 2)
-        || !TEST_true(read_fn(rstream, buf + 2, 12, &readbytes, &fin))
-        || !TEST_false(fin)
-        || !TEST_size_t_eq(readbytes, 12)
-        || !TEST_mem_eq(buf + 2, 12, simple_data + 2, 12)
-        || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[7],
-            sizeof(simple_data),
-            NULL,
-            0, 1))
-        || !TEST_true(read_fn(rstream, buf + 14, 5, &readbytes, &fin))
-        || !TEST_false(fin)
-        || !TEST_size_t_eq(readbytes, 5)
-        || !TEST_mem_eq(buf, 14 + 5, simple_data, 14 + 5)
-        || !TEST_true(read_fn(rstream, buf + 14 + 5, sizeof(buf) - 14 - 5,
-            &readbytes, &fin))
-        || !TEST_true(fin)
-        || !TEST_size_t_eq(readbytes, sizeof(buf) - 14 - 5)
-        || !TEST_mem_eq(buf, sizeof(buf), simple_data, sizeof(simple_data))
-        || !TEST_true(read_fn(rstream, buf, sizeof(buf), &readbytes, &fin))
-        || !TEST_true(fin)
-        || !TEST_size_t_eq(readbytes, 0))
-        goto err;
-
-    ret = 1;
-
-err:
-    ossl_quic_rstream_free(rstream);
-    /* All the references held by the stream must have been released */
-    for (i = 0; i < OSSL_NELEM(pkt); ++i) {
-        if (pkt[i] != NULL
-            && !TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
-            ret = 0;
-        pkt_test_free(pkt[i]);
-    }
-    return ret;
-}
-
 static int test_rstream_random(int idx)
 {
     unsigned char *bulk_data = NULL;
     unsigned char *read_buf = NULL;
     QUIC_RSTREAM *rstream = NULL;
     OSSL_QRX_PKT **pkts = NULL;
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
     size_t i, read_off, queued_min, queued_max, num_pkts = 0;
     const size_t data_size = 10000;
     /* At most two frames are queued per each of the 100 * 10 iterations */
@@ -524,7 +424,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(rstream = ossl_quic_rstream_new(NULL, NULL)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())
+        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;

     if (idx % 3 == 0)
@@ -644,6 +545,7 @@ err:
         }
         OPENSSL_free(pkts);
     }
+    ossl_quic_rstream_qparm_destroy(rsqp);
     OPENSSL_free(bulk_data);
     OPENSSL_free(read_buf);
     return ret;
@@ -656,6 +558,7 @@ err:
 static int test_rstream_pkt(void)
 {
     QUIC_RSTREAM *rstream = 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];
     size_t readbytes = 0, avail = 0, i;
@@ -668,7 +571,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(rstream = ossl_quic_rstream_new(NULL, NULL, 0)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())
+        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;

     /* A buffered frame holds a reference to its packet */
@@ -742,7 +646,7 @@ static int test_rstream_pkt(void)
      * data, leaving the surrounding bytes intact.
      */
     memset(cbuf, 0xAA, sizeof(cbuf));
-    if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0)))
+    if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;
     ossl_quic_rstream_set_cleanse(rstream, 1);
     if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_a, 0,
@@ -761,6 +665,7 @@ static int test_rstream_pkt(void)

 err:
     ossl_quic_rstream_free(rstream);
+    ossl_quic_rstream_qparm_destroy(rsqp);
     pkt_test_free(pkt_a);
     pkt_test_free(pkt_b);
     pkt_test_free(pkt_c);
@@ -777,6 +682,7 @@ static int test_rstream_pkt_overhead(void)
 {
     QUIC_RSTREAM *rstream = NULL;
     OSSL_QRX_PKT **pkt = NULL;
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
     unsigned char *data = NULL, *buf = NULL;
     const size_t framesz = 8;
     const size_t nframes = 4096; /* far past a 64 KiB overhead limit */
@@ -788,7 +694,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(rstream = ossl_quic_rstream_new(NULL, NULL, 0)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())
+        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;

     for (i = 0; i < total; ++i)
@@ -836,6 +743,7 @@ err:
     if (pkt != NULL)
         for (i = 0; i < nframes; ++i)
             pkt_test_free(pkt[i]);
+    ossl_quic_rstream_qparm_destroy(rsqp);
     OPENSSL_free(pkt);
     OPENSSL_free(data);
     OPENSSL_free(buf);
@@ -857,6 +765,7 @@ static int test_rstream_reorder(int idx)
     unsigned char *data = NULL, *buf = NULL, *arena = NULL, *ap;
     QUIC_RSTREAM *rstream = NULL;
     OSSL_QRX_PKT **pkts = NULL;
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
     const size_t data_size = 4096;
     const size_t framesz = 1 + (size_t)(idx % 17);
     const int cleanse = (idx & 1);
@@ -870,7 +779,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(rstream = ossl_quic_rstream_new(NULL, NULL, 0)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new())
+        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
         goto err;

     if (cleanse)
@@ -951,6 +861,7 @@ err:
         }
         OPENSSL_free(pkts);
     }
+    ossl_quic_rstream_qparm_destroy(rsqp);
     OPENSSL_free(order);
     OPENSSL_free(arena);
     OPENSSL_free(data);
@@ -958,14 +869,1022 @@ err:
     return ret;
 }

+#define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \
+                     "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+
+typedef struct test_stream_chunk {
+    const unsigned char *tsc_data;
+    uint64_t tsc_off; /* start == offset */
+    uint64_t tsc_len; /* end = offset + len */
+    int tsc_fin;
+    size_t tsc_chunks_exp;
+    size_t tsc_ranges_exp;
+} TEST_STREAM_CHUNK_T;
+
+static int test_rstream_chunk_partial_overlap(void)
+{
+    unsigned char data[4096];
+    unsigned char read_buf[4096];
+    TEST_STREAM_CHUNK_T tsc_buf[7];
+    OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    TEST_STREAM_CHUNK_T *tsc;
+    QUIC_RSTREAM *rstream;
+    size_t readbytes;
+    unsigned int i;
+    unsigned int send_order[7];
+    int fin = 0;
+    int ok = 0;
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+        return 0;
+
+    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));
+
+    /*
+     * 1 range, (0, 120) with 5 stream chunks. there is a partial overlap
+     * between chunks.
+     */
+    tsc = &tsc_buf[0];
+    tsc->tsc_data = &data[0];
+    tsc->tsc_off = 0;
+    tsc->tsc_len = 32;
+    tsc->tsc_chunks_exp = 5;
+    tsc->tsc_ranges_exp = 1;
+    send_order[6] = 0;
+
+    tsc = &tsc_buf[1];
+    tsc->tsc_data = &data[24];
+    tsc->tsc_off = 24;
+    tsc->tsc_len = 48;
+    tsc->tsc_chunks_exp = 5;
+    tsc->tsc_ranges_exp = 1;
+    send_order[4] = 1;
+
+    tsc = &tsc_buf[2];
+    tsc->tsc_data = &data[44];
+    tsc->tsc_off = 44;
+    tsc->tsc_len = 20;
+    tsc->tsc_chunks_exp = 4;
+    tsc->tsc_ranges_exp = 1;
+    send_order[3] = 2;
+
+    tsc = &tsc_buf[3];
+    tsc->tsc_data = &data[55];
+    tsc->tsc_off = 55;
+    tsc->tsc_len = 50;
+    tsc->tsc_chunks_exp = 1;
+    tsc->tsc_ranges_exp = 1;
+    send_order[0] = 3;
+
+    tsc = &tsc_buf[4];
+    tsc->tsc_data = &data[100];
+    tsc->tsc_off = 100;
+    tsc->tsc_len = 19;
+    tsc->tsc_chunks_exp = 3;
+    tsc->tsc_ranges_exp = 1;
+    send_order[2] = 4;
+
+    tsc = &tsc_buf[5];
+    tsc->tsc_data = &data[119];
+    tsc->tsc_off = 119;
+    tsc->tsc_len = 1;
+    tsc->tsc_fin = 1;
+    tsc->tsc_chunks_exp = 2;
+    tsc->tsc_ranges_exp = 2;
+    send_order[1] = 5;
+
+    /*
+     * add duplicate chunk, the chunk range 48, 64 exists already
+     * in the range, thus no additional stream chunk will be created.
+     */
+    tsc = &tsc_buf[6];
+    tsc->tsc_data = &data[49];
+    tsc->tsc_off = 48;
+    tsc->tsc_len = 16;
+    tsc->tsc_chunks_exp = 5;
+    tsc->tsc_ranges_exp = 1;
+    send_order[5] = 6;
+
+    /*
+     * send everything except offset 0.
+     */
+    assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt));
+    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))) {
+            TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+            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;
+        }
+
+        /*
+         * the offset 0 chunk is not transmitted in loop here,
+         * make sure the stream does not become readable.
+         */
+        if (!TEST_true(ossl_quic_rstream_peek(rstream, read_buf,
+                sizeof(read_buf), &readbytes, &fin))) {
+            TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+            goto err;
+        }
+
+        if (!TEST_false(fin)) {
+            TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+            goto err;
+        }
+
+        if (!TEST_size_t_eq(readbytes, 0)) {
+            TEST_info("%s failing iteration %u", OPENSSL_FUNC, i);
+            goto err;
+        }
+    }
+
+    /*
+     * although 6 chunks were inserted, we expect to find only 5 chunks
+     * in range, the last chunk was duplicate.
+     */
+    if (!TEST_size_t_eq(ossl_quic_rstream_get_chunk_count(rstream), 5))
+        goto err;
+
+    /*
+     * send offset 0 chunk, and try to read from stream.
+     */
+    pkt[i] = pkt_test_new(1200);
+    if (!TEST_ptr(pkt[i]))
+        goto err;
+    tsc = &tsc_buf[0];
+    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;
+
+    /*
+     * writing chunk offset 0 makes stream readable
+     */
+    if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf,
+            sizeof(read_buf), &readbytes, &fin)))
+        goto err;
+
+    /*
+     * we expect to read 120 bytes
+     */
+    if (!TEST_uint64_t_eq(readbytes, 120))
+        goto err;
+
+    /*
+     * the fin written by loop should be signaled too.
+     */
+    if (!TEST_true(fin))
+        goto err;
+
+    if (!TEST_mem_eq(read_buf, readbytes, data, readbytes))
+        goto err;
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
+        if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
+            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);
+
+    return ok;
+}
+
+/*
+ * use 64kB as datagram size to force QUIC stack
+ * to rach overhead threshold for all packets used
+ * by test. So all data will be moved to stream buffers
+ */
+#define MOVE_TRESHOLD 65535 * 2
+
+static int test_rstream_chunk_full_overlap(void)
+{
+    unsigned char data[4096];
+    unsigned char read_buf[4096];
+    TEST_STREAM_CHUNK_T tsc_buf[5];
+    OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    TEST_STREAM_CHUNK_T *tsc;
+    QUIC_RSTREAM *rstream;
+    size_t readbytes;
+    unsigned int i;
+    unsigned int send_order[5];
+    int fin;
+    int ok = 0;
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+        return 0;
+
+    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));
+
+    /*
+     * 1 range, (0, 256) with 5 stream chunks. 5 chunks overlap
+     * partially, The last chunk we insert overlaps the whole range.
+     */
+    tsc = &tsc_buf[0];
+    tsc->tsc_data = &data[0];
+    tsc->tsc_off = 0;
+    tsc->tsc_len = 256;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[4] = 0;
+
+    tsc = &tsc_buf[1];
+    tsc->tsc_data = &data[24];
+    tsc->tsc_off = 24;
+    tsc->tsc_len = 48;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 4;
+    send_order[3] = 1;
+
+    tsc = &tsc_buf[2];
+    tsc->tsc_data = &data[44];
+    tsc->tsc_off = 44;
+    tsc->tsc_len = 20;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 3;
+    send_order[2] = 2;
+
+    tsc = &tsc_buf[3];
+    tsc->tsc_data = &data[55];
+    tsc->tsc_off = 55;
+    tsc->tsc_len = 50;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[0] = 3;
+
+    tsc = &tsc_buf[4];
+    tsc->tsc_data = &data[100];
+    tsc->tsc_off = 100;
+    tsc->tsc_len = 20;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 2;
+    send_order[1] = 4;
+
+    assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt));
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++) {
+        pkt[i] = pkt_test_new(MOVE_TRESHOLD);
+        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, 0)))
+            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;
+        }
+    }
+
+    if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf,
+            sizeof(read_buf), &readbytes, &fin)))
+        goto err;
+
+    if (!TEST_false(fin))
+        goto err;
+
+    /*
+     * we expect to read 256 bytes
+     */
+    if (!TEST_uint64_t_eq(readbytes, 256))
+        goto err;
+
+    if (!TEST_mem_eq(read_buf, readbytes, data, readbytes))
+        goto err;
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
+        if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
+            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);
+
+    return ok;
+}
+
+static int test_rstream_range_overlap(void)
+{
+    unsigned char data[4096];
+    unsigned char read_buf[4096];
+    TEST_STREAM_CHUNK_T tsc_buf[9];
+    OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    TEST_STREAM_CHUNK_T *tsc;
+    QUIC_RSTREAM *rstream;
+    size_t readbytes;
+    unsigned int i;
+    int fin;
+    int ok = 0;
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+        return 0;
+
+    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));
+
+    /*
+     * start with 5 ranges,
+     */
+    tsc = &tsc_buf[0];
+    tsc->tsc_data = &data[0];
+    tsc->tsc_off = 0;
+    tsc->tsc_len = 64;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+
+    tsc = &tsc_buf[1];
+    tsc->tsc_data = &data[128];
+    tsc->tsc_off = 128;
+    tsc->tsc_len = 64;
+    tsc->tsc_ranges_exp = 2;
+    tsc->tsc_chunks_exp = 2;
+
+    tsc = &tsc_buf[2];
+    tsc->tsc_data = &data[256];
+    tsc->tsc_off = 256;
+    tsc->tsc_len = 64;
+    tsc->tsc_ranges_exp = 3;
+    tsc->tsc_chunks_exp = 3;
+
+    tsc = &tsc_buf[3];
+    tsc->tsc_data = &data[384];
+    tsc->tsc_off = 384;
+    tsc->tsc_len = 64;
+    tsc->tsc_ranges_exp = 4;
+    tsc->tsc_chunks_exp = 4;
+
+    tsc = &tsc_buf[4];
+    tsc->tsc_data = &data[512];
+    tsc->tsc_off = 512;
+    tsc->tsc_len = 64;
+    tsc->tsc_ranges_exp = 5;
+    tsc->tsc_chunks_exp = 5;
+
+    /*
+     * chunk 6 appends data to last range
+     */
+    tsc = &tsc_buf[5];
+    tsc->tsc_data = &data[548];
+    tsc->tsc_off = 548;
+    tsc->tsc_len = 220;
+    tsc->tsc_ranges_exp = 5;
+    tsc->tsc_chunks_exp = 6;
+
+    /*
+     * chunk 7 prepends data to last range
+     */
+    tsc = &tsc_buf[6];
+    tsc->tsc_data = &data[480];
+    tsc->tsc_off = 480;
+    tsc->tsc_len = 64;
+    tsc->tsc_chunks_exp = 7;
+    tsc->tsc_ranges_exp = 5;
+
+    /*
+     * chunk 8 fully covers range 4 and partially
+     * overlaps with 5
+     */
+    tsc = &tsc_buf[7];
+    tsc->tsc_data = &data[364];
+    tsc->tsc_off = 364;
+    tsc->tsc_len = 500;
+    /*
+     * note the expected number of chunks actually decreases!!!
+     * here is what happened:
+     * chunk [ 364, 864 ] is going to be inserted into range number 4
+     * which spans over [ 384, 448 ]. After chunk is inserted the
+     * 4th range looks as follows:
+     *   [ 364, 864 ], it contains 3 chunks:
+     *      [ 364, 384 ]
+     *      [ 384, 448 ]
+     *      [ 448, 864 ]
+     *
+     * however the 4th range now overlaps with 5th range [ 480, 768 ].
+     * the fifth range also contains 3 chunks:
+     *   [ 480, 768 ]
+     *      [ 480, 512 ]
+     *      [ 512, 576 ]
+     *      [ 576, 768 ]
+     * as you can see there is a full overlap. The new range is going
+     * to look as:
+     *   [ 364, 864 ]
+     *      [ 364, 384 ]
+     *      [ 384, 448 ]
+     *      [ 448, 864 ]
+     * the 5th range is gone with all its ranges. the ranges 1, 2 and 3
+     * where not touched so far, each of them contain one range, this
+     * makes total 6 ranges.
+     */
+    tsc->tsc_chunks_exp = 4;
+    tsc->tsc_ranges_exp = 4;
+
+    /*
+     * chunk 9 partially overlaps with the first and
+     * the last range,
+     */
+    tsc = &tsc_buf[8];
+    tsc->tsc_data = &data[32];
+    tsc->tsc_off = 32;
+    tsc->tsc_len = 500;
+    tsc->tsc_chunks_exp = 3;
+    tsc->tsc_ranges_exp = 1;
+
+    assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt));
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++) {
+        pkt[i] = pkt_test_new(1200);
+        if (!TEST_ptr(pkt[i]))
+            goto err;
+        tsc = &tsc_buf[i];
+        if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[i],
+                tsc->tsc_off, tsc->tsc_data, tsc->tsc_len, 0)))
+            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;
+        }
+    }
+
+    if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf,
+            sizeof(read_buf), &readbytes, &fin)))
+        goto err;
+
+    if (!TEST_false(fin))
+        goto err;
+
+    /*
+     * we expect to read 864 bytes
+     */
+    if (!TEST_uint64_t_eq(readbytes, 864))
+        goto err;
+
+    if (!TEST_mem_eq(read_buf, readbytes, data, readbytes))
+        goto err;
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
+        if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
+            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);
+
+    return ok;
+}
+
+static int test_rstream_prepend_byte_chunks(void)
+{
+    unsigned char data[4096];
+    unsigned char read_buf[4096];
+    TEST_STREAM_CHUNK_T tsc_buf[6];
+    OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    TEST_STREAM_CHUNK_T *tsc;
+    QUIC_RSTREAM *rstream;
+    size_t readbytes;
+    unsigned int i;
+    unsigned int send_order[6];
+    int fin;
+    int ok = 0;
+
+    if (sizeof(void *) != 8) {
+        TEST_info("%s is implemented for 64-bit platforms only", OPENSSL_FUNC);
+        return 1;
+    }
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+        return 0;
+
+    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));
+
+    /*
+     * here we test one byte stream chunks. this test verifies
+     * more short stream chunks are stored in single chunk buffer.
+     */
+    tsc = &tsc_buf[0];
+    tsc->tsc_data = &data[0];
+    tsc->tsc_off = 0;
+    tsc->tsc_len = 1;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[4] = 0;
+
+    tsc = &tsc_buf[1];
+    tsc->tsc_data = &data[1];
+    tsc->tsc_off = 1;
+    tsc->tsc_len = 2;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[3] = 1;
+
+    tsc = &tsc_buf[2];
+    tsc->tsc_data = &data[3];
+    tsc->tsc_off = 3;
+    tsc->tsc_len = 5;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[2] = 2;
+
+    tsc = &tsc_buf[3];
+    tsc->tsc_data = &data[8];
+    tsc->tsc_off = 8;
+    tsc->tsc_len = 4;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[1] = 3;
+
+    tsc = &tsc_buf[4];
+    tsc->tsc_data = &data[12];
+    tsc->tsc_off = 12;
+    tsc->tsc_len = 4;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[0] = 4;
+
+    tsc = &tsc_buf[5];
+    tsc->tsc_data = &data[12];
+    tsc->tsc_off = 12;
+    tsc->tsc_len = 12;
+    tsc->tsc_ranges_exp = 1;
+    /*
+     * this chunk partially overlaps. It does not fit to stream chunk buffer
+     * created earlier, therefore a new stream chunk will be created.
+     */
+    tsc->tsc_chunks_exp = 2;
+    send_order[5] = 5;
+
+    assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt));
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++) {
+        pkt[i] = pkt_test_new(MOVE_TRESHOLD);
+        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, 0)))
+            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;
+        }
+    }
+
+    if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf,
+            sizeof(read_buf), &readbytes, &fin)))
+        goto err;
+
+    if (!TEST_false(fin))
+        goto err;
+
+    /*
+     * we expect to read 24 bytes
+     */
+    if (!TEST_uint64_t_eq(readbytes, 24))
+        goto err;
+
+    if (!TEST_mem_eq(read_buf, readbytes, data, readbytes))
+        goto err;
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
+        if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
+            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);
+
+    return ok;
+}
+
+static int test_rstream_append_byte_chunks(void)
+{
+    unsigned char data[4096];
+    unsigned char read_buf[4096];
+    TEST_STREAM_CHUNK_T tsc_buf[6];
+    OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    TEST_STREAM_CHUNK_T *tsc;
+    QUIC_RSTREAM *rstream;
+    size_t readbytes;
+    unsigned int i;
+    unsigned int send_order[6];
+    int fin;
+    int ok = 0;
+
+    if (sizeof(void *) != 8) {
+        TEST_info("%s is implemented for 64-bit platforms only", OPENSSL_FUNC);
+        return 1;
+    }
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+        return 0;
+
+    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));
+
+    /*
+     * here we test one byte stream chunks. this test verifies
+     * more short stream chunks are stored in single chunk buffer.
+     */
+    tsc = &tsc_buf[0];
+    tsc->tsc_data = &data[0];
+    tsc->tsc_off = 0;
+    tsc->tsc_len = 1;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[0] = 0;
+
+    tsc = &tsc_buf[1];
+    tsc->tsc_data = &data[1];
+    tsc->tsc_off = 1;
+    tsc->tsc_len = 2;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[1] = 1;
+
+    tsc = &tsc_buf[2];
+    tsc->tsc_data = &data[3];
+    tsc->tsc_off = 3;
+    tsc->tsc_len = 5;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[2] = 2;
+
+    tsc = &tsc_buf[3];
+    tsc->tsc_data = &data[8];
+    tsc->tsc_off = 8;
+    tsc->tsc_len = 4;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[3] = 3;
+
+    tsc = &tsc_buf[4];
+    tsc->tsc_data = &data[12];
+    tsc->tsc_off = 12;
+    tsc->tsc_len = 4;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[4] = 4;
+
+    tsc = &tsc_buf[5];
+    tsc->tsc_data = &data[12];
+    tsc->tsc_off = 12;
+    tsc->tsc_len = 12;
+    /*
+     * this chunk partially overlaps. It does not fit to stream chunk buffer
+     * created by for() loop above, therefore a new stream chunk will be created.
+     */
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 2;
+    send_order[5] = 5;
+
+    assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt));
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++) {
+        pkt[i] = pkt_test_new(MOVE_TRESHOLD);
+        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, 0)))
+            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;
+        }
+    }
+
+    if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf,
+            sizeof(read_buf), &readbytes, &fin)))
+        goto err;
+
+    /*
+     * we expect to read 24 bytes
+     */
+    if (!TEST_uint64_t_eq(readbytes, 24))
+        goto err;
+
+    if (!TEST_false(fin))
+        goto err;
+
+    if (!TEST_mem_eq(read_buf, readbytes, data, readbytes))
+        goto err;
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
+        if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
+            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);
+
+    return ok;
+}
+
+static int test_rstream_mix_chunks(void)
+{
+    unsigned char data[4096];
+    unsigned char read_buf[4096];
+    TEST_STREAM_CHUNK_T tsc_buf[7];
+    OSSL_QRX_PKT *pkt[OSSL_NELEM(tsc_buf)] = { 0 };
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    TEST_STREAM_CHUNK_T *tsc;
+    QUIC_RSTREAM *rstream;
+    size_t readbytes;
+    unsigned int i;
+    unsigned int send_order[7];
+    int fin;
+    int ok = 0;
+
+    if (sizeof(void *) != 8) {
+        TEST_info("%s is implemented for 64-bit platforms only", OPENSSL_FUNC);
+        return 1;
+    }
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new()))
+        return 0;
+
+    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[8];
+    tsc->tsc_off = 8;
+    tsc->tsc_len = 4;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[0] = 0;
+
+    /*
+     * the next 3 byte nibble partially overlaps with
+     * earlier one. it adds 1 byte. it is prepended
+     */
+    tsc = &tsc_buf[1];
+    tsc->tsc_data = &data[7];
+    tsc->tsc_off = 7;
+    tsc->tsc_len = 3;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[1] = 1;
+
+    /*
+     * the next 3 byte nibble partially overlaps with
+     * range. it effectively adds 1 byte to end.
+     */
+    tsc = &tsc_buf[2];
+    tsc->tsc_data = &data[10];
+    tsc->tsc_off = 10;
+    tsc->tsc_len = 3;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[2] = 2;
+
+    /*
+     * append nibble that fully overlaps with range
+     * (the new nibble is superset of existing range)
+     * it effectively adds two bytes
+     */
+    tsc = &tsc_buf[3];
+    tsc->tsc_data = &data[6];
+    tsc->tsc_off = 6;
+    tsc->tsc_len = 8;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 1;
+    send_order[3] = 3;
+
+    /*
+     * prepend the nibble which starts yet another range.
+     */
+    tsc = &tsc_buf[4];
+    tsc->tsc_data = &data[0];
+    tsc->tsc_off = 0;
+    tsc->tsc_len = 1;
+    tsc->tsc_ranges_exp = 2;
+    tsc->tsc_chunks_exp = 2;
+    send_order[4] = 4;
+
+    /*
+     * nibble here appends bytes to right range. the range count and chunk
+     * count must not change as new data still fit to dstorage.
+     */
+    tsc = &tsc_buf[5];
+    tsc->tsc_data = &data[14];
+    tsc->tsc_off = 14;
+    tsc->tsc_len = 3;
+    tsc->tsc_ranges_exp = 2;
+    tsc->tsc_chunks_exp = 2;
+    send_order[5] = 5;
+
+    /*
+     * send chunk that overlaps everything
+     */
+    tsc = &tsc_buf[6];
+    tsc->tsc_data = &data[0];
+    tsc->tsc_off = 0;
+    tsc->tsc_len = 24;
+    tsc->tsc_ranges_exp = 1;
+    tsc->tsc_chunks_exp = 2;
+    send_order[6] = 6;
+
+    /*
+     * all nibbles we've sent so far must fit to single range.
+     */
+    assert(OSSL_NELEM(tsc_buf) == OSSL_NELEM(pkt));
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++) {
+        pkt[i] = pkt_test_new(MOVE_TRESHOLD);
+        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, 0)))
+            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;
+        }
+    }
+
+    if (!TEST_true(ossl_quic_rstream_read(rstream, read_buf,
+            sizeof(read_buf), &readbytes, &fin)))
+        goto err;
+
+    if (!TEST_uint64_t_eq(readbytes, 24))
+        goto err;
+
+    if (!TEST_false(fin))
+        goto err;
+
+    if (!TEST_mem_eq(read_buf, readbytes, data, readbytes))
+        goto err;
+
+    for (i = 0; i < OSSL_NELEM(tsc_buf); i++)
+        if (!TEST_size_t_eq(pkt_test_refcount(pkt[i]), 1))
+            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);
+
+    return ok;
+}
+
 int setup_tests(void)
 {
     ADD_TEST(test_sstream_simple);
     ADD_ALL_TESTS(test_sstream_bulk, 100);
-    ADD_ALL_TESTS(test_rstream_simple, 4);
     ADD_ALL_TESTS(test_rstream_random, 100);
     ADD_TEST(test_rstream_pkt);
     ADD_TEST(test_rstream_pkt_overhead);
     ADD_ALL_TESTS(test_rstream_reorder, 40);
+    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);
+
     return 1;
 }
diff --git a/test/quic_txp_test.c b/test/quic_txp_test.c
index a0b480c86d..50f51cc38a 100644
--- a/test/quic_txp_test.c
+++ b/test/quic_txp_test.c
@@ -12,6 +12,7 @@
 #include "internal/quic_demux.h"
 #include "internal/quic_record_rx.h"
 #include "internal/quic_channel.h"
+#include "../ssl/quic/quic_channel_local.h"
 #include "testutil.h"
 #include "quic_record_test_util.h"

@@ -1311,10 +1312,18 @@ static int run_script(int script_idx, const struct script_op *script)
     struct helper h;
     const struct script_op *op;
     size_t opn = 0;
+    QUIC_CHANNEL *ch = NULL;
+    QUIC_RSTREAM_QPARM *rsqp = NULL;

     if (!helper_init(&h))
         goto err;

+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL))))
+        goto err;
+
+    if (!TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch)))
+        goto err;
+
     have_helper = 1;
     for (op = script, opn = 0; op->opcode != OPK_END; ++op, ++opn) {
         switch (op->opcode) {
@@ -1508,7 +1517,7 @@ static int run_script(int script_idx, const struct script_op *script)
                     16 * 1024 * 1024,
                     fake_now, NULL))
                 || !TEST_ptr(s->rstream = ossl_quic_rstream_new(&s->rxfc,
-                                 NULL))) {
+                                 NULL, rsqp))) {
                 ossl_quic_sstream_free(s->sstream);
                 ossl_quic_stream_map_release(h.args.qsm, s);
                 goto err;
@@ -1606,6 +1615,8 @@ err:
         TEST_error("script %d failed at op %zu", script_idx + 1, opn + 1);
     if (have_helper)
         helper_cleanup(&h);
+    ossl_quic_rstream_qparm_destroy(rsqp);
+    ossl_quic_channel_free(ch);
     return testresult;
 }