Commit 51bfb8c606 for openssl.org

commit 51bfb8c606246e21556e37fa312bb636129637fa
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Tue Aug 25 23:36:06 2026 +0900

    QUIC: harden SSL_listen_ex() argument and failure handling

    Require new_conn to be a fresh standalone, non-thread-assisted QUIC
    connection before replacing its transport state. Reject started, shut
    down, stream-bearing, BIO-configured, already parented, or
    thread-assisted objects instead of destructively adopting them.

    Perform allocation and listener refcount setup before removing the
    incoming channel, return -1 for invalid arguments and internal failures,
    and reserve 0 for no connection being available. Document the
    released-API behavior change and cover it with driver-managed
    allocation-failure and invalid-target tests.

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

diff --git a/CHANGES.md b/CHANGES.md
index 2af8d45e56..c8e3d7ebea 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -92,6 +92,12 @@ OpenSSL 4.1

    *Ryan Hooper*

+ * Fixed `SSL_listen_ex()` to return a usable QUIC connection and to preserve
+   queued connections on allocation failure. Invalid arguments and internal
+   failures now return `-1`, reserving `0` for "no connection available".
+
+   *Mounir IDRASSI*
+
  * Fixed TLS 1.3 clients to encrypt 0-RTT early data with the first offered
    PSK identity (RFC 9846 section 4.3.10) when a 0-RTT-capable resumption
    ticket has aged out and an external PSK is offered in its place. The early
diff --git a/doc/man3/SSL_new_listener.pod b/doc/man3/SSL_new_listener.pod
index afc2866f88..138da996b7 100644
--- a/doc/man3/SSL_new_listener.pod
+++ b/doc/man3/SSL_new_listener.pod
@@ -145,9 +145,24 @@ it is the first I/O call made to the SSL object to which the port is attached.

 Likewise, if a listener has accepted a connection via
 L<SSL_accept_connection(3)>, it is impermissible to accept connections via
-B<SSL_listen_ex()>.  Note also that SSL objects passed in the B<new_conn>
-parameter to B<SSL_listen_ex()> must be created using L<OSSL_QUIC_method(3)> or
-L<OSSL_QUIC_server_method(3)>.
+B<SSL_listen_ex()>. The SSL object passed in the B<new_conn> parameter must be
+a fresh standalone connection returned by L<SSL_new(3)>.
+L<OSSL_QUIC_method(3)> is intended for this use. The connection must not use
+thread assisted mode, have been started or shut down, have streams or network
+BIOs configured on it, or belong to another QUIC object hierarchy.
+
+On success, I<new_conn> is adopted into the listener's object hierarchy and
+uses its shared network port and TLS configuration derived from the listener.
+Certificates, verification parameters, ciphersuites, ALPN configuration and
+other security policy must be configured on the listener's
+B<SSL_CTX>. Applications must not install a private connected BIO or file
+descriptor on the returned connection.
+
+Once B<SSL_listen_ex()> returns success, the connection written to I<new_conn>
+is already in the accept (server) state. Use L<SSL_do_handshake(3)> (or
+L<SSL_handle_events(3)>) to progress its handshake towards completion.
+L<SSL_accept(3)> must not be used for this purpose; use
+L<SSL_do_handshake(3)> instead.

 The SSL_accept_connection() call is supported only on a listener SSL object and
 accepts a new incoming connection. A new SSL object representing the accepted
@@ -341,8 +356,7 @@ SSL_listen() returns 1 on success or 0 on failure.

 SSL_listen_ex() returns 1 when a new connection was accepted on the new_conn
 parameter, 0 if no new connection was available at the time of the call, or -1
-in the event an internal error occurred, signaling a need to check the error
-queue.
+on error, signaling a need to check the error queue.

 SSL_accept_connection() returns a pointer to a new SSL object on success or NULL
 on failure. On success, the caller assumes ownership of the reference.
@@ -409,7 +423,7 @@ OpenSSL 4.1.

 =head1 COPYRIGHT

-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
diff --git a/ssl/quic/quic_impl.c b/ssl/quic/quic_impl.c
index e0bc13fa38..84d9ecdafe 100644
--- a/ssl/quic/quic_impl.c
+++ b/ssl/quic/quic_impl.c
@@ -5037,6 +5037,30 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
     if (!expect_quic_c(new_conn, &cctx))
         return -1;

+#if !defined(OPENSSL_NO_QUIC_THREAD_ASSIST)
+    if (cctx.qc->is_thread_assisted) {
+        QUIC_RAISE_NON_NORMAL_ERROR(NULL, ERR_R_PASSED_INVALID_ARGUMENT,
+            "SSL_listen_ex requires new_conn without thread assistance");
+        return -1;
+    }
+#endif
+
+    /* The standalone transport is replaced, so new_conn must be unused. */
+    if (cctx.qc->started || cctx.qc->shutting_down
+        || cctx.qc->num_xso != 0
+        || cctx.qc->default_xso_created
+        || cctx.qc->listener != NULL
+        || ossl_quic_port_get_net_rbio(cctx.qc->port) != NULL
+        || ossl_quic_port_get_net_wbio(cctx.qc->port) != NULL
+        || ossl_quic_channel_is_active(cctx.qc->ch)
+        || ossl_quic_channel_is_term_any(cctx.qc->ch)
+        || !cctx.obj->is_event_leader || !cctx.obj->is_port_leader
+        || cctx.obj->parent_obj != NULL) {
+        QUIC_RAISE_NON_NORMAL_ERROR(NULL, ERR_R_PASSED_INVALID_ARGUMENT,
+            "SSL_listen_ex requires a fresh connection created by SSL_new()");
+        return -1;
+    }
+
     qctx_lock_for_io(&lctx);

     if (!ossl_quic_port_test_and_set_peeloff(lctx.ql->port, PEELOFF_LISTEN)) {
@@ -5046,8 +5070,8 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
         goto out;
     }

-    new_ch = ossl_quic_port_pop_incoming(lctx.ql->port);
-    if (new_ch == NULL)
+    /* Do all fallible setup before consuming the queued channel. */
+    if (!ossl_quic_port_have_incoming(lctx.ql->port))
         goto out;

     qc = cctx.qc;
@@ -5055,13 +5079,17 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)

     tls = ossl_ssl_connection_new_int(
         ossl_quic_port_get_channel_ctx(ql->port), new_conn, TLS_method());
-    if (tls == NULL)
+    if (tls == NULL) {
+        /* An internal failure is not "no connection available" */
+        ret = -1;
         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);
+        ret = -1;
         goto out;
     }

@@ -5071,10 +5099,14 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)

     /* The connection keeps its listener alive. */
     if (!SSL_up_ref(&ql->obj.ssl)) {
+        QUIC_RAISE_NON_NORMAL_ERROR(NULL, ERR_R_INTERNAL_ERROR, NULL);
         SSL_free(tls);
+        ret = -1;
         goto out;
     }

+    new_ch = ossl_quic_port_pop_incoming(ql->port);
+
     /* Bind TLS before adopting the deferred incoming channel. */
     ossl_quic_channel_set0_tls(new_ch, tls);

@@ -5091,10 +5123,6 @@ int ossl_quic_peeloff_conn(SSL *listener, SSL *new_conn)
     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);

diff --git a/ssl/quic/quic_local.h b/ssl/quic/quic_local.h
index 49fa222f85..4f4a864fab 100644
--- a/ssl/quic/quic_local.h
+++ b/ssl/quic/quic_local.h
@@ -185,7 +185,7 @@ struct quic_conn_st {
      */
     unsigned int as_server_state : 1;

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

     /* Have we created a default XSO yet? */
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index 649a94cd8e..345e4c81f8 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -8508,7 +8508,7 @@ int SSL_listen_ex(SSL *listener, SSL *new_conn)
 #endif
         ERR_raise_data(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT,
             "SSL_listen_ex only operates on QUIC SSL objects");
-    return 0;
+    return -1;
 }

 int SSL_listen(SSL *ssl)
diff --git a/test/dtlsssllistenertest.c b/test/dtlsssllistenertest.c
index 74b5bab2d3..ad82aef713 100644
--- a/test/dtlsssllistenertest.c
+++ b/test/dtlsssllistenertest.c
@@ -2006,7 +2006,7 @@ err:

 /*
  * Test SSL_listen_ex for DTLS.
- * Currently SSL_listen_ex is QUIC-only, so it should return 0 for DTLS.
+ * SSL_listen_ex is QUIC-only, so it should reject DTLS objects.
  */
 static int test_dtls_listen_ex_returns_error(void)
 {
@@ -2024,8 +2024,9 @@ static int test_dtls_listen_ex_returns_error(void)
     if (!TEST_ptr(new_conn = SSL_new(ctx)))
         goto err;

-    /* SSL_listen_ex should return 0 for DTLS */
-    if (!TEST_int_eq(SSL_listen_ex(listener, new_conn), 0))
+    if (!TEST_int_eq(SSL_listen_ex(listener, new_conn), -1)
+        || !TEST_int_eq(ERR_GET_REASON(ERR_get_error()),
+            ERR_R_PASSED_INVALID_ARGUMENT))
         goto err;

     success = 1;
diff --git a/test/quicapitest.c b/test/quicapitest.c
index 282cdf6532..8d8ce409e0 100644
--- a/test/quicapitest.c
+++ b/test/quicapitest.c
@@ -2899,6 +2899,18 @@ static int queue_incoming_connection(SSL *qlistener, SSL *clientssl)
     return TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 1);
 }

+static int listen_ex_rejects_new_conn(SSL *qlistener, SSL *new_conn)
+{
+    int ret = SSL_listen_ex(qlistener, new_conn);
+    unsigned long err = ERR_get_error();
+    int ok = TEST_int_eq(ret, -1)
+        && TEST_int_eq(ERR_GET_REASON(err), ERR_R_PASSED_INVALID_ARGUMENT)
+        && TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 1);
+
+    ERR_clear_error();
+    return ok;
+}
+
 static int test_ssl_client_as_ossl_quic_method(void)
 {
     SSL_CTX *cctx = NULL, *sctx = NULL;
@@ -2965,22 +2977,18 @@ err:
     return testresult;
 }

-static int test_ssl_listen_ex(int idx)
+static int test_ssl_listen_ex(void)
 {
-    SSL_CTX *cctx = NULL, *sctx = NULL, *qmctx = NULL;
+    SSL_CTX *cctx = NULL, *sctx = NULL, *qmctx = NULL, *threadctx = NULL;
     SSL *clientssl = NULL, *serverssl = NULL, *qlistener = NULL;
+    SSL *preconf = NULL, *prestream = NULL, *threadssl = NULL;
     SSL *cstream = NULL, *sstream = NULL;
+    BIO *confbio = 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;
@@ -2993,18 +3001,71 @@ static int test_ssl_listen_ex(int idx)
     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;

-    if (!TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 0)
+    if (!TEST_int_eq(SSL_listen_ex(NULL, serverssl), -1)
+        || !TEST_true(ERR_GET_REASON(ERR_get_error())
+            == ERR_R_PASSED_INVALID_ARGUMENT)
+        || !TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 0)
         || !queue_incoming_connection(qlistener, clientssl))
         goto err;
+    ERR_clear_error();
+
+#if defined(OPENSSL_THREADS) && !defined(OPENSSL_NO_THREAD_POOL) \
+    && !defined(OPENSSL_NO_QUIC_THREAD_ASSIST)
+    /* Thread assistance can't be transferred to an accepted connection. */
+    threadctx = SSL_CTX_new_ex(libctx, NULL, OSSL_QUIC_method());
+    if (!TEST_ptr(threadctx)
+        || !TEST_true(SSL_CTX_set_domain_flags(threadctx,
+            SSL_DOMAIN_FLAG_THREAD_ASSISTED | SSL_DOMAIN_FLAG_BLOCKING))
+        || !TEST_ptr(threadssl = SSL_new(threadctx))
+        || !listen_ex_rejects_new_conn(qlistener, threadssl))
+        goto err;
+
+    SSL_free(threadssl);
+    threadssl = NULL;
+#endif
+
+    /* A connection which has already been started is not fresh. */
+    if (!listen_ex_rejects_new_conn(qlistener, clientssl))
+        goto err;
+
+    /* A connection which belongs to this listener isn't standalone. */
+    preconf = SSL_new_from_listener(qlistener, 0);
+    if (!TEST_ptr(preconf)
+        || !listen_ex_rejects_new_conn(qlistener, preconf))
+        goto err;
+
+    SSL_free(preconf);
+    preconf = NULL;
+
+    /* Network BIOs may not already be attached. */
+    preconf = SSL_new(qmctx);
+    if (!TEST_ptr(preconf)
+        || !TEST_ptr(confbio = BIO_new(BIO_s_mem())))
+        goto err;
+    SSL_set_bio(preconf, confbio, confbio);
+    confbio = NULL;
+    if (!listen_ex_rejects_new_conn(qlistener, preconf))
+        goto err;
+
+    SSL_free(preconf);
+    preconf = NULL;
+
+    /* Streams may not already have been created. */
+    preconf = SSL_new(qmctx);
+    if (!TEST_ptr(preconf)
+        || !TEST_ptr(prestream = SSL_new_stream(preconf,
+                         SSL_STREAM_FLAG_ADVANCE))
+        || !listen_ex_rejects_new_conn(qlistener, preconf))
+        goto err;
+
+    SSL_free(prestream);
+    prestream = NULL;
+    SSL_free(preconf);
+    preconf = NULL;

     if (!TEST_int_eq(SSL_listen_ex(qlistener, serverssl), 1)
         || !TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 0))
@@ -3064,12 +3125,17 @@ static int test_ssl_listen_ex(int idx)
 err:
     SSL_free(sstream);
     SSL_free(cstream);
+    SSL_free(prestream);
+    SSL_free(preconf);
+    SSL_free(threadssl);
+    BIO_free(confbio);
     SSL_free(qlistener);
     SSL_free(serverssl);
     SSL_free(clientssl);
     SSL_CTX_free(sctx);
     SSL_CTX_free(cctx);
     SSL_CTX_free(qmctx);
+    SSL_CTX_free(threadctx);

     return testresult;
 }
@@ -3105,6 +3171,53 @@ err:
     return testresult;
 }

+/* Internal failures are errors and must not consume the queued channel. */
+static int test_ssl_listen_ex_mfail(void)
+{
+    SSL_CTX *cctx = NULL, *sctx = NULL, *qmctx = NULL;
+    SSL *clientssl = NULL, *serverssl = NULL, *qlistener = NULL;
+    int testresult = 0, ret;
+
+    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;
+
+    MFAIL_start();
+    ret = SSL_listen_ex(qlistener, serverssl);
+    MFAIL_end();
+    ERR_clear_error();
+
+    if (mfail_was_triggered()) {
+        if (!TEST_int_eq(ret, -1)
+            || !TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 1)) {
+            /* ADD_MFAIL_NO_CHECK_TEST treats -1 as an unconditional failure. */
+            testresult = -1;
+            goto err;
+        }
+    } else if (!TEST_int_eq(ret, 1)
+        || !TEST_size_t_eq(SSL_get_accept_connection_queue_len(qlistener), 0)) {
+        goto err;
+    }
+
+    testresult = 1;
+
+err:
+    MFAIL_end();
+    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;
@@ -4138,8 +4251,9 @@ int setup_tests(void)
     ADD_TEST(test_quic_forbidden_options);
     ADD_ALL_TESTS(test_quic_set_fd, 3);
     ADD_TEST(test_bio_ssl);
-    ADD_ALL_TESTS(test_ssl_listen_ex, 2);
+    ADD_TEST(test_ssl_listen_ex);
     ADD_TEST(test_ssl_listen_ex_teardown);
+    ADD_MFAIL_NO_CHECK_TEST(test_ssl_listen_ex_mfail);
     ADD_TEST(test_ssl_client_as_ossl_quic_method);
     ADD_TEST(test_back_pressure);
     ADD_TEST(test_multiple_dgrams);