From: Jakub Zelenka Date: Fri, 10 Jul 2026 18:53:16 +0000 (+0200) Subject: quic-radix: add thread-assisted idle keepalive test X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=2d2363ee97be027fec500eb9652de67bd3be8c81;p=thirdparty%2Fopenssl.git quic-radix: add thread-assisted idle keepalive test Add a radix vignette covering thread-assisted mode: with the client's per-op ticking disabled, only its assist thread can act on the connection, so skipping the virtual time past the idle timeout keeps the server up only if the assist thread keeps sending keepalives. This migrates the thread-assisted idle coverage away from the QUIC_TSERVER-based test. The client is created with OSSL_QUIC_client_thread_method() so the method itself stays covered as well. The client and listener are linked with an in-memory datagram BIO pair instead of real UDP sockets, so keepalive delivery cannot race the virtual clock in the OS UDP path. New ops: hf_set_tick_active (enable/disable per-op ticking), hf_skip_time_wait (skip virtual time, wake the assist thread and wait for it to catch up), hf_expect_connected, and hf_link_dgram_pair. Assisted-by: Claude:claude-opus-4-8 Reviewed-by: Saša Nedvědický Reviewed-by: Andrew Dinh MergeDate: Mon Aug 3 02:12:44 2026 (Merged from https://github.com/openssl/openssl/pull/31918) --- diff --git a/test/radix/quic_bindings.c b/test/radix/quic_bindings.c index de066deb460..f31eb103a72 100644 --- a/test/radix/quic_bindings.c +++ b/test/radix/quic_bindings.c @@ -13,6 +13,7 @@ #include "internal/quic_channel.h" #include "internal/quic_ssl.h" #include "internal/quic_error.h" +#include "internal/quic_thread_assist.h" /* * RADIX 6D QUIC Test Framework diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c index f7f11bd3b94..25bcd797bfe 100644 --- a/test/radix/quic_ops.c +++ b/test/radix/quic_ops.c @@ -138,6 +138,18 @@ err: return ok; } +/* Attaches bio as both rbio and wbio, consuming the caller's reference. */ +static int ssl_attach_bio(SSL *ssl, BIO *bio) +{ + SSL_set0_rbio(ssl, bio); + if (!TEST_true(BIO_up_ref(bio))) + return 0; + + SSL_set0_wbio(ssl, bio); + + return 1; +} + static int ssl_attach_bio_dgram(SSL *ssl, uint16_t local_port, uint16_t *actual_port) { @@ -152,13 +164,7 @@ static int ssl_attach_bio_dgram(SSL *ssl, return 0; } - SSL_set0_rbio(ssl, bio); - if (!TEST_true(BIO_up_ref(bio))) - return 0; - - SSL_set0_wbio(ssl, bio); - - return 1; + return ssl_attach_bio(ssl, bio); } DEF_FUNC(hf_new_ssl) @@ -169,22 +175,51 @@ DEF_FUNC(hf_new_ssl) const SSL_METHOD *method; SSL *ssl; uint64_t flags; - int is_server, is_domain; + int is_server, is_domain, is_ta, is_no_bio; F_POP2(name, flags); is_domain = ((flags & 2) != 0); is_server = ((flags & 1) != 0); + is_ta = ((flags & 4) != 0); + is_no_bio = ((flags & 8) != 0); + + if (is_server) + method = OSSL_QUIC_server_method(); + else if (is_ta) + method = OSSL_QUIC_client_thread_method(); + else + method = OSSL_QUIC_client_method(); - method = is_server ? OSSL_QUIC_server_method() : OSSL_QUIC_client_method(); if (!TEST_ptr(ctx = SSL_CTX_new(method))) goto err; +#if defined(OPENSSL_NO_QUIC_THREAD_ASSIST) || !defined(OPENSSL_THREADS) + if (is_ta) { + TEST_skip("thread assisted mode not available"); + F_SKIP_REST(); + } +#endif + #if defined(OPENSSL_THREADS) - if (!TEST_true(SSL_CTX_set_domain_flags(ctx, - SSL_DOMAIN_FLAG_MULTI_THREAD - | SSL_DOMAIN_FLAG_BLOCKING))) + if (is_ta) { + uint64_t domain_flags = 0; + + /* + * Rely on the OSSL_QUIC_client_thread_method() domain flag defaults + * rather than setting them so the method's defaulting stays covered. + */ + if (!TEST_true(SSL_CTX_get_domain_flags(ctx, &domain_flags)) + || !TEST_uint64_t_eq(domain_flags, + SSL_DOMAIN_FLAG_MULTI_THREAD + | SSL_DOMAIN_FLAG_THREAD_ASSISTED + | SSL_DOMAIN_FLAG_BLOCKING)) + goto err; + } else if (!TEST_true(SSL_CTX_set_domain_flags(ctx, + SSL_DOMAIN_FLAG_MULTI_THREAD + | SSL_DOMAIN_FLAG_BLOCKING))) { goto err; + } #endif if (!TEST_true(ssl_ctx_configure(ctx, is_server))) @@ -202,7 +237,8 @@ DEF_FUNC(hf_new_ssl) goto err; } - if (!is_domain && !TEST_true(ssl_attach_bio_dgram(ssl, 0, NULL))) + if (!is_domain && !is_no_bio + && !TEST_true(ssl_attach_bio_dgram(ssl, 0, NULL))) goto err; if (!TEST_true(ossl_quic_set_override_now_cb(ssl, get_time, NULL))) { @@ -911,6 +947,62 @@ err: return ok; } +/* + * Link a client and a listener with an in-memory datagram BIO pair. Fake-time + * tests need this: a datagram sitting in the OS UDP path while fake time skips + * ahead could arrive only after a deadline it preceded in fake time. + */ +DEF_FUNC(hf_link_dgram_pair) +{ + int ok = 0; + SSL *c_ssl, *l_ssl; + BIO *c_bio = NULL, *l_bio = NULL; + BIO_ADDR *addr = NULL; + struct in_addr ina; + + REQUIRE_SSL_2(c_ssl, l_ssl); + + if (!TEST_true(BIO_new_bio_dgram_pair(&c_bio, 0, &l_bio, 0))) + goto err; + + if (!TEST_true(BIO_dgram_set_caps(c_bio, BIO_DGRAM_CAP_HANDLES_DST_ADDR)) + || !TEST_true(BIO_dgram_set_caps(l_bio, + BIO_DGRAM_CAP_HANDLES_DST_ADDR))) + goto err; + + ina.s_addr = htonl(INADDR_LOOPBACK); + if (!TEST_ptr(addr = BIO_ADDR_new()) + || !TEST_true(BIO_ADDR_rawmake(addr, AF_INET, &ina, sizeof(ina), 0))) + goto err; + + /* There are no real ports; a stable dummy address is all that is needed. */ + if (!TEST_true(SSL_set1_initial_peer_addr(c_ssl, addr))) + goto err; + + if (!TEST_true(BIO_dgram_set0_local_addr(c_bio, addr))) + goto err; + addr = NULL; + + if (!ssl_attach_bio(c_ssl, c_bio)) { + c_bio = NULL; + goto err; + } + c_bio = NULL; + + if (!ssl_attach_bio(l_ssl, l_bio)) { + l_bio = NULL; + goto err; + } + l_bio = NULL; + + ok = 1; +err: + BIO_free(c_bio); + BIO_free(l_bio); + BIO_ADDR_free(addr); + return ok; +} + DEF_FUNC(hf_set_peer_addr_from) { int ok = 0; @@ -966,6 +1058,80 @@ err: return ok; } +DEF_FUNC(hf_set_tick_active) +{ + int ok = 0; + uint64_t active; + const char *name; + RADIX_OBJ *obj; + + F_POP2(name, active); + if (!TEST_ptr(obj = RADIX_PROCESS_get_obj(RP(), name))) + goto err; + + obj->active = (active != 0); + ok = 1; +err: + return ok; +} + +/* + * Skip fake time and wait for the assist thread to catch up. It waits on real + * time internally, so wake it and spin until the event timeout is back in the + * future, meaning everything due up to now (any keepalive) has been serviced. + */ +DEF_FUNC(hf_skip_time_wait) +{ + int ok = 0; + uint64_t ms; + SSL *ssl; + struct timeval tv; + int is_infinite; + + REQUIRE_SSL(ssl); + F_POP(ms); + + if (RT()->scratch0 == 0) { + /* Skip only once; spin re-entries pass through here. */ + radix_skip_time(ossl_ms2time(ms)); + RT()->scratch0 = 1; + } + + ossl_quic_conn_force_assist_thread_wake(ssl); + + if (!TEST_true(SSL_get_event_timeout(ssl, &tv, &is_infinite))) + goto err; + + /* {0,0} (subtract saturates) means an event is still pending. */ + if (!is_infinite && tv.tv_sec == 0 && tv.tv_usec == 0) { + OSSL_sleep(1); /* Yield so the assist thread can run. */ + F_SPIN_AGAIN(); + } + + RT()->scratch0 = 0; /* done; not reset at err, as spins pass through it */ + ok = 1; +err: + return ok; +} + +DEF_FUNC(hf_expect_connected) +{ + int ok = 0; + SSL *ssl; + QUIC_CHANNEL *ch; + + REQUIRE_SSL(ssl); + if (!TEST_ptr(ch = ossl_quic_conn_get_channel(ssl))) + goto err; + + if (!TEST_true(ossl_quic_channel_is_active(ch))) + goto err; + + ok = 1; +err: + return ok; +} + DEF_FUNC(hf_override_key_update) { int ok = 0; @@ -1526,6 +1692,42 @@ err: (OP_PUSH_U64(ms), \ OP_FUNC(hf_sleep)) +/* Thread-assisted client, no socket (link a BIO pair instead). */ +#define OP_NEW_SSL_C_TA_MEM(name) \ + (OP_PUSH_PZ(#name), \ + OP_PUSH_U64(4 | 8), \ + OP_FUNC(hf_new_ssl)) + +/* Listener, no socket (link a BIO pair instead). */ +#define OP_NEW_SSL_L_MEM(name) \ + (OP_PUSH_PZ(#name), \ + OP_PUSH_U64(1 | 8), \ + OP_FUNC(hf_new_ssl)) + +#define OP_LINK_DGRAM_PAIR(client_name, listener_name) \ + (OP_SELECT_SSL(0, client_name), \ + OP_SELECT_SSL(1, listener_name), \ + OP_FUNC(hf_link_dgram_pair)) + +#define OP_TICK_DISABLE(name) \ + (OP_PUSH_PZ(#name), \ + OP_PUSH_U64(0), \ + OP_FUNC(hf_set_tick_active)) + +#define OP_TICK_ENABLE(name) \ + (OP_PUSH_PZ(#name), \ + OP_PUSH_U64(1), \ + OP_FUNC(hf_set_tick_active)) + +#define OP_SKIP_TIME_WAIT(name, ms) \ + (OP_SELECT_SSL(0, name), \ + OP_PUSH_U64(ms), \ + OP_FUNC(hf_skip_time_wait)) + +#define OP_EXPECT_CONNECTED(name) \ + (OP_SELECT_SSL(0, name), \ + OP_FUNC(hf_expect_connected)) + #define OP_OVERRIDE_KEY_UPDATE(name, threshold) \ (OP_SELECT_SSL(0, name), \ OP_PUSH_U64(threshold), \ diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c index fa96845ea2d..1945480e984 100644 --- a/test/radix/quic_tests.c +++ b/test/radix/quic_tests.c @@ -946,6 +946,39 @@ DEF_SCRIPT(check_ctx_cbks, "Check new_pending and client_hello callbacks") OP_FUNC(check_pending); } +/* + * With client ticking disabled only its assist thread can act, so skipping fake + * time past the 30s idle timeout keeps the server up only if the assist thread + * keeps sending keepalives. + */ +DEF_SCRIPT(check_thread_assisted_idle, + "thread-assisted mode keeps an idle connection alive") +{ + size_t i; + + OP_NEW_SSL_L_MEM(L); + OP_NEW_SSL_C_TA_MEM(C); + OP_LINK_DGRAM_PAIR(C, L); + OP_LISTEN(L); + OP_CONNECT_WAIT(C); + + OP_ACCEPT_CONN_WAIT(L, Sa, 0); + OP_ACCEPT_CONN_NONE(L); + + OP_WRITE_B(C, "apple"); + OP_READ_EXPECT_B(Sa, "apple"); + + OP_TICK_DISABLE(C); + + /* Step well below the keepalive interval so due PINGs can be serviced. */ + for (i = 0; i < 40; ++i) { + OP_SKIP_TIME_WAIT(C, 1000); + OP_EXPECT_CONNECTED(Sa); + } + + OP_TICK_ENABLE(C); +} + DEF_FUNC(check_stream_reset_5) { int ok = 0; @@ -2437,6 +2470,7 @@ static SCRIPT_INFO *const scripts[] = { USE(check_cwm), USE(check_pc_flood), USE(check_ctx_cbks), + USE(check_thread_assisted_idle), USE(script_5), USE(script_6), USE(script_7),