Repository navigation
Description
Description
With the function JIT at opcache.jit=1205 or 1204, compiling the method below writes this line to stderr once per process:
ENTRY 2 (block 15 start 68) - live var 66
The line is printed by ir_add_osr_entry_loads() in ext/opcache/jit/ir/ir_ra.c, which then sets ok = 0 and reaches IR_ASSERT(0). In a release build the assertion is compiled out, and the method is still compiled: it appears in the perf map written with opcache.jit_debug=0x10.
The following code:
<?php
// /tmp/entry.php
namespace Reproduce;
class Entry
{
public static function chr(int $ord, string $encoding = 'UTF-8'): string
{
if ($encoding === 'UCS-4BE') {
return pack('N', $ord);
}
return mb_convert_encoding(self::chr($ord, 'UCS-4BE'), $encoding, 'UCS-4BE');
}
}<?php
// /tmp/preload.php
opcache_compile_file('/tmp/entry.php');php -d opcache.enable_cli=1 -d opcache.jit_buffer_size=32M -d opcache.jit=1205 \
-d opcache.preload=/tmp/preload.php -d opcache.preload_user=root \
-r 'echo \Reproduce\Entry::chr(0x1F35E), "\n";'Resulted in this output:
ENTRY 2 (block 15 start 68) - live var 66
🍞
But I expected this output instead:
🍞
The same line, with the same numbers, appears on aarch64 in php:8.5.12-cli-alpine3.24 and php:8.5.12-cli-trixie, and on x86_64 in php:8.5.12-cli-trixie (linux/amd64, run under emulation on an aarch64 host).
What changes the outcome, each case measured with one process start (ENTRY lines counted on stderr):
| Case | ENTRY lines |
|---|---|
As above, opcache.jit=1205 |
1 |
opcache.jit=1204 |
1 |
opcache.jit=1203, 1255, tracing or disable |
0 |
\mb_convert_encoding(...) written fully qualified |
0 |
use function mb_convert_encoding; added below the namespace |
0 |
Only \pack(...) qualified |
1 |
static::chr(...) instead of self::chr(...) |
0 |
| The class in the global namespace | 0 |
opcache.file_cache set or unset |
no change |
Not preloaded: require '/tmp/entry.php' in the script, opcache.jit=1205, aarch64 |
0 |
| The same, x86_64 under emulation | 1 |
So the trigger appears to be an unqualified function call in a namespace (resolved at run time, namespaced name first) whose argument is the result of a recursive self:: call, compiled by the function JIT with optimization level 4 or 5.
No wrong result was observed. In an application that preloads graphql-php, whose GraphQL\Utils\Utils::chr() has exactly this shape and writes the same line, chr() returned the same bytes with opcache.jit=1205 as with opcache.jit=disable for every code point from 0 to 0x10FFFF, and so did Utils::ord() applied to each result. Whether the entry named in the message is ever taken at run time was not determined.
Possibly related: GH-24578 reports the same message from the function JIT, there with a wrong result, and a comment on it notes that function JIT entries are the receive entries and the continuations after DO_UCALL.
Only 8.5.12 was tested. ext/opcache/jit/ir/ir_ra.c on the PHP-8.5 branch has not changed since 8.5.12; on master it changed after 8.5.12 was tagged. Neither branch was built.
PHP Version
PHP 8.5.12 (cli) (built: Oct 21 2026 15:54:49) (NTS)
Copyright (c) The PHP Group
Built by https://github.com/docker-library/php
Zend Engine v4.5.12, Copyright (c) Zend Technologies
with Zend OPcache v8.5.12, Copyright (c), by Zend Technologies
The x86_64 run used the linux/amd64 php:8.5.12-cli-trixie image built on Oct 28 2026.
Operating System
Alpine Linux 3.24 and Debian trixie (official Docker images), aarch64; Debian trixie linux/amd64 under emulation
Activity
Fix loop-invariant hoisting across external entries
Fix loop-invariant hoisting across external entries
jorgsowa commentedon Nov 2, 2026
That's not yet fixed in php-src. I'm not sure why cross-repository commits close issues in github...