fix(lsphp): stop SIGPIPE+pipefail reporting the parity extension as missing
`entrypoint-lsphp.sh` decided whether cac_path_parity was loaded with
printf '%s\n' "$LSPHP_INFO" | grep -q '^cac_path_parity support => enabled$'
under `set -euo pipefail`. `grep -q` exits on its first match; printf is still
writing the remaining ~40 KB of `lsphp -i`, takes SIGPIPE, exits 141, and
pipefail prefers 141 over grep's 0. The branch therefore evaluated FALSE
*because the extension was present* — present early enough to stop the reader —
and every affected container fell back to the auto_prepend normaliser that a
customer's own .user.ini silently displaces, i.e. the exact failure the
extension exists to remove. Measured on whp02 against the published
cac-lsphp:php83: 5/5 runs status=141 with pipefail, 0 without.
The race is decided by pipe capacity, which is why it reproduced on whp02 and
not on other daemons: while the payload fits the pipe the writer never blocks
and always finishes first. Forced over the limit it is deterministic — 3x the
same `lsphp -i` (122100 bytes) gives 141 every time in the built image.
Fixed by reading with here-strings, which are not pipelines at all, so there is
no second exit status for pipefail to adopt. Same grep/awk patterns; plumbing
only. Same class fixed everywhere it existed under pipefail:
* entrypoint-lsphp.sh parity probe, and the SCAN_DIR awk probe
* entrypoint-litespeed.sh SCAN_DIR probe (a bare assignment: 141 there does
not degrade, `set -e` kills PID 1), and ols_running
* entrypoint-shared-ols.sh ols_running
* render-shared-ols-config.sh site.meta parsing (`sed | head -1`): measured
141 at 6000 duplicate keys, which under `set -e`
aborts the whole render
* fpm-parity-check.sh the `php-fpm -m` pre-flight, whose whole job is to
stop a harness fault being blamed on the extension
Also: the fallback used to announce "cac_path_parity extension not loadable in
this image" for every reason the branch was reached, including its own plumbing
breaking — a false diagnosis that sends operators to rebuild a good image whose
build gate passed. Verdicts now carry the evidence they rest on, and a probe
that produced nothing is reported as a probe failure that establishes nothing
about the image. Fail-open posture is unchanged: no probe failure is fatal.
Adds scripts/tests/lsphp-info-probe.test.sh, which runs the shipped probes
(extracted verbatim, so they cannot drift from what runs in production) under
`set -euo pipefail` against a realistic ~40 KB phpinfo body, and statically
outlaws the shape repo-wide. Against trunk it fails, naming all 9 offending
lines. Wired into CI as a new Shell-Checks job, because no existing gate ever
executed the entrypoint's branch logic — the .phpt suite and the Dockerfile's
own `lsphp -i | grep -q` probe (which has no pipefail) were both green for the
release whose entrypoint declared that same extension missing.
Verified: PHP 8.3 --no-cache build green, 10/10 .phpt, 9/9 FPM harness; the
built image logs `path parity = extension` and reports `Rewriting => active`
with .from/.to populated; ext-removed and probe-broken variants each produce
their own honest message and still start.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -60,13 +60,28 @@ command -v cgi-fcgi >/dev/null || { echo "SKIP: cgi-fcgi not installed (apt inst
|
||||
[ -n "$FPM_BIN" ] && [ -x "$FPM_BIN" ] || { echo "SKIP: php-fpm not found (pass it as \$2)"; exit 0; }
|
||||
[ -f "$EXT_SO" ] || { echo "SKIP: $EXT_SO not built (run phpize && ./configure && make)"; exit 0; }
|
||||
|
||||
echo "php-fpm: $FPM_BIN ($("$FPM_BIN" -n -v 2>/dev/null | head -1))"
|
||||
## `${VAR%%$'\n'*}` rather than `| head -1`: same first line, no pipeline, so
|
||||
## nothing here can be decided by a SIGPIPE race under the pipefail on line 39.
|
||||
## This one only ever fed an echo, so it could not have misled anyone — it is
|
||||
## changed so that "no pipefail script in this repo pipes into an early-exit
|
||||
## reader" stays a rule with no exceptions to remember.
|
||||
FPM_VERSION=$("$FPM_BIN" -n -v 2>/dev/null || true)
|
||||
echo "php-fpm: $FPM_BIN (${FPM_VERSION%%$'\n'*})"
|
||||
echo "extension: $EXT_SO"
|
||||
|
||||
## Pre-flight. If the .so will not load into THIS php-fpm (PHP API mismatch is
|
||||
## the usual cause) every assertion below would fail identically and blame the
|
||||
## extension's logic. Say what actually happened instead.
|
||||
if ! "$FPM_BIN" -n -d "extension=$EXT_SO" -m 2>/dev/null | grep -qx 'cac_path_parity'; then
|
||||
## Captured into a variable and matched with a here-string, not piped into
|
||||
## `grep -qx`. `grep -q` exits on its first match, and with `set -o pipefail`
|
||||
## (line 39) a writer still writing at that moment dies 141 and the pipeline
|
||||
## reads FALSE — announcing "cannot load the extension" *because* the extension
|
||||
## was listed. `php-fpm -m` is ~1 KB and loses that race only rarely, but this
|
||||
## pre-flight exists precisely to stop a harness malfunction being reported as
|
||||
## an extension fault, so it must not have one of its own. (The same construct
|
||||
## on 40 KB of `lsphp -i` is what broke entrypoint-lsphp.sh in production.)
|
||||
FPM_MODULES=$("$FPM_BIN" -n -d "extension=$EXT_SO" -m 2>/dev/null || true)
|
||||
if ! grep -qx 'cac_path_parity' <<<"$FPM_MODULES"; then
|
||||
echo "HARNESS FAILURE: $FPM_BIN cannot load $EXT_SO" >&2
|
||||
"$FPM_BIN" -n -d "extension=$EXT_SO" -m 2>&1 | grep -i 'unable\|warning\|error' >&2
|
||||
echo " The .so must be built against the same PHP as this php-fpm binary." >&2
|
||||
@@ -156,6 +171,10 @@ run_case() {
|
||||
local pid=$! out=""
|
||||
for _ in $(seq 1 40); do
|
||||
sleep 0.15
|
||||
## SC1007: `QUERY_STRING=` IS the intent — an empty FastCGI param in the
|
||||
## per-command environment prefix, exactly as a webserver sends it for a
|
||||
## URL with no query string. Not a truncated assignment.
|
||||
# shellcheck disable=SC1007
|
||||
out=$(SCRIPT_FILENAME="$DOCROOT/probe.php" DOCUMENT_ROOT="$DOCROOT" \
|
||||
SCRIPT_NAME=/probe.php REQUEST_METHOD=GET QUERY_STRING= \
|
||||
cgi-fcgi -bind -connect "127.0.0.1:$PORT" 2>/dev/null)
|
||||
|
||||
Reference in New Issue
Block a user