Commit 934d4ff95fa for php.net
commit 934d4ff95fad9c74eca7f6e203284644a04908ff
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Sep 21 17:32:32 2026 -0400
Pin $this in zend_call_function (#22151)
zend_call_function() ran the callback with fci_cache->object as $this
without holding a reference, so a callback that released the last
reference to its own receiver (an autoloader unregistering itself, a
SQLite3 authorizer calling setAuthorizer(null)) freed $this while its
frame was still executing. Take the reference where the receiver is
selected rather than after argument setup, so the error handlers
reachable from the deprecation notice and the by-reference warning
cannot free it first, and release it on every early exit.
Fixes GH-22060
Fixes GH-22122
diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c
index ada9a3ac75b..ac1683bb368 100644
--- a/Zend/zend_execute_API.c
+++ b/Zend/zend_execute_API.c
@@ -823,6 +823,7 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
zend_function *func;
uint32_t call_info;
void *object_or_called_scope;
+ zend_object *pinned_this = NULL;
ZVAL_UNDEF(fci->retval);
@@ -866,12 +867,17 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
} else {
object_or_called_scope = fci_cache->object;
call_info = ZEND_CALL_TOP_FUNCTION | ZEND_CALL_DYNAMIC | ZEND_CALL_HAS_THIS;
+ pinned_this = fci_cache->object;
+ GC_ADDREF(pinned_this);
}
if (UNEXPECTED(func->common.fn_flags & ZEND_ACC_DEPRECATED)) {
zend_deprecated_function(func);
if (UNEXPECTED(EG(exception))) {
+ if (pinned_this) {
+ OBJ_RELEASE(pinned_this);
+ }
return SUCCESS;
}
}
@@ -880,6 +886,9 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
if (UNEXPECTED(zend_call_stack_overflowed(EG(stack_limit)))) {
zend_call_stack_size_error();
zend_release_fcall_info_cache(fci_cache);
+ if (pinned_this) {
+ OBJ_RELEASE(pinned_this);
+ }
return SUCCESS;
}
#endif
@@ -917,6 +926,9 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
}
zend_vm_stack_free_call_frame(call);
zend_release_fcall_info_cache(fci_cache);
+ if (pinned_this) {
+ OBJ_RELEASE(pinned_this);
+ }
return SUCCESS;
}
}
@@ -1010,6 +1022,9 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
if (zend_handle_undef_args(call) == FAILURE) {
zend_vm_stack_free_args(call);
zend_vm_stack_free_call_frame(call);
+ if (pinned_this) {
+ OBJ_RELEASE(pinned_this);
+ }
return SUCCESS;
}
}
@@ -1096,6 +1111,10 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_
}
EG(fake_scope) = orig_fake_scope;
+ if (pinned_this) {
+ OBJ_RELEASE(pinned_this);
+ }
+
zend_vm_stack_free_call_frame(call);
if (UNEXPECTED(EG(exception))) {
diff --git a/ext/pdo_sqlite/tests/gh22122.phpt b/ext/pdo_sqlite/tests/gh22122.phpt
new file mode 100644
index 00000000000..ae15d7490ed
--- /dev/null
+++ b/ext/pdo_sqlite/tests/gh22122.phpt
@@ -0,0 +1,40 @@
+--TEST--
+GH-22122 (Use-after-free in Pdo\Sqlite authorizer when callback releases the authorizer)
+--EXTENSIONS--
+pdo_sqlite
+--FILE--
+<?php
+$db = Pdo\Sqlite::connect('sqlite::memory:');
+
+class Auth {
+ public string $state = "alive";
+
+ public function authorize(int $action, ...$args): int {
+ global $db;
+ $db->setAuthorizer(null);
+ echo "method: ", $this->state, "\n";
+ return Pdo\Sqlite::OK;
+ }
+}
+$auth = new Auth();
+$db->setAuthorizer([$auth, 'authorize']);
+unset($auth);
+$db->exec('SELECT 1');
+
+$capture = "closure-alive";
+$closure = function (int $action, ...$args) use (&$capture, $db): int {
+ $db->setAuthorizer(null);
+ echo "closure: ", $capture, "\n";
+ return Pdo\Sqlite::OK;
+};
+$db->setAuthorizer($closure);
+unset($closure);
+$db->exec('SELECT 2');
+
+$db->exec('SELECT 3');
+echo "post-disable query ok\n";
+?>
+--EXPECT--
+method: alive
+closure: closure-alive
+post-disable query ok
diff --git a/ext/spl/tests/autoloading/gh22060.phpt b/ext/spl/tests/autoloading/gh22060.phpt
new file mode 100644
index 00000000000..50dff5d71b1
--- /dev/null
+++ b/ext/spl/tests/autoloading/gh22060.phpt
@@ -0,0 +1,27 @@
+--TEST--
+GH-22060 (Class autoloader $this freed via spl_autoload_unregister during dispatch)
+--FILE--
+<?php
+
+class Loader {
+ public string $data = "loader-data";
+
+ public function load(string $class): void {
+ spl_autoload_unregister([$this, 'load']);
+ echo $this->data, "\n";
+ }
+}
+
+$obj = new Loader();
+spl_autoload_register([$obj, 'load']);
+unset($obj);
+
+try {
+ new NonExistentClass42();
+} catch (\Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+loader-data
+Error: Class "NonExistentClass42" not found
diff --git a/ext/spl/tests/autoloading/gh22060_deprecated.phpt b/ext/spl/tests/autoloading/gh22060_deprecated.phpt
new file mode 100644
index 00000000000..2132b33c241
--- /dev/null
+++ b/ext/spl/tests/autoloading/gh22060_deprecated.phpt
@@ -0,0 +1,36 @@
+--TEST--
+GH-22060 (Autoloader $this freed by the error handler of its own deprecation notice)
+--FILE--
+<?php
+
+class Loader {
+ public string $data = "loader-data";
+
+ #[\Deprecated]
+ public function load(string $class): void {
+ echo $this->data, "\n";
+ }
+}
+
+$obj = new Loader();
+spl_autoload_register([$obj, 'load']);
+unset($obj);
+
+set_error_handler(function (int $no, string $str): bool {
+ echo $str, "\n";
+ foreach (spl_autoload_functions() as $loader) {
+ spl_autoload_unregister($loader);
+ }
+ return true;
+});
+
+try {
+ new NonExistentClass42();
+} catch (\Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+?>
+--EXPECT--
+Method Loader::load() is deprecated
+loader-data
+Error: Class "NonExistentClass42" not found
diff --git a/ext/sqlite3/tests/gh22122.phpt b/ext/sqlite3/tests/gh22122.phpt
new file mode 100644
index 00000000000..1df1c3bc0e2
--- /dev/null
+++ b/ext/sqlite3/tests/gh22122.phpt
@@ -0,0 +1,40 @@
+--TEST--
+GH-22122 (Use-after-free in SQLite3 authorizer when callback releases the authorizer)
+--EXTENSIONS--
+sqlite3
+--FILE--
+<?php
+$db = new SQLite3(':memory:');
+
+class Auth {
+ public string $state = "alive";
+
+ public function authorize(int $action, ...$args): int {
+ global $db;
+ $db->setAuthorizer(null);
+ echo "method: ", $this->state, "\n";
+ return SQLite3::OK;
+ }
+}
+$auth = new Auth();
+$db->setAuthorizer([$auth, 'authorize']);
+unset($auth);
+$db->exec('SELECT 1');
+
+$capture = "closure-alive";
+$closure = function (int $action, ...$args) use (&$capture, $db): int {
+ $db->setAuthorizer(null);
+ echo "closure: ", $capture, "\n";
+ return SQLite3::OK;
+};
+$db->setAuthorizer($closure);
+unset($closure);
+$db->exec('SELECT 2');
+
+$db->exec('SELECT 3');
+echo "post-disable query ok\n";
+?>
+--EXPECT--
+method: alive
+closure: closure-alive
+post-disable query ok
diff --git a/tests/output/gh20352.phpt b/tests/output/gh20352.phpt
index 3074add99d3..16be0b920e8 100644
--- a/tests/output/gh20352.phpt
+++ b/tests/output/gh20352.phpt
@@ -21,7 +21,4 @@ public function __invoke($x) {
echo "trigger bug";
?>
--EXPECTF--
-%r(Notice: ob_start\(\): Failed to create buffer in [^\r\n]+ on line \d+\r?\n(\r?\n)?)+%r
-Notice: ob_start(): Failed to create buffer in %s on line %d
-
Fatal error: ob_start(): Cannot use output buffering in output buffering display handlers in %s on line %d