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

Ouverte
#25,220 3 commentaires 1 réaction 0 personnes assignées Voir sur GitHub

Les mainteneurs répondent en général sous 1 jour

Personne n'a encore pris cette issue.

Évaluation

Difficulté
4/5
Temps estimé
3-5 jours
Accessibilité débutants
54/100
Type d'issue
Bug
Clarté
Clairement spécifiée
Activité
Active
Stack technique
c
Domaine
backend, performance

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

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=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

Langage dominant
C
Étoiles
41.8k
Forks
8.5k
Merge moyen
2 j 12 h
PR mergées (30 j)
137

Préparer son environnement

Par où commencer

  1. Lisez l'issue en entier, puis le guide de contribution du projet.
  2. Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
  3. Forkez le dépôt et travaillez sur une branche.
  4. Ouvrez une pull request qui référence le numéro de l'issue.

Autres issues de php/php-src

Toutes les issues de php/php-src

Issues similaires

Plus d'issues C

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.