Hot-counter JIT segfaults when opcache shared memory runs out during `zend_jit_script()` (stale `func_info` after `jit_failure`)
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 54/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- c
- Ambito
- backend, performance
Direzione di ricerca
Start with zend_jit_script() and the jit_failure path in ext/opcache/jit/zend_jit.c; the issue provides a proposed fix that clears func_info for remaining op_arrays before releasing CG(arena). Read how call_graph.op_arrays_count and i track setup progress, then look for existing OPcache JIT tests that cover hot-counter triggers and shared memory exhaustion. Done means the reproducer no longer segfaults when shared memory fills and the relevant tests pass.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
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 @ 65f37bbb | 3/3 crash | 3/3 crash |
| master @ f3cb1d35 | 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 @ 65f37bbb and master @ f3cb1d35
Operating System
Debian (official php:8.5.12-cli image) and Ubuntu 24.04, aarch64; also reproduced on linux/amd64 under emulation
- Lingua principale
- C
- Stelle
- 41.8k
- Fork
- 8.5k
- Merge medio
- 2g 12h
- PR unite (30g)
- 137
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di php/php-src
-
SNMP accepts malformed remote portsForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 Mezza giornata Idoneità per principianti 62/100
php/php-src#25137 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Bug Status: Needs Triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 64/100
I maintainer di solito rispondono entro 1 giorno
-
quoted_printable_encode() produces lines longer than 76 characters for bytes 0xF5 to 0xFFForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
I maintainer di solito rispondono entro 1 giorno
-
Bug Status: Needs Triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
php/php-src#24196 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Feature Status: Needs Triage
Difficoltà 2/5 1-3 ore Idoneità per principianti 73/100
php/php-src#24183 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
Issue simili
-
research-radar
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
I maintainer di solito rispondono entro 1 giorno
-
good first issue help wanted program
Difficoltà 2/5 1-3 ore Idoneità per principianti 83/100
hffgzktbcc-blip/ounce-core#297 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 70/100
virtualsquare/picotcp#88 ·
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno