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)