Commit b24c43f9b8 for openssl.org

commit b24c43f9b8806f9fa7af89816db500f4dba7051b
Author: Matt Caswell <matt@openssl.foundation>
Date:   Tue Jun 16 11:35:33 2026 +0100

    Add regression test for remove_session_cb under lock

    Install a remove_session_cb that calls SSL_CTX_flush_sessions_ex().
    If the callback is invoked while ctx->lock is held, the nested
    flush call deadlocks immediately.

    The test covers the SSL_CTX_add_session() eviction path (adding a
    second session to a size == 1 cache evicts the first, firing the
    callback) and the SSL_CTX_flush_sessions_ex() path (SSL_CTX_free()
    flushes the remaining session via flush_sessions_ex()).

    Assisted-by: Claude:claude-sonnet-4-6

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Paul Dale <paul.dale@oracle.com>
    MergeDate: Wed Jul  8 17:58:48 2026
    (Merged from https://github.com/openssl/openssl/pull/31540)

diff --git a/test/sslapitest.c b/test/sslapitest.c
index c5517016c5..1c106355c6 100644
--- a/test/sslapitest.c
+++ b/test/sslapitest.c
@@ -3030,6 +3030,58 @@ static int test_session_with_both_cache(void)
 #endif
 }

+/*
+ * Test that remove_session_cb is not invoked while ctx->lock is held.
+ * The callback calls SSL_CTX_flush_sessions_ex(), which itself tries to
+ * acquire ctx->lock; if the lock is already held when the callback fires,
+ * the nested acquisition deadlocks immediately.  t = 1 (Unix epoch + 1s) is
+ * used so that no current sessions are flushed and the callback is not
+ * re-entered.
+ */
+static void remove_session_lock_test_cb(SSL_CTX *ctx, SSL_SESSION *sess)
+{
+    SSL_CTX_flush_sessions_ex(ctx, 1);
+}
+
+static int test_remove_session_cb_not_under_lock(void)
+{
+    SSL_CTX *ctx = NULL;
+    SSL_SESSION *sess1 = NULL, *sess2 = NULL;
+    static const unsigned char sid1[] = { 1 };
+    static const unsigned char sid2[] = { 2 };
+    int testresult = 0;
+
+    if (!TEST_ptr(ctx = SSL_CTX_new_ex(libctx, NULL, TLS_server_method())))
+        goto end;
+
+    SSL_CTX_sess_set_cache_size(ctx, 1);
+    SSL_CTX_sess_set_remove_cb(ctx, remove_session_lock_test_cb);
+
+    if (!TEST_ptr(sess1 = SSL_SESSION_new())
+        || !TEST_true(SSL_SESSION_set1_id(sess1, sid1, sizeof(sid1)))
+        || !TEST_true(SSL_CTX_add_session(ctx, sess1)))
+        goto end;
+
+    if (!TEST_ptr(sess2 = SSL_SESSION_new())
+        || !TEST_true(SSL_SESSION_set1_id(sess2, sid2, sizeof(sid2))))
+        goto end;
+
+    /*
+     * Adding sess2 evicts sess1 (cache is full), firing remove_session_cb.
+     * If the callback is invoked while holding ctx->lock the flush call
+     * inside it will deadlock.
+     */
+    if (!TEST_true(SSL_CTX_add_session(ctx, sess2)))
+        goto end;
+
+    testresult = 1;
+end:
+    SSL_SESSION_free(sess1);
+    SSL_SESSION_free(sess2);
+    SSL_CTX_free(ctx);
+    return testresult;
+}
+
 static int test_session_wo_ca_names(void)
 {
 #ifndef OSSL_NO_USABLE_TLS1_3
@@ -15174,6 +15226,7 @@ int setup_tests(void)
     ADD_TEST(test_session_with_only_int_cache);
     ADD_TEST(test_session_with_only_ext_cache);
     ADD_TEST(test_session_with_both_cache);
+    ADD_TEST(test_remove_session_cb_not_under_lock);
     ADD_TEST(test_session_wo_ca_names);
 #ifndef OSSL_NO_USABLE_TLS1_3
     ADD_ALL_TESTS(test_stateful_tickets, 3);