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