Commit db218a6d96 for openssl.org

commit db218a6d96a3928b9974ba8d5629db0a19ebee2d
Author: Ryan Hooper <ryanh@openssl.foundation>
Date:   Mon Oct 19 14:32:12 2026 -0400

    DTLS: Fix listener cleanup after failed SSL_clear

    SSL_clear() temporarily detaches an accepted connection's listener
    association while replacing its DTLS state. If initialization fails,
    unregister the saved peer before releasing the receive queue and
    listener reference. Otherwise, freeing the connection leaves a stale
    entry in the listener's established-connection table.

    Build the temporary unregister key in stack storage so removal does
    not depend on a successful allocation during cleanup. Share the key
    serialization with registration and lookup, preserving the existing
    peer-address matching and SipHash inputs.

    Add allocation-failure tests for SSL_clear() on an accepted DTLS 1.2
    connection. Cover both single failures and sustained allocation failure
    through reset cleanup. Check that detached connections are removed,
    while connections retaining their listener association stay registered.

    Fixes: #32974
    Assisted-by: OpenCode:github-copilot/gpt-6-astra
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Matt Caswell <matt@openssl.foundation>
    Reviewed-by: Mounir Idrassi <mounir.idrassi@idrix.fr>
    Merge-date: Wed Oct 21 15:41:49 2026
    Merged-from: https://github.com/openssl/openssl/pull/33270

diff --git a/ssl/dtls_conn_lookup.c b/ssl/dtls_conn_lookup.c
index 27f4c66076..fea8c7d277 100644
--- a/ssl/dtls_conn_lookup.c
+++ b/ssl/dtls_conn_lookup.c
@@ -40,63 +40,54 @@ typedef struct addr_lookup_data_st {
     uint64_t hash_key[2]; /* 128-bit SipHash key */
 } ADDR_LOOKUP_DATA;

+/* BIO_ADDR contains the raw address for every supported address family. */
+#define ADDR_HASHKEY_MAX_LEN (sizeof(int) + sizeof(uint16_t) + sizeof(BIO_ADDR))
+
 /*
- * Build a hash key from a BIO_ADDR.
+ * Build a hash key from a BIO_ADDR in caller-provided storage.
  * The key format is: family (int) + port (uint16_t) + raw address bytes.
  *
- * Returns allocated hashkey buffer on success, NULL on failure.
+ * Returns 1 on success, 0 on failure.
  */
-static uint8_t *build_hashkey(const BIO_ADDR *peer, size_t *out_len)
+static int build_hashkey_into(const BIO_ADDR *peer, uint8_t *hashkey,
+    size_t capacity, size_t *out_len)
 {
-    size_t hashkey_len = 0;
     size_t addr_len = 0;
-    int family;
+    size_t header_len = sizeof(int) + sizeof(uint16_t);
+    int family = BIO_ADDR_family(peer);
     uint16_t port;
-    uint8_t *hashkey;
-    int *famptr;
-    uint16_t *portptr;
-    uint8_t *addrptr;
-
-    family = BIO_ADDR_family(peer);

     /* For AF_UNSPEC (no peer address), use a minimal key */
     if (family == AF_UNSPEC) {
-        hashkey_len = sizeof(int);
-        hashkey = OPENSSL_zalloc(hashkey_len);
-        if (hashkey == NULL)
-            return NULL;
-        famptr = (int *)hashkey;
-        *famptr = family;
-        *out_len = hashkey_len;
-        return hashkey;
+        if (capacity < sizeof(family))
+            return 0;
+        memcpy(hashkey, &family, sizeof(family));
+        *out_len = sizeof(family);
+        return 1;
     }

-    if (!BIO_ADDR_rawaddress(peer, NULL, &addr_len))
-        return NULL;
+    if (!BIO_ADDR_rawaddress(peer, NULL, &addr_len)
+        || capacity < header_len || addr_len > capacity - header_len)
+        return 0;

     port = BIO_ADDR_rawport(peer);
+    memcpy(hashkey, &family, sizeof(family));
+    memcpy(hashkey + sizeof(family), &port, sizeof(port));
+    if (!BIO_ADDR_rawaddress(peer, hashkey + header_len, NULL))
+        return 0;

-    hashkey_len += sizeof(int); /* family */
-    hashkey_len += sizeof(uint16_t); /* port */
-    hashkey_len += addr_len; /* address */
-
-    hashkey = OPENSSL_zalloc(hashkey_len);
-    if (hashkey == NULL)
-        return NULL;
+    *out_len = header_len + addr_len;
+    return 1;
+}

-    famptr = (int *)hashkey;
-    portptr = (uint16_t *)(famptr + 1);
-    addrptr = (uint8_t *)(portptr + 1);
+/* Return an allocated hash key for callers which need owned storage. */
+static uint8_t *build_hashkey(const BIO_ADDR *peer, size_t *out_len)
+{
+    uint8_t hashkey[ADDR_HASHKEY_MAX_LEN];

-    *famptr = family;
-    *portptr = port;
-    if (!BIO_ADDR_rawaddress(peer, addrptr, NULL)) {
-        OPENSSL_free(hashkey);
+    if (!build_hashkey_into(peer, hashkey, sizeof(hashkey), out_len))
         return NULL;
-    }
-
-    *out_len = hashkey_len;
-    return hashkey;
+    return OPENSSL_memdup(hashkey, *out_len);
 }

 /*
@@ -313,6 +304,7 @@ static int addr_unregister_conn(DGRAM_CONN_LOOKUP *lookup, const BIO_ADDR *peer)
     ADDR_LOOKUP_DATA *data;
     DGRAM_CONN_ENTRY lookup_key;
     DGRAM_CONN_ENTRY *removed;
+    uint8_t hashkey[ADDR_HASHKEY_MAX_LEN];

     if (lookup == NULL || lookup->impl_data == NULL || peer == NULL)
         return 0;
@@ -322,17 +314,17 @@ static int addr_unregister_conn(DGRAM_CONN_LOOKUP *lookup, const BIO_ADDR *peer)
     memset(&lookup_key, 0, sizeof(lookup_key));
     BIO_ADDR_copy(&lookup_key.peer, peer);

-    lookup_key.hashkey = build_hashkey(peer, &lookup_key.hashkey_len);
-    if (lookup_key.hashkey == NULL)
+    /* Cleanup must not depend on allocating a temporary lookup key. */
+    if (!build_hashkey_into(peer, hashkey, sizeof(hashkey),
+            &lookup_key.hashkey_len))
         return 0;
+    lookup_key.hashkey = hashkey;

     /* Set SipHash key pointer for hash function */
     lookup_key.siphash_key = data->hash_key;

     removed = lh_DGRAM_CONN_ENTRY_delete(data->htable, &lookup_key);

-    OPENSSL_free(lookup_key.hashkey);
-
     if (removed != NULL) {
         conn_entry_free(removed);
         return 1;
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index b40783a21e..03914f1c1a 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -684,6 +684,9 @@ int ossl_ssl_connection_reset(SSL *s)
             if (is_dtls_listener_conn) {
                 DTLS_LISTENER *dl = (DTLS_LISTENER *)saved_listener;

+                /* The detached connection cannot unregister itself on free. */
+                ossl_dtls_listener_unregister_established_conn(saved_listener,
+                    &saved_peer_addr);
                 ossl_crypto_mutex_lock(dl->mutex);
                 sc->listener_rx = NULL;
                 ossl_crypto_mutex_unlock(dl->mutex);
diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index 79bf863d00..44d8934f5f 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -5583,6 +5583,156 @@ static int drive_until_connection_queued(SSL *listener, SSL *clientssl)
     return 0;
 }

+#ifndef OPENSSL_NO_DTLS1_2
+static CRYPTO_malloc_fn ssl_clear_malloc;
+static CRYPTO_realloc_fn ssl_clear_realloc;
+static int ssl_clear_oom_active;
+
+/*
+ * Preserve MFAIL's first-failure selection, then keep normal allocations
+ * failing until SSL_clear() returns. Error/debug allocations use a NULL file
+ * and remain available for reporting the original failure.
+ */
+static void *ssl_clear_oom_malloc(size_t num, const char *file, int line)
+{
+    if (ssl_clear_oom_active && num > 0 && file != NULL
+        && mfail_was_triggered())
+        return NULL;
+    return ssl_clear_malloc(num, file, line);
+}
+
+static void *ssl_clear_oom_realloc(void *ptr, size_t num,
+    const char *file, int line)
+{
+    if (ssl_clear_oom_active && num > 0 && file != NULL
+        && mfail_was_triggered())
+        return NULL;
+    return ssl_clear_realloc(ptr, num, file, line);
+}
+
+/*
+ * A failed replacement of the DTLS state must not leave a listener entry.
+ * idx == 0 injects a single failure; idx == 1 keeps allocations failing
+ * through reset cleanup, including any allocation needed to unregister.
+ */
+static int test_dtls_listener_ssl_clear_mfail(int idx)
+{
+    SSL_CTX *sctx = NULL, *cctx = NULL;
+    SSL *listener = NULL, *clientssl = NULL, *serverssl = NULL;
+    BIO_ADDR *client_addr = NULL;
+    SSL_CONNECTION *sc;
+    DTLS_LISTENER *dl = NULL;
+    DGRAM_URXE probe = { 0 };
+    CRYPTO_free_fn saved_free = NULL;
+    int have_peer = 0, wrapped = 0, ret;
+    /* Invariant failures must fail even when an allocation was injected. */
+    int testresult = -1;
+
+    if (!TEST_true(create_ssl_ctx_pair(NULL, DTLS_server_method(),
+            DTLS_client_method(), DTLS1_2_VERSION, DTLS1_2_VERSION,
+            &sctx, &cctx, cert, privkey))
+        || !TEST_true(create_dtls_listener_and_client_mem(sctx, cctx,
+            SSL_LISTENER_FLAG_NO_VALIDATE | SSL_LISTENER_FLAG_SINGLE_THREAD,
+            &listener, &clientssl, &client_addr))
+        || !drive_until_connection_queued(listener, clientssl)
+        || !TEST_ptr(serverssl = SSL_accept_connection(listener,
+                         SSL_ACCEPT_CONNECTION_NO_BLOCK))
+        || !TEST_ptr(sc = SSL_CONNECTION_FROM_SSL_ONLY(serverssl))
+        || !TEST_ptr(sc->d1)
+        || !TEST_ptr_eq(sc->d1->listener, listener)
+        || !TEST_ptr(sc->d1->rx)
+        || !TEST_ptr_eq(sc->listener_rx, sc->d1->rx)
+        || !TEST_ptr_ne(serverssl->method, serverssl->defltmeth))
+        goto end;
+
+    dl = (DTLS_LISTENER *)listener;
+    probe.peer = sc->d1->peer_addr;
+    have_peer = 1;
+    if (!TEST_int_eq(BIO_ADDR_family(&probe.peer), AF_INET)
+        || !TEST_ptr_eq(ossl_dtls_listener_find_established_conn(dl, &probe),
+            serverssl)
+        || !TEST_size_t_eq(
+            ossl_dgram_conn_lookup_num_items(dl->established_conns), 1))
+        goto end;
+
+    /* Wrap only the installed MFAIL allocator, never the default allocator. */
+    if (idx == 1 && mfail_is_installed()) {
+        CRYPTO_get_mem_functions(&ssl_clear_malloc, &ssl_clear_realloc,
+            &saved_free);
+        if (!TEST_true(CRYPTO_set_mem_functions(ssl_clear_oom_malloc,
+                ssl_clear_oom_realloc, saved_free)))
+            goto end;
+        wrapped = 1;
+    }
+
+    /* Only the reset is subject to allocation-failure injection. */
+    ssl_clear_oom_active = wrapped;
+    MFAIL_start();
+    ret = SSL_clear(serverssl);
+    MFAIL_end();
+    ssl_clear_oom_active = 0;
+    if (wrapped) {
+        if (!TEST_true(CRYPTO_set_mem_functions(ssl_clear_malloc,
+                ssl_clear_realloc, saved_free)))
+            goto end;
+        wrapped = 0;
+    }
+    ERR_clear_error();
+
+    if (!TEST_ptr_eq(serverssl->method, serverssl->defltmeth)
+        || (ret == 0 && !TEST_true(mfail_was_triggered())))
+        goto end;
+
+    if (ret == 0 && (sc->d1 == NULL || sc->d1->listener == NULL)) {
+        /*
+         * ssl_init failed before restoring the listener association. Check
+         * the saved peer while the SSL is still alive: freeing it first would
+         * turn a stale registration into a dangling pointer.
+         */
+        if (!TEST_ptr_null(sc->listener_rx)
+            || !TEST_size_t_eq(
+                ossl_dgram_conn_lookup_num_items(dl->established_conns), 0)
+            || !TEST_ptr_null(
+                ossl_dtls_listener_find_established_conn(dl, &probe)))
+            goto end;
+    } else {
+        /*
+         * A successful reset, or a failure after ssl_init restored the
+         * association, must preserve the connection's registration.
+         */
+        if (!TEST_ptr(sc->d1)
+            || !TEST_ptr_eq(sc->d1->listener, listener)
+            || !TEST_ptr(sc->d1->rx)
+            || !TEST_ptr_eq(sc->listener_rx, sc->d1->rx)
+            || !TEST_size_t_eq(
+                ossl_dgram_conn_lookup_num_items(dl->established_conns), 1)
+            || !TEST_ptr_eq(
+                ossl_dtls_listener_find_established_conn(dl, &probe), serverssl))
+            goto end;
+    }
+
+    testresult = 1;
+end:
+    ssl_clear_oom_active = 0;
+    MFAIL_end();
+    if (wrapped
+        && !TEST_true(CRYPTO_set_mem_functions(ssl_clear_malloc,
+            ssl_clear_realloc, saved_free)))
+        testresult = -1;
+    /* Remove any stale entry before freeing the object on a failing run. */
+    if (have_peer)
+        ossl_dtls_listener_unregister_established_conn(listener, &probe.peer);
+    SSL_free(serverssl);
+    SSL_free(clientssl);
+    SSL_free(listener);
+    BIO_ADDR_free(client_addr);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    ERR_clear_error();
+    return testresult;
+}
+#endif
+
 /*
  * Test the blocking mode of a DTLS listener and of the connections it creates.
  *
@@ -6879,6 +7029,9 @@ int setup_tests(void)
     ADD_TEST(test_ssl_ownership_set_rbio_incoming_leak);
     ADD_TEST(test_ssl_ownership_multiple_pending_leak);
     ADD_TEST(test_ssl_ownership_pending_timeout_cleanup);
+#ifndef OPENSSL_NO_DTLS1_2
+    ADD_MFAIL_ALL_NO_CHECK_TESTS(test_dtls_listener_ssl_clear_mfail, 2);
+#endif

     /* Max number of pending connections tests */
     ADD_TEST(test_dtls_listener_max_pending_conns_api);