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) {
+}