From 5024c3ed26ddcbbbd4b70795875d9e221e2ccfbf Mon Sep 17 00:00:00 2001 From: Pratik Bhujel Date: Mon, 25 May 2026 15:10:43 +0545 Subject: [PATCH 1/2] Fix GH-22118: Compare equivalent fake closures in FCCs --- Zend/zend_API.h | 47 ++++++++++++++++++- ext/spl/tests/autoloading/gh22118.phpt | 33 +++++++++++++ .../tests/general_functions/gh22118.phpt | 40 ++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 ext/spl/tests/autoloading/gh22118.phpt create mode 100644 ext/standard/tests/general_functions/gh22118.phpt diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 2487c8b632f2..8b5b53b63b5e 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -758,20 +758,63 @@ ZEND_API void zend_fcall_info_argn(zend_fcall_info *fci, uint32_t argc, ...); ZEND_API zend_result zend_fcall_info_call(zend_fcall_info *fci, zend_fcall_info_cache *fcc, zval *retval, zval *args); /* Zend FCC API to store and handle PHP userland functions */ +extern ZEND_API zend_class_entry *zend_ce_closure; +ZEND_API const zend_function *zend_get_closure_method_def(zend_object *obj); + +static zend_always_inline bool zend_fcc_closure_objects_equals(zend_object *closure1, zend_object *closure2) +{ + if (closure1 == closure2) { + return true; + } + if (!closure1 || !closure2) { + return false; + } + if (closure1->ce != zend_ce_closure || closure2->ce != zend_ce_closure) { + return false; + } + + const zend_function *func1 = zend_get_closure_method_def(closure1); + const zend_function *func2 = zend_get_closure_method_def(closure2); + + if (!(func1->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) || + !(func2->common.fn_flags & ZEND_ACC_FAKE_CLOSURE)) { + return false; + } + if (func1 == func2) { + return true; + } + if (func1->type != func2->type || + func1->common.scope != func2->common.scope || + !zend_string_equals(func1->common.function_name, func2->common.function_name)) { + return false; + } + + if (func1->type == ZEND_USER_FUNCTION) { + return func1->op_array.opcodes == func2->op_array.opcodes; + } + + return func1->internal_function.handler == func2->internal_function.handler; +} + static zend_always_inline bool zend_fcc_equals(const zend_fcall_info_cache* a, const zend_fcall_info_cache* b) { + if (a->closure || b->closure) { + return a->object == b->object + && a->calling_scope == b->calling_scope + && a->called_scope == b->called_scope + && zend_fcc_closure_objects_equals(a->closure, b->closure) + ; + } if (UNEXPECTED((a->function_handler->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) && (b->function_handler->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE))) { return a->object == b->object && a->calling_scope == b->calling_scope - && a->closure == b->closure && zend_string_equals(a->function_handler->common.function_name, b->function_handler->common.function_name) ; } return a->function_handler == b->function_handler && a->object == b->object && a->calling_scope == b->calling_scope - && a->closure == b->closure ; } diff --git a/ext/spl/tests/autoloading/gh22118.phpt b/ext/spl/tests/autoloading/gh22118.phpt new file mode 100644 index 000000000000..11af0e47ccc5 --- /dev/null +++ b/ext/spl/tests/autoloading/gh22118.phpt @@ -0,0 +1,33 @@ +--TEST-- +GH-22118: spl_autoload_unregister() unregisters equivalent first-class method callables +--FILE-- +load(...)); + var_dump(spl_autoload_unregister($this->load(...))); + spl_autoload_call('MissingDirect'); + + spl_autoload_register($this->missing(...)); + var_dump(spl_autoload_unregister($this->missing(...))); + spl_autoload_call('MissingTrampoline'); + } +} + +(new AutoloadTest())->run(); +?> +--EXPECT-- +bool(true) +bool(true) diff --git a/ext/standard/tests/general_functions/gh22118.phpt b/ext/standard/tests/general_functions/gh22118.phpt new file mode 100644 index 000000000000..a4e09fa7ecb2 --- /dev/null +++ b/ext/standard/tests/general_functions/gh22118.phpt @@ -0,0 +1,40 @@ +--TEST-- +GH-22118: unregister_tick_function() unregisters equivalent first-class method callables +--FILE-- +direct++; + } + + public function __call(string $name, array $arguments): void + { + $this->trampoline++; + } + + public function run(): void + { + register_tick_function($this->kick(...)); + unregister_tick_function($this->kick(...)); + echo "direct: {$this->direct}\n"; + + register_tick_function($this->missing(...)); + unregister_tick_function($this->missing(...)); + echo "trampoline: {$this->trampoline}\n"; + } +} + +(new TickTest())->run(); +echo "done\n"; +?> +--EXPECT-- +direct: 1 +trampoline: 1 +done From b35da50f5d3f17af6ab49a85cc0763322112331b Mon Sep 17 00:00:00 2001 From: Pratik Bhujel Date: Wed, 3 Jun 2026 21:39:36 +0545 Subject: [PATCH 2/2] Refine fake closure FCC comparison --- Zend/zend_API.c | 46 +++++++++++++++++++ Zend/zend_API.h | 40 +--------------- ext/spl/tests/autoloading/gh22118.phpt | 25 ++++++++++ .../tests/general_functions/gh22118.phpt | 35 ++++++++++++++ 4 files changed, 108 insertions(+), 38 deletions(-) diff --git a/Zend/zend_API.c b/Zend/zend_API.c index 65834adbafff..789b3ac2642e 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -4096,6 +4096,52 @@ ZEND_API zend_string *zend_get_callable_name_ex(const zval *callable, const zend } /* }}} */ +static bool zend_fcc_function_handler_equals(const zend_function *func1, const zend_function *func2) /* {{{ */ +{ + if (func1 == func2) { + return true; + } + + const bool fake_closure1 = (func1->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) != 0; + const bool fake_closure2 = (func2->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) != 0; + + if (!fake_closure1 && !fake_closure2) { + return false; + } + if (((func1->common.fn_flags & ZEND_ACC_CLOSURE) && !fake_closure1) || + ((func2->common.fn_flags & ZEND_ACC_CLOSURE) && !fake_closure2)) { + return false; + } + if (func1->type != func2->type || + func1->common.scope != func2->common.scope || + !zend_string_equals(func1->common.function_name, func2->common.function_name)) { + return false; + } + + if (func1->type == ZEND_USER_FUNCTION) { + return func1->op_array.opcodes == func2->op_array.opcodes; + } + + return func1->internal_function.handler == func2->internal_function.handler; +} +/* }}} */ + +ZEND_API bool zend_fcc_closure_equals_ex(const zend_fcall_info_cache* a, const zend_fcall_info_cache* b) /* {{{ */ +{ + const zend_function *func1 = a->function_handler; + const zend_function *func2 = b->function_handler; + + if (a->closure && a->closure->ce == zend_ce_closure) { + func1 = zend_get_closure_method_def(a->closure); + } + if (b->closure && b->closure->ce == zend_ce_closure) { + func2 = zend_get_closure_method_def(b->closure); + } + + return zend_fcc_function_handler_equals(func1, func2); +} +/* }}} */ + ZEND_API zend_string *zend_get_callable_name(const zval *callable) /* {{{ */ { return zend_get_callable_name_ex(callable, NULL); diff --git a/Zend/zend_API.h b/Zend/zend_API.h index 8b5b53b63b5e..593be26788d7 100644 --- a/Zend/zend_API.h +++ b/Zend/zend_API.h @@ -758,43 +758,7 @@ ZEND_API void zend_fcall_info_argn(zend_fcall_info *fci, uint32_t argc, ...); ZEND_API zend_result zend_fcall_info_call(zend_fcall_info *fci, zend_fcall_info_cache *fcc, zval *retval, zval *args); /* Zend FCC API to store and handle PHP userland functions */ -extern ZEND_API zend_class_entry *zend_ce_closure; -ZEND_API const zend_function *zend_get_closure_method_def(zend_object *obj); - -static zend_always_inline bool zend_fcc_closure_objects_equals(zend_object *closure1, zend_object *closure2) -{ - if (closure1 == closure2) { - return true; - } - if (!closure1 || !closure2) { - return false; - } - if (closure1->ce != zend_ce_closure || closure2->ce != zend_ce_closure) { - return false; - } - - const zend_function *func1 = zend_get_closure_method_def(closure1); - const zend_function *func2 = zend_get_closure_method_def(closure2); - - if (!(func1->common.fn_flags & ZEND_ACC_FAKE_CLOSURE) || - !(func2->common.fn_flags & ZEND_ACC_FAKE_CLOSURE)) { - return false; - } - if (func1 == func2) { - return true; - } - if (func1->type != func2->type || - func1->common.scope != func2->common.scope || - !zend_string_equals(func1->common.function_name, func2->common.function_name)) { - return false; - } - - if (func1->type == ZEND_USER_FUNCTION) { - return func1->op_array.opcodes == func2->op_array.opcodes; - } - - return func1->internal_function.handler == func2->internal_function.handler; -} +ZEND_API bool zend_fcc_closure_equals_ex(const zend_fcall_info_cache* a, const zend_fcall_info_cache* b); static zend_always_inline bool zend_fcc_equals(const zend_fcall_info_cache* a, const zend_fcall_info_cache* b) { @@ -802,7 +766,7 @@ static zend_always_inline bool zend_fcc_equals(const zend_fcall_info_cache* a, c return a->object == b->object && a->calling_scope == b->calling_scope && a->called_scope == b->called_scope - && zend_fcc_closure_objects_equals(a->closure, b->closure) + && (a->closure == b->closure || zend_fcc_closure_equals_ex(a, b)) ; } if (UNEXPECTED((a->function_handler->common.fn_flags & ZEND_ACC_CALL_VIA_TRAMPOLINE) && diff --git a/ext/spl/tests/autoloading/gh22118.phpt b/ext/spl/tests/autoloading/gh22118.phpt index 11af0e47ccc5..635d876c2740 100644 --- a/ext/spl/tests/autoloading/gh22118.phpt +++ b/ext/spl/tests/autoloading/gh22118.phpt @@ -2,6 +2,11 @@ GH-22118: spl_autoload_unregister() unregisters equivalent first-class method callables --FILE-- load(...)); @@ -23,11 +33,26 @@ class AutoloadTest spl_autoload_register($this->missing(...)); var_dump(spl_autoload_unregister($this->missing(...))); spl_autoload_call('MissingTrampoline'); + + spl_autoload_register($this->load(...)); + var_dump(spl_autoload_unregister([$this, 'load'])); + spl_autoload_call('MissingArray'); + + spl_autoload_register($this(...)); + var_dump(spl_autoload_unregister($this)); + spl_autoload_call('MissingInvokable'); } } +spl_autoload_register(autoload_string(...)); +var_dump(spl_autoload_unregister('autoload_string')); +spl_autoload_call('MissingString'); + (new AutoloadTest())->run(); ?> --EXPECT-- bool(true) bool(true) +bool(true) +bool(true) +bool(true) diff --git a/ext/standard/tests/general_functions/gh22118.phpt b/ext/standard/tests/general_functions/gh22118.phpt index a4e09fa7ecb2..32c1a0fa1dec 100644 --- a/ext/standard/tests/general_functions/gh22118.phpt +++ b/ext/standard/tests/general_functions/gh22118.phpt @@ -4,16 +4,36 @@ GH-22118: unregister_tick_function() unregisters equivalent first-class method c direct++; } + public function arrayCallable(): void + { + $this->array++; + } + + public function __invoke(): void + { + $this->invoke++; + } + public function __call(string $name, array $arguments): void { $this->trampoline++; @@ -28,13 +48,28 @@ class TickTest register_tick_function($this->missing(...)); unregister_tick_function($this->missing(...)); echo "trampoline: {$this->trampoline}\n"; + + register_tick_function($this->arrayCallable(...)); + unregister_tick_function([$this, 'arrayCallable']); + echo "array: {$this->array}\n"; + + register_tick_function($this(...)); + unregister_tick_function($this); + echo "invoke: {$this->invoke}\n"; } } +register_tick_function(tick_string(...)); +unregister_tick_function('tick_string'); +echo "string: {$string}\n"; + (new TickTest())->run(); echo "done\n"; ?> --EXPECT-- +string: 1 direct: 1 trampoline: 1 +array: 1 +invoke: 1 done