Commit 91a934ff02 for openssl.org
commit 91a934ff0221b166257221f56982ee8082c29c06
Author: Nikola Pajkovsky <nikolap@openssl.org>
Date: Tue May 5 14:09:50 2026 +0200
quic: make ch_cleanup() idempotent and simplify channel error path
ch_init() calls ch_cleanup() on its own failure, after which
port_make_channel() may still call ossl_quic_channel_free() (which calls
ch_cleanup() again). The second call double-freed fields such as
ch->qlog_title.
To handle this, ch_cleanup() now NULLs every owned pointer after its
free and clears the have_statm / have_qsm flags after their destructors,
making it safe to invoke twice on the same channel.
With ch_cleanup() idempotent, port_make_channel() no longer needs the
ch_cleaned flag and the bare OPENSSL_free(ch) branch: the error path
unconditionally calls ossl_quic_channel_free() regardless of whether
ch_init() succeeded, partially initialized the channel, or already ran
ch_cleanup() on itself.
Signed-off-by: Nikola Pajkovsky <nikolap@openssl.org>
Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Matt Caswell <matt@openssl.foundation>
Reviewed-by: Norbert Pocs <norbertp@openssl.org>
MergeDate: Thu May 28 07:26:22 2026
(Merged from https://github.com/openssl/openssl/pull/31177)
diff --git a/ssl/quic/quic_channel.c b/ssl/quic/quic_channel.c
index 4c299a53e5..07258f1a9b 100644
--- a/ssl/quic/quic_channel.c
+++ b/ssl/quic/quic_channel.c
@@ -357,6 +357,11 @@ err:
return 0;
}
+/*
+ * ch_cleanup() is idempotent: every owned pointer is NULL'd after its free,
+ * and every "have_*" flag is reset after its destructor runs. Calling this
+ * twice on the same channel is safe.
+ */
static void ch_cleanup(QUIC_CHANNEL *ch)
{
uint32_t pn_space;
@@ -374,33 +379,53 @@ static void ch_cleanup(QUIC_CHANNEL *ch)
ossl_quic_srtm_cull(ch->srtm, ch);
ossl_quic_tx_packetiser_free(ch->txp);
+ ch->txp = NULL;
ossl_quic_txpim_free(ch->txpim);
+ ch->txpim = NULL;
ossl_quic_cfq_free(ch->cfq);
+ ch->cfq = NULL;
ossl_qtx_free(ch->qtx);
- if (ch->cc_data != NULL)
+ ch->qtx = NULL;
+ if (ch->cc_data != NULL) {
ch->cc_method->free(ch->cc_data);
- if (ch->have_statm)
+ ch->cc_data = NULL;
+ }
+ if (ch->have_statm) {
ossl_statm_destroy(&ch->statm);
+ ch->have_statm = 0;
+ }
ossl_ackm_free(ch->ackm);
+ ch->ackm = NULL;
- if (ch->have_qsm)
+ if (ch->have_qsm) {
ossl_quic_stream_map_cleanup(&ch->qsm);
+ ch->have_qsm = 0;
+ }
for (pn_space = QUIC_PN_SPACE_INITIAL; pn_space < QUIC_PN_SPACE_NUM; ++pn_space) {
ossl_quic_sstream_free(ch->crypto_send[pn_space]);
+ ch->crypto_send[pn_space] = NULL;
ossl_quic_rstream_free(ch->crypto_recv[pn_space]);
+ ch->crypto_recv[pn_space] = NULL;
}
ossl_qrx_pkt_release(ch->qrx_pkt);
ch->qrx_pkt = NULL;
ossl_quic_tls_free(ch->qtls);
+ ch->qtls = NULL;
ossl_qrx_free(ch->qrx);
+ ch->qrx = NULL;
OPENSSL_free(ch->local_transport_params);
+ ch->local_transport_params = NULL;
OPENSSL_free((char *)ch->terminate_cause.reason);
+ ch->terminate_cause.reason = NULL;
OSSL_ERR_STATE_free(ch->err_state);
+ ch->err_state = NULL;
OPENSSL_free(ch->ack_range_scratch);
+ ch->ack_range_scratch = NULL;
OPENSSL_free(ch->pending_new_token);
+ ch->pending_new_token = NULL;
if (ch->on_port_list) {
ossl_list_ch_remove(&ch->port->channel_list, ch);
@@ -412,7 +437,9 @@ static void ch_cleanup(QUIC_CHANNEL *ch)
ossl_qlog_flush(ch->qlog); /* best effort */
OPENSSL_free(ch->qlog_title);
+ ch->qlog_title = NULL;
ossl_qlog_free(ch->qlog);
+ ch->qlog = NULL;
#endif
}
diff --git a/ssl/quic/quic_port.c b/ssl/quic/quic_port.c
index a1b134d9b1..42b121103d 100644
--- a/ssl/quic/quic_port.c
+++ b/ssl/quic/quic_port.c
@@ -588,7 +588,6 @@ 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;
@@ -658,10 +657,8 @@ 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)) {
- ch_cleaned = 1;
+ if (!ossl_quic_channel_init(ch))
goto err;
- }
ossl_qtx_set_bio(ch->qtx, port->net_wbio);
return ch;
@@ -670,11 +667,7 @@ err:
if (user_ssl != NULL)
((QUIC_CONNECTION *)user_ssl)->ch = NULL;
- if (ch_cleaned)
- OPENSSL_free(ch);
- else
- ossl_quic_channel_free(ch);
-
+ ossl_quic_channel_free(ch);
SSL_free(user_ssl);
return NULL;
diff --git a/test/quic_memfail_test.c b/test/quic_memfail_test.c
index 8c0dfaca6b..6e5b1be176 100644
--- a/test/quic_memfail_test.c
+++ b/test/quic_memfail_test.c
@@ -20,6 +20,7 @@
#include "internal/quic_ssl.h"
#include "internal/ssl_unwrap.h"
#include "../ssl/quic/quic_local.h"
+#include "../ssl/quic/quic_port_local.h"
#include "testutil.h"
@@ -80,9 +81,67 @@ err:
return ret;
}
+static int test_ch_cleanup_idempotent(void)
+{
+ SSL_CTX *ctx = NULL;
+ SSL *listener = NULL;
+ QUIC_LISTENER *ql;
+ QUIC_CHANNEL_ARGS args = { 0 };
+ QUIC_CHANNEL *ch = NULL;
+ int alloc_failed = 0;
+ int ret = 0;
+ OSSL_LIB_CTX *lctx = NULL;
+
+ 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;
+
+ listener = SSL_new_listener(ctx, SSL_LISTENER_FLAG_NO_VALIDATE);
+ if (!TEST_ptr(listener))
+ goto err;
+ ql = QUIC_LISTENER_FROM_SSL(listener);
+
+ args.port = ql->port;
+ args.lcidm = ql->port->lcidm;
+ args.srtm = ql->port->srtm;
+ args.is_server = 1;
+ args.is_tserver_ch = 1;
+ args.use_qlog = 1;
+ args.qlog_title = "qlog";
+
+ MFAIL_start();
+ ch = ossl_quic_channel_alloc(&args);
+ if (ch == NULL) {
+ alloc_failed = 1;
+ } else {
+ if (!ossl_quic_channel_init(ch))
+ alloc_failed = 1;
+
+ /*
+ * Whether init succeeded or failed, ossl_quic_channel_free() runs
+ * ch_cleanup(). On the failure path that's the second ch_cleanup()
+ * for this channel and must not crash or double-free.
+ */
+ ossl_quic_channel_free(ch);
+ ch = NULL;
+ }
+ MFAIL_end();
+
+ ret = alloc_failed ? 0 : 1;
+
+err:
+ 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);
+ ADD_MFAIL_TEST(test_ch_cleanup_idempotent);
return 1;
}