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;
 }