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:
@@ -82,18 +82,48 @@ awk '
|
||||
} >> "$TMP"
|
||||
|
||||
## --- 4. emit per-site vhost stanzas + collect listener map lines ---
|
||||
##
|
||||
## First value of KEY= in a site.meta, as plain data. This replaces
|
||||
## `sed -n 's/^KEY=//p' "$meta" | head -1`, which was a pipeline whose reader
|
||||
## (`head -1`) exits after one line while the writer (`sed`) may still be
|
||||
## flushing: the writer then dies 141, and `set -euo pipefail` (line 22) makes
|
||||
## the whole ASSIGNMENT fail, which aborts this script mid-render. A truncated
|
||||
## httpd_config.conf is never written (the render is atomic), but the effect is
|
||||
## that a site the panel just provisioned silently never appears in the config
|
||||
## and every subsequent render fails the same way.
|
||||
##
|
||||
## Measured in this image, `sed -n 's/^DOMAINS=//p' | head -1`:
|
||||
## 400 matching lines (~6 KB of sed output) -> 0 0 0 0 0
|
||||
## 6000 matching lines (~90 KB of sed output) -> 141 141 141
|
||||
## The threshold is the PIPE CAPACITY, not "is the file small": while the
|
||||
## writer's whole output fits, it never blocks and always finishes first;
|
||||
## once it does not, the reader's early exit is a guaranteed SIGPIPE. Linux
|
||||
## gives a pipe 64 KiB by default but drops NEW pipes to a single page once a
|
||||
## user passes fs.pipe-user-pages-soft, which is the state a busy host gets
|
||||
## into — and the reason a 40 KB probe failed 5/5 on whp02 and 0/10 here.
|
||||
## So "a site.meta would never be that big" is not a bound worth resting on
|
||||
## for panel-written input we do not validate.
|
||||
##
|
||||
## awk reads the FILE directly and stops at the first hit: no pipeline, so
|
||||
## nothing for pipefail to adopt. Same semantics as before, verified against
|
||||
## the old form on duplicate keys, decoy keys (`notVHNAME=`), empty values and
|
||||
## missing keys: first match wins, the rest of the line is the value, verbatim.
|
||||
meta_value() {
|
||||
awk -v k="$1" 'index($0, k "=") == 1 { print substr($0, length(k) + 2); exit }' "$2"
|
||||
}
|
||||
|
||||
maps=""
|
||||
site_count=0
|
||||
for meta in "$SITES_ROOT"/*/site.meta; do
|
||||
[ -e "$meta" ] || continue
|
||||
sdir=$(dirname "$meta")
|
||||
## PARSE site.meta with sed — do NOT `source` it. The panel writes these values
|
||||
## EXTRACT from site.meta — do NOT `source` it. The panel writes these values
|
||||
## (derived from DB domains), so they should be safe, but sourcing paneldata as
|
||||
## shell would execute any metacharacters as root in this container if a value
|
||||
## ever slipped validation. sed extraction treats them as plain data.
|
||||
VHNAME=$(sed -n 's/^VHNAME=//p' "$meta" | head -1)
|
||||
VHROOT=$(sed -n 's/^VHROOT=//p' "$meta" | head -1)
|
||||
DOMAINS=$(sed -n 's/^DOMAINS=//p' "$meta" | head -1)
|
||||
## ever slipped validation. meta_value treats them as plain data.
|
||||
VHNAME=$(meta_value VHNAME "$meta")
|
||||
VHROOT=$(meta_value VHROOT "$meta")
|
||||
DOMAINS=$(meta_value DOMAINS "$meta")
|
||||
if [ -z "$VHNAME" ] || [ -z "$VHROOT" ] || [ -z "$DOMAINS" ] || [ ! -f "$sdir/vhconf.conf" ]; then
|
||||
echo "render-shared-ols: skipping $sdir (incomplete: VHNAME/VHROOT/DOMAINS/vhconf.conf)" >&2
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user