Commit 77511e1baa for openssl.org

commit 77511e1baa038c2faf0c10d930c08ea87305dbec
Author: Andrew Dinh <andrewd@openssl.org>
Date:   Wed Jul 8 11:10:18 2026 +0700

    Port script_20

    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:39 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 a921f13610..8e74e83a70 100644
--- a/test/quic_multistream_test.c
+++ b/test/quic_multistream_test.c
@@ -2068,98 +2068,8 @@ static const struct script_op script_19[] = {
 };

 /* 20. Multiple threads accept stream with socket forcibly closed (error test) */
-static int script_20_trigger(struct helper *h, volatile uint64_t *counter)
-{
-#if defined(OPENSSL_THREADS)
-    ossl_crypto_mutex_lock(h->misc_m);
-    ++*counter;
-    ossl_crypto_condvar_broadcast(h->misc_cv);
-    ossl_crypto_mutex_unlock(h->misc_m);
-#endif
-    return 1;
-}
-
-static int script_20_wait(struct helper *h, volatile uint64_t *counter, uint64_t threshold)
-{
-#if defined(OPENSSL_THREADS)
-    int stop = 0;
-
-    ossl_crypto_mutex_lock(h->misc_m);
-    while (!stop) {
-        stop = (*counter >= threshold);
-        if (stop)
-            break;
-
-        ossl_crypto_condvar_wait(h->misc_cv, h->misc_m);
-    }
-
-    ossl_crypto_mutex_unlock(h->misc_m);
-#endif
-    return 1;
-}
-
-static int script_20_trigger1(struct helper *h, struct helper_local *hl)
-{
-    return script_20_trigger(h, &h->scratch0);
-}
-
-static int script_20_wait1(struct helper *h, struct helper_local *hl)
-{
-    return script_20_wait(h, &h->scratch0, hl->check_op->arg2);
-}
-
-static int script_20_trigger2(struct helper *h, struct helper_local *hl)
-{
-    return script_20_trigger(h, &h->scratch1);
-}
-
-static int script_20_wait2(struct helper *h, struct helper_local *hl)
-{
-    return script_20_wait(h, &h->scratch1, hl->check_op->arg2);
-}
-
-static const struct script_op script_20_child[] = {
-    OP_C_ACCEPT_STREAM_WAIT(a),
-    OP_C_READ_EXPECT(a, "foo", 3),
-
-    OP_CHECK(script_20_trigger1, 0),
-    OP_CHECK(script_20_wait2, 1),
-
-    OP_C_READ_FAIL_WAIT(a),
-    OP_C_EXPECT_SSL_ERR(a, SSL_ERROR_SYSCALL),
-
-    OP_EXPECT_ERR_LIB(ERR_LIB_SSL),
-    OP_EXPECT_ERR_REASON(SSL_R_PROTOCOL_IS_SHUTDOWN),
-
-    OP_POP_ERR(),
-    OP_EXPECT_ERR_LIB(ERR_LIB_SSL),
-    OP_EXPECT_ERR_REASON(SSL_R_QUIC_NETWORK_ERROR),
-
-    OP_C_FREE_STREAM(a),
-
-    OP_END
-};
-
 static const struct script_op script_20[] = {
-    OP_C_SET_ALPN("ossltest"),
-    OP_C_CONNECT_WAIT(),
-    OP_C_SET_DEFAULT_STREAM_MODE(SSL_DEFAULT_STREAM_MODE_NONE),
-
-    OP_NEW_THREAD(5, script_20_child),
-
-    OP_BEGIN_REPEAT(5),
-
-    OP_S_NEW_STREAM_BIDI(a, ANY_ID),
-    OP_S_WRITE(a, "foo", 3),
-    OP_S_UNBIND_STREAM_ID(a),
-
-    OP_END_REPEAT(),
-
-    OP_CHECK(script_20_wait1, 5),
-
-    OP_C_CLOSE_SOCKET(),
-    OP_CHECK(script_20_trigger2, 0),
-
+    /* test moved to test/radix/quic_tests.c */
     OP_END
 };

diff --git a/test/radix/quic_bindings.c b/test/radix/quic_bindings.c
index 63bcecad99..fb87c2245d 100644
--- a/test/radix/quic_bindings.c
+++ b/test/radix/quic_bindings.c
@@ -68,6 +68,7 @@ typedef struct radix_process_st {
     CRYPTO_MUTEX *gm; /* global mutex */
     LHASH_OF(RADIX_OBJ) *objs; /* protected by gm */
     BIO *keylog_out; /* protected by gm */
+    uint64_t counter[2]; /* protected by gm */

     CRYPTO_MUTEX *time_m;
     OSSL_TIME base_time; /* set once at init, constant thereafter */
@@ -107,7 +108,7 @@ typedef struct radix_thread_st {

 DEFINE_STACK_OF(RADIX_THREAD)

-/* ssl reference is transferred. name is copied and is required. */
+/* name is copied and is required. Created with no SSL object bound yet. */
 static RADIX_OBJ *RADIX_OBJ_new_empty(const char *name)
 {
     RADIX_OBJ *obj;
@@ -135,6 +136,7 @@ static RADIX_OBJ *RADIX_OBJ_new_empty(const char *name)
     return obj;
 }

+/* ssl reference is transferred. name is copied and is required. */
 static RADIX_OBJ *RADIX_OBJ_new(const char *name, SSL *ssl)
 {
     RADIX_OBJ *obj;
@@ -142,8 +144,7 @@ static RADIX_OBJ *RADIX_OBJ_new(const char *name, SSL *ssl)
     if (!TEST_ptr(ssl))
         return NULL;

-    obj = RADIX_OBJ_new_empty(name);
-    if (!TEST_ptr(obj))
+    if (!TEST_ptr(obj = RADIX_OBJ_new_empty(name)))
         return NULL;

     obj->ssl = ssl;
@@ -712,7 +713,7 @@ static void radix_skip_time(OSSL_TIME t)
 static void per_op_tick_obj(RADIX_OBJ *obj)
 {
     ossl_crypto_mutex_lock(obj->mx);
-    if (obj->active && obj->ssl)
+    if (obj->active && obj->ssl != NULL)
         SSL_handle_events(obj->ssl);
     ossl_crypto_mutex_unlock(obj->mx);
 }
diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c
index ed129d0ff7..6d7daa0148 100644
--- a/test/radix/quic_ops.c
+++ b/test/radix/quic_ops.c
@@ -29,20 +29,21 @@ err:

 DEF_FUNC(hf_bind)
 {
+    int ok = 0;
     const char *name;
     RADIX_OBJ *empty_obj;

     F_POP(name);

-    empty_obj = RADIX_OBJ_new_empty(name);
-    if (empty_obj == NULL)
-        return 0;
-
-    RADIX_PROCESS_set_obj(RP(), name, empty_obj);
+    if (!TEST_ptr(empty_obj = RADIX_OBJ_new_empty(name))
+        || !TEST_true(RADIX_PROCESS_set_obj(RP(), name, empty_obj))) {
+        RADIX_OBJ_free(empty_obj);
+        goto err;
+    }

-    return 1;
+    ok = 1;
 err:
-    return 0;
+    return ok;
 }

 static int ssl_ctx_select_alpn(SSL *ssl,
@@ -292,8 +293,9 @@ DEF_FUNC(hf_new_stream)
     if (replace == 0) {
         if (!TEST_ptr_null(stream_obj))
             goto err;
-    } else if (TEST_ptr_null(stream_obj))
+    } else if (TEST_ptr_null(stream_obj)) {
         goto err;
+    }

     if (do_accept) {
         stream = SSL_accept_stream(conn, flags & OP_F_MASK);
@@ -1049,6 +1051,62 @@ err:
     return ok;
 }

+DEF_FUNC(hf_close_socket)
+{
+    int ok = 0;
+    SSL *ssl;
+    BIO *bio;
+    int fd = -1;
+
+    REQUIRE_SSL(ssl);
+
+    bio = SSL_get_rbio(ssl);
+    if (!TEST_ptr(bio) || !TEST_true(BIO_get_fd(bio, &fd)) || !TEST_int_ge(fd, 0))
+        goto err;
+
+    BIO_closesocket(fd);
+
+    ok = 1;
+err:
+    return ok;
+}
+
+/* Process-global counters (RP()->counter[idx]), used for cross-thread sync. */
+DEF_FUNC(hf_trigger_counter)
+{
+    int ok = 0;
+    uint64_t idx;
+
+    F_POP(idx);
+
+    ossl_crypto_mutex_lock(RP()->gm);
+    ++RP()->counter[idx];
+    ossl_crypto_mutex_unlock(RP()->gm);
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_wait_counter)
+{
+    int ok = 0;
+    uint64_t idx, threshold, current;
+
+    F_POP2(idx, threshold);
+
+    ossl_crypto_mutex_lock(RP()->gm);
+    current = RP()->counter[idx];
+    ossl_crypto_mutex_unlock(RP()->gm);
+
+    if (current < threshold)
+        F_SPIN_AGAIN();
+
+    ok = 1;
+err:
+    return ok;
+}
+
 #define OP_UNBIND(name) \
     (OP_PUSH_PZ(#name), \
         OP_FUNC(hf_unbind))
@@ -1295,3 +1353,16 @@ err:
     (OP_SELECT_SSL(0, name),                     \
         OP_PUSH_U64(update_type),                \
         OP_FUNC(hf_trigger_key_update))
+
+#define OP_CLOSE_SOCKET(name) \
+    (OP_SELECT_SSL(0, name),  \
+        OP_FUNC(hf_close_socket))
+
+#define OP_TRIGGER_COUNTER(idx) \
+    (OP_PUSH_U64(idx),          \
+        OP_FUNC(hf_trigger_counter))
+
+#define OP_WAIT_COUNTER(idx, threshold) \
+    (OP_PUSH_U64(idx),                  \
+        OP_PUSH_U64(threshold),         \
+        OP_FUNC(hf_wait_counter))
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index f1abcce118..40d3290adc 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -1630,8 +1630,119 @@ DEF_SCRIPT(script_19, "Key update test - artificially triggered")
     OP_CHECK_KEY_UPDATE_GE(C, 1);
 }

-DEF_SCRIPT(script_20, "place holder for multistrem script_20")
+/* 20. Multiple threads accept stream with socket forcibly closed (error test) */
+DEF_SCRIPT(script_20_child_0,
+    "child: accept stream, read, signal ready, wait, expect read failure")
 {
+    OP_ACCEPT_STREAM_WAIT(C, Ca, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_READ_EXPECT_B(Ca, "foo");
+
+    OP_TRIGGER_COUNTER(0);
+    OP_WAIT_COUNTER(1, 1);
+
+    OP_READ_FAIL_WAIT(Ca);
+    OP_EXPECT_SSL_ERR(Ca, SSL_ERROR_SYSCALL);
+}
+
+DEF_SCRIPT(script_20_child_1,
+    "child: accept stream, read, signal ready, wait, expect read failure")
+{
+    OP_ACCEPT_STREAM_WAIT(C, Cb, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_READ_EXPECT_B(Cb, "foo");
+
+    OP_TRIGGER_COUNTER(0);
+    OP_WAIT_COUNTER(1, 1);
+
+    OP_READ_FAIL_WAIT(Cb);
+    OP_EXPECT_SSL_ERR(Cb, SSL_ERROR_SYSCALL);
+}
+
+DEF_SCRIPT(script_20_child_2,
+    "child: accept stream, read, signal ready, wait, expect read failure")
+{
+    OP_ACCEPT_STREAM_WAIT(C, Cc, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_READ_EXPECT_B(Cc, "foo");
+
+    OP_TRIGGER_COUNTER(0);
+    OP_WAIT_COUNTER(1, 1);
+
+    OP_READ_FAIL_WAIT(Cc);
+    OP_EXPECT_SSL_ERR(Cc, SSL_ERROR_SYSCALL);
+}
+
+DEF_SCRIPT(script_20_child_3,
+    "child: accept stream, read, signal ready, wait, expect read failure")
+{
+    OP_ACCEPT_STREAM_WAIT(C, Cd, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_READ_EXPECT_B(Cd, "foo");
+
+    OP_TRIGGER_COUNTER(0);
+    OP_WAIT_COUNTER(1, 1);
+
+    OP_READ_FAIL_WAIT(Cd);
+    OP_EXPECT_SSL_ERR(Cd, SSL_ERROR_SYSCALL);
+}
+
+DEF_SCRIPT(script_20_child_4,
+    "child: accept stream, read, signal ready, wait, expect read failure")
+{
+    OP_ACCEPT_STREAM_WAIT(C, Ce, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_READ_EXPECT_B(Ce, "foo");
+
+    OP_TRIGGER_COUNTER(0);
+    OP_WAIT_COUNTER(1, 1);
+
+    OP_READ_FAIL_WAIT(Ce);
+    OP_EXPECT_SSL_ERR(Ce, SSL_ERROR_SYSCALL);
+}
+
+DEF_SCRIPT(script_20, "Multiple threads accept stream with socket forcibly closed (error test)")
+{
+    OP_SIMPLE_PAIR_CONN_ND();
+    OP_ACCEPT_CONN_WAIT_ND(L, S, 0);
+
+    OP_BIND(Ca);
+    OP_BIND(Cb);
+    OP_BIND(Cc);
+    OP_BIND(Cd);
+    OP_BIND(Ce);
+    OP_BIND(Sa);
+    OP_BIND(Sb);
+    OP_BIND(Sc);
+    OP_BIND(Sd);
+    OP_BIND(Se);
+
+    OP_SPAWN_THREAD(script_20_child_0);
+    OP_SPAWN_THREAD(script_20_child_1);
+    OP_SPAWN_THREAD(script_20_child_2);
+    OP_SPAWN_THREAD(script_20_child_3);
+    OP_SPAWN_THREAD(script_20_child_4);
+
+    OP_NEW_STREAM(S, Sa, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_WRITE_B(Sa, "foo");
+    OP_CONCLUDE(Sa);
+
+    OP_NEW_STREAM(S, Sb, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_WRITE_B(Sb, "foo");
+    OP_CONCLUDE(Sb);
+
+    OP_NEW_STREAM(S, Sc, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_WRITE_B(Sc, "foo");
+    OP_CONCLUDE(Sc);
+
+    OP_NEW_STREAM(S, Sd, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_WRITE_B(Sd, "foo");
+    OP_CONCLUDE(Sd);
+
+    OP_NEW_STREAM(S, Se, OP_F_REPLACE_STREAM /* bidirectional */);
+    OP_WRITE_B(Se, "foo");
+    OP_CONCLUDE(Se);
+
+    OP_WAIT_COUNTER(0, 5);
+
+    OP_CLOSE_SOCKET(C);
+
+    OP_TRIGGER_COUNTER(1);
 }

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