Skip to content

Commit 6270c83

Browse files
lisachenkoclaude
andcommitted
Fix GH-23628: Tracing JIT reads undefined property slots of lazy proxies
A lazy proxy keeps its own property slots IS_UNDEF|IS_PROP_LAZY even after it has been initialized, and the object handlers forward every property access to the real instance. The tracing JIT was not aware of this in two places: 1. When the recorded trace contained a FETCH_OBJ_R/IS/W on a known property whose slot was IS_UNDEF, the known-offset fast path was still compiled. For a lazy proxy this path never succeeds, and it deoptimized on every execution. Use the generic code path (that falls back to the object handlers for undefined slots) when the slot was IS_UNDEF at recording time. This also covers uninitialized and unset properties. 2. During deoptimization of a failed result type guard after FETCH_OBJ_IS, an IS_UNDEF slot was turned into NULL, assuming an undefined property. For a slot flagged IS_PROP_LAZY the fetch has to be forwarded to the real instance instead, so re-execute the opline in the VM, the same way it is already done for FETCH_OBJ_R. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECekMqERF8jnqBxo1cXe3V
1 parent b9c64e2 commit 6270c83

5 files changed

Lines changed: 138 additions & 2 deletions

File tree

NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ PHP NEWS
77
registrations are freed while still reachable from the cycle collector.
88
(Ilia Alshanetsky)
99

10+
- Opcache:
11+
. Fixed bug GH-23628 (Tracing JIT reads undefined property slots of lazy
12+
proxy objects instead of forwarding to the real instance). (lisachenko)
13+
1014

1115
24 Sep 2026, PHP 8.5.11
1216

ext/opcache/jit/zend_jit_ir.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14247,6 +14247,18 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit,
1424714247
ZEND_ASSERT(Z_TYPE_P(member) == IS_STRING && Z_STRVAL_P(member)[0] != '\0');
1424814248
prop_info = zend_get_known_property_info(op_array, ce, Z_STR_P(member), on_this, op_array->filename);
1424914249

14250+
if (JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE && prop_type == IS_UNDEF) {
14251+
/* The property slot was IS_UNDEF when the trace was recorded. This
14252+
* happens for lazy objects (a lazy proxy keeps its own slots undefined
14253+
* and forwards the accesses to the real instance), as well as for
14254+
* uninitialized or unset properties. The fast path with a known
14255+
* property offset would deoptimize on every execution, so use the
14256+
* generic code path that falls back to the object handlers for
14257+
* undefined slots instead. */
14258+
prop_info = NULL;
14259+
trace_ce = NULL;
14260+
}
14261+
1425014262
if (on_this) {
1425114263
zend_jit_addr this_addr = ZEND_ADDR_MEM_ZVAL(ZREG_FP, offsetof(zend_execute_data, This));
1425214264
obj_ref = jit_Z_PTR(jit, this_addr);

ext/opcache/jit/zend_jit_trace.c

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8713,10 +8713,18 @@ int ZEND_FASTCALL zend_jit_trace_exit(uint32_t exit_num, zend_jit_registers_buf
87138713
const zend_op *op = t->exit_info[exit_num].opline;
87148714
ZEND_ASSERT(op);
87158715
op--;
8716-
if (op->opcode == ZEND_FETCH_DIM_IS || op->opcode == ZEND_FETCH_OBJ_IS) {
8716+
if (op->opcode == ZEND_FETCH_DIM_IS) {
8717+
ZVAL_NULL(EX_VAR_NUM(i));
8718+
} else if (op->opcode == ZEND_FETCH_OBJ_IS
8719+
&& !(Z_PROP_FLAG_P(val) & IS_PROP_LAZY)) {
87178720
ZVAL_NULL(EX_VAR_NUM(i));
87188721
} else {
8719-
ZEND_ASSERT(op->opcode == ZEND_FETCH_DIM_R || op->opcode == ZEND_FETCH_LIST_R || op->opcode == ZEND_FETCH_OBJ_R || op->opcode == ZEND_FETCH_DIM_FUNC_ARG || op->opcode == ZEND_FETCH_OBJ_FUNC_ARG);
8722+
/* Undefined array index or property that has to emit a warning,
8723+
* or a property slot of a lazy object that has to be forwarded
8724+
* to the real instance by the read_property handler (a lazy
8725+
* proxy keeps its own slots IS_UNDEF|IS_PROP_LAZY even after
8726+
* initialization): re-execute the opline in the VM. */
8727+
ZEND_ASSERT(op->opcode == ZEND_FETCH_DIM_R || op->opcode == ZEND_FETCH_LIST_R || op->opcode == ZEND_FETCH_OBJ_R || op->opcode == ZEND_FETCH_OBJ_IS || op->opcode == ZEND_FETCH_DIM_FUNC_ARG || op->opcode == ZEND_FETCH_OBJ_FUNC_ARG);
87208728
repeat_last_opline = 1;
87218729
}
87228730
} else {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
--TEST--
2+
GH-23628 001: Tracing JIT reads undefined property slots of a lazy proxy
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.file_update_protection=0
7+
opcache.jit=tracing
8+
opcache.jit_buffer_size=32M
9+
opcache.jit_hot_loop=16
10+
--EXTENSIONS--
11+
opcache
12+
--FILE--
13+
<?php
14+
final class Table {
15+
protected array $map = ['start' => ['next' => 1]];
16+
public int $count = 0;
17+
public function parse(int $n): int {
18+
$ok = 0;
19+
for ($i = 0; $i < $n; $i++) {
20+
if (isset($this->map['start']['next'])) {
21+
$ok++;
22+
} else {
23+
throw new RuntimeException('isset false at ' . $i);
24+
}
25+
}
26+
return $ok;
27+
}
28+
public function coalesce(int $n): int {
29+
$sum = 0;
30+
for ($i = 0; $i < $n; $i++) {
31+
$sum += $this->map['start']['next'] ?? 100;
32+
}
33+
return $sum;
34+
}
35+
public function read(int $n): int {
36+
$sum = 0;
37+
for ($i = 0; $i < $n; $i++) {
38+
$sum += $this->map['start']['next'];
39+
}
40+
return $sum;
41+
}
42+
public function write(int $n): int {
43+
for ($i = 0; $i < $n; $i++) {
44+
$this->map['start']['next'] = $i;
45+
$this->count++;
46+
}
47+
return $this->map['start']['next'];
48+
}
49+
}
50+
51+
$reflector = new ReflectionClass(Table::class);
52+
53+
$proxy = $reflector->newLazyProxy(fn () => new Table());
54+
var_dump($proxy->parse(100));
55+
$proxy = $reflector->newLazyProxy(fn () => new Table());
56+
var_dump($proxy->coalesce(100));
57+
$proxy = $reflector->newLazyProxy(fn () => new Table());
58+
var_dump($proxy->read(100));
59+
$proxy = $reflector->newLazyProxy(fn () => new Table());
60+
var_dump($proxy->write(100));
61+
var_dump($proxy->count);
62+
63+
$ghost = $reflector->newLazyGhost(function (Table $table) {});
64+
var_dump($ghost->parse(100));
65+
?>
66+
--EXPECT--
67+
int(100)
68+
int(100)
69+
int(100)
70+
int(99)
71+
int(100)
72+
int(100)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
--TEST--
2+
GH-23628 002: Tracing JIT deoptimization on an undefined property slot of a lazy proxy
3+
--INI--
4+
opcache.enable=1
5+
opcache.enable_cli=1
6+
opcache.file_update_protection=0
7+
opcache.jit=tracing
8+
opcache.jit_buffer_size=32M
9+
opcache.jit_hot_loop=16
10+
--EXTENSIONS--
11+
opcache
12+
--FILE--
13+
<?php
14+
final class Table {
15+
protected array $map = ['start' => ['next' => 1]];
16+
public function parse(int $n): int {
17+
$ok = 0;
18+
for ($i = 0; $i < $n; $i++) {
19+
if (isset($this->map['start']['next'])) {
20+
$ok++;
21+
} else {
22+
throw new RuntimeException('isset false at ' . $i);
23+
}
24+
}
25+
return $ok;
26+
}
27+
}
28+
29+
// The trace is recorded and compiled for a regular object, so that the
30+
// property is read directly from the property slot...
31+
var_dump((new Table())->parse(100));
32+
33+
// ... and later executed for a lazy proxy, whose property slot is undefined
34+
// and has to be forwarded to the real instance during deoptimization.
35+
$proxy = (new ReflectionClass(Table::class))->newLazyProxy(fn () => new Table());
36+
var_dump($proxy->parse(100));
37+
?>
38+
--EXPECT--
39+
int(100)
40+
int(100)

0 commit comments

Comments
 (0)