Three non-blocking findings from the review of 8790b02. The fix itself is
unchanged in intent; this makes the reasoning around it true, removes a
regression the fix introduced on the boot path, and stops the new test from
under-reporting.
F1 — the shipped comments explained the bug wrongly, and a wrong rule is what
the next maintainer reasons from. entrypoint-lsphp.sh and
render-shared-ols-config.sh both said the race is decided by PIPE CAPACITY:
"while the output fits the pipe the writer always wins; once it doesn't, SIGPIPE
is guaranteed." Both halves are refuted by measurement against a default
65536-byte pipe (F_GETPIPE_SZ):
41144 bytes -> 141 in 32/300 runs (11%) — well UNDER capacity
65012 bytes -> 141 in 25/30 runs — not certain even AT capacity
500 KB into a 1 MiB pipe -> 200/200 with 4096-byte writes, 0/200 with one
500 KB write
and strace caught printf dying having written 12086 of 40406 bytes into a
65536-byte pipe. The mechanism is a race on whether the reader closes before the
writer's final write() returns; capacity only modulates how many syscalls the
writer needs. What actually separated whp02 (5/5 failures) from a dev container
(10/10 clean) is the WRITER's syscall size: bash <= 5.2.15 writes ~37 KB at a
time, bash >= 5.2.21 writes 80-160 bytes. The fs.pipe-user-pages-soft aside was
also wrong: it clamps to two pages not one, needs one uid holding >1024 pipes,
and is skipped for CAP_SYS_RESOURCE.
Both blocks now state the rule that is actually true — any
`writer | early-exiting-reader` under pipefail is a latent 141; payload size is
not a safety argument; the only sound reasons a call site is safe are structural
(no pipefail, reader provably reads to EOF, or the status is discarded) — and
the same correction is applied to the three other comments that leaned on size
(`ols_running` x2, fpm-parity-check.sh's pre-flight). Nor is the reader's
implementation a defence: at 248 KB, mawk, gawk, `grep -q` and `head -1` all
gave 141 on 10/10, and these images already differ (mawk 1.3.4 vs gawk 5.2.1).
Comment-only; the test file's own section-4 output no longer contradicts the
prose next to it.
F2 — `<<<` added a writable-temp-dir precondition to the boot path. Above a
build-dependent size bash materialises a here-string as /tmp/sh-thd.XXXXXX
(measured switch: 65536 in this image's bash 5.2.21, and Debian's 5.2.15
switches between 4096 and 16384, where a ~40 KB `lsphp -i` WOULD spill). On a
bare assignment a temp file it cannot create is `set -e` killing PID 1 — the
exact failure this branch exists to remove, re-acquired from a different
direction and gated on which bash the base image ships. In cac-lsphp:f1f2f3
under `docker run --read-only`, same payload, same statement shape:
OLD (here-string) : bash: cannot create temp file for here-document
-> exit 1, script dead
NEW (pure bash) : REACHED NEXT STATEMENT, SCAN=[…/mods-available/], exit 0
So the boot-critical sites — the three probe helpers in entrypoint-lsphp.sh and
the SCAN_DIR extraction in entrypoint-litespeed.sh — now match with `[[ ]]` and
parameter expansion, which allocate nothing. The non-boot sites keep their
here-strings and say why at the call site: `ols_running` in both OLS entrypoints
(`lswsctrl status` is under 100 bytes, orders below any spill threshold) and
fpm-parity-check.sh's `php-fpm -m` pre-flight (~1 KB, in a harness that has
already written a docroot and a pool config).
Matching semantics are preserved, not approximated: the anchored whole-line
grep becomes a glob over a subject wrapped in newlines at BOTH ends (so first
and unterminated-last lines still match), and awk's `-F'=> ' {print $2; exit}`
becomes first-matching-line then the text between the FIRST and SECOND
separator. Section 6 of the test asserts that against the original grep/awk
patterns reading a FILE — 24 cases incl. trailing-space, prefix decoys, CRLF,
a second separator, an empty value, two candidate lines, glob metacharacters in
the body, and the full 40 KB fixture. Mutations verify the assertions bite:
dropping the trailing-newline wrap fails 3 cases, taking the whole rest of the
line fails "second separator", `##` instead of `#` fails "first of two wins",
dropping the `^` anchor on the banner fails "banner not at line start".
F3 — the structural scan missed shapes it implied it caught, and the extractor
was unbounded.
* `grep -l`/`-L`/`--quiet`/`--files-with-matches`, `-im1`-style clusters, a
bare `head` before `;`, and `sed q` / `sed 'q'` / `sed 2q` / `sed '$q'` were
all invisible. grep is now walked option by option the way grep reads them
(so `grep -eq foo` stays the pattern "q", not --quiet), and the sed test
reads the script with quote characters stripped but their contents kept.
Replaying the old regexes against the new fixtures: 10 shapes missed and 2
false positives (`sed s/a/q/`, `grep -eq foo`) — both now correct.
* new section 7 pins that coverage from both sides: 19 early-exit shapes must
be reported, 18 read-to-EOF / quoted / non-pipeline forms must not. Without
it the scan's regexes are unfalsified and can quietly stop matching, which
is precisely how `grep -l` and `sed q` stayed missing.
* the helper extraction is bounded. It buffers and emits nothing until it has
seen the END marker (exit 4 = BEGIN without END, exit 3 = no markers), so a
half-deleted pair is a marker error instead of a slurp. Measured on this
entrypoint with the END marker removed: the old extractor produced 301 lines
including `mkdir -p "$SCAN_DIR"` and three `rm -f "$SCAN_DIR/…"` — which the
harness then sourced and ran. It failed loudly last time only because `set
-u` happened to trip two statements in. The new one emits 0 bytes and says
what is wrong.
* the stated scope limits now include what remains: the reader list is an
enumeration, not a proof (nothing knows about `perl -ne … last`, `jq`,
`head -c`), and only the first word after a pipe is inspected.
Verified: PHP 8.3 `--no-cache` build exit 0, 10/10 .phpt; cac-lsphp boots and
logs `path parity = extension` with `Rewriting => active` and .from/.to
populated from the rendered ini; cac-litespeed boots, resolves SCAN_DIR and
writes 99-user-error-log.ini, OLS reports "running with PID", /healthz 200. The
FPM parity harness — never executed by the previous review because no cac-fpm
image existed locally — was built (Dockerfile.fpm, PHPVER=83), the extension
compiled inside it, and it reports 9/9 ALL PASS, exit 0. The new test exits 0
here and exits 1 against a `git archive 9343a56` export naming all 9 offending
lines. `bash -n` clean repo-wide; `shellcheck -S warning` clean on the CI set;
`-S style` is byte-identical to before this commit (5 pre-existing info-level
findings, 0 added — the earlier report's claim of `-S style` clean was wrong).
No `.c`/`.h` file touched and the C fail-open invariant grep is still empty.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
140 lines
5.2 KiB
Bash
140 lines
5.2 KiB
Bash
#!/usr/bin/env bash
|
|
## entrypoint-shared-ols.sh — PID 1 for the shared-ols tier.
|
|
##
|
|
## One OpenLiteSpeed container fronting MANY tenants' detached cac-lsphp
|
|
## sidecars (the OLS analogue of the shared-httpd container). Webserver ONLY —
|
|
## it runs NO PHP locally (render-shared-ols-config.sh strips the stock local
|
|
## lsphp; every site's PHP goes to its own sidecar over LSAPI). HAProxy stays
|
|
## the TLS/WAF/SNI edge and routes OLS-type hostnames here on :443.
|
|
##
|
|
## Reuses cac-litespeed's hard-won DAEMON-MODE supervision (NOT `openlitespeed
|
|
## -n` + wait): OLS self-restarts on QUIC.cloud IP refresh would otherwise exit
|
|
## PID 1 cleanly and tear the container down. See entrypoint-litespeed.sh and
|
|
## feedback_ols_quiccloud_restart_kills_container.
|
|
set -euo pipefail
|
|
|
|
: "${environment:=PROD}"
|
|
export CONTAINER_ROLE="shared_ols"
|
|
|
|
LSWS_CONF=/usr/local/lsws/conf
|
|
CERT_DIR="$LSWS_CONF/cert"
|
|
HEALTH_DIR=/usr/local/lsws/shared-ols-health
|
|
export SITES_ROOT="${SITES_ROOT:-$LSWS_CONF/shared-sites}"
|
|
export LSCACHE_ROOT="${LSCACHE_ROOT:-/var/lscache}"
|
|
export CERT_FILE="$CERT_DIR/shared-ols.crt"
|
|
export KEY_FILE="$CERT_DIR/shared-ols.key"
|
|
|
|
mkdir -p "$SITES_ROOT" "$LSCACHE_ROOT" "$CERT_DIR" "$HEALTH_DIR/html"
|
|
|
|
## ---- self-signed cert for the :443 listener (HAProxy verifies none) ----
|
|
if [ ! -f "$CERT_FILE" ]; then
|
|
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
|
-keyout "$KEY_FILE" -out "$CERT_FILE" -subj "/CN=shared-ols" 2>/dev/null
|
|
fi
|
|
|
|
## ---- health vhost (catch-all): valid server with zero customer sites +
|
|
## answers HAProxy health checks that hit by IP / unknown Host with a 200 ----
|
|
cat > "$HEALTH_DIR/vhconf.conf" <<'EOF'
|
|
docRoot $VH_ROOT/html
|
|
enableScript 0
|
|
context / {
|
|
allowBrowse 1
|
|
location $DOC_ROOT/
|
|
}
|
|
EOF
|
|
printf 'ok\n' > "$HEALTH_DIR/html/healthz"
|
|
printf 'shared-ols\n' > "$HEALTH_DIR/html/index.html"
|
|
|
|
## ---- ownership: OLS reads conf/ as lsadm. chown the base conf dir + health dir
|
|
## NON-recursively (the per-site files under conf/shared-sites are written by the
|
|
## panel and are world-readable; a recursive chown here would be O(N-sites) on
|
|
## every container (re)start, delaying first-listen after a crash). The render
|
|
## script chowns the httpd_config.conf it produces. ----
|
|
chown lsadm:nogroup "$LSWS_CONF" "$HEALTH_DIR" "$HEALTH_DIR/html" 2>/dev/null || true
|
|
chown lsadm:nogroup "$HEALTH_DIR/vhconf.conf" "$HEALTH_DIR/html/healthz" "$HEALTH_DIR/html/index.html" 2>/dev/null || true
|
|
|
|
## ---- assemble httpd_config.conf from the panel's per-site files ----
|
|
/scripts/render-shared-ols-config.sh
|
|
|
|
## ---- stream OLS logs to PID-1 stdout (follows across restarts) ----
|
|
mkdir -p /usr/local/lsws/logs
|
|
touch /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log
|
|
tail -F /usr/local/lsws/logs/error.log /usr/local/lsws/logs/access.log 2>/dev/null &
|
|
|
|
## ---- .htaccess watcher (required; spec 5.3). Background; the panel monitors
|
|
## that it stays alive (its death silently stops rewrite changes applying). ----
|
|
/scripts/ols-htaccess-watcher.sh &
|
|
WATCHER_PID=$!
|
|
|
|
## ---- supervise OLS in DAEMON mode (verbatim model from entrypoint-litespeed.sh) ----
|
|
STOP_REQUESTED=0
|
|
term_handler() {
|
|
STOP_REQUESTED=1
|
|
kill "$WATCHER_PID" 2>/dev/null || true
|
|
/usr/local/lsws/bin/lswsctrl stop >/dev/null 2>&1 || true
|
|
}
|
|
trap term_handler TERM INT
|
|
|
|
## Variable + here-string, not a pipe into `grep -qi` — see the long note on the
|
|
## identical function in entrypoint-litespeed.sh: `grep -q` closing the pipe on
|
|
## a match can leave the writer dying 141, and `set -o pipefail` (line 14) turns
|
|
## that into "OLS is down" *because* the running line matched. The reason is
|
|
## structural (a pipefail script must not pipe into an early-exit reader), not
|
|
## that this particular output is small; and the here-string is safe here for
|
|
## the separate reason that `lswsctrl status` is far below the size at which
|
|
## bash spills a here-string to a temp file. A non-zero `lswsctrl` still counts
|
|
## as not running, as pipefail made it count before.
|
|
ols_running() {
|
|
local st
|
|
st=$(/usr/local/lsws/bin/lswsctrl status 2>/dev/null) || return 1
|
|
grep -qi 'running with pid' <<<"$st"
|
|
}
|
|
|
|
MAX_STARTS=5
|
|
WINDOW=60
|
|
starts=""
|
|
|
|
start_ols() {
|
|
/usr/local/lsws/bin/lswsctrl start >/dev/null 2>&1 || true
|
|
for _ in $(seq 1 20); do
|
|
ols_running && return 0
|
|
sleep 0.5
|
|
done
|
|
return 1
|
|
}
|
|
|
|
if ! start_ols; then
|
|
echo "entrypoint-shared-ols: OLS failed to start (not running after 10s)." >&2
|
|
exit 1
|
|
fi
|
|
echo "entrypoint-shared-ols: OLS started in daemon mode — $(/usr/local/lsws/bin/lswsctrl status 2>/dev/null || true)"
|
|
|
|
while true; do
|
|
if ols_running; then
|
|
sleep 3
|
|
continue
|
|
fi
|
|
sleep 2
|
|
if [ "$STOP_REQUESTED" -eq 0 ] && ols_running; then
|
|
continue
|
|
fi
|
|
if [ "$STOP_REQUESTED" -eq 1 ]; then
|
|
echo "entrypoint-shared-ols: SIGTERM received, OLS stopped — exiting."
|
|
exit 0
|
|
fi
|
|
now=$(date +%s)
|
|
starts="$starts $now"
|
|
pruned=""
|
|
for t in $starts; do
|
|
[ $((now - t)) -lt "$WINDOW" ] && pruned="$pruned $t"
|
|
done
|
|
starts="$pruned"
|
|
n=$(echo $starts | wc -w)
|
|
echo "entrypoint-shared-ols: OLS not running — relaunching (attempt $n/$MAX_STARTS within ${WINDOW}s)." >&2
|
|
if [ "$n" -ge "$MAX_STARTS" ]; then
|
|
echo "entrypoint-shared-ols: OLS crash-looping — bailing for Docker restart policy / monitor." >&2
|
|
exit 1
|
|
fi
|
|
start_ols || true
|
|
done
|