Commit 0ed06337e3 for openssl.org
commit 0ed06337e38ec70e5beb043d5a1da9a6b6e8c57e
Author: Neil Horman <nhorman@openssl.org>
Date: Sun Mar 8 16:49:09 2026 -0400
Fix use after free in quic_connection freeing if up ref fails
Issue https://github.com/openssl/openssl/issues/3030
Found a use after free case in ossl_quic_accept_connection in the event
that we fail to up_ref the associated quic listener object.
If we fail to take the up ref on the listener object in this function,
we free the SSL object, which calls into
SSL_free->ossl_quic_free->qc_cleanup, which because we have an
associated listener, we free the mutex for, and then get a use-afer-free
when we try to unlock that mutex shortly thereafter.
We really need to fix 3 problems here:
1) The use after free. Handle this bt ensuring that the listener is
assigned first.
2) A deadlock, since we already hold the associated mutex, we need to
defer the free operation until after we unlock the mutex.
3) Don't drop the refcount on the listener object in ossl_quic_cleanup
(since we failed to up-ref it here). Handle this by adding a flag to
indicate up-ref failure in the quic-connection object.
Problem was confirmed by synthetically failing the up ref in local
testing, and this patch was confirmed to fix the issue.
Also, we need to adjust some of the tests in quicapitest here, as
several tests just assume that SSL_accept_connection will return a
non-null value.
Fixes #30307
Reviewed-by: Paul Dale <paul.dale@oracle.com>
Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.org>
MergeDate: Wed Mar 11 09:22:35 2026
(Merged from https://github.com/openssl/openssl/pull/30311)
diff --git a/ssl/quic/quic_impl.c b/ssl/quic/quic_impl.c
index fd4e0ced3f..ea76430009 100644
--- a/ssl/quic/quic_impl.c
+++ b/ssl/quic/quic_impl.c
@@ -4736,9 +4736,10 @@ SSL *ossl_quic_accept_connection(SSL *ssl, uint64_t flags)
int ret;
QCTX ctx;
SSL *conn_ssl = NULL;
+ SSL *conn_ssl_tmp = NULL;
SSL_CONNECTION *conn = NULL;
QUIC_CHANNEL *new_ch = NULL;
- QUIC_CONNECTION *qc;
+ QUIC_CONNECTION *qc = NULL;
int no_block = ((flags & SSL_ACCEPT_CONNECTION_NO_BLOCK) != 0);
if (!expect_quic_listener(ssl, &ctx))
@@ -4793,28 +4794,38 @@ SSL *ossl_quic_accept_connection(SSL *ssl, uint64_t flags)
* bound to new_ch. If channel constructor fails to create any item here
* it just fails to create channel.
*/
- if (!ossl_assert((conn_ssl = ossl_quic_channel_get0_tls(new_ch)) != NULL)
- || !ossl_assert((conn = SSL_CONNECTION_FROM_SSL(conn_ssl)) != NULL)
- || !ossl_assert((conn_ssl = SSL_CONNECTION_GET_USER_SSL(conn)) != NULL))
+ if (!ossl_assert((conn_ssl_tmp = ossl_quic_channel_get0_tls(new_ch)) != NULL)
+ || !ossl_assert((conn = SSL_CONNECTION_FROM_SSL(conn_ssl_tmp)) != NULL)
+ || !ossl_assert((conn_ssl_tmp = SSL_CONNECTION_GET_USER_SSL(conn)) != NULL))
goto out;
- qc = (QUIC_CONNECTION *)conn_ssl;
- qc->pending = 0;
- if (!SSL_up_ref(&ctx.ql->obj.ssl)) {
- /*
- * You might expect ossl_quic_channel_free() to be called here. Be
- * assured it happens, The process goes as follows:
- * - The SSL_free() here is being handled by ossl_quic_free().
- * - The very last step of ossl_quic_free() is call to qc_cleanup()
- * where channel gets freed.
- */
- SSL_free(conn_ssl);
+ qc = (QUIC_CONNECTION *)conn_ssl_tmp;
+ if (SSL_up_ref(&ctx.ql->obj.ssl)) {
+ qc->listener = ctx.ql;
+ conn_ssl = conn_ssl_tmp;
+ conn_ssl_tmp = NULL;
+ qc->pending = 0;
}
- qc->listener = ctx.ql;
out:
qctx_unlock(&ctx);
+ /*
+ * You might expect ossl_quic_channel_free() to be called here. Be
+ * assured it happens, The process goes as follows:
+ * - The SSL_free() here is being handled by ossl_quic_free().
+ * - The very last step of ossl_quic_free() is call to qc_cleanup()
+ * where channel gets freed.
+ * NOTE: We defer this SSL_free until after the call to qctx_unlock above
+ * to avoid the deadlock that would occur when ossl_quic_free attempts to
+ * re-acquire this mutex. We also do the gymnastics with conn_ssl and
+ * conn_ssl_tmp above so that we only actually do the free on the SSL
+ * object if the up-ref above fails, in such a way that we don't unbalance
+ * the listener refcount (i.e. if the up-ref fails above, we don't set the
+ * listener pointer so that we don't then drop the ref-count erroneously
+ * during the free operation.
+ */
+ SSL_free(conn_ssl_tmp);
return conn_ssl;
}
diff --git a/test/quicapitest.c b/test/quicapitest.c
index c322aed510..ad2c8ca2a9 100644
--- a/test/quicapitest.c
+++ b/test/quicapitest.c
@@ -2894,8 +2894,7 @@ static int test_ssl_listen_ex(void)
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_true(create_bare_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE, 0, 0)))
/*
* Ensure that, now that we have used SSL_listen_ex, SSL_accept_connection
@@ -3012,8 +3011,8 @@ static int test_ssl_set_verify(void)
serverssl = SSL_accept_connection(qlistener, 0);
/* 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(serverssl)
+ || !TEST_true(create_bare_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE, 0, 0)))
goto err;
testresult = 1;
@@ -3225,8 +3224,8 @@ static int test_client_hello_retry(void)
serverssl = SSL_accept_connection(qlistener, 0);
/* 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(serverssl)
+ || !TEST_true(create_bare_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE, 0, 0)))
goto err;
testresult = 1;