Commit fcba83dc4d for openssl.org

commit fcba83dc4d2a50581466eadd1598e1dd9c0cea03
Author: Andrew Dinh <andrewd@openssl.org>
Date:   Sun Jul 12 23:50:05 2026 +0700

    Wire radix QUIC test framework clock to simulated time

    script_17/18/19 (key update tests) rely on OP_SKIP_TIME to advance
    simulated time so that TXKU cooldown/RTT-gated behaviour can be
    exercised deterministically, but the framework never actually plumbed
    that simulated clock into the QUIC channels under test -- OP_SKIP_TIME
    only affected the terp's own bookkeeping, so real key updates only
    ever occurred by chance regardless of how much time was "skipped".

    Fix this by:

    - Calling ossl_quic_set_override_now_cb() in hf_new_ssl() so each QUIC
      channel actually uses the framework's simulated clock, matching what
      the older quictestlib.c harness already did via fake_now_cb().
    - Activating the server-side connection object in hf_accept_conn() so
      it gets ticked (SSL_handle_events()) like every other object, driving
      its internal key-update/timer processing forward.

    Two correctness issues had to be addressed to make this clock
    plumbing safe and deterministic:

    - The clock handed to QUIC channels must be purely virtual (a fixed
      base time plus an explicit slip that only advances via
      radix_skip_time()/OP_SKIP_TIME), not real wall-clock time, or
      protocol-timing-sensitive assertions become flaky depending on how
      long real cryptographic work takes on a given machine (e.g. much
      slower on ASAN+UBSAN debug builds), occasionally triggering a
      spurious second TXKU before the first is confirmed. radix_process is
      a single static struct reused across every script in the suite, so
      time_slip must be reset in RADIX_PROCESS_init() -- otherwise a
      script would inherit the previous script's accumulated slip and see
      time jump forward all at once for a brand new connection. The
      virtual clock also needs a small, fixed per-tick advance
      (do_per_op(), mirroring the old harness's qtest_add_time(1) in its
      own connect-wait loop), since QUIC's internal timers need to
      observe time actually passing to make progress during a busy-wait
      such as hf_connect_wait spinning on SSL_connect().
    - time_slip needs its own dedicated mutex (time_m) rather than being
      protected by the existing global mutex (gm): get_time() is now
      called by libssl/QUIC's internals (e.g. from within the reactor tick
      while holding QUIC's own locks), whereas gm is held by test code
      across calls into libssl (e.g. hf_clear() holds gm while calling
      SSL_free()). Sharing gm would take gm and QUIC's internal lock in
      opposite orders across those two paths, risking a real deadlock
      (confirmed via a ThreadSanitizer lock-order-inversion report).
    - The terp's own script-execution watchdog (max_execution_time) needs
      its own now_cb (terp_now) based on real wall-clock time, so that
      OP_SKIP_TIME doesn't eat into its execution budget, and so it can
      still catch a genuinely stuck script independent of the virtual
      protocol clock. Some scripts also legitimately need a larger budget
      than the terp default on slow or heavily instrumented CI machines.

    Assisted-by: Claude:claude-sonnet-5

    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Bob Beck <beck@openssl.org>
    MergeDate: Tue Jul 21 09:22:53 2026
    (Merged from https://github.com/openssl/openssl/pull/31889)

diff --git a/test/radix/quic_bindings.c b/test/radix/quic_bindings.c
index 7dc46f7950..63bcecad99 100644
--- a/test/radix/quic_bindings.c
+++ b/test/radix/quic_bindings.c
@@ -67,9 +67,12 @@ typedef struct radix_process_st {
     /* Process-global state. */
     CRYPTO_MUTEX *gm; /* global mutex */
     LHASH_OF(RADIX_OBJ) *objs; /* protected by gm */
-    OSSL_TIME time_slip; /* protected by gm */
     BIO *keylog_out; /* protected by gm */

+    CRYPTO_MUTEX *time_m;
+    OSSL_TIME base_time; /* set once at init, constant thereafter */
+    OSSL_TIME time_slip; /* protected by time_m */
+
     int done_join_all_threads;

     /*
@@ -178,6 +181,8 @@ static int RADIX_PROCESS_init(RADIX_PROCESS *rp, size_t node_idx, size_t process
 #if defined(OPENSSL_THREADS)
     if (!TEST_ptr(rp->gm = ossl_crypto_mutex_new()))
         goto err;
+    if (!TEST_ptr(rp->time_m = ossl_crypto_mutex_new()))
+        goto err;
 #endif

     if (!TEST_ptr(rp->objs = lh_RADIX_OBJ_new(RADIX_OBJ_hash, RADIX_OBJ_cmp)))
@@ -196,12 +201,15 @@ static int RADIX_PROCESS_init(RADIX_PROCESS *rp, size_t node_idx, size_t process
     rp->process_idx = process_idx;
     rp->done_join_all_threads = 0;
     rp->next_thread_idx = 0;
+    rp->base_time = ossl_time_now();
+    rp->time_slip = ossl_time_zero();
     return 1;

 err:
     lh_RADIX_OBJ_free(rp->objs);
     rp->objs = NULL;
     ossl_crypto_mutex_free(&rp->gm);
+    ossl_crypto_mutex_free(&rp->time_m);
     return 0;
 }

@@ -456,6 +464,7 @@ static void RADIX_PROCESS_cleanup(RADIX_PROCESS *rp)
     BIO_free_all(rp->keylog_out);
     rp->keylog_out = NULL;
     ossl_crypto_mutex_free(&rp->gm);
+    ossl_crypto_mutex_free(&rp->time_m);
 }

 static RADIX_OBJ *RADIX_PROCESS_get_obj(RADIX_PROCESS *rp, const char *name)
@@ -681,18 +690,23 @@ static OSSL_TIME get_time(void *arg)
 {
     OSSL_TIME time_slip;

-    ossl_crypto_mutex_lock(RP()->gm);
+    ossl_crypto_mutex_lock(RP()->time_m);
     time_slip = RP()->time_slip;
-    ossl_crypto_mutex_unlock(RP()->gm);
+    ossl_crypto_mutex_unlock(RP()->time_m);

-    return ossl_time_add(ossl_time_now(), time_slip);
+    return ossl_time_add(RP()->base_time, time_slip);
 }

-ossl_unused static void radix_skip_time(OSSL_TIME t)
+static OSSL_TIME terp_now(void *arg)
 {
-    ossl_crypto_mutex_lock(RP()->gm);
+    return ossl_time_now();
+}
+
+static void radix_skip_time(OSSL_TIME t)
+{
+    ossl_crypto_mutex_lock(RP()->time_m);
     RP()->time_slip = ossl_time_add(RP()->time_slip, t);
-    ossl_crypto_mutex_unlock(RP()->gm);
+    ossl_crypto_mutex_unlock(RP()->time_m);
 }

 static void per_op_tick_obj(RADIX_OBJ *obj)
@@ -705,14 +719,17 @@ static void per_op_tick_obj(RADIX_OBJ *obj)

 static int do_per_op(TERP *terp, void *arg)
 {
+    radix_skip_time(ossl_ms2time(1));
     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->now_cb = terp_now;
     cfg->per_op_cb = do_per_op;
+
+    cfg->max_execution_time = ossl_ms2time(60000);
     return 1;
 }

diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c
index f022907596..ed129d0ff7 100644
--- a/test/radix/quic_ops.c
+++ b/test/radix/quic_ops.c
@@ -204,6 +204,11 @@ DEF_FUNC(hf_new_ssl)
     if (!is_domain && !TEST_true(ssl_attach_bio_dgram(ssl, 0, NULL)))
         goto err;

+    if (!TEST_true(ossl_quic_set_override_now_cb(ssl, get_time, NULL))) {
+        SSL_free(ssl);
+        goto err;
+    }
+
     if (!TEST_true(RADIX_PROCESS_set_ssl(RP(), name, ssl))) {
         SSL_free(ssl);
         goto err;
@@ -342,6 +347,7 @@ DEF_FUNC(hf_accept_conn)
         SSL_free(conn);
         goto err;
     }
+    radix_activate_obj(RADIX_PROCESS_get_obj(RP(), conn_name));

     ok = 1;
 err: