Commit 98a726d3ca for openssl.org

commit 98a726d3ca369960d676e2691ca734448ef1a672
Author: Andrew Dinh <andrewd@openssl.org>
Date:   Fri Jul 3 18:18:17 2026 +0700

    Introduce OP_BIND() to QUIC RADIX test framework

    RADIX framework keeps objects needed by test scripts
    in two places:
      - hash table bound to radix process (`RP()->objs`), all
      objects are stored there
      - slot which is an array bound to radix thread (`RT()->slot[]`)

    The `slot` is an array which is used to pass arguments
    to RADIX ops. The typically script is doing something
    like this:
    ```
       OP_SELECT_SSL(0, C); /* places 'C' object to slot 0 in thread */
       OP_FUNC(print_ssl);  /* calls print_ssl function, which prints object */
    ```
    All objects are managed by RADIX framework, scripts have very
    limited options to control object's lifetime. The only way for
    scripts to let object go is to use `OP_UNBIND()`. The operation
    removes the object from hastable (`RP()->objs`) and frees the
    object afterwards. This is good enough as long a all tests
    are running in single thread. Currently `OP_UNBIND()` is
    required when test needs to accept/create more than one stream.
    The test has two options. It can use unique name for each
    stream it creates/accepts:
    ```
       OP_ACCEPT_STREAM_WAIT(C, C0, 0);
       OP_ACCEPT_STREAM_WAIT(C, C1, 0);
       OP_ACCEPT_STREAM_WAIT(C, C2, 0);
    ```
    Or script may re-use the same variable for stream,
    in that case `OP_UNBIND()` is needed:
    ```
       OP_ACCEPT_STREAM_WAIT(C, C0, 0);
       OP_UNBIND(C0);
       OP_ACCEPT_STREAM_WAIT(C, C0, 0);
       OP_UNBIND(C0);
       OP_ACCEPT_STREAM_WAIT(C, C0, 0);
       OP_UNBIND(C0);
    ```
    Unfortunately `OP_UNBIND()` can not be used when test
    uses more than one thread due to missing locking of `RP()->objs`.

    Introducing a locking scheme seems to be bit invasive change,
    The OP_BIND()  here hopes to be sufficient and good enough for now.

    The idea is as follows:
      - `OP_BIND()` allows  script to insert empty object
      into `RP()->objs` OP_BIND() is supposed to run before
      script spawns thread(s). No manipulation of `RP()->objs`
      is allowed after threads are spawned, operations
      OP_BIND()/OP_UNBIND() are not thread safe.

      - Introduce `OP_F_REPLACE_STREAM` flag which tells
      `OP_ACCEPT_STREAM_WAIT()`/`OP_NEW_STREAM()` to re-use
       existing id for stream. This `_REPLACE_` flag requires
       read-only access to `RP()->objs` hash table.

      - change introduces a per radix object mutex so object can
      be updated safely w.r.t. RADIX thread which ticks SSL object
      bound in radix object.

    The guideline for tests which require more then one thread
    is as follows:
       - the first thread creates complete set of empty objects
       for all threads.

       - each test thread gets its own set of variables, so it
       can populate them later during test with SSL objects

       - objects are not supposed to be shared between threads

    This is a snippet of script executed by main thread before
    additional threads are spawned:

    ```
       ...
       OP_BIND(C1);  /* stream id for child */
       OP_BIND(S1);  /* stream id for parent */

       OP_SPAWN_THREAD(child);
       for (i = 0; i < 10; i++) {
          OP_NEW_STREAM(S, S1, OP_F_REPLACE_STREAM);
          OP_WRITE_B(S1, "foo");
          OP_CONCLUDE(S1);
       }
    ```
    This snippet comes from child:
    ```
       for (i = 0; i < 10; i++) {
          OP_ACCEPT_STREAM_WAIT(C, C1, OP_F_REPLACE_STREAM);
          OP_READ_EXPECT_B(C1, "foo");
          OP_EXPECT_FIN(C1);
       }
    ```
    As you can see parent and child don't use OP_BIND()/OP_UNBIND()
    after child thread is spawned.

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    MergeDate: Fri Jul 17 08:02:04 2026
    (Merged from https://github.com/openssl/openssl/pull/31821)

diff --git a/test/radix/quic_bindings.c b/test/radix/quic_bindings.c
index 9f887d55c6..02356f48eb 100644
--- a/test/radix/quic_bindings.c
+++ b/test/radix/quic_bindings.c
@@ -52,6 +52,7 @@ typedef struct radix_obj_st {
     SSL *ssl; /* owns one reference */
     unsigned int registered : 1; /* in LHASH? */
     unsigned int active : 1; /* tick? */
+    CRYPTO_MUTEX *mx;
 } RADIX_OBJ;

 DEFINE_LHASH_OF_EX(RADIX_OBJ);
@@ -104,11 +105,11 @@ typedef struct radix_thread_st {
 DEFINE_STACK_OF(RADIX_THREAD)

 /* ssl reference is transferred. name is copied and is required. */
-static RADIX_OBJ *RADIX_OBJ_new(const char *name, SSL *ssl)
+static RADIX_OBJ *RADIX_OBJ_new_empty(const char *name)
 {
     RADIX_OBJ *obj;

-    if (!TEST_ptr(name) || !TEST_ptr(ssl))
+    if (!TEST_ptr(name))
         return NULL;

     if (!TEST_ptr(obj = OPENSSL_zalloc(sizeof(*obj))))
@@ -119,7 +120,31 @@ static RADIX_OBJ *RADIX_OBJ_new(const char *name, SSL *ssl)
         return NULL;
     }

+    obj->mx = ossl_crypto_mutex_new();
+#if !defined(OPENSSL_THREADS_NONE)
+    if (obj->mx == NULL) {
+        OPENSSL_free(obj->name);
+        OPENSSL_free(obj);
+        return NULL;
+    }
+#endif
+
+    return obj;
+}
+
+static RADIX_OBJ *RADIX_OBJ_new(const char *name, SSL *ssl)
+{
+    RADIX_OBJ *obj;
+
+    if (!TEST_ptr(ssl))
+        return NULL;
+
+    obj = RADIX_OBJ_new_empty(name);
+    if (!TEST_ptr(obj))
+        return NULL;
+
     obj->ssl = ssl;
+
     return obj;
 }

@@ -132,6 +157,7 @@ static void RADIX_OBJ_free(RADIX_OBJ *obj)

     SSL_free(obj->ssl);
     OPENSSL_free(obj->name);
+    ossl_crypto_mutex_free(&obj->mx);
     OPENSSL_free(obj);
 }

@@ -444,6 +470,9 @@ static int RADIX_PROCESS_set_obj(RADIX_PROCESS *rp,
     const char *name, RADIX_OBJ *obj)
 {
     RADIX_OBJ *existing;
+    SSL *existing_ssl = NULL;
+    RADIX_THREAD *rt;
+    int i, j;

     if (obj != NULL && !TEST_false(obj->registered))
         return 0;
@@ -455,7 +484,10 @@ static int RADIX_PROCESS_set_obj(RADIX_PROCESS *rp,

         lh_RADIX_OBJ_delete(rp->objs, existing);
         existing->registered = 0;
+        existing_ssl = existing->ssl;
         RADIX_OBJ_free(existing);
+    } else {
+        existing = NULL;
     }

     if (obj != NULL) {
@@ -463,6 +495,18 @@ static int RADIX_PROCESS_set_obj(RADIX_PROCESS *rp,
         obj->registered = 1;
     }

+    if (existing != NULL) {
+        for (i = 0; i < sk_RADIX_THREAD_num(rp->threads); i++) {
+            rt = (RADIX_THREAD *)sk_RADIX_THREAD_value(rp->threads, i);
+            for (j = 0; j < NUM_SLOTS; j++) {
+                if (rt->slot[j] == existing)
+                    rt->slot[j] = obj;
+                if (rt->ssl[j] == existing_ssl)
+                    rt->ssl[j] = (obj == NULL) ? NULL : obj->ssl;
+            }
+        }
+    }
+
     return 1;
 }

@@ -650,8 +694,10 @@ ossl_unused static void radix_skip_time(OSSL_TIME t)

 static void per_op_tick_obj(RADIX_OBJ *obj)
 {
-    if (obj->active)
+    ossl_crypto_mutex_lock(obj->mx);
+    if (obj->active && obj->ssl)
         SSL_handle_events(obj->ssl);
+    ossl_crypto_mutex_unlock(obj->mx);
 }

 static int do_per_op(TERP *terp, void *arg)
diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c
index 5d69d88de0..902dab4aed 100644
--- a/test/radix/quic_ops.c
+++ b/test/radix/quic_ops.c
@@ -27,6 +27,24 @@ err:
     return ok;
 }

+DEF_FUNC(hf_bind)
+{
+    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);
+
+    return 1;
+err:
+    return 0;
+}
+
 static int ssl_ctx_select_alpn(SSL *ssl,
     const unsigned char **out, unsigned char *out_len,
     const unsigned char *in, unsigned int in_len,
@@ -248,27 +266,37 @@ err:
     return ok;
 }

+#define OP_F_REPLACE_STREAM 0x8000000000000000
+#define OP_F_MASK 0x7fffffffffffffff
+
 DEF_FUNC(hf_new_stream)
 {
     int ok = 0;
+    int replace;
+    RADIX_OBJ *stream_obj;
     const char *stream_name;
-    SSL *conn, *stream;
+    SSL *conn, *stream, *old;
     uint64_t flags, do_accept;

     F_POP2(flags, do_accept);
     F_POP(stream_name);
     REQUIRE_SSL(conn);
+    replace = ((OP_F_REPLACE_STREAM & flags) != 0);

-    if (!TEST_ptr_null(RADIX_PROCESS_get_obj(RP(), stream_name)))
+    stream_obj = RADIX_PROCESS_get_obj(RP(), stream_name);
+    if (replace == 0) {
+        if (!TEST_ptr_null(stream_obj))
+            goto err;
+    } else if (TEST_ptr_null(stream_obj))
         goto err;

     if (do_accept) {
-        stream = SSL_accept_stream(conn, flags);
+        stream = SSL_accept_stream(conn, flags & OP_F_MASK);

         if (stream == NULL)
             F_SPIN_AGAIN();
     } else {
-        stream = SSL_new_stream(conn, flags);
+        stream = SSL_new_stream(conn, flags & OP_F_MASK);
     }

     if (!TEST_ptr(stream))
@@ -276,8 +304,14 @@ DEF_FUNC(hf_new_stream)

     /* TODO(QUIC RADIX): Implement wait behaviour */

-    if (stream != NULL
-        && !TEST_true(RADIX_PROCESS_set_ssl(RP(), stream_name, stream))) {
+    if (stream_obj != NULL) {
+        ossl_crypto_mutex_lock(stream_obj->mx);
+        old = stream_obj->ssl;
+        stream_obj->ssl = stream;
+        stream = NULL;
+        ossl_crypto_mutex_unlock(stream_obj->mx);
+        SSL_free(old);
+    } else if (!TEST_true(RADIX_PROCESS_set_ssl(RP(), stream_name, stream))) {
         SSL_free(stream);
         goto err;
     }
@@ -928,6 +962,10 @@ err:
     (OP_PUSH_PZ(#name), \
         OP_FUNC(hf_unbind))

+#define OP_BIND(name)   \
+    (OP_PUSH_PZ(#name), \
+        OP_FUNC(hf_bind))
+
 #define OP_SELECT_SSL(slot, name) \
     (OP_PUSH_U64(slot),           \
         OP_PUSH_PZ(#name),        \
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index d09c338505..3047ef08a1 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -918,7 +918,7 @@ DEF_SCRIPT(script_10, "Shutdown test")
 DEF_SCRIPT(script_11_child_0,
     "child: accept stream from C, read, sleep, expect FIN")
 {
-    OP_ACCEPT_STREAM_WAIT(C, C0, 0 /* bidirectional */);
+    OP_ACCEPT_STREAM_WAIT(C, C0, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_READ_EXPECT_B(C0, "foo");
     OP_SLEEP(10);
     OP_EXPECT_FIN(C0);
@@ -927,7 +927,7 @@ DEF_SCRIPT(script_11_child_0,
 DEF_SCRIPT(script_11_child_1,
     "child: accept stream from C, read, sleep, expect FIN")
 {
-    OP_ACCEPT_STREAM_WAIT(C, C1, 0 /* bidirectional */);
+    OP_ACCEPT_STREAM_WAIT(C, C1, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_READ_EXPECT_B(C1, "foo");
     OP_SLEEP(10);
     OP_EXPECT_FIN(C1);
@@ -936,7 +936,7 @@ DEF_SCRIPT(script_11_child_1,
 DEF_SCRIPT(script_11_child_2,
     "child: accept stream from C, read, sleep, expect FIN")
 {
-    OP_ACCEPT_STREAM_WAIT(C, C2, 0 /* bidirectional */);
+    OP_ACCEPT_STREAM_WAIT(C, C2, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_READ_EXPECT_B(C2, "foo");
     OP_SLEEP(10);
     OP_EXPECT_FIN(C2);
@@ -945,7 +945,7 @@ DEF_SCRIPT(script_11_child_2,
 DEF_SCRIPT(script_11_child_3,
     "child: accept stream from C, read, sleep, expect FIN")
 {
-    OP_ACCEPT_STREAM_WAIT(C, C3, 0 /* bidirectional */);
+    OP_ACCEPT_STREAM_WAIT(C, C3, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_READ_EXPECT_B(C3, "foo");
     OP_SLEEP(10);
     OP_EXPECT_FIN(C3);
@@ -954,7 +954,7 @@ DEF_SCRIPT(script_11_child_3,
 DEF_SCRIPT(script_11_child_4,
     "child: accept stream from C, read, sleep, expect FIN")
 {
-    OP_ACCEPT_STREAM_WAIT(C, C4, 0 /* bidirectional */);
+    OP_ACCEPT_STREAM_WAIT(C, C4, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_READ_EXPECT_B(C4, "foo");
     OP_SLEEP(10);
     OP_EXPECT_FIN(C4);
@@ -965,29 +965,40 @@ DEF_SCRIPT(script_11, "Many threads accepted on the same client connection")
     OP_SIMPLE_PAIR_CONN_ND();
     OP_ACCEPT_CONN_WAIT(L, S, 0);

+    OP_BIND(C0);
+    OP_BIND(C1);
+    OP_BIND(C2);
+    OP_BIND(C3);
+    OP_BIND(C4);
+    OP_BIND(Sa);
+    OP_BIND(Sb);
+    OP_BIND(Sc);
+    OP_BIND(Sd);
+    OP_BIND(Se);
+
     OP_SPAWN_THREAD(script_11_child_0);
     OP_SPAWN_THREAD(script_11_child_1);
     OP_SPAWN_THREAD(script_11_child_2);
     OP_SPAWN_THREAD(script_11_child_3);
     OP_SPAWN_THREAD(script_11_child_4);

-    OP_NEW_STREAM(S, Sa, 0 /* bidirectional */);
+    OP_NEW_STREAM(S, Sa, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(Sa, "foo");
     OP_CONCLUDE(Sa);

-    OP_NEW_STREAM(S, Sb, 0 /* bidirectional */);
+    OP_NEW_STREAM(S, Sb, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(Sb, "foo");
     OP_CONCLUDE(Sb);

-    OP_NEW_STREAM(S, Sc, 0 /* bidirectional */);
+    OP_NEW_STREAM(S, Sc, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(Sc, "foo");
     OP_CONCLUDE(Sc);

-    OP_NEW_STREAM(S, Sd, 0 /* bidirectional */);
+    OP_NEW_STREAM(S, Sd, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(Sd, "foo");
     OP_CONCLUDE(Sd);

-    OP_NEW_STREAM(S, Se, 0 /* bidirectional */);
+    OP_NEW_STREAM(S, Se, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(Se, "foo");
     OP_CONCLUDE(Se);
     OP_SLEEP(10);
@@ -997,7 +1008,7 @@ DEF_SCRIPT(script_11, "Many threads accepted on the same client connection")
 DEF_SCRIPT(script_12_child_0,
     "child: create stream on C, write, conclude")
 {
-    OP_NEW_STREAM(C, C0, 0 /* bidirectional */);
+    OP_NEW_STREAM(C, C0, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(C0, "foo");
     OP_CONCLUDE(C0);
 }
@@ -1005,7 +1016,7 @@ DEF_SCRIPT(script_12_child_0,
 DEF_SCRIPT(script_12_child_1,
     "child: create stream on C, write, conclude")
 {
-    OP_NEW_STREAM(C, C1, 0 /* bidirectional */);
+    OP_NEW_STREAM(C, C1, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(C1, "foo");
     OP_CONCLUDE(C1);
 }
@@ -1013,7 +1024,7 @@ DEF_SCRIPT(script_12_child_1,
 DEF_SCRIPT(script_12_child_2,
     "child: create stream on C, write, conclude")
 {
-    OP_NEW_STREAM(C, C2, 0 /* bidirectional */);
+    OP_NEW_STREAM(C, C2, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(C2, "foo");
     OP_CONCLUDE(C2);
 }
@@ -1021,7 +1032,7 @@ DEF_SCRIPT(script_12_child_2,
 DEF_SCRIPT(script_12_child_3,
     "child: create stream on C, write, conclude")
 {
-    OP_NEW_STREAM(C, C3, 0 /* bidirectional */);
+    OP_NEW_STREAM(C, C3, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(C3, "foo");
     OP_CONCLUDE(C3);
 }
@@ -1029,7 +1040,7 @@ DEF_SCRIPT(script_12_child_3,
 DEF_SCRIPT(script_12_child_4,
     "child: create stream on C, write, conclude")
 {
-    OP_NEW_STREAM(C, C4, 0 /* bidirectional */);
+    OP_NEW_STREAM(C, C4, OP_F_REPLACE_STREAM /* bidirectional */);
     OP_WRITE_B(C4, "foo");
     OP_CONCLUDE(C4);
 }
@@ -1039,25 +1050,36 @@ DEF_SCRIPT(script_12, "Many threads initiated on the same client connection")
     OP_SIMPLE_PAIR_CONN_ND();
     OP_ACCEPT_CONN_WAIT_ND(L, S, 0);

+    OP_BIND(C0);
+    OP_BIND(C1);
+    OP_BIND(C2);
+    OP_BIND(C3);
+    OP_BIND(C4);
+    OP_BIND(Sa);
+    OP_BIND(Sb);
+    OP_BIND(Sc);
+    OP_BIND(Sd);
+    OP_BIND(Se);
+
     OP_SPAWN_THREAD(script_12_child_0);
     OP_SPAWN_THREAD(script_12_child_1);
     OP_SPAWN_THREAD(script_12_child_2);
     OP_SPAWN_THREAD(script_12_child_3);
     OP_SPAWN_THREAD(script_12_child_4);

-    OP_ACCEPT_STREAM_WAIT(S, Sa, 0);
+    OP_ACCEPT_STREAM_WAIT(S, Sa, OP_F_REPLACE_STREAM);
     OP_READ_EXPECT_B(Sa, "foo");
     OP_EXPECT_FIN(Sa);
-    OP_ACCEPT_STREAM_WAIT(S, Sb, 0);
+    OP_ACCEPT_STREAM_WAIT(S, Sb, OP_F_REPLACE_STREAM);
     OP_READ_EXPECT_B(Sb, "foo");
     OP_EXPECT_FIN(Sb);
-    OP_ACCEPT_STREAM_WAIT(S, Sc, 0);
+    OP_ACCEPT_STREAM_WAIT(S, Sc, OP_F_REPLACE_STREAM);
     OP_READ_EXPECT_B(Sc, "foo");
     OP_EXPECT_FIN(Sc);
-    OP_ACCEPT_STREAM_WAIT(S, Sd, 0);
+    OP_ACCEPT_STREAM_WAIT(S, Sd, OP_F_REPLACE_STREAM);
     OP_READ_EXPECT_B(Sd, "foo");
     OP_EXPECT_FIN(Sd);
-    OP_ACCEPT_STREAM_WAIT(S, Se, 0);
+    OP_ACCEPT_STREAM_WAIT(S, Se, OP_F_REPLACE_STREAM);
     OP_READ_EXPECT_B(Se, "foo");
     OP_EXPECT_FIN(Se);
     OP_SLEEP(10);