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)