Commit 63bd920445 for openssl.org
commit 63bd920445f5339aa0ccaf07475df7abac2c8632
Author: Matt Caswell <matt@openssl.foundation>
Date: Mon Oct 19 10:58:18 2026 +0100
Don't report held-back DTLS app data as readable in SSL_poll()
ossl_dtls_conn_poll_events() reported SSL_POLL_EVENT_R whenever
SSL_has_pending() was true. For DTLS that includes records in
buffered_app_data, which dtls1_read_bytes() only releases once the
handshake has finished.
In DTLS 1.3 the sender of a KeyUpdate stays in init until it is ACKed.
If the peer sends application data without ACKing, e.g.:
1. Server sends KeyUpdate, enters TLS_ST_SW_KEY_UPDATE.
2. Client sends a record before reading the KeyUpdate.
3. Server's SSL_read() buffers the record, returns WANT_READ.
then every SSL_poll() returns at once with SSL_POLL_EVENT_R and every
SSL_read() returns SSL_ERROR_WANT_READ. An event loop spins until the
ACK arrives or the KeyUpdate retransmissions run out (483s). The same
happens to a client, and in DTLS 1.2 for data received between
ChangeCipherSpec and Finished.
Only count buffered_app_data once the handshake has finished, and
otherwise wait for incoming datagrams as usual.
Discovered by Anthropic's OSS Scanner (finding ANT-2026-QCQMEE2S).
Assisted-by: Claude Code:claude-opus-5-5
Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
Reviewed-by: Ryan Hooper <ryanh@openssl.foundation>
Merge-date: Wed Oct 21 09:21:56 2026
Merged-from: https://github.com/openssl/openssl/pull/33262
diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index c4b01779c9..c4a3d32bcd 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -3385,7 +3385,17 @@ int ossl_dtls_conn_poll_events(SSL *s, uint64_t events, int do_tick,
SSL_handle_events(s);
if ((events & SSL_POLL_EVENT_R) != 0) {
- if (SSL_has_pending(s) || SSL_pending(s) > 0) {
+ /*
+ * Application data buffered during a handshake cannot be read until
+ * the handshake finishes. Until then, reporting it would make
+ * SSL_poll() return immediately while SSL_read() keeps failing with
+ * SSL_ERROR_WANT_READ, so wait for the datagrams that complete the
+ * handshake instead.
+ */
+ if (RECORD_LAYER_processed_read_pending(&sc->rlayer)
+ || RECORD_LAYER_read_pending(&sc->rlayer)
+ || (SSL_is_init_finished(s)
+ && DTLS_RECORD_LAYER_buffered_app_data_pending(&sc->rlayer))) {
result |= SSL_POLL_EVENT_R;
} else if (sc->d1->rx != NULL) {
/* Listener-based connection: check URXE queue */
diff --git a/ssl/record/rec_layer_d1.c b/ssl/record/rec_layer_d1.c
index 19072af9cd..1c9ae2e138 100644
--- a/ssl/record/rec_layer_d1.c
+++ b/ssl/record/rec_layer_d1.c
@@ -72,6 +72,25 @@ void DTLS_RECORD_LAYER_clear(RECORD_LAYER *rl)
d->buffered_app_data = buffered_app_data;
}
+/*
+ * Checks for application data set aside while a handshake was in progress.
+ * dtls1_read_bytes() only returns it once the handshake has finished.
+ */
+int DTLS_RECORD_LAYER_buffered_app_data_pending(const RECORD_LAYER *rl)
+{
+ TLS_RECORD *rdata;
+ pitem *item, *iter;
+
+ iter = pqueue_iterator(rl->d->buffered_app_data);
+ while ((item = pqueue_next(&iter)) != NULL) {
+ rdata = item->data;
+ if (rdata->length > 0)
+ return 1;
+ }
+
+ return 0;
+}
+
static int dtls_buffer_record(SSL_CONNECTION *s, TLS_RECORD *rec)
{
TLS_RECORD *rdata;
diff --git a/ssl/record/record.h b/ssl/record/record.h
index 13b709c2bf..c622c1f0b4 100644
--- a/ssl/record/record.h
+++ b/ssl/record/record.h
@@ -165,6 +165,7 @@ __owur int ssl3_read_bytes(SSL *s, uint8_t type, uint8_t *recvd_type,
int DTLS_RECORD_LAYER_new(RECORD_LAYER *rl);
void DTLS_RECORD_LAYER_free(RECORD_LAYER *rl);
void DTLS_RECORD_LAYER_clear(RECORD_LAYER *rl);
+int DTLS_RECORD_LAYER_buffered_app_data_pending(const RECORD_LAYER *rl);
__owur int dtls1_read_bytes(SSL *s, uint8_t type, uint8_t *recvd_type,
unsigned char *buf, size_t len, int peek,
size_t *readbytes);
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index c71d7f59c6..b40783a21e 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -2116,17 +2116,9 @@ int SSL_has_pending(const SSL *s)
sc = SSL_CONNECTION_FROM_CONST_SSL(s);
/* Check buffered app data if any first */
- if (SSL_CONNECTION_IS_DTLS(sc)) {
- TLS_RECORD *rdata;
- pitem *item, *iter;
-
- iter = pqueue_iterator(sc->rlayer.d->buffered_app_data);
- while ((item = pqueue_next(&iter)) != NULL) {
- rdata = item->data;
- if (rdata->length > 0)
- return 1;
- }
- }
+ if (SSL_CONNECTION_IS_DTLS(sc)
+ && DTLS_RECORD_LAYER_buffered_app_data_pending(&sc->rlayer))
+ return 1;
if (RECORD_LAYER_processed_read_pending(&sc->rlayer))
return 1;
diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index 06c783cff9..79bf863d00 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -3551,6 +3551,162 @@ end:
return testresult;
}
+#ifndef OPENSSL_NO_DTLS1_3
+static int poll_conn_readable(SSL *s, int *readable)
+{
+ SSL_POLL_ITEM item;
+ struct timeval timeout = { 0, 0 };
+ size_t count;
+
+ item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
+ item.desc.value.ssl = s;
+ item.events = SSL_POLL_EVENT_R;
+ item.revents = 0;
+
+ if (!TEST_true(SSL_poll(&item, 1, sizeof(item), &timeout, 0, &count)))
+ return 0;
+ *readable = (item.revents & SSL_POLL_EVENT_R) != 0;
+ return 1;
+}
+
+/*
+ * Test: SSL_poll() does not report SSL_POLL_EVENT_R for application data that
+ * cannot be read until a handshake in progress completes.
+ *
+ * In DTLS 1.3 the sender of a KeyUpdate stays in init until it is ACKed, and
+ * application data arriving meanwhile is held back until then. If SSL_poll()
+ * reports that data as readable, an event loop spins: every SSL_read() returns
+ * SSL_ERROR_WANT_READ and every SSL_poll() returns immediately, until the peer
+ * sends its ACK or the retransmissions run out.
+ *
+ * idx 0: the server (a listener connection) sends the KeyUpdate
+ * idx 1: the client (a standalone connection) sends the KeyUpdate
+ */
+static int test_dtls13_poll_conn_held_app_data(int idx)
+{
+ SSL_CTX *sctx = NULL, *cctx = NULL;
+ SSL *listener = NULL;
+ SSL *serverssl = NULL, *clientssl = NULL;
+ SSL *updater, *peer;
+ BIO_ADDR *client_addr = NULL;
+ unsigned char buf[16];
+ size_t n;
+ int ret, readable = 0;
+ int testresult = 0;
+ int abortctr = 0;
+ SSL_POLL_ITEM poll_item;
+ struct timeval poll_timeout;
+ size_t poll_result;
+
+ if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+ DTLS_client_method(), DTLS1_3_VERSION, DTLS1_3_VERSION,
+ &sctx, &cctx, cert, privkey)))
+ goto end;
+
+ if (!TEST_true(create_dtls_listener_and_client_mem(sctx, cctx,
+ SSL_LISTENER_FLAG_SINGLE_THREAD,
+ &listener, &clientssl, &client_addr)))
+ goto end;
+
+ SSL_set_connect_state(clientssl);
+ while (serverssl == NULL) {
+ if (++abortctr > 100) {
+ TEST_error("Connection loop did not converge");
+ goto end;
+ }
+
+ ret = SSL_connect(clientssl);
+ if (ret <= 0 && SSL_get_error(clientssl, ret) != SSL_ERROR_WANT_READ) {
+ TEST_error("SSL_connect failed");
+ goto end;
+ }
+
+ 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 = 0;
+
+ if (!TEST_true(SSL_poll(&poll_item, 1, sizeof(poll_item), &poll_timeout, 0, &poll_result)))
+ goto end;
+
+ if (poll_result > 0 && (poll_item.revents & SSL_POLL_EVENT_IC) != 0)
+ serverssl = SSL_accept_connection(listener, SSL_ACCEPT_CONNECTION_NO_BLOCK);
+ }
+
+ if (!TEST_ptr(serverssl)
+ || !TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE)))
+ goto end;
+
+ /* Consume the tickets and their ACKs so that nothing is left to read. */
+ ret = SSL_read(clientssl, buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(clientssl, ret), SSL_ERROR_WANT_READ))
+ goto end;
+ ret = SSL_read(serverssl, buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(serverssl, ret), SSL_ERROR_WANT_READ))
+ goto end;
+
+ updater = idx == 0 ? serverssl : clientssl;
+ peer = idx == 0 ? clientssl : serverssl;
+
+ if (!TEST_true(SSL_key_update(updater, SSL_KEY_UPDATE_NOT_REQUESTED)))
+ goto end;
+ ret = SSL_do_handshake(updater);
+ if (!TEST_int_eq(SSL_get_error(updater, ret), SSL_ERROR_WANT_READ)
+ || !TEST_int_eq(SSL_get_state(updater),
+ idx == 0 ? TLS_ST_SW_KEY_UPDATE : TLS_ST_CW_KEY_UPDATE))
+ goto end;
+
+ /* The peer sends data before it has read, and so ACKed, the KeyUpdate. */
+ if (!TEST_true(SSL_write_ex(peer, "x", 1, &n)))
+ goto end;
+
+ /* The updater reads that record but must hold it back until the ACK. */
+ ret = SSL_read(updater, buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(updater, ret), SSL_ERROR_WANT_READ)
+ || !TEST_true(SSL_in_init(updater))
+ || !TEST_true(SSL_has_pending(updater)))
+ goto end;
+
+ if (!poll_conn_readable(updater, &readable)
+ || !TEST_false(readable))
+ goto end;
+
+ /* The peer reads the KeyUpdate and ACKs it. */
+ ret = SSL_read(peer, buf, sizeof(buf));
+ if (!TEST_int_eq(SSL_get_error(peer, ret), SSL_ERROR_WANT_READ))
+ goto end;
+
+ /*
+ * The waiting ACK makes the updater readable. SSL_poll() does not process
+ * it, SSL_read() does: that completes the KeyUpdate and releases the held
+ * record.
+ */
+ if (!poll_conn_readable(updater, &readable)
+ || !TEST_true(readable)
+ || !TEST_true(SSL_in_init(updater))
+ || !TEST_true(SSL_read_ex(updater, buf, sizeof(buf), &n))
+ || !TEST_mem_eq(buf, n, "x", 1)
+ || !TEST_false(SSL_in_init(updater)))
+ goto end;
+
+ if (!poll_conn_readable(updater, &readable)
+ || !TEST_false(readable))
+ goto end;
+
+ testresult = 1;
+end:
+ SSL_free(serverssl);
+ SSL_free(clientssl);
+ SSL_free(listener);
+ BIO_ADDR_free(client_addr);
+ SSL_CTX_free(sctx);
+ SSL_CTX_free(cctx);
+ return testresult;
+}
+#endif /* OPENSSL_NO_DTLS1_3 */
+
/*
* Test: SSL_poll() on listener with multiple event types.
*
@@ -6686,6 +6842,9 @@ int setup_tests(void)
ADD_TEST(test_dtls_poll_conn_event_w);
ADD_TEST(test_dtls_poll_conn_dgram_pair_readable);
ADD_TEST(test_dtls_poll_conn_no_events_before_data);
+#ifndef OPENSSL_NO_DTLS1_3
+ ADD_ALL_TESTS(test_dtls13_poll_conn_held_app_data, 2);
+#endif
ADD_TEST(test_dtls_poll_listener_multiple_events);
ADD_TEST(test_dtls_poll_conn_event_ec);
ADD_TEST(test_dtls_poll_null_item);