1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

Fix ReflectionMethod::invoke() for first class callables (#21389)

* Fix ReflectionMethod::invoke() crash with internal closures

The closure identity check added in GH-21366 accessed op_array.opcodes
unconditionally, but internal closures (e.g. var_dump(...)) use
internal_function, not op_array. This caused undefined behavior when
comparing closures created via first-class callable syntax on internal
functions.

Check the function type first: compare op_array.opcodes for user
closures, compare the function pointer directly for internal closures.

* Fix internal closure comparison and expand test coverage

The previous comparison (orig_func == given_func) could never match for
internal closures since zend_get_closure_method_def() returns a pointer
to each closure's embedded copy. Compare function_name and scope instead.

Also handle the mixed user/internal type case explicitly.

Add tests for: userland first-class callables, cloned internal closures,
and cross-type (user vs internal) closure rejection.

* php_reflection: Simplify the Closure::__invoke() check

---------

Co-authored-by: Tim Düsterhus <tim@bastelstu.be>
This commit is contained in:
Ilia Alshanetsky
2026-03-14 20:00:04 -04:00
committed by GitHub
parent 7e16d4e5f8
commit 4561e92fa1
2 changed files with 79 additions and 4 deletions

View File

@@ -3441,14 +3441,27 @@ static void reflection_method_invoke(INTERNAL_FUNCTION_PARAMETERS, int variadic)
}
/* For Closure::__invoke(), closures from different source locations have
* different signatures, so we must reject those. However, closures created
* from the same source (e.g. in a loop) share the same op_array and should
* be allowed. Compare the underlying function pointer via op_array. */
* different signatures, so we must reject those. */
if (obj_ce == zend_ce_closure && !Z_ISUNDEF(intern->obj)
&& Z_OBJ_P(object) != Z_OBJ(intern->obj)) {
const zend_function *orig_func = zend_get_closure_method_def(Z_OBJ(intern->obj));
const zend_function *given_func = zend_get_closure_method_def(Z_OBJ_P(object));
if (orig_func->op_array.opcodes != given_func->op_array.opcodes) {
bool same_closure = false;
/* Check if they are either both fake closures or they both are not. */
if ((orig_func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) == (given_func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE)) {
if (orig_func->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) {
/* For fake closures, scope and name must match. */
same_closure = orig_func->common.scope == given_func->common.scope
&& orig_func->common.function_name == given_func->common.function_name;
} else {
/* Otherwise the opcode structure must be identical. */
ZEND_ASSERT(orig_func->type == ZEND_USER_FUNCTION);
same_closure = orig_func->op_array.opcodes == given_func->op_array.opcodes;
}
}
if (!same_closure) {
if (!variadic) {
efree(params);
}

View File

@@ -43,6 +43,60 @@ $m2 = new ReflectionMethod($closures[0], '__invoke');
foreach ($closures as $closure) {
var_dump($m2->invoke($closure));
}
// First-class callable of a userland function
function my_func($x) { return "my_func: $x"; }
function other_func($x) { return "other_func: $x"; }
$mf = my_func(...);
$mf2 = my_func(...);
$of = other_func(...);
$m3 = new ReflectionMethod($mf, '__invoke');
var_dump($m3->invoke($mf, 'test'));
var_dump($m3->invoke($mf2, 'test'));
try {
$m3->invoke($of, 'test');
echo "No exception thrown\n";
} catch (ReflectionException $e) {
echo $e->getMessage() . "\n";
}
// Internal closures (first-class callable syntax) should also be validated
$vd = var_dump(...);
$pr = print_r(...);
$m4 = new ReflectionMethod($vd, '__invoke');
$m4->invoke($vd, 'internal closure OK');
// Cloned internal closure is a different object but same function - should work
$vd2 = clone $vd;
$m4->invoke($vd2, 'cloned internal closure OK');
// Different internal closure should throw
try {
$m4->invoke($pr, 'should not print');
echo "No exception thrown\n";
} catch (ReflectionException $e) {
echo $e->getMessage() . "\n";
}
// Cross-type: userland Closure to internal Closure's invoke should throw
try {
$m4->invoke($c1, 'should not print');
echo "No exception thrown\n";
} catch (ReflectionException $e) {
echo $e->getMessage() . "\n";
}
// Cross-type: internal Closure to userland Closure's invoke should throw
try {
$m->invoke($vd, 'should not print');
echo "No exception thrown\n";
} catch (ReflectionException $e) {
echo $e->getMessage() . "\n";
}
?>
--EXPECT--
c1: foo=FOO, bar=BAR
@@ -51,3 +105,11 @@ Given Closure is not the same as the reflected Closure
int(0)
int(1)
int(2)
string(13) "my_func: test"
string(13) "my_func: test"
Given Closure is not the same as the reflected Closure
string(19) "internal closure OK"
string(26) "cloned internal closure OK"
Given Closure is not the same as the reflected Closure
Given Closure is not the same as the reflected Closure
Given Closure is not the same as the reflected Closure