From 9990f74d480b495218bf7dc7c301cc87e4a09fd0 Mon Sep 17 00:00:00 2001 From: Matt Caswell Date: Wed, 9 Sep 2026 12:30:40 +0100 Subject: [PATCH] Fix out-of-bounds valid_flags access after SSL_set_SSL_CTX() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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ý Reviewed-by: Neil Horman Merge-date: Tue Sep 29 11:23:00 2026 --- ssl/ssl_lib.c | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c index e895774836..95a16f56ad 100644 --- a/ssl/ssl_lib.c +++ b/ssl/ssl_lib.c @@ -5209,6 +5209,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 */ @@ -5229,8 +5230,44 @@ SSL_CTX *SSL_set_SSL_CTX(SSL *ssl, SSL_CTX *ctx) return NULL; } + /* + * |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)) { + ssl_cert_free(new_cert); + return NULL; + } + new_valid_flags = OPENSSL_zalloc(new_cert->ssl_pkey_num + * sizeof(*new_valid_flags)); + if (new_valid_flags == NULL) { + ssl_cert_free(new_cert); + return NULL; + } + memcpy(new_valid_flags, sc->s3.tmp.valid_flags, + SSL_PKEY_NUM * sizeof(*new_valid_flags)); + } + 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; + } /* * Program invariant: |sid_ctx| has fixed size (SSL_MAX_SID_CTX_LENGTH), -- 2.55.0