Repository navigation
Description
Description
With opcache.jit=1235 (hot-function counters), a CLI process segfaults while JIT-compiling a hot function once opcache's shared memory is nearly full. The fault is in type inference or zend_jit_cleanup_func_info(), reading a callee's func_info.
My reading of the cause: zend_jit_script() builds the call graph in CG(arena), which points every op_array's func_info slot at arena memory. Then, for hot-counter triggers, it calls zend_jit_op_array() → zend_jit_setup_hot_counters(), which allocates each jit_extension with zend_shared_alloc(). zend_accel_script_persist_calc() doesn't count those allocations. So near a full cache the script itself persists, but an extension allocation returns NULL partway through the op_arrays. That jumps to jit_failure. For hot triggers, jit_failure only releases the arena; it doesn't clear func_info. So the remaining op_arrays keep pointers into freed arena memory. When a function from that script later gets hot, zend_real_jit_func() reads the callee's stale func_info, through zend_get_func_info() in type inference or through zend_jit_cleanup_func_info(), and crashes.
Clearing the remaining op_arrays' func_info on that path fixes the reproducer below: 12 of 12 runs clean on a patched 8.5.12, against 6 of 6 crashing unpatched.
--- a/ext/opcache/jit/zend_jit.c
+++ b/ext/opcache/jit/zend_jit.c
@@ int zend_jit_script(zend_script *script)
jit_failure:
if (JIT_G(trigger) == ZEND_JIT_ON_SCRIPT_LOAD) {
for (i = 0; i < call_graph.op_arrays_count; i++) {
ZEND_SET_FUNC_INFO(call_graph.op_arrays[i], NULL);
}
+ } else {
+ /* op_arrays not set up yet still point at func_info in the arena released below */
+ for (; i < call_graph.op_arrays_count; i++) {
+ ZEND_SET_FUNC_INFO(call_graph.op_arrays[i], NULL);
+ }
}
zend_arena_release(&CG(arena), checkpoint);
return FAILURE;Reproducer
gen.php writes 600 classes of 150 methods; m0() calls every other method:
<?php
// Generates $argv[2] classes of $argv[3] methods; m0 calls every other method.
[, $dir, $n, $methods] = $argv;
@mkdir($dir, 0777, true);
for ($i = 0; $i < (int) $n; $i++) {
$m = '';
$calls = '';
for ($k = 1; $k < (int) $methods; $k++) {
$calls .= " \$a += \$this->m$k(\$x + $k);\n";
$m .= " public function m$k(int \$x): int { \$t = 0; for (\$j = 0; \$j < 3; \$j++) { \$t += (\$x * $k + \$j) % 7; } return \$t > 3 ? \$t : \$t + $k; }\n";
}
file_put_contents("$dir/C$i.php", "<?php\nfinal class C$i\n{\n public function m0(int \$x): int\n {\n \$a = 0;\n$calls return \$a;\n }\n$m}\n");
}run.php loads them and calls each until hot:
<?php
// Loads the generated classes one by one and calls each until its methods are hot,
// revisiting earlier classes so their functions compile after opcache has filled.
$dir = $argv[1];
$n = (int) $argv[2];
$sum = 0;
$order = range(0, $n - 1);
mt_srand((int) ($argv[3] ?? 0));
shuffle($order);
foreach ($order as $i) {
require "$dir/C$i.php";
$class = "C$i";
$o = new $class();
for ($r = 0; $r < 300; $r++) {
$sum += $o->m0($r);
}
$back = "C".$order[mt_rand(0, array_search($i, $order))];
$p = new $back();
for ($r = 0; $r < 300; $r++) {
$sum += $p->m0($r);
}
if ($i % 500 === 0) {
$s = opcache_get_status(false);
fprintf(STDERR, "%d full=%d free=%d jit=%s\n", $i, $s['cache_full'], $s['memory_usage']['free_memory'], json_encode($s['jit']['buffer_free'] ?? null));
}
}
// Second pass: every class again, so the one script whose JIT setup ran out of
// shared memory part-way gets its hot functions compiled.
for ($pass = 0; $pass < 2; $pass++) {
for ($i = 0; $i < $n; $i++) {
$class = "C$i";
$o = new $class();
for ($r = 0; $r < 300; $r++) {
$sum += $o->m0($r);
}
}
$s = opcache_get_status(false);
fprintf(STDERR, "pass %d jit_free=%d\n", $pass, $s['jit']['buffer_free']);
}
echo "done $sum\n";docker run --rm -v "$PWD":/r -w /r php:8.5.12-cli bash -c '
php gen.php /tmp/c 600 150
php -d opcache.enable_cli=1 -d opcache.jit=1235 -d opcache.jit_buffer_size=64M \
-d opcache.memory_consumption=16 -d opcache.max_accelerated_files=20000 \
-d opcache.interned_strings_buffer=1 -d opcache.file_update_protection=0 \
-d opcache.validate_timestamps=0 -d memory_limit=-1 run.php /tmp/c 600 1; echo rc=$?'
Result: Segmentation fault, rc=139. With opcache.jit=off and everything else the same: completes.
The crash depends on where the memory boundary falls, so it's sensitive to the sizes. Two settings that crash every time here:
memory_consumption=16,max_accelerated_files=20000memory_consumption=8,max_accelerated_files=100000
Neighboring sizes (9–12M at 100000) don't crash.
| Build | 20000 / 16M | 100000 / 8M |
|---|---|---|
| php:8.5.12-cli (official image, aarch64) | 3/3 crash | 3/3 crash |
php-8.5.12 built from source, -g -O2 --disable-all |
6/6 crash | 6/6 crash |
| PHP-8.5 @ 65f37bb | 3/3 crash | 3/3 crash |
| master @ f3cb1d3 | 0/3 | 3/3 crash |
| php-8.5.12 + patch above | 0/6 | 0/6 |
| php:8.5.12-cli, linux/amd64 (emulated, 1 run each) | 0/1 | 1/1 crash |
opcache.jit=function and tracing don't crash with this script; only the hot-counter trigger (1235) does.
Backtrace (php-8.5.12 from source, 20000 / 16M):
Program received signal SIGSEGV, Segmentation fault.
_zend_update_type_info (op_array=0xaaaaa69d21d0, ssa=..., script=0x0, ..., optimization_level=2147270655)
3903 UPDATE_SSA_OBJ_TYPE(ce, ce_is_instanceof, ssa_op->result_def);
#0 _zend_update_type_info (...) Zend/Optimizer/zend_inference.c
#1 zend_infer_types_ex (...)
#2 zend_infer_types (...)
#3 zend_ssa_inference (...)
#4 zend_jit_op_array_analyze2 (...) ext/opcache/jit/zend_jit.c
#5 zend_real_jit_func (op_array=..., script=0x0, rt_opline=..., trigger=3) ext/opcache/jit/zend_jit.c:3091
#6 zend_jit_hot_func (...) ext/opcache/jit/zend_jit.c:3234
In a large application's test suite (paratest workers, PHP 8.5.12 aarch64, opcache at default sizes with ~36k PHP files), the same configuration also crashed inside zend_jit_cleanup_func_info(), recursing compiled function → callee → callee's caller_info → garbage caller_op_array:
#0 zend_jit_cleanup_func_info (op_array=<optimized out>) at ext/opcache/jit/zend_jit.c:3013
#1 zend_jit_cleanup_func_info (op_array=<optimized out>) at ext/opcache/jit/zend_jit.c:3035
#2 zend_jit_cleanup_func_info (op_array=...) at ext/opcache/jit/zend_jit.c:3041
#3 zend_real_jit_func (op_array=..., script=0x0, rt_opline=..., trigger=3) at ext/opcache/jit/zend_jit.c:3103
#4 zend_jit_hot_func (...) at ext/opcache/jit/zend_jit.c:3234
Raising opcache.memory_consumption / max_accelerated_files well past the codebase made those crashes disappear (22 crashes in 66 suite runs → 0 in 47).
PHP Version
PHP 8.5.12 (cli) (NTS); also PHP-8.5 @ 65f37bb and master @ f3cb1d3
Operating System
Debian (official php:8.5.12-cli image) and Ubuntu 24.04, aarch64; also reproduced on linux/amd64 under emulation
Activity
rafamolina1 commentedon Nov 5, 2026
Opened a fix against PHP-8.4: #25224