* 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
*
* @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;
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);
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);
#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;
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:
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);
* ============================
*/
-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;
*/
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;
/* 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,
{
QUIC_CHANNEL_ARGS args = { 0 };
QUIC_CHANNEL *ch;
+ SSL *user_ssl = NULL;
+ int ch_cleaned = 0;
args.port = port;
args.is_server = is_server;
/*
* 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
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
* 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)
*/
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;
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
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
--- /dev/null
+/*
+ * 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;
+}
--- /dev/null
+#! /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"])));