Commit 524dd6c010 for openssl.org
commit 524dd6c010a6c92b532d7febe903c4f8634f22f1
Author: Matt Caswell <matt@openssl.foundation>
Date: Fri Jun 26 09:36:25 2026 +0100
test: add regression test for SSL_poll() abort_blocking handling
The race fixed by the previous commit (an item becoming ready in the
window between poll_translate() registering it for cross-thread
notification and the OS-level poll) is normally vanishingly narrow and
not practical to hit by timing alone in a test.
Add ossl_quic_poll_translate_test_step_cb, a test-only instrumentation
hook (always NULL in production use) that poll_translate() calls between
translating consecutive items. This lets a test deterministically make
one item ready while an earlier item's blocking section is still
entered, exercising the abort_blocking path on demand.
Add the poll_abort_blocking RADIX script/test, which uses the hook to
trigger this on two independent QUIC connections (so the second item's
reactor is provably distinct from the first's) and checks that
SSL_poll() reports success and that the first item's blocking-section
entry is correctly balanced afterwards.
Assisted-by: Claude:claude-sonnet-4-6
Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
Reviewed-by: Paul Yang <paulyang.inf@gmail.com>
MergeDate: Wed Jul 29 07:20:33 2026
(Merged from https://github.com/openssl/openssl/pull/31743)
diff --git a/ssl/rio/poll_builder.h b/ssl/rio/poll_builder.h
index 1fe13eacba..0fcd543b59 100644
--- a/ssl/rio/poll_builder.h
+++ b/ssl/rio/poll_builder.h
@@ -76,4 +76,17 @@ int ossl_rio_poll_builder_poll(RIO_POLL_BUILDER *rpb, OSSL_TIME deadline);
* it is currently not needed.
*/
+#ifndef OPENSSL_NO_QUIC
+/*
+ * Test instrumentation only. If set, poll_translate() (see poll_immediate.c)
+ * calls this with the index of each item immediately before translating it,
+ * once all earlier items (if any) have finished translation. This lets
+ * tests inject a readiness change into the gap between translation of
+ * consecutive items, in order to deterministically exercise the
+ * abort-blocking path. Always NULL in production use.
+ */
+extern void (*ossl_quic_poll_translate_test_step_cb)(size_t idx, void *arg);
+extern void *ossl_quic_poll_translate_test_step_cb_arg;
+#endif
+
#endif
diff --git a/ssl/rio/poll_immediate.c b/ssl/rio/poll_immediate.c
index edb39b2a6c..6807926f1a 100644
--- a/ssl/rio/poll_immediate.c
+++ b/ssl/rio/poll_immediate.c
@@ -49,6 +49,13 @@
} while (0)
#ifndef OPENSSL_NO_QUIC
+/*
+ * Test instrumentation only; see poll_builder.h. Always NULL in production
+ * use.
+ */
+void (*ossl_quic_poll_translate_test_step_cb)(size_t idx, void *arg) = NULL;
+void *ossl_quic_poll_translate_test_step_cb_arg = NULL;
+
static int poll_translate_ssl_quic(SSL *ssl,
QUIC_REACTOR_WAIT_CTX *wctx,
RIO_POLL_BUILDER *rpb,
@@ -216,6 +223,10 @@ static int poll_translate(SSL_POLL_ITEM *items,
for (i = 0; i < num_items; ++i) {
item = &ITEM_N(items, stride, i);
+ if (ossl_quic_poll_translate_test_step_cb != NULL)
+ ossl_quic_poll_translate_test_step_cb(i,
+ ossl_quic_poll_translate_test_step_cb_arg);
+
switch (item->desc.type) {
case BIO_POLL_DESCRIPTOR_TYPE_SSL:
ssl = item->desc.value.ssl;
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index 154926a16f..cdcb673d28 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -8,6 +8,8 @@
*/
#include "internal/quic_stream_map.h"
+#include "internal/quic_reactor.h"
+#include "../../ssl/rio/poll_builder.h"
#if defined(_AIX)
/*
@@ -383,6 +385,178 @@ DEF_SCRIPT(ssl_poll,
}
}
+/*
+ * Test: poll_abort_blocking
+ * -------------------------
+ *
+ * SSL_poll(), when it has to block, registers each item's QUIC connection
+ * for cross-thread notification one item at a time (poll_translate() in
+ * ssl/rio/poll_immediate.c). If an item turns out to already be ready right
+ * as it is being registered, translation is aborted so the readout loop can
+ * retry instead of actually blocking. This exercises that abort path and
+ * checks that:
+ *
+ * - SSL_poll() reports success rather than spuriously failing, and
+ * - any items already registered before the abort have their blocking
+ * section correctly left (i.e. no leak in the QUIC reactor's blocking
+ * waiter count).
+ *
+ * The race between an item being registered and becoming ready is normally
+ * vanishingly narrow, so we use ossl_quic_poll_translate_test_step_cb (test
+ * instrumentation only, see ssl/rio/poll_builder.h) to deterministically
+ * make the second item ready immediately before poll_translate() processes
+ * it, while the first item is still mid-registration.
+ */
+struct poll_abort_test_ctx {
+ SSL *peer_writer; /* write here to make target ready */
+ SSL *target;
+ uint64_t target_events;
+ size_t trigger_idx;
+ int made_ready; /* set by poll_abort_test_step_cb() on success */
+};
+
+static void poll_abort_test_step_cb(size_t idx, void *arg)
+{
+ struct poll_abort_test_ctx *ctx = arg;
+ uint64_t revents = 0;
+ int i;
+
+ if (idx != ctx->trigger_idx)
+ return;
+
+ if (SSL_write(ctx->peer_writer, "x", 1) != 1)
+ return;
+
+ /* Force the data through synchronously so target is ready by the time we return. */
+ for (i = 0; i < 1000; ++i) {
+ if (!ossl_quic_conn_poll_events(ctx->target, ctx->target_events,
+ /* do_tick = */ 1, &revents))
+ return;
+
+ if (revents != 0) {
+ ctx->made_ready = 1;
+ return;
+ }
+
+ OSSL_sleep(1);
+ }
+}
+
+DEF_FUNC(check_poll_abort_blocking)
+{
+ int ok = 0;
+ SSL *C, *C0, *Cb0, *Lb0;
+ QUIC_CHANNEL *ch0;
+ QUIC_REACTOR *rtor0;
+ SSL_POLL_ITEM items[2] = { 0 };
+ size_t result_count = SIZE_MAX, waiters_before, waiters_after;
+ struct poll_abort_test_ctx ctx;
+ const struct timeval z_timeout = { 0 };
+
+ /*
+ * C0 and Cb0 are streams of two independent client connections, and so
+ * belong to two independent QUIC_REACTORs. The bug being tested for does
+ * not actually require this: it reproduces just as well if all items
+ * share one reactor. What needs two reactors is poll_abort_test_step_cb()
+ * below, which forces Cb0 ready by ticking its reactor directly, on this
+ * thread, while C0's blocking section is still open. Doing that on C0's
+ * own (shared) reactor would deadlock: ossl_quic_reactor_tick() would see
+ * a nonzero cur_blocking_waiters left over from C0 and call
+ * rtor_notify_other_threads(), which waits on a condvar for some *other*
+ * thread to clear the notifier signal - a thread that doesn't exist here.
+ * Using Cb0's own, still-untouched reactor keeps that tick a no-op.
+ */
+ REQUIRE_SSL_4(C, C0, Cb0, Lb0);
+
+ items[0].desc = SSL_as_poll_descriptor(C0);
+ items[0].events = SSL_POLL_EVENT_R;
+ items[1].desc = SSL_as_poll_descriptor(Cb0);
+ items[1].events = SSL_POLL_EVENT_R;
+
+ /* Sanity check: nothing ready yet, so SSL_poll() will need to block. */
+ if (!TEST_true(SSL_poll(items, OSSL_NELEM(items), sizeof(SSL_POLL_ITEM),
+ &z_timeout, 0, &result_count))
+ || !TEST_size_t_eq(result_count, 0))
+ goto err;
+
+ if (!TEST_ptr(ch0 = ossl_quic_conn_get_channel(C)))
+ goto err;
+ rtor0 = ossl_quic_channel_get_reactor(ch0);
+ waiters_before = rtor0->cur_blocking_waiters;
+
+ ctx.peer_writer = Lb0;
+ ctx.target = Cb0;
+ ctx.target_events = items[1].events;
+ ctx.trigger_idx = 1;
+ ctx.made_ready = 0;
+
+ ossl_quic_poll_translate_test_step_cb_arg = &ctx;
+ ossl_quic_poll_translate_test_step_cb = poll_abort_test_step_cb;
+
+ result_count = SIZE_MAX;
+ /*
+ * No timeout: if the abort_blocking case were instead to actually block,
+ * this call would hang forever rather than fail fast.
+ */
+ ok = TEST_true(SSL_poll(items, OSSL_NELEM(items), sizeof(SSL_POLL_ITEM),
+ NULL, 0, &result_count));
+
+ ossl_quic_poll_translate_test_step_cb = NULL;
+ ossl_quic_poll_translate_test_step_cb_arg = NULL;
+
+ if (!ok)
+ goto err;
+
+ ok = 0;
+ if (!TEST_true(ctx.made_ready)
+ || !TEST_size_t_ge(result_count, 1)
+ || !TEST_true((items[1].revents & SSL_POLL_EVENT_R) != 0))
+ goto err;
+
+ /* The first item's blocking-section entry must have been balanced. */
+ waiters_after = rtor0->cur_blocking_waiters;
+ if (!TEST_size_t_eq(waiters_after, waiters_before))
+ goto err;
+
+ ok = 1;
+err:
+ ossl_quic_poll_translate_test_step_cb = NULL;
+ ossl_quic_poll_translate_test_step_cb_arg = NULL;
+ return ok;
+}
+
+DEF_SCRIPT(poll_abort_blocking,
+ "test that SSL_poll() correctly handles an item becoming ready while blocking is being set up")
+{
+ OP_SIMPLE_PAIR_CONN_ND();
+
+ OP_NEW_STREAM(C, C0, 0);
+ OP_WRITE_B(C0, "probe0");
+
+ OP_ACCEPT_CONN_WAIT1_ND(L, La, 0);
+ OP_ACCEPT_STREAM_WAIT(La, La0, 0);
+ OP_READ_EXPECT_B(La0, "probe0");
+
+ /* A second, independent client connection to the same listener. */
+ OP_NEW_SSL_C(Cb);
+ OP_SET_PEER_ADDR_FROM(Cb, L);
+ OP_CONNECT_WAIT(Cb);
+ OP_SET_DEFAULT_STREAM_MODE(Cb, SSL_DEFAULT_STREAM_MODE_NONE);
+
+ OP_NEW_STREAM(Cb, Cb0, 0);
+ OP_WRITE_B(Cb0, "probe1");
+
+ OP_ACCEPT_CONN_WAIT1_ND(L, Lb, 0);
+ OP_ACCEPT_STREAM_WAIT(Lb, Lb0, 0);
+ OP_READ_EXPECT_B(Lb0, "probe1");
+
+ OP_SELECT_SSL(0, C);
+ OP_SELECT_SSL(1, C0);
+ OP_SELECT_SSL(2, Cb0);
+ OP_SELECT_SSL(3, Lb0);
+ OP_FUNC(check_poll_abort_blocking);
+}
+
DEF_FUNC(check_writeable)
{
int ok = 0;
@@ -1807,6 +1981,7 @@ static SCRIPT_INFO *const scripts[] = {
USE(simple_conn),
USE(simple_thread),
USE(ssl_poll),
+ USE(poll_abort_blocking),
USE(check_cwm),
USE(check_pc_flood),
USE(check_ctx_cbks),