Commit ea6c7f2c6d for openssl.org

commit ea6c7f2c6d05237c75fedcf6e03773f0afb36750
Author: Jakub Zelenka <jakub.zelenka@openssl.foundation>
Date:   Tue Sep 8 11:56:39 2026 +0200

    test: cover cleansing of dropped duplicate bytes

    With cleansing enabled every dropped duplicate byte of an incoming
    frame must be wiped in its packet buffer: a retransmit reaching below
    the consumed offset, a retransmit consumed entirely, a duplicate
    contained in an existing range and the tail trimmed away when a short
    frame is prepended to a direct storage chunk. Otherwise the plaintext
    stays in the recycled packet buffer which nothing else cleanses.

    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 3c4ffba17f..da156f2b72 100644
--- a/test/quic_stream_test.c
+++ b/test/quic_stream_test.c
@@ -1257,6 +1257,125 @@ err:
     return ret;
 }

+static int cleanse_buf_check(const unsigned char *buf, size_t size,
+    size_t data_start, size_t data_end)
+{
+    size_t i;
+
+    for (i = 0; i < size; ++i)
+        if (!TEST_uchar_eq(buf[i],
+                i >= data_start && i < data_end ? 0x00 : 0xAA)) {
+            TEST_info("byte %zu", i);
+            return 0;
+        }
+
+    return 1;
+}
+
+/*
+ * With cleansing enabled, every dropped duplicate byte of an incoming
+ * frame must be wiped in its packet buffer: a retransmit reaching below
+ * the consumed offset, a retransmit consumed entirely, a duplicate
+ * contained in an existing range and the tail trimmed away when a short
+ * frame is prepended to a direct storage chunk. Otherwise the plaintext
+ * stays in the recycled packet buffer which nothing else cleanses.
+ */
+static int test_rstream_cleanse_dropped_bytes(void)
+{
+    QUIC_RSTREAM *rstream = NULL;
+    QUIC_CHANNEL *ch = NULL;
+    QUIC_RSTREAM_QPARM *rsqp = NULL;
+    OSSL_QRX_PKT *pkts[60] = { NULL };
+    unsigned char src_a[16], src_b[16], src_c[16], src_d[16], src_e[16];
+    unsigned char buf[16], fill = 0xFF;
+    size_t num_pkts = 0, readbytes = 0, i;
+    int fin = 0;
+    int ret = 0;
+
+    memset(src_a, 0xAA, sizeof(src_a));
+    memset(src_b, 0xAA, sizeof(src_b));
+    memset(src_c, 0xAA, sizeof(src_c));
+    memset(src_d, 0xAA, sizeof(src_d));
+    memset(src_e, 0xAA, sizeof(src_e));
+    for (i = 0; i < 10; ++i)
+        src_a[3 + i] = (unsigned char)(0x40 + i);
+
+    for (i = 0; i < OSSL_NELEM(pkts); ++i)
+        if (!TEST_ptr(pkts[num_pkts++] = pkt_test_new(1200)))
+            goto err;
+
+    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;
+    ossl_quic_rstream_set_cleanse(rstream, 1);
+
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[0], 0,
+            src_a + 3, 10, 0))
+        || !TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes,
+            &fin))
+        || !TEST_size_t_eq(readbytes, 5))
+        goto err;
+
+    /* a retransmit reaching below the consumed offset is wiped whole */
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[1], 0,
+            src_b + 3, 10, 0))
+        || !TEST_size_t_eq(pkt_test_refcount(pkts[1]), 1)
+        || !cleanse_buf_check(src_b, sizeof(src_b), 3, 13))
+        goto err;
+
+    if (!TEST_true(ossl_quic_rstream_read(rstream, buf, 5, &readbytes, &fin))
+        || !TEST_size_t_eq(readbytes, 5))
+        goto err;
+
+    /* a fully consumed retransmit is wiped whole */
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[2], 0,
+            src_c + 3, 10, 0))
+        || !TEST_size_t_eq(pkt_test_refcount(pkts[2]), 1)
+        || !cleanse_buf_check(src_c, sizeof(src_c), 3, 13))
+        goto err;
+
+    /*
+     * disjoint 1-byte frames on 1200 byte datagrams take the stream
+     * past the overhead limit so short chunks use direct storage
+     */
+    for (i = 0; i < 55; ++i)
+        if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[3 + i],
+                100 + 2 * i, &fill, 1, 0)))
+            goto err;
+
+    /* the copied direct storage chunk has its source wiped already */
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[58], 20,
+            src_d + 3, 2, 0))
+        || !TEST_size_t_eq(pkt_test_refcount(pkts[58]), 1)
+        || !cleanse_buf_check(src_d, sizeof(src_d), 3, 5))
+        goto err;
+
+    /*
+     * [18, 21) prepends [18, 20) to the direct storage chunk [20, 22)
+     * and drops the duplicate byte [20, 21), all three source bytes
+     * must be wiped
+     */
+    if (!TEST_true(ossl_quic_rstream_queue_data(rstream, pkts[59], 18,
+            src_e + 3, 3, 0))
+        || !TEST_size_t_eq(pkt_test_refcount(pkts[59]), 1)
+        || !cleanse_buf_check(src_e, sizeof(src_e), 3, 6))
+        goto err;
+
+    if (!TEST_int_eq(ch->protocol_error, 0))
+        goto err;
+
+    ret = 1;
+
+err:
+    ossl_quic_rstream_free(rstream);
+    ossl_quic_rstream_qparm_destroy(rsqp);
+    for (i = 0; i < num_pkts; ++i)
+        pkt_test_free(pkts[i]);
+    ossl_quic_channel_free(ch);
+    return ret;
+}
+
 #define FILL_PATTERN "abcdefghijklmnopqrstuvwxyz0123456789" \
                      "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"

@@ -2566,6 +2685,7 @@ int setup_tests(void)
     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_cleanse_dropped_bytes);
     ADD_TEST(test_rstream_chunk_partial_overlap);
     ADD_TEST(test_rstream_chunk_full_overlap);
     ADD_TEST(test_rstream_range_overlap);