For when a PHP security release is out and the LiteSpeed apt repo lags (2026-10-21: php2shell CVE-2026-109618/-109619 fixed in 8.2.35/8.3.36/ 8.4.27/8.5.12, LiteSpeed still on 8.2.34/8.3.35/8.4.26/8.5.11). Dockerfile.lsphp / Dockerfile.litespeed gain an `lsphp-src` stage that is idle unless PHP_SRC_VERSION is set; normal builds are unchanged (measured: module list, file manifest, ldd, ini and dpkg identical to the published images). When set (lsphp-src-build.sh): - tarball sha256 (php.net releases JSON) + OpenPGP signature against the committed php.net keyring, signer pinned per branch - configure flags replayed from the packaged lsphp's own Configure Command - LiteSpeed's php-litespeed-8.3 SAPI (what the packages actually compile, measured) instead of php-src's sapi/litespeed; sha256-pinned and checked against the packaged binary's LSAPI_* names - packaged compiled-in ini defaults reproduced (session.save_path on 8.2/ 8.3) and gated (`php -n` diff) - ABI gate (lsphp-abi-gate.sh, pahole) before reusing LiteSpeed's packaged PECL .so files; regression-tested in Shell-Checks lsphp-src-overlay.sh swaps only php-src-owned files (decided by dpkg) and fails the build unless extensions, versions, effective ini and feature lines match the packaged lsphp under both CLI and litespeed SAPIs. CI: lsphp-source-emergency.yaml (workflow_dispatch only). build-push.yaml refuses to overwrite a newer php.net image with an older packaged one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
203 lines
11 KiB
Bash
Executable File
203 lines
11 KiB
Bash
Executable File
#!/bin/bash
|
|
## lsphp-src-overlay.sh — BUILD-TIME ONLY, EMERGENCY PATH. Applies the php.net
|
|
## build produced by lsphp-src-build.sh (staged in /lsphp-src-out by the
|
|
## `lsphp-src` stage) over the LiteSpeed-packaged lsphp${PHPVER} that this stage
|
|
## has just installed, then PROVES the result offers the same support.
|
|
##
|
|
## Usage: lsphp-src-overlay.sh runtime|dev PHPVER [OUT_DIR=/lsphp-src-out]
|
|
## runtime — the shipped stage: replace the php-src binaries (lsphp, php,
|
|
## php8.M, phar) and every php-src shared extension the packages
|
|
## installed. Everything else stays exactly as packaged: php.ini,
|
|
## mods-available/*.ini, the LiteSpeed PECL .so files (see
|
|
## lsphp-src-build.sh for why), helper scripts, man pages.
|
|
## dev — Dockerfile.lsphp's ext-build stage: runtime + the -dev files
|
|
## (headers, phpize, php-config, lib/php/build) so cac_path_parity is
|
|
## compiled against, and its .phpt suite run on, the php.net build.
|
|
##
|
|
## PHP_SRC_VERSION empty and no staged build => no-op (normal builds).
|
|
##
|
|
## WHAT IS REPLACED is decided by dpkg, not by a list kept here: a file is
|
|
## php-src output iff it is owned by an installed lsphp${PHPVER}* package whose
|
|
## version equals lsphp${PHPVER}'s (core, -common, -curl, -intl, -ldap, -mysql,
|
|
## -opcache, -dev ...; the PECL packages carry their own versions). Every ELF
|
|
## file among those MUST have a php.net counterpart — a missing one fails the
|
|
## build, so a new LiteSpeed sub-package can never keep an old binary silently.
|
|
##
|
|
## THE "SAME SUPPORT" GATE (all fatal): lsphp-src-probe.php is run under both
|
|
## SAPIs before and after; the loaded extension set (incl. Zend extensions) must
|
|
## be identical, every PECL extension must report its same version, every
|
|
## php-src extension the new version, no effective ini value may change or
|
|
## disappear (new directives are allowed and printed), and the
|
|
## gd/curl/openssl/icu/imagick/redis/memcached/password/hash/stream feature
|
|
## lines must be unchanged. `php -i` Configure Command must match the packaged
|
|
## one except CFLAGS. No ELF under the prefix may have an unresolved library.
|
|
set -euo pipefail
|
|
|
|
MODE="${1:?usage: lsphp-src-overlay.sh runtime|dev PHPVER}"
|
|
PHPVER="${2:?usage: lsphp-src-overlay.sh runtime|dev PHPVER}"
|
|
OUT="${3:-/lsphp-src-out}"
|
|
case "$MODE" in runtime|dev) ;; *) echo "FATAL(lsphp-src-overlay): mode must be runtime|dev" >&2; exit 1 ;; esac
|
|
|
|
die() { echo "FATAL(lsphp-src-overlay): $*" >&2; exit 1; }
|
|
log() { echo "lsphp-src-overlay[$MODE]: $*"; }
|
|
|
|
LSPHP_SOURCE="${LSPHP_SOURCE:-litespeed-apt}"
|
|
if [ ! -f "$OUT/meta.env" ]; then
|
|
[ -z "${PHP_SRC_VERSION:-}" ] || die "PHP_SRC_VERSION=$PHP_SRC_VERSION but no staged php.net build in $OUT (lsphp-src stage did not run?)."
|
|
[ "$LSPHP_SOURCE" = "litespeed-apt" ] || die "LSPHP_SOURCE=$LSPHP_SOURCE requires PHP_SRC_VERSION."
|
|
log "no php.net build staged — packaged lsphp${PHPVER} kept as is."
|
|
exit 0
|
|
fi
|
|
[ "$LSPHP_SOURCE" = "php.net" ] || die "a php.net build is staged but LSPHP_SOURCE=$LSPHP_SOURCE (must be 'php.net' so the image label tells the truth)."
|
|
# shellcheck disable=SC1091
|
|
. "$OUT/meta.env"
|
|
[ "${PHP_SRC_VERSION:-}" = "$LSPHP_SRC_PHP_VERSION" ] || die "staged build is $LSPHP_SRC_PHP_VERSION but PHP_SRC_VERSION=${PHP_SRC_VERSION:-<empty>}."
|
|
|
|
BRANCH="${PHPVER:0:1}.${PHPVER:1}"
|
|
P="/usr/local/lsws/lsphp${PHPVER}"
|
|
SRCROOT="$OUT/root"
|
|
PROBE=/usr/local/share/lsphp-src-probe.php
|
|
[ -f "$PROBE" ] || die "$PROBE missing."
|
|
|
|
CORE="lsphp${PHPVER}"
|
|
COREV=$(dpkg-query -W -f='${Version}' "$CORE")
|
|
[ "$COREV" = "$LSPHP_SRC_PKG_REFERENCE" ] || die "this stage has $CORE $COREV but the php.net build was configured from $LSPHP_SRC_PKG_REFERENCE (the LiteSpeed repo moved between stages? rebuild)."
|
|
|
|
probe() { # $1 = tag; writes $OUT/probe-<tag>-{cli,lsapi}.txt
|
|
"$P/bin/php" "$PROBE" >"$OUT/probe-$1-cli.txt" || die "php probe failed ($1)."
|
|
"$P/bin/lsphp" "$PROBE" >"$OUT/probe-$1-lsapi.txt" || die "lsphp probe failed ($1)."
|
|
local first
|
|
first=$(head -n1 "$OUT/probe-$1-lsapi.txt")
|
|
[ "$first" = "SAPI litespeed" ] || die "lsphp did not run the probe under the litespeed SAPI ($1): '$first'."
|
|
}
|
|
## Configure Command minus CFLAGS, whitespace-normalised (the CFLAGS argument
|
|
## sits in a different position in our ./configure invocation).
|
|
cfgcmd() { "$P/bin/php" -i | awk -F' => ' '$1 ~ /^Configure Command/ {print $2}' | sed -E "s/'CFLAGS=[^']*'//; s/[[:space:]]+/ /g; s/^ //; s/ $//"; }
|
|
|
|
probe before
|
|
CFG_BEFORE=$(cfgcmd)
|
|
OLDV=$(awk '$1 == "VERSION" {print $2}' "$OUT/probe-before-cli.txt")
|
|
[ "$OLDV" = "$LSPHP_SRC_PKG_PHP_VERSION" ] || die "packaged PHP is $OLDV, build meta says $LSPHP_SRC_PKG_PHP_VERSION."
|
|
|
|
## ---- which files are php-src output? (dpkg decides) ---------------------------
|
|
mapfile -t SRC_PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' "lsphp${PHPVER}*" \
|
|
| awk -v v="$COREV" '$1 == "ii" && $3 == v {print $2}')
|
|
mapfile -t PECL_PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' "lsphp${PHPVER}*" \
|
|
| awk -v v="$COREV" '$1 == "ii" && $3 != v {print $2 "=" $3}')
|
|
if [ "$MODE" = runtime ]; then
|
|
for p in "${SRC_PKGS[@]}"; do
|
|
[ "$p" != "lsphp${PHPVER}-dev" ] || die "runtime mode but lsphp${PHPVER}-dev is installed in this stage."
|
|
done
|
|
fi
|
|
log "php-src packages at $COREV: ${SRC_PKGS[*]}"
|
|
log "kept as packaged (own versions): ${PECL_PKGS[*]:-none}"
|
|
|
|
REPLACED=0
|
|
KEPT=()
|
|
while IFS= read -r f; do
|
|
[ -f "$f" ] && [ ! -L "$f" ] || continue
|
|
rel="${f#/}"
|
|
is_elf=0
|
|
if [ "$(head -c4 "$f" | od -An -c | tr -d ' ')" = '177ELF' ]; then is_elf=1; fi
|
|
case "$f" in
|
|
"$P/bin/phar${BRANCH}.phar"|"$P"/include/php/*|"$P"/lib/php/build/*|"$P/bin/php-config"|"$P/bin/php-config${BRANCH}"|"$P/bin/phpize"|"$P/bin/phpize${BRANCH}")
|
|
## text artefacts generated by the php-src build
|
|
[ "$MODE" = dev ] || [[ "$f" == "$P/bin/phar${BRANCH}.phar" ]] || { KEPT+=("$f"); continue; }
|
|
src="$SRCROOT/$rel"
|
|
case "$f" in
|
|
"$P/bin/php-config") src="$SRCROOT$P/bin/php-config${BRANCH}" ;;
|
|
"$P/bin/phpize") src="$SRCROOT$P/bin/phpize${BRANCH}" ;;
|
|
esac
|
|
if [ -f "$src" ]; then
|
|
install -m "$(stat -c %a "$f")" "$src" "$f"; REPLACED=$((REPLACED + 1))
|
|
elif [[ "$f" == "$P"/include/php/* ]]; then
|
|
log "header $f no longer exists in php.net $LSPHP_SRC_PHP_VERSION — removing the stale copy"
|
|
rm -f "$f"
|
|
else
|
|
die "php.net build has no counterpart for packaged $f."
|
|
fi
|
|
;;
|
|
*)
|
|
if [ "$is_elf" = 1 ]; then
|
|
src="$SRCROOT/$rel"
|
|
[ "$f" = "$P/bin/php" ] && src="$SRCROOT$P/bin/php${BRANCH}"
|
|
[ -f "$src" ] || die "packaged ELF $f (php-src output, owned by a $COREV package) has NO php.net counterpart at $src — refusing to ship an old binary next to the new PHP."
|
|
install -m "$(stat -c %a "$f")" "$src" "$f"; REPLACED=$((REPLACED + 1))
|
|
else
|
|
KEPT+=("$f")
|
|
fi
|
|
;;
|
|
esac
|
|
done < <(for p in "${SRC_PKGS[@]}"; do dpkg -L "$p"; done | sort -u)
|
|
|
|
if [ "$MODE" = dev ]; then
|
|
## headers that are NEW in the php.net release (none for most patch releases)
|
|
while IFS= read -r -d '' h; do
|
|
dst="${h#"$SRCROOT"}"
|
|
[ -e "$dst" ] || { mkdir -p "$(dirname "$dst")"; install -m 0644 "$h" "$dst"; log "added new header $dst"; }
|
|
done < <(find "$SRCROOT$P/include/php" -type f -print0)
|
|
CFV=$("$P/bin/php-config" --version)
|
|
[ "$CFV" = "$LSPHP_SRC_PHP_VERSION" ] || die "php-config reports $CFV after overlay."
|
|
fi
|
|
log "replaced $REPLACED php-src files; kept ${#KEPT[@]} packaging files (ini templates, helper scripts, docs, man pages)"
|
|
|
|
## ---- no unresolved shared libraries -------------------------------------------
|
|
while IFS= read -r -d '' f; do
|
|
if [ "$(head -c4 "$f" | od -An -c | tr -d ' ')" = '177ELF' ]; then
|
|
LDD=$(ldd "$f" 2>&1 || true)
|
|
[[ "$LDD" != *"not found"* ]] || die "$f has unresolved libraries:
|
|
$LDD"
|
|
fi
|
|
done < <(find "$P/bin" "$P/lib" -type f -print0)
|
|
|
|
## ---- the "same support" gate ----------------------------------------------------
|
|
probe after
|
|
CFG_AFTER=$(cfgcmd)
|
|
[ "$CFG_BEFORE" = "$CFG_AFTER" ] || die "Configure Command differs from the packaged build beyond CFLAGS:
|
|
packaged: $CFG_BEFORE
|
|
php.net : $CFG_AFTER"
|
|
|
|
FAIL=0
|
|
for sapi in cli lsapi; do
|
|
B="$OUT/probe-before-$sapi.txt"; A="$OUT/probe-after-$sapi.txt"
|
|
NEWV=$(awk '$1 == "VERSION" {print $2}' "$A")
|
|
[ "$NEWV" = "$LSPHP_SRC_PHP_VERSION" ] || { echo "[$sapi] reports PHP $NEWV, expected $LSPHP_SRC_PHP_VERSION" >&2; FAIL=1; }
|
|
## extension sets + versions
|
|
R=$(awk -v o="$OLDV" -v n="$LSPHP_SRC_PHP_VERSION" '
|
|
FNR == NR { if ($1 == "EXT" || $1 == "ZEXT") b[$1" "$2] = $3; next }
|
|
($1 == "EXT" || $1 == "ZEXT") { a[$1" "$2] = $3 }
|
|
END {
|
|
for (k in b) {
|
|
if (!(k in a)) { print "MISSING " k; continue }
|
|
want = (b[k] == o) ? n : b[k]
|
|
if (a[k] != want) print "VERSION " k " " b[k] " -> " a[k] " (want " want ")"
|
|
}
|
|
for (k in a) if (!(k in b)) print "EXTRA " k " " a[k]
|
|
}' "$B" "$A" | sort)
|
|
if [ -n "$R" ]; then echo "[$sapi] extension surface changed:"$'\n'"$R" >&2; FAIL=1; fi
|
|
## feature lines must be byte-identical
|
|
R=$(diff <(grep '^FEAT ' "$B") <(grep '^FEAT ' "$A") || true)
|
|
if [ -n "$R" ]; then echo "[$sapi] feature surface changed:"$'\n'"$R" >&2; FAIL=1; fi
|
|
## effective ini (packaged php.ini + mods-available/*.ini): no directive may
|
|
## vanish or change value; directives NEW in the release are printed.
|
|
R=$(awk 'FNR == NR { if ($1 == "INI") b[$2] = $0; next } $1 == "INI" { a[$2] = $0 }
|
|
END { for (k in b) { if (!(k in a)) print "REMOVED " b[k]; else if (a[k] != b[k]) print "CHANGED " b[k] " -> " a[k] } }' "$B" "$A" | sort)
|
|
if [ -n "$R" ]; then echo "[$sapi] effective ini changed:"$'\n'"$R" >&2; FAIL=1; fi
|
|
R=$(awk 'FNR == NR { if ($1 == "INI") b[$2] = 1; next } $1 == "INI" && !($2 in b) { print " " $0 }' "$B" "$A")
|
|
[ -z "$R" ] || log "[$sapi] ini directives new in $LSPHP_SRC_PHP_VERSION (informational):"$'\n'"$R"
|
|
log "[$sapi] $(grep -c '^EXT ' "$A") extensions + $(grep -c '^ZEXT ' "$A") zend extensions, identical to packaged; $(grep -c '^FEAT ' "$A") feature lines identical"
|
|
done
|
|
[ "$FAIL" = 0 ] || die "the php.net build does NOT offer the same support as packaged lsphp${PHPVER} $COREV (details above)."
|
|
|
|
printf '%s\n' "$LSPHP_SRC_PHP_VERSION" >/etc/cac-php-version
|
|
cat >/etc/cac-lsphp-source <<EOF
|
|
source=php.net
|
|
php_version=$LSPHP_SRC_PHP_VERSION
|
|
tarball_sha256=$LSPHP_SRC_SHA256
|
|
signer_primary_fpr=$LSPHP_SRC_SIGNER
|
|
replaces_packaged=$CORE=$COREV (PHP $OLDV)
|
|
pecl_kept=${PECL_PKGS[*]:-none}
|
|
EOF
|
|
log "OK: lsphp${PHPVER} is now php.net $LSPHP_SRC_PHP_VERSION (was packaged $OLDV); /etc/cac-lsphp-source written"
|
|
## $OUT is a BuildKit bind mount (rw = writes discarded); nothing to clean up.
|