From 437e0563573df923540ae3d1c02f9da7ba0fd62a Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Mon, 1 Jun 2026 07:17:03 -0400 Subject: [PATCH] Pin $this in zend_call_function 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. Hold a reference across the call. Fixes GH-22060 Fixes GH-22122 --- Zend/zend_execute_API.c | 9 ++++++ ext/pdo_sqlite/tests/gh22122.phpt | 40 ++++++++++++++++++++++++++ ext/spl/tests/autoloading/gh22060.phpt | 27 +++++++++++++++++ ext/sqlite3/tests/gh22122.phpt | 40 ++++++++++++++++++++++++++ tests/output/gh20352.phpt | 3 -- 5 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 ext/pdo_sqlite/tests/gh22122.phpt create mode 100644 ext/spl/tests/autoloading/gh22060.phpt create mode 100644 ext/sqlite3/tests/gh22122.phpt diff --git a/Zend/zend_execute_API.c b/Zend/zend_execute_API.c index 71e0c56a51c8..5535858ed655 100644 --- a/Zend/zend_execute_API.c +++ b/Zend/zend_execute_API.c @@ -1006,6 +1006,11 @@ zend_result zend_call_function(zend_fcall_info *fci, zend_fcall_info_cache *fci_ fci_cache->function_handler = NULL; } + zend_object *pinned_this = (call_info & ZEND_CALL_HAS_THIS) ? fci_cache->object : NULL; + if (pinned_this) { + GC_ADDREF(pinned_this); + } + const zend_class_entry *orig_fake_scope = EG(fake_scope); EG(fake_scope) = NULL; if (func->type == ZEND_USER_FUNCTION) { @@ -1073,6 +1078,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 000000000000..ae15d7490ed2 --- /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-- +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 000000000000..50dff5d71b11 --- /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-- +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/sqlite3/tests/gh22122.phpt b/ext/sqlite3/tests/gh22122.phpt new file mode 100644 index 000000000000..1df1c3bc0e28 --- /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-- +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 3074add99d36..16be0b920e80 100644 --- a/tests/output/gh20352.phpt +++ b/tests/output/gh20352.phpt @@ -21,7 +21,4 @@ ob_start(new Test, 1); 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