Commit 9682b1123a for openssl.org
commit 9682b1123accf8f6bf058a402bf7c8be0be5b5d2
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Fri Jul 31 18:01:15 2026 +0900
Fix SSL_poll() multi-reactor cleanup deadlock
SSL_poll() cleaned up QUIC reactor wait registrations in item-array
order. Leaving a reactor can block while its notifier is signalled, so
two callers spanning the same reactors could each wait on one reactor
while still registered on another. The translation abort path has the
same ordering problem.
Split QUIC leave into two phases: deregister from every QUIC reactor,
then wait for any notification cycles still pending. For QUIC-only poll
sets this removes every waiter registration before any notifier wait.
DTLS listener cleanup is unchanged. Guard the QUIC drain call for
no-quic builds.
Phase 2 also prevents an immediate re-registration from keeping the
notifier signalled. Capture notify_generation during deregistration so a
wait is bounded to the observed cycle. Apply the same generation bound
to the combined single-reactor leave operation.
Exercise real SSL_poll cleanup on two reactors in both the post-poll and
translation-abort paths, including repeated items sharing a reactor.
Check that a delayed phase-2 waiter completes while a later notification
cycle remains signalled.
Fixes #32130
Assisted-by: OpenCode:GLM-5.2
Assisted-by: Codex:gpt-5.6-sol
Assisted-by: Codex:gpt-6-astra
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Neil Horman <nhorman@openssl.org>
Merge-date: Tue Oct 20 12:29:05 2026
Merged-from: https://github.com/openssl/openssl/pull/32137
diff --git a/include/internal/quic_reactor.h b/include/internal/quic_reactor.h
index dffc37d754..3dfeb895eb 100644
--- a/include/internal/quic_reactor.h
+++ b/include/internal/quic_reactor.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2022-2026 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
@@ -139,6 +139,12 @@ struct quic_reactor_st {
/* 1 if a block_until_pred call has put the notifier in the signalled state. */
unsigned int signalled_notifier : 1;
+
+ /*
+ * Bumped each time the notifier is drained. Lets a phase-2 waiter identify
+ * the signal cycle it participated in (see two-phase leave below).
+ */
+ uint64_t notify_generation;
};
/* Create an OS notifier? */
@@ -251,11 +257,15 @@ int ossl_quic_reactor_block_until_pred(QUIC_REACTOR *rtor,
* via the notifier. The reactor mutex must be held while calling these
* functions.
*
- * The number of 'active' calls to these functions (i.e., the number of enter
- * calls which have yet to be matched with a subsequent leave call) must *at all
- * times* equal the number of threads blocking on the reactor. In other words, a
- * single thread is not permitted to use these functions "recursively". Failing
- * to adhere to this rule will result in deadlock.
+ * An active registration is an enter call which has not yet been matched by a
+ * subsequent deregister or leave call. A thread may have at most one active
+ * registration for a given reactor; recursive registration will result in
+ * deadlock. A thread which has deregistered may still wait for the notifier in
+ * phase 2, but is no longer counted as an active registration.
+ *
+ * A thread with an active registration on any reactor must not tick a reactor.
+ * A tick can block while notifying other registered threads, which would
+ * prevent the ticking thread from deregistering and can result in deadlock.
*
* This means that if a caller has the concept of multiple concurrent blocking
* calls on the same thread on the same reactor (which may occur in some
@@ -279,6 +289,23 @@ int ossl_quic_reactor_block_until_pred(QUIC_REACTOR *rtor,
void ossl_quic_reactor_enter_blocking_section(QUIC_REACTOR *rtor);
void ossl_quic_reactor_leave_blocking_section(QUIC_REACTOR *rtor);
+/*
+ * Two-phase leave for callers registered with multiple reactors. First call
+ * deregister_blocking_section() for every reactor; only then call
+ * wait_for_notifier_clear() for each deregistration that returned 1. This
+ * avoids waiting on one reactor while still registered with another.
+ *
+ * deregister_blocking_section() never blocks, requires the reactor mutex, and
+ * stores the signal generation to await in *gen. wait_for_notifier_clear()
+ * acquires the mutex itself and waits only for that generation. Single-reactor
+ * callers may continue to use leave_blocking_section(), whose combined wait is
+ * likewise bounded to the signal generation observed during deregistration.
+ */
+int ossl_quic_reactor_deregister_blocking_section(QUIC_REACTOR *rtor,
+ uint64_t *gen);
+void ossl_quic_reactor_wait_for_notifier_clear(QUIC_REACTOR *rtor,
+ uint64_t gen);
+
#endif
#endif
diff --git a/include/internal/quic_reactor_wait_ctx.h b/include/internal/quic_reactor_wait_ctx.h
index 8adc94d161..b20bd5c8bc 100644
--- a/include/internal/quic_reactor_wait_ctx.h
+++ b/include/internal/quic_reactor_wait_ctx.h
@@ -1,5 +1,5 @@
/*
- * Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2024-2026 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
@@ -56,12 +56,14 @@
* QUIC_REACTOR_WAIT_CTX before commencing a blocking operation, and then calls
* ossl_quic_reactor_wait_ctx_enter() whenever encountering a reactor involved
* in the imminent blocking operation. Later it must ensure it calls
- * ossl_quic_reactor_wait_ctx_leave() the same number of times for each reactor.
- * enter() and leave() may be called multiple times for the same reactor and
- * wait context so long as the number of calls is balanced. The last leave()
- * call for a given thread's wait context *and a given reactor* causes that
- * reactor to do the inter-thread notification housekeeping needed for
- * multithreaded blocking to work correctly.
+ * ossl_quic_reactor_wait_ctx_deregister() the same number of times for
+ * each reactor. enter() and deregister() may be called multiple times for
+ * the same reactor and wait context so long as the number of calls is balanced.
+ * The last deregister() call for a given thread's wait context *and a given
+ * reactor* begins the reactor's inter-thread notification housekeeping
+ * (deregistration, which never blocks); ossl_quic_reactor_wait_ctx_drain()
+ * completes it. Both phases are needed for multithreaded blocking to work
+ * correctly.
*
* The gist is that a simple reactor-level counter of active concurrent blocking
* calls across all threads is not accurate and we need an accurate count of how
@@ -75,7 +77,8 @@
* (QUIC_REACTOR *) -> (outstanding call count)
*
* When the count for a reactor transitions from 0 to a nonzero value, or vice
- * versa, ossl_quic_reactor_(enter/leave)_blocking_section() is called once.
+ * versa, ossl_quic_reactor_enter_blocking_section() /
+ * ossl_quic_reactor_deregister_blocking_section() is called once.
*
* The internal implementation is based on linked lists as we expect the actual
* number of reactors involved in a given blocking operation to be very small,
@@ -97,19 +100,31 @@ void ossl_quic_reactor_wait_ctx_init(QUIC_REACTOR_WAIT_CTX *ctx);
int ossl_quic_reactor_wait_ctx_enter(QUIC_REACTOR_WAIT_CTX *ctx,
QUIC_REACTOR *rtor);
-/* Downrefs a given reactor. */
-void ossl_quic_reactor_wait_ctx_leave(QUIC_REACTOR_WAIT_CTX *ctx,
- QUIC_REACTOR *rtor);
-
/*
* Destroys a wait context. Must be called after calling init().
*
* Precondition: All prior calls to ossl_quic_reactor_wait_ctx_enter() must have
- * been balanced with corresponding leave() calls before calling this
- * (unchecked).
+ * been balanced with corresponding deregister() calls, and
+ * ossl_quic_reactor_wait_ctx_drain() must have run for any reactor left
+ * signalled, before calling this (unchecked).
*/
void ossl_quic_reactor_wait_ctx_cleanup(QUIC_REACTOR_WAIT_CTX *ctx);
+/*
+ * Two-phase leave support (see quic_reactor.h for the rationale and the
+ * deadlock it avoids). wait_ctx_deregister() is phase 1: it decrements
+ * the per-thread refcount and, on the 1->0 transition, deregisters from the
+ * reactor without blocking, recording whether phase 2 must wait and the
+ * generation to wait for. Caller must hold the reactor mutex.
+ * wait_ctx_drain() is phase 2: for every reactor phase 1 left signalled, it
+ * waits for that signal cycle to clear, acquiring each reactor's mutex
+ * internally.
+ */
+void ossl_quic_reactor_wait_ctx_deregister(QUIC_REACTOR_WAIT_CTX *ctx,
+ QUIC_REACTOR *rtor);
+
+void ossl_quic_reactor_wait_ctx_drain(QUIC_REACTOR_WAIT_CTX *ctx);
+
#endif
#endif
diff --git a/include/internal/quic_ssl.h b/include/internal/quic_ssl.h
index 75a0fc5103..f7e554a9f0 100644
--- a/include/internal/quic_ssl.h
+++ b/include/internal/quic_ssl.h
@@ -177,8 +177,14 @@ int ossl_quic_set_diag_title(SSL_CTX *ctx, const char *title);
int ossl_quic_conn_poll_events(SSL *ssl, uint64_t events, int do_tick,
uint64_t *revents);
int ossl_quic_get_notifier_fd(SSL *ssl);
+/*
+ * Blocking-section registration. Multi-reactor callers (e.g. SSL_poll) must
+ * use ossl_quic_deregister_blocking_section() (phase 1, non-blocking) followed
+ * by ossl_quic_reactor_wait_ctx_drain() (phase 2); see quic_reactor.h.
+ */
void ossl_quic_enter_blocking_section(SSL *ssl, QUIC_REACTOR_WAIT_CTX *wctx);
-void ossl_quic_leave_blocking_section(SSL *ssl, QUIC_REACTOR_WAIT_CTX *wctx);
+void ossl_quic_deregister_blocking_section(SSL *ssl,
+ QUIC_REACTOR_WAIT_CTX *wctx);
QUIC_PORT *ossl_quic_listener_get_port(SSL *s);
#endif
diff --git a/ssl/quic/quic_impl.c b/ssl/quic/quic_impl.c
index 6ea7734b09..a858a8c04a 100644
--- a/ssl/quic/quic_impl.c
+++ b/ssl/quic/quic_impl.c
@@ -6019,7 +6019,8 @@ void ossl_quic_enter_blocking_section(SSL *ssl, QUIC_REACTOR_WAIT_CTX *wctx)
}
QUIC_TAKES_LOCK
-void ossl_quic_leave_blocking_section(SSL *ssl, QUIC_REACTOR_WAIT_CTX *wctx)
+void ossl_quic_deregister_blocking_section(SSL *ssl,
+ QUIC_REACTOR_WAIT_CTX *wctx)
{
QCTX ctx;
QUIC_REACTOR *rtor;
@@ -6029,7 +6030,7 @@ void ossl_quic_leave_blocking_section(SSL *ssl, QUIC_REACTOR_WAIT_CTX *wctx)
qctx_lock(&ctx);
rtor = ossl_quic_obj_get0_reactor(ctx.obj);
- ossl_quic_reactor_wait_ctx_leave(wctx, rtor);
+ ossl_quic_reactor_wait_ctx_deregister(wctx, rtor);
qctx_unlock(&ctx);
}
diff --git a/ssl/quic/quic_reactor.c b/ssl/quic/quic_reactor.c
index c30bc3c595..b570821aa3 100644
--- a/ssl/quic/quic_reactor.c
+++ b/ssl/quic/quic_reactor.c
@@ -44,6 +44,7 @@ int ossl_quic_reactor_init(QUIC_REACTOR *rtor,
rtor->mutex = mutex;
rtor->cur_blocking_waiters = 0;
+ rtor->notify_generation = 0;
if ((flags & QUIC_REACTOR_FLAG_USE_NOTIFIER) != 0) {
if (!ossl_rio_notifier_init(&rtor->notifier))
@@ -608,25 +609,56 @@ void ossl_quic_reactor_enter_blocking_section(QUIC_REACTOR *rtor)
++rtor->cur_blocking_waiters;
}
-void ossl_quic_reactor_leave_blocking_section(QUIC_REACTOR *rtor)
+int ossl_quic_reactor_deregister_blocking_section(QUIC_REACTOR *rtor,
+ uint64_t *gen)
{
assert(rtor->cur_blocking_waiters > 0);
--rtor->cur_blocking_waiters;
- if (rtor->have_notifier && rtor->signalled_notifier) {
- if (rtor->cur_blocking_waiters == 0) {
- ossl_rio_notifier_unsignal(&rtor->notifier);
- rtor->signalled_notifier = 0;
+ /* The last waiter drains the notifier and advances its signal generation. */
+ if (rtor->have_notifier && rtor->signalled_notifier
+ && rtor->cur_blocking_waiters == 0) {
+ ossl_rio_notifier_unsignal(&rtor->notifier);
+ rtor->signalled_notifier = 0;
+ ++rtor->notify_generation;
+ ossl_crypto_condvar_broadcast(rtor->notifier_cv);
+ }
- /*
- * Release the other threads which have woken up (and possibly
- * rtor_notify_other_threads as well).
- */
- ossl_crypto_condvar_broadcast(rtor->notifier_cv);
- } else {
- /* We are not the last waiter out - so wait for that one. */
- while (rtor->signalled_notifier)
- ossl_crypto_condvar_wait(rtor->notifier_cv, rtor->mutex);
- }
+ /* Remaining waiters require phase 2 for the current signal generation. */
+ if (rtor->have_notifier && rtor->signalled_notifier) {
+ *gen = rtor->notify_generation;
+ return 1;
}
+
+ return 0;
+}
+
+/*
+ * Wait only for the signal cycle observed during deregistration.
+ * Precondition: reactor mutex held.
+ */
+static void rtor_wait_for_notifier_clear_locked(QUIC_REACTOR *rtor,
+ uint64_t gen)
+{
+ if (rtor->have_notifier)
+ while (rtor->signalled_notifier
+ && rtor->notify_generation == gen)
+ ossl_crypto_condvar_wait(rtor->notifier_cv, rtor->mutex);
+}
+
+void ossl_quic_reactor_wait_for_notifier_clear(QUIC_REACTOR *rtor,
+ uint64_t gen)
+{
+ ossl_crypto_mutex_lock(rtor->mutex);
+ rtor_wait_for_notifier_clear_locked(rtor, gen);
+ ossl_crypto_mutex_unlock(rtor->mutex);
+}
+
+void ossl_quic_reactor_leave_blocking_section(QUIC_REACTOR *rtor)
+{
+ uint64_t gen;
+
+ /* Caller holds the reactor mutex across both phases. */
+ if (ossl_quic_reactor_deregister_blocking_section(rtor, &gen))
+ rtor_wait_for_notifier_clear_locked(rtor, gen);
}
diff --git a/ssl/quic/quic_reactor_wait_ctx.c b/ssl/quic/quic_reactor_wait_ctx.c
index b805d70cbf..53cdda1541 100644
--- a/ssl/quic/quic_reactor_wait_ctx.c
+++ b/ssl/quic/quic_reactor_wait_ctx.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2024-2026 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
@@ -15,6 +15,9 @@ struct quic_reactor_wait_slot_st {
OSSL_LIST_MEMBER(quic_reactor_wait_slot, QUIC_REACTOR_WAIT_SLOT);
QUIC_REACTOR *rtor; /* primary key */
size_t blocking_count; /* datum */
+ /* Generation captured at the 1->0 deregistration, for the phase-2 wait. */
+ uint64_t notify_generation;
+ unsigned int need_notify_wait : 1; /* set when phase 2 must wait */
};
DEFINE_LIST_OF_IMPL(quic_reactor_wait_slot, QUIC_REACTOR_WAIT_SLOT);
@@ -30,14 +33,22 @@ static void slot_activate(QUIC_REACTOR_WAIT_SLOT *slot)
ossl_quic_reactor_enter_blocking_section(slot->rtor);
}
-static void slot_deactivate(QUIC_REACTOR_WAIT_SLOT *slot)
+/*
+ * Phase-1 deregistration: on the 1->0 transition, deregister from the reactor
+ * without blocking and capture whether phase 2 must wait (and the generation).
+ * Caller must hold the reactor mutex.
+ */
+static void slot_deactivate_deregister(QUIC_REACTOR_WAIT_SLOT *slot)
{
assert(slot->blocking_count > 0);
if (--slot->blocking_count > 0)
return;
- ossl_quic_reactor_leave_blocking_section(slot->rtor);
+ slot->need_notify_wait
+ = ossl_quic_reactor_deregister_blocking_section(slot->rtor,
+ &slot->notify_generation)
+ != 0;
}
int ossl_quic_reactor_wait_ctx_enter(QUIC_REACTOR_WAIT_CTX *ctx,
@@ -61,7 +72,7 @@ int ossl_quic_reactor_wait_ctx_enter(QUIC_REACTOR_WAIT_CTX *ctx,
return 1;
}
-void ossl_quic_reactor_wait_ctx_leave(QUIC_REACTOR_WAIT_CTX *ctx,
+void ossl_quic_reactor_wait_ctx_deregister(QUIC_REACTOR_WAIT_CTX *ctx,
QUIC_REACTOR *rtor)
{
QUIC_REACTOR_WAIT_SLOT *slot;
@@ -71,7 +82,30 @@ void ossl_quic_reactor_wait_ctx_leave(QUIC_REACTOR_WAIT_CTX *ctx,
break;
assert(slot != NULL);
- slot_deactivate(slot);
+ slot_deactivate_deregister(slot);
+}
+
+/*
+ * Phase 2: wait for every notifier phase 1 left signalled to clear. Because
+ * phase 1 has already decremented every slot's refcount to zero, the calling
+ * thread holds no waiter registration here and cannot be part of a
+ * cross-reactor deadlock. Each reactor's mutex is acquired internally.
+ */
+void ossl_quic_reactor_wait_ctx_drain(QUIC_REACTOR_WAIT_CTX *ctx)
+{
+ QUIC_REACTOR_WAIT_SLOT *slot;
+
+ OSSL_LIST_FOREACH (slot, quic_reactor_wait_slot, &ctx->slots) {
+ /* Invariant: phase 1 must have deregistered every slot by now. */
+ assert(slot->blocking_count == 0);
+
+ if (!slot->need_notify_wait)
+ continue;
+
+ ossl_quic_reactor_wait_for_notifier_clear(slot->rtor,
+ slot->notify_generation);
+ slot->need_notify_wait = 0;
+ }
}
void ossl_quic_reactor_wait_ctx_cleanup(QUIC_REACTOR_WAIT_CTX *ctx)
@@ -80,6 +114,7 @@ void ossl_quic_reactor_wait_ctx_cleanup(QUIC_REACTOR_WAIT_CTX *ctx)
OSSL_LIST_FOREACH_DELSAFE (slot, nslot, quic_reactor_wait_slot, &ctx->slots) {
assert(slot->blocking_count == 0);
+ assert(!slot->need_notify_wait);
OPENSSL_free(slot);
}
}
diff --git a/ssl/rio/poll_immediate.c b/ssl/rio/poll_immediate.c
index a0346aac8c..258a264082 100644
--- a/ssl/rio/poll_immediate.c
+++ b/ssl/rio/poll_immediate.c
@@ -148,15 +148,16 @@ static int poll_translate_ssl_quic(SSL *ssl,
* it is possible the object became ready after our initial
* poll_readout() call (before we determined that nothing was ready and
* we needed to block). We now need to do another readout, in which case
- * blocking is to be aborted.
+ * blocking is to be aborted. do_tick is 0 because ticking here would
+ * block on the notifier condvar while still registered.
*/
if (!ossl_quic_conn_poll_events(ssl, events, /*do_tick = */ 0, &revents)) {
- ossl_quic_leave_blocking_section(ssl, wctx);
+ ossl_quic_deregister_blocking_section(ssl, wctx);
return 0;
}
if (revents != 0) {
- ossl_quic_leave_blocking_section(ssl, wctx);
+ ossl_quic_deregister_blocking_section(ssl, wctx);
*abort_blocking = 1;
return 1;
}
@@ -165,11 +166,11 @@ static int poll_translate_ssl_quic(SSL *ssl,
return 1;
}
-static void postpoll_translation_cleanup_ssl_quic(SSL *ssl,
+static void postpoll_translation_deregister_ssl_quic(SSL *ssl,
QUIC_REACTOR_WAIT_CTX *wctx)
{
if (ossl_quic_get_notifier_fd(ssl) != -1)
- ossl_quic_leave_blocking_section(ssl, wctx);
+ ossl_quic_deregister_blocking_section(ssl, wctx);
}
#endif /* OPENSSL_NO_QUIC */
@@ -408,6 +409,11 @@ static void postpoll_translation_cleanup(SSL_POLL_ITEM *items,
SSL *ssl;
size_t i;
+ /*
+ * QUIC cleanup uses two phases: deregister each QUIC item here without
+ * waiting for its notifier, then drain the recorded cycles below. See
+ * quic_reactor.h for the QUIC leave-ordering invariant.
+ */
for (i = 0; i < num_items; ++i) {
item = &ITEM_N(items, stride, i);
@@ -422,7 +428,7 @@ static void postpoll_translation_cleanup(SSL_POLL_ITEM *items,
case SSL_TYPE_QUIC_LISTENER:
case SSL_TYPE_QUIC_CONNECTION:
case SSL_TYPE_QUIC_XSO:
- postpoll_translation_cleanup_ssl_quic(ssl, wctx);
+ postpoll_translation_deregister_ssl_quic(ssl, wctx);
break;
#endif
@@ -444,6 +450,14 @@ static void postpoll_translation_cleanup(SSL_POLL_ITEM *items,
break;
}
}
+
+ /*
+ * Phase 2 also prevents the retry loop from re-registering before the
+ * signal cycle observed by this blocking attempt has been drained.
+ */
+#ifndef OPENSSL_NO_QUIC
+ ossl_quic_reactor_wait_ctx_drain(wctx);
+#endif
}
static int poll_translate(SSL_POLL_ITEM *items,
@@ -579,9 +593,11 @@ static int poll_translate(SSL_POLL_ITEM *items,
out:
/*
- * On abort_blocking, the item which triggered the abort has already
- * balanced its own enter/leave of the blocking section (see
- * poll_translate_ssl_quic()); only items 0..i-1 still need cleanup here.
+ * poll_translate_ssl_quic() either leaves the triggering item unregistered
+ * or deregisters it before returning. SSL_get_event_timeout() failures
+ * increment i so the registered triggering item is included in the cleanup
+ * prefix. Phase 2 drains every reactor recorded in wctx, including one
+ * already deregistered by the triggering item.
*/
if (!ok || *abort_blocking)
postpoll_translation_cleanup(items, i, stride, wctx);
diff --git a/test/radix/quic_tests.c b/test/radix/quic_tests.c
index 5b6a84db38..830aac1b03 100644
--- a/test/radix/quic_tests.c
+++ b/test/radix/quic_tests.c
@@ -9,6 +9,10 @@
#include "internal/quic_stream_map.h"
#include "internal/quic_reactor.h"
+#include "internal/quic_reactor_wait_ctx.h"
+#include "internal/quic_channel.h"
+#include "internal/quic_ssl.h"
+#include "internal/time.h"
#include "../../ssl/rio/poll_builder.h"
#if defined(_AIX)
@@ -581,6 +585,489 @@ DEF_SCRIPT(poll_abort_blocking,
OP_FUNC(check_poll_abort_blocking);
}
+/*
+ * Test: poll_multi_reactor_cleanup
+ * --------------------------------
+ *
+ * Exercise normal and translation-abort cleanup with two workers, each
+ * polling two notifier-enabled reactors. Repeated items also check that
+ * registrations are aggregated by reactor within each call.
+ *
+ * The fourth item's translation hook is a barrier. In the normal case it is
+ * a NULL sentinel, reached after both reactors have been registered in
+ * opposite orders. In the abort case only the first reactor is registered
+ * at the barrier; the coordinator changes the fourth item's event mask to
+ * writable so its registration on the other reactor takes the abort path.
+ * Both variants must finish with no outstanding waiters or signals.
+ */
+
+#if defined(OPENSSL_THREADS)
+struct poll_multi_reactor_ctx {
+ CRYPTO_MUTEX *mu;
+ CRYPTO_CONDVAR *cv;
+ int arrived;
+ int release;
+ int done;
+ int abort_blocking;
+ int rc[2];
+ size_t result_count[2];
+};
+
+struct poll_cleanup_thread_ctx {
+ SSL *ssl0, *ssl1;
+ SSL_POLL_ITEM items[4];
+ size_t idx;
+ struct poll_multi_reactor_ctx *ctx;
+};
+
+static int poll_cleanup_wait(struct poll_multi_reactor_ctx *ctx,
+ int *counter, int expected)
+{
+ OSSL_TIME deadline = ossl_time_add(ossl_time_now(), ossl_seconds2time(10));
+ int ok;
+
+ ossl_crypto_mutex_lock(ctx->mu);
+ while (*counter < expected
+ && ossl_time_compare(ossl_time_now(), deadline) < 0)
+ ossl_crypto_condvar_wait_timeout(ctx->cv, ctx->mu, deadline);
+ ok = (*counter == expected);
+ ossl_crypto_mutex_unlock(ctx->mu);
+ return ok;
+}
+
+static void poll_cleanup_release(struct poll_multi_reactor_ctx *ctx, int stage)
+{
+ ossl_crypto_mutex_lock(ctx->mu);
+ ctx->release = stage;
+ ossl_crypto_condvar_broadcast(ctx->cv);
+ ossl_crypto_mutex_unlock(ctx->mu);
+}
+
+static void poll_cleanup_join(struct poll_multi_reactor_ctx *ctx,
+ CRYPTO_THREAD **threads, size_t num_threads)
+{
+ size_t i;
+
+ /* Bound error-path cleanup as well as the normal worker completion. */
+ if (!poll_cleanup_wait(ctx, &ctx->done, (int)num_threads))
+ OPENSSL_die("SSL_poll cleanup workers are wedged", __FILE__, __LINE__);
+
+ for (i = 0; i < num_threads; ++i) {
+ ossl_crypto_thread_native_join(threads[i], NULL);
+ ossl_crypto_thread_native_clean(threads[i]);
+ }
+}
+
+/*
+ * Arm the notifier exactly as rtor_notify_other_threads() does, but without
+ * its condvar wait, so both reactors can be put into the required state
+ * deterministically before the workers are released.
+ */
+static void poll_cleanup_signal_notifier(QUIC_REACTOR *rtor)
+{
+ ossl_crypto_mutex_lock(rtor->mutex);
+ if (rtor->have_notifier && !rtor->signalled_notifier
+ && rtor->cur_blocking_waiters > 0) {
+ ossl_rio_notifier_signal(&rtor->notifier);
+ rtor->signalled_notifier = 1;
+ }
+ ossl_crypto_mutex_unlock(rtor->mutex);
+}
+
+static void poll_cleanup_test_step_cb(size_t idx, void *arg)
+{
+ struct poll_multi_reactor_ctx *ctx = arg;
+
+ if (idx != 3)
+ return;
+
+ ossl_crypto_mutex_lock(ctx->mu);
+ if (!ctx->release) {
+ ++ctx->arrived;
+ ossl_crypto_condvar_broadcast(ctx->cv);
+ while (!ctx->release)
+ ossl_crypto_condvar_wait(ctx->cv, ctx->mu);
+ }
+ ossl_crypto_mutex_unlock(ctx->mu);
+}
+
+static CRYPTO_THREAD_RETVAL poll_cleanup_thread(void *arg)
+{
+ struct poll_cleanup_thread_ctx *d = arg;
+ struct poll_multi_reactor_ctx *ctx = d->ctx;
+ SSL_POLL_ITEM *items = d->items;
+ const struct timeval timeout = { 1, 0 };
+ size_t result_count = 0, i;
+ int rc;
+
+ items[0].desc = SSL_as_poll_descriptor(d->ssl0);
+ items[1].desc = SSL_as_poll_descriptor(ctx->abort_blocking ? d->ssl0 : d->ssl1);
+ items[2].desc = SSL_as_poll_descriptor(ctx->abort_blocking ? NULL : d->ssl0);
+ items[3].desc = SSL_as_poll_descriptor(ctx->abort_blocking ? d->ssl1 : NULL);
+ for (i = 0; i < OSSL_NELEM(d->items); ++i)
+ items[i].events = SSL_POLL_EVENT_R;
+
+ /* Do not tick while the other worker may be stopped at the test barrier. */
+ rc = SSL_poll(items, OSSL_NELEM(d->items), sizeof(SSL_POLL_ITEM),
+ &timeout, SSL_POLL_FLAG_NO_HANDLE_EVENTS, &result_count);
+
+ ossl_crypto_mutex_lock(ctx->mu);
+ ctx->rc[d->idx] = rc;
+ ctx->result_count[d->idx] = result_count;
+ ++ctx->done;
+ ossl_crypto_condvar_broadcast(ctx->cv);
+ ossl_crypto_mutex_unlock(ctx->mu);
+ return 0;
+}
+#endif
+
+DEF_FUNC(check_poll_multi_reactor_cleanup)
+{
+ int ok = 0;
+ uint64_t abort_blocking;
+#if !defined(OPENSSL_THREADS)
+ F_POP(abort_blocking);
+ TEST_skip("threading not supported, skipping");
+ F_SKIP_REST();
+err:
+ return ok;
+#else
+ SSL *stream_a0, *stream_a1, *stream_b0, *stream_b1, *conn;
+ QUIC_CHANNEL *ch;
+ QUIC_REACTOR *rtor[2];
+ CRYPTO_THREAD *thread[2] = { NULL, NULL };
+ size_t i, started = 0;
+ int state_ok;
+ struct poll_multi_reactor_ctx ctx = { 0 };
+ struct poll_cleanup_thread_ctx worker[2] = { 0 };
+
+ F_POP(abort_blocking);
+ REQUIRE_SSL_4(stream_a0, stream_a1, stream_b0, stream_b1);
+ ctx.abort_blocking = (abort_blocking != 0);
+
+ for (i = 0; i < 2; ++i)
+ if (!TEST_ptr(conn = SSL_get0_connection(i == 0 ? stream_a0
+ : stream_b0))
+ || !TEST_ptr(ch = ossl_quic_conn_get_channel(conn))
+ || !TEST_ptr(rtor[i] = ossl_quic_channel_get_reactor(ch)))
+ return 0;
+
+ if (rtor[0] == rtor[1]
+ || !rtor[0]->have_notifier
+ || !rtor[1]->have_notifier) {
+ TEST_skip("test requires two distinct notifier-enabled reactors");
+ F_SKIP_REST();
+ }
+
+ if ((ctx.mu = ossl_crypto_mutex_new()) == NULL
+ || (ctx.cv = ossl_crypto_condvar_new()) == NULL)
+ goto err;
+
+ worker[0].ssl0 = stream_a0;
+ worker[0].ssl1 = stream_b0;
+ worker[1].ssl0 = stream_b1;
+ worker[1].ssl1 = stream_a1;
+ for (i = 0; i < 2; ++i) {
+ worker[i].idx = i;
+ worker[i].ctx = &ctx;
+ }
+
+ ossl_quic_poll_translate_test_step_cb_arg = &ctx;
+ ossl_quic_poll_translate_test_step_cb = poll_cleanup_test_step_cb;
+
+ for (i = 0; i < 2; ++i) {
+ if ((thread[i] = ossl_crypto_thread_native_start(poll_cleanup_thread,
+ &worker[i], 1))
+ == NULL)
+ goto err_join;
+ ++started;
+ }
+
+ if (!TEST_true(poll_cleanup_wait(&ctx, &ctx.arrived, 2)))
+ goto err_join;
+
+ /* The barrier keeps the waiter counts stable until release. */
+ state_ok = 1;
+ for (i = 0; i < 2; ++i) {
+ ossl_crypto_mutex_lock(rtor[i]->mutex);
+ if (!TEST_size_t_eq(rtor[i]->cur_blocking_waiters,
+ ctx.abort_blocking ? 1 : 2))
+ state_ok = 0;
+ ossl_crypto_mutex_unlock(rtor[i]->mutex);
+ }
+ if (!state_ok)
+ goto err_join;
+
+ for (i = 0; i < 2; ++i)
+ poll_cleanup_signal_notifier(rtor[i]);
+
+ /* The workers are at the barrier and cannot read these items yet. */
+ if (ctx.abort_blocking)
+ for (i = 0; i < 2; ++i)
+ worker[i].items[3].events = SSL_POLL_EVENT_W;
+
+ poll_cleanup_release(&ctx, 1);
+
+ /* Do not let a cleanup regression hang the test process. */
+ if (!TEST_true(poll_cleanup_wait(&ctx, &ctx.done, 2)))
+ goto err_wedge;
+
+ ossl_crypto_mutex_lock(ctx.mu);
+ state_ok = 1;
+ for (i = 0; i < 2; ++i)
+ if (!TEST_int_eq(ctx.rc[i], 1)
+ || !TEST_size_t_eq(ctx.result_count[i], ctx.abort_blocking ? 1 : 0))
+ state_ok = 0;
+ ossl_crypto_mutex_unlock(ctx.mu);
+ if (!state_ok)
+ goto err_join;
+
+ state_ok = 1;
+ for (i = 0; i < 2; ++i) {
+ ossl_crypto_mutex_lock(rtor[i]->mutex);
+ if (!TEST_size_t_eq(rtor[i]->cur_blocking_waiters, 0))
+ state_ok = 0;
+ if (!TEST_int_eq(rtor[i]->signalled_notifier, 0))
+ state_ok = 0;
+ ossl_crypto_mutex_unlock(rtor[i]->mutex);
+ }
+ if (!state_ok)
+ goto err_join;
+
+ ok = 1;
+
+err_join:
+ /* Release any worker stopped at the registration barrier. */
+ poll_cleanup_release(&ctx, 1);
+ poll_cleanup_join(&ctx, thread, started);
+ goto err;
+
+err_wedge:
+ /* Normal cleanup would re-enter reactors held by the wedged workers. */
+ OPENSSL_die("SSL_poll cleanup workers are wedged", __FILE__, __LINE__);
+
+err:
+ ossl_quic_poll_translate_test_step_cb = NULL;
+ ossl_quic_poll_translate_test_step_cb_arg = NULL;
+ ossl_crypto_condvar_free(&ctx.cv);
+ ossl_crypto_mutex_free(&ctx.mu);
+ return ok;
+#endif
+}
+
+DEF_SCRIPT(poll_multi_reactor_cleanup,
+ "verify SSL_poll() multi-reactor two-phase cleanup does not deadlock")
+{
+ size_t i;
+
+ OP_SIMPLE_PAIR_CONN_ND();
+
+ OP_NEW_STREAM(C, C0, 0);
+ OP_NEW_STREAM(C, C1, 0);
+
+ /* A second, independent client connection (its own reactor). */
+ 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_NEW_STREAM(Cb, Cb1, 0);
+
+ OP_SELECT_SSL(0, C0);
+ OP_SELECT_SSL(1, C1);
+ OP_SELECT_SSL(2, Cb0);
+ OP_SELECT_SSL(3, Cb1);
+ for (i = 0; i < 2; ++i) {
+ OP_PUSH_U64(i);
+ OP_FUNC(check_poll_multi_reactor_cleanup);
+ }
+}
+
+/*
+ * Test: poll_notification_generation
+ * ---------------------------------
+ *
+ * Pause a worker after deregistration but before draining its wait context.
+ * Complete that notification cycle and start another before letting it drain.
+ * The worker must finish while the later cycle is still signalled.
+ */
+#if defined(OPENSSL_THREADS)
+struct poll_generation_thread_ctx {
+ QUIC_REACTOR *rtor;
+ struct poll_multi_reactor_ctx *ctx;
+};
+
+static CRYPTO_THREAD_RETVAL poll_generation_thread(void *arg)
+{
+ struct poll_generation_thread_ctx *d = arg;
+ struct poll_multi_reactor_ctx *ctx = d->ctx;
+ QUIC_REACTOR_WAIT_CTX wctx;
+ int ok;
+
+ ossl_quic_reactor_wait_ctx_init(&wctx);
+ ossl_crypto_mutex_lock(d->rtor->mutex);
+ ok = ossl_quic_reactor_wait_ctx_enter(&wctx, d->rtor);
+ ossl_crypto_mutex_unlock(d->rtor->mutex);
+ if (!ok)
+ goto end;
+
+ ossl_crypto_mutex_lock(ctx->mu);
+ ctx->arrived = 1;
+ ossl_crypto_condvar_broadcast(ctx->cv);
+ while (ctx->release < 1)
+ ossl_crypto_condvar_wait(ctx->cv, ctx->mu);
+ ossl_crypto_mutex_unlock(ctx->mu);
+
+ ossl_crypto_mutex_lock(d->rtor->mutex);
+ ossl_quic_reactor_wait_ctx_deregister(&wctx, d->rtor);
+ ossl_crypto_mutex_unlock(d->rtor->mutex);
+
+ ossl_crypto_mutex_lock(ctx->mu);
+ ctx->arrived = 2;
+ ossl_crypto_condvar_broadcast(ctx->cv);
+ while (ctx->release < 2)
+ ossl_crypto_condvar_wait(ctx->cv, ctx->mu);
+ ossl_crypto_mutex_unlock(ctx->mu);
+
+ ossl_quic_reactor_wait_ctx_drain(&wctx);
+
+end:
+ ossl_quic_reactor_wait_ctx_cleanup(&wctx);
+ ossl_crypto_mutex_lock(ctx->mu);
+ ctx->rc[0] = ok;
+ ++ctx->done;
+ ossl_crypto_condvar_broadcast(ctx->cv);
+ ossl_crypto_mutex_unlock(ctx->mu);
+ return 0;
+}
+#endif
+
+DEF_FUNC(check_poll_notification_generation)
+{
+ int ok = 0;
+#if !defined(OPENSSL_THREADS)
+ TEST_skip("threading not supported, skipping");
+ F_SKIP_REST();
+err:
+ return ok;
+#else
+ SSL *conn;
+ QUIC_CHANNEL *ch;
+ QUIC_REACTOR *rtor;
+ CRYPTO_THREAD *thread[1] = { NULL };
+ struct poll_multi_reactor_ctx ctx = { 0 };
+ struct poll_generation_thread_ctx worker;
+ uint64_t generation, cleanup_generation = 0;
+ size_t started = 0;
+ int registered = 0, need_wait = 0, state_ok;
+
+ REQUIRE_SSL(conn);
+ if (!TEST_ptr(ch = ossl_quic_conn_get_channel(conn))
+ || !TEST_ptr(rtor = ossl_quic_channel_get_reactor(ch)))
+ return 0;
+
+ if (!rtor->have_notifier) {
+ TEST_skip("test requires a notifier-enabled reactor");
+ F_SKIP_REST();
+ }
+
+ if ((ctx.mu = ossl_crypto_mutex_new()) == NULL
+ || (ctx.cv = ossl_crypto_condvar_new()) == NULL)
+ goto err;
+
+ ossl_crypto_mutex_lock(rtor->mutex);
+ state_ok = TEST_size_t_eq(rtor->cur_blocking_waiters, 0)
+ && TEST_int_eq(rtor->signalled_notifier, 0);
+ generation = rtor->notify_generation;
+ if (state_ok) {
+ ossl_quic_reactor_enter_blocking_section(rtor);
+ registered = 1;
+ }
+ ossl_crypto_mutex_unlock(rtor->mutex);
+ if (!state_ok)
+ goto err;
+
+ worker.rtor = rtor;
+ worker.ctx = &ctx;
+ if (!TEST_ptr(thread[0] = ossl_crypto_thread_native_start(
+ poll_generation_thread, &worker, 1)))
+ goto err_join;
+ started = 1;
+
+ if (!TEST_true(poll_cleanup_wait(&ctx, &ctx.arrived, 1)))
+ goto err_join;
+
+ poll_cleanup_signal_notifier(rtor);
+ poll_cleanup_release(&ctx, 1);
+ if (!TEST_true(poll_cleanup_wait(&ctx, &ctx.arrived, 2)))
+ goto err_join;
+
+ ossl_crypto_mutex_lock(rtor->mutex);
+ state_ok = TEST_size_t_eq(rtor->cur_blocking_waiters, 1)
+ && TEST_int_eq(rtor->signalled_notifier, 1)
+ && TEST_uint64_t_eq(rtor->notify_generation, generation);
+ if (state_ok) {
+ /* The coordinator is the last registered waiter for this cycle. */
+ ossl_quic_reactor_leave_blocking_section(rtor);
+ registered = 0;
+ state_ok = TEST_uint64_t_eq(rtor->notify_generation, generation + 1);
+ if (state_ok) {
+ ossl_quic_reactor_enter_blocking_section(rtor);
+ registered = 1;
+ ossl_rio_notifier_signal(&rtor->notifier);
+ rtor->signalled_notifier = 1;
+ }
+ }
+ ossl_crypto_mutex_unlock(rtor->mutex);
+ if (!state_ok)
+ goto err_join;
+
+ poll_cleanup_release(&ctx, 2);
+ if (!TEST_true(poll_cleanup_wait(&ctx, &ctx.done, 1))
+ || !TEST_int_eq(ctx.rc[0], 1))
+ goto err_join;
+
+ /* Finishing the old wait must not consume the later notification. */
+ ossl_crypto_mutex_lock(rtor->mutex);
+ ok = TEST_size_t_eq(rtor->cur_blocking_waiters, 1)
+ && TEST_int_eq(rtor->signalled_notifier, 1)
+ && TEST_uint64_t_eq(rtor->notify_generation, generation + 1);
+ ossl_crypto_mutex_unlock(rtor->mutex);
+
+err_join:
+ if (registered) {
+ ossl_crypto_mutex_lock(rtor->mutex);
+ need_wait = ossl_quic_reactor_deregister_blocking_section(rtor,
+ &cleanup_generation);
+ ossl_crypto_mutex_unlock(rtor->mutex);
+ }
+ poll_cleanup_release(&ctx, 2);
+ poll_cleanup_join(&ctx, thread, started);
+ if (need_wait)
+ ossl_quic_reactor_wait_for_notifier_clear(rtor, cleanup_generation);
+
+ ossl_crypto_mutex_lock(rtor->mutex);
+ if (!TEST_size_t_eq(rtor->cur_blocking_waiters, 0)
+ || !TEST_int_eq(rtor->signalled_notifier, 0))
+ ok = 0;
+ ossl_crypto_mutex_unlock(rtor->mutex);
+
+err:
+ ossl_crypto_condvar_free(&ctx.cv);
+ ossl_crypto_mutex_free(&ctx.mu);
+ return ok;
+#endif
+}
+
+DEF_SCRIPT(poll_notification_generation,
+ "verify a QUIC poll wait completes independently of later notification cycles")
+{
+ OP_SIMPLE_PAIR_CONN_ND();
+ OP_SELECT_SSL(0, C);
+ OP_FUNC(check_poll_notification_generation);
+}
+
DEF_FUNC(check_writeable)
{
int ok = 0;
@@ -5569,6 +6056,8 @@ static SCRIPT_INFO *const scripts[] = {
USE(simple_thread),
USE(ssl_poll),
USE(poll_abort_blocking),
+ USE(poll_multi_reactor_cleanup),
+ USE(poll_notification_generation),
USE(check_cwm),
USE(check_pc_flood),
USE(check_ctx_cbks),