Commit ced1ca3a0b for openssl.org
commit ced1ca3a0be6f1c4d512805cba16ad4655004fd5
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date: Wed Aug 12 18:53:22 2026 +0900
ssl: handle session ID cache lock failure
SSL_has_matching_session_id() returns 0 both when no matching session
exists and when the cache lock cannot be acquired. Its use in the
built-in generator's retry loop and the final uniqueness check can
therefore treat a lock failure as proof that no conflict exists.
Add an internal tri-state lookup. The final uniqueness check uses it as
a backstop for IDs produced by both built-in and application callbacks,
recording an internal fatal error when the cache cannot be checked. The
retry loop also uses it to prevent the built-in generator from returning
an ID after its own lookup failed, through the existing callback failure
path. Preserve the public API's boolean contract.
Correct the callback-selection cache lock failure reason from
SSL_R_SESSION_ID_CONTEXT_UNINITIALIZED to ERR_R_INTERNAL_ERROR.
Remove a redundant caller-side SSLfatal() that appended a generic
ERR_R_INTERNAL_ERROR after the specific error already recorded. Add a
deterministic unit test for final cache lookup failure.
Fixes #32358
Assisted-by: Codex:gpt-5.6-sol
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
Merge-date: Wed Oct 21 16:25:29 2026
Merged-from: https://github.com/openssl/openssl/pull/32359
diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c
index 9b39de702c..ff039c7138 100644
--- a/ssl/ssl_lib.c
+++ b/ssl/ssl_lib.c
@@ -1158,8 +1158,11 @@ int SSL_set_generate_session_id(SSL *ssl, GEN_SESSION_CB cb)
return 1;
}
-int SSL_has_matching_session_id(const SSL *ssl, const unsigned char *id,
- unsigned int id_len)
+/*
+ * Returns -1 on error, 0 if there is no match and 1 if there is a match.
+ */
+int ssl_has_matching_session_id(const SSL_CONNECTION *sc,
+ const unsigned char *id, unsigned int id_len)
{
/*
* A quick examination of SSL_SESSION_hash and SSL_SESSION_cmp shows how
@@ -1169,22 +1172,30 @@ int SSL_has_matching_session_id(const SSL *ssl, const unsigned char *id,
* by this SSL.
*/
SSL_SESSION r, *p;
- const SSL_CONNECTION *sc = SSL_CONNECTION_FROM_CONST_SSL(ssl);
if (sc == NULL || id_len > sizeof(r.session_id))
- return 0;
+ return -1;
r.ssl_version = sc->version;
r.session_id_length = id_len;
memcpy(r.session_id, id, id_len);
if (!CRYPTO_THREAD_read_lock(sc->session_ctx->lock))
- return 0;
+ return -1;
p = lh_SSL_SESSION_retrieve(sc->session_ctx->sessions, &r);
CRYPTO_THREAD_unlock(sc->session_ctx->lock);
return (p != NULL);
}
+int SSL_has_matching_session_id(const SSL *ssl, const unsigned char *id,
+ unsigned int id_len)
+{
+ const SSL_CONNECTION *sc = SSL_CONNECTION_FROM_CONST_SSL(ssl);
+
+ /* Preserve the public API's boolean return value on internal errors. */
+ return ssl_has_matching_session_id(sc, id, id_len) > 0;
+}
+
int SSL_CTX_set_purpose(SSL_CTX *s, int purpose)
{
return X509_VERIFY_PARAM_set_purpose(s->param, purpose);
diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h
index 205f4d2275..6ba7c54911 100644
--- a/ssl/ssl_local.h
+++ b/ssl/ssl_local.h
@@ -2899,6 +2899,8 @@ __owur CERT *ssl_cert_new(size_t ssl_pkey_num);
__owur CERT *ssl_cert_dup(CERT *cert);
void ssl_cert_clear_certs(CERT *c);
void ssl_cert_free(CERT *c);
+__owur int ssl_has_matching_session_id(const SSL_CONNECTION *sc,
+ const unsigned char *id, unsigned int id_len);
__owur int ssl_generate_session_id(SSL_CONNECTION *s, SSL_SESSION *ss);
__owur int ssl_get_new_session(SSL_CONNECTION *s, int session);
__owur SSL_SESSION *lookup_sess_in_cache(SSL_CONNECTION *s,
diff --git a/ssl/ssl_sess.c b/ssl/ssl_sess.c
index 7a74b5bbef..99d35f5f89 100644
--- a/ssl/ssl_sess.c
+++ b/ssl/ssl_sess.c
@@ -328,7 +328,10 @@ unsigned int SSL_SESSION_get_compress_id(const SSL_SESSION *s)
static int def_generate_session_id(SSL *ssl, unsigned char *id,
unsigned int *id_len)
{
+ SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL(ssl);
+ int match;
unsigned int retry = 0;
+
do {
if (RAND_bytes_ex(ssl->ctx->libctx, id, *id_len, 0) <= 0)
return 0;
@@ -337,7 +340,10 @@ static int def_generate_session_id(SSL *ssl, unsigned char *id,
id[0]++;
}
#endif
- } while (SSL_has_matching_session_id(ssl, id, *id_len) && (++retry < MAX_SESS_ID_ATTEMPTS));
+ match = ssl_has_matching_session_id(sc, id, *id_len);
+ if (match < 0)
+ return 0;
+ } while (match > 0 && (++retry < MAX_SESS_ID_ATTEMPTS));
if (retry < MAX_SESS_ID_ATTEMPTS)
return 1;
/* else - woops a session_id match */
@@ -354,6 +360,7 @@ static int def_generate_session_id(SSL *ssl, unsigned char *id,
int ssl_generate_session_id(SSL_CONNECTION *s, SSL_SESSION *ss)
{
+ int match;
unsigned int tmp;
GEN_SESSION_CB cb = def_generate_session_id;
SSL *ssl = SSL_CONNECTION_GET_SSL(s);
@@ -401,8 +408,7 @@ int ssl_generate_session_id(SSL_CONNECTION *s, SSL_SESSION *ss)
}
if (!CRYPTO_THREAD_read_lock(s->session_ctx->lock)) {
CRYPTO_THREAD_unlock(ssl->lock);
- SSLfatal(s, SSL_AD_INTERNAL_ERROR,
- SSL_R_SESSION_ID_CONTEXT_UNINITIALIZED);
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
return 0;
}
if (s->generate_session_id)
@@ -432,8 +438,13 @@ int ssl_generate_session_id(SSL_CONNECTION *s, SSL_SESSION *ss)
}
ss->session_id_length = tmp;
/* Finally, check for a conflict */
- if (SSL_has_matching_session_id(ssl, ss->session_id,
- (unsigned int)ss->session_id_length)) {
+ match = ssl_has_matching_session_id(s, ss->session_id,
+ (unsigned int)ss->session_id_length);
+ if (match < 0) {
+ SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ return 0;
+ }
+ if (match > 0) {
SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_SSL_SESSION_ID_CONFLICT);
return 0;
}
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index 82a18721f4..bc00da04d1 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -1431,7 +1431,7 @@ static int final_server_name(SSL_CONNECTION *s, unsigned int context, int sent)
ss->ext.tick_lifetime_hint = 0;
ss->ext.tick_age_add = 0;
if (!ssl_generate_session_id(s, ss)) {
- SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+ /* SSLfatal() already called */
return 0;
}
} else {
diff --git a/test/unit/build.info b/test/unit/build.info
index 76598e358e..e086f0ed3d 100644
--- a/test/unit/build.info
+++ b/test/unit/build.info
@@ -68,6 +68,12 @@ IF[{- $config{target} =~ /^(?:linux|BSD)/ -}]
WRAP[crypto/bio/test_bss_sock]=read write BIO_closesocket
INCLUDE[crypto/bio/test_bss_sock]=../../include ../../crypto/bio
DEPEND[crypto/bio/test_bss_sock]=../../libcrypto.a
+
+ PROGRAMS{noinst}=ssl/test_ssl_sess
+ SOURCE[ssl/test_ssl_sess]=ssl/test_ssl_sess.c
+ INCLUDE[ssl/test_ssl_sess]=../../include ../../ssl
+ DEPEND[ssl/test_ssl_sess]=../../libssl.a ../../libcrypto.a
+ WRAP[ssl/test_ssl_sess]=CRYPTO_THREAD_read_lock
ENDIF
IF[{- $config{target} =~ /^VC-/ -}]
diff --git a/test/unit/ssl/test_ssl_sess.c b/test/unit/ssl/test_ssl_sess.c
new file mode 100644
index 0000000000..7a7c7d4cc3
--- /dev/null
+++ b/test/unit/ssl/test_ssl_sess.c
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2026 The OpenSSL Project Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License 2.0 (the "License"). You may not use
+ * this file except in compliance with the License. You can obtain a copy
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <stdarg.h>
+#include <stddef.h>
+#include <setjmp.h>
+#include <stdlib.h>
+#include <string.h>
+#include <cmocka.h>
+
+#include <openssl/crypto.h>
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+
+#include "internal/ssl_unwrap.h"
+#include "ssl_local.h"
+
+typedef struct {
+ SSL_CTX *ctx;
+ SSL *ssl;
+ SSL_CONNECTION *sc;
+ SSL_SESSION *session;
+} SESSION_ID_FIXTURE;
+
+/*
+ * Fail one explicitly armed lock and pass all other lock calls through. The
+ * callback arms the next cache read, which is the final collision check.
+ */
+static CRYPTO_RWLOCK *read_lock_to_fail;
+
+/* wraps */
+
+int __real_CRYPTO_THREAD_read_lock(CRYPTO_RWLOCK *lock);
+int __wrap_CRYPTO_THREAD_read_lock(CRYPTO_RWLOCK *lock);
+
+int __wrap_CRYPTO_THREAD_read_lock(CRYPTO_RWLOCK *lock)
+{
+ if (lock != NULL && lock == read_lock_to_fail) {
+ read_lock_to_fail = NULL;
+ return 0;
+ }
+
+ return __real_CRYPTO_THREAD_read_lock(lock);
+}
+
+/* fault injection */
+
+static void fail_next_read_lock(CRYPTO_RWLOCK *lock)
+{
+ assert_null(read_lock_to_fail);
+ assert_non_null(lock);
+ read_lock_to_fail = lock;
+}
+
+/* helpers */
+
+static int generate_session_id_then_fail_cache_lock(SSL *ssl,
+ unsigned char *id, unsigned int *id_len)
+{
+ SSL_CONNECTION *sc = SSL_CONNECTION_FROM_SSL(ssl);
+
+ memset(id, 0x5a, *id_len);
+ fail_next_read_lock(sc->session_ctx->lock);
+ return 1;
+}
+
+/* fixtures */
+
+static int setup(void **state)
+{
+ SESSION_ID_FIXTURE *fixture = calloc(1, sizeof(*fixture));
+
+ assert_non_null(fixture);
+ fixture->ctx = SSL_CTX_new(TLS_server_method());
+ assert_non_null(fixture->ctx);
+ fixture->ssl = SSL_new(fixture->ctx);
+ assert_non_null(fixture->ssl);
+ fixture->sc = SSL_CONNECTION_FROM_SSL(fixture->ssl);
+ assert_non_null(fixture->sc);
+ fixture->session = SSL_SESSION_new();
+ assert_non_null(fixture->session);
+
+ fixture->sc->version = TLS1_2_VERSION;
+ read_lock_to_fail = NULL;
+ ERR_clear_error();
+ *state = fixture;
+ return 0;
+}
+
+static int teardown(void **state)
+{
+ SESSION_ID_FIXTURE *fixture = *state;
+
+ assert_null(read_lock_to_fail);
+ SSL_SESSION_free(fixture->session);
+ SSL_free(fixture->ssl);
+ SSL_CTX_free(fixture->ctx);
+ free(fixture);
+ ERR_clear_error();
+ return 0;
+}
+
+static void test_generate_session_id_final_cache_lock_failure(void **state)
+{
+ SESSION_ID_FIXTURE *fixture = *state;
+ unsigned long err;
+
+ assert_true(SSL_set_generate_session_id(fixture->ssl,
+ generate_session_id_then_fail_cache_lock));
+
+ assert_false(ssl_generate_session_id(fixture->sc, fixture->session));
+ /* Inspect the first error recorded for this failure. */
+ err = ERR_peek_error();
+ assert_true(ossl_statem_in_error(fixture->sc));
+ assert_int_equal(ERR_GET_LIB(err), ERR_LIB_SSL);
+ assert_int_equal(ERR_GET_REASON(err), ERR_R_INTERNAL_ERROR);
+}
+
+int main(void)
+{
+ const struct CMUnitTest tests[] = {
+ cmocka_unit_test_setup_teardown(
+ test_generate_session_id_final_cache_lock_failure, setup,
+ teardown),
+ };
+
+ cmocka_set_message_output(CM_OUTPUT_TAP);
+
+ return cmocka_run_group_tests(tests, NULL, NULL);
+}