Fix GH-21362: ReflectionMethod::invoke/invokeArgs() rejects different Closure instances#21366
Conversation
|
I did not test, but based on reading the diff I think this is not quite right, yet. Instead of comparing the objects, we should compare the inner I expect this to successfully dump 0, 1, 2. All the closure instances are "of the same class". If that already works: Great, please just add that test case. |
…ent 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 phpGH-21362
1581e11 to
277d301
Compare
|
Good catch -- the object identity check was too strict. Updated the comparison to use Added the loop test case you suggested. All 515 reflection tests pass. P.S. Thanks for the feedback, my php internals are a bit rusty 😆 |
…nvoke() Use a Closure-specific message instead of the generic "not an instance of the class" phrasing, since all Closures share the same class entry.
Change "not compatible with" to "not the same as" since closures with different source locations aren't about compatibility.
DanielEScherzer
left a comment
There was a problem hiding this comment.
Thanks, this looks good now. Please add to NEWS/UPGRADING and I'll merge
|
Added NEWS entry, don't think UPGRADED needs updating |
|
Squashed and merged, thanks! |
| && 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) { |
There was a problem hiding this comment.
This code is wrong for internal closures because they don't have an op_array.
Intuitively, the code also seems a bit convoluted, it feels wrong to have to store the object.
Anyway, here is something that shouldn't be allowed most likely, and even if it were allowed then the access to op_array is still wrong (only allowed for userland):
<?php
$closure = var_dump(...);
$closure2 = print_r(...);
$rm = new ReflectionMethod($closure, '__invoke');
$rm->invoke($closure2, 1);
There was a problem hiding this comment.
It is a valid point, I made a follow-up PR #21389 to address this issue (along with corresponding test).
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.
Summary
Fixes #21362
ReflectionMethod::invokeArgs()(andinvoke()) forClosure::__invoke()incorrectly accepted any Closure object, not just the one theReflectionMethodwas created from.Root cause
All Closure objects share a single
zend_ce_closureclass entry. Theinstanceof_function()check inreflection_method_invoke()compares class entries, so it always seeszend_ce_closure == zend_ce_closureand passes -- it cannot distinguish between different Closure instances.Fix
Two changes to
ext/reflection/php_reflection.c:instantiate_reflection_method(): Store the original Closure object inintern->objviaZVAL_OBJ_COPY()when reflectingClosure::__invoke(). Previously this was a no-op (/* do nothing, mptr already set */).reflection_method_invoke(): After the existinginstanceof_functioncheck passes for Closures, add an identity check (Z_OBJ_P(object) != Z_OBJ(intern->obj)) to reject different Closure instances.Test
ext/reflection/tests/gh21362.phptcovers:invokeArgs()with the correct Closure (should work)invokeArgs()with a different Closure (should throwReflectionException)invoke()with a different Closure (should throwReflectionException)