Commit 751190d8e04 for php

commit 751190d8e0420a521ee87c77c623d33842db9fba
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date:   Wed Jul 29 12:00:00 2026 -0400

    ext/session: do not reuse session state a save handler tore down

    session_regenerate_id() runs the userland write or destroy handler and
    then the close handler, and keeps operating on PS(id) afterwards. Any of
    them may call session_destroy(): php_session_destroy() runs
    php_rshutdown_session_globals() even when the recursive call was
    rejected, releasing PS(id) and setting it to NULL, so the following
    zend_string_release_ex() dereferences NULL. Check that the session is
    still active once every handler has run.

    Closes GH-22926

diff --git a/ext/session/session.c b/ext/session/session.c
index bed25c2ebba..a9838cafe66 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -2414,8 +2414,14 @@ PHP_FUNCTION(session_regenerate_id)
 			RETURN_FALSE;
 		}
 	}
+
 	PS(mod)->s_close(&PS(mod_data));

+	if (PS(session_status) != php_session_active) {
+		php_error_docref(NULL, E_WARNING, "Session ID cannot be regenerated because the save handler closed the session");
+		RETURN_FALSE;
+	}
+
 	/* New session data */
 	if (PS(session_vars)) {
 		zend_string_release_ex(PS(session_vars), 0);
diff --git a/ext/session/tests/user_session_module/session_regenerate_id_handler_closes_session.phpt b/ext/session/tests/user_session_module/session_regenerate_id_handler_closes_session.phpt
new file mode 100644
index 00000000000..b5cef21413f
--- /dev/null
+++ b/ext/session/tests/user_session_module/session_regenerate_id_handler_closes_session.phpt
@@ -0,0 +1,65 @@
+--TEST--
+session_regenerate_id() when the close handler destroys the session
+--INI--
+session.gc_probability=0
+--EXTENSIONS--
+session
+--FILE--
+<?php
+
+ob_start();
+
+class MySessionHandler implements SessionHandlerInterface
+{
+    private bool $destroyed = false;
+
+    public function open(string $path, string $name): bool
+    {
+        return true;
+    }
+
+    public function close(): bool
+    {
+        if (!$this->destroyed) {
+            $this->destroyed = true;
+            session_destroy();
+        }
+        return true;
+    }
+
+    public function read(string $id): string|false
+    {
+        return '';
+    }
+
+    public function write(string $id, string $data): bool
+    {
+        return true;
+    }
+
+    public function destroy(string $id): bool
+    {
+        return true;
+    }
+
+    public function gc(int $max_lifetime): int|false
+    {
+        return 0;
+    }
+}
+
+session_set_save_handler(new MySessionHandler(), true);
+session_start();
+
+var_dump(session_regenerate_id(false));
+var_dump(session_status() === PHP_SESSION_NONE);
+
+?>
+--EXPECTF--
+Warning: session_destroy(): Cannot call session save handler in a recursive manner in %s on line %d
+
+Warning: session_destroy(): Session object destruction failed in %s on line %d
+
+Warning: session_regenerate_id(): Session ID cannot be regenerated because the save handler closed the session in %s on line %d
+bool(false)
+bool(true)
diff --git a/ext/session/tests/user_session_module/session_regenerate_id_handler_destroys_session.phpt b/ext/session/tests/user_session_module/session_regenerate_id_handler_destroys_session.phpt
new file mode 100644
index 00000000000..d7b5007ce1f
--- /dev/null
+++ b/ext/session/tests/user_session_module/session_regenerate_id_handler_destroys_session.phpt
@@ -0,0 +1,35 @@
+--TEST--
+session_regenerate_id() when the save handler destroys the session
+--INI--
+session.gc_probability=0
+--EXTENSIONS--
+session
+--FILE--
+<?php
+
+ob_start();
+
+class MySessionHandler extends SessionHandler
+{
+    public function destroy(string $id): bool
+    {
+        session_destroy();
+        return true;
+    }
+}
+
+session_set_save_handler(new MySessionHandler(), true);
+session_start();
+
+var_dump(session_regenerate_id(true));
+var_dump(session_status() === PHP_SESSION_NONE);
+
+?>
+--EXPECTF--
+Warning: session_destroy(): Cannot call session save handler in a recursive manner in %s on line %d
+
+Warning: session_destroy(): Session object destruction failed in %s on line %d
+
+Warning: session_regenerate_id(): Session ID cannot be regenerated because the save handler closed the session in %s on line %d
+bool(false)
+bool(true)