Commit 61c2b72eec9 for php.net
commit 61c2b72eec91ce57124bb66c686582d2ae12cf63
Author: Gina Peter Banyard <girgias@php.net>
Date: Sat Aug 15 16:54:05 2026 +0100
session: fix some expectations for C session handlers (#23297)
We *explicitly* say that mod_data will be not NULL for create_sid hooks and we violate this invariant if SessionHandler::open() is overwridden without calling the parent method.
At the same time add comments explaining the situation and fix some other potential footguns.
diff --git a/ext/session/mod_files.c b/ext/session/mod_files.c
index 3a7f8aec452..c3c616c6479 100644
--- a/ext/session/mod_files.c
+++ b/ext/session/mod_files.c
@@ -101,13 +101,13 @@ const ps_module ps_mod_files = {
PS_MOD_UPDATE_TIMESTAMP(files)
};
-static char *ps_files_path_create(char *buf, size_t buflen, ps_files *data, const zend_string *key)
+ZEND_ATTRIBUTE_NONNULL static char *ps_files_path_create(char *buf, size_t buflen, ps_files *data, const zend_string *key)
{
const char *p;
int i;
size_t n;
- if (!data || ZSTR_LEN(key) <= data->dirdepth ||
+ if (ZSTR_LEN(key) <= data->dirdepth ||
buflen < (ZSTR_LEN(data->basedir) + 2 * data->dirdepth + ZSTR_LEN(key) + 5 + sizeof(FILE_PREFIX))) {
return NULL;
}
@@ -351,12 +351,12 @@ static int ps_files_cleanup_dir(const zend_string *dirname, zend_long maxlifetim
return nrdels;
}
-static zend_result ps_files_key_exists(ps_files *data, const zend_string *key)
+ZEND_ATTRIBUTE_NONNULL static zend_result ps_files_key_exists(ps_files *data, const zend_string *key)
{
char buf[MAXPATHLEN];
zend_stat_t sbuf = {0};
- if (!key || !ps_files_path_create(buf, sizeof(buf), data, key)) {
+ if (!ps_files_path_create(buf, sizeof(buf), data, key)) {
return FAILURE;
}
if (VCWD_STAT(buf, &sbuf)) {
@@ -678,8 +678,7 @@ PS_CREATE_SID_FUNC(files)
}
}
/* Check collision */
- /* FIXME: mod_data(data) should not be NULL (User handler could be NULL) */
- if (data && ps_files_key_exists(data, sid) == SUCCESS) {
+ if (ps_files_key_exists(data, sid) == SUCCESS) {
zend_string_release_ex(sid, false);
sid = NULL;
if (--maxfail < 0) {
diff --git a/ext/session/mod_user.c b/ext/session/mod_user.c
index 71b8abdea8b..5c6fe557771 100644
--- a/ext/session/mod_user.c
+++ b/ext/session/mod_user.c
@@ -16,6 +16,8 @@
#include "php_session.h"
#include "mod_user.h"
+#include "zend_exceptions.h"
+
const ps_module ps_mod_user = {
PS_MOD_UPDATE_TIMESTAMP(user)
};
@@ -222,27 +224,23 @@ PS_CREATE_SID_FUNC(user)
{
/* maintain backwards compatibility */
if (!Z_ISUNDEF(PSF(create_sid))) {
- zend_string *id = NULL;
zval retval;
ps_call_handler(&PSF(create_sid), 0, NULL, &retval);
-
- if (!Z_ISUNDEF(retval)) {
- if (Z_TYPE(retval) == IS_STRING) {
- id = zend_string_copy(Z_STR(retval));
- }
- zval_ptr_dtor(&retval);
- } else {
- zend_throw_error(NULL, "No session id returned by function");
+ /* Exception was thrown */
+ if (Z_ISUNDEF(retval)) {
return NULL;
}
- if (!id) {
- zend_throw_error(NULL, "Session id must be a string");
+ if (UNEXPECTED(Z_TYPE(retval) != IS_STRING)) {
+ /* Will no longer be needed in PHP 9 as the interface return type will be in effect */
+ zend_throw_error(zend_ce_type_error, "Session id must be of type string, %s given", zend_zval_type_name(&retval));
+ zval_ptr_dtor(&retval);
return NULL;
}
+ ZEND_ASSERT(Z_TYPE(retval) == IS_STRING);
- return id;
+ return Z_STR(retval);
}
/* function as defined by PS_MOD */
diff --git a/ext/session/mod_user_class.c b/ext/session/mod_user_class.c
index a6bd69c91a0..9626dd27dfb 100644
--- a/ext/session/mod_user_class.c
+++ b/ext/session/mod_user_class.c
@@ -51,6 +51,7 @@ PHP_METHOD(SessionHandler, open)
} zend_end_try();
if (SUCCESS == ret) {
+ ZEND_ASSERT(PS(mod_data) && "opened default session must have mod_data");
PS(mod_user_is_open) = true;
}
@@ -142,13 +143,18 @@ PHP_METHOD(SessionHandler, gc)
PHP_METHOD(SessionHandler, create_sid)
{
- zend_string *id;
-
ZEND_PARSE_PARAMETERS_NONE();
PS_SANITY_CHECK;
+ if (!PS(mod_user_is_open)) {
+ php_error_docref(NULL, E_WARNING, "Parent session handler is not open, defaulting to session_create_id()");
+ RETURN_STR(php_session_create_id(NULL));
+ }
- id = PS(default_mod)->s_create_sid(&PS(mod_data));
+ zend_string *id = PS(default_mod)->s_create_sid(&PS(mod_data));
+ if (UNEXPECTED(id == NULL)) {
+ zend_throw_error(NULL, "Failed to create session ID: %s (path: %s)", PS(mod)->s_name, ZSTR_VAL(PS(save_path)));
+ }
RETURN_STR(id);
}
diff --git a/ext/session/session.c b/ext/session/session.c
index 452a3446fc1..b1f1d2a3630 100644
--- a/ext/session/session.c
+++ b/ext/session/session.c
@@ -429,9 +429,9 @@ static zend_result php_session_initialize(void)
}
/* Open session handler first */
- if (PS(mod)->s_open(&PS(mod_data), PS(save_path), PS(session_name)) == FAILURE
- /* || PS(mod_data) == NULL */ /* FIXME: open must set valid PS(mod_data) with success */
- ) {
+ const zend_result open_status = PS(mod)->s_open(&PS(mod_data), PS(save_path), PS(session_name));
+ /* NOTE: PS(mod_data) might be null if the session is a custom userland session handler */
+ if (open_status == FAILURE) {
php_session_abort();
if (!EG(exception)) {
php_error_docref(NULL, E_WARNING, "Failed to initialize storage module: %s (path: %s)", PS(mod)->s_name, ZSTR_VAL(PS(save_path)));
@@ -2399,7 +2399,10 @@ PHP_FUNCTION(session_regenerate_id)
zend_string_release_ex(PS(id), false);
PS(id) = NULL;
- if (PS(mod)->s_open(&PS(mod_data), PS(save_path), PS(session_name)) == FAILURE) {
+ /* Open session handler first */
+ const zend_result open_status = PS(mod)->s_open(&PS(mod_data), PS(save_path), PS(session_name));
+ /* NOTE: PS(mod_data) might be null if the session is a custom userland session handler */
+ if (open_status == FAILURE) {
PS(session_status) = php_session_none;
if (!EG(exception)) {
zend_throw_error(NULL, "Failed to open session: %s (path: %s)", PS(mod)->s_name, ZSTR_VAL(PS(save_path)));
@@ -2480,6 +2483,7 @@ PHP_FUNCTION(session_create_id)
}
}
+ /* NOTE: PS(mod_data) might be null if the session is a custom userland session handler */
if (!PS(in_save_handler) && PS(session_status) == php_session_active) {
int limit = 3;
while (limit--) {
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..c87df2ab458 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
@@ -44,6 +44,6 @@ public function validateId(string $id): bool
?>
--EXPECT--
-Error: Session id must be a string
+TypeError: Session id must be of type string, null given
Exception: create_sid failed
bool(true)
diff --git a/ext/session/tests/user_session_module/session_set_save_handler_class_005.phpt b/ext/session/tests/user_session_module/session_set_save_handler_class_005.phpt
index c989cf39d73..cf9460c53df 100644
--- a/ext/session/tests/user_session_module/session_set_save_handler_class_005.phpt
+++ b/ext/session/tests/user_session_module/session_set_save_handler_class_005.phpt
@@ -37,11 +37,13 @@ public function read($id): string|false {
--EXPECTF--
*** Testing session_set_save_handler() : incomplete implementation ***
-Warning: SessionHandler::read(): Parent session handler is not open in %ssession_set_save_handler_class_005.php on line %d
+Warning: SessionHandler::create_sid(): Parent session handler is not open, defaulting to session_create_id() in %s on line %d
-Warning: SessionHandler::close(): Parent session handler is not open in %ssession_set_save_handler_class_005.php on line %d
+Warning: SessionHandler::read(): Parent session handler is not open in %s on line %d
-Warning: session_start(): Failed to read session data: user (%s) in %ssession_set_save_handler_class_005.php on line %d
+Warning: SessionHandler::close(): Parent session handler is not open in %s on line %d
+
+Warning: session_start(): Failed to read session data: user (%s) in %s on line %d
bool(false)
string(0) ""
string(4) "user"
diff --git a/ext/session/tests/user_session_module/session_set_save_handler_class_016.phpt b/ext/session/tests/user_session_module/session_set_save_handler_class_016.phpt
index 242035380ea..61eecc7141d 100644
--- a/ext/session/tests/user_session_module/session_set_save_handler_class_016.phpt
+++ b/ext/session/tests/user_session_module/session_set_save_handler_class_016.phpt
@@ -72,6 +72,8 @@ public function create_sid(): string {
?>
--EXPECTF--
*** Testing session_set_save_handler() function: class with create_sid ***
+
+Warning: SessionHandler::create_sid(): Parent session handler is not open, defaulting to session_create_id() in %s on line %d
string(%d) "%s"
string(4) "user"
array(1) {
diff --git a/ext/session/tests/user_session_module/session_set_save_handler_sid_002.phpt b/ext/session/tests/user_session_module/session_set_save_handler_sid_002.phpt
index 3b9a3f411ef..c9a10de4e44 100644
--- a/ext/session/tests/user_session_module/session_set_save_handler_sid_002.phpt
+++ b/ext/session/tests/user_session_module/session_set_save_handler_sid_002.phpt
@@ -1,5 +1,5 @@
--TEST--
-Test session_set_save_handler() function: create_sid
+session_set_save_handler() with create_sid handler not returning string
--INI--
session.save_path="{TMP}"
--EXTENSIONS--
@@ -7,10 +7,6 @@
--FILE--
<?php
-ob_start();
-
-echo "*** Testing session_set_save_handler() function: create_sid ***\n";
-
class MySession2 implements SessionHandlerInterface, SessionIdInterface {
public $path;
@@ -27,7 +23,7 @@ public function close(): bool {
}
public function read($id): string|false {
- return @file_get_contents($this->path . $id);
+ return file_get_contents($this->path . $id);
}
public function write($id, $data): bool {
@@ -55,26 +51,13 @@ public function create_sid() {
}
session_set_save_handler(new MySession2());
-session_start();
-
-$_SESSION['foo'] = "hello";
-
-var_dump(session_id(), ini_get('session.save_handler'), $_SESSION);
-session_write_close();
-session_unset();
-
-session_start();
-var_dump($_SESSION);
+try {
+ session_start();
+} catch (Throwable $e) {
+ echo $e::class, ': ', $e->getMessage(), "\n";
+}
-session_write_close();
-session_unset();
?>
---EXPECTF--
-*** Testing session_set_save_handler() function: create_sid ***
-
-Fatal error: Uncaught Error: Session id must be a string in %s:%d
-Stack trace:
-#0 %s(%d): session_start()
-#1 {main}
- thrown in %s on line %d
+--EXPECT--
+TypeError: Session id must be of type string, bool given