Commit b98e1931a2 for openssl.org

commit b98e1931a2d46a297263bb831aa0ca5449602732
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date:   Mon Sep 7 15:28:13 2026 +0200

    test: cover two sided overlap of direct tail chunk

    A short retransmitted frame which starts below an existing range and
    ends past its direct storage tail chunk slips past the full overlap
    guard in try_dstorage() because it is not larger than the direct
    storage size. The append path then treats every byte below the tail
    chunk end as a duplicate and drops the genuinely new bytes below the
    range start while reporting success, so the frame is acked and the
    gap in the stream becomes permanent.

    Assisted-by: Claude:claude-fable-5
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Merge-date: Sat Sep 26 11:35:37 2026
    Merged-from: https://github.com/openssl/openssl/pull/32038

diff --git a/test/quic_stream_test.c b/test/quic_stream_test.c
index fdeca3bee9..3c4ffba17f 100644
--- a/test/quic_stream_test.c
+++ b/test/quic_stream_test.c
@@ -1013,6 +1013,81 @@ err:
     return ret;
 }

+/*
+ * A zero length read is a successful no-op returning zero read bytes,
+ * and releasing a record without consuming any bytes succeeds likewise.
+ * Neither may fail once at least one byte has been consumed, otherwise
+ * quic_read_actual() turns the failed read into a fatal SSL error for
+ * SSL_read_ex() called with a zero length buffer.
+ */
+static int test_rstream_zero_length_read(void)
+{
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    OSSL_QRX_PKT *pkt = NULL;
+    unsigned char pdata[10], buf[10];
+    const unsigned char *record = NULL;
+    size_t readbytes = 0, rec_len = 0, i;
+    int fin = 0;
+    int ret = 0;
+
+    for (i = 0; i < sizeof(pdata); ++i)
+        pdata[i] = (unsigned char)(0x40 + i);
+
+    if (!TEST_ptr(pkt = pkt_test_new(1200))
+        || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
+        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
+        goto err;
+
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, 0,
+            pdata, sizeof(pdata), 0)))
+        goto err;
+
+    /* a zero length read before anything is consumed */
+    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin))
+        || !TEST_size_t_eq(readbytes, 0))
+        goto err;
+
+    /* consume some bytes so the stream offset is not zero */
+    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin))
+        || !TEST_size_t_eq(readbytes, 5)
+        || !TEST_mem_eq(buf, 5, pdata, 5))
+        goto err;
+
+    /* a zero length read with data pending at a nonzero offset */
+    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin))
+        || !TEST_size_t_eq(readbytes, 0))
+        goto err;
+
+    /* releasing a record without consuming anything succeeds too */
+    if (!TEST_true(ossl_quic_rstream_get_record(rstream, &record, &rec_len,
+            &fin))
+        || !TEST_size_t_eq(rec_len, 5)
+        || !TEST_true(ossl_quic_rstream_release_record(rstream, 0)))
+        goto err;
+
+    /* the remaining bytes are intact and still readable */
+    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf),
+            &readbytes, &fin))
+        || !TEST_size_t_eq(readbytes, 5)
+        || !TEST_mem_eq(buf, 5, pdata + 5, 5))
+        goto err;
+
+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
+    ret = 1;
+
+err:
+    ossl_quic_rstream_free(rstream);
+    ossl_quic_rstream_qparm_destroy(rsqp);
+    pkt_test_free(pkt);
+    ossl_quic_channel_free(ch);
+    return ret;
+}
+
 /*
  * A short retransmit which starts below an existing range and ends past its
  * direct storage tail chunk is small enough to slip past the full overlap
@@ -1105,81 +1180,6 @@ err:
     return ret;
 }

-/*
- * A zero length read is a successful no-op returning zero read bytes,
- * and releasing a record without consuming any bytes succeeds likewise.
- * Neither may fail once at least one byte has been consumed, otherwise
- * quic_read_actual() turns the failed read into a fatal SSL error for
- * SSL_read_ex() called with a zero length buffer.
- */
-static int test_rstream_zero_length_read(void)
-{
-    QUIC_RSTREAM *rstream = NULL;
-    QUIC_CHANNEL *ch = NULL;
-    QUIC_RSTREAM_QPARM *rsqp = NULL;
-    OSSL_QRX_PKT *pkt = NULL;
-    unsigned char pdata[10], buf[10];
-    const unsigned char *record = NULL;
-    size_t readbytes = 0, rec_len = 0, i;
-    int fin = 0;
-    int ret = 0;
-
-    for (i = 0; i < sizeof(pdata); ++i)
-        pdata[i] = (unsigned char)(0x40 + i);
-
-    if (!TEST_ptr(pkt = pkt_test_new(1200))
-        || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
-        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
-        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
-        goto err;
-
-    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, 0,
-            pdata, sizeof(pdata), 0)))
-        goto err;
-
-    /* a zero length read before anything is consumed */
-    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin))
-        || !TEST_size_t_eq(readbytes, 0))
-        goto err;
-
-    /* consume some bytes so the stream offset is not zero */
-    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin))
-        || !TEST_size_t_eq(readbytes, 5)
-        || !TEST_mem_eq(buf, 5, pdata, 5))
-        goto err;
-
-    /* a zero length read with data pending at a nonzero offset */
-    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin))
-        || !TEST_size_t_eq(readbytes, 0))
-        goto err;
-
-    /* releasing a record without consuming anything succeeds too */
-    if (!TEST_true(ossl_quic_rstream_get_record(rstream, &record, &rec_len,
-            &fin))
-        || !TEST_size_t_eq(rec_len, 5)
-        || !TEST_true(ossl_quic_rstream_release_record(rstream, 0)))
-        goto err;
-
-    /* the remaining bytes are intact and still readable */
-    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf),
-            &readbytes, &fin))
-        || !TEST_size_t_eq(readbytes, 5)
-        || !TEST_mem_eq(buf, 5, pdata + 5, 5))
-        goto err;
-
-    if (!TEST_int_eq(ch->protocol_error, 0))
-        goto err;
-
-    ret = 1;
-
-err:
-    ossl_quic_rstream_free(rstream);
-    ossl_quic_rstream_qparm_destroy(rsqp);
-    pkt_test_free(pkt);
-    ossl_quic_channel_free(ch);
-    return ret;
-}
-
 /*
  * A FIN must be rejected as a final size error when data is already
  * buffered past its offset, or when the application has consumed more
@@ -1257,81 +1257,6 @@ err:
     return ret;
 }

-/*
- * A zero length read is a successful no-op returning zero read bytes,
- * and releasing a record without consuming any bytes succeeds likewise.
- * Neither may fail once at least one byte has been consumed, otherwise
- * quic_read_actual() turns the failed read into a fatal SSL error for
- * SSL_read_ex() called with a zero length buffer.
- */
-static int test_rstream_zero_length_read(void)
-{
-    QUIC_RSTREAM *rstream = NULL;
-    QUIC_CHANNEL *ch = NULL;
-    QUIC_RSTREAM_QPARM *rsqp = NULL;
-    OSSL_QRX_PKT *pkt = NULL;
-    unsigned char pdata[10], buf[10];
-    const unsigned char *record = NULL;
-    size_t readbytes = 0, rec_len = 0, i;
-    int fin = 0;
-    int ret = 0;
-
-    for (i = 0; i < sizeof(pdata); ++i)
-        pdata[i] = (unsigned char)(0x40 + i);
-
-    if (!TEST_ptr(pkt = pkt_test_new(1200))
-        || !TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
-        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
-        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
-        goto err;
-
-    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt, 0,
-            pdata, sizeof(pdata), 0)))
-        goto err;
-
-    /* a zero length read before anything is consumed */
-    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin))
-        || !TEST_size_t_eq(readbytes, 0))
-        goto err;
-
-    /* consume some bytes so the stream offset is not zero */
-    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin))
-        || !TEST_size_t_eq(readbytes, 5)
-        || !TEST_mem_eq(buf, 5, pdata, 5))
-        goto err;
-
-    /* a zero length read with data pending at a nonzero offset */
-    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 0, &readbytes, &fin))
-        || !TEST_size_t_eq(readbytes, 0))
-        goto err;
-
-    /* releasing a record without consuming anything succeeds too */
-    if (!TEST_true(ossl_quic_rstream_get_record(rstream, &record, &rec_len,
-            &fin))
-        || !TEST_size_t_eq(rec_len, 5)
-        || !TEST_true(ossl_quic_rstream_release_record(rstream, 0)))
-        goto err;
-
-    /* the remaining bytes are intact and still readable */
-    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf),
-            &readbytes, &fin))
-        || !TEST_size_t_eq(readbytes, 5)
-        || !TEST_mem_eq(buf, 5, pdata + 5, 5))
-        goto err;
-
-    if (!TEST_int_eq(ch->protocol_error, 0))
-        goto err;
-
-    ret = 1;
-
-err:
-    ossl_quic_rstream_free(rstream);
-    ossl_quic_rstream_qparm_destroy(rsqp);
-    pkt_test_free(pkt);
-    ossl_quic_channel_free(ch);
-    return ret;
-}
-
 #define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \
                      "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"