Skip to content

Commit 1581e11

Browse files
committed
Fix GH-21362: ReflectionMethod::invoke/invokeArgs() rejects different Closure instances
ReflectionMethod::invokeArgs() (and invoke()) 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 f99ca63 commit 1581e11

File tree

2 files changed

+52
-1
lines changed

2 files changed

+52
-1
lines changed

ext/reflection/php_reflection.c

Lines changed: 14 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,17 @@ 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(), each closure instance has its own signature.
3448+
* Verify that the passed object is the same closure this method was reflected from. */
3449+
if (obj_ce == zend_ce_closure && !Z_ISUNDEF(intern->obj)
3450+
&& Z_OBJ_P(object) != Z_OBJ(intern->obj)) {
3451+
if (!variadic) {
3452+
efree(params);
3453+
}
3454+
_DO_THROW("Given object is not an instance of the class this method was declared in");
3455+
RETURN_THROWS();
3456+
}
34443457
}
34453458
/* Copy the zend_function when calling via handler (e.g. Closure::__invoke()) */
34463459
callback = _copy_function(mptr);

ext/reflection/tests/gh21362.phpt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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+
--EXPECT--
36+
c1: foo=FOO, bar=BAR
37+
Given object is not an instance of the class this method was declared in
38+
Given object is not an instance of the class this method was declared in

0 commit comments

Comments
 (0)