summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorHugo Landau <hlandau@openssl.org>2024-02-05 17:48:49 +0000
committerHugo Landau <hlandau@openssl.org>2024-04-19 09:31:06 +0100
commita72832cd604907266f8c036c8685b893d6ec248e (patch)
tree128fd8002c2664fe8f798b08cef2553e05d44012
parentacbae997b5ba9bd65a06cdaa89cdb5dbd9208bac (diff)
QUIC RADIX: Add RADIX test framework implementation
Reviewed-by: Neil Horman <nhorman@openssl.org> Reviewed-by: Tomas Mraz <tomas@openssl.org> (Merged from https://github.com/openssl/openssl/pull/23487)
-rw-r--r--test/radix/main.c61
-rw-r--r--test/radix/quic_bindings.c768
-rw-r--r--test/radix/quic_ops.c1044
-rw-r--r--test/radix/quic_radix.c13
-rw-r--r--test/radix/quic_tests.c33
-rw-r--r--test/radix/terp.c882
6 files changed, 2801 insertions, 0 deletions
diff --git a/test/radix/main.c b/test/radix/main.c
new file mode 100644
index 0000000000..55f2dc000b
--- /dev/null
+++ b/test/radix/main.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+OPT_TEST_DECLARE_USAGE("cert_file key_file\n")
+
+/*
+ * A RADIX test suite binding must define:
+ *
+ * static SCRIPT_INFO *const scripts[];
+ *
+ * int bindings_process_init(size_t node_idx, size_t process_idx);
+ * void bindings_process_finish(int testresult);
+ * int bindings_adjust_terp_config(TERP_CONFIG *cfg);
+ *
+ */
+static int test_script(int idx)
+{
+ SCRIPT_INFO *script_info = scripts[idx];
+ int testresult;
+ TERP_CONFIG cfg = {0};
+
+ if (!TEST_true(bindings_process_init(0, 0)))
+ return 0;
+
+ cfg.debug_bio = bio_err;
+
+ if (!TEST_true(bindings_adjust_terp_config(&cfg)))
+ return 0;
+
+ testresult = TERP_run(script_info, &cfg);
+
+ if (!bindings_process_finish(testresult))
+ testresult = 0;
+
+ return testresult;
+}
+
+int setup_tests(void)
+{
+ if (!test_skip_common_options()) {
+ TEST_error("Error parsing test options\n");
+ return 0;
+ }
+
+ cert_file = test_get_argument(0);
+ if (cert_file == NULL)
+ cert_file = "test/certs/servercert.pem";
+
+ key_file = test_get_argument(1);
+ if (key_file == NULL)
+ key_file = "test/certs/serverkey.pem";
+
+ ADD_ALL_TESTS(test_script, OSSL_NELEM(scripts));
+ return 1;
+}
diff --git a/test/radix/quic_bindings.c b/test/radix/quic_bindings.c
new file mode 100644
index 0000000000..d738d8ceaf
--- /dev/null
+++ b/test/radix/quic_bindings.c
@@ -0,0 +1,768 @@
+/*
+ * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+#include <openssl/lhash.h>
+#include <assert.h>
+
+#include "internal/quic_engine.h"
+#include "internal/quic_channel.h"
+#include "internal/quic_ssl.h"
+#include "internal/quic_error.h"
+
+/*
+ * RADIX 6D QUIC Test Framework
+ * =============================================================================
+ *
+ * The radix test framework is a six-dimension script-driven facility to support
+ * execution of
+ *
+ * multi-stream
+ * multi-client
+ * multi-server
+ * multi-thread
+ * multi-process
+ * multi-node
+ *
+ * test vignettes for QUIC. Unlike the older multistream test framework, it does
+ * not assume a single client and a single server. Examples of vignettes
+ * designed to be supported by the radix test framework in future include:
+ *
+ * single client <-> single server
+ * multiple clients <-> single server
+ * single client <-> multiple servers
+ * multiple clients <-> multiple servers
+ *
+ * 'Multi-process' and 'multi-node' means there has been some consideration
+ * given to support of multi-process and multi-node testing in the future,
+ * though this is not currently supported.
+ */
+
+/*
+ * An object is something associated with a name in the process-level state. The
+ * process-level state primarily revolves around a global dictionary of SSL
+ * objects.
+ */
+typedef struct radix_obj_st {
+ char *name; /* owned, zero-terminated */
+ SSL *ssl; /* owns one reference */
+ unsigned int registered : 1; /* in LHASH? */
+ unsigned int active : 1; /* tick? */
+} RADIX_OBJ;
+
+DEFINE_LHASH_OF_EX(RADIX_OBJ);
+
+/* Process-level state (i.e. "globals" in the normal sense of the word) */
+typedef struct radix_process_st {
+ size_t node_idx;
+ size_t process_idx;
+ size_t next_thread_idx;
+ STACK_OF(RADIX_THREAD) *threads;
+
+ /* Process-global state. */
+ CRYPTO_MUTEX *gm; /* global mutex */
+ LHASH_OF(RADIX_OBJ) *objs; /* protected by gm */
+ OSSL_TIME time_slip; /* protected by gm */
+
+ int done_join_all_threads;
+
+ /*
+ * Valid if done_join_all threads. Logical AND of all child worker results.
+ */
+ int thread_composite_testresult;
+} RADIX_PROCESS;
+
+#define NUM_SLOTS 4
+
+/* Thread-level state within a process */
+typedef struct radix_thread_st {
+ RADIX_PROCESS *rp;
+ CRYPTO_THREAD *t;
+ unsigned char *tmp_buf;
+ size_t tmp_buf_offset;
+ size_t thread_idx; /* 0=main thread */
+ RADIX_OBJ *slot[NUM_SLOTS];
+ SSL *ssl[NUM_SLOTS];
+
+ /* child thread spawn arguments */
+ SCRIPT_INFO *child_script_info;
+ BIO *debug_bio;
+
+ /* m protects all of the below values */
+ CRYPTO_MUTEX *m;
+ int done;
+ int testresult; /* valid if done */
+
+ uint64_t scratch0;
+} RADIX_THREAD;
+
+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)
+{
+ RADIX_OBJ *obj;
+
+ if (!TEST_ptr(name) || !TEST_ptr(ssl))
+ return NULL;
+
+ if (!TEST_ptr(obj = OPENSSL_zalloc(sizeof(*obj))))
+ return NULL;
+
+ obj->name = OPENSSL_strdup(name);
+ obj->ssl = ssl;
+ return obj;
+}
+
+static void RADIX_OBJ_free(RADIX_OBJ *obj)
+{
+ if (obj == NULL)
+ return;
+
+ assert(!obj->registered);
+
+ SSL_free(obj->ssl);
+ OPENSSL_free(obj->name);
+ OPENSSL_free(obj);
+}
+
+static unsigned long RADIX_OBJ_hash(const RADIX_OBJ *obj)
+{
+ return OPENSSL_LH_strhash(obj->name);
+}
+
+static int RADIX_OBJ_cmp(const RADIX_OBJ *a, const RADIX_OBJ *b)
+{
+ return strcmp(a->name, b->name);
+}
+
+static int RADIX_PROCESS_init(RADIX_PROCESS *rp, size_t node_idx, size_t process_idx)
+{
+ if (!TEST_ptr(rp->gm = ossl_crypto_mutex_new()))
+ goto err;
+
+ if (!TEST_ptr(rp->objs = lh_RADIX_OBJ_new(RADIX_OBJ_hash, RADIX_OBJ_cmp)))
+ goto err;
+
+ if (!TEST_ptr(rp->threads = sk_RADIX_THREAD_new(NULL)))
+ goto err;
+
+ rp->node_idx = node_idx;
+ rp->process_idx = process_idx;
+ rp->done_join_all_threads = 0;
+ rp->next_thread_idx = 0;
+ return 1;
+
+err:
+ lh_RADIX_OBJ_free(rp->objs);
+ rp->objs = NULL;
+ ossl_crypto_mutex_free(&rp->gm);
+ return 0;
+}
+
+static const char *stream_state_to_str(int state)
+{
+ switch (state) {
+ case SSL_STREAM_STATE_NONE:
+ return "none";
+ case SSL_STREAM_STATE_OK:
+ return "OK";
+ case SSL_STREAM_STATE_WRONG_DIR:
+ return "wrong dir";
+ case SSL_STREAM_STATE_FINISHED:
+ return "finished";
+ case SSL_STREAM_STATE_RESET_LOCAL:
+ return "reset-local";
+ case SSL_STREAM_STATE_RESET_REMOTE:
+ return "reset-remote";
+ case SSL_STREAM_STATE_CONN_CLOSED:
+ return "conn-closed";
+ default:
+ return "?";
+ }
+}
+
+static void report_ssl_state(BIO *bio, const char *pfx, int is_write,
+ int state, uint64_t ec)
+{
+ const char *state_s = stream_state_to_str(state);
+
+ BIO_printf(bio, "%s%-15s%s(%d)", pfx, is_write ? "Write state: " : "Read state: ",
+ state_s, state);
+ if (ec != UINT64_MAX)
+ BIO_printf(bio, ", %llu", (unsigned long long)ec);
+ BIO_printf(bio, "\n");
+}
+
+static void report_ssl(SSL *ssl, BIO *bio, const char *pfx)
+{
+ const char *type = "SSL";
+ int is_quic = SSL_is_quic(ssl), is_conn = 0, is_listener = 0;
+ SSL_CONN_CLOSE_INFO cc_info = {0};
+ const char *e_str, *f_str;
+
+ if (is_quic) {
+ is_conn = SSL_is_connection(ssl);
+ is_listener = SSL_is_listener(ssl);
+
+ if (is_listener)
+ type = "QLSO";
+ else if (is_conn)
+ type = "QCSO";
+ else
+ type = "QSSO";
+ }
+
+ BIO_printf(bio, "%sType: %s\n", pfx, type);
+
+ if (is_quic && is_conn
+ && SSL_get_conn_close_info(ssl, &cc_info, sizeof(cc_info))) {
+
+ e_str = ossl_quic_err_to_string(cc_info.error_code);
+ f_str = ossl_quic_frame_type_to_string(cc_info.frame_type);
+
+ if (e_str == NULL)
+ e_str = "?";
+ if (f_str == NULL)
+ f_str = "?";
+
+ BIO_printf(bio, "%sConnection is closed: %s(%llu)/%s(%llu), "
+ "%s, %s, reason: \"%s\"\n",
+ pfx,
+ e_str,
+ (unsigned long long)cc_info.error_code,
+ f_str,
+ (unsigned long long)cc_info.frame_type,
+ (cc_info.flags & SSL_CONN_CLOSE_FLAG_LOCAL) != 0
+ ? "local" : "remote",
+ (cc_info.flags & SSL_CONN_CLOSE_FLAG_TRANSPORT) != 0
+ ? "transport" : "app",
+ cc_info.reason != NULL ? cc_info.reason : "-");
+ }
+
+ if (is_quic && !is_listener) {
+ uint64_t stream_id = SSL_get_stream_id(ssl), rec, wec;
+ int rstate, wstate;
+
+ if (stream_id != UINT64_MAX)
+ BIO_printf(bio, "%sStream ID: %llu\n", pfx,
+ (unsigned long long)stream_id);
+
+ rstate = SSL_get_stream_read_state(ssl);
+ wstate = SSL_get_stream_write_state(ssl);
+
+ if (SSL_get_stream_read_error_code(ssl, &rec) != 1)
+ rec = UINT64_MAX;
+
+ if (SSL_get_stream_write_error_code(ssl, &wec) != 1)
+ wec = UINT64_MAX;
+
+ report_ssl_state(bio, pfx, 0, rstate, rec);
+ report_ssl_state(bio, pfx, 1, wstate, wec);
+ }
+}
+
+static void report_obj(RADIX_OBJ *obj, void *arg)
+{
+ BIO *bio = arg;
+ SSL *ssl = obj->ssl;
+
+ BIO_printf(bio, " - %-16s @ %p\n", obj->name, (void *)obj->ssl);
+ ERR_set_mark();
+ report_ssl(ssl, bio, " ");
+ ERR_pop_to_mark();
+}
+
+static void RADIX_THREAD_report_state(RADIX_THREAD *rt, BIO *bio)
+{
+ size_t i;
+
+ BIO_printf(bio, " Slots:\n");
+ for (i = 0; i < NUM_SLOTS; ++i)
+ if (rt->slot[i] == NULL)
+ BIO_printf(bio, " %3zu) <NULL>\n", i);
+ else
+ BIO_printf(bio, " %3zu) '%s' (SSL: %p)\n", i,
+ rt->slot[i]->name,
+ (void *)rt->ssl[i]);
+}
+
+static void RADIX_PROCESS_report_state(RADIX_PROCESS *rp, BIO *bio,
+ int verbose)
+{
+ BIO_printf(bio, "Final process state for node %zu, process %zu:\n",
+ rp->node_idx, rp->process_idx);
+
+ BIO_printf(bio, " Threads (incl. main): %zu\n",
+ rp->next_thread_idx);
+ BIO_printf(bio, " Time slip: %zu ms\n",
+ ossl_time2ms(rp->time_slip));
+
+ BIO_printf(bio, " Objects:\n");
+ lh_RADIX_OBJ_doall_arg(rp->objs, report_obj, bio);
+
+ if (verbose)
+ RADIX_THREAD_report_state(sk_RADIX_THREAD_value(rp->threads, 0),
+ bio_err);
+
+ BIO_printf(bio, "\n==========================================="
+ "===========================\n");
+}
+
+static void RADIX_PROCESS_report_thread_results(RADIX_PROCESS *rp, BIO *bio)
+{
+ size_t i;
+ RADIX_THREAD *rt;
+ char *p;
+ long l;
+ char pfx_buf[64];
+ int rt_testresult;
+
+ for (i = 1; i < (size_t)sk_RADIX_THREAD_num(rp->threads); ++i) {
+ rt = sk_RADIX_THREAD_value(rp->threads, i);
+
+ ossl_crypto_mutex_lock(rt->m);
+ assert(rt->done);
+ rt_testresult = rt->testresult;
+ ossl_crypto_mutex_unlock(rt->m);
+
+ BIO_printf(bio, "\n====(n%zu/p%zu/t%zu)============================"
+ "===========================\n"
+ "Result for child thread with index %zu:\n",
+ rp->node_idx, rp->process_idx, rt->thread_idx, rt->thread_idx);
+
+ BIO_snprintf(pfx_buf, sizeof(pfx_buf), "# -T-%2zu:\t# ", rt->thread_idx);
+ BIO_set_prefix(bio_err, pfx_buf);
+
+ l = BIO_get_mem_data(rt->debug_bio, &p);
+ BIO_write(bio, p, l);
+ BIO_printf(bio, "\n");
+ BIO_set_prefix(bio_err, "# ");
+ BIO_printf(bio, "==> Child thread with index %zu exited with %d\n",
+ rt->thread_idx, rt_testresult);
+ if (!rt_testresult)
+ RADIX_THREAD_report_state(rt, bio);
+ }
+
+ BIO_printf(bio, "\n==========================================="
+ "===========================\n");
+}
+
+static int RADIX_THREAD_join(RADIX_THREAD *rt);
+
+static int RADIX_PROCESS_join_all_threads(RADIX_PROCESS *rp, int *testresult)
+{
+ int ok = 1;
+ size_t i;
+ RADIX_THREAD *rt;
+ int composite_testresult = 1;
+
+ if (rp->done_join_all_threads) {
+ *testresult = rp->thread_composite_testresult;
+ return 1;
+ }
+
+ for (i = 1; i < (size_t)sk_RADIX_THREAD_num(rp->threads); ++i) {
+ rt = sk_RADIX_THREAD_value(rp->threads, i);
+
+ BIO_printf(bio_err, "==> Joining thread %zu\n", i);
+
+ if (!TEST_true(RADIX_THREAD_join(rt)))
+ ok = 0;
+
+ if (!rt->testresult)
+ composite_testresult = 0;
+ }
+
+ rp->thread_composite_testresult = composite_testresult;
+ *testresult = composite_testresult;
+ rp->done_join_all_threads = 1;
+
+ RADIX_PROCESS_report_thread_results(rp, bio_err);
+ return ok;
+}
+
+static void cleanup_one(RADIX_OBJ *obj)
+{
+ obj->registered = 0;
+ RADIX_OBJ_free(obj);
+}
+
+static void RADIX_THREAD_free(RADIX_THREAD *rt);
+
+static void RADIX_PROCESS_cleanup(RADIX_PROCESS *rp)
+{
+ size_t i;
+
+ assert(rp->done_join_all_threads);
+
+ for (i = 0; i < (size_t)sk_RADIX_THREAD_num(rp->threads); ++i)
+ RADIX_THREAD_free(sk_RADIX_THREAD_value(rp->threads, i));
+
+ sk_RADIX_THREAD_free(rp->threads);
+ rp->threads = NULL;
+
+ lh_RADIX_OBJ_doall(rp->objs, cleanup_one);
+ lh_RADIX_OBJ_free(rp->objs);
+ rp->objs = NULL;
+
+ ossl_crypto_mutex_free(&rp->gm);
+}
+
+static RADIX_OBJ *RADIX_PROCESS_get_obj(RADIX_PROCESS *rp, const char *name)
+{
+ RADIX_OBJ key;
+
+ key.name = (char *)name;
+ return lh_RADIX_OBJ_retrieve(rp->objs, &key);
+}
+
+static int RADIX_PROCESS_set_obj(RADIX_PROCESS *rp,
+ const char *name, RADIX_OBJ *obj)
+{
+ RADIX_OBJ *existing;
+
+ if (obj != NULL && !TEST_false(obj->registered))
+ return 0;
+
+ existing = RADIX_PROCESS_get_obj(rp, name);
+ if (existing != NULL && obj != existing) {
+ if (!TEST_true(existing->registered))
+ return 0;
+
+ lh_RADIX_OBJ_delete(rp->objs, existing);
+ existing->registered = 0;
+ RADIX_OBJ_free(existing);
+ }
+
+ if (obj != NULL) {
+ lh_RADIX_OBJ_insert(rp->objs, obj);
+ obj->registered = 1;
+ }
+
+ return 1;
+}
+
+static int RADIX_PROCESS_set_ssl(RADIX_PROCESS *rp, const char *name, SSL *ssl)
+{
+ RADIX_OBJ *obj;
+
+ if (!TEST_ptr(obj = RADIX_OBJ_new(name, ssl)))
+ return 0;
+
+ if (!TEST_true(RADIX_PROCESS_set_obj(rp, name, obj))) {
+ RADIX_OBJ_free(obj);
+ return 0;
+ }
+
+ return 1;
+}
+
+static SSL *RADIX_PROCESS_get_ssl(RADIX_PROCESS *rp, const char *name)
+{
+ RADIX_OBJ *obj = RADIX_PROCESS_get_obj(rp, name);
+
+ if (obj == NULL)
+ return NULL;
+
+ return obj->ssl;
+}
+
+static RADIX_THREAD *RADIX_THREAD_new(RADIX_PROCESS *rp)
+{
+ RADIX_THREAD *rt;
+
+ if (!TEST_ptr(rp)
+ || !TEST_ptr(rt = OPENSSL_zalloc(sizeof(*rt))))
+ return 0;
+
+ rt->rp = rp;
+
+ if (!TEST_ptr(rt->m = ossl_crypto_mutex_new())) {
+ OPENSSL_free(rt);
+ return 0;
+ }
+
+ if (!TEST_true(sk_RADIX_THREAD_push(rp->threads, rt))) {
+ OPENSSL_free(rt);
+ return 0;
+ }
+
+ rt->thread_idx = rp->next_thread_idx++;
+ assert(rt->thread_idx + 1 == (size_t)sk_RADIX_THREAD_num(rp->threads));
+ return rt;
+}
+
+static void RADIX_THREAD_free(RADIX_THREAD *rt)
+{
+ if (rt == NULL)
+ return;
+
+ assert(rt->t == NULL);
+ BIO_free_all(rt->debug_bio);
+ OPENSSL_free(rt->tmp_buf);
+ ossl_crypto_mutex_free(&rt->m);
+ OPENSSL_free(rt);
+}
+
+static int RADIX_THREAD_join(RADIX_THREAD *rt)
+{
+ CRYPTO_THREAD_RETVAL rv;
+
+ if (rt->t != NULL)
+ ossl_crypto_thread_native_join(rt->t, &rv);
+
+ ossl_crypto_thread_native_clean(rt->t);
+ rt->t = NULL;
+
+ if (!TEST_true(rt->done))
+ return 0;
+
+ return 1;
+}
+
+static RADIX_PROCESS radix_process;
+static CRYPTO_THREAD_LOCAL radix_thread;
+
+static void radix_thread_cleanup_tl(void *p)
+{
+ /* Should already have been cleaned up. */
+ if (!TEST_ptr_null(p))
+ abort();
+}
+
+static RADIX_THREAD *radix_get_thread(void)
+{
+ return CRYPTO_THREAD_get_local(&radix_thread);
+}
+
+static int radix_thread_init(RADIX_THREAD *rt)
+{
+ if (!TEST_ptr(rt)
+ || !TEST_ptr_null(CRYPTO_THREAD_get_local(&radix_thread)))
+ return 0;
+
+ if (!TEST_true(CRYPTO_THREAD_set_local(&radix_thread, rt)))
+ return 0;
+
+ set_override_bio_out(rt->debug_bio);
+ set_override_bio_err(rt->debug_bio);
+ return 1;
+}
+
+static void radix_thread_cleanup(void)
+{
+ RADIX_THREAD *rt = radix_get_thread();
+
+ if (!TEST_ptr(rt))
+ return;
+
+ if (!TEST_true(CRYPTO_THREAD_set_local(&radix_thread, NULL)))
+ return;
+}
+
+static int bindings_process_init(size_t node_idx, size_t process_idx)
+{
+ RADIX_THREAD *rt;
+
+ if (!TEST_true(RADIX_PROCESS_init(&radix_process, node_idx, process_idx)))
+ return 0;
+
+ if (!TEST_true(CRYPTO_THREAD_init_local(&radix_thread,
+ radix_thread_cleanup_tl)))
+ return 0;
+
+ if (!TEST_ptr(rt = RADIX_THREAD_new(&radix_process)))
+ return 0;
+
+ /* Allocate structures for main thread. */
+ return radix_thread_init(rt);
+}
+
+static int bindings_process_finish(int testresult_main)
+{
+ int testresult, testresult_child;
+
+ if (!TEST_true(RADIX_PROCESS_join_all_threads(&radix_process,
+ &testresult_child)))
+ return 0;
+
+ testresult = testresult_main && testresult_child;
+ RADIX_PROCESS_report_state(&radix_process, bio_err,
+ /*verbose=*/!testresult);
+ radix_thread_cleanup(); /* cleanup main thread */
+ RADIX_PROCESS_cleanup(&radix_process);
+
+ if (testresult)
+ BIO_printf(bio_err, "==> OK\n\n");
+ else
+ BIO_printf(bio_err, "==> ERROR (main=%d, children=%d)\n\n",
+ testresult_main, testresult_child);
+
+ return testresult;
+}
+
+#define RP() (&radix_process)
+#define RT() (radix_get_thread())
+
+static OSSL_TIME get_time(void *arg)
+{
+ OSSL_TIME time_slip;
+
+ ossl_crypto_mutex_lock(RP()->gm);
+ time_slip = RP()->time_slip;
+ ossl_crypto_mutex_unlock(RP()->gm);
+
+ return ossl_time_add(ossl_time_now(), time_slip);
+}
+
+ossl_unused static void radix_skip_time(OSSL_TIME t)
+{
+ ossl_crypto_mutex_lock(RP()->gm);
+ RP()->time_slip = ossl_time_add(RP()->time_slip, t);
+ ossl_crypto_mutex_unlock(RP()->gm);
+}
+
+static void per_op_tick_obj(RADIX_OBJ *obj)
+{
+ if (obj->active)
+ SSL_handle_events(obj->ssl);
+}
+
+static int do_per_op(TERP *terp, void *arg)
+{
+ lh_RADIX_OBJ_doall(RP()->objs, per_op_tick_obj);
+ return 1;
+}
+
+static int bindings_adjust_terp_config(TERP_CONFIG *cfg)
+{
+ cfg->now_cb = get_time;
+ cfg->per_op_cb = do_per_op;
+ return 1;
+}
+
+static int expect_slot_ssl(FUNC_CTX *fctx, size_t idx, SSL **p_ssl)
+{
+ if (!TEST_size_t_lt(idx, NUM_SLOTS)
+ || !TEST_ptr(*p_ssl = RT()->ssl[idx]))
+ return 0;
+
+ return 1;
+}
+
+#define REQUIRE_SSL_N(idx, ssl) \
+ do { \
+ if (!TEST_true(expect_slot_ssl(fctx, (idx), &(ssl)))) \
+ goto err; \
+ } while (0)
+#define REQUIRE_SSL(ssl) REQUIRE_SSL_N(0, (ssl))
+
+#define C_BIDI_ID(ordinal) \
+ (((ordinal) << 2) | QUIC_STREAM_INITIATOR_CLIENT | QUIC_STREAM_DIR_BIDI)
+#define S_BIDI_ID(ordinal) \
+ (((ordinal) << 2) | QUIC_STREAM_INITIATOR_SERVER | QUIC_STREAM_DIR_BIDI)
+#define C_UNI_ID(ordinal) \
+ (((ordinal) << 2) | QUIC_STREAM_INITIATOR_CLIENT | QUIC_STREAM_DIR_UNI)
+#define S_UNI_ID(ordinal) \
+ (((ordinal) << 2) | QUIC_STREAM_INITIATOR_SERVER | QUIC_STREAM_DIR_UNI)
+
+static int RADIX_THREAD_worker_run(RADIX_THREAD *rt)
+{
+ int ok = 0;
+ TERP_CONFIG cfg = {0};
+
+ cfg.debug_bio = rt->debug_bio;
+ if (!TEST_true(bindings_adjust_terp_config(&cfg)))
+ goto err;
+
+ if (!TERP_run(rt->child_script_info, &cfg))
+ goto err;
+
+ ok = 1;
+err:
+ return ok;
+}
+
+static unsigned int RADIX_THREAD_worker_main(void *p)
+{
+ int testresult = 0;
+ RADIX_THREAD *rt = p;
+
+ if (!TEST_true(radix_thread_init(rt)))
+ return 0;
+
+ /* Wait until thread-specific init is done (e.g. setting rt->t) */
+ ossl_crypto_mutex_lock(rt->m);
+ ossl_crypto_mutex_unlock(rt->m);
+
+ testresult = RADIX_THREAD_worker_run(rt);
+
+ ossl_crypto_mutex_lock(rt->m);
+ rt->testresult = testresult;
+ rt->done = 1;
+ ossl_crypto_mutex_unlock(rt->m);
+
+ radix_thread_cleanup();
+ return 1;
+}
+
+static void radix_activate_obj(RADIX_OBJ *obj)
+{
+ if (obj != NULL)
+ obj->active = 1;
+}
+
+static void radix_activate_slot(size_t idx)
+{
+ if (idx >= NUM_SLOTS)
+ return;
+
+ radix_activate_obj(RT()->slot[idx]);
+}
+
+DEF_FUNC(hf_spawn_thread)
+{
+ int ok = 0;
+ RADIX_THREAD *child_rt = NULL;
+ SCRIPT_INFO *script_info = NULL;
+
+ F_POP(script_info);
+ if (!TEST_ptr(script_info))
+ goto err;
+
+#if !defined(OPENSSL_THREADS)
+ TEST_skip("threading not supported, skipping");
+ F_SKIP_REST();
+#else
+ if (!TEST_ptr(child_rt = RADIX_THREAD_new(&radix_process)))
+ return 0;
+
+ if (!TEST_ptr(child_rt->debug_bio = BIO_new(BIO_s_mem())))
+ goto err;
+
+ ossl_crypto_mutex_lock(child_rt->m);
+
+ child_rt->child_script_info = script_info;
+ if (!TEST_ptr(child_rt->t = ossl_crypto_thread_native_start(RADIX_THREAD_worker_main,
+ child_rt, 1))) {
+ ossl_crypto_mutex_unlock(child_rt->m);
+ goto err;
+ }
+
+ ossl_crypto_mutex_unlock(child_rt->m);
+ ok = 1;
+#endif
+err:
+ if (!ok)
+ RADIX_THREAD_free(child_rt);
+
+ return ok;
+}
+
+#define OP_SPAWN_THREAD(script_name) \
+ (OP_PUSH_P(SCRIPT(script_name)), OP_FUNC(hf_spawn_thread))
diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c
new file mode 100644
index 0000000000..f3dedc13ac
--- /dev/null
+++ b/test/radix/quic_ops.c
@@ -0,0 +1,1044 @@
+#include <netinet/in.h>
+
+static const unsigned char alpn_ossltest[] = {
+ /* "\x08ossltest" (hex for EBCDIC resilience) */
+ 0x08, 0x6f, 0x73, 0x73, 0x6c, 0x74, 0x65, 0x73, 0x74
+};
+
+DEF_FUNC(hf_unbind)
+{
+ int ok = 0;
+ const char *name;
+
+ F_POP(name);
+ RADIX_PROCESS_set_obj(RP(), name, NULL);
+
+ ok = 1;
+err:
+ return ok;
+}
+
+static int ssl_ctx_select_alpn(SSL *ssl,
+ const unsigned char **out, unsigned char *out_len,
+ const unsigned char *in, unsigned int in_len,
+ void *arg)
+{
+ if (SSL_select_next_proto((unsigned char **)out, out_len,
+ alpn_ossltest, sizeof(alpn_ossltest), in, in_len)
+ != OPENSSL_NPN_NEGOTIATED)
+ return SSL_TLSEXT_ERR_ALERT_FATAL;
+
+ return SSL_TLSEXT_ERR_OK;
+}
+
+static int ssl_ctx_configure(SSL_CTX *ctx, int is_server)
+{
+ if (!TEST_true(ossl_quic_set_diag_title(ctx, "quic_radix_test")))
+ return 0;
+
+ if (!is_server)
+ return 1;
+
+ if (!TEST_int_eq(SSL_CTX_use_certificate_file(ctx, cert_file,
+ SSL_FILETYPE_PEM), 1)
+ || !TEST_int_eq(SSL_CTX_use_PrivateKey_file(ctx, key_file,
+ SSL_FILETYPE_PEM), 1))
+ return 0;
+
+ SSL_CTX_set_alpn_select_cb(ctx, ssl_ctx_select_alpn, NULL);
+ return 1;
+}
+
+static int ssl_create_bound_socket(uint16_t listen_port,
+ int *p_fd, uint16_t *p_result_port)
+{
+ int ok = 0;
+ int fd = -1;
+ BIO_ADDR *addr = NULL;
+ union BIO_sock_info_u info;
+ struct in_addr ina = { htonl(INADDR_LOOPBACK) };
+
+ fd = BIO_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP, 0);
+ if (!TEST_int_ge(fd, 0))
+ goto err;
+
+ if (!TEST_true(BIO_socket_nbio(fd, 1)))
+ goto err;
+
+ if (!TEST_ptr(addr = BIO_ADDR_new()))
+ goto err;
+
+ if (!TEST_true(BIO_ADDR_rawmake(addr, AF_INET,
+ &ina, sizeof(ina), 0)))
+ goto err;
+
+ if (!TEST_true(BIO_bind(fd, addr, 0)))
+ goto err;
+
+ info.addr = addr;
+ if (!TEST_true(BIO_sock_info(fd, BIO_SOCK_INFO_ADDRESS, &info)))
+ goto err;
+
+ if (!TEST_int_gt(BIO_ADDR_rawport(addr), 0))
+ goto err;
+
+ ok = 1;
+err:
+ if (!ok && fd >= 0)
+ BIO_closesocket(fd);
+ else if (ok) {
+ *p_fd = fd;
+ if (p_result_port != NULL)
+ *p_result_port = BIO_ADDR_rawport(addr);
+ }
+ BIO_ADDR_free(addr);
+ return ok;
+}
+
+static int ssl_attach_bio_dgram(SSL *ssl,
+ uint16_t local_port, uint16_t *actual_port)
+{
+ int s_fd = -1;
+ BIO *bio;
+
+ if (!TEST_true(ssl_create_bound_socket(local_port, &s_fd, actual_port)))
+ return 0;
+
+ if (!TEST_ptr(bio = BIO_new_dgram(s_fd, BIO_CLOSE))) {
+ BIO_closesocket(s_fd);
+ return 0;
+ }
+
+ SSL_set0_rbio(ssl, bio);
+ if (!TEST_true(BIO_up_ref(bio)))
+ return 0;
+
+ SSL_set0_wbio(ssl, bio);
+
+ return 1;
+}
+
+DEF_FUNC(hf_new_ssl)
+{
+ int ok = 0;
+ const char *name;
+ SSL_CTX *ctx = NULL;
+ const SSL_METHOD *method;
+ SSL *ssl;
+ uint64_t flags;
+ int is_server;
+
+ F_POP2(name, flags);
+
+ is_server = (flags != 0);
+ method = is_server ? OSSL_QUIC_server_method() : OSSL_QUIC_client_method();
+ if (!TEST_ptr(ctx = SSL_CTX_new(method)))
+ goto err;
+
+ if (!TEST_true(ssl_ctx_configure(ctx, is_server)))
+ goto err;
+
+ if (is_server) {
+ if (!TEST_ptr(ssl = SSL_new_listener(ctx, 0)))
+ goto err;
+ } else {
+ if (!TEST_ptr(ssl = SSL_new(ctx)))
+ goto err;
+ }
+
+ if (!TEST_true(ssl_attach_bio_dgram(ssl, 0, NULL)))
+ goto err;
+
+ if (!TEST_true(RADIX_PROCESS_set_ssl(RP(), name, ssl))) {
+ SSL_free(ssl);
+ goto err;
+ }
+
+ ok = 1;
+err:
+ /* SSL object will hold ref, we don't need it */
+ SSL_CTX_free(ctx);
+ return ok;
+}
+
+DEF_FUNC(hf_listen)
+{
+ int ok = 0, r;
+ SSL *ssl;
+
+ REQUIRE_SSL(ssl);
+
+ r = SSL_listen(ssl);
+ if (!TEST_true(r))
+ goto err;
+
+ radix_activate_slot(0);
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_FUNC(hf_new_stream)
+{
+ int ok = 0;
+ const char *conn_name, *stream_name;
+ SSL *conn, *stream;
+ uint64_t flags, do_accept;
+
+ F_POP2(flags, do_accept);
+ F_POP2(conn_name, stream_name);
+
+ if (!TEST_ptr_null(RADIX_PROCESS_get_obj(RP(), stream_name)))
+ goto err;
+
+ if (!TEST_ptr(conn = RADIX_PROCESS_get_ssl(RP(), conn_name)))
+ goto err;
+
+ if (do_accept) {
+ stream = SSL_accept_stream(conn, flags);
+
+ if (stream == NULL)
+ F_SPIN_AGAIN();
+ } else {
+ stream = SSL_new_stream(conn, flags);
+ }
+
+ if (!TEST_ptr(stream))
+ goto err;
+
+ /* TODO(QUIC RADIX): Implement wait behaviour */
+
+ if (stream != NULL
+ && !TEST_true(RADIX_PROCESS_set_ssl(RP(), stream_name, stream))) {
+ SSL_free(stream);
+ goto err;
+ }
+
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_FUNC(hf_accept_conn)
+{
+ int ok = 0;
+ const char *conn_name;
+ uint64_t flags;
+ SSL *listener, *conn;
+
+ F_POP2(conn_name, flags);
+ REQUIRE_SSL(listener);
+
+ if (!TEST_ptr_null(RADIX_PROCESS_get_obj(RP(), conn_name)))
+ goto err;
+
+ conn = SSL_accept_connection(listener, flags);
+ if (conn == NULL)
+ F_SPIN_AGAIN();
+
+ if (!TEST_true(RADIX_PROCESS_set_ssl(RP(), conn_name, conn))) {
+ SSL_free(conn);
+ goto err;
+ }
+
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_FUNC(hf_accept_conn_none)
+{
+ int ok = 0;
+ SSL *listener, *conn;
+
+ REQUIRE_SSL(listener);
+
+ conn = SSL_accept_connection(listener, 0);
+ if (!TEST_ptr_null(conn)) {
+ SSL_free(conn);
+ goto err;
+ }
+
+ ok = 1;
+err:
+ return ok;
+}
+
+DEF_FUNC(hf_accept_stream_none)
+{
+ int ok = 0;
+ const char *conn_name;
+ uint64_t flags;
+ SSL *conn, *stream;
+
+ F_POP2(conn_name, flags);
+
+ if (!TEST_ptr(conn = RADIX_PROCESS_get_ssl(RP(), conn_name)))
+ goto er