Commit de4a197916 for openssl.org

commit de4a1979169d61d4a45d44b5c4978beceec4a1f8
Author: Matt Caswell <matt@openssl.foundation>
Date:   Fri Aug 7 16:26:20 2026 +0100

    DTLS 1.3 Enter a blocking section when polling a DTLS listener

    A DTLS listener added only the network socket to the poll set. Three things
    followed from that. The notifier was not in the poll set, so a thread blocked
    in SSL_poll() on the listener could not be woken by it. The thread never
    called ossl_dtls_listener_enter_blocking_section(), so cur_blocking_waiters
    stayed at zero - and since signalling is conditional on there being a waiter,
    no other thread even attempted to signal. And there was no re-check after
    registering, so readiness arising between the readout deciding nothing was
    ready and the wait actually starting was lost.

    Polling the socket alone is not sufficient, though not because a wakeup can
    be missed outright. poll() reports whatever is currently sitting in the
    socket buffer and returns immediately if there is any, so a thread cannot
    miss a datagram just by being outside poll() when it arrives. What it can
    miss is a datagram another thread has already taken. With several threads
    polling the one shared socket that happens constantly: an arriving datagram
    wakes all of them, only one gets it, and the rest find nothing. Any of them
    can be the one that takes it, because SSL_read() on a connection pumps the
    demux and SSL_poll() on a connection ticks the whole listener.

      1. Accept thread A polls the listener for SSL_POLL_EVENT_IC. Its readout
         ticks the listener, finds nothing, and it decides to block.
      2. A client's final ClientHello lands on the shared socket.
      3. Worker thread B, polling one of its own connections, ticks the listener
         and is the one that takes the datagram. Its tick completes the pending
         connection and pushes it onto the accept queue. Signalling is attempted,
         but A never registered as a waiter, so nothing is signalled.
      4. A reaches its poll, watching the socket alone. B drained it, so it is
         empty. A sleeps with a validated connection sitting on the accept queue,
         and nothing can wake it until an unrelated datagram arrives or its
         deadline expires.

    A's readout, back at step 1, would have found that connection had it run
    after step 3 rather than before it.

    Add the notifier FD to the poll set and bracket the wait with the enter/leave
    blocking section calls, mirroring what the listener-based connection path
    already does. As there, re-check readiness once inside the section, since it
    is only from that point that a readiness event is guaranteed to make the
    notifier readable, and abort blocking if the listener became ready in the
    meantime. The re-check removes the dependency on that ordering; the notifier
    covers anything arising after it.

    Note that the signal raised when a connection is pushed onto the accept queue
    is itself conditional on a registered waiter, so it does nothing for a thread
    polling the listener until that thread registers here. The two changes are
    complementary and neither is sufficient alone.

    Assisted-by: Claude Code:claude-opus-5
    Reviewed-by: Ryan Hooper <ryanh@openssl.foundation>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Fri Aug 14 15:45:35 2026
    Merged-from: https://github.com/openssl/openssl/pull/32239

diff --git a/ssl/rio/poll_immediate.c b/ssl/rio/poll_immediate.c
index 7331999d95..7df9694538 100644
--- a/ssl/rio/poll_immediate.c
+++ b/ssl/rio/poll_immediate.c
@@ -176,10 +176,14 @@ static void postpoll_translation_cleanup_ssl_quic(SSL *ssl,
 #ifndef OPENSSL_NO_DTLS
 static int poll_translate_ssl_dtls_listener(SSL *ssl,
     RIO_POLL_BUILDER *rpb,
-    uint64_t events)
+    uint64_t events,
+    int *abort_blocking)
 {
     BIO *rbio;
     BIO_POLL_DESCRIPTOR desc;
+    DTLS_LISTENER *dl = (DTLS_LISTENER *)ssl;
+    uint64_t revents = 0;
+    int nfd;

     rbio = SSL_get_rbio(ssl);
     if (rbio == NULL)
@@ -195,6 +199,41 @@ static int poll_translate_ssl_dtls_listener(SSL *ssl,
     if (!ossl_rio_poll_builder_add_fd(rpb, desc.value.fd, /*r=*/1, /*w=*/0))
         return 0;

+    /*
+     * Add the notifier FD for the DTLS listener (if multi-threaded mode is
+     * enabled). Another thread may queue an incoming connection for us, or
+     * demux data to one of our connections, without the underlying network
+     * socket ever becoming readable from our perspective.
+     */
+    if (dl->have_notifier) {
+        nfd = ossl_rio_notifier_as_fd(&dl->notifier);
+        if (nfd != -1) {
+            if (!ossl_rio_poll_builder_add_fd(rpb, nfd, /*r=*/1, /*w=*/0))
+                return 0;
+
+            /* Tell the listener we need to receive notifications. */
+            ossl_dtls_listener_enter_blocking_section(ssl);
+
+            /*
+             * Only after the above call returns is it guaranteed that any
+             * readiness events will cause the notifier to become readable.
+             * Therefore it is possible the listener became ready after the
+             * readout which decided we needed to block. Re-check now.
+             */
+            if (!ossl_dtls_listener_poll_events(ssl, events, /*do_tick=*/0,
+                    &revents)) {
+                ossl_dtls_listener_leave_blocking_section(ssl);
+                return 0;
+            }
+
+            if (revents != 0) {
+                ossl_dtls_listener_leave_blocking_section(ssl);
+                *abort_blocking = 1;
+                return 1;
+            }
+        }
+    }
+
     return 1;
 }

@@ -317,6 +356,15 @@ static int poll_translate_ssl_dtls_conn(SSL *ssl,
     return 1;
 }

+static void postpoll_translation_cleanup_ssl_dtls_listener(SSL *ssl)
+{
+    DTLS_LISTENER *dl = (DTLS_LISTENER *)ssl;
+
+    /* Need to mirror the enter blocking section call */
+    if (dl->have_notifier && ossl_rio_notifier_as_fd(&dl->notifier) != -1)
+        ossl_dtls_listener_leave_blocking_section(ssl);
+}
+
 static void postpoll_translation_cleanup_ssl_dtls_conn(SSL *ssl, uint64_t events)
 {
     SSL_CONNECTION *sc;
@@ -373,7 +421,7 @@ static void postpoll_translation_cleanup(SSL_POLL_ITEM *items,

 #ifndef OPENSSL_NO_DTLS
             case SSL_TYPE_DTLS_LISTENER:
-                /* Listeners don't enter blocking sections */
+                postpoll_translation_cleanup_ssl_dtls_listener(ssl);
                 break;
             case SSL_TYPE_SSL_CONNECTION:
                 if (SSL_is_dtls(ssl))
@@ -451,8 +499,13 @@ static int poll_translate(SSL_POLL_ITEM *items,

 #ifndef OPENSSL_NO_DTLS
             case SSL_TYPE_DTLS_LISTENER:
-                if (!poll_translate_ssl_dtls_listener(ssl, rpb, item->events))
+                if (!poll_translate_ssl_dtls_listener(ssl, rpb, item->events,
+                        abort_blocking))
                     FAIL_ITEM(i);
+
+                if (*abort_blocking)
+                    goto out;
+
                 break;
             case SSL_TYPE_SSL_CONNECTION:
                 if (SSL_is_dtls(ssl)) {
diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index 79c1d18380..2f6a9f9a66 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -4843,7 +4843,7 @@ static int test_new_pending_cb_alternate(void)
 }

 /*
- * The test below needs a listener with a notifier, which only exists
+ * The two tests below need a listener with a notifier, which only exists
  * when the listener is created without SSL_LISTENER_FLAG_SINGLE_THREAD. In a
  * no-threads build that listener cannot be created at all, because the
  * condition variable it needs is unavailable.
@@ -5018,6 +5018,144 @@ end:
     return testresult;
 }

+/*
+ * Test that a blocking SSL_poll() on a listener enters a blocking section.
+ *
+ * Unless it does, three things follow: the notifier is not in the poll set, so
+ * it cannot wake this thread; cur_blocking_waiters is never incremented, and
+ * since signalling is conditional on there being a waiter, no other thread
+ * even attempts to signal; and there is no re-check after registering, so
+ * readiness arising between the readout and the wait is lost.
+ *
+ * Polling the socket alone is not enough, though not because a wakeup can be
+ * missed outright. poll() reports whatever is currently sitting in the socket
+ * buffer and returns immediately if there is any, so a thread cannot miss a
+ * datagram just by being outside poll() when it arrives. What it can miss is a
+ * datagram another thread has already taken. With several threads polling the
+ * one shared socket that happens constantly: an arriving datagram wakes all of
+ * them, only one gets it, and the rest find nothing. Any of them can be the
+ * one that takes it, because SSL_read() on a connection pumps the demux and
+ * SSL_poll() on a connection ticks the whole listener.
+ *
+ *   1. Accept thread A polls the listener for SSL_POLL_EVENT_IC. Its readout
+ *      ticks the listener, finds nothing, and it decides to block.
+ *   2. A client's final ClientHello lands on the shared socket.
+ *   3. Worker thread B, polling one of its own connections, ticks the listener
+ *      and is the one that takes the datagram. Its tick completes the pending
+ *      connection and pushes it onto the accept queue. Signalling is attempted,
+ *      but A never registered as a waiter, so nothing is signalled.
+ *   4. A reaches its poll, watching the socket alone. B drained it, so it is
+ *      empty, and A sleeps with a validated connection sitting on the accept
+ *      queue.
+ *
+ * A's readout, back at step 1, would have found that connection had it run
+ * after step 3 rather than before it. Registering as a waiter and re-checking
+ * is what removes the dependency on that ordering.
+ *
+ * Note that the signal added for the step 3 queue push is itself conditional on
+ * a registered waiter, so it does nothing for a thread polling the listener
+ * until that thread registers. The two fixes are complementary.
+ *
+ * None of that has a public observable, and this deliberately does not time
+ * the wait. Instead it relies on the last waiter out of a blocking section
+ * draining a raised notifier signal: raise one beforehand, poll briefly with
+ * nothing ready, and check afterwards. Drained means a blocking section was
+ * entered and left, since only a leave drains it and only an enter can be left;
+ * still standing means neither happened.
+ *
+ * signalled_notifier is protected by the listener mutex in the library, which
+ * has to assume concurrent access. This test is single threaded throughout, so
+ * it reads and writes the field directly without holding the mutex.
+ *
+ * Note what this does not cover. That the notifier is in the poll set, and so
+ * can actually deliver a wakeup, is not checked: with a signal raised the poll
+ * returns at once if the notifier is being watched and sleeps out its timeout
+ * if it is not, and only timing separates those. A longer timeout would not
+ * help, because the first iteration's leave drains the notifier and the next
+ * one sleeps out the remainder either way. The re-check after registering is
+ * not covered either, since readiness arriving between the readout and the
+ * registration cannot be produced from a single thread.
+ */
+static int test_dtls_poll_listener_enters_blocking_section(void)
+{
+    SSL_CTX *sctx = NULL;
+    SSL *listener = NULL;
+    BIO_ADDR *server_addr = NULL;
+    DTLS_LISTENER *dl;
+    SSL_POLL_ITEM poll_item;
+    struct timeval poll_timeout;
+    size_t poll_result = 0;
+    int server_fd = -1, nfd = -1;
+    int testresult = 0;
+
+    if (!TEST_ptr(sctx = SSL_CTX_new(DTLS_server_method())))
+        goto end;
+
+    if (!TEST_true(create_dtls_listener(sctx, 0, &listener, &server_addr,
+            &server_fd)))
+        goto end;
+
+    dl = (DTLS_LISTENER *)listener;
+    if (!TEST_true(dl->have_notifier))
+        goto end;
+
+    /*
+     * Raise the notifier as another thread reporting readiness would, which
+     * means both writing to the notifier and recording that it is raised, as
+     * dtls_listener_signal_notifier() does.
+     */
+    if (!TEST_true(ossl_rio_notifier_signal(&dl->notifier)))
+        goto end;
+    dl->signalled_notifier = 1;
+
+    nfd = ossl_rio_notifier_as_fd(&dl->notifier);
+    if (!TEST_int_ge(nfd, 0)
+        || !TEST_int_gt(BIO_socket_ready(nfd, /*for_read=*/1), 0))
+        goto end;
+
+    /*
+     * Poll with a short timeout. No client exists, so nothing is ever ready
+     * and SSL_poll() must block, which is what drives the translation that
+     * enters the blocking section.
+     */
+    poll_item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
+    poll_item.desc.value.ssl = listener;
+    poll_item.events = SSL_POLL_EVENT_IC;
+    poll_item.revents = 0;
+    poll_timeout.tv_sec = 0;
+    poll_timeout.tv_usec = 100000;
+
+    if (!TEST_true(SSL_poll(&poll_item, 1, sizeof(poll_item), &poll_timeout, 0,
+            &poll_result)))
+        goto end;
+
+    if (!TEST_size_t_eq(poll_result, 0))
+        goto end;
+
+    /*
+     * Leaving the blocking section must have drained the notifier. Check the
+     * notifier itself and not only the flag recording its state, since it is
+     * the notifier being readable that would spuriously wake a later waiter.
+     */
+    if (!TEST_int_eq(BIO_socket_ready(nfd, /*for_read=*/1), 0))
+        goto end;
+
+    if (!TEST_int_eq(dl->signalled_notifier, 0))
+        goto end;
+
+    if (!TEST_size_t_eq(dl->cur_blocking_waiters, 0))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_free(listener);
+    BIO_ADDR_free(server_addr);
+    if (server_fd >= 0)
+        BIO_closesocket(server_fd);
+    SSL_CTX_free(sctx);
+    return testresult;
+}
+
 #endif /* OPENSSL_THREADS */


@@ -5129,6 +5267,7 @@ int setup_tests(void)
     /* Blocking SSL_poll() wakeup tests */
 #if defined(OPENSSL_THREADS)
     ADD_TEST(test_dtls_notifier_signalled_on_accept_queue_push);
+    ADD_TEST(test_dtls_poll_listener_enters_blocking_section);
 #endif