Commit 87e65c6079 for openssl.org

commit 87e65c6079fab1ba556fbc1d358c2af455ac720c
Author: Matt Caswell <matt@openssl.foundation>
Date:   Fri Aug 7 16:26:20 2026 +0100

    DTLS 1.3 Honour the DTLS retransmit timer in SSL_poll

    When SSL_poll() decides to block it computes a wakeup deadline from the
    per-object event timeout, so that timer driven work is not delayed by the
    wait. This was done for QUIC objects but not for DTLS connections, whose
    timeout is the handshake retransmission timer.

    The consequence was that a poll with no user timeout would sleep until a
    datagram arrived, straight through the point at which the connection should
    have retransmitted. If the peer had itself lost the message we were waiting
    for, neither side would make progress.

    Fold SSL_get_event_timeout() into the deadline for DTLS connections as the
    QUIC branch already does. It reports the DTLS timer already, so no new
    plumbing is needed there.

    Waking at the deadline is only useful if something then services the timer.
    Unlike the QUIC case, where the readout ticks the reactor and that handles
    timeouts, the DTLS readout only pumped the listener's demux. Nothing
    retransmitted, and because an expired timer reports zero time remaining the
    recomputed deadline would be "now" on every subsequent wait, turning the
    sleep into a spin. Call SSL_handle_events() from the readout as well, which
    is what a caller polling without SSL_POLL_FLAG_NO_HANDLE_EVENTS is asking
    for, and which for a DTLS connection services that timer.

    Both of those are the calls the documentation asks new code to use:
    DTLSv1_get_timeout(3) and DTLSv1_handle_timeout(3) each record that
    SSL_get_event_timeout(3) and SSL_handle_events(3) respectively supersede all
    of their use cases. The test asserts its precondition through
    SSL_get_event_timeout() for the same reason, which has the side benefit of
    using the same call SSL_poll() uses to compute the deadline being tested.

    Assisted-by: Claude Code:claude-opus-5
    Reviewed-by: Ryan Hooper <ryanh@openssl.foundation>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Merge-date: Fri Aug 14 15:45:37 2026
    Merged-from: https://github.com/openssl/openssl/pull/32239

diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index fc978dd5bd..0c88f979e7 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -3048,6 +3048,18 @@ int ossl_dtls_conn_poll_events(SSL *s, uint64_t events, int do_tick,
         ossl_dtls_tick(dl);
     }

+    /*
+     * Handle events for the connection itself, which for DTLS means servicing
+     * the retransmission timer. The caller may have blocked until that timer
+     * expired, so if nothing retransmits here then nothing will, and the
+     * deadline would be recomputed as "now" on every subsequent wait.
+     *
+     * A failure here leaves the connection in a fatal error state, which the
+     * SSL_POLL_EVENT_EC check below reports.
+     */
+    if (do_tick)
+        SSL_handle_events(s);
+
     if ((events & SSL_POLL_EVENT_R) != 0) {
         if (SSL_has_pending(s) || SSL_pending(s) > 0) {
             result |= SSL_POLL_EVENT_R;
diff --git a/ssl/rio/poll_immediate.c b/ssl/rio/poll_immediate.c
index 7df9694538..3b32d679b8 100644
--- a/ssl/rio/poll_immediate.c
+++ b/ssl/rio/poll_immediate.c
@@ -453,7 +453,7 @@ static int poll_translate(SSL_POLL_ITEM *items,
     size_t result_count = 0;
     SSL *ssl;
     OSSL_TIME earliest_wakeup_deadline = ossl_time_infinite();
-#ifndef OPENSSL_NO_QUIC
+#if !defined(OPENSSL_NO_QUIC) || !defined(OPENSSL_NO_DTLS)
     struct timeval timeout;
     int is_infinite = 0;
 #endif
@@ -516,6 +516,20 @@ static int poll_translate(SSL_POLL_ITEM *items,
                     if (*abort_blocking)
                         goto out;

+                    /*
+                     * Bound the wait by the DTLS retransmission timer,
+                     * otherwise a poll with no timeout sleeps straight through
+                     * the point at which we should be retransmitting.
+                     */
+                    if (!SSL_get_event_timeout(ssl, &timeout, &is_infinite))
+                        FAIL_ITEM(i++); /* need to clean up this item too */
+
+                    if (!is_infinite)
+                        earliest_wakeup_deadline
+                            = ossl_time_min(earliest_wakeup_deadline,
+                                ossl_time_add(ossl_time_now(),
+                                    ossl_time_from_timeval(timeout)));
+
                 } else {
                     ERR_raise_data(ERR_LIB_SSL, SSL_R_POLL_REQUEST_NOT_SUPPORTED,
                         "SSL_poll currently only supports DTLS listeners for DTLS connections");
diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index 2f6a9f9a66..2a48e37791 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -5158,6 +5158,117 @@ end:

 #endif /* OPENSSL_THREADS */

+static unsigned int short_timer_cb_count;
+
+/* Force a short retransmission timeout and count how often it is consulted. */
+static unsigned int short_timer_cb(SSL *s, unsigned int timer_us)
+{
+    ++short_timer_cb_count;
+    return 50000; /* 50ms */
+}
+
+/*
+ * Test that a blocking SSL_poll() on a DTLS connection honours the
+ * retransmission timer.
+ *
+ * SSL_poll() bounds its wait by the per-object event timeout so that timer
+ * driven work is not delayed. For a DTLS connection that timeout is the
+ * handshake retransmission timer: if it is ignored, a poll with a long user
+ * timeout sleeps straight through the point at which the flight should have
+ * been resent, and if the peer had lost that flight neither side progresses.
+ *
+ * This does not time the wait. The retransmission timeout is forced down to
+ * 50ms and the poll is given much longer, so a correct implementation must
+ * wake and retransmit at least once before the poll deadline; an
+ * implementation which ignores the timer retransmits not at all.
+ */
+static int test_dtls_poll_conn_honours_retransmit_timer(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *clientssl = NULL, *serverssl = NULL;
+    SSL_POLL_ITEM poll_item;
+    struct timeval poll_timeout, timer_left;
+    size_t poll_result = 0;
+    int is_infinite = 0;
+    int retc, rets, err_code;
+    int testresult = 0;
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), DTLS1_VERSION, 0, &sctx, &cctx, cert,
+            privkey)))
+        goto end;
+
+    if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl,
+            NULL, NULL)))
+        goto end;
+
+    /*
+     * Install the short timeout before the server sends anything, so that it
+     * is picked up when the retransmission timer is first started rather than
+     * only on a later expiry.
+     */
+    DTLS_set_timer_cb(serverssl, short_timer_cb);
+
+    /*
+     * Drive just far enough for the server to send its first flight and start
+     * its retransmission timer. The client is then left alone, so the flight
+     * stays unacknowledged for the duration of the poll.
+     */
+    retc = SSL_connect(clientssl);
+    if (!TEST_int_le(retc, 0)
+        || !TEST_int_eq(SSL_get_error(clientssl, retc), SSL_ERROR_WANT_READ))
+        goto end;
+
+    /*
+     * The server consumes the ClientHello, sends its flight and then waits for
+     * the client, so this does not complete the handshake.
+     */
+    rets = SSL_accept(serverssl);
+    if (!TEST_int_le(rets, 0))
+        goto end;
+
+    err_code = SSL_get_error(serverssl, rets);
+    if (!TEST_true(err_code == SSL_ERROR_WANT_READ
+            || err_code == SSL_ERROR_WANT_WRITE))
+        goto end;
+
+    /*
+     * Assert the precondition through the same call SSL_poll() uses to compute
+     * its deadline: a running timer is reported as a finite timeout.
+     */
+    if (!TEST_true(SSL_get_event_timeout(serverssl, &timer_left, &is_infinite))
+        || !TEST_false(is_infinite))
+        goto end;
+
+    short_timer_cb_count = 0;
+
+    /*
+     * Nothing will arrive for this connection during the poll, so it runs to
+     * its deadline. Along the way the retransmission timer must expire and be
+     * serviced, which re-arms the timer via the callback.
+     */
+    poll_item.desc.type = BIO_POLL_DESCRIPTOR_TYPE_SSL;
+    poll_item.desc.value.ssl = serverssl;
+    poll_item.events = SSL_POLL_EVENT_R;
+    poll_item.revents = 0;
+    poll_timeout.tv_sec = 0;
+    poll_timeout.tv_usec = 500000;
+
+    if (!TEST_true(SSL_poll(&poll_item, 1, sizeof(poll_item), &poll_timeout, 0,
+            &poll_result)))
+        goto end;
+
+    if (!TEST_uint_gt(short_timer_cb_count, 0))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_free(serverssl);
+    SSL_free(clientssl);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}

 OPT_TEST_DECLARE_USAGE("certfile privkeyfile\n")

@@ -5269,7 +5380,7 @@ int setup_tests(void)
     ADD_TEST(test_dtls_notifier_signalled_on_accept_queue_push);
     ADD_TEST(test_dtls_poll_listener_enters_blocking_section);
 #endif
-
+    ADD_TEST(test_dtls_poll_conn_honours_retransmit_timer);

     return 1;
 }