Commit 37e52a2f46 for openssl.org

commit 37e52a2f463e500fbe3ccbb16b8ae175f5315f21
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Tue Aug 25 23:34:42 2026 +0900

    QUIC: fix deferred SSL_listen_ex() connection adoption

    SSL_listen_ex() could attach a deferred incoming channel without
    binding its QUIC TLS layer or fully adopting the caller-provided
    connection into the listener hierarchy. The resulting connection could
    not complete its handshake and retained stale port and ownership state.

    Keep deferred channels idle until TLS is attached, bind the handshake
    layer during peeloff, reparent the connection, and drain unpeeled
    deferred channels during listener teardown. Make the regression test
    require handshake completion and stream I/O.

    Fixes #32490
    Assisted-by: Pi:Kimi-k3
    Assisted-by: Codex:gpt-5.6-sol

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Merge-date: Tue Sep  1 14:19:16 2026
    Merged-from: https://github.com/openssl/openssl/pull/32491

diff --git a/include/internal/quic_channel.h b/include/internal/quic_channel.h
index 3f6efffb99..fd600fb44c 100644
--- a/include/internal/quic_channel.h
+++ b/include/internal/quic_channel.h
@@ -331,7 +331,7 @@ OSSL_STATM *ossl_quic_channel_get_statm(QUIC_CHANNEL *ch);
 /* Gets the TLS handshake layer used with the channel. */
 SSL *ossl_quic_channel_get0_tls(QUIC_CHANNEL *ch);

-/* Sets the TLS handshake layer used for the channel */
+/* Attaches the TLS layer to a deferred channel. */
 void ossl_quic_channel_set0_tls(QUIC_CHANNEL *ch, SSL *ssl);

 /* Gets the channels short header connection id length */
diff --git a/include/internal/quic_tls.h b/include/internal/quic_tls.h
index 707f7df9e3..03b755e960 100644
--- a/include/internal/quic_tls.h
+++ b/include/internal/quic_tls.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2022-2026 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy
@@ -89,6 +89,9 @@ typedef struct quic_tls_args_st {

 QUIC_TLS *ossl_quic_tls_new(const QUIC_TLS_ARGS *args);

+/* Attach an SSL to an unconfigured, deferred handshake layer. */
+int ossl_quic_tls_set0_ssl(QUIC_TLS *qtls, SSL *ssl);
+
 void ossl_quic_tls_free(QUIC_TLS *qtls);

 int ossl_quic_tls_configure(QUIC_TLS *qtls);
diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index 99f131cb78..dcb0974b49 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -584,7 +584,15 @@ SSL *ossl_quic_channel_get0_tls(QUIC_CHANNEL *ch)

 void ossl_quic_channel_set0_tls(QUIC_CHANNEL *ch, SSL *ssl)
 {
-    SSL_free(ch->tls);
+    /*
+     * Rebind the handshake layer first, so that a failure leaves the channel
+     * entirely unmodified rather than with a TLS connection the handshake
+     * layer does not know about.
+     */
+    if (!ossl_assert(ch != NULL && ssl != NULL && ch->tls == NULL)
+        || !ossl_quic_tls_set0_ssl(ch->qtls, ssl))
+        return;
+
     ch->tls = ssl;
 #ifndef OPENSSL_NO_QLOG
     /*
diff --git a/ssl/quic/quic_impl.c b/ssl/quic/quic_impl.c
index e80790aa25..e0bc13fa38 100644
--- a/ssl/quic/quic_impl.c
+++ b/ssl/quic/quic_impl.c
@@ -5019,10 +5019,16 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
 {
     QCTX lctx;
     QCTX cctx;
-    QUIC_CHANNEL *new_ch;
+    QUIC_CHANNEL *new_ch, *old_ch;
+    QUIC_PORT *old_port;
+    QUIC_ENGINE *old_engine;
+#if defined(OPENSSL_THREADS)
+    CRYPTO_MUTEX *old_mutex = NULL;
+#endif
     QUIC_CONNECTION *qc = NULL;
     QUIC_LISTENER *ql = NULL;
     SSL *tls = NULL;
+    SSL_CONNECTION *tls_conn = NULL;
     int ret = 0;

     if (!expect_quic_listener(listener, &lctx))
@@ -5041,52 +5047,86 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
     }

     new_ch = ossl_quic_port_pop_incoming(lctx.ql->port);
-    if (new_ch != NULL) {
-        tls = ossl_ssl_connection_new_int(ossl_quic_port_get_channel_ctx(lctx.ql->port),
-            new_conn, TLS_method());
-        if (tls == NULL)
-            goto out;
+    if (new_ch == NULL)
+        goto out;

-        qc = cctx.qc;
-        ql = lctx.ql;
-        /*
-         * Need to ensure that we take a reference on our new listener
-         * so that we don't free it before this connection
-         */
-        if (!SSL_up_ref(&ql->obj.ssl))
-            goto out;
+    qc = cctx.qc;
+    ql = lctx.ql;

-        ossl_quic_channel_free(qc->ch);
-        ossl_quic_port_free(qc->port);
-        ossl_quic_engine_free(qc->engine);
-        /*
-         * Ensure that we point to our listener so we can drop
-         * the above refcount when this SSL object is freed
-         */
-        qc->listener = ql;
-        qc->obj.engine = ql->engine;
-        qc->engine = ql->engine;
-        qc->port = ql->port;
-        qc->pending = 1;
+    tls = ossl_ssl_connection_new_int(
+        ossl_quic_port_get_channel_ctx(ql->port), new_conn, TLS_method());
+    if (tls == NULL)
+        goto out;
+
+    tls_conn = SSL_CONNECTION_FROM_SSL(tls);
+    if (tls_conn == NULL) {
+        QUIC_RAISE_NON_NORMAL_ERROR(NULL, ERR_R_INTERNAL_ERROR, NULL);
+        SSL_free(tls);
+        goto out;
+    }
+
+    tls_conn->s3.flags |= TLS1_FLAGS_QUIC | TLS1_FLAGS_QUIC_INTERNAL;
+    tls_conn->options &= OSSL_QUIC_PERMITTED_OPTIONS_CONN;
+    tls_conn->pha_enabled = 0;
+
+    /* The connection keeps its listener alive. */
+    if (!SSL_up_ref(&ql->obj.ssl)) {
+        SSL_free(tls);
+        goto out;
+    }
+
+    /* Bind TLS before adopting the deferred incoming channel. */
+    ossl_quic_channel_set0_tls(new_ch, tls);
+
+    old_ch = qc->ch;
+    old_port = qc->port;
+    old_engine = qc->engine;
 #if defined(OPENSSL_THREADS)
-        ossl_crypto_mutex_free(&qc->mutex);
-        qc->mutex = ql->mutex;
+    old_mutex = qc->mutex;
 #endif
-        qc->ch = new_ch;
-        SSL_free(qc->tls);
-        ossl_quic_channel_set0_tls(new_ch, tls);
-        qc->tls = tls;
-        ossl_quic_channel_get_peer_addr(new_ch, &qc->init_peer_addr); /* best effort */
-        qc->started = 1;
-        qc->as_server = 1;
-        qc->as_server_state = 1;
-        qc->default_stream_mode = SSL_DEFAULT_STREAM_MODE_AUTO_BIDI;
-        qc->default_ssl_options = ql->obj.ssl.ctx->options & OSSL_QUIC_PERMITTED_OPTIONS;
-        qc->incoming_stream_policy = SSL_INCOMING_STREAM_POLICY_AUTO;
-        qc->last_error = SSL_ERROR_NONE;
-        qc_update_reject_policy(qc);
-        ret = 1;
-    }
+
+    /* Ensure that SSL_free() drops the listener reference. */
+    qc->listener = ql;
+    qc->engine = ql->engine;
+    qc->port = ql->port;
+    /* The connection is handed to the caller, as in SSL_accept_connection() */
+    qc->pending = 0;
+#if !defined(OPENSSL_NO_QUIC_THREAD_ASSIST)
+    /* Incoming connections never start a thread-assist worker. */
+    qc->is_thread_assisted = 0;
+#endif
+    /* Demote the standalone object into the listener's hierarchy. */
+    ossl_quic_obj_reparent(&qc->obj, &ql->obj);
+
+    /* Release the resources the connection owned as a standalone object. */
+    ossl_quic_channel_free(old_ch);
+    quic_unref_port_bios(old_port);
+    ossl_quic_port_free(old_port);
+    ossl_quic_engine_free(old_engine);
+#if defined(OPENSSL_THREADS)
+    /* The standalone mutex was borrowed by all three resources above. */
+    qc->mutex = ql->mutex;
+    ossl_crypto_mutex_free(&old_mutex);
+#endif
+
+    qc->ch = new_ch;
+    SSL_free(qc->tls);
+    qc->tls = tls;
+    ossl_quic_channel_get_peer_addr(new_ch,
+        &qc->init_peer_addr); /* best effort */
+    qc->started = 1;
+    qc->as_server = 1;
+    qc->as_server_state = 1;
+    /* Reinitialise configuration to the accepted-connection defaults. */
+    qc->default_stream_mode = SSL_DEFAULT_STREAM_MODE_AUTO_BIDI;
+    qc->default_ssl_options
+        = ql->obj.ssl.ctx->options & OSSL_QUIC_PERMITTED_OPTIONS;
+    qc->incoming_stream_policy = SSL_INCOMING_STREAM_POLICY_AUTO;
+    qc->incoming_stream_aec = 0;
+    qc->last_error = SSL_ERROR_NONE;
+    qc_update_reject_policy(qc);
+    ret = 1;
+
 out:
     qctx_unlock(&lctx);
     return ret;
diff --git a/ssl/quic/quic_local.h b/ssl/quic/quic_local.h
index 0c8594b315..49fa222f85 100644
--- a/ssl/quic/quic_local.h
+++ b/ssl/quic/quic_local.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2022-2026 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy
@@ -185,7 +185,7 @@ struct quic_conn_st {
      */
     unsigned int as_server_state : 1;

-    /* Are we using thread assisted mode? Never changes after init. */
+    /* Are we using thread assisted mode? Cleared on SSL_listen_ex adoption. */
     unsigned int is_thread_assisted : 1;

     /* Have we created a default XSO yet? */
diff --git a/ssl/quic/quic_obj.c b/ssl/quic/quic_obj.c
index 0c0a3faa2e..d2673b236b 100644
--- a/ssl/quic/quic_obj.c
+++ b/ssl/quic/quic_obj.c
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2024-2026 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy
@@ -53,6 +53,21 @@ err:
     return 0;
 }

+void ossl_quic_obj_reparent(QUIC_OBJ *obj, QUIC_OBJ *parent)
+{
+    if (!ossl_assert(obj != NULL && obj->init_done
+            && parent != NULL && parent->init_done
+            && obj->is_event_leader && obj->is_port_leader
+            && obj->parent_obj == NULL))
+        return;
+
+    obj->parent_obj = parent;
+    obj->is_event_leader = 0;
+    obj->is_port_leader = 0;
+    obj->domain_flags = parent->domain_flags;
+    (void)obj_update_cache(obj);
+}
+
 static int obj_update_cache(QUIC_OBJ *obj)
 {
     QUIC_OBJ *p;
diff --git a/ssl/quic/quic_obj_local.h b/ssl/quic/quic_obj_local.h
index d454ade330..75ca1eee37 100644
--- a/ssl/quic/quic_obj_local.h
+++ b/ssl/quic/quic_obj_local.h
@@ -1,5 +1,5 @@
 /*
- * Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2024-2026 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy
@@ -155,6 +155,9 @@ int ossl_quic_obj_init(QUIC_OBJ *obj,
     QUIC_ENGINE *engine,
     QUIC_PORT *port);

+/* Reparent a standalone leader; the caller must release its old resources. */
+void ossl_quic_obj_reparent(QUIC_OBJ *obj, QUIC_OBJ *parent);
+
 /*
  * Returns a pointer to the handshake layer object which should be accessible on
  * obj for purposes of handshake API autoforwarding, if any.
diff --git a/ssl/quic/quic_port.c b/ssl/quic/quic_port.c
index 9a1760e496..0dfe4c6cfa 100644
--- a/ssl/quic/quic_port.c
+++ b/ssl/quic/quic_port.c
@@ -731,6 +731,12 @@ void ossl_quic_port_drop_incoming(QUIC_PORT *port)
             break;

         tls = ossl_quic_channel_get0_tls(ch);
+        if (tls == NULL) {
+            /* Unpeeled SSL_listen_ex() channels have no user SSL. */
+            ossl_quic_channel_free(ch);
+            continue;
+        }
+
         /*
          * The user ssl may or may not have been created via the
          * get_conn_user_ssl callback in the QUIC stack.  The
diff --git a/ssl/quic/quic_tls.c b/ssl/quic/quic_tls.c
index c76cef9d92..4d57ecbf81 100644
--- a/ssl/quic/quic_tls.c
+++ b/ssl/quic/quic_tls.c
@@ -1,5 +1,5 @@
 /*
- * Copyright 2022-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2022-2026 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy
@@ -664,6 +664,16 @@ QUIC_TLS *ossl_quic_tls_new(const QUIC_TLS_ARGS *args)
     return qtls;
 }

+int ossl_quic_tls_set0_ssl(QUIC_TLS *qtls, SSL *ssl)
+{
+    if (!ossl_assert(qtls != NULL && ssl != NULL
+            && qtls->args.s == NULL && !qtls->configured))
+        return 0;
+
+    qtls->args.s = ssl;
+    return 1;
+}
+
 void ossl_quic_tls_free(QUIC_TLS *qtls)
 {
     if (qtls == NULL)
@@ -767,6 +777,10 @@ int ossl_quic_tls_tick(QUIC_TLS *qtls)
     if (qtls->inerror)
         return 0;

+    /* SSL_listen_ex() attaches the SSL after the channel is queued. */
+    if (qtls->args.s == NULL)
+        return 1;
+
     /*
      * SSL_get_error does not truly know what the cause of an SSL_read failure
      * is and to some extent guesses based on contextual information. In
diff --git a/test/quicapitest.c b/test/quicapitest.c
index a999937d8a..282cdf6532 100644
--- a/test/quicapitest.c
+++ b/test/quicapitest.c
@@ -44,6 +44,7 @@ static SSL_CTX *create_server_ctx(void);
 static SSL_CTX *create_client_ctx(void);
 static int create_quic_ssl_objects(SSL_CTX *sctx, SSL_CTX *cctx,
     SSL **lssl, SSL **cssl);
+static void quic_advance_time(SSL *clientssl, SSL *serverssl);
 static int qc_init(SSL *qconn, BIO_ADDR *dst_addr);

 /* The ssltrace test assumes some options are switched on/off */
@@ -2880,6 +2881,24 @@ static int create_quic_ssl_objects(SSL_CTX *sctx, SSL_CTX *cctx,
     return create_quic_ssl_objects_ex(sctx, cctx, lssl, cssl, 0);
 }

+static int queue_incoming_connection(SSL *qlistener, SSL *clientssl)
+{
+    int i, ret;
+
+    for (i = 0; i < 5; i++) {
+        ret = SSL_connect(clientssl);
+        if (!TEST_int_le(ret, 0)
+            || !TEST_int_eq(SSL_get_error(clientssl, ret), SSL_ERROR_WANT_READ))
+            return 0;
+
+        SSL_handle_events(qlistener);
+        if (SSL_get_accept_connection_queue_len(qlistener) == 1)
+            break;
+    }
+
+    return TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 1);
+}
+
 static int test_ssl_client_as_ossl_quic_method(void)
 {
     SSL_CTX *cctx = NULL, *sctx = NULL;
@@ -2946,63 +2965,105 @@ err:
     return testresult;
 }

-static int test_ssl_listen_ex(void)
+static int test_ssl_listen_ex(int idx)
 {
     SSL_CTX *cctx = NULL, *sctx = NULL, *qmctx = NULL;
     SSL *clientssl = NULL, *serverssl = NULL, *qlistener = NULL;
+    SSL *cstream = NULL, *sstream = NULL;
+    unsigned char buf[16], msg[] = "Hello, World!";
+    size_t readbytes, written;
     int testresult = 0;
     int ret = 0, i;

+#if !defined(OPENSSL_THREADS) || defined(OPENSSL_NO_THREAD_POOL) \
+    || defined(OPENSSL_NO_QUIC_THREAD_ASSIST)
+    if (idx == 1)
+        return TEST_skip("thread assisted mode not available in this build");
+#endif
+
     if (!TEST_ptr(sctx = create_server_ctx())
         || !TEST_ptr(cctx = create_client_ctx()))
         goto err;

-    if (!create_quic_ssl_objects(sctx, cctx, &qlistener, &clientssl))
+    if (!create_quic_ssl_objects_ex(sctx, cctx,
+            &qlistener, &clientssl, 1))
         goto err;

     qmctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_method());
     if (!TEST_ptr(qmctx))
         goto err;

+    if (idx == 1
+        && !TEST_true(SSL_CTX_set_domain_flags(qmctx,
+            SSL_DOMAIN_FLAG_THREAD_ASSISTED | SSL_DOMAIN_FLAG_BLOCKING)))
+        goto err;
+
     serverssl = SSL_new(qmctx);
     if (!TEST_ptr(serverssl))
         goto err;

-    /* Send ClientHello and server retry */
-    for (i = 0; i < 5; i++) {
+    if (!TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 0)
+        || !queue_incoming_connection(qlistener, clientssl))
+        goto err;
+
+    if (!TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 1)
+        || !TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 0))
+        goto err;
+
+    for (i = 0; i < 10; i++) {
+        ret = SSL_do_handshake(serverssl);
+        if (ret != 1
+            && !TEST_int_eq(SSL_get_error(serverssl, ret), SSL_ERROR_WANT_READ))
+            goto err;
+
         ret = SSL_connect(clientssl);
-        if (!TEST_int_le(ret, 0)
-            || !TEST_int_eq(SSL_get_error(clientssl, ret), SSL_ERROR_WANT_READ))
+        if (ret != 1
+            && !TEST_int_eq(SSL_get_error(clientssl, ret), SSL_ERROR_WANT_READ))
             goto err;
-        ret = SSL_listen_ex(qlistener, serverssl);
-        if (ret == 1)
+
+        if (SSL_is_init_finished(serverssl) && SSL_is_init_finished(clientssl))
             break;
-        SSL_handle_events(qlistener);
+        quic_advance_time(clientssl, serverssl);
     }

-    /*
-     * Check to make sure we got a good return code from SSL_listen_ex
-     */
-    if (!TEST_int_eq(ret, 1))
+    if (!TEST_int_lt(i, 10)
+        || !TEST_true(SSL_is_init_finished(serverssl))
+        || !TEST_true(SSL_is_init_finished(clientssl)))
         goto err;

-    /* Call SSL_accept() and SSL_connect() until we are connected */
-    if (!TEST_true(create_bare_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE, 0, 0)))
+    if (!TEST_ptr_null(SSL_accept_connection(qlistener, 0))
+        || !TEST_true(ERR_GET_REASON(ERR_get_error())
+            == ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED))
+        goto err;
+    ERR_clear_error();

-        /*
-         * Ensure that, now that we have used SSL_listen_ex, SSL_accept_connection
-         * produces an error
-         */
-        if (!TEST_ptr_null(SSL_accept_connection(qlistener, 0)))
-            goto err;
+    if (!TEST_true(SSL_set_default_stream_mode(serverssl,
+            SSL_DEFAULT_STREAM_MODE_NONE))
+        || !TEST_true(SSL_set_default_stream_mode(clientssl,
+            SSL_DEFAULT_STREAM_MODE_NONE))
+        || !TEST_ptr(cstream = SSL_new_stream(clientssl, 0))
+        || !TEST_true(SSL_write_ex(cstream, msg, sizeof(msg), &written))
+        || !TEST_size_t_eq(written, sizeof(msg))
+        || !TEST_int_eq(SSL_handle_events(serverssl), 1))
+        goto err;

-    if (!TEST_true((ERR_GET_REASON(ERR_get_error())) == ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED))
+    sstream = SSL_accept_stream(serverssl, 0);
+    if (!TEST_ptr(sstream)
+        || !TEST_true(SSL_read_ex(sstream, buf, sizeof(buf), &readbytes))
+        || !TEST_size_t_eq(readbytes, sizeof(msg))
+        || !TEST_mem_eq(buf, readbytes, msg, sizeof(msg))
+        || !TEST_true(SSL_write_ex(sstream, msg, sizeof(msg), &written))
+        || !TEST_size_t_eq(written, sizeof(msg))
+        || !TEST_true(SSL_read_ex(cstream, buf, sizeof(buf), &readbytes))
+        || !TEST_size_t_eq(readbytes, sizeof(msg))
+        || !TEST_mem_eq(buf, readbytes, msg, sizeof(msg)))
         goto err;

-    ERR_clear_error();
     testresult = 1;

 err:
+    SSL_free(sstream);
+    SSL_free(cstream);
     SSL_free(qlistener);
     SSL_free(serverssl);
     SSL_free(clientssl);
@@ -3013,6 +3074,37 @@ err:
     return testresult;
 }

+/* Free a listener which still owns a deferred, unpeeled channel. */
+static int test_ssl_listen_ex_teardown(void)
+{
+    SSL_CTX *cctx = NULL, *sctx = NULL, *qmctx = NULL;
+    SSL *clientssl = NULL, *serverssl = NULL, *qlistener = NULL;
+    int testresult = 0;
+
+    if (!TEST_ptr(sctx = create_server_ctx())
+        || !TEST_ptr(cctx = create_client_ctx())
+        || !create_quic_ssl_objects_ex(sctx, cctx,
+            &qlistener, &clientssl, 1)
+        || !TEST_ptr(qmctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_method()))
+        || !TEST_ptr(serverssl = SSL_new(qmctx))
+        || !TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 0)
+        || !queue_incoming_connection(qlistener, clientssl))
+        goto err;
+
+    SSL_free(qlistener);
+    qlistener = NULL;
+    testresult = 1;
+
+err:
+    SSL_free(qlistener);
+    SSL_free(serverssl);
+    SSL_free(clientssl);
+    SSL_CTX_free(sctx);
+    SSL_CTX_free(cctx);
+    SSL_CTX_free(qmctx);
+    return testresult;
+}
+
 static int test_ssl_accept_connection(void)
 {
     SSL_CTX *cctx = NULL, *sctx = NULL;
@@ -4046,7 +4138,8 @@ int setup_tests(void)
     ADD_TEST(test_quic_forbidden_options);
     ADD_ALL_TESTS(test_quic_set_fd, 3);
     ADD_TEST(test_bio_ssl);
-    ADD_TEST(test_ssl_listen_ex);
+    ADD_ALL_TESTS(test_ssl_listen_ex, 2);
+    ADD_TEST(test_ssl_listen_ex_teardown);
     ADD_TEST(test_ssl_client_as_ossl_quic_method);
     ADD_TEST(test_back_pressure);
     ADD_TEST(test_multiple_dgrams);