Hot-counter JIT segfaults when opcache shared memory runs out during `zend_jit_script()` (stale `func_info` after `jit_failure`)

Offen
#25,220 3 Kommentare 1 Reaktion 0 zugewiesene Personen Auf GitHub ansehen

Maintainer antworten meist innerhalb von 1 Tag

Dieses Issue hat noch niemand übernommen.

Bewertung

Schwierigkeit
4/5
Geschätzter Aufwand
3-5 Tage
Anfängerfreundlichkeit
54/100
Issue-Typ
Bug
Klarheit
Klar beschrieben
Aktivitätsstatus
Aktiv
Tech-Stack
c
Bereich
backend, performance

Rechercherichtung

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.

Vom Indexierungsmodell aus dem Issue-Text verfasst.

Beschreibung

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=20000
  • memory_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

Vorherrschende Sprache
C
Sterne
41.8k
Forks
8.5k
Ø Merge
2 T. 12 Std.
Gemergte PRs (30 T.)
137

Entwicklungsumgebung

Erste Schritte

  1. Lesen Sie das ganze Issue und danach den Beitragsleitfaden des Projekts.
  2. Schreiben Sie ins Issue, dass Sie es übernehmen — das erspart doppelte Arbeit.
  3. Forken Sie das Repository und arbeiten Sie in einem Branch.
  4. Öffnen Sie einen Pull Request, der die Issue-Nummer nennt.

Mehr aus php/php-src

Alle Issues in php/php-src

Ähnliche Issues

Weitere Issues zu C

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.