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>
41 lines
2.0 KiB
PHP
41 lines
2.0 KiB
PHP
<?php
|
|
/**
|
|
* cac-lsphp $_SERVER path normaliser (auto_prepend) — DEGRADED FALLBACK ONLY.
|
|
*
|
|
* SUPERSEDED by the cac_path_parity PHP extension (ext/cac-path-parity/), which
|
|
* does this from RINIT where a customer's .user.ini cannot displace it. The
|
|
* entrypoint only wires this file up when that extension is not loadable in the
|
|
* running image, and logs a WARNING when it does. Do not extend this script —
|
|
* fix the extension instead.
|
|
*
|
|
* It is kept because the flaw documented at the bottom of this docblock is
|
|
* exactly why the extension exists, and because an image where the extension
|
|
* failed to load should degrade to the old behaviour rather than to nothing.
|
|
*
|
|
* The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
|
|
* so OLS sends lsphp $_SERVER['DOCUMENT_ROOT'] / ['SCRIPT_FILENAME'] under
|
|
* /mnt/users/<user>/<domain>/... . The sidecar symlinks that back to the real
|
|
* /home/<user> mount, so file operations resolve and PHP's own __FILE__/__DIR__/
|
|
* realpath()/getcwd() already report /home/<user>/public_html. But the RAW env
|
|
* strings OLS set still read /mnt/users, which would leak to the (uncommon) apps
|
|
* that build or compare paths from $_SERVER['DOCUMENT_ROOT'].
|
|
*
|
|
* Canonicalise those two via realpath() so cac-lsphp is byte-for-byte 1:1 with
|
|
* cac-fpm/cac-litespeed (where DOCUMENT_ROOT is natively /home/<user>/public_html).
|
|
* Cheap (two realpath calls, cached by realpath_cache) and side-effect-free.
|
|
*
|
|
* Customer sites have no auto_prepend by default, so this is the only prepend in
|
|
* play. If a site sets its own auto_prepend_file via .user.ini it overrides this
|
|
* (theirs wins) — acceptable: paths still resolve via the symlink, only the raw
|
|
* string differs.
|
|
*/
|
|
foreach (array('DOCUMENT_ROOT', 'SCRIPT_FILENAME') as $__cl_key) {
|
|
if (!empty($_SERVER[$__cl_key]) && strncmp($_SERVER[$__cl_key], '/mnt/users/', 11) === 0) {
|
|
$__cl_real = realpath($_SERVER[$__cl_key]);
|
|
if ($__cl_real !== false) {
|
|
$_SERVER[$__cl_key] = $__cl_real;
|
|
}
|
|
}
|
|
}
|
|
unset($__cl_key, $__cl_real);
|