Hacktoberfest 2026: los issues que los mantenedores marcaron para octubre, abiertos y aptos para principiantes. Explorar issues de Hacktoberfest

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

Abierto
#23,628 0 comentarios 0 reacciones 0 asignados Ver en GitHub

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
45/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
c, php

Línea de trabajo

Ejecuta el script proxy autocontenido con tracing JIT y compáralo después con function JIT y con el JIT deshabilitado. Lee zend_jit_fetch_obj() en ext/opcache/jit/zend_jit_ir.c, zend_get_known_property_info() en zend_jit.c y zend_jit_trace_exit() en zend_jit_trace.c; el trabajo estará terminado cuando la configuración de tracing conserve el resultado esperado de 1000 para el reproductor.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Bug Status: Needs Triage

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

Lenguaje dominante
C
Estrellas
40.4k
Forks
8.2k
Merge medio
2 d 17 h
PR fusionados (30 d)
115

Guía de contribución

Abrir la guía de contribución

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de php/php-src

Todos los issues de php/php-src

Issues similares

Más issues de C

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.