php / php/php-src

file_exists() returns true for a deleted file in a long-lived FPM worker (PHP 8.5.2, open_basedir enabled) — stat()/is_file()/scandir() disagree

Ouverte
#23,544 2 commentaires 0 réactions 0 personnes assignées Voir sur GitHub

Personne n'a encore pris cette issue.

Bug Status: Needs Triage
Langage dominant
C
Étoiles
40.4k
Forks
8.1k
Merge moyen
2 j 13 h
PR mergées (30 j)
96

Description

Description

In a long-lived PHP-FPM worker, after a file has been created, required and then unlink()ed (the WordPress .maintenance cycle), file_exists() keeps returning true for it, while is_file(), stat(), realpath(), fopen() and scandir() all report the file as gone. clearstatcache(true, $path) has no effect; the stale answer survives until the worker process exits. Creating the file again (even empty) clears the state.

The trigger in production is WordPress' updater: it writes ABSPATH/.maintenance, core requires it on every request in wp_is_maintenance_mode(), and the updater unlink()s it when done. Afterwards, some workers still answer file_exists()true, WordPress does require '.maintenance' and dies with Failed opening required '.../.maintenance'.

The following code, run over HTTP in an affected worker while the file is absent on disk:

<?php
header('Content-Type: text/plain');
$m = __DIR__ . '/.maintenance';
clearstatcache(true, $m);
var_dump(file_exists($m));
var_dump(is_file($m));
var_dump(@stat($m));
var_dump(realpath($m));
var_dump(@fopen($m, 'r'));
var_dump(in_array('.maintenance', scandir(__DIR__), true));
stream_wrapper_restore('file');
var_dump(file_exists($m));
echo shell_exec('test -e ' . escapeshellarg($m) . ' && echo EXISTS || echo MISSING');

Resulted in this output:

bool(true)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(true)
MISSING

But I expected this output instead:

bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
bool(false)
MISSING

The shell_exec() child inherits uid, cwd and mount namespace from the worker and confirms the file does not exist, so the kernel is consistent and the true originates inside the PHP process.

Ruled out
  • Filesystem/caching layers: FPM worker and an SSH shell are on the same host (gethostname()), same mount namespace (/proc/self/ns/mnt identical), docroot on a local ext4 mount. From the shell, os.access(path, F_OK) and os.path.exists(path) both return False.
  • OPcache disabled.
  • Custom stream wrappers / auto_prepend_file: only built-in wrappers registered, stream_wrapper_restore('file') changes nothing, auto_prepend_file empty.
  • Realpath cache TTL: state persists far beyond realpath_cache_ttl=120 with no requests in between.
  • Same worker across requests verified with getmypid().
  • PHP 8.4.17 (fpm-fcgi) on the same host and pool configuration: not reproducible; downgrading the pool fixed the issue.
Relevant configuration
  • open_basedir set for the pool (docroot, session and tmp dirs, /tmp:/usr/bin)
  • realpath_cache_ttl=120
  • disable_functions: pcntl_*, leak, dl, stream_socket_server, stream_socket_sendto
  • Extensions: Core, date, lexbor, openssl, pcre, zlib, filter, hash, json, uri, Zend OPcache, random, Reflection, SPL, session, standard, sodium, libxml, cgi-fcgi, apcu, bcmath, bz2, calendar, ctype, curl, dba, dom, fileinfo, ftp, gd, gmp, gettext, iconv, igbinary, imagick, imap, intl, mbstring, exif, msgpack, memcached, mysqlnd, mysqli, pgsql, sqlite3, PDO, pdo_mysql, pdo_pgsql, pdo_sqlite, Phar, posix, redis, shmop, SimpleXML, sockets, sysvmsg, sysvsem, sysvshm, tokenizer, xml, xmlreader, xmlwriter, xsl, soap, zip
PHP Version
8.5.2 fpm-fcgi
Operating System

Debian GNU/Linux 12 (bookworm), kernel 4.19.0-27-amd64, Apache + PHP-FPM

Guide de contribution

Ouvrir le guide de contribution

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.

Piste de recherche

Commencez par reproduire la requête HTTP dans un PHP-FPM worker de longue durée avec open_basedir activé, en suivant la séquence file_exists(), clearstatcache(), unlink(), stat(), realpath(), fopen() et scandir() du rapport. Comparez PHP 8.5.2 à 8.4.17 et confirmez que le fix fait que file_exists() renvoie false après la suppression sans que le worker ait besoin de se terminer.

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

Évaluation

Stack technique
c, php
Domaine
backend, operating-systems
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Active
Clarté
Plutôt claire
Accessibilité débutants
48/100

Recevez les nouvelles issues par e-mail

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