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