Commit 3c74cbd32d for openssl.org

commit 3c74cbd32da0289e7f22bd76a0693d550180ab20
Author: mzfr <github@mzfr.in>
Date:   Thu Sep 24 17:51:51 2026 +0800

    Fix DTLS listener reset races

    Keep listener drive guards and the RX queue alias in SSL_CONNECTION so
    concurrent listener threads do not access resettable DTLS state during
    SSL_clear(). Check stable state before d1 and clear the RX alias before
    freeing its queue.

    Add focused multithreaded coverage for both SSL_clear() paths and run
    the race tests in ThreadSanitizer CI.

    Incorporates changes from #32962.

    Fixes #32948
    Fixes #32949
    Assisted-by: Codex:gpt-5.6-sol

    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Reviewed-by: Ryan Hooper <ryanh@openssl.foundation>
    Merge-date: Tue Sep 29 16:24:45 2026
    Merged-from: https://github.com/openssl/openssl/pull/32964

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 8111bced09..927ef0b972 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -561,7 +561,7 @@ jobs:
         cat /proc/cpuinfo
         ./util/opensslwrap.sh version -c
     - name: make test
-      run: .github/workflows/make-test V=1 TESTS="test_lhash test_threads test_internal_provider test_provfetch test_provider test_pbe test_evp_kdf test_pkcs12 test_store test_evp test_quic*"
+      run: .github/workflows/make-test V=1 TESTS="test_lhash test_threads test_internal_provider test_provfetch test_provider test_pbe test_evp_kdf test_pkcs12 test_store test_evp test_quic* test_dtls_listener_races"
     - name: save artifacts
       if: success() || failure()
       uses: actions/upload-artifact@v5
diff --git a/.github/workflows/run-checker-merge.yml b/.github/workflows/run-checker-merge.yml
index db40542ca1..8b571582c8 100644
--- a/.github/workflows/run-checker-merge.yml
+++ b/.github/workflows/run-checker-merge.yml
@@ -163,4 +163,4 @@ jobs:
         cat /proc/cpuinfo
         ./util/opensslwrap.sh version -c
     - name: make test
-      run: make test V=1 TESTS="test_lhash test_threads test_internal_provider test_provfetch test_provider test_pbe test_evp_kdf test_pkcs12 test_store test_evp test_quic*"
+      run: make test V=1 TESTS="test_lhash test_threads test_internal_provider test_provfetch test_provider test_pbe test_evp_kdf test_pkcs12 test_store test_evp test_quic* test_dtls_listener_races"
diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index 54e6513705..8372d0927e 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -296,7 +296,11 @@ void dtls1_free(SSL *ssl)

 #ifndef OPENSSL_NO_DTLS
     if (s->d1 != NULL) {
-        ossl_dtls_rx_free(s->d1->rx);
+        DTLS_RX *rx = s->d1->rx;
+
+        if (s->listener_rx == rx)
+            s->listener_rx = NULL;
+        ossl_dtls_rx_free(rx);

         if (s->d1->listener != NULL)
             SSL_free(s->d1->listener);
@@ -337,8 +341,6 @@ int dtls1_clear(SSL *ssl)
         SSL *listener = s->d1->listener;
         OSSL_TIME created_at = s->d1->created_at;
         unsigned int req_blocking_mode = s->d1->req_blocking_mode;
-        unsigned int force_nonblocking = s->d1->force_nonblocking;
-        unsigned int being_driven = s->d1->being_driven;
 #endif

         mtu = s->d1->mtu;
@@ -369,19 +371,6 @@ int dtls1_clear(SSL *ssl)
          * configured it, not of the handshake, so it survives a clear.
          */
         s->d1->req_blocking_mode = req_blocking_mode;
-        /*
-         * SSL_clear() can be called from inside the very SSL_accept() the
-         * listener is driving, so losing this would let the connection block
-         * there and stall the listener.
-         */
-        s->d1->force_nonblocking = force_nonblocking;
-        /*
-         * being_driven says the listener is driving this connection's
-         * handshake, and is what keeps a concurrent tick from collecting it a
-         * second time. Losing it would let two threads into the state machine
-         * for one connection.
-         */
-        s->d1->being_driven = being_driven;
         s->d1->created_at = created_at;
 #endif

@@ -1333,6 +1322,7 @@ static SSL *dtls_listener_create_conn_ssl(DTLS_LISTENER *dl,
     sc->d1->rx = ossl_dtls_rx_new(dl->demux);
     if (sc->d1->rx == NULL)
         goto err;
+    sc->listener_rx = sc->d1->rx;

     /*
      * Update the read record layer to use the URXE queue if it already exists.
@@ -1493,7 +1483,7 @@ static void dtls_listener_packet_handler(DGRAM_URXE *urxe, void *arg)
              * Mark the connection being_driven while the mutex is dropped for
              * the callback. This keeps the tick loop away from this connection.
              */
-            sc->d1->being_driven = 1;
+            sc->listener_being_driven = 1;
             ossl_crypto_mutex_unlock(dl->mutex);
             keep = dl->ssl.ctx->new_pending_conn_cb(dl->ssl.ctx, conn_ssl,
                 dl->ssl.ctx->new_pending_conn_arg);
@@ -1514,16 +1504,16 @@ static void dtls_listener_packet_handler(DGRAM_URXE *urxe, void *arg)
                 goto release;
             }

-            sc->d1->being_driven = 0;
+            sc->listener_being_driven = 0;
         }
     }

     sc = SSL_CONNECTION_FROM_SSL_ONLY(conn_ssl);
-    if (sc == NULL || sc->d1 == NULL || sc->d1->rx == NULL)
+    if (sc == NULL || sc->listener_rx == NULL)
         goto release;

     /* Inject packet into connection's URXE queue */
-    ossl_dtls_rx_inject_urxe(sc->d1->rx, urxe);
+    ossl_dtls_rx_inject_urxe(sc->listener_rx, urxe);

     /* Signal notifier if needed */
     dtls_listener_signal_notifier(dl);
@@ -2150,13 +2140,13 @@ static void collect_pending_cb(SSL *ssl, const BIO_ADDR *peer, void *arg)
     if (sc == NULL)
         return;

-    if (sc->d1 == NULL || sc->d1->rx == NULL)
-        return;
-
     /*
      * Skip if already being driven by another thread.
      */
-    if (sc->d1->being_driven)
+    if (sc->listener_being_driven)
+        return;
+
+    if (sc->listener_rx == NULL || sc->d1 == NULL)
         return;

     /*
@@ -2188,7 +2178,7 @@ static void collect_pending_cb(SSL *ssl, const BIO_ADDR *peer, void *arg)
              * in pending_conns and is simply retried on the next tick.
              */
             if (ctx->failed_conns != NULL && sk_SSL_push(ctx->failed_conns, ssl) > 0) {
-                sc->d1->being_driven = 1;
+                sc->listener_being_driven = 1;
                 ctx->error_count++;
             }
             return;
@@ -2208,7 +2198,7 @@ static void collect_pending_cb(SSL *ssl, const BIO_ADDR *peer, void *arg)
         return;
     }

-    sc->d1->being_driven = 1;
+    sc->listener_being_driven = 1;
 }

 /*
@@ -2242,9 +2232,9 @@ static void drive_single_connection(SSL *ssl, DTLS_LISTENER *dl,
      * else can make progress while it does, including whatever it would be
      * waiting for.
      */
-    sc->d1->force_nonblocking = 1;
+    sc->listener_force_nonblocking = 1;
     ret = SSL_accept(ssl);
-    sc->d1->force_nonblocking = 0;
+    sc->listener_force_nonblocking = 0;

     /*
      * Always clear the stateless flag after SSL_accept() completes.
@@ -2349,8 +2339,8 @@ static int dtls_listener_drive_pending(DTLS_LISTENER *dl)
         ssl = sk_SSL_value(ctx.to_drive, i);
         sc = SSL_CONNECTION_FROM_SSL_ONLY(ssl);

-        if (sc != NULL && sc->d1 != NULL)
-            sc->d1->being_driven = 0;
+        if (sc != NULL)
+            sc->listener_being_driven = 0;

         SSL_free(ssl); /* Release reference from phase 1 */
     }
@@ -3053,7 +3043,7 @@ static int ossl_dtls_desires_blocking(const SSL *s)

     if (sc != NULL && sc->d1 != NULL) {
         /* The listener is driving this connection; it must not block. */
-        if (sc->d1->force_nonblocking)
+        if (sc->listener_force_nonblocking)
             return 0;

         if (sc->d1->req_blocking_mode != DTLS_BLOCKING_MODE_INHERIT)
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index daea36fa71..918f5633cb 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -657,8 +657,6 @@ int ossl_ssl_connection_reset(SSL *s)
         SSL *saved_listener = NULL;
         OSSL_TIME saved_created_at = ossl_time_zero();
         unsigned int saved_req_blocking_mode = DTLS_BLOCKING_MODE_INHERIT;
-        unsigned int saved_force_nonblocking = 0;
-        unsigned int saved_being_driven = 0;
         int is_dtls_listener_conn = 0;

         if (SSL_CONNECTION_IS_DTLS(sc) && sc->d1 != NULL
@@ -669,8 +667,6 @@ int ossl_ssl_connection_reset(SSL *s)
             saved_listener = sc->d1->listener;
             saved_created_at = sc->d1->created_at;
             saved_req_blocking_mode = sc->d1->req_blocking_mode;
-            saved_force_nonblocking = sc->d1->force_nonblocking;
-            saved_being_driven = sc->d1->being_driven;
             /*
              * Prevent dtls1_free from freeing rx and releasing the listener
              * reference - we'll restore them after ssl_init.
@@ -685,6 +681,11 @@ int ossl_ssl_connection_reset(SSL *s)
         if (!s->method->ssl_init(s)) {
 #if !defined(OPENSSL_NO_DTLS) && !defined(OPENSSL_NO_SOCK)
             if (is_dtls_listener_conn) {
+                DTLS_LISTENER *dl = (DTLS_LISTENER *)saved_listener;
+
+                ossl_crypto_mutex_lock(dl->mutex);
+                sc->listener_rx = NULL;
+                ossl_crypto_mutex_unlock(dl->mutex);
                 ossl_dtls_rx_free(saved_rx);
                 SSL_free(saved_listener);
             }
@@ -704,14 +705,6 @@ int ossl_ssl_connection_reset(SSL *s)
              * connection, not handshake state, so it survives a clear.
              */
             sc->d1->req_blocking_mode = saved_req_blocking_mode;
-            /*
-             * Both of these say something about the call this SSL_clear() may
-             * be nested inside: that the listener is driving the handshake and
-             * that it must not block while doing so. dtls1_clear() carries them
-             * over for the same reason.
-             */
-            sc->d1->force_nonblocking = saved_force_nonblocking;
-            sc->d1->being_driven = saved_being_driven;
         }
 #endif
     } else {
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index d31574648b..8e2a7e9ce0 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -1596,6 +1596,24 @@ struct ssl_connection_st {
     } s3;

     struct dtls1_state_st *d1; /* DTLSv1 variables */
+#ifndef OPENSSL_NO_DTLS
+    /* Stable alias for d1->rx while SSL_clear() resets or replaces d1. */
+    DTLS_RX *listener_rx;
+    /*
+     * Set when this connection is being driven by dtls_listener_drive_pending().
+     * Used to prevent multiple threads from driving the same connection
+     * concurrently and to allow the demux pump to be called without holding
+     * the listener mutex.
+     */
+    unsigned int listener_being_driven;
+    /*
+     * Set while the listener itself is driving this connection's handshake, to
+     * stop it blocking. The listener drives pending connections from inside its
+     * own tick, so a connection which blocked there would stop the listener
+     * making any further progress, including the progress being waited for.
+     */
+    unsigned int listener_force_nonblocking;
+#endif
     /* callback that allows applications to peek at protocol messages */
     void (*msg_callback)(int write_p, int version, int content_type,
         const void *buf, size_t len, SSL *ssl, void *arg);
@@ -2278,27 +2296,11 @@ typedef struct dtls1_state_st {
      */
     OSSL_TIME created_at;

-    /*
-     * Set when this connection is being driven by dtls_listener_drive_pending().
-     * Used to prevent multiple threads from driving the same connection
-     * concurrently and to allow the demux pump to be called without holding
-     * the listener mutex.
-     */
-    unsigned int being_driven : 1;
-
     /*
      * Blocking mode requested for this connection, as a DTLS_BLOCKING_MODE.
      * Defaults to inheriting from the listener it came from.
      */
     unsigned int req_blocking_mode : 2;
-
-    /*
-     * Set while the listener itself is driving this connection's handshake, to
-     * stop it blocking. The listener drives pending connections from inside its
-     * own tick, so a connection which blocked there would stop the listener
-     * making any further progress, including the progress being waited for.
-     */
-    unsigned int force_nonblocking : 1;
 #endif

 } DTLS1_STATE;
diff --git a/test/dtls_multithread_test.c b/test/dtls_multithread_test.c
index 48918ffb19..a478590fb8 100644
--- a/test/dtls_multithread_test.c
+++ b/test/dtls_multithread_test.c
@@ -170,7 +170,8 @@ static unsigned int client_conn_thread(void *arg)
 /*
  * Helper: create DTLS listener with real UDP socket
  */
-static int create_listener(SSL_CTX *ctx, SSL **listener, BIO_ADDR **addr, int *fd)
+static int create_listener_ex(SSL_CTX *ctx, uint64_t flags, SSL **listener,
+    BIO_ADDR **addr, int *fd)
 {
     BIO *bio = NULL;
     struct in_addr ina;
@@ -206,7 +207,7 @@ static int create_listener(SSL_CTX *ctx, SSL **listener, BIO_ADDR **addr, int *f
     if (!TEST_ptr(bio = BIO_new_dgram(*fd, BIO_NOCLOSE)))
         goto err;

-    if (!TEST_ptr(*listener = SSL_new_listener(ctx, 0)))
+    if (!TEST_ptr(*listener = SSL_new_listener(ctx, flags)))
         goto err;

     SSL_set_bio(*listener, bio, bio);
@@ -231,6 +232,12 @@ err:
     return ret;
 }

+static int create_listener(SSL_CTX *ctx, SSL **listener, BIO_ADDR **addr,
+    int *fd)
+{
+    return create_listener_ex(ctx, 0, listener, addr, fd);
+}
+
 /*
  * Helper: create DTLS client connected to server address
  */
@@ -326,6 +333,242 @@ static int do_handshake(SSL *client, SSL *listener, SSL **server_conn)
     return 1;
 }

+#define NUM_LISTENER_THREADS 2
+#define LISTENER_RACE_ITERATIONS 1000
+#define CLIENT_TRAFFIC_ITERATIONS 25
+#define CLEAR_REPLACE_CYCLES 3
+
+struct listener_thread_args {
+    SSL *listener;
+    CRYPTO_THREAD *thread;
+    int result;
+};
+
+static unsigned int listener_thread(void *arg)
+{
+    struct listener_thread_args *ta = arg;
+    SSL_POLL_ITEM item;
+    struct timeval timeout;
+    size_t result_count;
+    int i;
+
+    ta->result = 0;
+    item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
+    item.desc.value.ssl = ta->listener;
+    item.events = SSL_POLL_EVENT_IC;
+    timeout.tv_sec = 0;
+    timeout.tv_usec = 1000;
+
+    for (i = 0; i < LISTENER_RACE_ITERATIONS; i++) {
+        item.revents = 0;
+        if (!SSL_poll(&item, 1, sizeof(item), &timeout, 0, &result_count))
+            return 0;
+        OSSL_sleep(1);
+    }
+
+    ta->result = 1;
+    return 0;
+}
+
+struct client_traffic_thread_args {
+    SSL *client;
+    CRYPTO_THREAD *thread;
+    int result;
+};
+
+static unsigned int client_traffic_thread(void *arg)
+{
+    struct client_traffic_thread_args *ta = arg;
+    size_t written;
+    int i, ret, err;
+
+    ta->result = 0;
+    for (i = 0; i < CLIENT_TRAFFIC_ITERATIONS; i++) {
+        ret = SSL_write_ex(ta->client, CLIENT_TO_SERVER_MSG,
+            strlen(CLIENT_TO_SERVER_MSG), &written);
+        if (ret <= 0) {
+            err = SSL_get_error(ta->client, ret);
+            if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE)
+                return 0;
+        }
+        OSSL_sleep(1);
+    }
+
+    ta->result = 1;
+    return 0;
+}
+
+/*
+ * Exercise concurrent listener polling while pending connections enter their
+ * first SSL_accept(), which calls SSL_clear(). This covers listener state that
+ * must remain stable while d1 is reset or replaced. ThreadSanitizer verifies
+ * that the listener threads do not race on that state.
+ */
+static int test_dtls_listener_clear_race(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *listener = NULL;
+    SSL *clients[NUM_CLIENTS] = { NULL };
+    SSL *server_conns[NUM_CLIENTS] = { NULL };
+    struct listener_thread_args thread_args[NUM_LISTENER_THREADS];
+    BIO_ADDR *server_addr = NULL;
+    int server_fd = -1;
+    int client_fds[NUM_CLIENTS] = { -1, -1, -1 };
+    int testresult = 0;
+    int i;
+
+    memset(thread_args, 0, sizeof(thread_args));
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), 0, 0, &sctx, &cctx, cert, privkey))
+        || !TEST_true(create_listener_ex(sctx, SSL_LISTENER_FLAG_NO_VALIDATE,
+            &listener, &server_addr, &server_fd))
+        || !TEST_true(SSL_set_blocking_mode(listener, 0)))
+        goto err;
+
+    for (i = 0; i < NUM_CLIENTS; i++) {
+        if (!TEST_true(create_client(cctx, server_addr, &clients[i],
+                &client_fds[i])))
+            goto err;
+    }
+
+    for (i = 0; i < NUM_LISTENER_THREADS; i++) {
+        thread_args[i].listener = listener;
+        thread_args[i].thread = ossl_crypto_thread_native_start(
+            listener_thread, &thread_args[i], 1);
+        if (!TEST_ptr(thread_args[i].thread))
+            goto err;
+    }
+
+    for (i = 0; i < NUM_CLIENTS; i++)
+        if (!TEST_true(do_handshake(clients[i], listener, &server_conns[i])))
+            goto err;
+
+    for (i = 0; i < NUM_LISTENER_THREADS; i++) {
+        ossl_crypto_thread_native_join(thread_args[i].thread, NULL);
+        ossl_crypto_thread_native_clean(thread_args[i].thread);
+        thread_args[i].thread = NULL;
+        if (!TEST_int_eq(thread_args[i].result, 1))
+            goto err;
+    }
+
+    testresult = 1;
+err:
+    for (i = 0; i < NUM_LISTENER_THREADS; i++) {
+        if (thread_args[i].thread != NULL) {
+            ossl_crypto_thread_native_join(thread_args[i].thread, NULL);
+            ossl_crypto_thread_native_clean(thread_args[i].thread);
+        }
+    }
+    for (i = 0; i < NUM_CLIENTS; i++) {
+        SSL_free(clients[i]);
+        SSL_free(server_conns[i]);
+        if (client_fds[i] >= 0)
+            BIO_closesocket(client_fds[i]);
+    }
+    SSL_free(listener);
+    BIO_ADDR_free(server_addr);
+    if (server_fd >= 0)
+        BIO_closesocket(server_fd);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}
+
+/* Exercise the SSL_clear() path which replaces d1 after a handshake. */
+static int test_dtls_listener_clear_replace_race(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *listener = NULL, *client = NULL, *server_conn = NULL;
+    struct listener_thread_args thread_args[NUM_LISTENER_THREADS];
+    struct client_traffic_thread_args traffic_args;
+    BIO_ADDR *server_addr = NULL;
+    int server_fd = -1, client_fd = -1;
+    int testresult = 0;
+    int i;
+
+    memset(thread_args, 0, sizeof(thread_args));
+    memset(&traffic_args, 0, sizeof(traffic_args));
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), 0, 0, &sctx, &cctx, cert, privkey))
+        || !TEST_true(create_listener_ex(sctx, SSL_LISTENER_FLAG_NO_VALIDATE,
+            &listener, &server_addr, &server_fd))
+        || !TEST_true(SSL_set_blocking_mode(listener, 0))
+        || !TEST_true(create_client(cctx, server_addr, &client, &client_fd))
+        || !TEST_true(do_handshake(client, listener, &server_conn)))
+        goto err;
+
+    for (i = 0; i < NUM_LISTENER_THREADS; i++) {
+        thread_args[i].listener = listener;
+        thread_args[i].thread = ossl_crypto_thread_native_start(
+            listener_thread, &thread_args[i], 1);
+        if (!TEST_ptr(thread_args[i].thread))
+            goto err;
+    }
+
+    for (i = 0; i < CLEAR_REPLACE_CYCLES; i++) {
+        traffic_args.client = client;
+        traffic_args.result = 0;
+        traffic_args.thread = ossl_crypto_thread_native_start(
+            client_traffic_thread, &traffic_args, 1);
+        if (!TEST_ptr(traffic_args.thread))
+            goto err;
+
+        OSSL_sleep(2);
+        if (!TEST_true(SSL_clear(server_conn)))
+            goto err;
+
+        /* Let the listener threads route client datagrams through the reset. */
+        OSSL_sleep(5);
+
+        ossl_crypto_thread_native_join(traffic_args.thread, NULL);
+        ossl_crypto_thread_native_clean(traffic_args.thread);
+        traffic_args.thread = NULL;
+        if (!TEST_int_eq(traffic_args.result, 1)
+            || !TEST_true(SSL_clear(client)))
+            goto err;
+
+        SSL_set_accept_state(server_conn);
+        SSL_set_connect_state(client);
+        if (!TEST_true(create_ssl_connection(server_conn, client,
+                SSL_ERROR_NONE)))
+            goto err;
+    }
+
+    for (i = 0; i < NUM_LISTENER_THREADS; i++) {
+        ossl_crypto_thread_native_join(thread_args[i].thread, NULL);
+        ossl_crypto_thread_native_clean(thread_args[i].thread);
+        thread_args[i].thread = NULL;
+        if (!TEST_int_eq(thread_args[i].result, 1))
+            goto err;
+    }
+
+    testresult = 1;
+err:
+    if (traffic_args.thread != NULL) {
+        ossl_crypto_thread_native_join(traffic_args.thread, NULL);
+        ossl_crypto_thread_native_clean(traffic_args.thread);
+    }
+    for (i = 0; i < NUM_LISTENER_THREADS; i++) {
+        if (thread_args[i].thread != NULL) {
+            ossl_crypto_thread_native_join(thread_args[i].thread, NULL);
+            ossl_crypto_thread_native_clean(thread_args[i].thread);
+        }
+    }
+    SSL_free(client);
+    SSL_free(server_conn);
+    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;
+}
+
 /*
  * Main test: multiple threads polling on different DTLS connections
  */
@@ -796,6 +1039,8 @@ int setup_tests(void)
         return 0;

     ADD_TEST(test_dtls_multithread);
+    ADD_TEST(test_dtls_listener_clear_race);
+    ADD_TEST(test_dtls_listener_clear_replace_race);
     ADD_TEST(test_dtls_blocking_accept);
     ADD_ALL_TESTS(test_dtls_blocking_read, 2);
     return 1;
diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index d25d690aed..e77867bdf8 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -5457,6 +5457,7 @@ static int test_dtls_blocking_mode(void)
     SSL *memlistener = NULL, *memclient = NULL, *plainssl = NULL;
     BIO_ADDR *server_addr = NULL, *client_addr = NULL;
     SSL_CONNECTION *sc;
+    DTLS_RX *listener_rx;
     int server_fd = -1, client_fd = -1;
     int testresult = 0;

@@ -5537,12 +5538,17 @@ static int test_dtls_blocking_mode(void)
      */
     if (!TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(serverssl)))
         goto end;
-    sc->d1->being_driven = 1;
+    listener_rx = sc->listener_rx;
+    if (!TEST_ptr(listener_rx) || !TEST_ptr_eq(listener_rx, sc->d1->rx))
+        goto end;
+    sc->listener_being_driven = 1;

     if (!TEST_true(SSL_clear(serverssl))
         || !TEST_int_eq(SSL_get_blocking_mode(serverssl), 0)
         || !TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(serverssl))
-        || !TEST_int_eq(sc->d1->being_driven, 1))
+        || !TEST_ptr_eq(sc->listener_rx, listener_rx)
+        || !TEST_ptr_eq(sc->d1->rx, listener_rx)
+        || !TEST_int_eq(sc->listener_being_driven, 1))
         goto end;

     /*
@@ -5554,10 +5560,12 @@ static int test_dtls_blocking_mode(void)
     if (!TEST_true(SSL_clear(serverssl))
         || !TEST_int_eq(SSL_get_blocking_mode(serverssl), 0)
         || !TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(serverssl))
-        || !TEST_int_eq(sc->d1->being_driven, 1))
+        || !TEST_ptr_eq(sc->listener_rx, listener_rx)
+        || !TEST_ptr_eq(sc->d1->rx, listener_rx)
+        || !TEST_int_eq(sc->listener_being_driven, 1))
         goto end;

-    sc->d1->being_driven = 0;
+    sc->listener_being_driven = 0;

     /* And back the other way round. */
     if (!TEST_true(SSL_set_blocking_mode(listener, 1))
diff --git a/test/recipes/80-test_dtls_listener_races.t b/test/recipes/80-test_dtls_listener_races.t
new file mode 100644
index 0000000000..1fe74cbcbe
--- /dev/null
+++ b/test/recipes/80-test_dtls_listener_races.t
@@ -0,0 +1,34 @@
+#! /usr/bin/env perl
+# Copyright 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 may obtain a copy
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use OpenSSL::Test::Utils;
+use OpenSSL::Test qw/:DEFAULT srctop_file/;
+
+setup("test_dtls_listener_races");
+
+plan skip_all => "test_dtls_listener_races needs the sock feature enabled"
+    if disabled("sock");
+
+plan skip_all => "test_dtls_listener_races needs DTLS enabled"
+    if disabled("dtls");
+
+plan skip_all => "test_dtls_listener_races needs the threads feature enabled"
+    if disabled("threads");
+
+plan tests => 2;
+
+my @credentials = (srctop_file("apps", "server.pem"),
+                   srctop_file("apps", "server.pem"));
+
+ok(run(test(["dtls_multithread_test", "-test",
+             "test_dtls_listener_clear_race", @credentials])),
+   "running in-place listener clear race test");
+
+ok(run(test(["dtls_multithread_test", "-test",
+             "test_dtls_listener_clear_replace_race", @credentials])),
+   "running replacement listener clear race test");