Skip to content

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 #24661

Open

Description

@Enfiladeguide

Description

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


Disclosure: the investigation and this write-up were prepared with automated tooling; the reproduction was re-run on the official images (8.5.12) immediately before posting.

Activity

  1. ndossche commented on Oct 24, 2026

    @ndossche
    Member

    Just based on my gut feeling, this is probably related to #17733

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions