FPM: pools with different disable_functions sharing one opcache run scripts wrongly (var_export() calls var_dump(), SIGSEGV in the restricted pool) on 8.4/8.5/8.6

Abierto
#24,661 3 comentarios 0 reacciones 0 asignados Ver en GitHub

Los mantenedores suelen responder en 1 día

Nadie ha tomado este issue todavía.

Evaluación

Dificultad
4/5
Tiempo estimado
3-5 días
Aptitud para principiantes
48/100
Tipo de issue
Error
Claridad
Bien especificado
Estado de actividad
Activo
Stack tecnológico
php

Línea de trabajo

Start by reproducing with the provided FPM configuration, scripts, and run.sh on PHP 8.5; compare pool request order and confirm that disabling OPcache or optimizer bits 0x20 and 0x40 avoids the issue. The report does not identify source files or tests, so trace the OPcache optimizer passes and how calls are resolved across pools. Done when shared OPcache preserves correct function dispatch for pools with different disable_functions settings, including the reported crash case.

Escrito por el modelo de indexación a partir del texto del issue.

Descripción

Description

Quoted text written with an LLM and checked by a human developer.

Two pools in one FPM master, one with php_admin_value[disable_functions] and one without, share the opcache. Once a script has been compiled and cached through one pool, the other pool executes it wrongly: an internal function call is dispatched to a different internal function, and a worker of the restricted pool can crash.

fpm.conf:

[global]
daemonize = no
error_log = /dev/stderr
[a]
user = nobody
listen = /tmp/a.sock
listen.owner = nobody
pm = static
pm.max_children = 1
php_admin_value[disable_functions] = exec
[b]
user = nobody
listen = /tmp/b.sock
listen.owner = nobody
pm = static
pm.max_children = 1

t.php:

<?php
echo var_export([1, 'x' => true], true), "\n";

Request t.php through pool a, then through pool b (a ~20-line FastCGI client, fcgi.php, is attached below so that no web server is needed). Resulting output, php:8.5-fpm (8.5.12), default settings (opcache is built in):

pool a: array (
  0 => 1,
  'x' => true,
)
pool b: array(2) {
  [0]=>
  int(1)
  ["x"]=>
  bool(true)
}
bool(true)

Pool b printed the output of var_dump() (and its return value) instead of var_export(). Expected output: the same text as pool a. With the order reversed (b first, then a) pool a, the one with exec disabled, prints debug_zval_dump() style output (array(2) interned {...}).

On Ubuntu's 8.5.4 (8.5.4-0ubuntu1.5, -n with zend_extension=opcache) pool b fails with convert_uudecode() expects exactly 1 argument, 2 given for the same script, i.e. the call is dispatched to a third function.

A script that calls the disabled function itself (t2.php: echo exec('echo hi');), requested through b first and then through a, kills the worker of the restricted pool:

WARNING: [pool a] child 10 exited on signal 11 (SIGSEGV) after 2.015193 seconds from start

What I checked:

result
8.5.12, 8.4.27 (official php:*-fpm images), 8.6.0RC4 (php:8.6-rc-fpm, ZTS), Ubuntu 8.5.4 wrong function called
8.3.36 (php:8.3-fpm) correct
opcache.enable=0 correct
opcache.optimization_level=0 correct
8.4 without zend_extension=opcache (-n) correct
default opcache.optimization_level with the bit 0x20 or the bit 0x40 cleared (one at a time; every other bit alone makes no difference) correct
two separate FPM masters, one pool each, same disable_functions split (8.5.12 and 8.4.27) correct, no cross-effect

I did not trace what exactly gets cached or resolved by index or by pointer, so I cannot say more than that the optimizer passes 0x20 (DFA) and 0x40 (call graph) are both needed. I saw no case where a disabled function actually ran: the restricted pool crashed or printed the output of another function, but never executed exec. FPM accepts php_admin_value[disable_functions] per pool, so a setup like this is not exotic, and the symptom (wrong output rather than an error) is hard to diagnose; the workaround is opcache.enable=0, or one master per differing disable_functions set.

I found no open or closed report about this. Same root cause as GH-17733. Related but different: #21974 (SIGSEGV with disable_functions per pool, ZTS builds only) and #24047 (system id and frameless functions).

To reproduce (podman/docker; the directory holds fpm.conf, t.php, t2.php, fcgi.php, run.sh):

docker run --rm -v "$PWD":/m docker.io/library/php:8.5-fpm sh /m/run.sh "a b a b" t.php
docker run --rm -v "$PWD":/m docker.io/library/php:8.5-fpm sh /m/run.sh "b a" t2.php
docker run --rm -v "$PWD":/m docker.io/library/php:8.5-fpm sh /m/run.sh "a b a b" t.php -d opcache.enable=0   # correct
control: one master per pool (correct on 8.5.12 and 8.4.27)
sed -n '1,10p' /m/fpm.conf > /tmp/fpm-a.conf
{ sed -n '1,3p' /m/fpm.conf; sed -n '11,16p' /m/fpm.conf; } > /tmp/fpm-b.conf
php-fpm -y /tmp/fpm-a.conf -F >/m/fpm-a.out 2>&1 &
php-fpm -y /tmp/fpm-b.conf -F >/m/fpm-b.out 2>&1 &
sleep 2
for p in a b a b b a; do printf -- "pool %s: " "$p"; php /m/fcgi.php /tmp/$p.sock /m/t.php 2>&1 | tr '\n' ' '; echo; done

fcgi.php:

<?php
// Minimal FastCGI GET client: php fcgi.php /tmp/a.sock /m/t.php
[$sock, $script] = [$argv[1], $argv[2]];
$rec = fn(int $t, string $b) => pack('CCnnCx', 1, $t, 1, strlen($b), 0) . $b;
$kv = function (string $k, string $v): string {
    $l = fn(int $n) => $n < 128 ? chr($n) : pack('N', $n | 0x80000000);
    return $l(strlen($k)) . $l(strlen($v)) . $k . $v;
};
$p = $kv('SCRIPT_FILENAME', $script) . $kv('REQUEST_METHOD', 'GET') . $kv('SCRIPT_NAME', '/t.php')
   . $kv('SERVER_PROTOCOL', 'HTTP/1.1') . $kv('REQUEST_URI', '/t.php') . $kv('QUERY_STRING', '');
$s = stream_socket_client("unix://$sock", $en, $es, 10);
fwrite($s, $rec(1, pack('nCx5', 1, 0)) . $rec(4, $p) . $rec(4, '') . $rec(5, ''));
$out = '';
while (!feof($s)) {
    $h = stream_get_contents($s, 8);
    if (strlen($h) < 8) break;
    $d = unpack('Cv/Ct/nid/nlen/Cpad', $h);
    $body = $d['len'] + $d['pad'] ? stream_get_contents($s, $d['len'] + $d['pad']) : '';
    if ($d['t'] === 6 || $d['t'] === 7) $out .= substr($body, 0, $d['len']);
    if ($d['t'] === 3) break;
}
echo preg_replace('~^.*?\r\n\r\n~s', '', $out) ?: "(empty response / connection reset)\n";

run.sh:

ord=$1; scr=$2; shift 2
php -v | head -1
php-fpm "$@" -y /m/fpm.conf -F >/m/fpm.out 2>&1 &
sleep 2
for p in $ord; do printf -- "pool %s: " "$p"; php /m/fcgi.php /tmp/$p.sock /m/$scr 2>&1 | tr '\n' ' '; echo; done
grep -E "SIGSEGV|signal" /m/fpm.out
PHP Version
PHP 8.5.12 (cli) (built: Oct 21 2026 15:49:51) (NTS)   [php:8.5-fpm]
PHP 8.4.27 (cli) (built: Oct 21 2026 15:53:31) (NTS)   [php:8.4-fpm, opcache loaded]
PHP 8.6.0RC4 (cli) (built: Oct 21 2026 22:05:36) (ZTS) [php:8.6-rc-fpm]
PHP 8.5.4-0ubuntu1.5 (Ubuntu 26.04)
Operating System

Debian (official Docker images), Ubuntu 26.04

Lenguaje dominante
C
Estrellas
41.8k
Forks
8.5k
Merge medio
2 d 13 h
PR fusionados (30 d)
145

Preparar el entorno

Primeros pasos

  1. Lee el issue completo y luego la guía de contribución del proyecto.
  2. Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
  3. Haz un fork del repositorio y trabaja en una rama.
  4. Abre un pull request que haga referencia al número del issue.

Más de php/php-src

Todos los issues de php/php-src

Issues similares

Más issues de C

Recibe los nuevos issues en tu correo

Un resumen breve de issues de GitHub para principiantes.