diff --git a/ext/reflection/php_reflection.c b/ext/reflection/php_reflection.c index 2692e1928068d..26d4341c1acdf 100644 --- a/ext/reflection/php_reflection.c +++ b/ext/reflection/php_reflection.c @@ -3445,14 +3445,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); } diff --git a/ext/reflection/tests/gh21362.phpt b/ext/reflection/tests/gh21362.phpt index ce7840b494ed8..1f5619ce779d0 100644 --- a/ext/reflection/tests/gh21362.phpt +++ b/ext/reflection/tests/gh21362.phpt @@ -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