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

未关闭
#24,661 3 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

还没有人认领这个 Issue。

评估

难度
4/5
预计耗时
3-5 天
新手友好度
48/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
php

调研方向

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.

由索引模型根据 Issue 内容生成。

描述

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

主要语言
C
星标
41.8k
派生
8.5k
平均合并
2 天 13 小时
30 天内合并 PR
145

环境准备

  • 没有 Dockerfile 或 Docker Compose 文件
  • 没有 Pull Request 模板
  • 阅读贡献指南

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

php/php-src 的其他 Issue

查看 php/php-src 的全部 Issue

相似的 Issue

更多 C Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。