From a85e9ba35a6c2ebf81b0208152c8be3bd449d755 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_known_fcc zend_call_known_fcc() ran the callback with fcc->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 --- NEWS | 4 +++ Zend/zend_API.h | 7 +++++ ext/pdo_sqlite/tests/gh22122.phpt | 40 ++++++++++++++++++++++++++ ext/spl/tests/autoloading/gh22060.phpt | 27 +++++++++++++++++ ext/sqlite3/tests/gh22122.phpt | 40 ++++++++++++++++++++++++++ 5 files changed, 118 insertions(+) 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/NEWS b/NEWS index 3c4d6993f46c..e1ad376f3355 100644 --- a/NEWS +++ b/NEWS @@ -21,6 +21,8 @@ PHP NEWS . Deprecate specifying a nullable return type for __debugInfo(). (timwolla) . Fixed bug GH-22142 (Assertion failure in zendi_try_get_long() on IS_UNDEF). (David Carlier) + . Fixed GH-22060 (Use-after-free when an autoloader unregisters itself + during dispatch). (iliaal) - BCMath: . Added NUL-byte validation to BCMath functions. (jorgsowa) @@ -200,6 +202,8 @@ PHP NEWS - Sqlite3: . Fix NUL byte truncation in sqlite3 TEXT column handling. (ndossche) + . Fixed GH-22122 (Use-after-free in the authorizer callback when it releases + the authorizer). (iliaal) - Standard: . Fixed bug GH-19926 (reset internal pointer earlier while splicing array diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 2487c8b632f2..3ad993b278e8 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -860,7 +860,14 @@ 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; + if (pinned_object) { + GC_ADDREF(pinned_object); + } zend_call_known_function(func, fcc->object, fcc->called_scope, retval_ptr, param_count, params, named_params); + if (pinned_object) { + OBJ_RELEASE(pinned_object); + } } /* Call the provided zend_function instance method on an object. */ 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