Skip to content

Commit d6ac8ec

Browse files
committed
Fix phpGH-22060 and phpGH-22122: pin object/closure in callback dispatch
zend_call_known_fcc and the SPL autoload loop both forward the callback's bound object and closure into the call frame without addref'ing, on the convention that the caller pins them for the call duration. The convention breaks whenever the pointer is borrowed from persistent storage that the callback can mutate: a method-bound autoloader self-unregistering via spl_autoload_unregister, or a SQLite3 authorizer callback calling setAuthorizer(null) both release the only refcount holder mid-call and the method body dereferences freed memory. GC_ADDREF object and closure before the call and OBJ_RELEASE after, in both dispatch sites. Initialize fcc.closure in ReflectionFunction::invoke and invokeArgs alongside the other fields, since the new pin reads it. Fixes phpGH-22060 Fixes phpGH-22122
1 parent 5dd3909 commit d6ac8ec

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)