Commit e4d3898697 for openssl.org
commit e4d3898697cf553f45989db145d8a57842082fc6
Author: Matt Caswell <matt@openssl.foundation>
Date: Wed Aug 12 10:18:58 2026 +0100
DTLS 1.3 Support blocking reads on listener connections
A connection created by a DTLS listener has no BIO of its own. The
listener owns the socket and demultiplexes datagrams into a per
connection queue, so there is nothing for a read to block in, and every
read on an empty queue reported WANT_READ however the connection was
configured. SSL_set_blocking_mode() therefore had no effect on reads.
Wait in the record layer instead, where a blocking BIO would have. When
rlayer_dtls_get_urxe_packet() finds the queue empty and the connection is
in blocking mode, ossl_dtls_conn_wait_for_datagram() waits for one to
arrive. The state machine above is untouched, so SSL_read(), SSL_peek(),
SSL_do_handshake() and SSL_accept() all become blocking together, which
is what an application moving from a plain DTLS object expects.
The wait loops, because the socket is shared: a wakeup may be for a
datagram belonging to a different connection, in which case our queue is
still empty and there is nothing to return. It also handles events after
each wakeup, since while a thread is in here nothing else services the
connection, and the retransmission timer has to keep running - the poll
translation folds the event timeout into the deadline so the wait ends in
time for it.
Existing tests drive both ends of a connection from a single thread,
which cannot work if the server side blocks, so create_dtls_listener() in
dtlsssllistenertest.c now turns blocking mode off for its callers, with
create_dtls_listener_unconfigured() for the one test which has to observe
the default. test_dtls_multithread opts out for the same reason.
The new test_dtls_blocking_read has the client stay silent for a while
after its handshake, so the server thread is already parked in
SSL_read_ex() when the write finally comes. Its second iteration
handshakes in non-blocking mode and switches to blocking only for the
read, which pins the read path on its own: in the first iteration the
handshake is what fails without this change, and the read is never
reached.
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:57 2026
Merged-from: https://github.com/openssl/openssl/pull/32324
diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index 92ac3751e4..79e86a97ec 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -3131,6 +3131,70 @@ int ossl_dtls_get_blocking_mode(const SSL *s)
return ossl_dtls_blocking(s);
}
+/*
+ * Wait until a datagram has been demultiplexed to this connection's receive
+ * queue, for a connection which is in blocking mode.
+ *
+ * This is what makes a blocking read on a listener based connection block. Such
+ * a connection has no BIO of its own to block in: it reads from a queue which
+ * the listener fills, so the wait has to happen here instead.
+ *
+ * A wakeup does not mean the datagram was ours - the listener's socket is
+ * shared, and another connection may be the one with data - so this loops until
+ * something actually lands in our queue. Events are handled after each wait
+ * because nothing else will do it while we are in here, and the retransmission
+ * timer needs servicing if it is what woke us.
+ *
+ * A datagram which is already waiting costs nothing: the wait pumps the
+ * listener's demux while translating the poll, and returns without sleeping if
+ * anything has been queued for us by then.
+ *
+ * Returns 1 if a datagram is now queued for this connection, or 0 if the wait
+ * could not be performed or the listener has failed.
+ */
+int ossl_dtls_conn_wait_for_datagram(SSL *s)
+{
+ SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL_ONLY(s);
+ DTLS_LISTENER *dl;
+ int empty;
+
+ if (sc == NULL || sc->d1 == NULL || sc->d1->rx == NULL
+ || sc->d1->listener == NULL)
+ return 0;
+
+ dl = (DTLS_LISTENER *)sc->d1->listener;
+
+ for (;;) {
+ ossl_crypto_mutex_lock(dl->mutex);
+ if (dl->fatal) {
+ ossl_crypto_mutex_unlock(dl->mutex);
+ return 0;
+ }
+ ossl_crypto_mutex_unlock(dl->mutex);
+
+ /*
+ * An infinite deadline here is bounded by the connection's own event
+ * timeout, which the poll translation folds in, so this still wakes in
+ * time to retransmit.
+ */
+ if (!ossl_dtls_block_until_ready(s, SSL_POLL_EVENT_R,
+ ossl_time_infinite()))
+ return 0;
+
+ if (!SSL_handle_events(s))
+ return 0;
+
+ ossl_dgram_demux_pump(sc->d1->rx->demux);
+
+ ossl_crypto_mutex_lock(sc->d1->rx->mutex);
+ empty = ossl_list_urxe_is_empty(&sc->d1->rx->urxe_pending);
+ ossl_crypto_mutex_unlock(sc->d1->rx->mutex);
+
+ if (!empty)
+ return 1;
+ }
+}
+
void ossl_dtls_listener_enter_blocking_section(SSL *s)
{
DTLS_LISTENER *dl;
diff --git a/ssl/record/rec_layer_s3.c b/ssl/record/rec_layer_s3.c
index 69596f8a59..81092cc2a2 100644
--- a/ssl/record/rec_layer_s3.c
+++ b/ssl/record/rec_layer_s3.c
@@ -1176,6 +1176,19 @@ static int rlayer_dtls_get_urxe_packet(void *cbarg, unsigned char **data,
urxe = ossl_dtls_read_datagram(s->d1->rx);
}
+ /*
+ * Still nothing. In blocking mode this is where the caller waits: the
+ * connection has no BIO of its own to block in, so returning here would
+ * report SSL_ERROR_WANT_READ instead of blocking. Waiting inside this
+ * callback keeps that out of the record layer and the state machine, which
+ * see only a read which took a while, exactly as a blocking BIO would give
+ * them.
+ */
+ if (urxe == NULL && s->d1->listener != NULL
+ && ossl_dtls_blocking(SSL_CONNECTION_GET_SSL(s))
+ && ossl_dtls_conn_wait_for_datagram(SSL_CONNECTION_GET_SSL(s)))
+ urxe = ossl_dtls_read_datagram(s->d1->rx);
+
if (urxe == NULL)
return 0;
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index cc059ca647..524317b4c4 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -3102,6 +3102,7 @@ int ossl_dtls_block_until_ready(SSL *ssl, uint64_t events, OSSL_TIME deadline);
int ossl_dtls_blocking(const SSL *s);
int ossl_dtls_set_blocking_mode(SSL *s, int blocking);
int ossl_dtls_get_blocking_mode(const SSL *s);
+int ossl_dtls_conn_wait_for_datagram(SSL *s);
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 8d312a40cc..278c403ad5 100644
--- a/test/dtls_multithread_test.c
+++ b/test/dtls_multithread_test.c
@@ -355,6 +355,16 @@ static int test_dtls_multithread(void)
if (!TEST_true(create_listener(sctx, &listener, &server_addr, &server_fd)))
goto err;
+ /*
+ * do_handshake() below drives both ends from this thread, so the server
+ * side must not block: a blocking read would wait for a client which only
+ * this thread can advance. Connections accepted from the listener inherit
+ * this. The worker threads which follow use SSL_poll() and expect
+ * SSL_ERROR_WANT_READ, so they need it too.
+ */
+ if (!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;
@@ -503,6 +513,13 @@ static int test_dtls_blocking_accept(void)
if (!TEST_true(create_listener(sctx, &listener, &server_addr, &server_fd)))
goto err;
+ /*
+ * This test needs the blocking accept, so ask for it rather than relying
+ * on the default.
+ */
+ if (!TEST_true(SSL_set_blocking_mode(listener, 1)))
+ 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
@@ -562,6 +579,209 @@ err:
return testresult;
}
+/*
+ * How long the client waits, in milliseconds, after its handshake completes
+ * before sending anything. The server thread has to still be inside its
+ * blocking read when the write finally happens, or the test proves nothing.
+ *
+ * A machine slow enough to get there late does not make the test fail: the read
+ * then finds the datagram already queued and returns it, which is a pass with
+ * nothing demonstrated. Only the absence of blocking turns it into a failure.
+ */
+#define BLOCKING_READ_QUIET_MS 250
+
+/*
+ * Per-thread state for the blocking read
+ */
+struct read_thread_args {
+ SSL *listener;
+ SSL *conn; /* connection the accept returned */
+ CRYPTO_THREAD *thread;
+ char buf[256];
+ size_t readbytes;
+ int nonblocking_handshake; /* handshake without blocking mode */
+ int result; /* 1 = success, 0 = failure */
+};
+
+/*
+ * Helper: complete the handshake with the connection in non-blocking mode,
+ * driving the listener by hand, so that the blocking read which follows is the
+ * only thing relying on the emulation.
+ */
+static int nonblocking_handshake(struct read_thread_args *ta)
+{
+ int i, ret = -1, err;
+
+ if (!TEST_true(SSL_set_blocking_mode(ta->conn, 0)))
+ return 0;
+
+ for (i = 0; i < 200; i++) {
+ ret = SSL_accept(ta->conn);
+ if (ret == 1)
+ break;
+ err = SSL_get_error(ta->conn, ret);
+ if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
+ TEST_error("SSL_accept failed (err %d)", err);
+ return 0;
+ }
+ /* Nothing else will service the listener while we are in here. */
+ if (!TEST_true(SSL_handle_events(ta->listener)))
+ return 0;
+ OSSL_sleep(10);
+ }
+
+ if (!TEST_int_eq(ret, 1))
+ return 0;
+
+ return TEST_true(SSL_set_blocking_mode(ta->conn, 1));
+}
+
+/*
+ * Thread function: accept a connection, complete its handshake and read from
+ * it, so that the thread never has to handle WANT_READ.
+ */
+static unsigned int blocking_read_thread(void *arg)
+{
+ struct read_thread_args *ta = (struct read_thread_args *)arg;
+
+ ta->conn = SSL_accept_connection(ta->listener, 0);
+ if (!TEST_ptr(ta->conn))
+ return 0;
+
+ /*
+ * A listener-created connection has no BIO of its own to block in - it
+ * reads from a queue the listener demultiplexes into - so without the
+ * emulation these calls return immediately with WANT_READ instead of
+ * waiting.
+ */
+ if (ta->nonblocking_handshake) {
+ if (!nonblocking_handshake(ta))
+ return 0;
+ } else if (!TEST_int_gt(SSL_accept(ta->conn), 0)) {
+ return 0;
+ }
+
+ if (!TEST_true(SSL_read_ex(ta->conn, ta->buf, sizeof(ta->buf) - 1,
+ &ta->readbytes)))
+ return 0;
+
+ ta->result = 1;
+ return 0;
+}
+
+/*
+ * Test that a blocking listener connection waits for a datagram rather than
+ * reporting WANT_READ.
+ *
+ * The client stays silent for BLOCKING_READ_QUIET_MS after its own handshake
+ * completes, so by the time it writes, the server thread is already parked in
+ * SSL_read_ex() with nothing to return. A non-blocking connection reports
+ * WANT_READ immediately, so the absence of the emulation shows up as a failed
+ * assertion rather than as a hang.
+ *
+ * idx 0 blocks in the handshake as well as in the read. idx 1 handshakes in
+ * non-blocking mode and only then switches the connection to blocking, which
+ * leaves the read as the sole assertion the emulation has to satisfy - without
+ * it, idx 0 fails at SSL_accept() and never reaches the read, so on its own it
+ * would not tell us the read path works.
+ *
+ * Only the client is driven from this thread: the accepting thread ticks the
+ * listener itself, which is what lets a blocked connection make progress at
+ * all.
+ */
+static int test_dtls_blocking_read(int idx)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *listener = NULL, *client = NULL;
+ struct read_thread_args read_args;
+ BIO_ADDR *server_addr = NULL;
+ int server_fd = -1, client_fd = -1;
+ int testresult = 0;
+ size_t written;
+ int i, ret = -1, err;
+
+ memset(&read_args, 0, sizeof(read_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;
+
+ /* Blocking is the default, but this test depends on it, so be explicit. */
+ if (!TEST_true(SSL_set_blocking_mode(listener, 1)))
+ goto err;
+
+ /*
+ * As in test_dtls_blocking_accept, create the client's socket before the
+ * thread which will park in the blocking accept, because nothing can
+ * release that thread except a connection arriving. Nothing is sent until
+ * SSL_connect() below, so the accept still waits.
+ */
+ if (!TEST_true(create_client(cctx, server_addr, &client, &client_fd)))
+ goto err;
+
+ read_args.listener = listener;
+ read_args.nonblocking_handshake = idx;
+ read_args.thread = ossl_crypto_thread_native_start(blocking_read_thread,
+ &read_args, 1);
+ if (!TEST_ptr(read_args.thread))
+ goto err;
+
+ /* Drive the client's handshake to completion. */
+ SSL_set_connect_state(client);
+ for (i = 0; i < 200; i++) {
+ ret = SSL_connect(client);
+ if (ret == 1)
+ break;
+ err = SSL_get_error(client, ret);
+ if (err != SSL_ERROR_WANT_READ && err != SSL_ERROR_WANT_WRITE) {
+ TEST_error("SSL_connect failed (err %d)", err);
+ goto err;
+ }
+ OSSL_sleep(10);
+ }
+ if (!TEST_int_eq(ret, 1))
+ goto err;
+
+ /* Let the server thread reach its read and find nothing there. */
+ OSSL_sleep(BLOCKING_READ_QUIET_MS);
+
+ if (!TEST_true(SSL_write_ex(client, CLIENT_TO_SERVER_MSG,
+ strlen(CLIENT_TO_SERVER_MSG), &written)))
+ goto err;
+
+ ossl_crypto_thread_native_join(read_args.thread, NULL);
+ ossl_crypto_thread_native_clean(read_args.thread);
+ read_args.thread = NULL;
+
+ if (!TEST_int_eq(read_args.result, 1))
+ goto err;
+
+ read_args.buf[read_args.readbytes] = '\0';
+ if (!TEST_str_eq(read_args.buf, CLIENT_TO_SERVER_MSG))
+ goto err;
+
+ testresult = 1;
+err:
+ if (read_args.thread != NULL) {
+ ossl_crypto_thread_native_join(read_args.thread, NULL);
+ ossl_crypto_thread_native_clean(read_args.thread);
+ }
+ SSL_free(read_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))
@@ -570,5 +790,6 @@ int setup_tests(void)
ADD_TEST(test_dtls_multithread);
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 b93dc268cc..9bc395f359 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -115,7 +115,7 @@ static int dtls_read_with_retry(SSL *ssl, void *buf, size_t bufsize,
* Returns 1 on success, 0 on failure.
* On success, caller is responsible for cleanup using the returned pointers/fds.
*/
-static int create_dtls_listener(SSL_CTX *sctx, uint64_t listener_flags,
+static int create_dtls_listener_unconfigured(SSL_CTX *sctx, uint64_t listener_flags,
SSL **listener, BIO_ADDR **server_addr, int *server_fd)
{
BIO *listener_bio = NULL;
@@ -183,6 +183,38 @@ err:
return ret;
}
+/*
+ * As create_dtls_listener_unconfigured(), but also opts out of blocking mode.
+ *
+ * These tests drive both ends of a handshake from a single thread, so the server
+ * side must not block: a blocking read would wait for a client which only this
+ * thread can advance. A listener is blocking by default, so opt out here rather
+ * than in each test, and note that connections accepted from it inherit this.
+ *
+ * A test which needs to observe the default, or to exercise blocking mode, should
+ * use create_dtls_listener_unconfigured() and configure what it needs.
+ */
+static int create_dtls_listener(SSL_CTX *sctx, uint64_t listener_flags,
+ SSL **listener, BIO_ADDR **server_addr, int *server_fd)
+{
+ if (!create_dtls_listener_unconfigured(sctx, listener_flags, listener,
+ server_addr, server_fd))
+ return 0;
+
+ if (!TEST_true(SSL_set_blocking_mode(*listener, 0))) {
+ SSL_free(*listener);
+ BIO_ADDR_free(*server_addr);
+ if (*server_fd >= 0)
+ BIO_closesocket(*server_fd);
+ *listener = NULL;
+ *server_addr = NULL;
+ *server_fd = -1;
+ return 0;
+ }
+
+ return 1;
+}
+
/*
* Helper to create a DTLS client connected to a server address.
*
@@ -5421,8 +5453,15 @@ static int test_dtls_blocking_mode(void)
privkey)))
goto end;
- /* A socket BIO supplies a poll descriptor, so blocking is available. */
- if (!TEST_true(create_dtls_listener(sctx,
+ /*
+ * create_dtls_listener() turns blocking mode off on behalf of the single
+ * threaded tests, so it cannot be used here: this test has to see the mode
+ * a listener has when the application has never set it. Hence the
+ * unconfigured variant.
+ *
+ * A socket BIO supplies a poll descriptor, so blocking is available.
+ */
+ if (!TEST_true(create_dtls_listener_unconfigured(sctx,
SSL_LISTENER_FLAG_REQUIRE_HVR | SSL_LISTENER_FLAG_REQUIRE_HRR
| SSL_LISTENER_FLAG_SINGLE_THREAD,
&listener, &server_addr, &server_fd)))