Commit 8c8c85cdc4 for openssl.org
commit 8c8c85cdc4470140c6b918d85ab45a6a0ad9547a
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Wed Oct 14 18:37:37 2026 +0900
test: make RADIX QUIC objects nonblocking by default
RADIX drives both endpoints and advances its simulated clock from one
interpreter. Accepted server connections inherit blocking mode from their
listeners, even though the underlying UDP sockets are nonblocking.
Extra handshake retries advance fake time and can inflate the initial
RTT estimates. In the one-way key update loops the server's estimate can
stay high while later samples reduce the client's PTO. The server can
then retain its previous receive keys for longer than the client waits
before its next update. Packets starting that update are then discarded,
as permitted by RFC 9001 section 6.5. A blocking server read prevents the
interpreter from driving client retransmission or advancing the clock,
so this recoverable discard becomes a hang. The interpreter's 60-second
deadline cannot interrupt the call, leaving the process watchdog to abort.
External UDP loss is not required to reproduce the failure.
Set clients and listeners to nonblocking mode at construction, including
listeners created from a domain and the custom listener constructors.
Accepted connections and their streams inherit this setting. The client
setting makes the construction policy consistent before OP_CONNECT_WAIT,
which also sets its mode. Retain the domain blocking capability needed
by SSL_poll and document how tests can explicitly enable blocking I/O.
Add a regression using existing opcodes to delay an ACK, increase the
server's PTO and require recovery across successive client key updates.
Cover both listener creation paths. Check listener and client modes before
connection establishment and the accepted server's mode, so a recurrence
fails promptly instead of waiting for the process watchdog.
Fixes #33206.
Assisted-by: Codex:gpt-6-astra
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Nikola Pajkovsky <nikolap@openssl.org>
Merge-date: Fri Oct 23 12:40:35 2026
Merged-from: https://github.com/openssl/openssl/pull/33213
diff --git a/test/radix/README.md b/test/radix/README.md
index f79a4d4230..a1bf133a60 100644
--- a/test/radix/README.md
+++ b/test/radix/README.md
@@ -95,6 +95,21 @@ The RADIX test features the following improvements relative to the
- An explicit `OP_END` is no longer needed.
+Blocking operations
+-------------------
+
+QUIC clients and listeners are explicitly nonblocking by default. Accepted
+connections and their streams inherit this setting. The interpreter drives
+active endpoints and advances the simulated clock between operations and on
+retries, so an operation must return to allow its peer to make progress.
+
+Tests which require blocking operations must enable them explicitly with
+`SSL_set_blocking_mode` and provide independent progress for the peer and the
+simulated clock while the call waits. `OP_CONNECT_WAIT` always sets the client
+to nonblocking mode, so enable blocking I/O after that operation. The domain
+flags still enable blocking support, which is also needed by tests of
+`SSL_poll` with a nonzero timeout.
+
Architecture
------------
diff --git a/test/radix/quic_ops.c b/test/radix/quic_ops.c
index c16509ea89..4ed639830a 100644
--- a/test/radix/quic_ops.c
+++ b/test/radix/quic_ops.c
@@ -242,6 +242,12 @@ DEF_FUNC(hf_new_ssl)
&& !TEST_true(ssl_attach_bio_dgram(ssl, 0, NULL)))
goto err;
+ /* The interpreter must remain free to tick peers and advance the clock. */
+ if (!is_domain && !TEST_true(SSL_set_blocking_mode(ssl, 0))) {
+ SSL_free(ssl);
+ goto err;
+ }
+
if (!TEST_true(ossl_quic_set_override_now_cb(ssl, get_time, NULL))) {
SSL_free(ssl);
goto err;
@@ -275,8 +281,11 @@ DEF_FUNC(hf_new_ssl_listener_from)
if (!TEST_ptr(listener = SSL_new_listener_from(domain, flags)))
goto err;
- if (!TEST_true(ssl_attach_bio_dgram(listener, 0, NULL)))
+ if (!TEST_true(ssl_attach_bio_dgram(listener, 0, NULL))
+ || !TEST_true(SSL_set_blocking_mode(listener, 0))) {
+ SSL_free(listener);
goto err;
+ }
if (!TEST_true(RADIX_PROCESS_set_ssl(RP(), listener_name, listener))) {
SSL_free(listener);
@@ -633,8 +642,13 @@ DEF_FUNC(hf_write)
REQUIRE_SSL(ssl);
r = SSL_write_ex(ssl, buf, buf_len, &bytes_written);
+ if (!check_consistent_want(ssl, r))
+ goto err;
+
+ if (r == 0 && is_want(ssl, r))
+ F_SPIN_AGAIN();
+
if (!TEST_true(r)
- || !check_consistent_want(ssl, r)
|| !TEST_size_t_eq(bytes_written, buf_len))
goto err;
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index 830aac1b03..e0b673a1df 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -1438,7 +1438,8 @@ DEF_FUNC(new_listener)
if (!TEST_ptr(listener = SSL_new_listener(ctx, 0)))
goto err;
- if (!TEST_true(ssl_attach_bio_dgram(listener, 0, NULL))) {
+ if (!TEST_true(ssl_attach_bio_dgram(listener, 0, NULL))
+ || !TEST_true(SSL_set_blocking_mode(listener, 0))) {
SSL_free(listener);
goto err;
}
@@ -3932,6 +3933,7 @@ DEF_FUNC(new_listener_54)
if (!TEST_ptr(listener = SSL_new_listener(ctx, 0))
|| !TEST_true(ssl_attach_bio_dgram(listener, 0, NULL))
+ || !TEST_true(SSL_set_blocking_mode(listener, 0))
|| !TEST_true(RADIX_PROCESS_set_ssl(RP(), name, listener))) {
SSL_free(listener);
goto err;
@@ -6044,6 +6046,73 @@ DEF_SCRIPT(new_connid, "verify remote peer does not send excessive amount of NEW
OP_EXPECT_CONN_CLOSE_INFO(C, OSSL_QUIC_ERR_CONNECTION_ID_LIMIT_ERROR, 0, 0);
}
+DEF_FUNC(check_blocking_mode)
+{
+ int ok = 0;
+ SSL *ssl;
+ uint64_t blocking;
+
+ REQUIRE_SSL(ssl);
+ F_POP(blocking);
+
+ ok = TEST_int_eq(SSL_get_blocking_mode(ssl), (int)blocking);
+err:
+ return ok;
+}
+
+#define OP_CHECK_BLOCKING_MODE(name, blocking) \
+ (OP_SELECT_SSL(0, name), \
+ OP_PUSH_U64(blocking), \
+ OP_FUNC(check_blocking_mode))
+
+/*
+ * A packet which starts a key update while the peer still retains its old keys
+ * may be discarded (RFC 9001 s. 6.5), requiring retransmission. Delay an
+ * ACK to give the server a much larger PTO than the client, which makes the
+ * client's next key update arrive too early. The read waiting for that data
+ * must let the interpreter drive the client and advance the clock.
+ */
+DEF_SCRIPT(key_update_retained_keys,
+ "Key update recovery when the peer still retains old keys")
+{
+ size_t i, j;
+
+ for (i = 0; i < 2; ++i) {
+ if (i == 0) {
+ OP_NEW_SSL_L_LISTEN(L);
+ } else {
+ OP_CLEAR();
+ OP_NEW_SSL_D(Ds);
+ OP_NEW_SSL_L_FROM_LISTEN(Ds, L, 0);
+ }
+
+ OP_CHECK_BLOCKING_MODE(L, 0);
+ OP_NEW_SSL_C(C);
+ /* Check construction before OP_CONNECT_WAIT sets the mode again. */
+ OP_CHECK_BLOCKING_MODE(C, 0);
+ OP_SET_PEER_ADDR_FROM(C, L);
+ OP_CONNECT_WAIT(C);
+ OP_ACCEPT_CONN_WAIT(L, S, 0);
+ OP_CHECK_BLOCKING_MODE(S, 0);
+
+ OP_WRITE(C, "apple", 5);
+ OP_READ_EXPECT(S, "apple", 5);
+
+ OP_WRITE(S, "x", 1);
+ OP_SKIP_TIME(400);
+ OP_READ_EXPECT(C, "x", 1);
+
+ OP_OVERRIDE_KEY_UPDATE(C, 1);
+ for (j = 0; j < 200; ++j) {
+ OP_WRITE(C, "apple", 5);
+ OP_READ_EXPECT(S, "apple", 5);
+ OP_SKIP_TIME(8);
+ }
+
+ OP_CHECK_KEY_UPDATE_GE(C, 2);
+ }
+}
+
/*
* List of Test Scripts
* ============================================================================
@@ -6166,4 +6235,5 @@ static SCRIPT_INFO *const scripts[] = {
USE(script_106),
USE(new_connid),
USE(forbidden_frame_types),
+ USE(key_update_retained_keys),
};