]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
Introduce OP_BIND() to QUIC RADIX test framework
authorAndrew Dinh <andrewd@openssl.org>
Fri, 3 Jul 2026 11:18:17 +0000 (18:18 +0700)
committerAlexandr Nedvedicky <sashan@openssl.org>
Fri, 17 Jul 2026 08:01:58 +0000 (10:01 +0200)
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)

test/radix/quic_bindings.c
test/radix/quic_ops.c
test/radix/quic_tests.c

index 9f887d55c6f58e5b65aa5ad255c0e9e14e1a8f66..02356f48eba7c7c8425f084f6c7f1b05653ba841 100644 (file)
@@ -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)
index 5d69d88de0bc1ade41e24886436f80a5029dcf01..902dab4aed9e68b5a35f8484d618635416bf610b 100644 (file)
@@ -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),        \
index d09c3385053edbf6d41f9e7bfeed9d6fcdd5e000..3047ef08a12195f4df17ee0139d71a4df86ebe19 100644 (file)
@@ -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);