Commit dbc3ceb4ca for openssl.org

commit dbc3ceb4ca582a304eda542f12d4338ed421d3c7
Author: Alexandr Nedvedicky <sashan@openssl.org>
Date:   Mon Aug 10 10:49:47 2026 +0200

    New implementation of stream reassembly for QUIC.

    The new implementation makes clear distinction between stream chunk
    and stream range.

    The strem chunk is defined by its `start` and `end` offset with
    respect to offset 0 (the start of the stream). The `start` < `end`.
    The lenght of the stream chunk is `end - start`. The stream chunks
    are delivered as QUIC stream frames.

    The stream range is list of stream chunks which together create
    one continuous range of stream. The range is also deined by
    `start` and `end` offset. The ranges are kept in R/B tree.

    The chunks which are arriving in order are kept in list which
    forms one node of R/B tree. If newly arriving stream chunk
    can not be inserted to existing stream range for example because
    chunk.start > range.end (there is a gap between existing range and
    new chunk), then the new range is created and inserted to R/B
    tree.

    If the newly arriving chunk closes the gap between two existing
    ranges, the ranges are merged to single tree node. The join
    proces uses list join operation with O(1) complexity to make
    the new continuous range of stream chunks.

    In a nutshell: stream reassembly process is R/B tree look up/insert
    with tail/head insertion to list.

    There is also a preparatory work that will allow us to deal with
    memory hog issue. Currently the QUIC stack keeps all stream
    frame data  on packet buffers until data is moved to application.
    On hone hand, this saves one copy opration between on the other
    hand it may cause receiver to hold lot more memory than currently
    needed. Consider situation where QUIC stack needs to hold reference
    to whole packet buffer, just because of sinle 1byte stream chunk.
    The 1byte stream chunk (at let's say offset 10)  can not be moved
    to application yet, because QUIC stack is waiting for data at
    offset 0 - 9. Such buffer might be waiting in reassembly queue
    for a long time holding a reference to whole packet.

    To mitigate this we need to allow the QUIC stack to move data
    from packet buffers to stream buffers. The logic which decides
    when data should be moved from packet buffers is missing in this
    change. Here we just bring the code that supports improved
    buffer handling.  The data for short stream frames/chunks (16B)
    are kept inside the stream chunk structure itself (this is referred
    as direct storage, or dstorage). The code coalesces data from adjacent
    stream frames into dstorage until it fill up. Once dstorage is full the
    new stream chunk is allocated and added to range.

    Larger stream chunks (size > 16B) get buffer from the heap.
    It is then linked to stream buffer.

    This commit just brings the quic_strm_reas.c in. It is not currently
    hooked to build.

    Fixes: CVE-2026-42772

    Co-authored-by: Tomáš Mráz <tm@t8m.info>

    Co-authored-by: Jakub Zelenka <bukka@php.net>

    Co-authored-by: Mounir IDRASSI <mounir.idrassi@amcrypto.jp>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Merge-date: Sat Sep 26 11:35:31 2026
    Merged-from: https://github.com/openssl/openssl/pull/32038

diff --git a/include/internal/quic_sf_list.h b/include/internal/quic_sf_list.h
deleted file mode 100644
index 1a22cc3f38..0000000000
--- a/include/internal/quic_sf_list.h
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright 2022-2023 The OpenSSL Project Authors. All Rights Reserved.
- *
- * Licensed under the Apache License 2.0 (the "License").  You may not use
- * this file except in compliance with the License.  You can obtain a copy
- * in the file LICENSE in the source distribution or at
- * https://www.openssl.org/source/license.html
- */
-
-#ifndef OSSL_QUIC_SF_LIST_H
-#define OSSL_QUIC_SF_LIST_H
-
-#include "internal/common.h"
-#include "internal/uint_set.h"
-#include "internal/quic_record_rx.h"
-
-/*
- * Stream frame list
- * =================
- *
- * This data structure supports similar operations as uint64 set but
- * it has slightly different invariants and also carries data associated with
- * the ranges in the list.
- *
- * Operations:
- *   Insert frame (optimized insertion at the beginning and at the end).
- *   Iterated peek into the frame(s) from the beginning.
- *   Dropping frames from the beginning up to an offset (exclusive).
- *
- * Invariant: The frames in the list are sorted by the start and end bounds.
- * Invariant: There are no fully overlapping frames or frames that would
- *            be fully encompassed by another frame in the list.
- * Invariant: No frame has start > end.
- * Invariant: The range start is inclusive the end is exclusive to be
- *            able to mark an empty frame.
- * Invariant: The offset never points further than into the first frame.
- */
-#ifndef OPENSSL_NO_QUIC
-
-typedef struct stream_frame_st STREAM_FRAME;
-
-typedef struct sframe_list_st {
-    STREAM_FRAME *head, *tail;
-    /* Is the tail frame final. */
-    unsigned int fin;
-    /* Number of stream frames in the list. */
-    size_t num_frames;
-    /* Offset of data not yet dropped */
-    uint64_t offset;
-    /* Is head locked ? */
-    int head_locked;
-    /* Cleanse data on release? */
-    int cleanse;
-} SFRAME_LIST;
-
-/*
- * Initializes the stream frame list fl.
- */
-void ossl_sframe_list_init(SFRAME_LIST *fl);
-
-/*
- * Destroys the stream frame list fl releasing any data
- * still present inside it.
- */
-void ossl_sframe_list_destroy(SFRAME_LIST *fl);
-
-/*
- * Insert a stream frame data into the list.
- * The data covers an offset range (range.start is inclusive,
- * range.end is exclusive).
- * fin should be set if this is the final frame of the stream.
- * Returns an error if a frame cannot be inserted - due to
- * STREAM_FRAME allocation error, or in case of erroneous
- * fin flag (this is an ossl_assert() check so a caller must
- * check it on its own too).
- */
-int ossl_sframe_list_insert(SFRAME_LIST *fl, UINT_RANGE *range,
-    OSSL_QRX_PKT *pkt,
-    const unsigned char *data, int fin);
-
-/*
- * Iterator to peek at the contiguous frames at the beginning
- * of the frame list fl.
- * The *data covers an offset range (range.start is inclusive,
- * range.end is exclusive).
- * *fin is set if this is the final frame of the stream.
- * Opaque iterator *iter can be used to peek at the subsequent
- * frame if there is any without any gap before it.
- * Returns 1 on success.
- * Returns 0 if there is no further contiguous frame. In that
- * case *fin is set, if the end of the stream is reached.
- */
-int ossl_sframe_list_peek(const SFRAME_LIST *fl, void **iter,
-    UINT_RANGE *range, const unsigned char **data,
-    int *fin);
-
-/*
- * Drop all frames up to the offset limit.
- * Also unlocks the head frame if locked.
- * Returns 1 on success.
- * Returns 0 when trying to drop frames at offsets that were not
- * received yet. (ossl_assert() is used to check, so this is an invalid call.)
- */
-int ossl_sframe_list_drop_frames(SFRAME_LIST *fl, uint64_t limit);
-
-/*
- * Locks and returns the head frame of fl if it is readable - read offset is
- * at the beginning or middle of the frame.
- * range is set to encompass the not yet read part of the head frame,
- * data pointer is set to appropriate offset within the frame if the read
- * offset points in the middle of the frame,
- * fin is set to 1 if the head frame is also the tail frame.
- * Returns 1 on success, 0 if there is no readable data or the head
- * frame is already locked.
- */
-int ossl_sframe_list_lock_head(SFRAME_LIST *fl, UINT_RANGE *range,
-    const unsigned char **data,
-    int *fin);
-
-/*
- * Just returns whether the head frame is locked by previous
- * ossl_sframe_list_lock_head() call.
- */
-int ossl_sframe_list_is_head_locked(SFRAME_LIST *fl);
-
-/*
- * Callback function type to write stream frame data to some
- * side storage before the packet containing the frame data
- * is released.
- * It should return 1 on success or 0 if there is not enough
- * space available in the side storage.
- */
-typedef int(sframe_list_write_at_cb)(uint64_t logical_offset,
-    const unsigned char *buf,
-    size_t buf_len,
-    void *cb_arg);
-
-/*
- * Move the frame data in all the stream frames in the list fl
- * from the packets to the side storage using the write_at_cb
- * callback.
- * Returns 1 if all the calls to the callback return 1.
- * If the callback returns 0, the function stops processing further
- * frames and returns 0.
- */
-int ossl_sframe_list_move_data(SFRAME_LIST *fl,
-    sframe_list_write_at_cb *write_at_cb,
-    void *cb_arg);
-#endif
-
-#endif
diff --git a/include/internal/quic_stream.h b/include/internal/quic_stream.h
index 824d4b8969..b9431831d0 100644
--- a/include/internal/quic_stream.h
+++ b/include/internal/quic_stream.h
@@ -318,11 +318,9 @@ void ossl_quic_sstream_set_cleanse(QUIC_SSTREAM *qss, int cleanse);
  * controller and statistics module. They can be NULL for unit testing.
  * If they are non-NULL, the `rxfc` is called when receive stream data
  * is read by application. `statm` is queried for current rtt.
- * `rbuf_size` is the initial size of the ring buffer to be used
- * when ossl_quic_rstream_move_to_rbuf() is called.
  */
 QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc,
-    OSSL_STATM *statm, size_t rbuf_size);
+    OSSL_STATM *statm);

 /*
  * Frees a QUIC_RSTREAM and any associated storage.
@@ -330,10 +328,10 @@ QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc,
 void ossl_quic_rstream_free(QUIC_RSTREAM *qrs);

 /*
- * Adds received stream frame data to `qrs`. The `pkt_wrap` refcount is
- * incremented if the `data` is queued directly without copying.
- * It can be NULL for unit-testing purposes, i.e. if `data` is static or
- * never released before calling ossl_quic_rstream_free().
+ * Adds received stream frame data to `qrs`. `pkt` must be the packet
+ * carrying `data`; its refcount is incremented if the data is kept
+ * referenced on the packet rather than copied. `pkt` and `data` can
+ * be NULL only for an empty frame indicating `fin`.
  * The `offset` is the absolute offset of the data in the stream.
  * `data_len` can be 0 - can be useful for indicating `fin` for empty stream.
  * Or to indicate `fin` without any further data added to the stream.
@@ -378,8 +376,6 @@ int ossl_quic_rstream_available(QUIC_RSTREAM *qrs, size_t *avail, int *fin);
  * Returns 1 on success (including calls if no record is available, or
  * after end of the stream - in that case *fin will be set to 1 and
  * *rec_len to 0), 0 on error.
- * It is an error to call ossl_quic_rstream_get_record() multiple times
- * without calling ossl_quic_rstream_release_record() in between.
  */
 int ossl_quic_rstream_get_record(QUIC_RSTREAM *qrs,
     const unsigned char **record, size_t *rec_len,
@@ -394,35 +390,23 @@ int ossl_quic_rstream_get_record(QUIC_RSTREAM *qrs,
  * call to ossl_quic_rstream_get_record() is needed to obtain further
  * stream data.
  * Returns 1 on success, 0 on error.
- * It is an error to call ossl_quic_rstream_release_record() multiple
- * times without calling ossl_quic_rstream_get_record() in between.
  */
 int ossl_quic_rstream_release_record(QUIC_RSTREAM *qrs, size_t read_len);

 /*
- * Moves received frame data from decrypted packets to ring buffer.
- * This should be called when there are too many decrypted packets allocated.
- * Returns 1 on success, 0 when it was not possible to release all
- * referenced packets due to an insufficient size of the ring buffer.
- * Exception is the packet from the record returned previously by
- * ossl_quic_rstream_get_record() - that one will be always skipped.
+ * Sets flag to cleanse the buffered data when user reads it.
  */
-int ossl_quic_rstream_move_to_rbuf(QUIC_RSTREAM *qrs);
+void ossl_quic_rstream_set_cleanse(QUIC_RSTREAM *qrs, int cleanse);

 /*
- * Resizes the internal ring buffer to a new `rbuf_size` size.
- * Returns 1 on success, 0 on error.
- * Possible error conditions are an allocation failure, trying to resize
- * the ring buffer when ossl_quic_rstream_get_record() was called and
- * not yet released, or trying to resize the ring buffer to a smaller size
- * than currently occupied.
+ * returns the number of stream chunks kept in rstream
  */
-int ossl_quic_rstream_resize_rbuf(QUIC_RSTREAM *qrs, size_t rbuf_size);
+size_t ossl_quic_rstream_get_chunk_count(QUIC_RSTREAM *qrs);

 /*
- * Sets flag to cleanse the buffered data when user reads it.
+ * returns the number of stream ranges kept in rstream
  */
-void ossl_quic_rstream_set_cleanse(QUIC_RSTREAM *qrs, int cleanse);
+size_t ossl_quic_rstream_get_range_count(QUIC_RSTREAM *qrs);
 #endif

 #endif
diff --git a/include/internal/quic_strm_reas.h b/include/internal/quic_strm_reas.h
new file mode 100644
index 0000000000..1d1af12c74
--- /dev/null
+++ b/include/internal/quic_strm_reas.h
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2022-2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#ifndef OSSL_QUIC_STRM_REAS_H
+#define OSSL_QUIC_STRM_REAS_H
+
+#include "internal/common.h"
+#include "internal/uint_set.h"
+#include "internal/quic_record_rx.h"
+
+#ifndef OPENSSL_NO_QUIC
+#include "internal/ossl_rbtree.h"
+
+typedef struct sframe_set_t {
+    OSSL_RBT_HEAD(srange, sframe_set_t)
+    ranges;
+    /* Is the tail frame final. */
+    unsigned int fin;
+    uint64_t fin_off;
+    /* Number of stream frames in the list. */
+    size_t stream_ranges;
+    size_t stream_chunks;
+    /* Offset of data not yet dropped */
+    uint64_t offset;
+    /* Cleanse data on release? */
+    int cleanse;
+    int move_buffers;
+} SFRAME_SET;
+
+/*
+ * Initializes the stream frame list fs.
+ */
+void ossl_sframe_set_init(SFRAME_SET *fs);
+
+/*
+ * Destroys the stream frame list fs releasing any data
+ * still present inside it.
+ */
+void ossl_sframe_set_destroy_ranges(SFRAME_SET *fs);
+
+/*
+ * Insert a stream frame data into the list.
+ * The data covers an offset range (range.start is inclusive,
+ * range.end is exclusive).
+ * fin should be set if this is the final frame of the stream.
+ * Returns an error if a frame cannot be inserted - due to
+ * STREAM_FRAME allocation error, or in case of erroneous
+ * fin flag.
+ */
+int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *range,
+    OSSL_QRX_PKT *pkt,
+    const unsigned char *data, int fin);
+
+/*
+ * Iterator to peek at the contiguous frames at the beginning
+ * of the frame set (the first stream range).
+ * The *data covers an offset range (range.start is inclusive,
+ * range.end is exclusive).
+ * *fin is set if this is the final frame of the stream.
+ * Opaque iterator *iter can be used to peek at the subsequent
+ * frame if there is any without any gap before it.
+ * Returns 1 on success.
+ * Returns 0 if there is no further contiguous frame. In that
+ * case *fin is set, if the end of the stream is reached.
+ */
+int ossl_sframe_set_peek(SFRAME_SET *fs, void **iter,
+    UINT_RANGE *range, const unsigned char **data,
+    int *fin);
+
+/*
+ * moves reading offset to new position, discarding all consumed
+ * chunks (which end offset is less than offset).
+ */
+int ossl_sframe_set_move_offset(SFRAME_SET *fs, uint64_t offset);
+
+/*
+ * returns how many bytes is available to read from stream.
+ */
+int ossl_sframe_set_avail(SFRAME_SET *fs, uint64_t *avail, int *fin);
+
+#endif
+
+#endif
diff --git a/ssl/quic/build.info b/ssl/quic/build.info
index 230341db76..c13c6dffbf 100644
--- a/ssl/quic/build.info
+++ b/ssl/quic/build.info
@@ -10,7 +10,7 @@ IF[{- !$disabled{quic} -}]
     SOURCE[$LIBSSL]=quic_fc.c uint_set.c
     SOURCE[$LIBSSL]=quic_cfq.c quic_txpim.c quic_fifd.c quic_txp.c
     SOURCE[$LIBSSL]=quic_stream_map.c
-    SOURCE[$LIBSSL]=quic_sf_list.c quic_rstream.c quic_sstream.c
+    SOURCE[$LIBSSL]=quic_strm_reas.c quic_rstream.c quic_sstream.c
     SOURCE[$LIBSSL]=quic_reactor.c
     SOURCE[$LIBSSL]=quic_reactor_wait_ctx.c
     SOURCE[$LIBSSL]=quic_channel.c quic_port.c quic_engine.c
diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index 117bf50eb1..73065fd7e0 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -309,7 +309,7 @@ static int ch_init(QUIC_CHANNEL *ch)
     }

     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, 0);
+        ch->crypto_recv[pn_space] = ossl_quic_rstream_new(NULL, NULL);
         if (ch->crypto_recv[pn_space] == NULL)
             goto err;
     }
@@ -3838,7 +3838,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, 0)) == NULL)
+        if ((qs->rstream = ossl_quic_rstream_new(NULL, NULL)) == NULL)
             goto err;

     /* TXFC */
diff --git a/ssl/quic/quic_rstream.c b/ssl/quic/quic_rstream.c
index 2fe1cb2cdb..a2dc038e4f 100644
--- a/ssl/quic/quic_rstream.c
+++ b/ssl/quic/quic_rstream.c
@@ -10,32 +10,32 @@
 #include "internal/common.h"
 #include "internal/time.h"
 #include "internal/quic_stream.h"
-#include "internal/quic_sf_list.h"
+#include "internal/quic_strm_reas.h"
 #include "internal/ring_buf.h"

 struct quic_rstream_st {
-    SFRAME_LIST fl;
+    SFRAME_SET fs;
     QUIC_RXFC *rxfc;
     OSSL_STATM *statm;
     UINT_RANGE head_range;
-    struct ring_buf rbuf;
 };

+#if !defined(NDEBUG) && defined(WITH_RSTREAM_DEBUG)
+#include <stdio.h>
+#define DEBUG_PRINT(...) fprintf(__VA_ARGS__)
+#else
+#define DEBUG_PRINT(...) (void)(0)
+#endif
+
 QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc,
-    OSSL_STATM *statm, size_t rbuf_size)
+    OSSL_STATM *statm)
 {
     QUIC_RSTREAM *ret = OPENSSL_zalloc(sizeof(*ret));

     if (ret == NULL)
         return NULL;

-    ring_buf_init(&ret->rbuf);
-    if (!ring_buf_resize(&ret->rbuf, rbuf_size, 0)) {
-        OPENSSL_free(ret);
-        return NULL;
-    }
-
-    ossl_sframe_list_init(&ret->fl);
+    ossl_sframe_set_init(&ret->fs);
     ret->rxfc = rxfc;
     ret->statm = statm;
     return ret;
@@ -43,14 +43,10 @@ QUIC_RSTREAM *ossl_quic_rstream_new(QUIC_RXFC *rxfc,

 void ossl_quic_rstream_free(QUIC_RSTREAM *qrs)
 {
-    int cleanse;
-
     if (qrs == NULL)
         return;

-    cleanse = qrs->fl.cleanse;
-    ossl_sframe_list_destroy(&qrs->fl);
-    ring_buf_destroy(&qrs->rbuf, cleanse);
+    ossl_sframe_set_destroy_ranges(&qrs->fs);
     OPENSSL_free(qrs);
 }

@@ -70,7 +66,7 @@ int ossl_quic_rstream_queue_data(QUIC_RSTREAM *qrs, OSSL_QRX_PKT *pkt,
     range.start = offset;
     range.end = offset + data_len;

-    return ossl_sframe_list_insert(&qrs->fl, &range, pkt, data, fin);
+    return ossl_sframe_set_insert(&qrs->fs, &range, pkt, data, fin);
 }

 static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size,
@@ -83,9 +79,11 @@ static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size,
     size_t readbytes_ = 0;
     int fin_ = 0, ret = 1;

-    while (ossl_sframe_list_peek(&qrs->fl, &iter, &range, &data, &fin_)) {
+    DEBUG_PRINT(stderr, "%s want: %zu\n", OPENSSL_FUNC, size);
+    while (ossl_sframe_set_peek(&qrs->fs, &iter, &range, &data, &fin_)) {
         size_t l = (size_t)(range.end - range.start);

+        DEBUG_PRINT(stderr, "\t[ %llu, %llu ]\n", range.start, range.end);
         if (l > size) {
             l = size;
             fin_ = 0;
@@ -94,25 +92,6 @@ static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size,
         if (l == 0)
             break;

-        if (data == NULL) {
-            size_t max_len;
-
-            data = ring_buf_get_ptr(&qrs->rbuf, range.start, &max_len);
-            if (!ossl_assert(data != NULL))
-                return 0;
-            if (max_len < l) {
-                memcpy(buf, data, max_len);
-                size -= max_len;
-                buf += max_len;
-                readbytes_ += max_len;
-                l -= max_len;
-                data = ring_buf_get_ptr(&qrs->rbuf, range.start + max_len,
-                    &max_len);
-                if (!ossl_assert(data != NULL) || !ossl_assert(max_len > l))
-                    return 0;
-            }
-        }
-
         memcpy(buf, data, l);
         size -= l;
         buf += l;
@@ -121,14 +100,15 @@ static int read_internal(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size,
             break;
     }

-    if (drop && offset != 0) {
-        ret = ossl_sframe_list_drop_frames(&qrs->fl, offset);
-        ring_buf_cpop_range(&qrs->rbuf, 0, offset - 1, qrs->fl.cleanse);
-    }
+    if (drop && offset != 0)
+        ret = ossl_sframe_set_move_offset(&qrs->fs, offset);

     if (ret) {
+        DEBUG_PRINT(stderr, "%s got: %zu\n", OPENSSL_FUNC, readbytes_);
         *readbytes = readbytes_;
         *fin = fin_;
+    } else {
+        DEBUG_PRINT(stderr, "%s got: nothing\n", OPENSSL_FUNC);
     }

     return ret;
@@ -172,13 +152,9 @@ int ossl_quic_rstream_peek(QUIC_RSTREAM *qrs, unsigned char *buf, size_t size,

 int ossl_quic_rstream_available(QUIC_RSTREAM *qrs, size_t *avail, int *fin)
 {
-    void *iter = NULL;
-    UINT_RANGE range;
-    const unsigned char *data;
     uint64_t avail_ = 0;

-    while (ossl_sframe_list_peek(&qrs->fl, &iter, &range, &data, fin))
-        avail_ += range.end - range.start;
+    ossl_sframe_set_avail(&qrs->fs, &avail_, fin);

 #if SIZE_MAX < UINT64_MAX
     *avail = avail_ > SIZE_MAX ? SIZE_MAX : (size_t)avail_;
@@ -193,38 +169,32 @@ int ossl_quic_rstream_get_record(QUIC_RSTREAM *qrs,
     int *fin)
 {
     const unsigned char *record_ = NULL;
-    size_t rec_len_, max_len;
+    void *iterator = NULL;
+    size_t rec_len_;
+    int ok;

-    if (!ossl_sframe_list_lock_head(&qrs->fl, &qrs->head_range, &record_, fin)) {
-        /* No head frame to lock and return */
+    ok = ossl_sframe_set_peek(&qrs->fs, &iterator, &qrs->head_range, &record_,
+        fin);
+    if (ok == 0) {
         *record = NULL;
         *rec_len = 0;
         return 1;
     }

+    DEBUG_PRINT(stderr, "%s head: [ %llu, %llu ]\n", OPENSSL_FUNC,
+        qrs->head_range.start, qrs->head_range.end);
     /* if final empty frame, we drop it immediately */
     if (qrs->head_range.end == qrs->head_range.start) {
         if (!ossl_assert(*fin))
             return 0;
-        if (!ossl_sframe_list_drop_frames(&qrs->fl, qrs->head_range.end))
+        if (!ossl_sframe_set_move_offset(&qrs->fs, qrs->head_range.end))
             return 0;
     }

     rec_len_ = (size_t)(qrs->head_range.end - qrs->head_range.start);
-
-    if (record_ == NULL && rec_len_ != 0) {
-        record_ = ring_buf_get_ptr(&qrs->rbuf, qrs->head_range.start,
-            &max_len);
-        if (!ossl_assert(record_ != NULL))
-            return 0;
-        if (max_len < rec_len_) {
-            rec_len_ = max_len;
-            qrs->head_range.end = qrs->head_range.start + max_len;
-        }
-    }
-
     *rec_len = rec_len_;
     *record = record_;
+
     return 1;
 }

@@ -232,9 +202,6 @@ int ossl_quic_rstream_release_record(QUIC_RSTREAM *qrs, size_t read_len)
 {
     uint64_t offset;

-    if (!ossl_sframe_list_is_head_locked(&qrs->fl))
-        return 0;
-
     if (read_len > qrs->head_range.end - qrs->head_range.start) {
         if (read_len != SIZE_MAX)
             return 0;
@@ -243,12 +210,9 @@ int ossl_quic_rstream_release_record(QUIC_RSTREAM *qrs, size_t read_len)
         offset = qrs->head_range.start + read_len;
     }

-    if (!ossl_sframe_list_drop_frames(&qrs->fl, offset))
+    if (!ossl_sframe_set_move_offset(&qrs->fs, offset))
         return 0;

-    if (offset > 0)
-        ring_buf_cpop_range(&qrs->rbuf, 0, offset - 1, qrs->fl.cleanse);
-
     if (qrs->rxfc != NULL) {
         OSSL_TIME rtt = get_rtt(qrs);

@@ -259,36 +223,17 @@ int ossl_quic_rstream_release_record(QUIC_RSTREAM *qrs, size_t read_len)
     return 1;
 }

-static int write_at_ring_buf_cb(uint64_t logical_offset,
-    const unsigned char *buf,
-    size_t buf_len,
-    void *cb_arg)
-{
-    struct ring_buf *rbuf = cb_arg;
-
-    return ring_buf_write_at(rbuf, logical_offset, buf, buf_len);
-}
-
-int ossl_quic_rstream_move_to_rbuf(QUIC_RSTREAM *qrs)
+void ossl_quic_rstream_set_cleanse(QUIC_RSTREAM *qrs, int cleanse)
 {
-    if (ring_buf_avail(&qrs->rbuf) == 0)
-        return 0;
-    return ossl_sframe_list_move_data(&qrs->fl,
-        write_at_ring_buf_cb, &qrs->rbuf);
+    qrs->fs.cleanse = cleanse;
 }

-int ossl_quic_rstream_resize_rbuf(QUIC_RSTREAM *qrs, size_t rbuf_size)
+size_t ossl_quic_rstream_get_chunk_count(QUIC_RSTREAM *qrs)
 {
-    if (ossl_sframe_list_is_head_locked(&qrs->fl))
-        return 0;
-
-    if (!ring_buf_resize(&qrs->rbuf, rbuf_size, qrs->fl.cleanse))
-        return 0;
-
-    return 1;
+    return qrs->fs.stream_chunks;
 }

-void ossl_quic_rstream_set_cleanse(QUIC_RSTREAM *qrs, int cleanse)
+size_t ossl_quic_rstream_get_range_count(QUIC_RSTREAM *qrs)
 {
-    qrs->fl.cleanse = cleanse;
+    return qrs->fs.stream_ranges;
 }
diff --git a/ssl/quic/quic_sf_list.c b/ssl/quic/quic_sf_list.c
deleted file mode 100644
index 03bbbe6d35..0000000000
--- a/ssl/quic/quic_sf_list.c
+++ /dev/null
@@ -1,334 +0,0 @@
-/*
- * Copyright 2022-2023 The OpenSSL Project Authors. All Rights Reserved.
- *
- * Licensed under the Apache License 2.0 (the "License").  You may not use
- * this file except in compliance with the License.  You can obtain a copy
- * in the file LICENSE in the source distribution or at
- * https://www.openssl.org/source/license.html
- */
-
-#include "internal/uint_set.h"
-#include "internal/common.h"
-#include "internal/quic_sf_list.h"
-
-struct stream_frame_st {
-    struct stream_frame_st *prev, *next;
-    UINT_RANGE range;
-    OSSL_QRX_PKT *pkt;
-    const unsigned char *data;
-};
-
-static void stream_frame_free(SFRAME_LIST *fl, STREAM_FRAME *sf)
-{
-    if (fl->cleanse && sf->data != NULL)
-        OPENSSL_cleanse((unsigned char *)sf->data,
-            (size_t)(sf->range.end - sf->range.start));
-    ossl_qrx_pkt_release(sf->pkt);
-    OPENSSL_free(sf);
-}
-
-static STREAM_FRAME *stream_frame_new(UINT_RANGE *range, OSSL_QRX_PKT *pkt,
-    const unsigned char *data)
-{
-    STREAM_FRAME *sf = OPENSSL_zalloc(sizeof(*sf));
-
-    if (sf == NULL)
-        return NULL;
-
-    if (pkt != NULL)
-        ossl_qrx_pkt_up_ref(pkt);
-
-    sf->range = *range;
-    sf->pkt = pkt;
-    sf->data = data;
-
-    return sf;
-}
-
-void ossl_sframe_list_init(SFRAME_LIST *fl)
-{
-    memset(fl, 0, sizeof(*fl));
-}
-
-void ossl_sframe_list_destroy(SFRAME_LIST *fl)
-{
-    STREAM_FRAME *sf, *next_frame;
-
-    for (sf = fl->head; sf != NULL; sf = next_frame) {
-        next_frame = sf->next;
-        stream_frame_free(fl, sf);
-    }
-}
-
-static int append_frame(SFRAME_LIST *fl, UINT_RANGE *range,
-    OSSL_QRX_PKT *pkt,
-    const unsigned char *data)
-{
-    STREAM_FRAME *new_frame;
-
-    if ((new_frame = stream_frame_new(range, pkt, data)) == NULL)
-        return 0;
-    new_frame->prev = fl->tail;
-    if (fl->tail != NULL)
-        fl->tail->next = new_frame;
-    fl->tail = new_frame;
-    ++fl->num_frames;
-    return 1;
-}
-
-int ossl_sframe_list_insert(SFRAME_LIST *fl, UINT_RANGE *range,
-    OSSL_QRX_PKT *pkt,
-    const unsigned char *data, int fin)
-{
-    STREAM_FRAME *sf, *new_frame, *prev_frame, *next_frame;
-#ifndef NDEBUG
-    uint64_t curr_end = fl->tail != NULL ? fl->tail->range.end
-                                         : fl->offset;
-
-    /* This check for FINAL_SIZE_ERROR is handled by QUIC FC already */
-    assert((!fin || curr_end <= range->end)
-        && (!fl->fin || curr_end >= range->end));
-#endif
-
-    if (fl->offset >= range->end)
-        goto end;
-
-    /* nothing there yet */
-    if (fl->tail == NULL) {
-        fl->tail = fl->head = stream_frame_new(range, pkt, data);
-        if (fl->tail == NULL)
-            return 0;
-
-        ++fl->num_frames;
-        goto end;
-    }
-
-    /* optimize insertion at the end */
-    if (fl->tail->range.start < range->start) {
-        if (fl->tail->range.end >= range->end)
-            goto end;
-
-        if (!append_frame(fl, range, pkt, data))
-            return 0;
-        goto end;
-    }
-
-    prev_frame = NULL;
-    for (sf = fl->head; sf != NULL && sf->range.start < range->start;
-        sf = sf->next)
-        prev_frame = sf;
-
-    if (!ossl_assert(sf != NULL))
-        /* frame list invariant broken */
-        return 0;
-
-    if (prev_frame != NULL && prev_frame->range.end >= range->end)
-        goto end;
-
-    /*
-     * Now we must create a new frame although in the end we might drop it,
-     * because we will be potentially dropping existing overlapping frames.
-     */
-    new_frame = stream_frame_new(range, pkt, data);
-    if (new_frame == NULL)
-        return 0;
-
-    for (next_frame = sf;
-        next_frame != NULL && next_frame->range.end <= range->end;) {
-        STREAM_FRAME *drop_frame = next_frame;
-
-        next_frame = next_frame->next;
-        if (next_frame != NULL)
-            next_frame->prev = drop_frame->prev;
-        if (prev_frame != NULL)
-            prev_frame->next = drop_frame->next;
-        if (fl->head == drop_frame)
-            fl->head = next_frame;
-        if (fl->tail == drop_frame)
-            fl->tail = prev_frame;
-        --fl->num_frames;
-        stream_frame_free(fl, drop_frame);
-    }
-
-    if (next_frame != NULL) {
-        /* check whether the new_frame is redundant because there is no gap */
-        if (prev_frame != NULL
-            && next_frame->range.start <= prev_frame->range.end) {
-            stream_frame_free(fl, new_frame);
-            goto end;
-        }
-        next_frame->prev = new_frame;
-    } else {
-        fl->tail = new_frame;
-    }
-
-    new_frame->next = next_frame;
-    new_frame->prev = prev_frame;
-
-    if (prev_frame != NULL)
-        prev_frame->next = new_frame;
-    else
-        fl->head = new_frame;
-
-    ++fl->num_frames;
-
-end:
-    fl->fin = fin || fl->fin;
-
-    return 1;
-}
-
-int ossl_sframe_list_peek(const SFRAME_LIST *fl, void **iter,
-    UINT_RANGE *range, const unsigned char **data,
-    int *fin)
-{
-    STREAM_FRAME *sf = *iter;
-    uint64_t start;
-
-    if (sf == NULL) {
-        start = fl->offset;
-        sf = fl->head;
-    } else {
-        start = sf->range.end;
-        sf = sf->next;
-    }
-
-    range->start = start;
-
-    if (sf == NULL || sf->range.start > start
-        || !ossl_assert(start < sf->range.end)) {
-        range->end = start;
-        *data = NULL;
-        *iter = NULL;
-        /* set fin only if we are at the end */
-        *fin = sf == NULL ? fl->fin : 0;
-        return 0;
-    }
-
-    range->end = sf->range.end;
-    if (sf->data != NULL)
-        *data = sf->data + (start - sf->range.start);
-    else
-        *data = NULL;
-    *fin = sf->next == NULL ? fl->fin : 0;
-    *iter = sf;
-    return 1;
-}
-
-int ossl_sframe_list_drop_frames(SFRAME_LIST *fl, uint64_t limit)
-{
-    STREAM_FRAME *sf;
-
-    /* offset cannot move back or past the data received */
-    if (!ossl_assert(limit >= fl->offset)
-        || !ossl_assert(fl->tail == NULL
-            || limit <= fl->tail->range.end)
-        || !ossl_assert(fl->tail != NULL
-            || limit == fl->offset))
-        return 0;
-
-    fl->offset = limit;
-
-    for (sf = fl->head; sf != NULL && sf->range.end <= limit;) {
-        STREAM_FRAME *drop_frame = sf;
-
-        sf = sf->next;
-        --fl->num_frames;
-        stream_frame_free(fl, drop_frame);
-    }
-    fl->head = sf;
-
-    if (sf != NULL)
-        sf->prev = NULL;
-    else
-        fl->tail = NULL;
-
-    fl->head_locked = 0;
-
-    return 1;
-}
-
-int ossl_sframe_list_lock_head(SFRAME_LIST *fl, UINT_RANGE *range,
-    const unsigned char **data,
-    int *fin)
-{
-    int ret;
-    void *iter = NULL;
-
-    if (fl->head_locked)
-        return 0;
-
-    ret = ossl_sframe_list_peek(fl, &iter, range, data, fin);
-    if (ret)
-        fl->head_locked = 1;
-    return ret;
-}
-
-int ossl_sframe_list_is_head_locked(SFRAME_LIST *fl)
-{
-    return fl->head_locked;
-}
-
-int ossl_sframe_list_move_data(SFRAME_LIST *fl,
-    sframe_list_write_at_cb *write_at_cb,
-    void *cb_arg)
-{
-    STREAM_FRAME *sf = fl->head, *prev_frame = NULL;
-    uint64_t limit = fl->offset;
-
-    if (sf == NULL)
-        return 1;
-
-    if (fl->head_locked)
-        sf = sf->next;
-
-    for (; sf != NULL; sf = sf->next) {
-        size_t len;
-        const unsigned char *data = sf->data;
-
-        if (limit < sf->range.start)
-            limit = sf->range.start;
-
-        if (data != NULL) {
-            if (limit > sf->range.start)
-                data += (size_t)(limit - sf->range.start);
-            len = (size_t)(sf->range.end - limit);
-
-            if (!write_at_cb(limit, data, len, cb_arg))
-                /* data did not fit */
-                return 0;
-
-            if (fl->cleanse)
-                OPENSSL_cleanse((unsigned char *)sf->data,
-                    (size_t)(sf->range.end - sf->range.start));
-
-            /* release the packet */
-            sf->data = NULL;
-            ossl_qrx_pkt_release(sf->pkt);
-            sf->pkt = NULL;
-        }
-
-        limit = sf->range.end;
-
-        /* merge contiguous frames */
-        if (prev_frame != NULL
-            && prev_frame->range.end >= sf->range.start) {
-            prev_frame->range.end = sf->range.end;
-            prev_frame->next = sf->next;
-
-            if (sf->next != NULL)
-                sf->next->prev = prev_frame;
-            else
-                fl->tail = prev_frame;
-
-            --fl->num_frames;
-            stream_frame_free(fl, sf);
-            sf = prev_frame;
-            continue;
-        }
-
-        prev_frame = sf;
-    }
-
-    return 1;
-}
diff --git a/ssl/quic/quic_strm_reas.c b/ssl/quic/quic_strm_reas.c
new file mode 100644
index 0000000000..34f399db1d
--- /dev/null
+++ b/ssl/quic/quic_strm_reas.c
@@ -0,0 +1,1252 @@
+/*
+ * Copyright 2022-2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License").  You may not use
+ * this file except in compliance with the License.  You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include "internal/uint_set.h"
+#include "internal/common.h"
+#include "internal/quic_strm_reas.h"
+#include "internal/list.h"
+
+#if !defined(NDEBUG) && defined(WITH_STRM_REAS_DEBUG)
+#include <stdio.h>
+#define DEBUG_PRINT(...) fprintf(__VA_ARGS__)
+#else
+#define DEBUG_PRINT(...) (void)(0)
+#endif
+
+#define DIRECT_STORAGE_SZ (2 * sizeof(void *))
+
+/*
+ * storage type indicates where stream data bytes
+ * are stored.
+ */
+enum {
+    ST_TYPE_DIRECT, /* in chunk structure itself (sc_dstorage) */
+    ST_TYPE_PKT, /* bytes are stored in attached pkt (sc_pkt) */
+    ST_TYPE_HEAP /* data are stored on memory heap buffer */
+};
+
+/*
+ * Stream chunk keeps stream bytes as received from QUIC STREAM_FRAME.
+ * Each chunk of stream data by [start, end).
+ */
+struct stream_chunk_t {
+    OSSL_LIST_MEMBER(sc, struct stream_chunk_t);
+    UINT_RANGE sc_range;
+    int sc_st; /* storage type */
+    union {
+        const unsigned char *u_data;
+        unsigned char *u_data_w;
+    } sc_data_u;
+    union {
+        OSSL_QRX_PKT *u_sc_pkt;
+        unsigned char *u_sc_buf;
+        unsigned char u_sc_dstorage[DIRECT_STORAGE_SZ];
+    } sc_storage_u;
+};
+
+#define sc_data sc_data_u.u_data
+#define sc_data_w sc_data_u.u_data_w
+
+#define sc_pkt sc_storage_u.u_sc_pkt
+#define sc_buf sc_storage_u.u_sc_buf
+#define sc_dstorage sc_storage_u.u_sc_dstorage
+
+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)
+
+/*
+ * Stream range keeps list of continuous stream chunks. The range
+ * is also defined by [start, end) interval. For every chunk
+ * in range this assertion must hold:
+ *    sc->sc_range.end == sc->sc_next->sc_range.start
+ *
+ * If newly arriving stream chunk can not be inserted to existing
+ * stream range, then new range must be created.
+ */
+struct stream_range_t {
+    OSSL_LIST(sc)
+    sr_chunks;
+    OSSL_RBT_ENTRY(stream_range_t)
+    sr_rbe;
+    UINT_RANGE sr_range;
+    struct stream_chunk_t *sr_it_sc; /* iterator */
+};
+
+static int srange_cmp(const struct stream_range_t *, const struct stream_range_t *);
+
+OSSL_RBT_PROTOTYPE(srange, stream_range_t, sr_rbe, srange_cmp)
+
+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)))
+
+/*
+  * 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 {
+        const unsigned char *u_data;
+        unsigned char *u_data_w;
+    } data_u;
+
+    data_u.u_data = data;
+
+    return data_u.u_data_w;
+}
+
+static void sc_data_trim_left(struct stream_chunk_t *sc, size_t trim_sz,
+    int cleanse)
+{
+    if (sc->sc_st == ST_TYPE_DIRECT) {
+        assert(SCHUNK_SIZE(sc) >= trim_sz);
+        memmove(sc->sc_data_w, &sc->sc_data[trim_sz],
+            UINT64_TO_SIZE_T((SCHUNK_SIZE(sc) - trim_sz)));
+        if (cleanse && trim_sz > 0) {
+            OPENSSL_cleanse(
+                sc->sc_data_w + UINT64_TO_SIZE_T(SCHUNK_SIZE(sc) - trim_sz),
+                UINT64_TO_SIZE_T(trim_sz));
+        }
+    } else {
+        if (cleanse && trim_sz > 0) {
+            OPENSSL_cleanse(sc->sc_data_w, trim_sz);
+        }
+        sc->sc_data += trim_sz;
+    }
+}
+
+static void sc_data_trim_right(struct stream_chunk_t *sc, size_t trim_sz,
+    int cleanse)
+{
+    unsigned char *data_realloc;
+    size_t w_offset;
+
+    assert(SCHUNK_SIZE(sc) >= trim_sz);
+
+    if (cleanse)
+        OPENSSL_cleanse(
+            &sc->sc_data_w[sc->sc_range.end - trim_sz - sc->sc_range.start],
+            trim_sz);
+
+    if (sc->sc_st == ST_TYPE_HEAP) {
+        /*
+         * this is a shrinking realloc() here, so it should not fail.
+         * even if it fails, we still don't care, the worst outcome
+         * of such failure is waste of memory.
+         */
+        assert(sc->sc_data_w >= sc->sc_buf);
+        w_offset = sc->sc_data_w - sc->sc_buf;
+        if (trim_sz > 0) {
+            assert(SCHUNK_SIZE(sc) > trim_sz);
+            data_realloc = OPENSSL_realloc(sc->sc_buf,
+                w_offset + UINT64_TO_SIZE_T(SCHUNK_SIZE(sc)) - trim_sz);
+            if (data_realloc != NULL) {
+                sc->sc_buf = data_realloc;
+                sc->sc_data_w = sc->sc_buf + w_offset;
+            }
+        }
+    }
+}
+
+static int srange_cmp(const struct stream_range_t *a_sr,
+    const struct stream_range_t *b_sr)
+{
+    assert(a_sr->sr_range.start < a_sr->sr_range.end);
+    assert(b_sr->sr_range.start < b_sr->sr_range.end);
+    /*
+     * no overlap, A precedes B
+     */
+    if (a_sr->sr_range.end < b_sr->sr_range.start)
+        return -1;
+
+    /*
+     * no overlap, A follows B
+     */
+    if (a_sr->sr_range.start > b_sr->sr_range.end)
+        return 1;
+
+    /*
+     * partial or full overlap or ranges are adjacent.
+     * the program needs to do close examination on
+     * how to add new chunk to existing stream range.
+     */
+    return 0;
+}
+
+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.
+     */
+
+    return 1;
+}
+
+static struct stream_chunk_t *new_schunk(SFRAME_SET *fs, OSSL_QRX_PKT *pkt,
+    UINT_RANGE *r, const unsigned char *data)
+{
+    struct stream_chunk_t *sc;
+    uint64_t rsize;
+
+    if (pkt == NULL)
+        return NULL;
+
+    sc = OPENSSL_zalloc(sizeof(*sc));
+    if (sc == NULL)
+        return NULL;
+
+    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;
+    } else {
+        rsize = r->end - r->start;
+        if (rsize <= DIRECT_STORAGE_SZ) {
+            DEBUG_PRINT(stderr, "%s ST_TYPE_DIRECT sc: %p %llu\n", OPENSSL_FUNC,
+                (void *)sc, rsize);
+            sc->sc_st = ST_TYPE_DIRECT;
+            sc->sc_data_w = sc->sc_dstorage;
+        } else {
+            DEBUG_PRINT(stderr, "%s ST_TYPE_HEAP sc: %p %llu\n", OPENSSL_FUNC,
+                (void *)sc, rsize);
+            sc->sc_st = ST_TYPE_HEAP;
+            sc->sc_buf = OPENSSL_malloc(UINT64_TO_SIZE_T(rsize));
+            if (sc->sc_buf == NULL) {
+                OPENSSL_free(sc);
+                return NULL;
+            }
+            sc->sc_data_w = sc->sc_buf;
+        }
+        sc->sc_range = *r;
+        memcpy(sc->sc_data_w, data, UINT64_TO_SIZE_T(rsize));
+
+        if (fs->cleanse)
+            OPENSSL_cleanse(deconst(data), UINT64_TO_SIZE_T(rsize));
+    }
+
+    return sc;
+}
+
+static void destroy_schunk(SFRAME_SET *fs, struct stream_chunk_t *sc)
+{
+    if (sc == NULL)
+        return;
+
+    if (fs->cleanse)
+        OPENSSL_cleanse(sc->sc_data_w,
+            UINT64_TO_SIZE_T(sc->sc_st == ST_TYPE_DIRECT
+                    ? DIRECT_STORAGE_SZ
+                    : SCHUNK_SIZE(sc)));
+
+    switch (sc->sc_st) {
+    case ST_TYPE_PKT:
+        ossl_qrx_pkt_release(sc->sc_pkt);
+        break;
+    case ST_TYPE_HEAP:
+        OPENSSL_free(sc->sc_buf);
+        break;
+    default:
+        assert(sc->sc_st == ST_TYPE_DIRECT);
+    }
+
+    OPENSSL_free(sc);
+}
+
+static struct stream_range_t *new_srange(void)
+{
+    struct stream_range_t *sr;
+
+    sr = OPENSSL_zalloc(sizeof(*sr));
+    if (sr != NULL) {
+        ossl_list_sc_init(&sr->sr_chunks);
+    }
+
+    return sr;
+}
+
+static void destroy_srange(SFRAME_SET *fs, struct stream_range_t *sr)
+{
+    struct stream_chunk_t *sc;
+
+    if (sr == NULL)
+        return;
+
+    assert(sr->sr_rbe.rb_parent == NULL);
+    assert(sr->sr_rbe.rb_left == NULL);
+    assert(sr->sr_rbe.rb_right == NULL);
+
+    while ((sc = ossl_list_sc_head(&sr->sr_chunks)) != NULL) {
+        ossl_list_sc_remove(&sr->sr_chunks, sc);
+        fs->stream_chunks--;
+        destroy_schunk(fs, sc);
+    }
+
+    OPENSSL_free(sr);
+}
+
+static struct stream_range_t *create_range(SFRAME_SET *fs,
+    struct stream_chunk_t *sc)
+{
+    struct stream_range_t *sr;
+
+    assert(sc != NULL);
+
+    sr = new_srange();
+    if (sr != NULL) {
+        ossl_list_sc_insert_head(&sr->sr_chunks, sc);
+        sr->sr_range = sc->sc_range;
+        fs->stream_chunks++;
+    }
+
+    return sr;
+}
+
+void ossl_sframe_set_init(SFRAME_SET *fs)
+{
+    memset(fs, 0, sizeof(*fs));
+    OSSL_RBT_INIT(srange, &fs->ranges);
+}
+
+static uint64_t get_sc_dstorage_sz(struct stream_chunk_t *sc)
+{
+    uint64_t sz = 0;
+
+    if (sc->sc_st == ST_TYPE_DIRECT && SCHUNK_SIZE(sc) < DIRECT_STORAGE_SZ)
+        sz = DIRECT_STORAGE_SZ - SCHUNK_SIZE(sc);
+
+    return sz;
+}
+
+static int try_dstorage(SFRAME_SET *fs, OSSL_QRX_PKT *pkt,
+    struct stream_range_t *sr, UINT_RANGE *r, const unsigned char **data)
+{
+    struct stream_chunk_t *head_sc, *tail_sc, *new_sc;
+    uint64_t rsize;
+    uint64_t offset;
+    uint64_t dsize;
+
+    /*
+     * full overlap which spans over more range with more than 1 chunk,
+     * nothing to be done here, caller will handle that.
+     */
+    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)
+        return 0;
+
+    head_sc = ossl_list_sc_head(&sr->sr_chunks);
+    assert(head_sc != NULL);
+    tail_sc = ossl_list_sc_tail(&sr->sr_chunks);
+    assert(tail_sc != NULL);
+
+    /*
+     * full overlap of direct storage can be treated when range contains
+     * exactly one chunk.
+     */
+    if (head_sc == tail_sc
+        && head_sc->sc_range.start > r->start
+        && head_sc->sc_range.end < r->end) {
+        /*
+         * can deal with full overlap
+         */
+        if (head_sc->sc_st != ST_TYPE_DIRECT)
+            return 0;
+
+        DEBUG_PRINT(stderr, "%s @in %p [ %llu, %llu ]\n",
+            OPENSSL_FUNC, *data, r->start, r->end);
+        rsize = r->end - r->start;
+        if (rsize <= DIRECT_STORAGE_SZ) {
+            /*
+             * update existing chunk
+             */
+            DEBUG_PRINT(stderr,
+                "%s overwrite dstorage %p [ %llu, %llu ] -> [ %llu, %llu ] "
+                "sr: %p [ %llu, %llu ]\n",
+                OPENSSL_FUNC, (void *)head_sc,
+                head_sc->sc_range.start, head_sc->sc_range.end,
+                r->start, r->end,
+                (void *)sr, sr->sr_range.start, sr->sr_range.end);
+            memcpy(head_sc->sc_data_w, *data, UINT64_TO_SIZE_T(rsize));
+
+            if (fs->cleanse)
+                OPENSSL_cleanse(deconst(*data), UINT64_TO_SIZE_T(rsize));
+
+            *data += rsize;
+            head_sc->sc_range.start = r->start;
+            head_sc->sc_range.end = r->end;
+        } else {
+            /*
+             * try to replace existing chunk
+             */
+            new_sc = new_schunk(fs, pkt, r, *data);
+            if (new_sc == NULL) {
+                DEBUG_PRINT(stderr, "%s new_chunk() alloc failed\n",
+                    OPENSSL_FUNC);
+                return -1;
+            }
+
+            DEBUG_PRINT(stderr,
+                "%s replace chunk %p [ %llu, %llu ] -> %p [ %llu, %llu ]\n",
+                OPENSSL_FUNC,
+                (void *)head_sc, head_sc->sc_range.start, head_sc->sc_range.end,
+                (void *)new_sc, new_sc->sc_range.start, new_sc->sc_range.end);
+            ossl_list_sc_remove(&sr->sr_chunks, head_sc);
+            ossl_list_sc_insert_head(&sr->sr_chunks, new_sc);
+            destroy_schunk(fs, head_sc);
+        }
+        DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> [ %llu, %llu ]\n",
+            (void *)sr, sr->sr_range.start, sr->sr_range.end, r->start, r->end);
+        sr->sr_range.start = r->start;
+        sr->sr_range.end = r->end;
+        /*
+         * indicate that while range got consumed.
+         */
+        r->start = 0;
+        r->end = 0;
+    } else if (tail_sc->sc_range.end < r->end) {
+        /*
+         * append only
+         */
+        if (tail_sc->sc_st != ST_TYPE_DIRECT)
+            return 0;
+
+        dsize = get_sc_dstorage_sz(tail_sc);
+        if (dsize == 0)
+            return 0;
+
+        DEBUG_PRINT(stderr, "%s append: @in %p [ %llu, %llu ] dsize: %llu "
+                            "tail_sc: %p [ %llu, %llu] sr: %p [ %llu, %llu ]\n",
+            OPENSSL_FUNC, *data, r->start, r->end, dsize,
+            (void *)tail_sc, tail_sc->sc_range.start, tail_sc->sc_range.end,
+            (void *)sr, sr->sr_range.start, sr->sr_range.end);
+
+        if (r->start < tail_sc->sc_range.end) {
+            rsize = tail_sc->sc_range.end - r->start;
+
+            if (fs->cleanse)
+                OPENSSL_cleanse(deconst(*data), UINT64_TO_SIZE_T(rsize));
+
+            *data += rsize;
+
+            r->start = tail_sc->sc_range.end;
+        }
+
+        rsize = r->end - r->start;
+        /*
+         * earlier check done in ossl_sframe_set_insert() ensures
+         * there is at least some data to append.
+         */
+        assert(rsize > 0);
+        rsize = (rsize < dsize) ? rsize : dsize;
+        offset = SCHUNK_SIZE(tail_sc);
+        memcpy(&tail_sc->sc_data_w[offset], *data, UINT64_TO_SIZE_T(rsize));
+        DEBUG_PRINT(stderr, "%s append tail_sc: %p [ %llu, %llu ] -> ",
+            OPENSSL_FUNC, (void *)tail_sc,
+            tail_sc->sc_range.start, tail_sc->sc_range.end);
+        tail_sc->sc_range.end += rsize;
+        DEBUG_PRINT(stderr, "[ %llu, %llu ]\n",
+            tail_sc->sc_range.start, tail_sc->sc_range.end);
+        assert(SCHUNK_SIZE(tail_sc) <= DIRECT_STORAGE_SZ);
+        DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> ",
+            (void *)sr, sr->sr_range.start, sr->sr_range.end);
+        sr->sr_range.end = tail_sc->sc_range.end;
+        DEBUG_PRINT(stderr, "[ %llu, %llu ]\n",
+            sr->sr_range.start, sr->sr_range.end);
+
+        if (fs->cleanse)
+            OPENSSL_cleanse(deconst(*data), UINT64_TO_SIZE_T(rsize));
+
+        *data += rsize;
+        r->start = tail_sc->sc_range.end;
+    } else if (head_sc->sc_range.start > r->start) {
+        const unsigned char *data_buf;
+        /*
+         * prepend only
+         */
+        if (head_sc->sc_st != ST_TYPE_DIRECT)
+            return 0;
+
+        dsize = get_sc_dstorage_sz(head_sc);
+        if (dsize == 0)
+            return 0;
+
+        DEBUG_PRINT(stderr, "%s prepend: @in %p [ %llu, %llu ] dsize: %llu "
+                            "sr: %p [ %llu, %llu ]\n",
+            OPENSSL_FUNC, *data, r->start, r->end, dsize,
+            (void *)sr, sr->sr_range.start, sr->sr_range.end);
+
+        if (r->end > head_sc->sc_range.start) {
+            if (fs->cleanse)
+                OPENSSL_cleanse(
+                    deconst(*data + (head_sc->sc_range.start - r->start)),
+                    UINT64_TO_SIZE_T(r->end - head_sc->sc_range.start));
+            r->end = head_sc->sc_range.start;
+        }
+
+        rsize = r->end - r->start;
+        /*
+         * earlier check done in ossl_sframe_set_insert() ensures
+         * there is at least some data to append.
+         */
+        assert(rsize > 0);
+        rsize = (rsize < dsize) ? rsize : dsize;
+        memmove(&head_sc->sc_data_w[rsize], head_sc->sc_data,
+            UINT64_TO_SIZE_T(SCHUNK_SIZE(head_sc)));
+        offset = r->end - rsize - r->start;
+        assert(offset < r->end - r->start);
+        data_buf = *data;
+        memcpy(head_sc->sc_data_w, &data_buf[offset], UINT64_TO_SIZE_T(rsize));
+        DEBUG_PRINT(stderr, "%s prepend head_sc: %p [ %llu, %llu ] -> ",
+            OPENSSL_FUNC, (void *)head_sc,
+            head_sc->sc_range.start, head_sc->sc_range.end);
+        head_sc->sc_range.start -= rsize;
+        DEBUG_PRINT(stderr, "[ %llu, %llu ]\n",
+            head_sc->sc_range.start, head_sc->sc_range.end);
+        assert(SCHUNK_SIZE(head_sc) <= DIRECT_STORAGE_SZ);
+        DEBUG_PRINT(stderr, "\trange: %p [ %llu, %llu ] -> ",
+            (void *)sr, sr->sr_range.start, sr->sr_range.end);
+        sr->sr_range.start = head_sc->sc_range.start;
+        DEBUG_PRINT(stderr, "[ %llu, %llu ]\n",
+            sr->sr_range.start, sr->sr_range.end);
+        assert(r->end - r->start >= rsize);
+
+        if (fs->cleanse)
+            OPENSSL_cleanse(deconst(&data_buf[offset]),
+                UINT64_TO_SIZE_T(rsize));
+
+        r->end -= rsize;
+    } else {
+        assert(0);
+        return -1;
+    }
+
+    /*
+     * returns 1 if all data were consumed
+     */
+    assert(r->end >= r->start);
+    DEBUG_PRINT(stderr, "%s @out %p [ %llu, %llu ]\n",
+        OPENSSL_FUNC, *data, r->start, r->end);
+
+    return ((r->end - r->start) == 0) ? 1 : 0;
+}
+
+/*
+ * If there is partial overlap between newly received data `r` and
+ * existing stream range `sr`, then this function trims overlapping
+ * bytes from `r`.
+ *    sr   - pointer to stream range
+ *    r    - pointer to range of bytes received in stream frame
+ *    data - pointer to data bytes delivered in stream frame
+ * function updates r so there is no partial overlap between and sr
+ * after function returns. Function returns pointer to the first
+ * data byte in stream after `r` is adjusted. Function returns `data`
+ * when no trimming happened.
+ */
+static const unsigned char *trim_partial_overlap(SFRAME_SET *fs,
+    struct stream_range_t *sr, UINT_RANGE *r, const unsigned char *data)
+{
+    uint64_t unused_sz;
+
+    if (!(r->start < sr->sr_range.start && r->end > sr->sr_range.end)) {
+        if (r->end > sr->sr_range.end && r->start < sr->sr_range.end) {
+            unused_sz = sr->sr_range.end - r->start;
+            if (fs->cleanse)
+                OPENSSL_cleanse(deconst(data), UINT64_TO_SIZE_T(unused_sz));
+
+            DEBUG_PRINT(stderr, "%s right overlap %p [ %llu, %llu ]:\n\t"
+                                "r: [ %llu, %llu ] -> [ %llu, %llu ]\n",
+                OPENSSL_FUNC,
+                (void *)sr, sr->sr_range.start, sr->sr_range.end,
+                r->start, r->end,
+                sr->sr_range.end, r->end);
+
+            data += unused_sz;
+            r->start = sr->sr_range.end;
+        } else if (r->start < sr->sr_range.start
+            && r->end > sr->sr_range.start) {
+            unused_sz = r->end - sr->sr_range.start;
+            if (fs->cleanse)
+                OPENSSL_cleanse(deconst(data
+                                    + (sr->sr_range.start - r->start)),
+                    UINT64_TO_SIZE_T(unused_sz));
+
+            DEBUG_PRINT(stderr, "%s left overlap %p [ %llu, %llu]:\n\t"
+                                "r: [ %llu, %llu ] -> [ %llu, %llu ]\n",
+                OPENSSL_FUNC,
+                (void *)sr, sr->sr_range.start, sr->sr_range.end,
+                r->start, r->end,
+                r->start, sr->sr_range.start);
+            r->end = sr->sr_range.start;
+        }
+    }
+
+    return data;
+}
+
+/*
+ * Inserts a newly received chunk to the head of the chunk list.
+ */
+static void prepend_chunk(SFRAME_SET *fs, struct stream_range_t *sr,
+    struct stream_chunk_t *sc)
+{
+    DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] add to head %p [ %llu, %llu ] "
+                        "-> [ %llu, %llu ]\n",
+        OPENSSL_FUNC,
+        (void *)sc, sc->sc_range.start, sc->sc_range.end,
+        (void *)sr, sr->sr_range.start, sr->sr_range.end,
+        sc->sc_range.start, sr->sr_range.end);
+
+    /*
+     * the new chunk must not be empty
+     */
+    assert(sc->sc_range.end > sc->sc_range.start);
+    /*
+     * and must not overlap range.
+     */
+    assert(sc->sc_range.end == sr->sr_range.start);
+
+    ossl_list_sc_insert_head(&sr->sr_chunks, sc);
+    sr->sr_range.start = sc->sc_range.start;
+    fs->stream_chunks++;
+}
+
+/*
+ * Inserts a newly received chunk to the tail of the chunk list.
+ */
+static void append_chunk(SFRAME_SET *fs, struct stream_range_t *sr,
+    struct stream_chunk_t *sc)
+{
+    DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] add to tail %p [ %llu, %llu ] "
+                        "-> [ %llu, %llu ]\n",
+        OPENSSL_FUNC,
+        (void *)sc, sc->sc_range.start, sc->sc_range.end,
+        (void *)sr, sr->sr_range.start, sr->sr_range.end,
+        sr->sr_range.start, sc->sc_range.end);
+
+    /*
+     * the new chunk must not be empty
+     */
+    assert(sc->sc_range.end > sc->sc_range.start);
+    /*
+     * and must not overlap range
+     */
+    assert(sc->sc_range.start == sr->sr_range.end);
+
+    ossl_list_sc_insert_tail(&sr->sr_chunks, sc);
+    sr->sr_range.end = sc->sc_range.end;
+    fs->stream_chunks++;
+}
+
+static void replace_chunks_in_range(SFRAME_SET *fs, struct stream_range_t *sr,
+    struct stream_chunk_t *sc)
+{
+    struct stream_chunk_t *destroy_sc;
+
+    while ((destroy_sc = ossl_list_sc_head(&sr->sr_chunks)) != NULL) {
+        ossl_list_sc_remove(&sr->sr_chunks, destroy_sc);
+        fs->stream_chunks--;
+        destroy_schunk(fs, destroy_sc);
+    }
+
+    ossl_list_sc_insert_head(&sr->sr_chunks, sc);
+    fs->stream_chunks++;
+    DEBUG_PRINT(stderr, "%s range: %p [ %llu, %llu ] -> [ %llu, %llu ]\n",
+        OPENSSL_FUNC, (void *)sr, sr->sr_range.start, sr->sr_range.end,
+        sc->sc_range.start, sc->sc_range.end);
+    sr->sr_range.start = sc->sc_range.start;
+    sr->sr_range.end = sc->sc_range.end;
+}
+
+static struct stream_range_t *find_range(SFRAME_SET *fs,
+    struct stream_range_t *key)
+{
+    struct stream_range_t *sr = NULL;
+
+    if (!OSSL_RBT_EMPTY(srange, &fs->ranges))
+        sr = OSSL_RBT_FIND(srange, &fs->ranges, key);
+
+    return sr;
+}
+
+/*
+ * This function help us to merge two ranges (list of chunks)
+ * into single range. Function moves the end of the range
+ * towards start. It effectively chops n last chunks until
+ * new_end is found.
+ */
+static int chop_range(SFRAME_SET *fs, struct stream_range_t *sr,
+    uint64_t new_end)
+{
+    struct stream_chunk_t *sc;
+
+    assert(sr->sr_range.end >= new_end);
+
+    while ((sc = ossl_list_sc_tail(&sr->sr_chunks)) != NULL) {
+        if (sc->sc_range.start >= new_end) {
+            ossl_list_sc_remove(&sr->sr_chunks, sc);
+            fs->stream_chunks--;
+            destroy_schunk(fs, sc);
+        } else {
+            break;
+        }
+    }
+
+    if (sc == NULL)
+        return 0;
+
+    assert(new_end <= sc->sc_range.end);
+    assert(sc->sc_range.start < new_end);
+
+    unused_sz = UINT64_TO_SIZE_T(sc->sc_range.end - new_end);
+    if (unused_sz == 0) {
+        sr->sr_range.end = new_end;
+        return 1;
+    }
+
+    sc_data_trim_right(sc, unused_sz, fs->cleanse);
+    sc->sc_range.end = new_end;
+    sr->sr_range.end = new_end;
+
+    return 1;
+}
+
+/*
+ * function merges two with full overlap. The super_sr range
+ * contains the whole sub_sr range. The function destroys
+ * sub_sr and returns super_sr.
+ */
+static struct stream_range_t *merge_ranges(SFRAME_SET *fs,
+    struct stream_range_t *super_sr, struct stream_range_t *sub_sr)
+{
+    /*
+     * both ranges must not be empty
+     */
+    assert(super_sr->sr_range.start < super_sr->sr_range.end);
+    assert(sub_sr->sr_range.start < sub_sr->sr_range.end);
+    /*
+     * 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);
+
+    DEBUG_PRINT(stderr, "%s super: %p [ %llu, %llu ], sub: %p [ %llu, %llu]\n",
+        OPENSSL_FUNC, (void *)super_sr, super_sr->sr_range.start,
+        super_sr->sr_range.end, (void *)sub_sr, sub_sr->sr_range.start,
+        sub_sr->sr_range.end);
+    destroy_srange(fs, sub_sr);
+
+    return super_sr;
+}
+
+/*
+ * The ranges are either adjacent
+ * (left_sr->sr_range.end == right_sr->sr_range.end) or there
+ * is partial overlap between left_sr and right_sr(
+ * (left_sr->sr_range.end >= right_sr->sr_range.start).
+ * If there is partial overlap, then the left range is chopped
+ * so its end is aligned with start of right_sr.
+ */
+static struct stream_range_t *append_range(SFRAME_SET *fs,
+    struct stream_range_t *left_sr, struct stream_range_t *right_sr)
+{
+    /*
+     * both ranges must not be empty
+     */
+    assert(left_sr->sr_range.start < left_sr->sr_range.end);
+    assert(right_sr->sr_range.start < right_sr->sr_range.end);
+    /*
+     * right range follows left range (left < right)
+     */
+    assert(left_sr->sr_range.end >= right_sr->sr_range.start);
+
+    DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] + %p [ %llu, %llu ] = %p "
+                        "[ %llu, %llu ]\n",
+        OPENSSL_FUNC, (void *)left_sr, left_sr->sr_range.start,
+        left_sr->sr_range.end, (void *)right_sr, right_sr->sr_range.start,
+        right_sr->sr_range.end, (void *)left_sr,
+        left_sr->sr_range.start, right_sr->sr_range.end);
+
+    /*
+     * make sure there is no overlap between ranges
+     *    (right_sr->sr_range.start == left_sr->sr_range.end)
+     */
+    if (chop_range(fs, left_sr, right_sr->sr_range.start) == 0)
+        return NULL;
+
+    ossl_list_sc_join(&left_sr->sr_chunks, &right_sr->sr_chunks);
+    left_sr->sr_range.end = right_sr->sr_range.end;
+
+    destroy_srange(fs, right_sr);
+
+    return left_sr;
+}
+
+/*
+ * receives a chunk of data from stream frame.
+ */
+int ossl_sframe_set_insert(SFRAME_SET *fs, UINT_RANGE *r, OSSL_QRX_PKT *pkt,
+    const unsigned char *data, int fin)
+{
+    struct stream_range_t *sr = NULL;
+    struct stream_range_t *adjacent_sr = NULL;
+    struct stream_range_t *joined_sr = NULL;
+    struct stream_chunk_t *sc = NULL;
+    struct stream_range_t key_sr = { 0 };
+
+    /*
+     * 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.
+     */
+    if (fin != 0) {
+        if (fs->fin == 0) {
+            fs->fin = 1;
+            fs->fin_off = r->end;
+        } else if (fs->fin_off != r->end) {
+            return 0;
+        }
+    }
+
+    /*
+     * discard any data past FIN offset (of 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)
+            return 0;
+    }
+
+    if (r->end <= fs->offset) {
+        /*
+         * retransmitted range got consumed already.
+         */
+        DEBUG_PRINT(stderr, "%s [ %llu, %llu ] <= %llu\n", OPENSSL_FUNC,
+            r->start, r->end, fs->offset);
+        if (fs->cleanse && data != NULL)
+            OPENSSL_cleanse(deconst(data), UINT64_TO_SIZE_T(r->end - r->start));
+        return 1;
+    }
+
+    if (r->start < fs->offset) {
+        /*
+         * Make sure retransmitted chunk does not reintroduce
+         * bytes which were consumed already.
+         * Make sure retransmitted chunk does not reintroduce
+         * bytes which were consumed already.
+         */
+        DEBUG_PRINT(stderr, "%s [ %llu, %llu ] -> [ %llu, %llu ]\n", OPENSSL_FUNC,
+            r->start, r->end, fs->offset, r->end);
+        if (fs->cleanse)
+            OPENSSL_cleanse(deconst(data),
+                UINT64_TO_SIZE_T(fs->offset - r->start));
+        data += fs->offset - r->start;
+        r->start = fs->offset;
+    }
+
+    key_sr.sr_range = *r;
+
+    /*
+     * Empty, 0 size chunk can carry the FIN bit only,
+     * and that has been just handled above.
+     */
+    if (r->start == r->end)
+        return 1;
+
+    assert(r->start < r->end);
+
+    if ((sr = find_range(fs, &key_sr)) == NULL) {
+        sc = new_schunk(fs, pkt, r, data);
+        if (sc == NULL)
+            goto err;
+
+        sr = create_range(fs, sc);
+        if (sr == NULL)
+            goto err;
+        DEBUG_PRINT(stderr, "%s chunk: %p [ %llu, %llu ] new range: %p\n",
+            OPENSSL_FUNC, (void *)sc, sc->sc_range.start, sc->sc_range.end,
+            (void *)sr);
+        sc = NULL;
+        OSSL_RBT_INSERT(srange, &fs->ranges, sr);
+        fs->stream_ranges++;
+    } else {
+        /*
+         * retransmission, the whole chunk is found in existing range already
+         */
+        if (r->start >= sr->sr_range.start && r->end <= sr->sr_range.end) {
+            DEBUG_PRINT(stderr,
+                "%s [ %llu, %llu ] found in %p [ %llu, %llu ]\n", OPENSSL_FUNC,
+                r->start, r->end, (void *)sr, sr->sr_range.start,
+                sr->sr_range.end);
+            if (fs->cleanse)
+                OPENSSL_cleanse(deconst(data),
+                    UINT64_TO_SIZE_T(r->end - r->start));
+            goto done; /* Range is present already. */
+        }
+
+        switch (try_dstorage(fs, pkt, sr, r, &data)) {
+        case 0:
+            break;
+        case 1:
+            /*
+             * all data were consumed, range is updated.
+             */
+            goto range_updated;
+        default:
+            /*
+             * malloc error. forget the range we found.
+             */
+            sr = NULL;
+            goto err;
+        }
+
+        /*
+         * full overlap between sr and r is handled by replace_chunks_in_range()
+         * we call after we allocate stream chunk sc for newly received range r.
+         */
+        data = trim_partial_overlap(fs, sr, r, data);
+
+        sc = new_schunk(fs, pkt, r, data);
+        if (sc == NULL) {
+            sr = NULL;
+            goto err;
+        }
+        DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] -> %p [ %llu, %llu ]\n",
+            OPENSSL_FUNC, (void *)sc, sc->sc_range.start, sc->sc_range.end,
+            (void *)sr, sr->sr_range.start, sr->sr_range.end);
+
+        /*
+         * sandwich, append, prepend 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.
+         */
+        if (sc->sc_range.start < sr->sr_range.start
+            && sc->sc_range.end > sr->sr_range.end) {
+            /* new chunk includes the whole range */
+            replace_chunks_in_range(fs, sr, sc);
+            sc = NULL; /* chunk got consumed */
+        } else if (sc->sc_range.end > sr->sr_range.end
+            && sc->sc_range.start <= sr->sr_range.end) {
+            append_chunk(fs, sr, sc);
+            sc = NULL; /* chunk got consumed */
+        } else if (sc->sc_range.start < sr->sr_range.start
+            && sc->sc_range.end >= sr->sr_range.start) {
+            prepend_chunk(fs, sr, sc);
+            sc = NULL; /* chunk got consumed */
+        } else {
+            assert(NULL); /* unreachable */
+            sr = NULL;
+            goto err;
+        }
+
+    range_updated:
+        /*
+         * Range got updated we may need to join updated range with
+         * another ranges which exist in tree. The current range
+         * is removed here and used as a search key. If nothing is found
+         * range is inserted back to tree.
+         *
+         * If another range is found the ranges are merged to single
+         * range. The process repeats (merging ranges may be cascade effect,
+         * where more ranges collapse to single range).  The merge result is
+         * removed from tree and used as a search key to find another range.
+         * If nothing is found then update is done. otherwise the ranges
+         * are merged again.
+         */
+        OSSL_RBT_REMOVE(srange, &fs->ranges, sr);
+        fs->stream_ranges--;
+        /*
+         * _INSERT() returns range where sr needs to be joined
+         */
+        adjacent_sr = OSSL_RBT_INSERT(srange, &fs->ranges, sr);
+
+        while (adjacent_sr != NULL) {
+            OSSL_RBT_REMOVE(srange, &fs->ranges, adjacent_sr);
+            DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] >< %p [ %llu, %llu ]\n",
+                OPENSSL_FUNC, (void *)sr, sr->sr_range.start, sr->sr_range.end,
+                (void *)adjacent_sr, adjacent_sr->sr_range.start,
+                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) {
+                /*
+                 *  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) {
+                /*
+                 *  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) {
+                /*
+                 * 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) {
+                /*
+                 *  sr follows adjacent_sr
+                 */
+                assert(sr->sr_range.end > adjacent_sr->sr_range.end);
+                joined_sr = append_range(fs, adjacent_sr, sr);
+            } else {
+                assert(NULL); /* never happens */
+                joined_sr = NULL;
+            }
+            if (joined_sr == NULL)
+                goto err;
+
+            sr = joined_sr;
+            adjacent_sr = OSSL_RBT_INSERT(srange, &fs->ranges, sr);
+        }
+        fs->stream_ranges++;
+    }
+
+done:
+    return 1;
+
+err:
+    destroy_schunk(fs, sc);
+    destroy_srange(fs, sr);
+    destroy_srange(fs, adjacent_sr);
+    /*
+     * not enough memory (or another serious error) has occurred,
+     * any error here is fatal as some stream chunks could be ACKed
+     * already (RFC 9000, 31.1 Packet processing). At least stream
+     * needs to be reset. Preferred action is to close connection.
+     */
+
+    return 0;
+}
+
+/*
+ * peeks over the continuous range which is ready to
+ * read. ready to read means the fs->offset must be
+ * found in range. Also fs->offset can not reach past
+ * the first gap in stream data received so far, thus
+ * the only range we can use for peek operation is
+ * OSSL_RBT_MIN(&fs->ranges).
+ *
+ * NOTE: it is unsafe to carry more _peek() operations
+ * over single SFRMAE_SET.
+ */
+int ossl_sframe_set_peek(SFRAME_SET *fs, void **iterator,
+    UINT_RANGE *range, const unsigned char **data,
+    int *fin)
+{
+    uint64_t start;
+    struct stream_range_t *sr = (struct stream_range_t *)*iterator;
+    struct stream_chunk_t *sc = NULL;
+
+    if (sr == NULL) {
+        sr = OSSL_RBT_MIN(srange, &fs->ranges);
+        start = fs->offset;
+        if (sr != NULL) {
+            sc = ossl_list_sc_head(&sr->sr_chunks);
+            sr->sr_it_sc = NULL;
+            assert(sc->sc_range.start == sr->sr_range.start);
+        }
+        /*
+         * no chunks are ready to be consumed, if there is a gap.
+         */
+        if (sc != NULL && sc->sc_range.start > start) {
+            DEBUG_PRINT(stderr, "%s sc: %p sr: %p sc->start %llu, fs->offset: %llu\n",
+                OPENSSL_FUNC, (void *)sc, (void *)sr, sc->sc_range.start, start);
+            sc = NULL;
+        }
+    } else if (sr == OSSL_RBT_MIN(srange, &fs->ranges) && sr->sr_it_sc != NULL) {
+        /*
+         * sr == _RB_MIN(), revalidates iterator in case the range we
+         * work with disappears because it's got joined with other range
+         * after new chunk arrival. perhaps not issue now as those operations
+         * are mutually exclusive now.
+         *
+         * sr->sr_it_sc becomes NULL on _move() or _flatten() operation.
+         *
+         * We may need to revisit iterator implementation as current
+         * iterator supports one caller only.
+         */
+        start = sr->sr_it_sc->sc_range.end;
+        sc = ossl_list_sc_next(sr->sr_it_sc);
+        assert(sc == NULL || sc->sc_range.start == start);
+        assert(sc == NULL || sc->sc_range.start < sc->sc_range.end);
+    } else {
+        /* iterator got invalidated by move/flatten operation on range */
+        DEBUG_PRINT(stderr, "%s iterator got invalidated\n", OPENSSL_FUNC);
+        return 0;
+    }
+
+    range->start = start;
+
+    if (sc == NULL) {
+        range->end = start;
+        *data = NULL;
+        *iterator = NULL;
+
+        /*
+         * set fin only if we are at the end of the stream and application
+         * can read from the stream. In other words: there must be no gap
+         * between FIN offset and offset where application reads from stream.
+         */
+        if (fs->fin && start == fs->fin_off)
+            *fin = fs->fin;
+        else
+            *fin = 0;
+
+        DEBUG_PRINT(stderr, "%s no more chunks\n", OPENSSL_FUNC);
+
+        return 0;
+    }
+
+    range->end = sc->sc_range.end;
+    /* chunk keeps data always attached, data dies with chunk */
+    assert(sc->sc_data != NULL);
+    assert(sc->sc_range.start <= start);
+    *data = sc->sc_data + (start - sc->sc_range.start);
+    *fin = fs->fin && sc->sc_range.end == fs->fin_off;
+
+    if (sr->sr_it_sc != NULL)
+        DEBUG_PRINT(stderr, "%s %p [ %llu, %llu ] %p [ %llu, %llu ]\n",
+            OPENSSL_FUNC, (void *)sr->sr_it_sc,
+            sr->sr_it_sc->sc_range.start, sr->sr_it_sc->sc_range.end,
+            (void *)sc, sc->sc_range.start, sc->sc_range.end);
+
+    sr->sr_it_sc = sc;
+    *iterator = sr;
+
+    /*
+     * peek operation indicates error if there are no data to read
+     * in range.
+     */
+    DEBUG_PRINT(stderr,
+        "%s peek range: [ %llu, %llu ] range: %p [ %llu, %llu ]\n", OPENSSL_FUNC,
+        range->start, range->end, (void *)sr, sr->sr_range.start,
+        sr->sr_range.end);
+
+    return (range->start == range->end) ? 0 : 1;
+}
+
+void ossl_sframe_set_destroy_ranges(SFRAME_SET *fs)
+{
+    struct stream_range_t *sr, *save_sr;
+
+    OSSL_RBT_FOREACH_SAFE (sr, srange, &fs->ranges, save_sr) {
+        OSSL_RBT_REMOVE(srange, &fs->ranges, sr);
+        fs->stream_ranges--;
+        destroy_srange(fs, sr);
+    }
+}
+
+/*
+ * moves the read offset, freeing all chunks which end offset
+ * is less than new_offset
+ *   sc->sc_range.end < new_offset
+ */
+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;
+
+    if (new_offset == fs->offset)
+        return 1;
+
+    /*
+     * Offset can move forward within the continuous head range only.
+     * It can not move backward, into a gap or past the head range end.
+     */
+    if (sr == NULL || new_offset < fs->offset
+        || new_offset < sr->sr_range.start || new_offset > sr->sr_range.end)
+        return 0;
+
+    fs->offset = new_offset;
+
+    OSSL_LIST_FOREACH_DELSAFE(sc, save_sc, sc, &sr->sr_chunks)
+    {
+        if (new_offset >= sc->sc_range.end) {
+            ossl_list_sc_remove(&sr->sr_chunks, sc);
+            fs->stream_chunks--;
+            if (sr->sr_it_sc == sc)
+                sr->sr_it_sc = NULL; /* invalidate iterator chunk */
+            destroy_schunk(fs, sc);
+        } else {
+            break;
+        }
+    }
+
+    DEBUG_PRINT(stderr, "%s offset: %llu -> %llu range: %p [ %llu, %llu ] -> ",
+        OPENSSL_FUNC, fs->offset - new_offset, new_offset,
+        (void *)sr, sr->sr_range.start, sr->sr_range.end);
+
+    if (sc == NULL) {
+        /*
+         * the whole range was consumed.
+         * this step invalidates iterator we use in ossl_sframe_peek()
+         */
+        OSSL_RBT_REMOVE(srange, &fs->ranges, sr);
+        destroy_srange(fs, sr);
+        fs->stream_ranges--;
+        DEBUG_PRINT(stderr, "[ NULL ]\n");
+    } else {
+        unused_sz = UINT64_TO_SIZE_T(new_offset - sc->sc_range.start);
+        sc_data_trim_left(sc, unused_sz, fs->cleanse);
+        sc->sc_range.start = new_offset;
+        sr->sr_range.start = new_offset;
+        DEBUG_PRINT(stderr, "[ %lli, %llu ]\n",
+            sr->sr_range.start, sr->sr_range.end);
+    }
+
+    return 1;
+}
+
+/* Contiguous bytes available from fs->offset, in O(log n): the first range. */
+int ossl_sframe_set_avail(SFRAME_SET *fs, uint64_t *avail, int *fin)
+{
+    struct stream_range_t *sr = OSSL_RBT_MIN(srange, &fs->ranges);
+
+    if (sr != NULL && sr->sr_range.start <= fs->offset
+        && sr->sr_range.end > fs->offset)
+        *avail = sr->sr_range.end - fs->offset;
+    else
+        *avail = 0;
+    *fin = (fs->fin && fs->offset + *avail == fs->fin_off) ? 1 : 0;
+    return 1;
+}
diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c
index 7a3bbf2208..f9584aae42 100644
--- a/test/quic_stream_test.c
+++ b/test/quic_stream_test.c
@@ -413,8 +413,7 @@ static int test_rstream_simple(int idx)
     unsigned char buf[sizeof(simple_data)];
     size_t readbytes = 0, avail = 0, i;
     int fin = 0;
-    int use_sc = (idx & 1) != 0;
-    int use_rbuf = (idx & 2) != 0;
+    int use_sc = idx % 2;
     int (*read_fn)(QUIC_RSTREAM *, unsigned char *, size_t, size_t *,
         int *)
         = use_sc ? test_single_copy_read
@@ -425,7 +424,7 @@ static int test_rstream_simple(int idx)
         if (!TEST_ptr(pkt[i] = pkt_test_new(1200)))
             goto err;

-    if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, 0)))
+    if (!TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL)))
         goto err;

     if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[0], 5,
@@ -449,11 +448,6 @@ static int test_rstream_simple(int idx)
         || !TEST_false(fin)
         || !TEST_size_t_eq(readbytes, 1)
         || !TEST_mem_eq(buf, 1, simple_data, 1)
-        || (use_rbuf && !TEST_false(ossl_quic_rstream_move_to_rbuf(rstream)))
-        || (use_rbuf
-            && !TEST_true(ossl_quic_rstream_resize_rbuf(rstream,
-                sizeof(simple_data))))
-        || (use_rbuf && !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream)))
         || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt[4],
             0, simple_data,
             10, 0))
@@ -485,10 +479,6 @@ static int test_rstream_simple(int idx)
             sizeof(simple_data),
             NULL,
             0, 1))
-        || (use_rbuf
-            && !TEST_true(ossl_quic_rstream_resize_rbuf(rstream,
-                2 * sizeof(simple_data))))
-        || (use_rbuf && !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream)))
         || !TEST_true(read_fn(rstream, buf + 14, 5, &readbytes, &fin))
         || !TEST_false(fin)
         || !TEST_size_t_eq(readbytes, 5)
@@ -498,7 +488,6 @@ static int test_rstream_simple(int idx)
         || !TEST_true(fin)
         || !TEST_size_t_eq(readbytes, sizeof(buf) - 14 - 5)
         || !TEST_mem_eq(buf, sizeof(buf), simple_data, sizeof(simple_data))
-        || (use_rbuf && !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream)))
         || !TEST_true(read_fn(rstream, buf, sizeof(buf), &readbytes, &fin))
         || !TEST_true(fin)
         || !TEST_size_t_eq(readbytes, 0))
@@ -535,7 +524,7 @@ 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, 0)))
+        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL)))
         goto err;

     if (idx % 3 == 0)
@@ -611,11 +600,6 @@ static int test_rstream_random(int idx)
             goto err;
         read_off += readbytes;
         queued_min = read_off;
-        if (test_random() % 50 == 0)
-            if (!TEST_true(ossl_quic_rstream_resize_rbuf(rstream,
-                    queued_max - read_off + 1))
-                || !TEST_true(ossl_quic_rstream_move_to_rbuf(rstream)))
-                goto err;
         if (!fin_set && queued_max >= data_size - test_random() % 200) {
             fin_set = 1;
             /* Queue empty fin frame */
diff --git a/test/quic_txp_test.c b/test/quic_txp_test.c
index ee1432129b..a0b480c86d 100644
--- a/test/quic_txp_test.c
+++ b/test/quic_txp_test.c
@@ -1508,7 +1508,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, 1024))) {
+                                 NULL))) {
                 ossl_quic_sstream_free(s->sstream);
                 ossl_quic_stream_map_release(h.args.qsm, s);
                 goto err;