Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ PHP NEWS
registrations are freed while still reachable from the cycle collector.
(Ilia Alshanetsky)

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


24 Sep 2026, PHP 8.5.11

Expand Down
5 changes: 5 additions & 0 deletions ext/opcache/jit/zend_jit_ir.c
Original file line number Diff line number Diff line change
Expand Up @@ -14247,6 +14247,11 @@ static int zend_jit_fetch_obj(zend_jit_ctx *jit,
ZEND_ASSERT(Z_TYPE_P(member) == IS_STRING && Z_STRVAL_P(member)[0] != '\0');
prop_info = zend_get_known_property_info(op_array, ce, Z_STR_P(member), on_this, op_array->filename);

if (JIT_G(trigger) == ZEND_JIT_ON_HOT_TRACE && prop_type == IS_UNDEF) {
prop_info = NULL;
trace_ce = NULL;
}

if (on_this) {
zend_jit_addr this_addr = ZEND_ADDR_MEM_ZVAL(ZREG_FP, offsetof(zend_execute_data, This));
obj_ref = jit_Z_PTR(jit, this_addr);
Expand Down
7 changes: 5 additions & 2 deletions ext/opcache/jit/zend_jit_trace.c
Original file line number Diff line number Diff line change
Expand Up @@ -8713,10 +8713,13 @@ int ZEND_FASTCALL zend_jit_trace_exit(uint32_t exit_num, zend_jit_registers_buf
const zend_op *op = t->exit_info[exit_num].opline;
ZEND_ASSERT(op);
op--;
if (op->opcode == ZEND_FETCH_DIM_IS || op->opcode == ZEND_FETCH_OBJ_IS) {
if (op->opcode == ZEND_FETCH_DIM_IS) {
ZVAL_NULL(EX_VAR_NUM(i));
} else if (op->opcode == ZEND_FETCH_OBJ_IS
&& (Z_PROP_FLAG_P(val) & (IS_PROP_LAZY|IS_PROP_UNINIT)) == IS_PROP_UNINIT) {
ZVAL_NULL(EX_VAR_NUM(i));
} else {
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);
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);
repeat_last_opline = 1;
}
} else {
Expand Down
72 changes: 72 additions & 0 deletions ext/opcache/tests/jit/gh23628_001.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
--TEST--
GH-23628 001: Tracing JIT reads undefined property slots of a lazy proxy
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_update_protection=0
opcache.jit=tracing
opcache.jit_buffer_size=32M
opcache.jit_hot_loop=16
--EXTENSIONS--
opcache
--FILE--
<?php
final class Table {
protected array $map = ['start' => ['next' => 1]];
public int $count = 0;
public function parse(int $n): int {
$ok = 0;
for ($i = 0; $i < $n; $i++) {
if (isset($this->map['start']['next'])) {
$ok++;
} else {
throw new RuntimeException('isset false at ' . $i);
}
}
return $ok;
}
public function coalesce(int $n): int {
$sum = 0;
for ($i = 0; $i < $n; $i++) {
$sum += $this->map['start']['next'] ?? 100;
}
return $sum;
}
public function read(int $n): int {
$sum = 0;
for ($i = 0; $i < $n; $i++) {
$sum += $this->map['start']['next'];
}
return $sum;
}
public function write(int $n): int {
for ($i = 0; $i < $n; $i++) {
$this->map['start']['next'] = $i;
$this->count++;
}
return $this->map['start']['next'];
}
}

$reflector = new ReflectionClass(Table::class);

$proxy = $reflector->newLazyProxy(fn () => new Table());
var_dump($proxy->parse(100));
$proxy = $reflector->newLazyProxy(fn () => new Table());
var_dump($proxy->coalesce(100));
$proxy = $reflector->newLazyProxy(fn () => new Table());
var_dump($proxy->read(100));
$proxy = $reflector->newLazyProxy(fn () => new Table());
var_dump($proxy->write(100));
var_dump($proxy->count);

$ghost = $reflector->newLazyGhost(function (Table $table) {});
var_dump($ghost->parse(100));
?>
--EXPECT--
int(100)
int(100)
int(100)
int(99)
int(100)
int(100)
46 changes: 46 additions & 0 deletions ext/opcache/tests/jit/gh23628_002.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
--TEST--
GH-23628 002: Tracing JIT deoptimization on an undefined property slot of a lazy proxy
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_update_protection=0
opcache.jit=tracing
opcache.jit_buffer_size=32M
opcache.jit_hot_loop=16
--EXTENSIONS--
opcache
--FILE--
<?php
final class Table {
protected array $map = ['start' => ['next' => 1]];
public function parse(int $n): int {
$ok = 0;
for ($i = 0; $i < $n; $i++) {
if (isset($this->map['start']['next'])) {
$ok++;
} else {
throw new RuntimeException('isset false at ' . $i);
}
}
return $ok;
}
}

// The trace is recorded and compiled for a regular object, so that the
// property is read directly from the property slot...
var_dump((new Table())->parse(100));

// ... and later executed for a lazy proxy, whose property slot is undefined
// and has to be forwarded to the real instance during deoptimization.
$proxy = (new ReflectionClass(Table::class))->newLazyProxy(fn () => new Table());
var_dump($proxy->parse(100));

// ... and for an uninitialized lazy ghost, which is initialized on the first
// property access.
$ghost = (new ReflectionClass(Table::class))->newLazyGhost(function (Table $table) {});
var_dump($ghost->parse(100));
?>
--EXPECT--
int(100)
int(100)
int(100)
38 changes: 38 additions & 0 deletions ext/opcache/tests/jit/gh23628_003.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
--TEST--
GH-23628 003: Tracing JIT deoptimization on an unset() property served by __isset()/__get()
--INI--
opcache.enable=1
opcache.enable_cli=1
opcache.file_update_protection=0
opcache.jit=tracing
opcache.jit_buffer_size=32M
opcache.jit_hot_loop=16
--EXTENSIONS--
opcache
--FILE--
<?php
class A {
public $p = ['x' => 1];
public function __isset($n) { return true; }
public function __get($n) { return ['x' => 42]; }
function f($n) {
$s = 0;
for ($i = 0; $i < $n; $i++) {
$s += $this->p['x'] ?? 1000;
}
return $s;
}
}

// The trace is recorded and compiled while the property is initialized...
var_dump((new A)->f(100));

// ... and then executed after the property was unset(), so the fetch has to
// go through __isset()/__get() instead of yielding NULL.
$a = new A;
unset($a->p);
var_dump($a->f(100));
?>
--EXPECT--
int(100)
int(4200)
Loading