Skip to content

Commit 53e31d5

Browse files
authored
Fix GH-21362: ReflectionMethod::invoke() allows different Closures (#21366)
ReflectionMethod::invoke() (and invokeArgs()) for Closure::__invoke() incorrectly accepted any Closure object, not just the one the ReflectionMethod was created from. This happened because all Closures share a single zend_ce_closure class entry, so the instanceof_function() check always passed. Fix: store the original Closure object in intern->obj during ReflectionMethod construction, then compare object identity in reflection_method_invoke() to reject different Closure instances. Closes GH-21362
1 parent eedbffe commit 53e31d5

File tree

3 files changed

+75
-1
lines changed

3 files changed

+75
-1
lines changed

NEWS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ PHP NEWS
9090
. Added ReflectionConstant::inNamespace(). (Khaled Alam)
9191
. Added ReflectionProperty::isReadable() and ReflectionProperty::isWritable().
9292
(ilutov)
93+
. Fixed bug GH-21362 (ReflectionMethod::invoke/invokeArgs() did not verify
94+
Closure instance identity for Closure::__invoke()). (Ilia Alshanetsky)
9395

9496
- Session:
9597
. Fixed bug 71162 (updateTimestamp never called when session data is empty).

ext/reflection/php_reflection.c

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3306,7 +3306,9 @@ static void instantiate_reflection_method(INTERNAL_FUNCTION_PARAMETERS, bool is_
33063306
&& memcmp(lcname, ZEND_INVOKE_FUNC_NAME, sizeof(ZEND_INVOKE_FUNC_NAME)-1) == 0
33073307
&& (mptr = zend_get_closure_invoke_method(orig_obj)) != NULL)
33083308
{
3309-
/* do nothing, mptr already set */
3309+
/* Store the original closure object so we can validate it in invoke/invokeArgs.
3310+
* Each closure has a unique __invoke signature, so we must reject different closures. */
3311+
ZVAL_OBJ_COPY(&intern->obj, orig_obj);
33103312
} else if ((mptr = zend_hash_str_find_ptr(&ce->function_table, lcname, method_name_len)) == NULL) {
33113313
efree(lcname);
33123314
zend_throw_exception_ex(reflection_exception_ptr, 0,
@@ -3441,6 +3443,23 @@ static void reflection_method_invoke(INTERNAL_FUNCTION_PARAMETERS, int variadic)
34413443
_DO_THROW("Given object is not an instance of the class this method was declared in");
34423444
RETURN_THROWS();
34433445
}
3446+
3447+
/* For Closure::__invoke(), closures from different source locations have
3448+
* different signatures, so we must reject those. However, closures created
3449+
* from the same source (e.g. in a loop) share the same op_array and should
3450+
* be allowed. Compare the underlying function pointer via op_array. */
3451+
if (obj_ce == zend_ce_closure && !Z_ISUNDEF(intern->obj)
3452+
&& Z_OBJ_P(object) != Z_OBJ(intern->obj)) {
3453+
const zend_function *orig_func = zend_get_closure_method_def(Z_OBJ(intern->obj));
3454+
const zend_function *given_func = zend_get_closure_method_def(Z_OBJ_P(object));
3455+
if (orig_func->op_array.opcodes != given_func->op_array.opcodes) {
3456+
if (!variadic) {
3457+
efree(params);
3458+
}
3459+
_DO_THROW("Given Closure is not the same as the reflected Closure");
3460+
RETURN_THROWS();
3461+
}
3462+
}
34443463
}
34453464
/* Copy the zend_function when calling via handler (e.g. Closure::__invoke()) */
34463465
callback = _copy_function(mptr);

ext/reflection/tests/gh21362.phpt

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
--TEST--
2+
GH-21362 (ReflectionMethod::invokeArgs() for Closure::__invoke() accepts objects from different Closures)
3+
--FILE--
4+
<?php
5+
6+
$c1 = function ($foo, $bar) {
7+
echo "c1: foo={$foo}, bar={$bar}\n";
8+
};
9+
10+
$c2 = function ($bar, $foo) {
11+
echo "c2: foo={$foo}, bar={$bar}\n";
12+
};
13+
14+
$m = new ReflectionMethod($c1, '__invoke');
15+
16+
// invokeArgs with the correct Closure should work
17+
$m->invokeArgs($c1, ['foo' => 'FOO', 'bar' => 'BAR']);
18+
19+
// invokeArgs with a different Closure should throw
20+
try {
21+
$m->invokeArgs($c2, ['foo' => 'FOO', 'bar' => 'BAR']);
22+
echo "No exception thrown\n";
23+
} catch (ReflectionException $e) {
24+
echo $e->getMessage() . "\n";
25+
}
26+
27+
// invoke with a different Closure should also throw
28+
try {
29+
$m->invoke($c2, 'FOO', 'BAR');
30+
echo "No exception thrown\n";
31+
} catch (ReflectionException $e) {
32+
echo $e->getMessage() . "\n";
33+
}
34+
35+
// Closures from the same source (e.g. loop) share the same op_array
36+
// and should be allowed to invoke each other's ReflectionMethod
37+
$closures = [];
38+
for ($i = 0; $i < 3; $i++) {
39+
$closures[] = function () use ($i) { return $i; };
40+
}
41+
42+
$m2 = new ReflectionMethod($closures[0], '__invoke');
43+
foreach ($closures as $closure) {
44+
var_dump($m2->invoke($closure));
45+
}
46+
?>
47+
--EXPECT--
48+
c1: foo=FOO, bar=BAR
49+
Given Closure is not the same as the reflected Closure
50+
Given Closure is not the same as the reflected Closure
51+
int(0)
52+
int(1)
53+
int(2)

0 commit comments

Comments
 (0)