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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user