Skip to content

Commit c194f64

Browse files
committed
Fix GH-23911: Cached static closure keeps static:: of the first called class
The stateless closure cache in ZEND_DECLARE_LAMBDA_FUNCTION stored a single closure per opline. Since the declaring op_array 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. The slot is only filled once, as replacing the cached closure on a mismatch would grow EG(lambda_cache) without bounds. Fixes GH-23911 Signed-off-by: Roman Ondráček <mail@romanondracek.cz>
1 parent 464c95a commit c194f64

5 files changed

Lines changed: 107 additions & 38 deletions

File tree

‎Zend/Optimizer/compact_literals.c‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,12 +757,17 @@ void zend_optimizer_compact_literals(zend_op_array *op_array, zend_optimizer_ctx
757757
}
758758
break;
759759
case ZEND_CALLABLE_CONVERT:
760-
case ZEND_DECLARE_LAMBDA_FUNCTION:
761760
if (opline->extended_value != (uint32_t)-1) {
762761
opline->extended_value = cache_size;
763762
cache_size += sizeof(void *);
764763
}
765764
break;
765+
case ZEND_DECLARE_LAMBDA_FUNCTION:
766+
if (opline->extended_value != (uint32_t)-1) {
767+
opline->extended_value = cache_size;
768+
cache_size += 2 * sizeof(void *);
769+
}
770+
break;
766771
case ZEND_CALLABLE_CONVERT_PARTIAL:
767772
opline->extended_value = cache_size | (opline->extended_value & ZEND_PARTIAL_FLAGS);
768773
cache_size += 2 * sizeof(void *);

‎Zend/tests/closures/gh23911.phpt‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
--TEST--
2+
GH-23911: Cached static closure keeps static:: of the first called class
3+
--FILE--
4+
<?php
5+
6+
class A {
7+
public static function name(): string {
8+
return (static fn () => static::class)();
9+
}
10+
11+
public static function create(): static {
12+
return (static fn () => new static())();
13+
}
14+
15+
public function calledClass(): string {
16+
return (static function () {
17+
return get_called_class();
18+
})();
19+
}
20+
}
21+
class B extends A {}
22+
class C extends A {}
23+
24+
var_dump(B::name(), C::name(), B::name(), A::name());
25+
var_dump(B::create()::class, C::create()::class);
26+
var_dump((new B)->calledClass(), (new C)->calledClass());
27+
28+
function f() {
29+
return static fn () => 42;
30+
}
31+
var_dump(f() === f());
32+
var_dump(B::name(...) !== C::name(...));
33+
34+
?>
35+
--EXPECT--
36+
string(1) "B"
37+
string(1) "C"
38+
string(1) "B"
39+
string(1) "A"
40+
string(1) "B"
41+
string(1) "C"
42+
string(1) "B"
43+
string(1) "C"
44+
bool(true)
45+
bool(true)

‎Zend/zend_compile.c‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9273,8 +9273,9 @@ static zend_op_array *zend_compile_func_decl_ex(
92739273
&& declaring_op_array->last) {
92749274
zend_op *declare_lambda_op = &declaring_op_array->opcodes[declaring_op_array->last - 1];
92759275
if (declare_lambda_op->opcode == ZEND_DECLARE_LAMBDA_FUNCTION) {
9276+
/* Cache slot pair of called scope and closure. */
92769277
declare_lambda_op->extended_value = declaring_op_array->cache_size;
9277-
declaring_op_array->cache_size += sizeof(void *);
9278+
declaring_op_array->cache_size += 2 * sizeof(void *);
92789279
}
92799280
}
92809281
}

‎Zend/zend_vm_def.h‎

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8420,34 +8420,40 @@ ZEND_VM_HANDLER(142, ZEND_DECLARE_LAMBDA_FUNCTION, UNUSED, NUM, NUM|CACHE_SLOT)
84208420
zend_object *object;
84218421
zend_class_entry *called_scope;
84228422

8423+
if (Z_TYPE(EX(This)) == IS_OBJECT) {
8424+
called_scope = Z_OBJCE(EX(This));
8425+
} else {
8426+
called_scope = Z_CE(EX(This));
8427+
}
8428+
84238429
if (opline->extended_value != (uint32_t)-1) {
8424-
zend_object *closure = CACHED_PTR(opline->extended_value);
8430+
/* The closure is bound to the called scope, so it may only be reused
8431+
* for the same called scope. */
8432+
zend_object *closure = CACHED_POLYMORPHIC_PTR(opline->extended_value, called_scope);
84258433
if (closure) {
84268434
ZVAL_OBJ_COPY(EX_VAR(opline->result.var), closure);
84278435
ZEND_VM_NEXT_OPCODE();
84288436
}
84298437
}
84308438

84318439
func = (zend_function *) EX(func)->op_array.dynamic_func_defs[opline->op2.num];
8432-
if (Z_TYPE(EX(This)) == IS_OBJECT) {
8433-
called_scope = Z_OBJCE(EX(This));
8434-
if (UNEXPECTED((func->common.fn_flags & ZEND_ACC_STATIC) ||
8435-
(EX(func)->common.fn_flags & ZEND_ACC_STATIC))) {
8436-
object = NULL;
8437-
} else {
8438-
object = Z_OBJ(EX(This));
8439-
}
8440+
if (Z_TYPE(EX(This)) == IS_OBJECT
8441+
&& EXPECTED(!(func->common.fn_flags & ZEND_ACC_STATIC))
8442+
&& EXPECTED(!(EX(func)->common.fn_flags & ZEND_ACC_STATIC))) {
8443+
object = Z_OBJ(EX(This));
84408444
} else {
8441-
called_scope = Z_CE(EX(This));
84428445
object = NULL;
84438446
}
84448447
SAVE_OPLINE();
84458448
zend_create_closure(EX_VAR(opline->result.var), func,
84468449
EX(func)->op_array.scope, called_scope, object);
8447-
if (opline->extended_value != (uint32_t)-1) {
8450+
/* Only fill an empty cache slot. Replacing a cached closure on a called
8451+
* scope mismatch would grow EG(lambda_cache) without bounds. */
8452+
if (opline->extended_value != (uint32_t)-1
8453+
&& !CACHED_PTR(opline->extended_value + sizeof(void *))) {
84488454
zend_object *closure = Z_OBJ_P(EX_VAR(opline->result.var));
84498455
GC_ADDREF(closure);
8450-
CACHE_PTR(opline->extended_value, closure);
8456+
CACHE_POLYMORPHIC_PTR(opline->extended_value, called_scope, closure);
84518457
zend_stack_push(&EG(lambda_cache), &closure);
84528458
}
84538459
ZEND_VM_NEXT_OPCODE();

‎Zend/zend_vm_execute.h‎

Lines changed: 36 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)