Commit 57e3ec738a4 for php
commit 57e3ec738a4d35c5970f17b4eda8a2deb93bc24d
Author: Gina Peter Banyard <girgias@php.net>
Date: Tue Nov 10 00:57:59 2026 +0000
Zend: follow-up for GH-22145. Fix zend_fcc_closure_equals_ex() implementation (#24007)
This is effectively a follow-up to #22145 to properly fix #22118 with internal functions.
It turns out that to prevent memory leaks for fake closures of internal functions, the zif_handler is changed to `zend_closure_internal_handler` and the true zif_handler is stored in the `orig_internal_handler` field of the zend_closure struct.
So `return func1->internal_function.handler == func2->internal_function.handler;` could never return true for internal functions.
We need to keep `zend_fcc_closure_equals_ex()` as the ABI is frozen, but it probably makes sense to clean this all up in master.
diff --git a/Zend/zend_API.c b/Zend/zend_API.c
index e43b3abf9ae..ab5b2e1b190 100644
--- a/Zend/zend_API.c
+++ b/Zend/zend_API.c
@@ -4138,51 +4138,15 @@ 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) /* {{{ */
+ZEND_API bool zend_fcc_closure_equals_ex(const zend_fcall_info_cache* a, const zend_fcall_info_cache* b)
{
- 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;
+ const zend_function *fn_a = a->function_handler;
+ const zend_object *closure_zobj_a = a->closure && a->closure->ce == zend_ce_closure ? a->closure : NULL;
+ const zend_function *fn_b = b->function_handler;
+ const zend_object *closure_zobj_b = b->closure && b->closure->ce == zend_ce_closure ? b->closure : NULL;
- 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);
+ return zend_fn_closure_equals_ex(fn_a, closure_zobj_a, fn_b, closure_zobj_b);
}
-/* }}} */
ZEND_API bool zend_is_callable_at_frame(
const zval *callable, zend_object *object, const zend_execute_data *frame,
diff --git a/Zend/zend_closures.c b/Zend/zend_closures.c
index 828c66b021e..3d729cbd522 100644
--- a/Zend/zend_closures.c
+++ b/Zend/zend_closures.c
@@ -535,6 +535,61 @@ static int zend_closure_compare(zval *o1, zval *o2) /* {{{ */
}
/* }}} */
+ZEND_API bool zend_fn_closure_equals_ex(
+ const zend_function *fn_a, const zend_object *closure_zobj_a,
+ const zend_function *fn_b, const zend_object *closure_zobj_b
+) {
+ const zend_closure *closure_a = NULL;
+ bool is_closure_a_fake = false;
+
+ const zend_closure *closure_b = NULL;
+ bool is_closure_b_fake = false;
+
+ if (closure_zobj_a) {
+ closure_a = (const zend_closure *) closure_zobj_a;
+ is_closure_a_fake = zend_closure_is_fake(closure_a);
+ fn_a = &closure_a->func;
+ }
+ if (closure_zobj_b) {
+ closure_b = (const zend_closure *) closure_zobj_b;
+ is_closure_b_fake = zend_closure_is_fake(closure_b);
+ fn_b = &closure_b->func;
+ }
+
+ // TODO is this even possible as closure allocates a new zend_function pointer on creation?
+ if (fn_a == fn_b) {
+ return true;
+ }
+
+ if (!is_closure_a_fake && !is_closure_b_fake) {
+ return false;
+ }
+ if ((closure_a && !is_closure_a_fake) || (closure_b && !is_closure_b_fake)) {
+ return false;
+ }
+ if (fn_a->type != fn_b->type ||
+ fn_a->common.scope != fn_b->common.scope ||
+ !zend_string_equals(fn_a->common.function_name, fn_b->common.function_name)) {
+ return false;
+ }
+
+ if (fn_a->type == ZEND_USER_FUNCTION) {
+ return fn_a->op_array.opcodes == fn_b->op_array.opcodes;
+ } else {
+ zif_handler zif_a = fn_a->internal_function.handler;
+ zif_handler zif_b = fn_b->internal_function.handler;
+
+ if (closure_a) {
+ zif_a = closure_a->orig_internal_handler;
+ }
+ if (closure_b) {
+ zif_b = closure_b->orig_internal_handler;
+ }
+
+ return zif_a == zif_b;
+ }
+}
+
ZEND_API zend_function *zend_get_closure_invoke_method(zend_object *object) /* {{{ */
{
zend_closure *closure = (zend_closure *)object;
diff --git a/Zend/zend_closures.h b/Zend/zend_closures.h
index c421c100833..1d3fc89c1df 100644
--- a/Zend/zend_closures.h
+++ b/Zend/zend_closures.h
@@ -41,6 +41,11 @@ ZEND_API zend_function *zend_get_closure_invoke_method(zend_object *obj);
ZEND_API const zend_function *zend_get_closure_method_def(zend_object *obj);
ZEND_API zend_object* zend_get_closure_this_ptr(zval *obj);
+ZEND_API bool zend_fn_closure_equals_ex(
+ const zend_function *fn_a, const zend_object *closure_zobj_a,
+ const zend_function *fn_b, const zend_object *closure_zobj_b
+);
+
END_EXTERN_C()
#endif
diff --git a/ext/spl/tests/autoloading/gh22118_internal_fake_closures.phpt b/ext/spl/tests/autoloading/gh22118_internal_fake_closures.phpt
new file mode 100644
index 00000000000..085e0fd6d6b
--- /dev/null
+++ b/ext/spl/tests/autoloading/gh22118_internal_fake_closures.phpt
@@ -0,0 +1,27 @@
+--TEST--
+GH-22118: spl_autoload_unregister() should also unregister fake closure representing internal function
+--FILE--
+<?php
+
+spl_autoload_register(strlen(...));
+var_dump(spl_autoload_functions());
+var_dump(spl_autoload_unregister('strlen'));
+var_dump(spl_autoload_functions());
+
+?>
+--EXPECT--
+array(1) {
+ [0]=>
+ object(Closure)#1 (2) {
+ ["function"]=>
+ string(6) "strlen"
+ ["parameter"]=>
+ array(1) {
+ ["$string"]=>
+ string(10) "<required>"
+ }
+ }
+}
+bool(true)
+array(0) {
+}