From 02a264f7af517e048d9f2913dabe6bfe6cfcada6 Mon Sep 17 00:00:00 2001 From: Ilia Alshanetsky Date: Sun, 24 May 2026 21:08:56 -0400 Subject: [PATCH] Fix GH-22060 and GH-22122: pin object/closure in callback dispatch Pin object and closure across zend_call_known_fcc and spl_perform_autoload so a callback that releases the borrowed FCC (autoloader self-unregister, SQLite3 setAuthorizer(null)) doesn't free $this mid-call. Initialize fcc.closure in ReflectionFunction::invoke/invokeArgs since the pin reads it. Fixes GH-22060 Fixes GH-22122 --- Zend/zend_API.h | 14 +++++++++ ext/reflection/php_reflection.c | 2 ++ ext/spl/php_spl.c | 14 +++++++++ ext/spl/tests/gh22060.phpt | 27 +++++++++++++++++ ext/sqlite3/tests/gh22122.phpt | 54 +++++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+) create mode 100644 ext/spl/tests/gh22060.phpt create mode 100644 ext/sqlite3/tests/gh22122.phpt diff --git a/Zend/zend_API.h b/Zend/zend_API.h index d91da91bf299..d7644e02c97c 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -849,7 +849,21 @@ static zend_always_inline void zend_call_known_fcc( memcpy(func, fcc->function_handler, sizeof(zend_function)); zend_string_addref(func->op_array.function_name); } + zend_object *pinned_object = fcc->object; + zend_object *pinned_closure = fcc->closure; + if (pinned_object) { + GC_ADDREF(pinned_object); + } + if (pinned_closure) { + GC_ADDREF(pinned_closure); + } zend_call_known_function(func, fcc->object, fcc->called_scope, retval_ptr, param_count, params, named_params); + if (pinned_object) { + OBJ_RELEASE(pinned_object); + } + if (pinned_closure) { + OBJ_RELEASE(pinned_closure); + } } /* Call the provided zend_function instance method on an object. */ diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index e718b1815fb3..9758e4ab31a1 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -2074,6 +2074,7 @@ ZEND_METHOD(ReflectionFunction, invoke) fcc.function_handler = fptr; fcc.called_scope = NULL; fcc.object = NULL; + fcc.closure = NULL; if (!Z_ISUNDEF(intern->obj)) { Z_OBJ_HT(intern->obj)->get_closure( @@ -2113,6 +2114,7 @@ ZEND_METHOD(ReflectionFunction, invokeArgs) fcc.function_handler = fptr; fcc.called_scope = NULL; fcc.object = NULL; + fcc.closure = NULL; if (!Z_ISUNDEF(intern->obj)) { Z_OBJ_HT(intern->obj)->get_closure( diff --git a/ext/spl/php_spl.c b/ext/spl/php_spl.c index 78315e9880b4..b5ead10217d3 100644 --- a/ext/spl/php_spl.c +++ b/ext/spl/php_spl.c @@ -439,7 +439,21 @@ static zend_class_entry *spl_perform_autoload(zend_string *class_name, zend_stri zval param; ZVAL_STR(¶m, class_name); + zend_object *pinned_obj = alfi->obj; + zend_object *pinned_closure = alfi->closure; + if (pinned_obj) { + GC_ADDREF(pinned_obj); + } + if (pinned_closure) { + GC_ADDREF(pinned_closure); + } zend_call_known_function(func, alfi->obj, alfi->ce, NULL, 1, ¶m, NULL); + if (pinned_obj) { + OBJ_RELEASE(pinned_obj); + } + if (pinned_closure) { + OBJ_RELEASE(pinned_closure); + } if (EG(exception)) { break; } diff --git a/ext/spl/tests/gh22060.phpt b/ext/spl/tests/gh22060.phpt new file mode 100644 index 000000000000..50dff5d71b11 --- /dev/null +++ b/ext/spl/tests/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..d2359cc22fb7 --- /dev/null +++ b/ext/sqlite3/tests/gh22122.phpt @@ -0,0 +1,54 @@ +--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'); + +/* Closure receiver - exercises the saved_closure release path. */ +$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'); + +/* Confirm the authorizer was actually disabled by the callback (setAuthorizer null). */ +$db->exec('SELECT 3'); +echo "post-disable query ok\n"; + +/* Throwing callback should propagate the user's exception without redundant warnings. */ +$db->setAuthorizer(function () { throw new RuntimeException("from authorizer"); }); +try { + @$db->exec('SELECT 4'); +} catch (RuntimeException $e) { + echo "throw: ", $e->getMessage(), "\n"; +} +echo "done\n"; +?> +--EXPECT-- +method: alive +closure: closure-alive +post-disable query ok +throw: from authorizer +done