Commit eacd1576adb for php.net
commit eacd1576adb5ec088ebf42ed8a69363a0174826e
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Tue Sep 22 13:20:16 2026 -0400
ext/session: Preserve exceptions thrown by create_sid()
Do not replace pending exceptions with return-value validation errors in
the user handler adapter, including exceptions raised while destroying
invalid return values. This preserves the original exception for
session_start(), session_create_id(), and session_regenerate_id().
Closes GH-23854
diff --git a/NEWS b/NEWS
index 6235628cbb1..9021450c122 100644
--- a/NEWS
+++ b/NEWS
@@ -71,6 +71,8 @@ PHP NEWS
- Session:
. Fixed session_start() continuing after a failed create_sid() when
session.use_strict_mode rejects the supplied ID. (Ilia Alshanetsky)
+ . Fixed exceptions from user-defined create_sid() handlers being replaced
+ by return-value validation errors. (Ilia Alshanetsky)
- Sockets:
. Fixed socket_select() silently truncating sets larger than FD_SETSIZE on
diff --git a/ext/session/mod_user.c b/ext/session/mod_user.c
index 71b18612683..61db72b4cf1 100644
--- a/ext/session/mod_user.c
+++ b/ext/session/mod_user.c
@@ -237,12 +237,16 @@ PS_CREATE_SID_FUNC(user)
}
zval_ptr_dtor(&retval);
} else {
- zend_throw_error(NULL, "No session id returned by function");
+ if (!EG(exception)) {
+ zend_throw_error(NULL, "No session id returned by function");
+ }
return NULL;
}
if (!id) {
- zend_throw_error(NULL, "Session id must be a string");
+ if (!EG(exception)) {
+ zend_throw_error(NULL, "Session id must be a string");
+ }
return NULL;
}
diff --git a/ext/session/tests/user_session_module/session_create_id_create_sid_throws.phpt b/ext/session/tests/user_session_module/session_create_id_create_sid_throws.phpt
index b65c0671d94..a8648fdb5d6 100644
--- a/ext/session/tests/user_session_module/session_create_id_create_sid_throws.phpt
+++ b/ext/session/tests/user_session_module/session_create_id_create_sid_throws.phpt
@@ -36,14 +36,13 @@ public function validateId(string $id): bool
session_create_id();
} catch (Throwable $e) {
echo $e::class, ": ", $e->getMessage(), PHP_EOL;
- $previous = $e->getPrevious();
- echo $previous::class, ": ", $previous->getMessage(), PHP_EOL;
+ var_dump($e->getPrevious());
}
var_dump(session_status() === PHP_SESSION_ACTIVE);
?>
--EXPECT--
-Error: Session id must be a string
Exception: create_sid failed
+NULL
bool(true)
diff --git a/ext/session/tests/user_session_module/session_create_sid_return_destructor_throws.phpt b/ext/session/tests/user_session_module/session_create_sid_return_destructor_throws.phpt
new file mode 100644
index 00000000000..18bb97779e0
--- /dev/null
+++ b/ext/session/tests/user_session_module/session_create_sid_return_destructor_throws.phpt
@@ -0,0 +1,37 @@
+--TEST--
+Exceptions from destruction of an invalid create_sid() return value are preserved
+--EXTENSIONS--
+session
+--FILE--
+<?php
+
+class FailingHandler implements SessionHandlerInterface, SessionIdInterface
+{
+ public function open($path, $name): bool { return true; }
+ public function close(): bool { return true; }
+ public function read($id): string|false { return ''; }
+ public function write($id, $data): bool { return true; }
+ public function destroy($id): bool { return true; }
+ public function gc($max_lifetime): int|false { return 0; }
+
+ #[ReturnTypeWillChange]
+ public function create_sid()
+ {
+ return new class {
+ public function __destruct()
+ {
+ throw new RuntimeException('destructor failed');
+ }
+ };
+ }
+}
+
+session_set_save_handler(new FailingHandler());
+try {
+ session_start();
+} catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+}
+?>
+--EXPECT--
+RuntimeException: destructor failed
diff --git a/ext/session/tests/user_session_module/session_regenerate_id_create_sid_throws.phpt b/ext/session/tests/user_session_module/session_regenerate_id_create_sid_throws.phpt
new file mode 100644
index 00000000000..21e796ca13d
--- /dev/null
+++ b/ext/session/tests/user_session_module/session_regenerate_id_create_sid_throws.phpt
@@ -0,0 +1,57 @@
+--TEST--
+session_regenerate_id() preserves exceptions from create_sid(), including collision retries
+--EXTENSIONS--
+session
+--INI--
+session.use_cookies=0
+session.cache_limiter=
+session.use_strict_mode=1
+session.gc_probability=0
+--FILE--
+<?php
+
+ob_start();
+
+class FailingHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
+{
+ public int $calls = 0;
+ public int $throwAt;
+
+ public function open($path, $name): bool { return true; }
+ public function close(): bool { return true; }
+ public function read($id): string|false { return ''; }
+ public function write($id, $data): bool { return true; }
+ public function destroy($id): bool { return true; }
+ public function gc($max_lifetime): int|false { return 0; }
+ public function updateTimestamp($id, $data): bool { return true; }
+ public function validateId($id): bool { return true; }
+
+ public function create_sid(): string
+ {
+ if (++$this->calls === $this->throwAt) {
+ throw new RuntimeException('create_sid failed');
+ }
+ return 'session' . $this->calls;
+ }
+}
+
+foreach ([2, 3] as $throwAt) {
+ $handler = new FailingHandler();
+ $handler->throwAt = $throwAt;
+ session_set_save_handler($handler);
+ session_id('');
+ session_start();
+
+ try {
+ session_regenerate_id();
+ } catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+ echo 'create_sid calls: ', $handler->calls, PHP_EOL;
+}
+?>
+--EXPECT--
+RuntimeException: create_sid failed
+create_sid calls: 2
+RuntimeException: create_sid failed
+create_sid calls: 3
diff --git a/ext/session/tests/user_session_module/session_start_create_sid_throws.phpt b/ext/session/tests/user_session_module/session_start_create_sid_throws.phpt
new file mode 100644
index 00000000000..b7cd663007e
--- /dev/null
+++ b/ext/session/tests/user_session_module/session_start_create_sid_throws.phpt
@@ -0,0 +1,64 @@
+--TEST--
+session_start() preserves exceptions from create_sid()
+--EXTENSIONS--
+session
+--INI--
+session.use_cookies=0
+session.cache_limiter=
+session.gc_probability=0
+--FILE--
+<?php
+
+ob_start();
+
+class FailingHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface
+{
+ public bool $invalidReturn = false;
+
+ public function open($path, $name): bool { return true; }
+ public function close(): bool { return true; }
+ public function read($id): string|false { return ''; }
+ public function write($id, $data): bool { return true; }
+ public function destroy($id): bool { return true; }
+ public function gc($max_lifetime): int|false { return 0; }
+ public function updateTimestamp($id, $data): bool { return true; }
+
+ public function create_sid(): string
+ {
+ if ($this->invalidReturn) {
+ return [];
+ }
+ throw new RuntimeException('create_sid failed');
+ }
+
+ public function validateId(string $id): bool
+ {
+ return false;
+ }
+}
+
+$handler = new FailingHandler();
+session_set_save_handler($handler);
+
+foreach ([false, true] as $strict) {
+ foreach ([false, true] as $invalidReturn) {
+ echo 'strict mode: ', (int) $strict, ', invalid return: ', (int) $invalidReturn, PHP_EOL;
+ $handler->invalidReturn = $invalidReturn;
+ session_id($strict ? 'rejected' : '');
+ try {
+ session_start(['use_strict_mode' => $strict]);
+ } catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), PHP_EOL;
+ }
+ }
+}
+?>
+--EXPECT--
+strict mode: 0, invalid return: 0
+RuntimeException: create_sid failed
+strict mode: 0, invalid return: 1
+TypeError: FailingHandler::create_sid(): Return value must be of type string, array returned
+strict mode: 1, invalid return: 0
+RuntimeException: create_sid failed
+strict mode: 1, invalid return: 1
+TypeError: FailingHandler::create_sid(): Return value must be of type string, array returned
diff --git a/ext/session/tests/user_session_module/session_start_strict_recreate_create_sid_fails.phpt b/ext/session/tests/user_session_module/session_start_strict_recreate_create_sid_fails.phpt
index 3c3ac1dea32..0ddddaefdbb 100644
--- a/ext/session/tests/user_session_module/session_start_strict_recreate_create_sid_fails.phpt
+++ b/ext/session/tests/user_session_module/session_start_strict_recreate_create_sid_fails.phpt
@@ -44,6 +44,6 @@ public function validateId($id): bool
?>
--EXPECT--
-Error: Session id must be a string
+RuntimeException: create_sid failed
bool(false)
bool(false)