Commit a4f89aa07f for openssl.org
commit a4f89aa07fe59075bde0719a0821f0438a366680
Author: Matt Caswell <matt@openssl.foundation>
Date: Tue Sep 8 13:34:53 2026 +0100
Fix out-of-bounds valid_flags access after SSL_set_SSL_CTX()
SSL_new() sizes the connection-local signature algorithm state from the
SSL_CTX the connection was created from: sc->ssl_pkey_num counts the
built-in certificate slots plus one for each of that context's provider
TLS-SIGALG entries, and s3.tmp.valid_flags is later allocated to match.
SSL_set_SSL_CTX() installs a duplicate of the replacement context's CERT
but leaves both of those describing the original context. Peer signature
algorithm codepoints are subsequently resolved against the replacement
context, where a provider sigalg's sig_idx is simply its position in
that context's list. If the replacement context advertises more provider
sigalgs than the original, a codepoint occupying one of the excess slots
yields an index past the end of valid_flags.
The usual route is a switch from the servername callback, which runs
before tls1_set_server_sigalgs() allocates the buffer: the stale
ssl_pkey_num undersizes the allocation, and tls1_process_sigalgs() then
reads one 4-byte word past the end for each such codepoint the peer
offered, and writes CERT_PKEY_EXPLICIT_SIGN|CERT_PKEY_SIGN where that
word already reads zero. The peer chooses how many of these accesses
occur, and at which offsets, by selecting which codepoints to send.
Refresh ssl_pkey_num from the newly installed CERT and, where a
valid_flags buffer already exists, replace it with one sized for that
context. A count which is too large is wrong in the same way as one that
is too small: loops bounded by ssl_pkey_num index cert->pkeys, which the
replacement context sizes. The replacement buffer is allocated before
the point at which the switch is committed, so that a failure can still
be reported rather than leaving the connection half switched.
The built-in slots are copied across rather than zeroed. They may
already hold peer signature algorithm state which is not derived from
the SSL_CTX, and nothing recomputes it after a context switch: at TLS
1.2 and above tls1_check_chain() only ORs CERT_PKEY_SIGN and
CERT_PKEY_EXPLICIT_SIGN in from the existing value, and ssl_set_masks()
needs them to enable ECDSA, Ed25519 and Ed448. Zeroing them makes a
TLSv1.2 handshake with an ECDSA certificate fail with "no shared
cipher". The provider slots are positional and context specific, so they
are reset.
Fixes CVE-2026-72897
Assisted-by: Claude Code:claude-opus-5[1m]
Reviewed-by: Saša NedvÄ›dický <sashan@openssl.org>
Reviewed-by: Neil Horman <nhorman@openssl.org>
Merge-date: Tue Sep 29 11:20:18 2026
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index 48e3fe19ee..daea36fa71 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -5914,6 +5914,7 @@ SSL_CTX *SSL_get_SSL_CTX(const SSL *ssl)
SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx)
{
CERT *new_cert;
+ uint32_t *new_valid_flags = NULL;
SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL_ONLY(ssl);
/* TODO(QUIC FUTURE): Add support for QUIC */
@@ -5938,6 +5939,34 @@ SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx)
*/
if (!ossl_assert(sc->sid_ctx_length <= sizeof(sc->sid_ctx)))
goto err;
+
+ /*
+ * |valid_flags| is sized from the number of signature algorithm slots of
+ * the SSL_CTX the connection was created from, so it must be resized for
+ * the replacement context.
+ *
+ * The built-in slots are indexed by the fixed SSL_PKEY_* constants and so
+ * mean the same thing in either context. They are preserved because they
+ * may already hold peer signature algorithm state which does not depend
+ * on the SSL_CTX. A provider slot index is instead a position in one
+ * context's provider list, so the same index denotes a different
+ * algorithm here and the old value cannot be carried over. They are reset
+ * rather than recomputed: recomputing them means recomputing the shared
+ * signature algorithms against the replacement context, which would let
+ * its preferences take effect on an established connection.
+ */
+ if (sc->s3.tmp.valid_flags != NULL) {
+ /* Should never happen: ssl_cert_new() enforces this */
+ if (!ossl_assert(new_cert->ssl_pkey_num >= SSL_PKEY_NUM))
+ goto err;
+ new_valid_flags = OPENSSL_calloc(new_cert->ssl_pkey_num,
+ sizeof(*new_valid_flags));
+ if (new_valid_flags == NULL)
+ goto err;
+ memcpy(new_valid_flags, sc->s3.tmp.valid_flags,
+ SSL_PKEY_NUM * sizeof(*new_valid_flags));
+ }
+
if (!SSL_CTX_up_ref(ctx))
goto err;
@@ -5954,12 +5983,18 @@ SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx)
ssl_cert_free(sc->cert);
sc->cert = new_cert;
+ sc->ssl_pkey_num = new_cert->ssl_pkey_num;
+ if (new_valid_flags != NULL) {
+ OPENSSL_free(sc->s3.tmp.valid_flags);
+ sc->s3.tmp.valid_flags = new_valid_flags;
+ }
SSL_CTX_free(ssl->ctx); /* decrement reference count */
ssl->ctx = ctx;
return ssl->ctx;
err:
+ OPENSSL_free(new_valid_flags);
ssl_cert_free(new_cert);
return NULL;
}