Skip to content

JIT tracing: infinite loop on FETCH_OBJ_R with IS_UNDEF property in polymorphic context #21267

@esistgut

Description

@esistgut

Description

When using opcache.jit=tracing, a class with:

  1. An untyped declared property with a default value (public $incrementing = true)
  2. A __get() magic method
  3. Three or more subclasses (polymorphic call context)

If unset() is called on that property on some instances (making the property slot IS_UNDEF), and the getter is then called in a hot loop mixing normal and unset instances, the JIT-compiled FETCH_OBJ_R handler enters an infinite loop burning 100% CPU instead of falling back to the interpreter to call __get().

Reproduction script

<?php
class Base
{
    public $incrementing = true;

    public function __get($name) { return null; }

    public function getIncrementing() { return $this->incrementing; }

    public function getCasts(): array
    {
        if ($this->getIncrementing()) {
            return ['id' => 'int'];
        }
        return [];
    }

    public function hasCast(string $key): bool
    {
        return array_key_exists($key, $this->getCasts());
    }
}

class ChildA extends Base {}
class ChildB extends Base {}
class ChildC extends Base { public $incrementing = false; }

$classes = [ChildA::class, ChildB::class, ChildC::class];

// Warmup: train the JIT on the hot path ($incrementing = true/false)
for ($round = 0; $round < 5; $round++) {
    for ($i = 0; $i < 1000; $i++) {
        $m = new $classes[$i % 3]();
        $m->getIncrementing();
        $m->getCasts();
        $m->hasCast('id');
    }
}

// Trigger: mix normal and unset($incrementing) objects in a hot loop.
// The JIT-compiled trace for FETCH_OBJ_R on $this->incrementing
// enters an infinite loop when it encounters IS_UNDEF.
for ($i = 0; $i < 2000; $i++) {
    $m = new $classes[$i % 3]();
    if ($i % 7 === 0) {
        unset($m->incrementing);
    }
    @$m->getIncrementing();
    @$m->getCasts();
    @$m->hasCast('id');
}

echo "OK — bug not triggered\n";

Run with:

timeout 10 php -d opcache.enable_cli=1 -d opcache.jit_buffer_size=64M -d opcache.jit=tracing repro.php

Expected: prints "OK" and exits
Actual: hangs forever burning 100% CPU (killed by timeout)

Affected versions

Version ZTS/NTS Result
PHP 8.4.15 ZTS HANG
PHP 8.4.15 NTS HANG
PHP 8.4.18 NTS HANG
PHP 8.5.3 NTS HANG

Not affected with opcache.jit=disable or opcache.jit=function.

Analysis from production incident

This bug was discovered during a production outage where a FrankenPHP server (PHP 8.4.15 ZTS, opcache.jit=tracing) became completely unresponsive. All 8 PHP worker threads were stuck in an infinite loop inside JIT-compiled code.

Using gdb to disassemble the JIT-compiled code at the stuck address, the root cause was identified:

  1. The JIT compiles FETCH_OBJ_R for $this->incrementing with a fast path for IS_TRUE (type 3)
  2. When the property type is IS_UNDEF (type 0, after unset()), the JIT code jumps to a fallback handler
  3. The fallback handler at the IS_UNDEF check (cmpb $0x0, 0x8(%rax) / je ...) dispatches back to the same opcode handler entry point instead of the interpreter
  4. This creates an infinite loop with no way to break out (no VM interrupt check in the loop path)

Since max_execution_time=0 in CLI/FrankenPHP mode, the loop runs forever.

Key conditions for triggering

  • opcache.jit=tracing (not function)
  • Polymorphic calls (3+ subclasses calling the same method)
  • Warmup phase trains JIT on the "property is initialized" path
  • Then unset() on the property creates IS_UNDEF
  • The @ error suppression or __get() magic method must be present

Note

This bug was diagnosed and this report was compiled by Claude Code (Claude Opus 4.6), Anthropic's AI coding agent, during a production incident investigation.

PHP Version

PHP 8.5.3 (cli) (built: Feb 12 2026 16:29:14) (NTS)
Copyright (c) The PHP Group
Zend Engine v4.5.3, Copyright (c) Zend Technologies
    with Zend OPcache v8.5.3, Copyright (c), by Zend Technologies

and multiple other versions via Docker

Operating System

Linux x86_64

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions