Fix ReflectionMethod::invoke() for first class callables#21389
Fix ReflectionMethod::invoke() for first class callables#21389iliaal wants to merge 3 commits intophp:masterfrom
Conversation
The closure identity check added in phpGH-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.
|
On first sight this seems right. One other thing that I notice is that |
|
I think it should be fine, but another pair of eyes never hurts :-) Thanks! |
I don't think that's possible for the reason you've mentioned. We're keeping the |
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.
|
@iliaal I've taken the liberty to push into your branch, since this was easier than explaining the change. AFAIK the only way for a Closure to be an internal function is by using first class callables (“fake closure”) and this is the check we should make, since for userland fake Closures we should also compare scope and function name. The “proper closure” check can then remain the OPcode structure comparison. |
|
This still looks wrong: <?php
class Broken {
function foo(){}
}
$func = (new Broken)->foo(...);
$rm = new ReflectionMethod(new Broken, 'foo');
$rm->invoke($func); |
|
@ndossche After Tim's change it looks to ok as far as I can tell, the code throws "ReflectionException: Given object is not an instance of the class this method was declared in". As far as I can tell that is correct behavior. intern->obj is only set when the ReflectionMethod is constructed for Closure::__invoke (the ce == zend_ce_closure guard in instantiate_reflection_method). For new ReflectionMethod(new Broken, 'foo'), intern->obj remains UNDEF, so the closure validation block is skipped entirely. The instanceof_function check then correctly rejects the Closure as not being an instance of Broken. |
Or to put it another way: That has always been rejected, as per https://3v4l.org/4UiEW. The original issue was the check not rejecting enough. |
Thanks for the help on the patch BTW :) |
Summary
Follow-up to GH-21366. The closure identity check accessed
op_array.opcodesunconditionally, but internal closures created via first-class callable syntax (e.g.var_dump(...)) useinternal_function, notop_array. This is undefined behavior and will crash under ASAN/debug builds.func->type == ZEND_USER_FUNCTIONbefore accessingop_array.opcodeszend_functionpointer directlyReported by @ndossche in #21366 (comment)