Commit 619c95a8c9 for openssl.org

commit 619c95a8c9d127c26f2f2afa9130eb7a298604ad
Author: Matt Caswell <matt@openssl.foundation>
Date:   Mon Aug 10 08:58:55 2026 +0100

    Stop the DTLS timer when the retransmit budget is exhausted

    dtls1_handle_timeout() fails the connection with SSLfatal() once
    dtls1_check_timeout_num() reports that DTLS1_TMO_ALERT_COUNT unanswered
    retransmissions have been sent. It returns at that point without reaching
    dtls1_start_timer(), so next_timeout is left holding a time in the past.

    Nothing re-arms or clears it afterwards, the connection being finished, so
    every later dtls1_get_timeout() computes a negative remaining time, clamps it
    to zero and reports the timeout as due immediately. A caller which waits on
    that timeout therefore never waits at all. The documented
    DTLSv1_get_timeout() plus select() loop turns into a busy loop on any
    connection which has given up retransmitting, consuming a core until the
    application gives up on the connection itself.

    Stop the timer instead. dtls1_stop_timer() zeroes next_timeout, which
    dtls1_get_timeout() already reports as "no timer running", so callers see no
    pending timeout and wait on whatever other deadline they have.

    The test drives this without waiting by forcing a retransmission timeout of
    1ms, which is below the 15ms floor at which dtls1_get_timeout() treats a
    timer as already expired. The server therefore retransmits on every read
    attempt and exhausts its budget inside SSL_accept().

    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:40 2026
    Merged-from: https://github.com/openssl/openssl/pull/32239

diff --git a/ssl/d1_lib.c b/ssl/d1_lib.c
index 0c88f979e7..05988b7bc1 100644
--- a/ssl/d1_lib.c
+++ b/ssl/d1_lib.c
@@ -565,7 +565,14 @@ int dtls1_handle_timeout(SSL_CONNECTION *s)
         dtls1_double_timeout(s);

     if (dtls1_check_timeout_num(s) < 0) {
-        /* SSLfatal() already called */
+        /*
+         * SSLfatal() already called, so the connection is finished. Stop the
+         * timer rather than returning with next_timeout left in the past:
+         * nothing will re-arm or clear it from here, so DTLSv1_get_timeout()
+         * would report "due now" for ever and spin any caller which waits on
+         * it.
+         */
+        dtls1_stop_timer(s);
         return -1;
     }

diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index 2a48e37791..5c797bdf48 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -5167,6 +5167,81 @@ static unsigned int short_timer_cb(SSL *s, unsigned int timer_us)
     return 50000; /* 50ms */
 }

+/*
+ * Force a retransmission timeout short enough that it is always already
+ * expired, so the timeout can be driven repeatedly without waiting. Anything
+ * at or below 15ms is treated as expired by dtls1_get_timeout().
+ */
+static unsigned int tiny_timer_cb(SSL *s, unsigned int timer_us)
+{
+    return 1000; /* 1ms */
+}
+
+/*
+ * Test that the DTLS retransmission timer is stopped once the connection gives
+ * up retransmitting.
+ *
+ * dtls1_handle_timeout() fails the connection after DTLS1_TMO_ALERT_COUNT
+ * unanswered retransmissions. It must not leave the timer armed in the past
+ * when it does: nothing will re-arm or clear it afterwards, so every later
+ * query reports the timeout as due immediately, and any caller which waits on
+ * it spins instead of sleeping - whether that is an application using
+ * DTLSv1_get_timeout() with select(), or SSL_poll() bounding its own wait.
+ */
+static int test_dtls_timer_stopped_when_retransmits_exhausted(void)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *clientssl = NULL, *serverssl = NULL;
+    struct timeval timer_left;
+    int is_infinite = 0;
+    int retc, rets;
+    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;
+
+    DTLS_set_timer_cb(serverssl, tiny_timer_cb);
+
+    retc = SSL_connect(clientssl);
+    if (!TEST_int_le(retc, 0)
+        || !TEST_int_eq(SSL_get_error(clientssl, retc), SSL_ERROR_WANT_READ))
+        goto end;
+
+    /*
+     * With a timeout this short the server's timer is expired on every read
+     * attempt, so the accept retransmits until the budget runs out and fails
+     * the connection by itself. The client is never fed, so nothing is ever
+     * acknowledged.
+     *
+     * The retransmissions happen in dtls1_read_bytes(), which calls
+     * dtls1_handle_timeout() and, when it reports that it retransmitted, goes
+     * back to its start label to try the read again.
+     */
+    rets = SSL_accept(serverssl);
+    if (!TEST_int_le(rets, 0)
+        || !TEST_int_eq(SSL_get_error(serverssl, rets), SSL_ERROR_SSL))
+        goto end;
+
+    /* The timer must not have been left armed in the past. */
+    if (!TEST_true(SSL_get_event_timeout(serverssl, &timer_left, &is_infinite))
+        || !TEST_true(is_infinite))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_free(serverssl);
+    SSL_free(clientssl);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    return testresult;
+}
+
 /*
  * Test that a blocking SSL_poll() on a DTLS connection honours the
  * retransmission timer.
@@ -5381,6 +5456,7 @@ int setup_tests(void)
     ADD_TEST(test_dtls_poll_listener_enters_blocking_section);
 #endif
     ADD_TEST(test_dtls_poll_conn_honours_retransmit_timer);
+    ADD_TEST(test_dtls_timer_stopped_when_retransmits_exhausted);

     return 1;
 }