Commit 0584091e76 for openssl.org

commit 0584091e765404434f8c5e449143a870dc026519
Author: Andrew Dinh <andrewd@openssl.org>
Date:   Wed Jul 8 13:30:01 2026 +0700

    Port script_21

    Assisted-by: Claude:claude-sonnet-5

    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Reviewed-by: Bob Beck <beck@openssl.org>
    MergeDate: Thu Jul 30 06:12:40 2026
    (Merged from https://github.com/openssl/openssl/pull/32003)

diff --git a/test/quic_multistream_test.c b/test/quic_multistream_test.c
index 8e74e83a70..ed124a5e46 100644
--- a/test/quic_multistream_test.c
+++ b/test/quic_multistream_test.c
@@ -2196,20 +2196,7 @@ err:
 }

 static const struct script_op script_21[] = {
-    OP_S_SET_INJECT_PLAIN(script_21_inject_plain),
-    OP_C_SET_ALPN("ossltest"),
-    OP_C_CONNECT_WAIT(),
-
-    OP_C_WRITE(DEFAULT, "apple", 5),
-    OP_S_BIND_STREAM_ID(a, C_BIDI_ID(0)),
-    OP_S_READ_EXPECT(a, "apple", 5),
-
-    OP_SET_INJECT_WORD(QUIC_PKT_TYPE_1RTT, OSSL_QUIC_VLINT_MAX),
-
-    OP_S_WRITE(a, "orange", 6),
-
-    OP_C_EXPECT_CONN_CLOSE_INFO(OSSL_QUIC_ERR_FRAME_ENCODING_ERROR, 0, 0),
-
+    /* test moved to test/radix/quic_tests.c */
     OP_END
 };

diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c
index 6d7daa0148..f7f11bd3b9 100644
--- a/test/radix/quic_ops.c
+++ b/test/radix/quic_ops.c
@@ -1107,6 +1107,198 @@ err:
     return ok;
 }

+/*
+ * Fault injection: intercepts a QUIC channel's outgoing packet in plaintext,
+ * before it is encrypted, so a script can tamper with its frames.
+ */
+typedef struct radix_fault_st RADIX_FAULT;
+
+typedef int (*radix_fault_plain_cb)(RADIX_FAULT *fault, QUIC_PKT_HDR *hdr,
+    unsigned char *buf, size_t len);
+
+static ossl_inline void *radix_fault_plain_cb_to_ptr(radix_fault_plain_cb cb)
+{
+    union {
+        radix_fault_plain_cb cb;
+        void *ptr;
+    } u;
+
+    u.cb = cb;
+    return u.ptr;
+}
+
+static ossl_inline radix_fault_plain_cb radix_fault_ptr_to_plain_cb(void *ptr)
+{
+    union {
+        radix_fault_plain_cb cb;
+        void *ptr;
+    } u;
+
+    u.ptr = ptr;
+    return u.cb;
+}
+
+struct radix_fault_st {
+    QUIC_PKT_HDR hdr;
+    OSSL_QTX_IOVEC io;
+    size_t buf_alloc;
+    radix_fault_plain_cb cb;
+    uint64_t word0, word1;
+};
+
+/* Fault injection against one channel at a time. */
+static RADIX_FAULT radix_fault;
+
+static int radix_fault_mutate(const QUIC_PKT_HDR *hdrin,
+    const OSSL_QTX_IOVEC *iovecin, size_t numin,
+    QUIC_PKT_HDR **hdrout,
+    const OSSL_QTX_IOVEC **iovecout,
+    size_t *numout,
+    void *arg)
+{
+    RADIX_FAULT *fault = arg;
+    size_t i, bufsz = 0;
+    unsigned char *cur;
+    int grow_allowance;
+
+    for (i = 0; i < numin; i++)
+        bufsz += iovecin[i].buf_len;
+
+    fault->io.buf_len = bufsz;
+
+    /*
+     * 1200 is the length of the QUIC payload used by the record layer, bufsz is
+     * what we got from the txp, 16 is the AEAD tag length and 14 is the
+     * long header allowance (assume zero token length).
+     */
+    grow_allowance = 1200 - (int)bufsz - 16 - 14;
+    grow_allowance -= hdrin->dst_conn_id.id_len;
+    grow_allowance -= hdrin->src_conn_id.id_len;
+    if (!TEST_int_ge(grow_allowance, 0))
+        return 0;
+    bufsz += grow_allowance;
+
+    OPENSSL_free((unsigned char *)fault->io.buf);
+    fault->io.buf = cur = OPENSSL_malloc(bufsz);
+    if (cur == NULL) {
+        fault->io.buf_len = 0;
+        fault->buf_alloc = 0;
+        return 0;
+    }
+    fault->buf_alloc = bufsz;
+
+    for (i = 0; i < numin; i++) {
+        memcpy(cur, iovecin[i].buf, iovecin[i].buf_len);
+        cur += iovecin[i].buf_len;
+    }
+
+    fault->hdr = *hdrin;
+
+    if (fault->cb != NULL
+        && !fault->cb(fault, &fault->hdr, (unsigned char *)fault->io.buf,
+            fault->io.buf_len))
+        return 0;
+
+    *hdrout = &fault->hdr;
+    *iovecout = &fault->io;
+    *numout = 1;
+
+    return 1;
+}
+
+static void radix_fault_finish(void *arg)
+{
+    RADIX_FAULT *fault = arg;
+
+    OPENSSL_free((unsigned char *)fault->io.buf);
+    fault->io.buf = NULL;
+    fault->io.buf_len = 0;
+    fault->buf_alloc = 0;
+}
+
+/* To be called from a radix_fault_plain_cb callback. */
+static int radix_fault_resize_plain_packet(RADIX_FAULT *fault, size_t newlen)
+{
+    unsigned char *buf;
+    size_t oldlen = fault->io.buf_len;
+
+    if (fault->buf_alloc == 0 || newlen > fault->buf_alloc)
+        return 0;
+
+    buf = (unsigned char *)fault->io.buf;
+
+    if (newlen > oldlen)
+        memset(buf + oldlen, 0, newlen - oldlen);
+
+    fault->io.buf_len = newlen;
+    fault->hdr.len = newlen;
+
+    return 1;
+}
+
+/*
+ * Prepend frame data into a packet. To be called from a
+ * radix_fault_plain_cb callback.
+ */
+static int radix_fault_prepend_frame(RADIX_FAULT *fault,
+    const unsigned char *frame, size_t frame_len)
+{
+    unsigned char *buf;
+    size_t old_len;
+
+    if (fault->buf_alloc == 0)
+        return 0;
+
+    /* Cast below is safe because we allocated the buffer. */
+    buf = (unsigned char *)fault->io.buf;
+    old_len = fault->io.buf_len;
+
+    if (!radix_fault_resize_plain_packet(fault, old_len + frame_len))
+        return 0;
+
+    memmove(buf + frame_len, buf, old_len);
+    memcpy(buf, frame, frame_len);
+
+    return 1;
+}
+
+DEF_FUNC(hf_set_inject_plain)
+{
+    int ok = 0;
+    SSL *ssl;
+    void *cbptr;
+    QUIC_CHANNEL *ch;
+
+    F_POP(cbptr);
+    REQUIRE_SSL(ssl);
+
+    if (!TEST_ptr(ch = ossl_quic_conn_get_channel(ssl)))
+        goto err;
+
+    OPENSSL_free((unsigned char *)radix_fault.io.buf);
+    memset(&radix_fault, 0, sizeof(radix_fault));
+    radix_fault.cb = radix_fault_ptr_to_plain_cb(cbptr);
+
+    if (!TEST_true(ossl_quic_channel_set_mutator(ch, radix_fault_mutate,
+            radix_fault_finish, &radix_fault)))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_set_inject_word)
+{
+    int ok = 0;
+
+    F_POP2(radix_fault.word0, radix_fault.word1);
+
+    ok = 1;
+err:
+    return ok;
+}
+
 #define OP_UNBIND(name) \
     (OP_PUSH_PZ(#name), \
         OP_FUNC(hf_unbind))
@@ -1366,3 +1558,13 @@ err:
     (OP_PUSH_U64(idx),                  \
         OP_PUSH_U64(threshold),         \
         OP_FUNC(hf_wait_counter))
+
+#define OP_SET_INJECT_PLAIN(name, cb)               \
+    (OP_SELECT_SSL(0, name),                        \
+        OP_PUSH_P(radix_fault_plain_cb_to_ptr(cb)), \
+        OP_FUNC(hf_set_inject_plain))
+
+#define OP_SET_INJECT_WORD(word0, word1) \
+    (OP_PUSH_U64(word0),                 \
+        OP_PUSH_U64(word1),              \
+        OP_FUNC(hf_set_inject_word))
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index 40d3290adc..e213bfcad9 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -1745,8 +1745,106 @@ DEF_SCRIPT(script_20, "Multiple threads accept stream with socket forcibly close
     OP_TRIGGER_COUNTER(1);
 }

-DEF_SCRIPT(script_21, "place holder for multistrem script_21")
+/* 21. Fault injection - unknown frame in 1-RTT packet */
+static int script_21_inject_plain(RADIX_FAULT *fault, QUIC_PKT_HDR *hdr,
+    unsigned char *buf, size_t len)
 {
+    int ok = 0;
+    WPACKET wpkt;
+    unsigned char frame_buf[21];
+    size_t written;
+
+    if (fault->word0 == 0 || hdr->type != fault->word0)
+        return 1;
+
+    if (!TEST_true(WPACKET_init_static_len(&wpkt, frame_buf,
+            sizeof(frame_buf), 0)))
+        return 0;
+
+    if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, fault->word1)))
+        goto err;
+
+    switch (fault->word1) {
+    case OSSL_QUIC_FRAME_TYPE_PATH_CHALLENGE:
+    case OSSL_QUIC_FRAME_TYPE_PATH_RESPONSE:
+    case OSSL_QUIC_FRAME_TYPE_RETIRE_CONN_ID:
+        if (!TEST_true(WPACKET_put_bytes_u64(&wpkt, (uint64_t)0)))
+            goto err;
+        break;
+    case OSSL_QUIC_FRAME_TYPE_MAX_DATA:
+    case OSSL_QUIC_FRAME_TYPE_STREAMS_BLOCKED_UNI:
+    case OSSL_QUIC_FRAME_TYPE_STREAMS_BLOCKED_BIDI:
+    case OSSL_QUIC_FRAME_TYPE_MAX_STREAMS_BIDI:
+    case OSSL_QUIC_FRAME_TYPE_MAX_STREAMS_UNI:
+    case OSSL_QUIC_FRAME_TYPE_DATA_BLOCKED:
+        if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0)))
+            goto err;
+        break;
+    case OSSL_QUIC_FRAME_TYPE_STOP_SENDING:
+    case OSSL_QUIC_FRAME_TYPE_MAX_STREAM_DATA:
+    case OSSL_QUIC_FRAME_TYPE_STREAM_DATA_BLOCKED:
+        if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+            || !TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0)))
+            goto err;
+        break;
+    case OSSL_QUIC_FRAME_TYPE_STREAM:
+    case OSSL_QUIC_FRAME_TYPE_RESET_STREAM:
+    case OSSL_QUIC_FRAME_TYPE_CONN_CLOSE_APP:
+        if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+            || !TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+            || !TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0)))
+            goto err;
+        break;
+    case OSSL_QUIC_FRAME_TYPE_NEW_TOKEN:
+        if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)1))
+            || !TEST_true(WPACKET_put_bytes_u8(&wpkt, (uint8_t)0)))
+            goto err;
+        break;
+    case OSSL_QUIC_FRAME_TYPE_NEW_CONN_ID:
+        /* seq number */
+        if (!TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+            /* retire prior to */
+            || !TEST_true(WPACKET_quic_write_vlint(&wpkt, (uint64_t)0))
+            /* Connection id length, arbitrary at 1 bytes */
+            || !TEST_true(WPACKET_put_bytes_u8(&wpkt, (uint8_t)1))
+            /* The connection id */
+            || !TEST_true(WPACKET_put_bytes_u8(&wpkt, (uint8_t)0))
+            /* 16 bytes total for the stateless reset token */
+            || !TEST_true(WPACKET_memset(&wpkt, 0, 16)))
+            goto err;
+
+        break;
+    }
+
+    if (!TEST_true(WPACKET_get_total_written(&wpkt, &written))
+        || !radix_fault_prepend_frame(fault, frame_buf, written))
+        goto err;
+
+    ok = 1;
+err:
+    if (ok)
+        WPACKET_finish(&wpkt);
+    else
+        WPACKET_cleanup(&wpkt);
+    return ok;
+}
+
+DEF_SCRIPT(script_21, "Fault injection - unknown frame in 1-RTT packet")
+{
+    OP_SIMPLE_PAIR_CONN();
+    OP_ACCEPT_CONN_WAIT(L, S, 0);
+
+    OP_SET_INJECT_PLAIN(S, script_21_inject_plain);
+
+    OP_WRITE(C, "apple", 5);
+    OP_ACCEPT_STREAM_WAIT(S, Sa, 0);
+    OP_READ_EXPECT(Sa, "apple", 5);
+
+    OP_SET_INJECT_WORD(QUIC_PKT_TYPE_1RTT, OSSL_QUIC_VLINT_MAX);
+
+    OP_WRITE(Sa, "orange", 6);
+
+    OP_EXPECT_CONN_CLOSE_INFO(C, OSSL_QUIC_ERR_FRAME_ENCODING_ERROR, 0, 0);
 }

 DEF_SCRIPT(script_22, "place holder for multistrem script_22")