Skip to content

Commit 02a264f

Browse files
committed
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
1 parent 5dd3909 commit 02a264f

5 files changed

Lines changed: 111 additions & 0 deletions

File tree

Zend/zend_API.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,7 +849,21 @@ static zend_always_inline void zend_call_known_fcc(
849849
memcpy(func, fcc->function_handler, sizeof(zend_function));
850850
zend_string_addref(func->op_array.function_name);
851851
}
852+
zend_object *pinned_object = fcc->object;
853+
zend_object *pinned_closure = fcc->closure;
854+
if (pinned_object) {
855+
GC_ADDREF(pinned_object);
856+
}
857+
if (pinned_closure) {
858+
GC_ADDREF(pinned_closure);
859+
}
852860
zend_call_known_function(func, fcc->object, fcc->called_scope, retval_ptr, param_count, params, named_params);
861+
if (pinned_object) {
862+
OBJ_RELEASE(pinned_object);
863+
}
864+
if (pinned_closure) {
865+
OBJ_RELEASE(pinned_closure);
866+
}
853867
}
854868

855869
/* Call the provided zend_function instance method on an object. */

ext/reflection/php_reflection.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2074,6 +2074,7 @@ ZEND_METHOD(ReflectionFunction, invoke)
20742074
fcc.function_handler = fptr;
20752075
fcc.called_scope = NULL;
20762076
fcc.object = NULL;
2077+
fcc.closure = NULL;
20772078

20782079
if (!Z_ISUNDEF(intern->obj)) {
20792080
Z_OBJ_HT(intern->obj)->get_closure(
@@ -2113,6 +2114,7 @@ ZEND_METHOD(ReflectionFunction, invokeArgs)
21132114
fcc.function_handler = fptr;
21142115
fcc.called_scope = NULL;
21152116
fcc.object = NULL;
2117+
fcc.closure = NULL;
21162118

21172119
if (!Z_ISUNDEF(intern->obj)) {
21182120
Z_OBJ_HT(intern->obj)->get_closure(

ext/spl/php_spl.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,21 @@ static zend_class_entry *spl_perform_autoload(zend_string *class_name, zend_stri
439439

440440
zval param;
441441
ZVAL_STR(&param, class_name);
442+
zend_object *pinned_obj = alfi->obj;
443+
zend_object *pinned_closure = alfi->closure;
444+
if (pinned_obj) {
445+
GC_ADDREF(pinned_obj);
446+
}
447+
if (pinned_closure) {
448+
GC_ADDREF(pinned_closure);
449+
}
442450
zend_call_known_function(func, alfi->obj, alfi->ce, NULL, 1, &param, NULL);
451+
if (pinned_obj) {
452+
OBJ_RELEASE(pinned_obj);
453+
}
454+
if (pinned_closure) {
455+
OBJ_RELEASE(pinned_closure);
456+
}
443457
if (EG(exception)) {
444458
break;
445459
}

ext/spl/tests/gh22060.phpt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
--TEST--
2+
GH-22060 (Class autoloader $this freed via spl_autoload_unregister during dispatch)
3+
--FILE--
4+
<?php
5+
6+
class Loader {
7+
public string $data = "loader-data";
8+
9+
public function load(string $class): void {
10+
spl_autoload_unregister([$this, 'load']);
11+
echo $this->data, "\n";
12+
}
13+
}
14+
15+
$obj = new Loader();
16+
spl_autoload_register([$obj, 'load']);
17+
unset($obj);
18+
19+
try {
20+
new NonExistentClass42();
21+
} catch (\Throwable $e) {
22+
echo $e::class, ": ", $e->getMessage(), "\n";
23+
}
24+
?>
25+
--EXPECT--
26+
loader-data
27+
Error: Class "NonExistentClass42" not found

ext/sqlite3/tests/gh22122.phpt

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
--TEST--
2+
GH-22122 (Use-after-free in SQLite3 authorizer when callback releases the authorizer)
3+
--EXTENSIONS--
4+
sqlite3
5+
--FILE--
6+
<?php
7+
$db = new SQLite3(':memory:');
8+
9+
/* Method receiver - the reported UAF shape. */
10+
class Auth {
11+
public string $state = "alive";
12+
13+
public function authorize(int $action, ...$args): int {
14+
global $db;
15+
$db->setAuthorizer(null);
16+
echo "method: ", $this->state, "\n";
17+
return SQLite3::OK;
18+
}
19+
}
20+
$auth = new Auth();
21+
$db->setAuthorizer([$auth, 'authorize']);
22+
unset($auth);
23+
$db->exec('SELECT 1');
24+
25+
/* Closure receiver - exercises the saved_closure release path. */
26+
$capture = "closure-alive";
27+
$closure = function (int $action, ...$args) use (&$capture, $db): int {
28+
$db->setAuthorizer(null);
29+
echo "closure: ", $capture, "\n";
30+
return SQLite3::OK;
31+
};
32+
$db->setAuthorizer($closure);
33+
unset($closure);
34+
$db->exec('SELECT 2');
35+
36+
/* Confirm the authorizer was actually disabled by the callback (setAuthorizer null). */
37+
$db->exec('SELECT 3');
38+
echo "post-disable query ok\n";
39+
40+
/* Throwing callback should propagate the user's exception without redundant warnings. */
41+
$db->setAuthorizer(function () { throw new RuntimeException("from authorizer"); });
42+
try {
43+
@$db->exec('SELECT 4');
44+
} catch (RuntimeException $e) {
45+
echo "throw: ", $e->getMessage(), "\n";
46+
}
47+
echo "done\n";
48+
?>
49+
--EXPECT--
50+
method: alive
51+
closure: closure-alive
52+
post-disable query ok
53+
throw: from authorizer
54+
done

0 commit comments

Comments
 (0)