Commit deabb2fdae for openssl.org

commit deabb2fdae40be741f9a78d5e14066338ba6b546
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date:   Mon Sep 7 17:09:41 2026 +0200

    test: cover FIN final size against buffered data

    A FIN whose final size lies below data already buffered in a higher
    range passes the current validation because only the lowest range is
    checked, so the frames beyond the final size stay pinned unreadable
    until the stream is torn down. A FIN below the offset the application
    has consumed already skips the validation entirely when no ranges are
    buffered, so it is recorded but can never signal the end of the stream.
    Both must be rejected as a final size error per RFC 9000 section 4.5.

    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:35 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 8a10b64db6..a8754e3dbb 100644
--- a/test/quic_stream_test.c
+++ b/test/quic_stream_test.c
@@ -1105,6 +1105,158 @@ 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
+ * bytes than the final size claims. Otherwise the frames beyond the FIN
+ * offset stay pinned unreadable until the stream is torn down, and a
+ * FIN below the consumed offset is recorded but can never signal the
+ * end of the stream to the application.
+ */
+static int test_rstream_fin_final_size(void)
+{
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    OSSL_QRX_PKT *pkt_a = NULL, *pkt_b = NULL;
+    unsigned char pdata[16], buf[16];
+    size_t readbytes = 0, i;
+    int fin = 0;
+    int ret = 0;
+
+    for (i = 0; i < sizeof(pdata); ++i)
+        pdata[i] = (unsigned char)(0x40 + i);
+
+    /* a FIN below data which is already buffered in a higher range */
+    if (!TEST_ptr(pkt_a = pkt_test_new(1200))
+        || !TEST_ptr(pkt_b = 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_a, 0,
+            pdata, 10, 0))
+        || !TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_b, 100,
+            pdata, 10, 0)))
+        goto err;
+
+    if (!TEST_false(ossl_quic_rstream_queue_data(rstream, NULL, 50, NULL,
+            0, 1))
+        || !TEST_int_eq(ch->protocol_error, 1))
+        goto err;
+
+    ossl_quic_rstream_free(rstream);
+    rstream = NULL;
+    ossl_quic_rstream_qparm_destroy(rsqp);
+    rsqp = NULL;
+    ossl_quic_channel_free(ch);
+    ch = NULL;
+
+    /* a FIN below the offset the application has consumed already */
+    if (!TEST_ptr(ch = OPENSSL_zalloc(sizeof(QUIC_CHANNEL)))
+        || !TEST_ptr(rsqp = ossl_quic_rstream_qparm_new(ch))
+        || !TEST_ptr(rstream = ossl_quic_rstream_new(NULL, NULL, rsqp)))
+        goto err;
+
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkt_a, 0,
+            pdata, 10, 0))
+        || !TEST_true(ossl_quic_rstream_read(rstream, buf, sizeof(buf),
+            &readbytes, &fin))
+        || !TEST_size_t_eq(readbytes, 10))
+        goto err;
+
+    if (!TEST_false(ossl_quic_rstream_queue_data(rstream, NULL, 3, NULL,
+            0, 1))
+        || !TEST_int_eq(ch->protocol_error, 1))
+        goto err;
+
+    ret = 1;
+
+err:
+    ossl_quic_rstream_free(rstream);
+    ossl_quic_rstream_qparm_destroy(rsqp);
+    pkt_test_free(pkt_a);
+    pkt_test_free(pkt_b);
+    ossl_quic_channel_free(ch);
+    return ret;
+}
+
 #define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \
                      "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

@@ -2412,6 +2564,8 @@ int setup_tests(void)
     ADD_TEST(test_rstream_pkt_overhead);
     ADD_ALL_TESTS(test_rstream_reorder, 40);
     ADD_TEST(test_rstream_dstorage_two_sided_overlap);
+    ADD_TEST(test_rstream_zero_length_read);
+    ADD_TEST(test_rstream_fin_final_size);
     ADD_TEST(test_rstream_chunk_partial_overlap);
     ADD_TEST(test_rstream_chunk_full_overlap);
     ADD_TEST(test_rstream_range_overlap);