Commit af3173bd11 for openssl.org

commit af3173bd11a83a18bc32ffe11f809c5d4a34336a
Author: Andrew Dinh <andrewd@openssl.org>
Date:   Mon Aug 3 12:38:10 2026 +0700

    Port script_54

    Assisted-by: Claude:claude-sonnet-5
    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Reviewed-by: Norbert Pocs <norbertp@openssl.org>
    Merge-date: Tue Sep 15 08:52:57 2026
    Merged-from: https://github.com/openssl/openssl/pull/32469

diff --git a/test/quic_multistream_test.c b/test/quic_multistream_test.c
index f407bf669b..20d4554970 100644
--- a/test/quic_multistream_test.c
+++ b/test/quic_multistream_test.c
@@ -2583,24 +2583,8 @@ static const struct script_op script_53[] = {
 };

 /* 54. Fault injection - corrupted crypto stream data */
-static int script_54_inject_handshake(struct helper *h,
-    unsigned char *buf, size_t buf_len)
-{
-    size_t i;
-
-    for (i = 0; i < buf_len; ++i)
-        buf[i] ^= 0xff;
-
-    return 1;
-}
-
 static const struct script_op script_54[] = {
-    OP_S_SET_INJECT_HANDSHAKE(script_54_inject_handshake),
-    OP_C_SET_ALPN("ossltest"),
-    OP_C_CONNECT_WAIT_OR_FAIL(),
-
-    OP_C_EXPECT_CONN_CLOSE_INFO(OSSL_QUIC_ERR_CRYPTO_UNEXPECTED_MESSAGE, 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 e5f314c1b3..b4bd59e9ec 100644
--- a/test/radix/quic_ops.c
+++ b/test/radix/quic_ops.c
@@ -7,6 +7,7 @@
  * https://www.openssl.org/source/license.html
  */
 #include "internal/sockets.h"
+#include "internal/statem.h"
 #include <openssl/rand.h>

 static const unsigned char alpn_ossltest[] = {
@@ -728,11 +729,19 @@ err:
     return ok;
 }

+/*
+ * If tolerate_failure is set, tolerates the connection attempt itself failing
+ * (rather than the connection later closing after a successful handshake),
+ * for scripts where fault injection is expected to prevent the handshake
+ * from completing at all.
+ */
 DEF_FUNC(hf_connect_wait)
 {
     int ok = 0, ret;
     SSL *ssl;
+    uint64_t tolerate_failure;

+    F_POP(tolerate_failure);
     REQUIRE_SSL(ssl);

     /* if not started */
@@ -756,7 +765,7 @@ DEF_FUNC(hf_connect_wait)
         if (is_want(ssl, ret))
             F_SPIN_AGAIN();

-        if (!TEST_int_eq(ret, 1))
+        if (!tolerate_failure && !TEST_int_eq(ret, 1))
             goto err;
     }

@@ -1325,6 +1334,38 @@ static ossl_inline radix_fault_plain_cb radix_fault_ptr_to_plain_cb(void *ptr)
     return u.cb;
 }

+/*
+ * Handshake (crypto stream) message mutator, used to tamper with a QUIC
+ * connection's outgoing TLS handshake messages before they are queued for
+ * transmission.
+ */
+typedef int (*radix_fault_handshake_cb)(RADIX_FAULT *fault,
+    unsigned char *buf, size_t len);
+
+static ossl_inline void *
+radix_fault_handshake_cb_to_ptr(radix_fault_handshake_cb cb)
+{
+    union {
+        radix_fault_handshake_cb cb;
+        void *ptr;
+    } u;
+
+    u.cb = cb;
+    return u.ptr;
+}
+
+static ossl_inline radix_fault_handshake_cb
+radix_fault_ptr_to_handshake_cb(void *ptr)
+{
+    union {
+        radix_fault_handshake_cb cb;
+        void *ptr;
+    } u;
+
+    u.ptr = ptr;
+    return u.cb;
+}
+
 struct radix_fault_st {
     QUIC_PKT_HDR hdr;
     OSSL_QTX_IOVEC io;
@@ -1332,6 +1373,10 @@ struct radix_fault_st {
     radix_fault_plain_cb cb;
     QUIC_CHANNEL *ch;
     uint64_t word0, word1;
+    /* Handshake message mutator state. */
+    unsigned char *handbuf;
+    size_t handbuflen;
+    radix_fault_handshake_cb hcb;
 };

 /* Fault injection against one channel at a time. */
@@ -1488,6 +1533,71 @@ err:
     return ok;
 }

+/*
+ * ossl_statem_mutate_handshake_cb: intercepts an outgoing TLS handshake
+ * message on the crypto stream before it is queued for transmission.
+ */
+static int radix_fault_handshake_mutate(const unsigned char *msgin,
+    size_t msginlen,
+    unsigned char **msgout,
+    size_t *msgoutlen,
+    void *arg)
+{
+    RADIX_FAULT *fault = arg;
+    unsigned char *buf;
+
+    buf = OPENSSL_malloc(msginlen);
+    if (buf == NULL)
+        return 0;
+
+    OPENSSL_free(fault->handbuf);
+    fault->handbuf = buf;
+    fault->handbuflen = msginlen;
+    memcpy(buf, msgin, msginlen);
+
+    if (fault->hcb != NULL && !fault->hcb(fault, buf, fault->handbuflen))
+        return 0;
+
+    *msgout = buf;
+    *msgoutlen = fault->handbuflen;
+
+    return 1;
+}
+
+static void radix_fault_handshake_finish(void *arg)
+{
+    RADIX_FAULT *fault = arg;
+
+    OPENSSL_free(fault->handbuf);
+    fault->handbuf = NULL;
+}
+
+/*
+ * Arms the handshake mutator callback without attaching it to any
+ * connection yet. Used by scripts that need to intercept a server's very
+ * first handshake flight: the corresponding connection does not exist until
+ * a client's Initial packet is accepted, by which point that first flight
+ * has typically already been generated, so the mutator must instead be
+ * installed from a SSL_CTX client_hello callback (see script_54 for an
+ * example) armed with this callback beforehand.
+ */
+DEF_FUNC(hf_set_inject_handshake_cb)
+{
+    int ok = 0;
+    void *cbptr;
+
+    F_POP(cbptr);
+
+    OPENSSL_free(radix_fault.handbuf);
+    radix_fault.handbuf = NULL;
+    radix_fault.handbuflen = 0;
+    radix_fault.hcb = radix_fault_ptr_to_handshake_cb(cbptr);
+
+    ok = 1;
+err:
+    return ok;
+}
+
 DEF_FUNC(hf_push_stream_id_plus_one)
 {
     int ok = 0;
@@ -1557,6 +1667,12 @@ err:

 #define OP_CONNECT_WAIT(name) \
     (OP_SELECT_SSL(0, name),  \
+        OP_PUSH_U64(0),       \
+        OP_FUNC(hf_connect_wait))
+
+#define OP_CONNECT_WAIT_OR_FAIL(name) \
+    (OP_SELECT_SSL(0, name),          \
+        OP_PUSH_U64(1),               \
         OP_FUNC(hf_connect_wait))

 #define OP_LISTEN(name)      \
@@ -1858,6 +1974,10 @@ err:
         OP_PUSH_U64(word1),              \
         OP_FUNC(hf_set_inject_word))

+#define OP_SET_INJECT_HANDSHAKE_CB(cb)               \
+    (OP_PUSH_P(radix_fault_handshake_cb_to_ptr(cb)), \
+        OP_FUNC(hf_set_inject_handshake_cb))
+
 #define OP_PUSH_STREAM_ID_PLUS_ONE(name) \
     (OP_SELECT_SSL(0, name),             \
         OP_FUNC(hf_push_stream_id_plus_one))
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index a8190f813f..bf03a08929 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -3293,8 +3293,83 @@ DEF_SCRIPT(script_53, "Fault injection - excess CRYPTO buffer size")
     OP_EXPECT_CONN_CLOSE_INFO(C, OSSL_QUIC_ERR_CRYPTO_BUFFER_EXCEEDED, 0, 0);
 }

-DEF_SCRIPT(script_54, "place holder for multistrem script_54")
+/* 54. Fault injection - corrupted crypto stream data */
+static int script_54_inject_handshake(RADIX_FAULT *fault, unsigned char *buf,
+    size_t buf_len)
 {
+    size_t i;
+
+    for (i = 0; i < buf_len; ++i)
+        buf[i] ^= 0xff;
+
+    return 1;
+}
+
+/*
+ * The corrupted connection's channel does not exist until the client's
+ * Initial packet is accepted, by which point the server's first handshake
+ * flight has already been generated. So instead of arming the handshake
+ * mutator on an already-accepted connection (too late for this test), a
+ * client_hello callback is used to install it as soon as the ClientHello is
+ * processed, before any response is generated.
+ */
+static int script_54_client_hello_cb(SSL *s, int *al, void *arg)
+{
+    return ossl_statem_set_mutator(s, radix_fault_handshake_mutate,
+        radix_fault_handshake_finish, &radix_fault);
+}
+
+DEF_FUNC(new_listener_54)
+{
+    int ok = 0;
+    SSL_CTX *ctx = NULL;
+    SSL *listener = NULL;
+    const char *name;
+
+    F_POP(name);
+
+    if (!TEST_ptr(ctx = SSL_CTX_new(OSSL_QUIC_server_method())))
+        goto err;
+
+#if defined(OPENSSL_THREADS)
+    if (!TEST_true(SSL_CTX_set_domain_flags(ctx,
+            SSL_DOMAIN_FLAG_MULTI_THREAD
+                | SSL_DOMAIN_FLAG_BLOCKING)))
+        goto err;
+#endif
+
+    if (!TEST_true(ssl_ctx_configure(ctx, 1)))
+        goto err;
+
+    SSL_CTX_set_client_hello_cb(ctx, script_54_client_hello_cb, NULL);
+
+    if (!TEST_ptr(listener = SSL_new_listener(ctx, 0))
+        || !TEST_true(ssl_attach_bio_dgram(listener, 0, NULL))
+        || !TEST_true(RADIX_PROCESS_set_ssl(RP(), name, listener))) {
+        SSL_free(listener);
+        goto err;
+    }
+
+    ok = 1;
+err:
+    /* SSL object will hold ref, we don't need it */
+    SSL_CTX_free(ctx);
+    return ok;
+}
+
+DEF_SCRIPT(script_54, "Fault injection - corrupted crypto stream data")
+{
+    OP_SET_INJECT_HANDSHAKE_CB(script_54_inject_handshake);
+
+    OP_PUSH_PZ("L");
+    OP_FUNC(new_listener_54);
+    OP_LISTEN(L);
+    OP_NEW_SSL_C(C);
+    OP_SET_PEER_ADDR_FROM(C, L);
+
+    OP_CONNECT_WAIT_OR_FAIL(C);
+
+    OP_EXPECT_CONN_CLOSE_INFO(C, OSSL_QUIC_ERR_CRYPTO_UNEXPECTED_MESSAGE, 0, 0);
 }

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