Skip to content

Fix ReflectionMethod::invoke() with internal closures#21389

Open
iliaal wants to merge 1 commit intophp:masterfrom
iliaal:fix/gh-21362-internal-closure-invoke
Open

Fix ReflectionMethod::invoke() with internal closures#21389
iliaal wants to merge 1 commit intophp:masterfrom
iliaal:fix/gh-21362-internal-closure-invoke

Conversation

@iliaal
Copy link
Contributor

@iliaal iliaal commented Mar 8, 2026

Summary

Follow-up to GH-21366. The closure identity check accessed op_array.opcodes unconditionally, but internal closures created via first-class callable syntax (e.g. var_dump(...)) use internal_function, not op_array. This is undefined behavior and will crash under ASAN/debug builds.

  • Check func->type == ZEND_USER_FUNCTION before accessing op_array.opcodes
  • For internal closures, compare the zend_function pointer directly
  • Added test coverage for internal closure validation

Reported by @ndossche in #21366 (comment)

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.
@ndossche
Copy link
Member

ndossche commented Mar 8, 2026

On first sight this seems right. One other thing that I notice is that op_array.opcodes isn't necessarily a unique address unless the ZEND_ACC_IMMUTABLE flag is set. However, the current code is likely fine because you hold onto the object so that op_array.opcodes will not alias to another function.
I'll have another look tomorrow.

@ndossche ndossche requested a review from TimWolla March 8, 2026 22:20
@iliaal
Copy link
Contributor Author

iliaal commented Mar 8, 2026

I think it should be fine, but another pair of eyes never hurts :-) Thanks!

@iluuu1994
Copy link
Member

However, the current code is likely fine because you hold onto the object so that op_array.opcodes will not alias to another function.

I don't think that's possible for the reason you've mentioned. We're keeping the op_array alive through zend_op_array->refcount. zend_closure_free_storage() then calls destroy_op_array().

Comment on lines +47 to +49
// Internal closures (first-class callable syntax) should also be validated
$vd = var_dump(...);
$pr = print_r(...);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we, for completeness, also add a test for:

  1. First-class-callables of a userland-defined function.
  2. Trying to pass a userland Closure to invoke() created for a internal Closure and vice versa.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants