Commit e46e742f45e for php

commit e46e742f45e66cd4c6379160e3d378062a2e2b4f
Author: Roman Ondráček <mail@romanondracek.cz>
Date:   Mon Oct 5 21:24:44 2026 +0200

    Don't use static closure cache when called_scope diverges

    The stateless closure cache in ZEND_DECLARE_LAMBDA_FUNCTION stored a single
    closure per opline. Since the op_array and its cache slot is shared by all
    subclasses, the closure created for the first called scope was returned for
    every other called scope, breaking static::, new static() and
    get_called_class().

    Store the called scope alongside the cached closure and only reuse the closure
    for a matching called scope.

    Fixes GH-23911
    Closes GH-24143

    Signed-off-by: Roman Ondráček <mail@romanondracek.cz>

diff --git a/NEWS b/NEWS
index 16da23a9c0c..e7906d480d4 100644
--- a/NEWS
+++ b/NEWS
@@ -21,6 +21,8 @@ PHP                                                                        NEWS
     (Jorg Sowa)
   . Fixed bug GH-25061 ($x = $x - 1 on null is optimized to PRE_DEC).
     (Lazizbek Ergashev)
+  . Fixed bug GH-23911 (Don't use static closure cache when called_scope
+    diverges). (Roman Ondráček)

 - CLI:
   . Fixed bug GH-24697 (PHP_HTTP_PARSER_STRICT build option doesn't work).
diff --git a/Zend/Optimizer/compact_literals.c b/Zend/Optimizer/compact_literals.c
index a73b46c0e33..6ed7e870da6 100644
--- a/Zend/Optimizer/compact_literals.c
+++ b/Zend/Optimizer/compact_literals.c
@@ -757,12 +757,17 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
 					}
 					break;
 				case ZEND_CALLABLE_CONVERT:
-				case ZEND_DECLARE_LAMBDA_FUNCTION:
 					if (opline->extended_value != (uint32_t)-1) {
 						opline->extended_value = cache_size;
 						cache_size += sizeof(void *);
 					}
 					break;
+				case ZEND_DECLARE_LAMBDA_FUNCTION:
+					if (opline->extended_value != (uint32_t)-1) {
+						opline->extended_value = cache_size;
+						cache_size += 2 * sizeof(void *);
+					}
+					break;
 				case ZEND_CALLABLE_CONVERT_PARTIAL:
 					opline->extended_value = cache_size | (opline->extended_value & ZEND_PARTIAL_FLAGS);
 					cache_size += 2 * sizeof(void *);
diff --git a/Zend/tests/closures/gh23911.phpt b/Zend/tests/closures/gh23911.phpt
new file mode 100644
index 00000000000..d522a9e8cbb
--- /dev/null
+++ b/Zend/tests/closures/gh23911.phpt
@@ -0,0 +1,86 @@
+--TEST--
+GH-23911: Cached static closure keeps static:: of the first called class
+--CREDITS--
+Sander Muller (SanderMuller)
+--FILE--
+<?php
+
+class A {
+    public static function name(): string {
+        return (static fn () => static::class)();
+    }
+
+    public static function create(): static {
+        return (static fn () => new static())();
+    }
+
+    public function calledClass(): string {
+        return (static function () {
+            return get_called_class();
+        })();
+    }
+}
+class B extends A {}
+class C extends A {}
+
+var_dump(B::name(), C::name(), B::name(), A::name());
+var_dump(B::create()::class, C::create()::class);
+var_dump((new B)->calledClass(), (new C)->calledClass());
+
+function f() {
+    return static fn () => 42;
+}
+var_dump(f() === f());
+var_dump(B::name(...) !== C::name(...));
+
+trait T {
+    public static function names(): array {
+        return (static fn () => [self::class, static::class])();
+    }
+}
+class U1 { use T; }
+class U2 { use T; }
+class U3 extends U1 {}
+var_dump(U1::names(), U2::names(), U3::names());
+
+class N {
+    public static function nested(): string {
+        return (static fn () => (static fn () => static::class)())();
+    }
+}
+class N2 extends N {}
+var_dump(N::nested(), N2::nested(), N::nested());
+
+?>
+--EXPECT--
+string(1) "B"
+string(1) "C"
+string(1) "B"
+string(1) "A"
+string(1) "B"
+string(1) "C"
+string(1) "B"
+string(1) "C"
+bool(true)
+bool(true)
+array(2) {
+  [0]=>
+  string(2) "U1"
+  [1]=>
+  string(2) "U1"
+}
+array(2) {
+  [0]=>
+  string(2) "U2"
+  [1]=>
+  string(2) "U2"
+}
+array(2) {
+  [0]=>
+  string(2) "U1"
+  [1]=>
+  string(2) "U3"
+}
+string(1) "N"
+string(2) "N2"
+string(1) "N"
diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c
index 8b5f0251701..4de326b3677 100644
--- a/Zend/zend_compile.c
+++ b/Zend/zend_compile.c
@@ -9295,8 +9295,9 @@ static zend_op_array *zend_compile_func_decl_ex(
 		 && declaring_op_array->last) {
 			zend_op *declare_lambda_op = &declaring_op_array->opcodes[declaring_op_array->last - 1];
 			if (declare_lambda_op->opcode == ZEND_DECLARE_LAMBDA_FUNCTION) {
+				/* Cache slot pair of called scope and closure. */
 				declare_lambda_op->extended_value = declaring_op_array->cache_size;
-				declaring_op_array->cache_size += sizeof(void *);
+				declaring_op_array->cache_size += 2 * sizeof(void *);
 			}
 		}
 	}
diff --git a/Zend/zend_vm_def.h b/Zend/zend_vm_def.h
index f456e33948f..519587110cd 100644
--- a/Zend/zend_vm_def.h
+++ b/Zend/zend_vm_def.h
@@ -8424,8 +8424,16 @@ ZEND_VM_HANDLER(142, ZEND_DECLARE_LAMBDA_FUNCTION, UNUSED, NUM, NUM|CACHE_SLOT)
 	zend_class_entry *called_scope;
 	uint32_t cache_slot = opline->extended_value;

+	if (Z_TYPE(EX(This)) == IS_OBJECT) {
+		called_scope = Z_OBJCE(EX(This));
+	} else {
+		called_scope = Z_CE(EX(This));
+	}
+
 	if (cache_slot != (uint32_t)-1) {
-		zend_object *closure = CACHED_PTR(cache_slot);
+		/* The closure is bound to the called scope, so it may only be reused
+		 * for the same called scope. */
+		zend_object *closure = CACHED_POLYMORPHIC_PTR(cache_slot, called_scope);
 		if (closure) {
 			ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
 			ZEND_VM_NEXT_OPCODE();
@@ -8433,25 +8441,21 @@ ZEND_VM_HANDLER(142, ZEND_DECLARE_LAMBDA_FUNCTION, UNUSED, NUM, NUM|CACHE_SLOT)
 	}

 	func = (zend_function *) EX(func)->op_array.dynamic_func_defs[opline->op2.num];
-	if (Z_TYPE(EX(This)) == IS_OBJECT) {
-		called_scope = Z_OBJCE(EX(This));
-		if (UNEXPECTED((func->common.fn_flags & ZEND_ACC_STATIC) ||
-				(EX(func)->common.fn_flags & ZEND_ACC_STATIC))) {
-			object = NULL;
-		} else {
-			object = Z_OBJ(EX(This));
-		}
+	if (Z_TYPE(EX(This)) == IS_OBJECT
+	 && EXPECTED(!(func->common.fn_flags & ZEND_ACC_STATIC))
+	 && EXPECTED(!(EX(func)->common.fn_flags & ZEND_ACC_STATIC))) {
+		object = Z_OBJ(EX(This));
 	} else {
-		called_scope = Z_CE(EX(This));
 		object = NULL;
 	}
 	SAVE_OPLINE();
 	zend_create_closure(EX_VAR(opline->result.var), func,
 		EX(func)->op_array.scope, called_scope, object);
-	if (cache_slot != (uint32_t)-1) {
+	if (cache_slot != (uint32_t)-1
+	 && !CACHED_PTR(cache_slot + sizeof(void *))) {
 		zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var));
 		GC_ADDREF(closure);
-		CACHE_PTR(cache_slot, closure);
+		CACHE_POLYMORPHIC_PTR(cache_slot, called_scope, closure);
 		zend_stack_push(&EG(lambda_cache), &closure);
 	}
 	ZEND_VM_NEXT_OPCODE();
diff --git a/Zend/zend_vm_execute.h b/Zend/zend_vm_execute.h
index 66a57ed4598..7b6f2bcd974 100644
Binary files a/Zend/zend_vm_execute.h and b/Zend/zend_vm_execute.h differ