Commit d14b736193 for openssl.org

commit d14b7361938d9695f10b0581f5875ca701146961
Author: Matt Caswell <matt@openssl.foundation>
Date:   Mon Aug 10 15:52:00 2026 +0100

    DTLS 1.3 Wait rather than spin in the blocking accept path

    SSL_accept_connection() looped calling ossl_dtls_tick() with nothing in
    between, so it only avoided spinning if the application supplied a
    blocking network BIO and each tick slept inside BIO_recvmmsg().

    A listener cannot require that. It demultiplexes one socket to many
    connections and ossl_dgram_demux_pump() holds the demux lock across the
    receive, so a thread blocked in accept stalls reads on every established
    connection until a datagram for any peer arrives. QUIC takes the
    opposite approach, keeping the socket non-blocking and waiting for
    readiness instead.

    Do the same: set the listener's network BIO non-blocking and wait
    between ticks via ossl_dtls_block_until_ready(), which reuses
    SSL_poll()'s blocking machinery. It reads nothing out, so the caller
    re-tests its own condition and waits again if the wakeup was not for it.

    Assisted-by: Claude Code:claude-opus-5
    Reviewed-by: Ryan Hooper <ryanh@openssl.foundation>
    Reviewed-by: Jakub Zelenka <jakub.zelenka@openssl.foundation>
    Merge-date: Mon Aug 17 08:29:51 2026
    Merged-from: https://github.com/openssl/openssl/pull/32324

diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index 05988b7bc1..0e8f1741f8 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -2457,19 +2457,13 @@ SSL *ossl_dtls_accept_connection(SSL *ssl, uint64_t flags)
     }

     /*
-     * Loop calling ossl_dtls_tick() until a verified connection arrives or
-     * a fatal error occurs.
+     * Blocking path: tick to make whatever progress is possible now, and if
+     * that did not produce a connection, wait for readiness before ticking
+     * again.
      *
-     * This blocking accept path requires net_rbio to be a blocking BIO. With
-     * a blocking BIO each tick sleeps inside BIO_recvmmsg() until a datagram
-     * is received, so the loop waits efficiently and does not spin.
-     *
-     * If net_rbio were non-blocking, BIO_recvmmsg() would return a transient
-     * (non-fatal) error when no datagram is ready; ossl_dtls_tick() would then
-     * return >= 0 with the incoming queue still empty and this loop would busy
-     * spin. Callers that want non-blocking behaviour must use
-     * SSL_ACCEPT_CONNECTION_NO_BLOCK (handled above) instead of a non-blocking
-     * BIO on this path.
+     * The wait is what stops this from being a busy loop. The network BIO is
+     * non-blocking, so a tick which finds no datagram returns immediately;
+     * without waiting in between, this loop would spin.
      */
     for (;;) {
         if (ossl_dtls_tick(dl) < 0) {
@@ -2483,6 +2477,16 @@ SSL *ossl_dtls_accept_connection(SSL *ssl, uint64_t flags)
         ossl_crypto_mutex_unlock(dl->mutex);
         if (conn != NULL)
             break;
+
+        /*
+         * Nothing yet, so wait for the listener to become ready before ticking
+         * again. What that amounts to is decided by the poll translation for a
+         * listener: the network socket becoming readable, or another thread
+         * signalling the notifier because it produced readiness on our behalf.
+         */
+        if (!ossl_dtls_block_until_ready(ssl, SSL_POLL_EVENT_IC,
+                ossl_time_infinite()))
+            break;
     }

 end:
@@ -2534,6 +2538,16 @@ void ossl_dtls_listener_set0_net_rbio(SSL *s, BIO *bio)

     dl = (DTLS_LISTENER *)s;

+    /*
+     * The listener demultiplexes one socket to many connections, so it can
+     * never afford to block inside a read: a read for one connection would
+     * stall every other, and the demux lock is held across it. Blocking
+     * behaviour is provided by waiting for readiness instead, so configure the
+     * BIO for non-blocking operation on the application's behalf, as QUIC does.
+     */
+    if (bio != NULL)
+        BIO_set_nbio(bio, 1); /* best effort autoconfig */
+
     ossl_crypto_mutex_lock(dl->mutex);

     /*
@@ -2601,6 +2615,10 @@ void ossl_dtls_listener_set0_net_wbio(SSL *s, BIO *bio)

     dl = (DTLS_LISTENER *)s;

+    /* See ossl_dtls_listener_set0_net_rbio() as to why. */
+    if (bio != NULL)
+        BIO_set_nbio(bio, 1); /* best effort autoconfig */
+
     old_wbio = dl->net_wbio;

     /* No change - nothing to do */
diff --git a/ssl/rio/poll_immediate.c b/ssl/rio/poll_immediate.c
index 3b32d679b8..6a622fc16a 100644
--- a/ssl/rio/poll_immediate.c
+++ b/ssl/rio/poll_immediate.c
@@ -196,6 +196,13 @@ static int poll_translate_ssl_dtls_listener(SSL *ssl,
         return 0;
     }

+    /*
+     * Watch the socket for readability whatever was asked for. Every event a
+     * listener reports is ultimately driven by a datagram arriving, both an
+     * incoming connection and data pending on the listener itself, so there is
+     * no event for which this is the wrong thing to wait on. events is
+     * therefore only consulted for the readiness re-check below.
+     */
     if (!ossl_rio_poll_builder_add_fd(rpb, desc.value.fd, /*r=*/1, /*w=*/0))
         return 0;

@@ -641,6 +648,37 @@ out:
 #endif
     return ok;
 }
+
+#ifndef OPENSSL_NO_DTLS
+/*
+ * Wait until the given DTLS listener or listener-based connection may have
+ * become ready for one of the given events, or until the deadline expires.
+ *
+ * This is the wait that libssl itself performs when a DTLS object is used in
+ * blocking mode. It is the same wait SSL_poll() performs, and reuses it, so a
+ * blocking call is woken by the same means an application polling the object
+ * would be: readiness of the listener's socket, or the listener's notifier if
+ * another thread produces readiness without the socket becoming readable here.
+ *
+ * No readout is performed. A spurious wakeup is always possible - the socket
+ * becoming readable says nothing about which connection the datagram is for -
+ * so the caller must re-test its own condition and wait again if needed.
+ *
+ * Returns 1 if the wait completed and 0 on error.
+ */
+int ossl_dtls_block_until_ready(SSL *ssl, uint64_t events, OSSL_TIME deadline)
+{
+    SSL_POLL_ITEM item;
+    size_t result_count = 0;
+
+    item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
+    item.desc.value.ssl = ssl;
+    item.events = events;
+    item.revents = 0;
+
+    return poll_block(&item, 1, sizeof(item), deadline, &result_count);
+}
+#endif /* OPENSSL_NO_DTLS */
 #endif

 static int poll_readout(SSL_POLL_ITEM *items,
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index 4a83302c31..3020f44ec5 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -3064,6 +3064,7 @@ int ossl_dtls_conn_poll_events(SSL *s, uint64_t events, int do_tick,
     uint64_t *revents);
 void ossl_dtls_listener_enter_blocking_section(SSL *s);
 void ossl_dtls_listener_leave_blocking_section(SSL *s);
+int ossl_dtls_block_until_ready(SSL *ssl, uint64_t events, OSSL_TIME deadline);
 int ossl_dtls_tick(DTLS_LISTENER *dl);

 /* DTLS Listener internal cookie callbacks */
diff --git a/test/dtls_multithread_test.c b/test/dtls_multithread_test.c
index ac5998ae32..8d312a40cc 100644
--- a/test/dtls_multithread_test.c
+++ b/test/dtls_multithread_test.c
@@ -444,6 +444,124 @@ err:
     return testresult;
 }

+/*
+ * Per-thread state for the blocking accept
+ */
+struct accept_thread_args {
+    SSL *listener;
+    SSL *conn; /* connection the accept returned */
+    CRYPTO_THREAD *thread;
+    int result; /* 1 = success, 0 = failure */
+};
+
+/*
+ * Thread function: block in SSL_accept_connection() until a connection turns
+ * up. This is the accept path which ticks the listener itself, so no other
+ * thread needs to drive it.
+ */
+static unsigned int blocking_accept_thread(void *arg)
+{
+    struct accept_thread_args *ta = (struct accept_thread_args *)arg;
+
+    ta->conn = SSL_accept_connection(ta->listener, 0);
+    ta->result = (ta->conn != NULL);
+    return 1;
+}
+
+/*
+ * Test that a blocking SSL_accept_connection() waits for a connection and
+ * returns it.
+ *
+ * The listener demultiplexes one socket to many connections, so it cannot
+ * block inside a read: doing so would stall every other connection, and the
+ * demux lock is held across it. Blocking accept therefore has to wait for
+ * readiness rather than for a datagram, which is what this exercises - a
+ * client is only created once the accepting thread is already in the call.
+ *
+ * Note that this cannot distinguish waiting from spinning: the accept returns
+ * the connection either way, and the difference is CPU consumed rather than
+ * anything observable through the API. It is a test that the blocking path
+ * works at all, which was previously only covered for the failure case of
+ * having no BIO set.
+ */
+static int test_dtls_blocking_accept(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *listener = NULL, *client = NULL;
+    struct accept_thread_args accept_args;
+    BIO_ADDR *server_addr = NULL;
+    int server_fd = -1, client_fd = -1;
+    int testresult = 0;
+    int i, ret, err;
+
+    memset(&accept_args, 0, sizeof(accept_args));
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), 0, 0, &sctx, &cctx, cert, privkey)))
+        goto err;
+
+    if (!TEST_true(create_listener(sctx, &listener, &server_addr, &server_fd)))
+        goto err;
+
+    /*
+     * Create the client's socket first, but do not connect with it yet. There
+     * is no way to cancel a blocking SSL_accept_connection(), so a thread
+     * parked in one is released only by a connection arriving - which means
+     * nothing between starting the thread and the exchange completing may bail
+     * out, or the join below would wait for ever. Anything which can fail is
+     * therefore done up front. The accept still has to wait, since no datagram
+     * is sent until SSL_connect() below.
+     */
+    if (!TEST_true(create_client(cctx, server_addr, &client, &client_fd)))
+        goto err;
+
+    accept_args.listener = listener;
+    accept_args.thread = ossl_crypto_thread_native_start(blocking_accept_thread,
+        &accept_args, 1);
+    if (!TEST_ptr(accept_args.thread))
+        goto err;
+
+    /*
+     * Drive the client's side of the cookie exchange. The accepting thread
+     * ticks the listener, so this only has to keep the client moving.
+     */
+    SSL_set_connect_state(client);
+    for (i = 0; i < 200 && accept_args.result == 0; i++) {
+        ret = SSL_connect(client);
+        err = SSL_get_error(client, ret);
+        if (ret <= 0 && err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
+            TEST_error("SSL_connect failed (err %d)", err);
+            goto err;
+        }
+        OSSL_sleep(10);
+    }
+
+    ossl_crypto_thread_native_join(accept_args.thread, NULL);
+    ossl_crypto_thread_native_clean(accept_args.thread);
+    accept_args.thread = NULL;
+
+    if (!TEST_int_eq(accept_args.result, 1) || !TEST_ptr(accept_args.conn))
+        goto err;
+
+    testresult = 1;
+err:
+    if (accept_args.thread != NULL) {
+        ossl_crypto_thread_native_join(accept_args.thread, NULL);
+        ossl_crypto_thread_native_clean(accept_args.thread);
+    }
+    SSL_free(accept_args.conn);
+    SSL_free(client);
+    SSL_free(listener);
+    BIO_ADDR_free(server_addr);
+    if (server_fd >= 0)
+        BIO_closesocket(server_fd);
+    if (client_fd >= 0)
+        BIO_closesocket(client_fd);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}
+
 int setup_tests(void)
 {
     if (!TEST_ptr(cert = test_get_argument(0))
@@ -451,5 +569,6 @@ int setup_tests(void)
         return 0;

     ADD_TEST(test_dtls_multithread);
+    ADD_TEST(test_dtls_blocking_accept);
     return 1;
 }