php / php/php-src

[PHP8.5] [JIT] Tracing JIT wrong optimization for LazyProxy instances

Aperta
#23,628 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

Nessuno ha ancora preso questa issue.

Bug Status: Needs Triage
Lingua principale
C
Stelle
40.4k
Fork
8.1k
Merge medio
2g 13h
PR unite (30g)
96

Descrizione

When $this is an initialized lazy proxy created by ReflectionClass::newLazyProxy(), the
proxy object's own declared-property slots are permanently IS_UNDEF | IS_PROP_LAZY
(set in zend_lazy_object_init_proxy()), and every property access is meant to be
forwarded to the real backing instance through read_property.

The tracing JIT compiles a hot loop that reads such a property in isset()
context (FETCH_OBJ_IS on $this) using the known-property fast path from
zend_jit_fetch_obj(). That fast path reads the property directly at
prop_info->offset and only guards Z_TYPE != IS_UNDEF; it does not account for
the object being a lazy proxy, so it never forwards to read_property. It reads the
proxy's own dead slot (IS_UNDEF), takes the deopt exit, and in
zend_jit_trace_exit() the ZREG_ZVAL_COPY handler converts the IS_UNDEF value of a
FETCH_OBJ_IS into NULL (treating it as an undefined property). The result is that
isset($this->prop['k']) evaluates to false and property reads see a missing value,
even though the backing instance holds the data.

opcache.jit=function and opcache.jit=0 are correct; only tracing JIT (1254/1255/tracing) is affected.
Lazy ghosts (newLazyGhost) and eager objects are unaffected, because a ghost
initializes in place so its own slots become real values.

Reproducer (self-contained)
<?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;
    }
}
$proxy = (new ReflectionClass(Table::class))->newLazyProxy(fn() => new Table());
echo $proxy->parse(1000), "\n";
$ php -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_buffer_size=64M script.php
PHP Fatal error:  Uncaught RuntimeException: isset false at 61   # trace compiles at jit_hot_loop=61

$ php -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_buffer_size=64M -d opcache.jit_hot_loop=1 script.php
PHP Fatal error:  Uncaught RuntimeException: isset false at 1    # deterministic: fails on the first traced iteration

$ php -d opcache.jit=function script.php   # 1000  (OK)
$ php -d opcache.jit=0        script.php   # 1000  (OK)

Note: as a tiny standalone script the failure is JIT trace-layout sensitive (byte-identical
files can reproduce or not depending only on their path), because it depends on which trace
compiles first. Forcing opcache.jit_hot_loop to a small value makes it deterministic on the
affected file. The behaviour is 100% reproducible in a real workload (see below).

Confirmed on
  • PHP 8.5.10 (release) and PHP-8.5 branch head (8.5.12-dev), NTS, x86_64, Zend OPcache, tracing JIT.
Root cause (code)
  • ext/opcache/jit/zend_jit_ir.c, zend_jit_fetch_obj(): the prop_info != NULL
    (known property) branch computes prop_addr = obj + prop_info->offset and guards only
    on IS_UNDEF; there is no lazy-object check, so a lazy proxy's slot is read directly.
  • ext/opcache/jit/zend_jit.c, zend_get_known_property_info(): returns a usable
    prop_info whenever the class is not immutable but is declared in the same file as the
    accessing op_array (ce->info.user.filename == op_array->filename) — which is the common
    case for a class and its own method — enabling that fast path.
  • ext/opcache/jit/zend_jit_trace.c, zend_jit_trace_exit(): for a FETCH_OBJ_IS
    deopt whose value is IS_UNDEF, the ZREG_ZVAL_COPY branch sets the result to
    NULL (undefined-property semantics), which is wrong when the slot is
    IS_UNDEF | IS_PROP_LAZY on a lazy proxy.

The interpreter and function JIT reach zend_std_read_property(), whose uninit_error
path checks zend_lazy_object_must_init() / IS_PROP_LAZY and forwards to the backing
instance. The tracing JIT fast path skips this.

Real-world reproducer (100% deterministic)

goaop/framework wraps its PointcutParser (subclass of Dissect\Parser\LALR1\Parser,
which reads $this->parseTable[...][...][...] via isset() in a hot loop) in a
ReflectionClass::newLazyProxy() via its service container. Under the container image's
default CLI JIT settings this makes every pointcut fail to parse:

git clone https://github.com/goaop/framework.git && cd framework && composer install
rm -rf tests/Fixtures/project/var/cache/aspect
php -d opcache.enable_cli=1 -d opcache.jit=tracing -d opcache.jit_buffer_size=64M \
    tests/Fixtures/project/bin/console --no-ansi cache:warmup:aop tests/Fixtures/project/web/index.php
# -> "Unexpected token ... Expected one of: ..." where the rejected token IS in the expected list

See https://github.com/goaop/framework/issues/661 for the full context.

PHP Version
PHP 8.5.10 (cli) (built: Aug 28 2026 06:46:18) (NTS)
Copyright (c) The PHP Group
Built by Ubuntu
Zend Engine v4.5.10, Copyright (c) Zend Technologies
    with Zend OPcache v8.5.10, Copyright (c), by Zend Technologies
Operating System

No response

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Direzione di ricerca

Esegui lo script proxy autonomo con il tracing JIT, quindi confrontalo con il function JIT e con il JIT disabilitato. Leggi zend_jit_fetch_obj() in ext/opcache/jit/zend_jit_ir.c, zend_get_known_property_info() in zend_jit.c e zend_jit_trace_exit() in zend_jit_trace.c; il lavoro è completo quando la configurazione di tracing preserva il risultato atteso di 1000 per il riproduttore.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
c, php
Ambito
compilers, performance
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Attiva
Chiarezza
Specificata chiaramente
Idoneità per principianti
45/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.