]> git.ipfire.org Git - thirdparty/openssl.git/commitdiff
QUIC RADIX: Add RADIX test framework implementation
authorHugo Landau <hlandau@openssl.org>
Mon, 5 Feb 2024 17:48:49 +0000 (17:48 +0000)
committerViktor Dukhovni <openssl-users@dukhovni.org>
Wed, 11 Sep 2024 08:35:22 +0000 (18:35 +1000)
Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
(Merged from https://github.com/openssl/openssl/pull/23487)

test/radix/main.c [new file with mode: 0644]
test/radix/quic_bindings.c [new file with mode: 0644]
test/radix/quic_ops.c [new file with mode: 0644]
test/radix/quic_radix.c [new file with mode: 0644]
test/radix/quic_tests.c [new file with mode: 0644]
test/radix/terp.c [new file with mode: 0644]

diff --git a/test/radix/main.c b/test/radix/main.c
new file mode 100644 (file)
index 0000000..55f2dc0
--- /dev/null
@@ -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 (file)
index 0000000..d738d8c
--- /dev/null
@@ -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 (file)
index 0000000..f3dedc1
--- /dev/null
@@ -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 err;
+
+    stream = SSL_accept_stream(conn, flags);
+    if (!TEST_ptr_null(stream)) {
+        SSL_free(stream);
+        goto err;
+    }
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_pop_err)
+{
+    ERR_pop();
+
+    return 1;
+}
+
+DEF_FUNC(hf_stream_reset)
+{
+    int ok = 0;
+    const char *name;
+    SSL_STREAM_RESET_ARGS args = {0};
+    SSL *ssl;
+
+    F_POP2(name, args.quic_error_code);
+    REQUIRE_SSL(ssl);
+
+    if (!TEST_true(SSL_stream_reset(ssl, &args, sizeof(args))))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_set_default_stream_mode)
+{
+    int ok = 0;
+    uint64_t mode;
+    SSL *ssl;
+
+    F_POP(mode);
+    REQUIRE_SSL(ssl);
+
+    if (!TEST_true(SSL_set_default_stream_mode(ssl, mode)))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_set_incoming_stream_policy)
+{
+    int ok = 0;
+    uint64_t policy, error_code;
+    SSL *ssl;
+
+    F_POP(error_code);
+    F_POP(policy);
+    REQUIRE_SSL(ssl);
+
+    if (!TEST_true(SSL_set_incoming_stream_policy(ssl, policy, error_code)))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_shutdown_wait)
+{
+    int ok = 0, ret;
+    uint64_t flags;
+    SSL *ssl;
+    SSL_SHUTDOWN_EX_ARGS args = {0};
+    QUIC_CHANNEL *ch;
+
+    F_POP(args.quic_reason);
+    F_POP(args.quic_error_code);
+    F_POP(flags);
+    REQUIRE_SSL(ssl);
+
+    ch = ossl_quic_conn_get_channel(ssl);
+    ossl_quic_engine_set_inhibit_tick(ossl_quic_channel_get0_engine(ch), 0);
+
+    ret = SSL_shutdown_ex(ssl, flags, &args, sizeof(args));
+    if (!TEST_int_ge(ret, 0))
+        goto err;
+
+    if (ret == 0)
+        F_SPIN_AGAIN();
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_conclude)
+{
+    int ok = 0;
+    SSL *ssl;
+
+    REQUIRE_SSL(ssl);
+
+    if (!TEST_true(SSL_stream_conclude(ssl, 0)))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+static int is_want(SSL *s, int ret)
+{
+    int ec = SSL_get_error(s, ret);
+
+    return ec == SSL_ERROR_WANT_READ || ec == SSL_ERROR_WANT_WRITE;
+}
+
+static int check_consistent_want(SSL *s, int ret)
+{
+    int ec = SSL_get_error(s, ret);
+    int w = SSL_want(s);
+
+    int ok = TEST_true(
+        (ec == SSL_ERROR_NONE                 && w == SSL_NOTHING)
+    ||  (ec == SSL_ERROR_ZERO_RETURN          && w == SSL_NOTHING)
+    ||  (ec == SSL_ERROR_SSL                  && w == SSL_NOTHING)
+    ||  (ec == SSL_ERROR_SYSCALL              && w == SSL_NOTHING)
+    ||  (ec == SSL_ERROR_WANT_READ            && w == SSL_READING)
+    ||  (ec == SSL_ERROR_WANT_WRITE           && w == SSL_WRITING)
+    ||  (ec == SSL_ERROR_WANT_CLIENT_HELLO_CB && w == SSL_CLIENT_HELLO_CB)
+    ||  (ec == SSL_ERROR_WANT_X509_LOOKUP     && w == SSL_X509_LOOKUP)
+    ||  (ec == SSL_ERROR_WANT_RETRY_VERIFY    && w == SSL_RETRY_VERIFY)
+    );
+
+    if (!ok)
+        TEST_error("got error=%d, want=%d", ec, w);
+
+    return ok;
+}
+
+DEF_FUNC(hf_write)
+{
+    int ok = 0, r;
+    SSL *ssl;
+    const void *buf;
+    size_t buf_len, bytes_written = 0;
+
+    F_POP2(buf, buf_len);
+    REQUIRE_SSL(ssl);
+
+    r = SSL_write_ex(ssl, buf, buf_len, &bytes_written);
+    if (!TEST_true(r)
+        || !check_consistent_want(ssl, r)
+        || !TEST_size_t_eq(bytes_written, buf_len))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_write_ex2)
+{
+    int ok = 0, r;
+    SSL *ssl;
+    const void *buf;
+    size_t buf_len, bytes_written = 0;
+    uint64_t flags;
+
+    F_POP(flags);
+    F_POP2(buf, buf_len);
+    REQUIRE_SSL(ssl);
+
+    r = SSL_write_ex2(ssl, buf, buf_len, flags, &bytes_written);
+    if (!TEST_true(r)
+        || !check_consistent_want(ssl, r)
+        || !TEST_size_t_eq(bytes_written, buf_len))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_write_fail)
+{
+    int ok = 0, ret;
+    SSL *ssl;
+    size_t bytes_written = 0;
+
+    REQUIRE_SSL(ssl);
+
+    ret = SSL_write_ex(ssl, "apple", 5, &bytes_written);
+    if (!TEST_false(ret)
+        || !TEST_true(check_consistent_want(ssl, ret))
+        || !TEST_size_t_eq(bytes_written, 0))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_read_expect)
+{
+    int ok = 0, r;
+    SSL *ssl;
+    const void *buf;
+    uint64_t buf_len, bytes_read = 0;
+
+    F_POP2(buf, buf_len);
+    REQUIRE_SSL(ssl);
+
+    if (buf_len > 0 && RT()->tmp_buf == NULL
+        && !TEST_ptr(RT()->tmp_buf = OPENSSL_malloc(buf_len)))
+        goto err;
+
+    r = SSL_read_ex(ssl, RT()->tmp_buf + RT()->tmp_buf_offset,
+                    buf_len - RT()->tmp_buf_offset,
+                    &bytes_read);
+    if (!TEST_true(check_consistent_want(ssl, r)))
+        goto err;
+
+    if (!r)
+        F_SPIN_AGAIN();
+
+    if (bytes_read + RT()->tmp_buf_offset != buf_len) {
+        RT()->tmp_buf_offset += bytes_read;
+        F_SPIN_AGAIN();
+    }
+
+    if (buf_len > 0
+        && !TEST_mem_eq(RT()->tmp_buf, buf_len, buf, buf_len))
+        goto err;
+
+    OPENSSL_free(RT()->tmp_buf);
+    RT()->tmp_buf         = NULL;
+    RT()->tmp_buf_offset  = 0;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_read_fail)
+{
+    int ok = 0, r;
+    SSL *ssl;
+    char buf[1] = {0};
+    size_t bytes_read = 0;
+    uint64_t do_wait;
+
+    F_POP(do_wait);
+    REQUIRE_SSL(ssl);
+
+    r = SSL_read_ex(ssl, buf, sizeof(buf), &bytes_read);
+    if (!TEST_false(r)
+        || !TEST_true(check_consistent_want(ssl, r))
+        || !TEST_size_t_eq(bytes_read, 0))
+        goto err;
+
+    if (do_wait && is_want(ssl, 0))
+        F_SPIN_AGAIN();
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_connect_wait)
+{
+    int ok = 0, ret;
+    SSL *ssl;
+
+    REQUIRE_SSL(ssl);
+
+    /* if not started */
+    if (RT()->scratch0 == 0) {
+        if (!TEST_true(SSL_set_blocking_mode(ssl, 0)))
+            return 0;
+
+        SSL_CONN_CLOSE_INFO cc_info = {0};
+        if (!TEST_false(SSL_get_conn_close_info(ssl, &cc_info, sizeof(cc_info))))
+            goto err;
+
+        /* 0 is the success case for SSL_set_alpn_protos(). */
+        if (!TEST_false(SSL_set_alpn_protos(ssl, alpn_ossltest,
+                                            sizeof(alpn_ossltest))))
+            goto err;
+    }
+
+    RT()->scratch0 = 1; /* connect started */
+    ret = SSL_connect(ssl);
+    radix_activate_slot(0);
+    if (!TEST_true(check_consistent_want(ssl, ret)))
+        goto err;
+
+    if (ret != 1) {
+        if (1 /* TODO */ && is_want(ssl, ret))
+            F_SPIN_AGAIN();
+
+        if (!TEST_int_eq(ret, 1))
+            goto err;
+    }
+
+    ok = 1;
+err:
+    RT()->scratch0 = 0;
+    return ok;
+}
+
+DEF_FUNC(hf_detach)
+{
+    int ok = 0;
+    const char *conn_name, *stream_name;
+    SSL *conn, *stream;
+
+    F_POP2(conn_name, stream_name);
+    if (!TEST_ptr(conn = RADIX_PROCESS_get_ssl(RP(), conn_name)))
+        goto err;
+
+    if (!TEST_ptr(stream = ossl_quic_detach_stream(conn)))
+        goto err;
+
+    if (!TEST_true(RADIX_PROCESS_set_ssl(RP(), stream_name, stream))) {
+        SSL_free(stream);
+        goto err;
+    }
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_attach)
+{
+    int ok = 0;
+    const char *conn_name, *stream_name;
+    SSL *conn, *stream;
+
+    F_POP2(conn_name, stream_name);
+
+    if (!TEST_ptr(conn = RADIX_PROCESS_get_ssl(RP(), conn_name)))
+        goto err;
+
+    if (!TEST_ptr(stream = RADIX_PROCESS_get_ssl(RP(), stream_name)))
+        goto err;
+
+    if (!TEST_true(ossl_quic_attach_stream(conn, stream)))
+        goto err;
+
+    if (!TEST_true(RADIX_PROCESS_set_ssl(RP(), stream_name, NULL)))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_expect_fin)
+{
+    int ok = 0, ret;
+    SSL *ssl;
+    char buf[1];
+    size_t bytes_read = 0;
+
+    REQUIRE_SSL(ssl);
+
+    ret = SSL_read_ex(ssl, buf, sizeof(buf), &bytes_read);
+    if (!TEST_true(check_consistent_want(ssl, ret))
+        || !TEST_false(ret)
+        || !TEST_size_t_eq(bytes_read, 0))
+        goto err;
+
+    if (is_want(ssl, 0))
+        F_SPIN_AGAIN();
+
+    if (!TEST_int_eq(SSL_get_error(ssl, 0),
+                     SSL_ERROR_ZERO_RETURN))
+        goto err;
+
+    if (!TEST_int_eq(SSL_want(ssl), SSL_NOTHING))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_expect_conn_close_info)
+{
+    int ok = 0;
+    SSL *ssl;
+    SSL_CONN_CLOSE_INFO cc_info = {0};
+    uint64_t error_code, expect_app, expect_remote;
+
+    F_POP(error_code);
+    F_POP2(expect_app, expect_remote);
+    REQUIRE_SSL(ssl);
+
+    /* TODO BLOCKING */
+
+    if (!SSL_get_conn_close_info(ssl, &cc_info, sizeof(cc_info)))
+        F_SPIN_AGAIN();
+
+    if (!TEST_int_eq(expect_app,
+                     (cc_info.flags & SSL_CONN_CLOSE_FLAG_TRANSPORT) == 0)
+        || !TEST_int_eq(expect_remote,
+                        (cc_info.flags & SSL_CONN_CLOSE_FLAG_LOCAL) == 0)
+        || !TEST_uint64_t_eq(error_code, cc_info.error_code)) {
+        TEST_info("connection close reason: %s", cc_info.reason);
+        goto err;
+    }
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_wait_for_data)
+{
+    int ok = 0;
+    SSL *ssl;
+    char buf[1];
+    size_t bytes_read = 0;
+
+    REQUIRE_SSL(ssl);
+
+    if (!SSL_peek_ex(ssl, buf, sizeof(buf), &bytes_read)
+        || bytes_read == 0)
+        F_SPIN_AGAIN();
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_expect_err)
+{
+    int ok = 0;
+    uint64_t lib, reason;
+
+    F_POP2(lib, reason);
+    if (!TEST_size_t_eq((size_t)ERR_GET_LIB(ERR_peek_last_error()), lib)
+        || !TEST_size_t_eq((size_t)ERR_GET_REASON(ERR_peek_last_error()), reason))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_expect_ssl_err)
+{
+    int ok = 0;
+    uint64_t expected;
+    SSL *ssl;
+
+    F_POP(expected);
+    REQUIRE_SSL(ssl);
+
+    if (!TEST_size_t_eq((size_t)SSL_get_error(ssl, 0), expected)
+        || !TEST_int_eq(SSL_want(ssl), SSL_NOTHING))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_expect_stream_id)
+{
+    int ok = 0;
+    SSL *ssl;
+    uint64_t expected, actual;
+
+    F_POP(expected);
+    REQUIRE_SSL(ssl);
+
+    actual = SSL_get_stream_id(ssl);
+    if (!TEST_uint64_t_eq(actual, expected))
+        goto err;
+
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_select_ssl)
+{
+    int ok = 0;
+    uint64_t slot;
+    const char *name;
+    RADIX_OBJ *obj;
+
+    F_POP2(slot, name);
+    if (!TEST_ptr(obj = RADIX_PROCESS_get_obj(RP(), name)))
+        goto err;
+
+    if (!TEST_uint64_t_lt(slot, NUM_SLOTS))
+        goto err;
+
+    RT()->slot[slot]    = obj;
+    RT()->ssl[slot]     = obj->ssl;
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_clear_slot)
+{
+    int ok = 0;
+    uint64_t slot;
+
+    F_POP(slot);
+    if (!TEST_uint64_t_lt(slot, NUM_SLOTS))
+        goto err;
+
+    RT()->slot[slot]    = NULL;
+    RT()->ssl[slot]     = NULL;
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_skip_time)
+{
+    int ok = 0;
+    uint64_t ms;
+
+    F_POP(ms);
+
+    radix_skip_time(ossl_ms2time(ms));
+    ok = 1;
+err:
+    return ok;
+}
+
+DEF_FUNC(hf_set_peer_addr_from)
+{
+    int ok = 0;
+    SSL *dst_ssl, *src_ssl;
+    BIO *dst_bio, *src_bio;
+    int src_fd = -1;
+    union BIO_sock_info_u src_info;
+    BIO_ADDR *src_addr = NULL;
+
+    REQUIRE_SSL_N(0, dst_ssl);
+    REQUIRE_SSL_N(1, src_ssl);
+    dst_bio = SSL_get_rbio(dst_ssl);
+    src_bio = SSL_get_rbio(src_ssl);
+    if (!TEST_ptr(dst_bio) || !TEST_ptr(src_bio))
+        goto err;
+
+    if (!TEST_ptr(src_addr = BIO_ADDR_new()))
+        goto err;
+
+    if (!TEST_true(BIO_get_fd(src_bio, &src_fd))
+        || !TEST_int_ge(src_fd, 0))
+        goto err;
+
+    src_info.addr = src_addr;
+    if (!TEST_true(BIO_sock_info(src_fd, BIO_SOCK_INFO_ADDRESS, &src_info))
+        || !TEST_int_ge(ntohs(BIO_ADDR_rawport(src_addr)), 0))
+        goto err;
+
+    /*
+     * Could use SSL_set_initial_peer_addr here, but set it on the
+     * BIO_s_datagram instead and make sure we pick it up automatically.
+     */
+    if (!TEST_true(BIO_dgram_set_peer(dst_bio, src_addr)))
+        goto err;
+
+    ok = 1;
+err:
+    BIO_ADDR_free(src_addr);
+    return ok;
+}
+
+#define OP_UNBIND(name)                                         \
+    (OP_PUSH_PZ(#name),                                         \
+     OP_FUNC(hf_unbind))
+
+#define OP_SELECT_SSL(slot, name)                               \
+    (OP_PUSH_U64(slot),                                         \
+     OP_PUSH_PZ(#name),                                         \
+     OP_FUNC(hf_select_ssl))
+
+#define OP_CLEAR_SLOT(slot)                                     \
+    (OP_PUSH_U64(slot),                                         \
+     OP_FUNC(hf_clear_slot))
+
+#define OP_CONNECT_WAIT(name)                                   \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_FUNC(hf_connect_wait))
+
+#define OP_LISTEN(name)                                         \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_FUNC(hf_listen))
+
+#define OP_NEW_SSL_C(name)                                      \
+    (OP_PUSH_PZ(#name),                                         \
+     OP_PUSH_U64(0),                                            \
+     OP_FUNC(hf_new_ssl))
+
+#define OP_NEW_SSL_L(name)                                      \
+    (OP_PUSH_PZ(#name),                                         \
+     OP_PUSH_U64(1),                                            \
+     OP_FUNC(hf_new_ssl))
+
+#define OP_NEW_SSL_L_LISTEN(name)                               \
+    (OP_NEW_SSL_L(name),                                        \
+     OP_LISTEN(name))
+
+#define OP_SET_PEER_ADDR_FROM(dst_name, src_name)               \
+    (OP_SELECT_SSL(0, dst_name),                                \
+     OP_SELECT_SSL(1, src_name),                                \
+     OP_FUNC(hf_set_peer_addr_from))
+
+#define OP_SIMPLE_PAIR_CONN()                                   \
+    (OP_NEW_SSL_L_LISTEN(L),                                    \
+     OP_NEW_SSL_C(C),                                           \
+     OP_SET_PEER_ADDR_FROM(C, L),                               \
+     OP_CONNECT_WAIT(C))
+
+#define OP_NEW_STREAM(conn_name, stream_name, flags)            \
+    (OP_SELECT_SSL(0, conn_name),                               \
+     OP_PUSH_PZ(#stream_name),                                  \
+     OP_PUSH_U64(flags),                                        \
+     OP_PUSH_U64(0),                                            \
+     OP_FUNC(hf_new_stream))
+
+#define OP_ACCEPT_STREAM_WAIT(conn_name, stream_name, flags)    \
+    (OP_SELECT_SSL(0, conn_name),                               \
+     OP_PUSH_PZ(#stream_name),                                  \
+     OP_PUSH_U64(flags),                                        \
+     OP_PUSH_U64(1),                                            \
+     OP_FUNC(hf_new_stream))
+
+#define OP_ACCEPT_STREAM_NONE(conn_name)                        \
+    (OP_SELECT_SSL(0, conn_name),                               \
+     OP_FUNC(hf_accept_stream_none))
+
+#define OP_ACCEPT_CONN_WAIT(listener_name, conn_name, flags)    \
+    (OP_SELECT_SSL(0, listener_name),                           \
+     OP_PUSH_PZ(#conn_name),                                    \
+     OP_PUSH_U64(flags),                                        \
+     OP_FUNC(hf_accept_conn))
+
+#define OP_ACCEPT_CONN_NONE(listener_name)                      \
+    (OP_SELECT_SSL(0, listener_name),                           \
+     OP_FUNC(hf_accept_conn_none))
+
+#define OP_WRITE(name, buf, buf_len)                            \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_BUFP(buf, buf_len),                                \
+     OP_FUNC(hf_write))
+
+#define OP_WRITE_B(name, buf)                                   \
+    OP_WRITE(name, (buf), sizeof(buf))
+
+#define OP_WRITE_EX2(name, buf, buf_len, flags)                 \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_BUFP(buf, buf_len),                                \
+     OP_PUSH_U64(flags),                                        \
+     OP_FUNC(hf_write_ex2))
+
+#define OP_WRITE_FAIL(name)                                     \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_FUNC(hf_write_fail))
+
+#define OP_CONCLUDE(name)                                       \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_FUNC(hf_conclude))
+
+#define OP_READ_EXPECT(name, buf, buf_len)                      \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_BUFP(buf, buf_len),                                \
+     OP_FUNC(hf_read_expect))
+
+#define OP_READ_EXPECT_B(name, buf)                             \
+    OP_READ_EXPECT(name, (buf), sizeof(buf))
+
+#define OP_READ_FAIL()                                          \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_U64(0),                                            \
+     OP_FUNC(hf_read_fail))
+
+#define OP_READ_FAIL_WAIT(name)                                 \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_U64(1),                                            \
+     OP_FUNC(hf_read_fail)
+
+#define OP_POP_ERR()                                            \
+    OP_FUNC(hf_pop_err)
+
+#define OP_SET_DEFAULT_STREAM_MODE(name, mode)                  \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_U64(mode),                                         \
+     OP_FUNC(hf_set_default_stream_mode))
+
+#define OP_SET_INCOMING_STREAM_POLICY(name, policy, error_code) \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_U64(policy),                                       \
+     OP_PUSH_U64(error_code),                                   \
+     OP_FUNC(hf_set_incoming_stream_policy))
+
+#define OP_STREAM_RESET(name, error_code)                       \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_U64(flags),                                        \
+     OP_PUSH_U64(error_code),                                   \
+     OP_FUNC(hf_stream_reset))                                  \
+
+#define OP_SHUTDOWN_WAIT(name, flags, error_code, reason)       \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_U64(flags),                                        \
+     OP_PUSH_U64(error_code),                                   \
+     OP_PUSH_PZ(reason),                                        \
+     OP_FUNC(hf_shutdown_wait))
+
+#define OP_DETACH(conn_name, stream_name)                       \
+    (OP_SELECT_SSL(0, conn_name),                               \
+     OP_PUSH_PZ(#stream_name),                                  \
+     OP_FUNC(hf_detach))
+
+#define OP_ATTACH(conn_name, stream_name)                       \
+    (OP_SELECT_SSL(0, conn_name),                               \
+     OP_PUSH_PZ(stream_name),                                   \
+     OP_FUNC(hf_attach))
+
+#define OP_EXPECT_FIN(name)                                     \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_FUNC(hf_expect_fin))
+
+#define OP_EXPECT_CONN_CLOSE_INFO(name, error_code, expect_app, expect_remote) \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_U64(expect_app),                                   \
+     OP_PUSH_U64(expect_remote),                                \
+     OP_PUSH_U64(error_code),                                   \
+     OP_FUNC(hf_expect_conn_close_info))
+
+#define OP_WAIT_FOR_DATA(name)                                  \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_FUNC(hf_wait_for_data))
+
+#define OP_EXPECT_ERR(lib, reason)                              \
+    (OP_PUSH_U64(lib),                                          \
+     OP_PUSH_U64(reason),                                       \
+     OP_FUNC(hf_expect_err))
+
+#define OP_EXPECT_SSL_ERR(name, expected)                       \
+    (OP_SELECT_SSL(0, name),                                    \
+     OP_PUSH_U64(expected),                                     \
+     OP_FUNC(hf_expect_ssl_err))
+
+#define OP_EXPECT_STREAM_ID(expected)                           \
+    (OP_PUSH_U64(expected),                                     \
+     OP_FUNC(hf_expect_stream_id))
+
+#define OP_SKIP_TIME(ms)                                        \
+    (OP_PUSH_U64(ms),                                           \
+     OP_FUNC(hf_skip_time))
diff --git a/test/radix/quic_radix.c b/test/radix/quic_radix.c
new file mode 100644 (file)
index 0000000..2715f26
--- /dev/null
@@ -0,0 +1,13 @@
+/*
+ * 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 "terp.c"
+#include "quic_bindings.c"
+#include "quic_ops.c"
+#include "quic_tests.c"
+#include "main.c"
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
new file mode 100644 (file)
index 0000000..3398c9d
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * 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
+ */
+
+/*
+ * Test Scripts
+ * ============================================================================
+ */
+
+DEF_SCRIPT(simple_conn, "simple connection to server")
+{
+    OP_SIMPLE_PAIR_CONN();
+    OP_WRITE_B(C, "apple");
+
+    OP_ACCEPT_CONN_WAIT(L, La, 0);
+    OP_ACCEPT_CONN_NONE(L);
+
+    OP_WRITE_B(La, "orange");
+    OP_READ_EXPECT_B(C, "orange");
+}
+
+/*
+ * List of Test Scripts
+ * ============================================================================
+ */
+static SCRIPT_INFO *const scripts[] = {
+    USE(simple_conn)
+};
diff --git a/test/radix/terp.c b/test/radix/terp.c
new file mode 100644 (file)
index 0000000..06aa0aa
--- /dev/null
@@ -0,0 +1,882 @@
+/*
+ * 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/ssl.h>
+#include <openssl/quic.h>
+#include <openssl/bio.h>
+#include <openssl/lhash.h>
+#include <openssl/rand.h>
+#include "../testutil.h"
+#include "internal/numbers.h"   /* UINT64_C */
+#include "internal/time.h"      /* OSSL_TIME */
+
+static const char *cert_file, *key_file;
+
+/*
+ * TERP - Test Executive Script Interpreter
+ * ========================================
+ */
+typedef struct gen_ctx_st GEN_CTX;
+
+typedef void (*script_gen_t)(GEN_CTX *ctx);
+
+typedef struct script_info_st {
+    /* name: A symbolic name, like simple_conn. */
+    const char      *name;
+    /* desc: A short, one-line description. */
+    const char      *desc;
+    const char      *file;
+    int             line;
+    /* gen_func: The script generation function. */
+    script_gen_t    gen_func;
+} SCRIPT_INFO;
+
+struct gen_ctx_st {
+    SCRIPT_INFO *script_info;
+    const char  *cur_file;
+    int         error, cur_line;
+    const char  *first_error_msg, *first_error_file;
+    int         first_error_line;
+
+    uint8_t     *build_buf_beg, *build_buf_cur, *build_buf_end;
+};
+
+static int GEN_CTX_init(GEN_CTX *ctx, SCRIPT_INFO *script_info)
+{
+    ctx->script_info        = script_info;
+    ctx->error              = 0;
+    ctx->cur_file           = NULL;
+    ctx->cur_line           = 0;
+    ctx->first_error_msg    = NULL;
+    ctx->first_error_line   = 0;
+    ctx->build_buf_beg      = NULL;
+    ctx->build_buf_cur      = NULL;
+    ctx->build_buf_end      = NULL;
+    return 1;
+}
+
+static void GEN_CTX_cleanup(GEN_CTX *ctx)
+{
+    OPENSSL_free(ctx->build_buf_beg);
+    ctx->build_buf_beg = ctx->build_buf_cur = ctx->build_buf_end = NULL;
+}
+
+typedef struct terp_st TERP;
+
+#define F_RET_SPIN_AGAIN        2
+#define F_RET_SKIP_REST         3
+
+#define F_SPIN_AGAIN()                          \
+    do {                                        \
+        ok                  = F_RET_SPIN_AGAIN; \
+        fctx->spin_again    = 1;                \
+        goto err;                               \
+    } while (0)
+
+#define F_SKIP_REST()                           \
+    do {                                        \
+        ok                  = F_RET_SKIP_REST;  \
+        fctx->skip_rest     = 1;                \
+        goto err;                               \
+    } while (0)
+
+typedef struct func_ctx_st {
+    TERP    *terp;
+
+    /*
+     * Set to 1 inside a user function if the function should spin again.
+     * Cleared automatically after the user function returns.
+     */
+    int     spin_again;
+
+    /*
+     * Immediately exit script successfully. Useful for skipping.
+     */
+    int     skip_rest;
+} FUNC_CTX;
+
+static ossl_inline int TERP_stk_pop(TERP *terp,
+                                    void *buf, size_t buf_len);
+
+#define TERP_STK_PUSH(terp, v)                                  \
+    do {                                                        \
+        if (!TEST_true(TERP_stk_push((terp), &(v), sizeof(v)))) \
+            goto err;                                           \
+    } while (0)
+
+#define TERP_STK_POP(terp, v)                                   \
+    do {                                                        \
+        if (!TEST_true(TERP_stk_pop((terp), &(v), sizeof(v))))  \
+            goto err;                                           \
+    } while (0)
+
+#define TERP_STK_POP2(terp, a, b)                               \
+    do {                                                        \
+        TERP_STK_POP((terp), (b));                              \
+        TERP_STK_POP((terp), (a));                              \
+    } while (0)
+
+#define F_PUSH(v)       TERP_STK_PUSH(fctx->terp, (v))
+#define F_POP(v)        TERP_STK_POP (fctx->terp, (v))
+#define F_POP2(a, b)    TERP_STK_POP2(fctx->terp, (a), (b))
+
+typedef int (*helper_func_t)(FUNC_CTX *fctx);
+
+#define DEF_FUNC(name) ossl_unused static int name(FUNC_CTX *fctx)
+
+#define DEF_SCRIPT(name, desc)                          \
+    static void script_gen_##name(GEN_CTX *ctx);        \
+    static SCRIPT_INFO script_info_##name = {           \
+        #name, desc, __FILE__, __LINE__,                \
+        script_gen_##name                               \
+    };                                                  \
+    static void script_gen_##name(GEN_CTX *ctx)
+
+enum {
+    OPK_INVALID,
+    OPK_END,
+    OPK_PUSH_P,
+    /*
+     * This is exactly like PUSH_P, but the script dumper knows the pointer
+     * points to a static NUL-terminated string and can therefore print it.
+     */
+    OPK_PUSH_PZ,
+    OPK_PUSH_U64,
+    /*
+     * Could use OPK_PUSH_U64 for this but it's annoying to have to avoid using
+     * size_t in case it is a different size.
+     */
+    OPK_PUSH_SIZE,
+    OPK_FUNC,
+    OPK_LABEL
+};
+
+static void *openc_alloc_space(GEN_CTX *ctx, size_t num_bytes);
+
+#define DEF_ENCODER(name, type)                         \
+    static void name(GEN_CTX *ctx, type v)              \
+    {                                                   \
+        void *dst = openc_alloc_space(ctx, sizeof(v));  \
+        if (dst == NULL)                                \
+            return;                                     \
+                                                        \
+        memcpy(dst, &v, sizeof(v));                     \
+    }
+
+DEF_ENCODER(openc_u64, uint64_t)
+DEF_ENCODER(openc_size, size_t)
+DEF_ENCODER(openc_p, void *)
+DEF_ENCODER(openc_fp, helper_func_t)
+#define openc_opcode    openc_u64
+
+static void opgen_END(GEN_CTX *ctx)
+{
+    openc_opcode(ctx, OPK_END);
+}
+
+static ossl_unused void opgen_PUSH_P(GEN_CTX *ctx, void *p)
+{
+    openc_opcode(ctx, OPK_PUSH_P);
+    openc_p(ctx, p);
+}
+
+static void opgen_PUSH_PZ(GEN_CTX *ctx, void *p)
+{
+    openc_opcode(ctx, OPK_PUSH_PZ);
+    openc_p(ctx, p);
+}
+
+static void opgen_PUSH_U64(GEN_CTX *ctx, uint64_t v)
+{
+    openc_opcode(ctx, OPK_PUSH_U64);
+    openc_u64(ctx, v);
+}
+
+ossl_unused static void opgen_PUSH_SIZE(GEN_CTX *ctx, size_t v)
+{
+    openc_opcode(ctx, OPK_PUSH_SIZE);
+    openc_size(ctx, v);
+}
+
+ossl_unused static void opgen_FUNC(GEN_CTX *ctx, helper_func_t f,
+                                   const char *f_name)
+{
+    openc_opcode(ctx, OPK_FUNC);
+    openc_fp(ctx, f);
+    openc_p(ctx, (void *)f_name);
+}
+
+ossl_unused static void opgen_LABEL(GEN_CTX *ctx, const char *name)
+{
+    openc_opcode(ctx, OPK_LABEL);
+    openc_p(ctx, (void *)name);
+}
+
+static void opgen_set_line(GEN_CTX *ctx, const char *file, int line)
+{
+    ctx->cur_file = file;
+    ctx->cur_line = line;
+}
+
+static ossl_unused void opgen_fail(GEN_CTX *ctx, const char *msg)
+{
+    if (!ctx->error) {
+        ctx->first_error_file = ctx->cur_file;
+        ctx->first_error_line = ctx->cur_line;
+        ctx->first_error_msg  = msg;
+    }
+
+    ctx->error = 1;
+}
+
+#define OPGEN(n)        (opgen_set_line(ctx, __FILE__, __LINE__), \
+                         opgen_##n)
+#define OP_END()        OPGEN(END)      (ctx)
+#define OP_PUSH_P(v)    OPGEN(PUSH_P)   (ctx, (v))
+#define OP_PUSH_PZ(v)   OPGEN(PUSH_PZ)  (ctx, (v))
+#define OP_PUSH_U64(v)  OPGEN(PUSH_U64) (ctx, (v))
+#define OP_PUSH_SIZE(v) OPGEN(PUSH_SIZE) (ctx, (v))
+#define OP_PUSH_BUFP(p, l)  (OP_PUSH_P(p), OP_PUSH_SIZE(l))
+#define OP_PUSH_BUF(v)      OP_PUSH_BUFP(&(v), sizeof(v))
+#define OP_PUSH_LREF(v) OPGEN(PUSH_LREF)(ctx, (lref))
+#define OP_FUNC(f)      OPGEN(FUNC)     (ctx, (f), #f)
+#define OP_LABEL(name)  OPGEN(LABEL)    (ctx, (name))
+#define GEN_FAIL(msg)   OPGEN(fail)     (ctx, (msg))
+
+static void *openc_alloc_space(GEN_CTX *ctx, size_t num_bytes)
+{
+    void *p;
+    size_t cur_spare, old_size, new_size, off;
+
+    cur_spare = ctx->build_buf_end - ctx->build_buf_cur;
+    if (cur_spare < num_bytes) {
+        off         = ctx->build_buf_cur - ctx->build_buf_beg;
+        old_size    = ctx->build_buf_end - ctx->build_buf_beg;
+        new_size    = (old_size == 0) ? 1024 : old_size * 2;
+        p = OPENSSL_realloc(ctx->build_buf_beg, new_size);
+        if (!TEST_ptr(p))
+            return NULL;
+
+        ctx->build_buf_beg = p;
+        ctx->build_buf_cur = ctx->build_buf_beg + off;
+        ctx->build_buf_end = ctx->build_buf_beg + new_size;
+    }
+
+    p = ctx->build_buf_cur;
+    ctx->build_buf_cur += num_bytes;
+    return p;
+}
+
+/*
+ * Script Interpreter
+ * ============================================================================
+ */
+typedef struct gen_script_st {
+    const uint8_t *buf;
+    size_t buf_len;
+} GEN_SCRIPT;
+
+static int GEN_CTX_finish(GEN_CTX *ctx, GEN_SCRIPT *script)
+{
+    script->buf         = ctx->build_buf_beg;
+    script->buf_len     = ctx->build_buf_cur - ctx->build_buf_beg;
+    ctx->build_buf_beg = ctx->build_buf_cur = ctx->build_buf_end = NULL;
+    return 1;
+}
+
+static void GEN_SCRIPT_cleanup(GEN_SCRIPT *script)
+{
+    OPENSSL_free((char *)script->buf);
+
+    script->buf     = NULL;
+    script->buf_len = 0;
+}
+
+static int GEN_SCRIPT_init(GEN_SCRIPT *gen_script, SCRIPT_INFO *script_info)
+{
+    int ok = 0;
+    GEN_CTX gctx;
+
+    if (!TEST_true(GEN_CTX_init(&gctx, script_info)))
+        return 0;
+
+    script_info->gen_func(&gctx);
+    opgen_END(&gctx);
+
+    if (!TEST_false(gctx.error))
+        goto err;
+
+    if (!TEST_true(GEN_CTX_finish(&gctx, gen_script)))
+        goto err;
+
+    ok = 1;
+err:
+    if (!ok) {
+        if (gctx.error)
+            TEST_error("script generation failed: %s (at %s:%d)",
+                       gctx.first_error_msg,
+                       gctx.first_error_file,
+                       gctx.first_error_line);
+
+        GEN_CTX_cleanup(&gctx);
+    }
+    return ok;
+}
+
+typedef struct srdr_st {
+    const uint8_t   *beg, *cur, *end, *save_cur;
+} SRDR;
+
+static void SRDR_init(SRDR *rdr, const uint8_t *buf, size_t buf_len)
+{
+    rdr->beg = rdr->cur = buf;
+    rdr->end = rdr->beg + buf_len;
+    rdr->save_cur = NULL;
+}
+
+static ossl_inline int SRDR_get_operand(SRDR *srdr, void *buf, size_t buf_len)
+{
+    if (!TEST_size_t_ge(srdr->end - srdr->cur, buf_len))
+        return 0; /* malformed script */
+
+    memcpy(buf, srdr->cur, buf_len);
+    srdr->cur += buf_len;
+    return 1;
+}
+
+static ossl_inline void SRDR_save(SRDR *srdr)
+{
+    srdr->save_cur = srdr->cur;
+}
+
+static ossl_inline void SRDR_restore(SRDR *srdr)
+{
+    srdr->cur = srdr->save_cur;
+}
+
+#define GET_OPERAND(srdr, v)                                        \
+    do {                                                            \
+        if (!TEST_true(SRDR_get_operand(srdr, &(v), sizeof(v))))    \
+            goto err;                                               \
+    } while (0)
+
+
+static void print_opc(BIO *bio, size_t op_num, size_t offset, const char *name)
+{
+    if (op_num != SIZE_MAX)
+        BIO_printf(bio, "%3zu-  %4zx>\t%-8s \t", op_num,
+                   offset, name);
+    else
+        BIO_printf(bio, "      %4zx>\t%-8s \t",
+                   offset, name);
+}
+
+static int SRDR_print_one(SRDR *srdr, BIO *bio, size_t i, int *was_end)
+{
+    int ok = 0;
+    const uint8_t *opc_start;
+    uint64_t opc;
+
+    if (was_end != NULL)
+        *was_end = 0;
+
+    opc_start = srdr->cur;
+    GET_OPERAND(srdr, opc);
+
+#define PRINT_OPC(name) print_opc(bio, i, (size_t)(opc_start - srdr->beg), #name)
+
+    switch (opc) {
+    case OPK_END:
+        PRINT_OPC(END);
+        opc_start = srdr->cur;
+        if (was_end != NULL)
+            *was_end = 1;
+        break;
+    case OPK_PUSH_P:
+        {
+            void *v;
+
+            GET_OPERAND(srdr, v);
+            PRINT_OPC(PUSH_P);
+            BIO_printf(bio, "%20p", v);
+        }
+        break;
+    case OPK_PUSH_PZ:
+        {
+            void *v;
+
+            GET_OPERAND(srdr, v);
+            PRINT_OPC(PUSH_P);
+            if (v != NULL && strlen((const char *)v) == 1)
+                BIO_printf(bio, "%20p (%s)", v, (const char *)v);
+            else
+                BIO_printf(bio, "%20p (\"%s\")", v, (const char *)v);
+        }
+        break;
+    case OPK_PUSH_U64:
+        {
+            uint64_t v;
+
+            GET_OPERAND(srdr, v);
+            PRINT_OPC(PUSH_U64);
+            BIO_printf(bio, "%#20llx (%lld)",
+                       (unsigned long long)v, (unsigned long long)v);
+        }
+        break;
+    case OPK_PUSH_SIZE:
+        {
+            size_t v;
+
+            GET_OPERAND(srdr, v);
+            PRINT_OPC(PUSH_SIZE);
+            BIO_printf(bio, "%#20llx (%lld)",
+                       (unsigned long long)v, (unsigned long long)v);
+        }
+        break;
+    case OPK_FUNC:
+        {
+            helper_func_t v;
+            void *f_name, *x;
+
+            GET_OPERAND(srdr, v);
+            GET_OPERAND(srdr, f_name);
+
+            PRINT_OPC(FUNC);
+            memcpy(&x, &v, sizeof(x) < sizeof(v) ? sizeof(x) : sizeof(v));
+            BIO_printf(bio, "%s", (const char *)f_name);
+        }
+        break;
+    case OPK_LABEL:
+        {
+            void *l_name;
+
+            GET_OPERAND(srdr, l_name);
+
+            BIO_printf(bio, "\n%s:\n", (const char *)l_name);
+            PRINT_OPC(LABEL);
+        }
+        break;
+    default:
+        TEST_error("unsupported opcode while printing: %llu",
+                   (unsigned long long)opc);
+        goto err;
+    }
+
+    ok = 1;
+err:
+    return ok;
+}
+
+static int GEN_SCRIPT_print(GEN_SCRIPT *gen_script, BIO *bio,
+                            const SCRIPT_INFO *script_info)
+{
+    int ok = 0;
+    size_t i;
+    SRDR srdr_v, *srdr = &srdr_v;
+    int was_end = 0;
+
+    SRDR_init(srdr, gen_script->buf, gen_script->buf_len);
+
+    if (script_info != NULL) {
+        BIO_printf(bio, "\nGenerated script for '%s':\n",
+                               script_info->name);
+        BIO_printf(bio, "\n--GENERATED-------------------------------------"
+                  "----------------------\n");
+        BIO_printf(bio, "  # NAME:\n  #   %s\n",
+                   script_info->name);
+        BIO_printf(bio, "  # SOURCE:\n  #   %s:%d\n",
+                   script_info->file, script_info->line);
+        BIO_printf(bio, "  # DESCRIPTION:\n  #   %s\n", script_info->desc);
+    }
+
+    for (i = 0; !was_end; ++i) {
+        BIO_printf(bio, "\n");
+
+        if (!TEST_true(SRDR_print_one(srdr, bio, i, &was_end)))
+            goto err;
+    }
+
+    if (script_info != NULL) {
+        const unsigned char *opc_start = srdr->cur;
+
+        BIO_printf(bio, "\n");
+        PRINT_OPC(+++);
+        BIO_printf(bio, "\n------------------------------------------------"
+                  "----------------------\n\n");
+    }
+
+    ok = 1;
+err:
+    return ok;
+}
+
+static void SCRIPT_INFO_print(SCRIPT_INFO *script_info, BIO *bio, int error,
+                              const char *msg)
+{
+    if (error)
+        TEST_error("%s: script '%s' (%s)",
+                   msg, script_info->name, script_info->desc);
+    else
+        TEST_info("%s: script '%s' (%s)",
+                  msg, script_info->name, script_info->desc);
+}
+
+typedef struct terp_config_st {
+    BIO         *debug_bio;
+
+    OSSL_TIME   (*now_cb)(void *arg);
+    void        *now_cb_arg;
+
+    int         (*per_op_cb)(TERP *terp, void *arg);
+    void        *per_op_cb_arg;
+
+    OSSL_TIME   max_execution_time; /* duration */
+} TERP_CONFIG;
+
+#define TERP_DEFAULT_MAX_EXECUTION_TIME     (ossl_ms2time(3000))
+
+struct terp_st {
+    TERP_CONFIG         cfg;
+    const SCRIPT_INFO   *script_info;
+    const GEN_SCRIPT    *gen_script;
+    SRDR                srdr;
+    uint8_t             *stk_beg, *stk_cur, *stk_end, *stk_save_cur;
+    FUNC_CTX            fctx;
+    uint64_t            ops_executed;
+    int                 log_execute;
+    OSSL_TIME           start_time, deadline_time;
+};
+
+static int TERP_init(TERP *terp,
+                     const TERP_CONFIG *cfg,
+                     const SCRIPT_INFO *script_info,
+                     const GEN_SCRIPT *gen_script)
+{
+    if (!TEST_true(cfg->now_cb != NULL))
+        return 0;
+
+    terp->cfg               = *cfg;
+    terp->script_info       = script_info;
+    terp->gen_script        = gen_script;
+    terp->fctx.terp         = terp;
+    terp->fctx.spin_again   = 0;
+    terp->fctx.skip_rest    = 0;
+    terp->stk_beg           = NULL;
+    terp->stk_cur           = NULL;
+    terp->stk_end           = NULL;
+    terp->stk_save_cur      = NULL;
+    terp->ops_executed      = 0;
+    terp->log_execute       = 1;
+
+    if (ossl_time_is_zero(terp->cfg.max_execution_time))
+        terp->cfg.max_execution_time = TERP_DEFAULT_MAX_EXECUTION_TIME;
+
+    return 1;
+}
+
+static void TERP_cleanup(TERP *terp)
+{
+    if (terp->script_info == NULL)
+        return;
+
+    OPENSSL_free(terp->stk_beg);
+    terp->stk_beg = terp->stk_cur = terp->stk_end = NULL;
+    terp->script_info = NULL;
+}
+
+static int TERP_stk_ensure_capacity(TERP *terp, size_t spare)
+{
+    uint8_t *p;
+    size_t old_size, new_size, off;
+
+    old_size = terp->stk_end - terp->stk_beg;
+    if (old_size >= spare)
+        return 1;
+
+    off         = terp->stk_end - terp->stk_cur;
+    new_size    = old_size != 0 ? old_size * 2 : 256;
+    p = OPENSSL_realloc(terp->stk_beg, new_size);
+    if (!TEST_ptr(p))
+        return 0;
+
+    terp->stk_beg = p;
+    terp->stk_end = terp->stk_beg + new_size;
+    terp->stk_cur = terp->stk_end - off;
+    return 1;
+}
+
+static ossl_inline int TERP_stk_push(TERP *terp,
+                                     const void *buf, size_t buf_len)
+{
+    if (!TEST_true(TERP_stk_ensure_capacity(terp, buf_len)))
+        return 0;
+
+    terp->stk_cur -= buf_len;
+    memcpy(terp->stk_cur, buf, buf_len);
+    return 1;
+}
+
+static ossl_inline int TERP_stk_pop(TERP *terp,
+                                    void *buf, size_t buf_len)
+{
+    if (!TEST_size_t_ge(terp->stk_end - terp->stk_cur, buf_len)) {
+        asm("int3");
+        return 0;
+    }
+
+    memcpy(buf, terp->stk_cur, buf_len);
+    terp->stk_cur += buf_len;
+    return 1;
+}
+
+static void TERP_print_stack(TERP *terp, BIO *bio, const char *header)
+{
+    test_output_memory(header, terp->stk_cur, terp->stk_end - terp->stk_cur);
+    BIO_printf(bio, "  (%zu bytes)\n", terp->stk_end - terp->stk_cur);
+    BIO_printf(bio, "\n");
+}
+
+#define TERP_GET_OPERAND(v) GET_OPERAND(&terp->srdr, (v))
+
+#define TERP_SPIN_AGAIN()                       \
+    do {                                        \
+        SRDR_restore(&terp->srdr);              \
+        terp->stk_cur = terp->stk_save_cur;     \
+        ++spin_count;                           \
+        goto spin_again;                        \
+    } while (0)
+
+static OSSL_TIME TERP_now(TERP *terp)
+{
+    return terp->cfg.now_cb(terp->cfg.now_cb_arg);
+}
+
+static void TERP_log_spin(TERP *terp, size_t spin_count)
+{
+    if (spin_count > 0)
+        BIO_printf(terp->cfg.debug_bio, "           \t\t(span %zu times)\n",
+                   spin_count);
+}
+
+static int TERP_execute(TERP *terp)
+{
+    int ok = 0;
+    uint64_t opc;
+    size_t op_num = SIZE_MAX;
+    int in_debug_output = 0;
+    size_t spin_count = 0;
+    BIO *debug_bio = terp->cfg.debug_bio;
+
+    SRDR_init(&terp->srdr, terp->gen_script->buf, terp->gen_script->buf_len);
+
+    terp->start_time    = TERP_now(terp);
+    terp->deadline_time = ossl_time_add(terp->start_time,
+                                        terp->cfg.max_execution_time);
+
+    for (;;) {
+        if (terp->log_execute) {
+            SRDR srdr_copy = terp->srdr;
+
+            if (!in_debug_output) {
+                BIO_printf(debug_bio, "\n--EXECUTION-----------------------------"
+                          "------------------------------\n");
+                in_debug_output = 1;
+            }
+
+            TERP_log_spin(terp, spin_count);
+            if (!TEST_true(SRDR_print_one(&srdr_copy, debug_bio, SIZE_MAX, NULL)))
+                goto err;
+
+            BIO_printf(debug_bio, "\n");
+        }
+
+        TERP_GET_OPERAND(opc);
+        ++op_num;
+        SRDR_save(&terp->srdr);
+        terp->stk_save_cur = terp->stk_cur;
+        spin_count = 0;
+
+        ++terp->ops_executed;
+
+spin_again:
+        if (ossl_time_compare(TERP_now(terp), terp->deadline_time) >= 0) {
+            TEST_error("timed out while executing op %zu", op_num);
+            if (terp->log_execute)
+                TERP_log_spin(terp, spin_count);
+            goto err;
+        }
+
+        if (terp->cfg.per_op_cb != NULL)
+            if (!TEST_true(terp->cfg.per_op_cb(terp, terp->cfg.per_op_cb_arg))) {
+                TEST_error("pre-operation processing failed at op %zu", op_num);
+                if (terp->log_execute)
+                    TERP_log_spin(terp, spin_count);
+                goto err;
+            }
+
+        switch (opc) {
+        case OPK_END:
+            goto stop;
+        case OPK_PUSH_P:
+        case OPK_PUSH_PZ:
+            {
+                void *v;
+
+                TERP_GET_OPERAND(v);
+                TERP_STK_PUSH(terp, v);
+            }
+            break;
+        case OPK_PUSH_U64:
+            {
+                uint64_t v;
+
+                TERP_GET_OPERAND(v);
+                TERP_STK_PUSH(terp, v);
+            }
+            break;
+        case OPK_PUSH_SIZE:
+            {
+                uint64_t v;
+
+                TERP_GET_OPERAND(v);
+                TERP_STK_PUSH(terp, v);
+            }
+            break;
+        case OPK_LABEL:
+            {
+                const char *l_name;
+
+                TERP_GET_OPERAND(l_name);
+                /* no-op */
+            }
+            break;
+        case OPK_FUNC:
+            {
+                helper_func_t v;
+                const void *f_name;
+                int ret;
+
+                TERP_GET_OPERAND(v);
+                TERP_GET_OPERAND(f_name);
+
+                if (!TEST_true(v != NULL))
+                    goto err;
+
+                ret = v(&terp->fctx);
+
+                if (terp->fctx.skip_rest) {
+                    if (!TEST_int_eq(ret, F_RET_SKIP_REST))
+                        goto err;
+
+                    if (terp->log_execute)
+                        BIO_printf(terp->cfg.debug_bio, "           \t\t(skipping)\n");
+
+                    terp->fctx.skip_rest = 0;
+                    goto stop;
+                } else if (terp->fctx.spin_again) {
+                    if (!TEST_int_eq(ret, F_RET_SPIN_AGAIN))
+                        goto err;
+
+                    terp->fctx.spin_again = 0;
+                    TERP_SPIN_AGAIN();
+                } else {
+                    if (!TEST_false(terp->fctx.spin_again))
+                        goto err;
+
+                    if (ret != 1) {
+                        TEST_error("op %zu (FUNC %s) failed with return value %d",
+                                   op_num, (const char *)f_name, ret);
+                        goto err;
+                    }
+                }
+            }
+            break;
+        default:
+            TEST_error("unknown opcode: %llu", (unsigned long long)opc);
+            goto err;
+        }
+    }
+
+stop:
+    ok = 1;
+err:
+    if (in_debug_output)
+        BIO_printf(debug_bio, "----------------------------------------"
+                   "------------------------------\n");
+
+    if (!ok) {
+        TEST_error("FAILED while executing script: %s at op %zu, error stack:",
+                   terp->script_info->name, op_num);
+        ERR_print_errors(terp->cfg.debug_bio);
+        BIO_printf(debug_bio, "\n");
+    } else if (ERR_peek_last_error() != 0) {
+        TEST_info("WARNING: errors on error stack despite success:");
+        ERR_print_errors(terp->cfg.debug_bio);
+        BIO_printf(debug_bio, "\n");
+    }
+
+    return ok;
+}
+
+static int TERP_run(SCRIPT_INFO *script_info, TERP_CONFIG *cfg)
+{
+    int ok = 0, have_terp = 0;
+    TERP terp;
+    GEN_SCRIPT gen_script = {0};
+    BIO *debug_bio = cfg->debug_bio;
+
+    SCRIPT_INFO_print(script_info, debug_bio, /*error=*/0, "generating script");
+
+    /* Generate the script by calling the generator function. */
+    if (!TEST_true(GEN_SCRIPT_init(&gen_script, script_info))) {
+        SCRIPT_INFO_print(script_info, debug_bio, /*error=*/1,
+                          "error while generating script");
+        goto err;
+    }
+
+    /* Output the script for debugging purposes. */
+    if (!TEST_true(GEN_SCRIPT_print(&gen_script, debug_bio, script_info))) {
+        SCRIPT_INFO_print(script_info, debug_bio, /*error=*/1,
+                          "error while printing script");
+        goto err;
+    }
+
+    /* Execute the script. */
+    if (!TEST_true(TERP_init(&terp, cfg, script_info, &gen_script)))
+        goto err;
+
+    have_terp = 1;
+
+    SCRIPT_INFO_print(script_info, debug_bio, /*error=*/0, "executing script");
+
+    if (!TERP_execute(&terp))
+        goto err;
+
+    if (terp.stk_end - terp.stk_cur != 0) {
+        TEST_error("stack not empty: %zu bytes left",
+                   terp.stk_end - terp.stk_cur);
+        goto err;
+    }
+
+    ok = 1;
+err:
+    if (have_terp) {
+        TERP_print_stack(&terp, debug_bio, "Final state of stack");
+        TERP_cleanup(&terp);
+    }
+
+    GEN_SCRIPT_cleanup(&gen_script);
+    BIO_printf(debug_bio, "Stats:\n  Ops executed: %16llu\n\n",
+               (unsigned long long)terp.ops_executed);
+    SCRIPT_INFO_print(script_info, debug_bio, /*error=*/!ok,
+                      ok ? "completed" : "failed, exiting");
+    return ok;
+}
+
+#define SCRIPT(name)    (&script_info_##name)
+#define USE(name)       SCRIPT(name),