Commit de68b193a5 for openssl.org

commit de68b193a580ea2fdc800ee08cb30b4c28601a22
Author: Nikola Pajkovsky <nikolap@openssl.org>
Date:   Mon May 4 22:47:04 2026 +0200

    quic: fix incoming port cleanup on failure

    port_make_channel() builds an incoming QUIC channel in stages: allocate
    the channel, create the user SSL and inner TLS objects, optionally copy
    qlog state, then initialise the channel. Under the mfail allocator,
    failures in the middle of that sequence could leave ownership split
    between the partially-created channel and the user SSL, leaking
    allocations from ossl_quic_channel_alloc().

    Make port_new_handshake_layer() return the created user SSL to
    port_make_channel() and detach the borrowed channel before cleaning up
    its own failures. port_make_channel() now owns the error path: it
    detaches any created user SSL from the channel, frees the channel exactly
    once according to whether channel initialisation already ran cleanup, and
    then frees the user SSL.

    Also make QUIC stream map cleanup tolerate a NULL map during partial
    channel cleanup, and add a focused mfail regression test for
    ossl_quic_port_create_incoming().

    Fixes: https://github.com/openssl/openssl/issues/31014
    Signed-off-by: Nikola Pajkovsky <nikolap@openssl.org>

    Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
    Reviewed-by: Matt Caswell <matt@openssl.foundation>
    Reviewed-by: Neil Horman <nhorman@openssl.org>
    MergeDate: Tue May 12 12:01:04 2026
    (Merged from https://github.com/openssl/openssl/pull/31038)

diff --git a/include/internal/quic_port.h b/include/internal/quic_port.h
index 5ddbdd4cd1..0ce54dea97 100644
--- a/include/internal/quic_port.h
+++ b/include/internal/quic_port.h
@@ -47,8 +47,8 @@ typedef struct quic_port_args_st {
      * connection object for the incoming channel
      * user_ssl_arg is expected to point to a quic listener object
      */
-    SSL *(*get_conn_user_ssl)(QUIC_CHANNEL *ch, void *arg);
-    void *user_ssl_arg;
+    SSL *(*get_conn_user_ssl)(QUIC_CHANNEL *ch, QUIC_LISTENER *ql);
+    QUIC_LISTENER *ql;

     /*
      * This SSL_CTX will be used when constructing the handshake layer object
diff --git a/ssl/quic/quic_impl.c b/ssl/quic/quic_impl.c
index ab6da2f566..f2fa0d542b 100644
--- a/ssl/quic/quic_impl.c
+++ b/ssl/quic/quic_impl.c
@@ -4655,9 +4655,8 @@ int ossl_quic_get_key_update_type(const SSL *s)
  *
  * @return Pointer to the SSL object on success, or NULL on failure.
  */
-static SSL *alloc_port_user_ssl(QUIC_CHANNEL *ch, void *arg)
+static SSL *alloc_port_user_ssl(QUIC_CHANNEL *ch, QUIC_LISTENER *ql)
 {
-    QUIC_LISTENER *ql = arg;
     QUIC_CONNECTION *qc = create_qc_from_incoming_conn(ql, ch);

     return (qc == NULL) ? NULL : &qc->obj.ssl;
@@ -4707,7 +4706,7 @@ SSL *ossl_quic_new_listener(SSL_CTX *ctx, uint64_t flags)
     port_args.channel_ctx = ctx;
     port_args.is_multi_conn = 1;
     port_args.get_conn_user_ssl = alloc_port_user_ssl;
-    port_args.user_ssl_arg = ql;
+    port_args.ql = ql;
     if ((flags & SSL_LISTENER_FLAG_NO_VALIDATE) == 0)
         port_args.do_addr_validation = 1;
     ql->port = ossl_quic_engine_create_port(ql->engine, &port_args);
@@ -4764,7 +4763,7 @@ SSL *ossl_quic_new_listener_from(SSL *ssl, uint64_t flags)
     port_args.channel_ctx = ssl->ctx;
     port_args.is_multi_conn = 1;
     port_args.get_conn_user_ssl = alloc_port_user_ssl;
-    port_args.user_ssl_arg = ql;
+    port_args.ql = ql;
     if ((flags & SSL_LISTENER_FLAG_NO_VALIDATE) == 0)
         port_args.do_addr_validation = 1;
     ql->port = ossl_quic_engine_create_port(ctx.qd->engine, &port_args);
@@ -5186,7 +5185,6 @@ static QUIC_CONNECTION *create_qc_from_incoming_conn(QUIC_LISTENER *ql, QUIC_CHA
 #if defined(OPENSSL_THREADS)
     qc->mutex = ql->mutex;
 #endif
-    qc->tls = ossl_quic_channel_get0_tls(ch);
     qc->started = 1;
     qc->as_server = 1;
     qc->as_server_state = 1;
@@ -5195,6 +5193,27 @@ static QUIC_CONNECTION *create_qc_from_incoming_conn(QUIC_LISTENER *ql, QUIC_CHA
     qc->incoming_stream_policy = SSL_INCOMING_STREAM_POLICY_AUTO;
     qc->last_error = SSL_ERROR_NONE;
     qc_update_reject_policy(qc);
+
+    /*
+     * Detach the channel from the freshly-built qc before handing it back.
+     *
+     * qc->ch was set to @p ch above so the in-function initialisers
+     * (e.g. qc_update_reject_policy()) can reach the channel during setup.
+     * Once setup is done we clear it again because, at this point, the qc
+     * does NOT yet own the channel: @p ch is still owned by the caller of
+     * port_new_handshake_layer(), which only commits ownership (by setting
+     * qc->ch = ch on the success path) after the rest of channel
+     * construction has succeeded.
+     *
+     * Leaving qc->ch set here would mean any error path that does
+     * SSL_free(user_ssl) before the commit point cascades into
+     * qc_cleanup() -> ossl_quic_channel_free(qc->ch) and frees a channel
+     * the caller is still using -- the use-after-free / double-free class
+     * of bug we hit before. Resetting to NULL makes SSL_free(user_ssl)
+     * safe at any point until the caller explicitly hands ch over.
+     */
+    qc->ch = NULL;
+
     return qc;

 err:
diff --git a/ssl/quic/quic_port.c b/ssl/quic/quic_port.c
index d4f40a9561..a1b134d9b1 100644
--- a/ssl/quic/quic_port.c
+++ b/ssl/quic/quic_port.c
@@ -117,7 +117,7 @@ QUIC_PORT *ossl_quic_port_new(const QUIC_PORT_ARGS *args)
     port->is_multi_conn = args->is_multi_conn;
     port->validate_addr = args->do_addr_validation;
     port->get_conn_user_ssl = args->get_conn_user_ssl;
-    port->user_ssl_arg = args->user_ssl_arg;
+    port->ql = args->ql;

     if (!port_init(port)) {
         OPENSSL_free(port);
@@ -492,7 +492,41 @@ SSL_CTX *ossl_quic_port_get_channel_ctx(QUIC_PORT *port)
  * ============================
  */

-static SSL *port_new_handshake_layer(QUIC_PORT *port, QUIC_CHANNEL *ch)
+/**
+ * @brief Create the inner TLS handshake layer for a QUIC channel.
+ *
+ * After a successful return:
+ *   - @c *user_sslp holds the user_ssl. The caller is expected to also
+ *     stash the returned @c tls in @c ch->tls so the channel can find its
+ *     inner TLS.
+ *   - @c qc->tls and @c qc->ch are both set, so a single
+ *     @c SSL_free(user_ssl) cascades through @c ossl_quic_free() ->
+ *     @c qc_cleanup() to free the inner TLS and the channel together.
+ *
+ * Failure semantics (returns @c NULL)
+ * -----------------------------------
+ *   - If the callback never returned a user_ssl (callback missing or it
+ *     returned @c NULL), nothing was allocated; @c *user_sslp is left
+ *     untouched and stays whatever the caller initialised it to.
+ *   - Otherwise, this function frees what it allocated and resets
+ *     @c *user_sslp to @c NULL before returning. The caller retains ownership
+ *     of @c ch on failure.
+ *
+ * @param port       Port supplying the channel @c SSL_CTX and the
+ *                   @c get_conn_user_ssl callback.
+ * @param ch         Channel that the new handshake layer is being attached
+ *                   to. Borrowed; on success the channel is shared with
+ *                   user_ssl via @c qc->ch.
+ * @param user_sslp  In/out parameter. On success, set to the user_ssl
+ *                   so the caller can later free the whole graph with
+ *                   @c SSL_free(*user_sslp). On failure, set to @c NULL
+ *                   if the function actually obtained and freed a
+ *                   user_ssl; otherwise left untouched.
+ *
+ * @return The inner TLS @c SSL_CONNECTION (also stored as @c qc->tls)
+ *         on success, or @c NULL on failure.
+ */
+static SSL *port_new_handshake_layer(QUIC_PORT *port, QUIC_CHANNEL *ch, SSL **user_sslp)
 {
     SSL *tls = NULL;
     SSL_CONNECTION *tls_conn = NULL;
@@ -506,34 +540,30 @@ static SSL *port_new_handshake_layer(QUIC_PORT *port, QUIC_CHANNEL *ch)
      */
     if (!ossl_assert(port->get_conn_user_ssl != NULL))
         return NULL;
-    user_ssl = port->get_conn_user_ssl(ch, port->user_ssl_arg);
+    user_ssl = port->get_conn_user_ssl(ch, port->ql);
     if (user_ssl == NULL)
         return NULL;
     qc = (QUIC_CONNECTION *)user_ssl;
-    ql = (QUIC_LISTENER *)port->user_ssl_arg;
+    ql = port->ql;

     /*
      * We expect the user_ssl to be newly created so it must not have an
      * existing qc->tls
      */
-    if (!ossl_assert(qc->tls == NULL)) {
-        SSL_free(user_ssl);
-        return NULL;
-    }
+    if (!ossl_assert(qc->tls == NULL))
+        goto err;

     tls = ossl_ssl_connection_new_int(port->channel_ctx, user_ssl, TLS_method());
-    qc->tls = tls;
-    if (tls == NULL || (tls_conn = SSL_CONNECTION_FROM_SSL(tls)) == NULL) {
-        SSL_free(user_ssl);
-        return NULL;
-    }
+    if (tls == NULL || (tls_conn = SSL_CONNECTION_FROM_SSL(tls)) == NULL)
+        goto err;

     if (ql != NULL && ql->obj.ssl.ctx->new_pending_conn_cb != NULL)
         if (!ql->obj.ssl.ctx->new_pending_conn_cb(ql->obj.ssl.ctx, user_ssl,
-                ql->obj.ssl.ctx->new_pending_conn_arg)) {
-            SSL_free(user_ssl);
-            return NULL;
-        }
+                ql->obj.ssl.ctx->new_pending_conn_arg))
+            goto err;
+    qc->tls = tls;
+    qc->ch = ch;
+    *user_sslp = user_ssl;

     /* Override the user_ssl of the inner connection. */
     tls_conn->s3.flags |= TLS1_FLAGS_QUIC | TLS1_FLAGS_QUIC_INTERNAL;
@@ -541,7 +571,15 @@ static SSL *port_new_handshake_layer(QUIC_PORT *port, QUIC_CHANNEL *ch)
     /* Restrict options derived from the SSL_CTX. */
     tls_conn->options &= OSSL_QUIC_PERMITTED_OPTIONS_CONN;
     tls_conn->pha_enabled = 0;
-    return tls;
+
+    return qc->tls;
+
+err:
+    SSL_free(tls);
+    SSL_free(user_ssl);
+    *user_sslp = NULL;
+
+    return NULL;
 }

 static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
@@ -549,6 +587,8 @@ static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
 {
     QUIC_CHANNEL_ARGS args = { 0 };
     QUIC_CHANNEL *ch;
+    SSL *user_ssl = NULL;
+    int ch_cleaned = 0;

     args.port = port;
     args.is_server = is_server;
@@ -592,11 +632,10 @@ static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
             /*
              * We're using the normal SSL_accept_connection_path
              */
-            ch->tls = port_new_handshake_layer(port, ch);
-            if (ch->tls == NULL) {
-                ossl_quic_channel_free(ch);
-                return NULL;
-            }
+            tls = port_new_handshake_layer(port, ch, &user_ssl);
+            if (tls == NULL)
+                goto err;
+            ch->tls = tls;
         } else {
             /*
              * We're deferring user ssl creation until SSL_listen_ex is called
@@ -611,10 +650,8 @@ static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
     ch->use_qlog = 1;
     if (ch->tls != NULL && ch->tls->ctx->qlog_title != NULL) {
         OPENSSL_free(ch->qlog_title);
-        if ((ch->qlog_title = OPENSSL_strdup(ch->tls->ctx->qlog_title)) == NULL) {
-            ossl_quic_channel_free(ch);
-            return NULL;
-        }
+        if ((ch->qlog_title = OPENSSL_strdup(ch->tls->ctx->qlog_title)) == NULL)
+            goto err;
     }
 #endif

@@ -622,12 +659,25 @@ static QUIC_CHANNEL *port_make_channel(QUIC_PORT *port, SSL *tls, OSSL_QRX *qrx,
      * And finally init the channel struct
      */
     if (!ossl_quic_channel_init(ch)) {
-        OPENSSL_free(ch);
-        return NULL;
+        ch_cleaned = 1;
+        goto err;
     }

     ossl_qtx_set_bio(ch->qtx, port->net_wbio);
     return ch;
+
+err:
+    if (user_ssl != NULL)
+        ((QUIC_CONNECTION *)user_ssl)->ch = NULL;
+
+    if (ch_cleaned)
+        OPENSSL_free(ch);
+    else
+        ossl_quic_channel_free(ch);
+
+    SSL_free(user_ssl);
+
+    return NULL;
 }

 QUIC_CHANNEL *ossl_quic_port_create_outgoing(QUIC_PORT *port, SSL *tls)
diff --git a/ssl/quic/quic_port_local.h b/ssl/quic/quic_port_local.h
index 3343f879cd..c096210863 100644
--- a/ssl/quic/quic_port_local.h
+++ b/ssl/quic/quic_port_local.h
@@ -51,8 +51,8 @@ struct quic_port_st {
      */
     OSSL_LIST_MEMBER(port, QUIC_PORT);

-    SSL *(*get_conn_user_ssl)(QUIC_CHANNEL *ch, void *arg);
-    void *user_ssl_arg;
+    SSL *(*get_conn_user_ssl)(QUIC_CHANNEL *ch, QUIC_LISTENER *ql);
+    QUIC_LISTENER *ql;

     /* Used to create handshake layer objects inside newly created channels. */
     SSL_CTX *channel_ctx;
diff --git a/test/build.info b/test/build.info
index 322a24a1c6..16266b594b 100644
--- a/test/build.info
+++ b/test/build.info
@@ -90,6 +90,10 @@ IF[{- !$disabled{tests} -}]
                      quic_newcid_test quic_srt_gen_test
   ENDIF

+  IF[{- !$disabled{quic} && !$disabled{qlog} -}]
+    PROGRAMS{noinst}=quic_memfail_test
+  ENDIF
+
   IF[{- !$disabled{qlog} -}]
     PROGRAMS{noinst}=json_test quic_qlog_test
   ENDIF
@@ -449,6 +453,12 @@ IF[{- !$disabled{tests} -}]
       SOURCE[quic_qlog_test]=quic_qlog_test.c
       INCLUDE[quic_qlog_test]=../include ../apps/include
       DEPEND[quic_qlog_test]=../libcrypto.a ../libssl.a libtestutil.a
+
+    IF[{- !$disabled{quic} -}]
+      SOURCE[quic_memfail_test]=quic_memfail_test.c
+      INCLUDE[quic_memfail_test]=../include ../apps/include
+      DEPEND[quic_memfail_test]=../libcrypto.a ../libssl.a libtestutil.a
+    ENDIF
   ENDIF

   SOURCE[asynctest]=asynctest.c
diff --git a/test/quic_memfail_test.c b/test/quic_memfail_test.c
new file mode 100644
index 0000000000..8c0dfaca6b
--- /dev/null
+++ b/test/quic_memfail_test.c
@@ -0,0 +1,88 @@
+/*
+ * Copyright 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
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <openssl/crypto.h>
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+
+#include "internal/quic_channel.h"
+#include "internal/quic_port.h"
+#include "internal/quic_ssl.h"
+#include "internal/ssl_unwrap.h"
+#include "../ssl/quic/quic_local.h"
+
+#include "testutil.h"
+
+static int test_ossl_quic_port_create_incoming(void)
+{
+    SSL_CTX *ctx = NULL;
+    SSL *listener = NULL;
+    QUIC_LISTENER *ql;
+    QUIC_CHANNEL *ch = NULL;
+    int ret = 0;
+    OSSL_LIB_CTX *lctx;
+
+    if (!TEST_ptr(lctx = OSSL_LIB_CTX_new()))
+        goto err;
+    ctx = SSL_CTX_new_ex(lctx, NULL, OSSL_QUIC_server_method());
+    if (!TEST_ptr(ctx))
+        goto err;
+
+    if (!TEST_true(ossl_quic_set_diag_title(ctx, "QUIC port qlog leak test")))
+        goto err;
+
+    listener = SSL_new_listener(ctx, SSL_LISTENER_FLAG_NO_VALIDATE);
+    if (!TEST_ptr(listener))
+        goto err;
+
+    ql = QUIC_LISTENER_FROM_SSL(listener);
+    if (!TEST_true(ossl_quic_port_test_and_set_peeloff(ql->port, PEELOFF_ACCEPT)))
+        goto err;
+
+    MFAIL_start();
+    ch = ossl_quic_port_create_incoming(ql->port, NULL);
+    MFAIL_end();
+
+    if (ch == NULL)
+        goto err;
+
+    ret = 1;
+
+err:
+    /*
+     * On success, the channel and the inner TLS are owned by the user_ssl
+     * created inside port_new_handshake_layer (we passed tls=NULL). Freeing
+     * user_ssl cascades through qc_cleanup() to free both the inner TLS and
+     * the channel. ossl_quic_channel_free() alone would leak both.
+     *
+     * On failure (ch == NULL), port_make_channel already cleaned everything up.
+     */
+    if (ch != NULL) {
+        SSL *inner_tls = ossl_quic_channel_get0_tls(ch);
+        SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL(inner_tls);
+        SSL *user_ssl = SSL_CONNECTION_GET_USER_SSL(sc);
+        SSL_free(user_ssl);
+    }
+
+    SSL_free(listener);
+    SSL_CTX_free(ctx);
+    OSSL_LIB_CTX_free(lctx);
+    return ret;
+}
+
+int setup_tests(void)
+{
+    ADD_MFAIL_TEST(test_ossl_quic_port_create_incoming);
+
+    return 1;
+}
diff --git a/test/recipes/90-test_quic_memfail.t b/test/recipes/90-test_quic_memfail.t
new file mode 100644
index 0000000000..c90ac772c9
--- /dev/null
+++ b/test/recipes/90-test_quic_memfail.t
@@ -0,0 +1,22 @@
+#! /usr/bin/env perl
+# Copyright 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
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use OpenSSL::Test;
+use OpenSSL::Test::Utils;
+
+setup("quic_memfail_test");
+
+plan skip_all => "QUIC protocol is not supported by this OpenSSL build"
+    if disabled('quic');
+
+plan skip_all => "qlog is not supported by this OpenSSL build"
+    if disabled('qlog');
+
+plan tests => 1;
+
+ok(run(test(["quic_memfail_test"])));