Hot-counter JIT segfaults when opcache shared memory runs out during `zend_jit_script()` (stale `func_info` after `jit_failure`)
維護者通常 1 天內回覆
還沒有人認領這個 Issue。
評估
- 難度
- 4/5
- 預估耗時
- 3-5 天
- 新手友好度
- 54/100
- Issue 類型
- 缺陷
- 描述清晰度
- 描述清楚
- 活躍度
- 活躍
- 技術堆疊
- c
- 領域
- backend, performance
研究方向
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.
由索引模型根據 Issue 內容生成。
描述
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
- 主要語言
- C
- 星號
- 41.8k
- 分支
- 8.5k
- 平均合併
- 2 天 12 小時
- 30 天內合併 PR
- 137
環境準備
- 沒有 Dockerfile 或 Docker Compose 檔案
- 沒有 Pull Request 範本
- 閱讀貢獻指南
從這裡開始
- 先讀完整個 Issue,再讀專案的貢獻指南。
- 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
- Fork 儲存庫,在一個分支上完成修改。
- 送出 Pull Request,並在描述裡引用這個 Issue 編號。
php/php-src 的其他 Issue
-
SNMP accepts malformed remote ports可能已有人在做 關聯的 PR 仍在進行中或已合併。 未關閉
難度 2/5 半天 新手友好度 62/100
維護者通常 1 天內回覆
-
Bug Status: Needs Triage
難度 2/5 1-3 小時 新手友好度 64/100
維護者通常 1 天內回覆
-
quoted_printable_encode() produces lines longer than 76 characters for bytes 0xF5 to 0xFF可能已有人在做 關聯的 PR 仍在進行中或已合併。 未關閉
難度 2/5 1-3 小時 新手友好度 76/100
維護者通常 1 天內回覆
-
Bug Status: Needs Triage
難度 2/5 1-3 小時 新手友好度 62/100
維護者通常 1 天內回覆
-
Feature Status: Needs Triage
難度 2/5 1-3 小時 新手友好度 73/100
維護者通常 1 天內回覆
相似的 Issue
-
enhancement good first issue help wanted
難度 2/5 1-3 小時 新手友好度 82/100
-
bug multiplayer status: planned
難度 2/5 1-3 小時 新手友好度 67/100
ChupathingyCE/chupathingyce#851 ·
維護者通常 1 天內回覆
-
bug
難度 2/5 1-3 小時 新手友好度 73/100
HigherOrderCO/Bend#2104 ·
-
research-radar
難度 2/5 1-3 小時 新手友好度 68/100
維護者通常 1 天內回覆
-
good first issue help wanted program
難度 2/5 1-3 小時 新手友好度 83/100
hffgzktbcc-blip/ounce-core#297 ·
維護者通常 1 天內回覆