The artifact cited as the web-SAPI evidence could not have been run as it stood.
Measured in an official php:8.3-fpm container with the extension built in place:
- as shipped, no args: "SKIP: php-fpm not found", exit 0.
The default was `php-fpm8.3`, which matches neither the official images
(`php-fpm`) nor this repo's images.
- with the binary supplied by hand: 9 FAIL, every one with an empty `got:`.
The generated pool had no user/group, so php-fpm refused to start as root
("please specify user and group other than root"). A startup failure was
wearing the costume of nine parity bugs.
Changes:
- auto-detect the binary (php-fpm, php-fpm8.N, /usr/local/sbin, /usr/sbin) and
print which one was chosen plus its version;
- pre-flight the extension with `php-fpm -m`, so a .so that will not load into
THIS php-fpm reports as a harness failure naming the ABI mismatch rather
than as nine wrong paths;
- emit user/group in the pool when running as root, resolved from accounts
that actually exist (www-data / nobody / daemon), and chmod the fixture tmpdir
so the non-root worker can read it;
- run_case() now returns non-zero when php-fpm never answered, and every call
site routes that to die_startup(), which prints the php-fpm output and the
pool error_log and exits 2 — an exit code deliberately distinct from 1
(assertion failure).
After: 9/9 ALL PASS from a clean checkout with no arguments and no environment
fixing, running as root in php:8.3-fpm. Mutation-tested both new paths: a pool
user that does not exist reports "HARNESS FAILURE ... STARTUP/environment
failure" with the real php-fpm error and exit 2; an EXT_SO that is not a loadable
extension is caught by the pre-flight, also exit 2.
Also fixes doc drift: 001-rewrite.phpt pointed at tests/web-sapi-parity-check.sh,
which has never existed. The file it means is tests/fpm-parity-check.sh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A site moved from cac-fpm to cac-lsphp must see byte-identical
$_SERVER['DOCUMENT_ROOT'] and ['SCRIPT_FILENAME'] (/home/<user>/...).
The auto_prepend_file normaliser that did this was PHP_INI_PERDIR, so
any site with its own .user.ini auto_prepend_file silently displaced it
— the state 7 live shared_ols sites (Wordfence, cPanel imports) are
actually in. Hardening the hook was not an option either: making our
prepend win would have disabled those Wordfence WAFs.
Replace it with cac_path_parity, a small PHP extension that rewrites the
filesystem-path $_SERVER keys from RINIT. RINIT cannot be displaced by
.user.ini, and it occupies no userland hook, so the customer's own
auto_prepend_file stays the only prepend in play and keeps working. The
mapping lives in two PHP_INI_SYSTEM settings, which .user.ini (PERDIR /
USER only) and ini_set() cannot reach.
Mechanism is a path-component-bounded string prefix swap, not realpath():
byte-identical to cac-fpm by construction (realpath would resolve a
customer's own symlinked public_html to some third path), no syscall, and
no failure path. Every guard fails open and leaves $_SERVER untouched;
nothing here can warn, throw or 500 a site. Unconfigured it is fully
inert, so cac-fpm and cac-litespeed are unaffected.
Built in a separate Dockerfile stage keyed off the existing ARG PHPVER —
gcc/phpize/headers never reach the shipped image (verified absent; the
image grows ~155kB), and a base-image PHP bump recompiles with no human
step. A `lsphp -i | grep` assertion fails the build if the .so does not
load, so an image can never ship having silently lost parity.
The entrypoint selects the extension when present and removes any stale
prepend ini left by an older image; if the extension is somehow not
loadable it falls back to the old normaliser and logs a WARNING rather
than losing normalisation entirely. It also now logs the active parity
mode, and warns when lsphp reports no ini scan dir (previously silent).
Probe lsphp with `-i` only: it is the LSAPI SAPI, not the CLI, and
answers `-m`/`-r` by printing usage and exiting 0 — a `lsphp -m | grep`
check never matches and never errors, which is the exact class of silent
always-false assertion this change exists to remove.
Verified: 6 .phpt tests; tests/fpm-parity-check.sh proves under the FPM
SAPI that with a customer .user.ini auto_prepend_file present both keys
are still corrected AND the customer's prepend still runs, and that the
old mechanism does not; and in a real built cac-lsphp:php83 container
that SCRIPT_FILENAME is rewritten, the customer prepend still fires, and
another tenant's path is left untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>