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");