feat(lsphp): emergency build path compiling lsphp from php.net source

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>
This commit is contained in:
shadowdaoandClaude Opus 5.5 committed 2026-10-21 09:46:22 -07:00
1 parent be1540f559
commit 83c2ee9c2a
13 files changed
+2021 -3

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
## lsphp-abi-gate.sh — may PHP extensions compiled against OLD_INC be loaded into
## a PHP built with NEW_INC? Used by lsphp-src-build.sh (emergency php.net build)
## before it keeps LiteSpeed's packaged PECL .so files (redis, imagick,
## memcached, igbinary, msgpack) next to a newer PHP patch release.
##
## WHY: PHP refuses a module whose ZEND_MODULE_API_NO / build-id differ (loud),
## but a changed struct layout under the SAME API number loads fine and corrupts
## memory (silent). Patch releases are not supposed to change layouts, yet the
## php2shell fix (8.x.y, 2026-10-21) did add fields — so this is measured, not
## assumed.
##
## HOW: compile one TU including the public PHP headers (each header in the list
## below that exists in BOTH trees) with -g against each tree, dump every struct/
## union with pahole (incl. anonymous typedef'd ones), and compare:
## * every pre-existing member keeps its offset and size (else FAIL)
## * a new member may only use bytes no old member used (else FAIL)
## (padding, or appended at the end)
## * a struct may change size only if it is *_globals — engine/module globals
## are singletons behind one exported symbol (executor_globals, ps_globals);
## no extension embeds or allocates them (else FAIL)
## A member that is renamed or retyped counts as removed: a false alarm that fails
## loud, never a silent pass.
##
## Usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR (each *_INC contains php/)
## Env: ABI_MIN_MEMBERS (default 1000) — fewer members extracted from OLD_INC
## means the probe itself is broken; refuse rather than pass vacuously.
## Exit 0 = compatible (report on stdout), 1 = incompatible (report on stderr).
## Tested by scripts/tests/lsphp-abi-gate.test.sh.
set -euo pipefail
OLD_INC="${1:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
NEW_INC="${2:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
WORK="${3:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
MIN="${ABI_MIN_MEMBERS:-1000}"
mkdir -p "$WORK"
HDRS=(php.h php_ini.h SAPI.h ext/standard/info.h ext/standard/php_var.h
ext/standard/php_string.h ext/standard/basic_functions.h ext/standard/url.h
ext/standard/php_incomplete_class.h ext/standard/php_smart_string.h
ext/session/php_session.h ext/json/php_json.h ext/date/php_date.h
ext/hash/php_hash.h ext/random/php_random.h ext/spl/spl_exceptions.h
ext/pcre/php_pcre.h zend_exceptions.h zend_interfaces.h zend_smart_str.h
zend_closures.h zend_generators.h zend_observer.h zend_attributes.h
zend_enum.h zend_weakrefs.h zend_fibers.h zend_extensions.h)
: >"$WORK/abi.c"
for h in "${HDRS[@]}"; do
for t in "$OLD_INC/php" "$NEW_INC/php"; do
[ -e "$t/$h" ] || [ -e "$t/main/$h" ] || [ -e "$t/Zend/$h" ] || continue 2
done
printf '#include "%s"\n' "$h" >>"$WORK/abi.c"
done
[ -s "$WORK/abi.c" ] || { echo "ABI gate: none of the probe headers exist in both trees" >&2; exit 1; }
dump() { # $1 = include root, $2 = tag
local inc="$1/php" o="$WORK/abi-$2.o"
gcc -g -O0 -c -fno-eliminate-unused-debug-types \
-I"$inc" -I"$inc/main" -I"$inc/TSRM" -I"$inc/Zend" -I"$inc/ext" -I"$inc/ext/date/lib" \
-o "$o" "$WORK/abi.c"
## M <struct> <decl>#<n> <offset[:bit]> <size> and S <struct> <size>
## Named: "struct _zend_object {" ... "};" /* size: N ... */
## Anonymous: "typedef struct {" ... "} zend_fcall_info_cache;"
pahole -a -F dwarf "$o" | awk '
/^(typedef )?(struct|union) ([A-Za-z_0-9]+ )?\{/ {
depth = 1; nm = 0; sz = ""; delete seen
name = ($1 == "typedef") ? "" : $2; next }
depth == 0 { next }
depth == 1 && /^\}/ {
if (name == "") { n = $0; sub(/^\}[ \t]*/, "", n); sub(/;.*$/, "", n); name = n }
for (i = 1; i <= nm; i++) print "M", name, mem[i]
if (sz != "") print "S", name, sz
depth = 0; next }
depth == 1 && /\/\* size: [0-9]+/ { t = $0; sub(/^.*size: /, "", t); sub(/[^0-9].*$/, "", t); sz = t; next }
/\{[ \t]*$/ { depth++; next }
/^[ \t]+\}/ { depth-- }
/;[ \t]*\/\*[ \t]+[0-9]+(:[ \t]*[0-9]+)?[ \t]+[0-9]+[ \t]+\*\// {
d = $0; sub(/;.*$/, "", d); gsub(/[ \t]+/, " ", d); sub(/^ /, "", d); gsub(/ /, "_", d)
c = $0; sub(/^.*\/\*[ \t]+/, "", c); sub(/[ \t]+\*\/.*$/, "", c); gsub(/:[ \t]+/, ":", c)
k = split(c, w, /[ \t]+/)
seen[d]++
mem[++nm] = d "#" seen[d] " " w[1] " " w[k] }
' >"$WORK/abi-$2.txt"
}
dump "$OLD_INC" old
dump "$NEW_INC" new
NOLD=$(awk '$1 == "M"' "$WORK/abi-old.txt" | wc -l)
NSTRUCT=$(awk '$1 == "S"' "$WORK/abi-old.txt" | wc -l)
if [ "$NOLD" -lt "$MIN" ]; then
echo "ABI gate: only $NOLD struct members extracted from $OLD_INC (< $MIN) — the probe is broken; refusing to call that compatible." >&2
exit 1
fi
REPORT=$(awk '
FNR == NR { if ($1 == "M") { old[$2 " " $3] = $4 " " $5; ost[$2] = 1 } else if ($1 == "S") osz[$2] = $3; next }
$1 == "M" { new[$2 " " $3] = $4 " " $5; nst[$2] = 1 }
$1 == "S" { nsz[$2] = $3 }
END {
for (k in old) { split(k, p, " "); if (!(p[1] in nst)) continue
if (!(k in new)) print "BAD removed/renamed member " k " (was " old[k] ")"
else if (old[k] != new[k]) print "BAD moved member " k ": " old[k] " -> " new[k] }
for (k in new) { if (k in old) continue; split(k, p, " "); if (!(p[1] in ost)) continue
split(new[k], q, " "); s = q[1] + 0; e = s + q[2]
hit = ""
for (o in old) { split(o, r, " "); if (r[1] != p[1]) continue; split(old[o], z, " ")
if (s < z[1] + z[2] && z[1] + 0 < e) { hit = o; break } }
if (hit != "") print "BAD new member " k " at " new[k] " overlaps old " hit
else print "OK new member " k " at " new[k] " (padding/appended)" }
for (st in osz) { if (!(st in nsz) || osz[st] == nsz[st]) continue
if (st ~ /_globals$/) print "OK size " st " " osz[st] " -> " nsz[st] " (*_globals singleton)"
else print "BAD size " st " " osz[st] " -> " nsz[st] }
}' "$WORK/abi-old.txt" "$WORK/abi-new.txt" | sort)
if [[ $'\n'"$REPORT" == *$'\nBAD '* ]]; then
printf '%s\n' "$REPORT" >&2
echo "ABI gate: INCOMPATIBLE ($NOLD members / $NSTRUCT structs compared)" >&2
exit 1
fi
[ -z "$REPORT" ] || printf '%s\n' "$REPORT"
echo "ABI gate: compatible ($NOLD members / $NSTRUCT structs compared)"
+303
View File
@@ -0,0 +1,303 @@
#!/bin/bash
## lsphp-src-build.sh — BUILD-TIME ONLY, EMERGENCY PATH. Compiles lsphp from the
## official php.net release tarball, configured EXACTLY like the LiteSpeed-packaged
## lsphp${PHPVER} it will replace, for the window in which a PHP security release
## is out but rpms.litespeedtech.com/debian has not published it yet.
##
## Runs in the throwaway `lsphp-src` stage of Dockerfile.lsphp / Dockerfile.litespeed,
## FROM the same litespeedtech/openlitespeed base as the shipped stage. The output
## (/lsphp-src-out) is applied to the shipped stage by lsphp-src-overlay.sh, which
## is where the "same support as the packaged lsphp" assertions live.
##
## PHP_SRC_VERSION empty (every normal build) => does nothing but create an empty
## /lsphp-src-out, in well under a second. Nothing is downloaded or installed.
##
## WHAT "CONFIGURED EXACTLY LIKE THE PACKAGE" MEANS, and why it cannot drift:
## the configure arguments are NOT kept in this repo. They are read, at build
## time, from the `Configure Command` line of the packaged lsphp that the same
## build just installed from the LiteSpeed repo (php -i), and replayed verbatim —
## only CFLAGS is edited, to drop the two -f*-prefix-map flags that encode
## LiteSpeed's build directory. When LiteSpeed changes their flags, this follows
## automatically; if a new flag needs a -dev package we do not install, configure
## FAILS (loud), it does not silently drop the feature. CPPFLAGS/LDFLAGS are not
## recorded by PHP; they come from Ubuntu noble's dpkg-buildflags with
## hardening=+all, which reproduces the packaged binary's measured hardening
## (PIE, full RELRO, BIND_NOW, FORTIFY _chk symbols) — checked again below.
##
## TRUST: the tarball must (1) match PHP_SRC_SHA256 (CI takes it from
## https://www.php.net/releases/index.php?json&version=X.Y; the build fails if it
## is not supplied) AND (2) carry a valid OpenPGP signature (.asc from php.net)
## by a key in keys/php-src/php-keyring.asc (the official keyring from
## https://www.php.net/distributions/php-keyring.gpg, committed
## unmodified as php-keyring.asc — it is ASCII-armoured despite the name) whose PRIMARY
## fingerprint is listed for this branch in keys/php-src/release-managers.txt
## (transcribed from https://www.php.net/gpg-keys.php). Either check failing
## aborts the build. A new release manager key therefore needs a reviewed commit.
##
## The LSAPI SAPI itself is LiteSpeed's php-litespeed release (pinned by sha256
## in keys/php-src/litespeed-sapi.txt), NOT php-src's bundled sapi/litespeed —
## that is what LiteSpeed's packages compile (measured, see that file).
##
## PECL extensions (igbinary, imagick, memcached, msgpack, redis) are NOT built
## here: the shipped stage keeps LiteSpeed's own packaged .so files — the same
## binaries the primary image ships, which LiteSpeed itself does not rebuild per
## PHP patch release (e.g. lsphp85-msgpack 3.0.1-1 is from June, core 8.5.11 from
## September). That is only safe while the Zend ABI is unchanged, so this script
## also runs an ABI gate (scripts/lsphp-abi-gate.sh: pahole over every struct the
## public PHP headers define, packaged -dev headers vs the new headers); any
## changed offset/size of an existing member fails the build.
##
## Usage (env): PHPVER=85 PHP_SRC_VERSION=8.5.12 PHP_SRC_SHA256=<64 hex> lsphp-src-build.sh
set -euo pipefail
HERE=$(cd "$(dirname "$0")" && pwd) # sibling scripts (lsphp-abi-gate.sh)
OUT=/lsphp-src-out
KEYDIR=${KEYDIR:-/usr/local/share/php-src-keys}
mkdir -p "$OUT"
die() { echo "FATAL(lsphp-src-build): $*" >&2; exit 1; }
log() { echo "lsphp-src-build: $*"; }
PHPVER="${PHPVER:?PHPVER required}"
if [ -z "${PHP_SRC_VERSION:-}" ]; then
[ -z "${PHP_SRC_SHA256:-}" ] || die "PHP_SRC_SHA256 given without PHP_SRC_VERSION."
log "PHP_SRC_VERSION empty — packaged lsphp build, nothing to do."
exit 0
fi
BRANCH="${PHPVER:0:1}.${PHPVER:1}" # 85 -> 8.5
[[ "$PHP_SRC_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "PHP_SRC_VERSION='$PHP_SRC_VERSION' is not X.Y.Z (RCs/alphas are refused)."
[ "${PHP_SRC_VERSION%.*}" = "$BRANCH" ] || die "PHP_SRC_VERSION=$PHP_SRC_VERSION does not belong to PHPVER=$PHPVER (branch $BRANCH)."
[[ "${PHP_SRC_SHA256:-}" =~ ^[0-9a-f]{64}$ ]] || die "PHP_SRC_SHA256 must be the 64-hex sha256 of php-${PHP_SRC_VERSION}.tar.xz from the php.net releases JSON."
P="/usr/local/lsws/lsphp${PHPVER}"
START=$(date +%s)
## ---- 1. toolchain + every -dev library the packaged configure line needs -----
## Same script as the shipped stage: refuses stale/unreachable repo lists and
## brings lsphp${PHPVER}* (core + -dev) to the repo's current release, so the
## Configure Command we read is the one the shipped stage's packages carry
## (lsphp-src-overlay.sh re-checks that the versions match).
## -dev packages: one per library in the packaged Configure Command. Extras are
## harmless; a missing one makes configure fail, which is the loud outcome.
DEVPKGS=(
build-essential autoconf pkg-config file dwarves
curl ca-certificates gpg gpgv xz-utils
libxml2-dev libssl-dev libcurl4-openssl-dev libenchant-2-dev libgmp-dev
libonig-dev libedit-dev libbz2-dev libxslt1-dev libzip-dev libsodium-dev
libargon2-dev libjpeg-dev libxpm-dev libpng-dev libwebp-dev libfreetype-dev
libldap-dev libsasl2-dev libpq-dev libsnmp-dev libsqlite3-dev freetds-dev
libtidy-dev libicu-dev zlib1g-dev libkrb5-dev
)
case "$PHPVER" in
81) DEVPKGS+=(libc-client2007e-dev libpspell-dev) ;; # --with-imap, --with-pspell
82|83) DEVPKGS+=(libpspell-dev) ;; # --with-pspell (gone in 8.4)
esac
/usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" "${DEVPKGS[@]}" "lsphp${PHPVER}-dev"
PKGV=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}")
DEVV=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}-dev")
[ "$PKGV" = "$DEVV" ] || die "lsphp${PHPVER}=$PKGV but lsphp${PHPVER}-dev=$DEVV — packaged headers do not match the packaged binary."
PKG_PHPV=$("$P/bin/php" -r 'echo PHP_VERSION;')
log "packaged reference: lsphp${PHPVER} ${PKGV} (PHP ${PKG_PHPV}); building php.net ${PHP_SRC_VERSION}"
if [ "$(printf '%s\n%s\n' "$PKG_PHPV" "$PHP_SRC_VERSION" | sort -V | tail -n1)" != "$PHP_SRC_VERSION" ] \
|| [ "$PKG_PHPV" = "$PHP_SRC_VERSION" ]; then
die "PHP_SRC_VERSION=$PHP_SRC_VERSION is not NEWER than the packaged $PKG_PHPV — the emergency path is pointless (or a downgrade). Use the normal build."
fi
## ---- 2. fetch + verify -------------------------------------------------------
WORK=/usr/src/php-src
mkdir -p "$WORK"
cd "$WORK"
TARBALL="php-${PHP_SRC_VERSION}.tar.xz"
for f in "$TARBALL" "$TARBALL.asc"; do
curl -fsSL --retry 3 --max-time 600 -o "$f" "https://www.php.net/distributions/$f" \
|| die "download of https://www.php.net/distributions/$f failed."
done
GOT=$(sha256sum "$TARBALL" | awk '{print $1}')
[ "$GOT" = "$PHP_SRC_SHA256" ] || die "sha256 mismatch for $TARBALL: got $GOT, expected $PHP_SRC_SHA256 (php.net releases JSON)."
log "sha256 OK $GOT"
[ -s "$KEYDIR/php-keyring.asc" ] || die "pinned keyring $KEYDIR/php-keyring.asc missing."
## The committed file is php.net's keyring byte-for-byte (ASCII-armoured); gpgv
## only reads binary keyrings.
gpg --batch --yes --dearmor -o "$WORK/php-keyring.gpg" "$KEYDIR/php-keyring.asc" || die "could not dearmor $KEYDIR/php-keyring.asc."
[ -s "$KEYDIR/release-managers.txt" ] || die "pinned fingerprint list $KEYDIR/release-managers.txt missing."
STATUS=$(gpgv --status-fd 1 --keyring "$WORK/php-keyring.gpg" "$TARBALL.asc" "$TARBALL" 2>"$WORK/gpgv.err") || {
cat "$WORK/gpgv.err" >&2; die "OpenPGP signature verification of $TARBALL FAILED."; }
cat "$WORK/gpgv.err"
## [GNUPG:] VALIDSIG <signing-key-fpr> <date> <ts> <expire> <ver> <rsv> <pk-algo> <hash> <class> <PRIMARY-fpr>
PRIMARY=$(awk '$1 == "[GNUPG:]" && $2 == "VALIDSIG" {print $NF}' <<<"$STATUS")
[ -n "$PRIMARY" ] || die "gpgv reported no VALIDSIG for $TARBALL."
ALLOWED=$(awk -v b="$BRANCH" '$1 == b {print $2}' "$KEYDIR/release-managers.txt")
[ -n "$ALLOWED" ] || die "no release-manager fingerprints pinned for PHP $BRANCH in release-managers.txt."
OK=0
while read -r fp; do [ "$fp" = "$PRIMARY" ] && OK=1; done <<<"$ALLOWED"
[ "$OK" = 1 ] || die "$TARBALL is validly signed by $PRIMARY, which is NOT a pinned PHP $BRANCH release manager (release-managers.txt)."
log "signature OK: primary key $PRIMARY is a pinned PHP $BRANCH release manager"
tar -xJf "$TARBALL"
SRC="$WORK/php-${PHP_SRC_VERSION}"
grep -F "PHP ${PHP_SRC_VERSION}" "$SRC/NEWS" >/dev/null || die "NEWS in $TARBALL has no 'PHP ${PHP_SRC_VERSION}' entry."
## ---- 2b. LiteSpeed's LSAPI SAPI, as the packages use (keys/php-src/litespeed-sapi.txt)
read -r SAPI_URL SAPI_SHA < <(awk '!/^#/ && NF == 2 {print; exit}' "$KEYDIR/litespeed-sapi.txt")
[ -n "${SAPI_URL:-}" ] && [[ "${SAPI_SHA:-}" =~ ^[0-9a-f]{64}$ ]] || die "no pinned LiteSpeed SAPI in $KEYDIR/litespeed-sapi.txt."
curl -fsSL --retry 3 --max-time 300 -o "$WORK/lsapi.tgz" "$SAPI_URL" || die "download of $SAPI_URL failed."
GOT=$(sha256sum "$WORK/lsapi.tgz" | awk '{print $1}')
[ "$GOT" = "$SAPI_SHA" ] || die "LiteSpeed SAPI $SAPI_URL sha256 $GOT != pinned $SAPI_SHA (LiteSpeed republished it? re-measure against the packaged lsphp before re-pinning)."
mkdir -p "$WORK/lsapi"
tar -xzf "$WORK/lsapi.tgz" -C "$WORK/lsapi"
## Per its README: copy only the *.c/*.h over php-src's sapi/litespeed and keep
## php-src's config.m4 / Makefile.frag / arginfo.
N=0
for f in "$WORK"/lsapi/*/*.c "$WORK"/lsapi/*/*.h; do
[ -f "$f" ] || continue
install -m 0644 "$f" "$SRC/sapi/litespeed/$(basename "$f")"; N=$((N + 1))
done
[ "$N" -ge 4 ] || die "LiteSpeed SAPI archive had only $N .c/.h files."
log "LiteSpeed SAPI $(basename "$SAPI_URL") (sha256 OK) replaces php-src sapi/litespeed sources ($N files)"
## ---- 2c. compiled-in ini defaults LiteSpeed patched ----------------------------
## Measured 2026-10-21 with `php -n`: LiteSpeed's lsphp82/83 carry Debian's
## session.save_path patch (default '/var/lib/php/sessions' instead of php-src's
## ''); lsphp85 does not. Reproduce whatever the packaged binary reports. Any
## OTHER compiled-in default that differs is caught by the gate in step 4b.
PKG_SAVE_PATH=$("$P/bin/php" -n -r 'echo ini_get("session.save_path");')
if [ -n "$PKG_SAVE_PATH" ]; then
[[ "$PKG_SAVE_PATH" =~ ^[A-Za-z0-9/._-]+$ ]] || die "unexpected packaged session.save_path default '$PKG_SAVE_PATH'."
sed -i -E 's|(STD_PHP_INI_ENTRY\("session\.save_path",[[:space:]]*)""|\1"'"$PKG_SAVE_PATH"'"|' "$SRC/ext/session/session.c"
grep -F "\"session.save_path\"" "$SRC/ext/session/session.c" | grep -F "\"$PKG_SAVE_PATH\"" >/dev/null \
|| die "could not apply the packaged session.save_path default '$PKG_SAVE_PATH' to ext/session/session.c."
log "compiled-in session.save_path default set to '$PKG_SAVE_PATH' (as the packaged lsphp${PHPVER})"
fi
## ---- 3. replay the packaged configure line -----------------------------------
CFG_LINE=$("$P/bin/php" -i | awk -F' => ' '$1 == "Configure Command " || $1 == "Configure Command" {sub(/^[^=]*=> /, ""); print}')
[ -n "$CFG_LINE" ] || die "could not read 'Configure Command' from the packaged $P/bin/php -i."
## xargs does the shell-style single-quote splitting without an eval.
mapfile -t CFG_ARGS < <(printf '%s\n' "$CFG_LINE" | xargs -n1 printf '%s\n')
[ "${CFG_ARGS[0]}" = "./configure" ] || die "unexpected Configure Command shape (first word '${CFG_ARGS[0]}')."
CFG_ARGS=("${CFG_ARGS[@]:1}")
ARGS=()
PKG_CFLAGS=""
for a in "${CFG_ARGS[@]}"; do
case "$a" in
CFLAGS=*) PKG_CFLAGS="${a#CFLAGS=}" ;;
*) ARGS+=("$a") ;;
esac
done
[ -n "$PKG_CFLAGS" ] || die "packaged Configure Command carries no CFLAGS — refusing to guess."
CFLAGS=""
for w in $PKG_CFLAGS; do
case "$w" in -ffile-prefix-map=*|-fdebug-prefix-map=*) continue ;; esac
CFLAGS+="${CFLAGS:+ }$w"
done
CFLAGS+=" -ffile-prefix-map=${SRC}=."
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
CPPFLAGS=$(dpkg-buildflags --get CPPFLAGS)
LDFLAGS=""
for w in $(dpkg-buildflags --get LDFLAGS); do
## CFLAGS ends in -fno-lto (LiteSpeed's choice); keep the link step consistent.
case "$w" in -flto*|-ffat-lto-objects) continue ;; esac
LDFLAGS+="${LDFLAGS:+ }$w"
done
CXXFLAGS="$CFLAGS"
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
log "configure args (from packaged lsphp${PHPVER} ${PKGV}): ${ARGS[*]}"
log "CFLAGS=$CFLAGS"
log "CPPFLAGS=$CPPFLAGS"
log "LDFLAGS=$LDFLAGS"
## Build-environment shim (builder stage only, never shipped): ext/pdo_dblib's
## config.m4 looks for libsybdb in $prefix/lib, not the multiarch dir Ubuntu's
## freetds-dev uses. pdo_dblib is built but not shipped (the packaged image has
## no lsphpNN-dblib), exactly as in the packaged build.
MA=$(dpkg-architecture -qDEB_HOST_MULTIARCH)
[ -e /usr/lib/libsybdb.so ] || ln -s "/usr/lib/$MA/libsybdb.so" /usr/lib/libsybdb.so
cd "$SRC"
if ! ./configure "${ARGS[@]}" "CFLAGS=$CFLAGS" >"$WORK/configure.log" 2>&1; then
tail -n 60 "$WORK/configure.log" >&2
die "configure FAILED with the packaged flags — most likely a -dev package missing from DEVPKGS in this script (see the error above)."
fi
## Unknown options are expected for a handful of historic flags LiteSpeed still
## passes (--enable-gd-native-ttf, --with-vpx-dir, ...); they are just as unknown
## to the packaged build. Print them so a NEW unknown flag is visible in the log.
grep -E "^configure: WARNING: unrecognized options" "$WORK/configure.log" || true
T0=$(date +%s)
make -j"$(nproc)" >"$WORK/make.log" 2>&1 || { tail -n 80 "$WORK/make.log" >&2; die "make FAILED."; }
log "make took $(( $(date +%s) - T0 ))s on $(nproc) CPUs"
STAGE="$WORK/stage"
make install INSTALL_ROOT="$STAGE" >"$WORK/install.log" 2>&1 || { tail -n 40 "$WORK/install.log" >&2; die "make install FAILED."; }
SP="$STAGE$P"
[ -x "$SP/bin/lsphp${BRANCH}" ] || die "make install produced no $P/bin/lsphp${BRANCH} (did --enable-litespeed vanish from the packaged flags?)."
[ -x "$SP/bin/php${BRANCH}" ] || die "make install produced no $P/bin/php${BRANCH}."
## --program-suffix names everything <tool>8.M; LiteSpeed's packages ship the
## SAPI as bin/lsphp and the CLI as both bin/php and bin/php8.M (two identical
## regular files, measured). Reproduce those names; the overlay copies only the
## files the packaged image actually has.
mv "$SP/bin/lsphp${BRANCH}" "$SP/bin/lsphp"
cp -p "$SP/bin/php${BRANCH}" "$SP/bin/php"
## Strip like dh_strip does for the packaged binaries (measured: no .comment /
## debug sections in the packaged lsphp).
find "$SP" -type f \( -name '*.so' -o -path "*/bin/*" \) -print0 | while IFS= read -r -d '' f; do
case "$(file -b "$f")" in
ELF*shared\ object*|ELF*pie\ executable*|ELF*executable*)
strip --remove-section=.comment --remove-section=.note --strip-unneeded "$f" ;;
esac
done
NEWV=$("$SP/bin/php${BRANCH}" -n -r 'echo PHP_VERSION;' 2>/dev/null || true)
[ "$NEWV" = "$PHP_SRC_VERSION" ] || die "built php reports '$NEWV', expected $PHP_SRC_VERSION."
## ---- 4b. compiled-in ini defaults must match the packaged binary ---------------
## `php -n` = no php.ini, no scan dir, no shared extensions: what is left is
## what the binary itself defaults to. New directives (a security release may
## add one, e.g. memory_hard_limit_percent in 8.x.y php2shell fix) are allowed
## and printed; a removed or changed default fails.
ini_defaults() { "$1" -n -r 'foreach (ini_get_all(null, false) as $k => $v) echo $k, "=", var_export($v, true), "\n";' | sort; }
INI_PKG=$(ini_defaults "$P/bin/php")
INI_NEW=$(ini_defaults "$SP/bin/php${BRANCH}")
INI_BAD=$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") | grep '^<' || true)
INI_ADD=$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") | grep '^>' || true)
[ -z "$INI_BAD" ] || die "compiled-in ini defaults differ from the packaged lsphp${PHPVER} (< packaged, > php.net build):
$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") || true)
LiteSpeed patched a default this script does not reproduce (see step 2c)."
log "compiled-in ini defaults identical to packaged ($(printf '%s\n' "$INI_PKG" | wc -l) directives)${INI_ADD:+; new in $PHP_SRC_VERSION: $(printf '%s' "$INI_ADD" | tr '\n' ' ')}"
## Same LSAPI SAPI as the packaged binary? Compare the LSAPI_*/LSCAPI_* names
## (its env knobs + feature switches) compiled into each. A difference means
## LiteSpeed moved to a SAPI release we have not pinned (or the copy failed).
lsapi_names() { grep -aoE 'LS(C)?API_[A-Z0-9_]+' "$1" | sort -u; }
LSAPI_DIFF=$(diff <(lsapi_names "$P/bin/lsphp") <(lsapi_names "$SP/bin/lsphp") || true)
[ -z "$LSAPI_DIFF" ] || die "LSAPI SAPI differs from the packaged lsphp (< packaged, > php.net build):
$LSAPI_DIFF"
log "LSAPI SAPI matches the packaged lsphp ($(lsapi_names "$SP/bin/lsphp" | wc -l) LSAPI_* names identical)"
## ---- 4. ABI gate: may LiteSpeed's packaged PECL .so files load into this? -----
## See scripts/lsphp-abi-gate.sh (and its test). Measured for 8.5.11 -> 8.5.12:
## the php2shell fix adds CG(interned_strings_fallible) in padding and appends
## EG(delayed_oom) / PS(in_rfc1867_update); no existing member moved.
ABI_OUT=$(bash "$HERE/lsphp-abi-gate.sh" "$P/include" "$SP/include" "$WORK/abi" 2>&1) || {
printf '%s\n' "$ABI_OUT" >&2
die "ABI gate: PHP $PHP_SRC_VERSION changed struct layouts relative to the packaged $PKG_PHPV headers. LiteSpeed's packaged PECL .so files (redis, imagick, memcached, igbinary, msgpack) were compiled against the old layout and must NOT be loaded into this build — they would have to be rebuilt from source, which this emergency path does not do (docs/lsphp-source-emergency.md)."
}
printf '%s\n' "$ABI_OUT" | sed 's/^/lsphp-src-build: /'
## ---- 5. hand-off ----------------------------------------------------------------
mkdir -p "$OUT/root"
cp -a "$STAGE/." "$OUT/root/"
cat >"$OUT/meta.env" <<EOF
LSPHP_SRC_PHP_VERSION=$PHP_SRC_VERSION
LSPHP_SRC_SHA256=$PHP_SRC_SHA256
LSPHP_SRC_SIGNER=$PRIMARY
LSPHP_SRC_PKG_REFERENCE=$PKGV
LSPHP_SRC_PKG_PHP_VERSION=$PKG_PHPV
EOF
printf '%s\n' "${ARGS[@]}" >"$OUT/configure-args.txt"
log "done in $(( $(date +%s) - START ))s: php.net ${PHP_SRC_VERSION} staged at $OUT/root (reference lsphp${PHPVER} ${PKGV})"
+202
View File
@@ -0,0 +1,202 @@
#!/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.
+47
View File
@@ -0,0 +1,47 @@
<?php
// lsphp-src-probe.php — prints the runtime "support surface" of the PHP that
// runs it, one fact per line, for lsphp-src-overlay.sh to diff before/after the
// php.net overlay. Run under BOTH SAPIs: `bin/php probe.php` and
// `bin/lsphp probe.php` (lsphp executes a script argument under the litespeed
// SAPI — measured; it is only `-m`/`-r` it does not understand).
// Line kinds: SAPI <name> | VERSION <v> | EXT <name> <version> | ZEXT <name> <version>
// INI <name> <value> | FEAT <key> <value>
// The overlay script relies on the stable "<KIND> <key> <rest>" shape; keep it.
echo 'SAPI ', PHP_SAPI, "\n";
echo 'VERSION ', PHP_VERSION, "\n";
echo 'FEAT ZTS ', PHP_ZTS, "\n";
echo 'FEAT DEBUG ', PHP_DEBUG, "\n";
echo 'FEAT INT_SIZE ', PHP_INT_SIZE, "\n";
foreach (get_loaded_extensions() as $e) {
echo 'EXT ', str_replace(' ', '_', $e), ' ', phpversion($e), "\n";
}
foreach (get_loaded_extensions(true) as $e) {
echo 'ZEXT ', str_replace(' ', '_', $e), ' ', phpversion($e), "\n";
}
foreach (ini_get_all(null, false) as $k => $v) {
echo 'INI ', $k, ' ', var_export($v, true), "\n";
}
if (function_exists('gd_info')) {
foreach (gd_info() as $k => $v) {
echo 'FEAT gd:', str_replace(' ', '_', $k), ' ', var_export($v, true), "\n";
}
}
if (function_exists('curl_version')) {
$c = curl_version();
echo 'FEAT curl:version ', $c['version'], "\n";
echo 'FEAT curl:ssl ', $c['ssl_version'], "\n";
echo 'FEAT curl:protocols ', implode(',', $c['protocols']), "\n";
}
echo 'FEAT openssl ', defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : 'none', "\n";
if (defined('INTL_ICU_VERSION')) echo 'FEAT icu ', INTL_ICU_VERSION, "\n";
if (class_exists('Imagick')) echo 'FEAT imagick:formats ', count(Imagick::queryFormats()), "\n";
if (function_exists('sodium_crypto_aead_aes256gcm_is_available')) echo 'FEAT sodium:aes256gcm ', var_export(sodium_crypto_aead_aes256gcm_is_available(), true), "\n";
echo 'FEAT password_algos ', implode(',', password_algos()), "\n";
echo 'FEAT hash_algos ', count(hash_algos()), "\n";
echo 'FEAT stream_wrappers ', implode(',', stream_get_wrappers()), "\n";
echo 'FEAT stream_filters ', implode(',', stream_get_filters()), "\n";
if (function_exists('mb_list_encodings')) echo 'FEAT mb_encodings ', count(mb_list_encodings()), "\n";
if (function_exists('opcache_get_status')) echo 'FEAT opcache:loaded 1', "\n";
if (class_exists('Redis')) echo 'FEAT redis:serializers ', implode(',', array_filter(['IGBINARY', 'MSGPACK', 'JSON'], fn($s) => defined("Redis::SERIALIZER_$s"))), "\n";
if (class_exists('Redis')) echo 'FEAT redis:compression ', implode(',', array_filter(['LZF', 'ZSTD', 'LZ4'], fn($s) => defined("Redis::COMPRESSION_$s"))), "\n";
if (class_exists('Memcached')) echo 'FEAT memcached:igbinary ', var_export(Memcached::HAVE_IGBINARY, true), ' msgpack ', var_export(Memcached::HAVE_MSGPACK, true), ' sasl ', var_export(Memcached::HAVE_SASL, true), "\n";
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
## lsphp-abi-gate.test.sh — proves scripts/lsphp-abi-gate.sh can FAIL.
##
## The gate is what lets the emergency php.net lsphp build keep LiteSpeed's
## packaged PECL .so files. A gate that always says "compatible" would be worse
## than none, so every rule is exercised against synthetic header trees whose
## outcome is known: three layouts that must pass (unchanged; new member in
## padding; member appended to a *_globals struct) and five that must fail
## (member inserted mid-struct; member removed; member retyped/resized; a
## non-globals struct grown at the end; a struct embedded in another growing).
## Plus: a probe that extracts too few members must refuse, not pass.
##
## Needs gcc + pahole (dwarves). Exits 0 only if every case behaves.
set -euo pipefail
HERE=$(cd "$(dirname "$0")/.." && pwd)
GATE="$HERE/lsphp-abi-gate.sh"
for t in gcc pahole; do
command -v "$t" >/dev/null 2>&1 || { echo "SKIP-FAIL: $t not installed (apt-get install gcc dwarves)" >&2; exit 1; }
done
T=$(mktemp -d)
trap 'rm -rf "$T"' EXIT
## $1 = dir, $2 = body of struct _zend_object, $3 = body of _zend_executor_globals,
## $4 = extra declarations
mk() {
mkdir -p "$1/php/main"
cat >"$1/php/main/php.h" <<EOF
#include <stdint.h>
#include <stdbool.h>
struct _zend_object { $2 };
struct _zend_executor_globals { $3 };
struct _holder { struct _zend_object obj; int after; };
typedef struct { int a; void *b; } anon_cache;
$4
struct _zend_executor_globals executor_globals;
struct _holder holder_sym;
anon_cache anon_sym;
EOF
}
BASE_OBJ='uint32_t gc; uint32_t handle; bool flag; void *ce; void *handlers;'
BASE_EG='void *a; int b; void *reserved[4];'
mk "$T/old" "$BASE_OBJ" "$BASE_EG" ''
PASS=0; FAILN=0
expect() { # $1 = pass|fail, $2 = name, $3 = new dir
local rc=0
ABI_MIN_MEMBERS=5 bash "$GATE" "$T/old" "$3" "$T/w-$2" >"$T/out-$2" 2>&1 || rc=$?
if { [ "$1" = pass ] && [ "$rc" = 0 ]; } || { [ "$1" = fail ] && [ "$rc" != 0 ]; }; then
PASS=$((PASS + 1)); echo "ok - $2 ($1, rc=$rc)"
else
FAILN=$((FAILN + 1)); echo "FAIL - $2: expected $1, got rc=$rc"; sed 's/^/ /' "$T/out-$2"
fi
}
mk "$T/same" "$BASE_OBJ" "$BASE_EG" ''
expect pass unchanged "$T/same"
## bool after `flag` lands in the 3 padding bytes before the pointer `ce`
mk "$T/pad" 'uint32_t gc; uint32_t handle; bool flag; bool newflag; void *ce; void *handlers;' "$BASE_EG" ''
expect pass new-member-in-padding "$T/pad"
mk "$T/eg" "$BASE_OBJ" 'void *a; int b; void *reserved[4]; void *delayed_oom;' ''
expect pass globals-appended "$T/eg"
mk "$T/mid" 'uint32_t gc; void *inserted; uint32_t handle; bool flag; void *ce; void *handlers;' "$BASE_EG" ''
expect fail member-inserted-mid-struct "$T/mid"
mk "$T/rm" 'uint32_t gc; uint32_t handle; bool flag; void *handlers;' "$BASE_EG" ''
expect fail member-removed "$T/rm"
mk "$T/retype" 'uint32_t gc; uint64_t handle; bool flag; void *ce; void *handlers;' "$BASE_EG" ''
expect fail member-resized "$T/retype"
## appended to a NON-globals struct: no old member moves, but sizeof grows —
## extensions that allocate sizeof(zend_object)+x would under-allocate
mk "$T/grow" 'uint32_t gc; uint32_t handle; bool flag; void *ce; void *handlers; void *extra;' "$BASE_EG" ''
expect fail non-globals-struct-grew "$T/grow"
## same change seen from the embedding struct: the report must name _holder.after
## as MOVED, i.e. the container check works on its own, not only via the size rule
if grep -E '^BAD moved member _holder int_after#1' "$T/out-non-globals-struct-grew" >/dev/null; then
PASS=$((PASS + 1)); echo "ok - embedded-struct-shifts-container (reported _holder.after moved)"
else
FAILN=$((FAILN + 1)); echo "FAIL - embedded-struct-shifts-container: _holder.after not reported"; sed 's/^/ /' "$T/out-non-globals-struct-grew"
fi
## the anonymous typedef is tracked too
mkdir -p "$T/anon/php/main"
sed 's/typedef struct { int a; void \*b; } anon_cache;/typedef struct { int a; int z; void *b; long c; } anon_cache;/' "$T/old/php/main/php.h" >"$T/anon/php/main/php.h"
expect fail anonymous-typedef-changed "$T/anon"
## a probe that finds (almost) nothing must refuse
rc=0; ABI_MIN_MEMBERS=1000 bash "$GATE" "$T/old" "$T/same" "$T/w-min" >"$T/out-min" 2>&1 || rc=$?
if [ "$rc" != 0 ] && grep 'probe is broken' "$T/out-min" >/dev/null; then PASS=$((PASS + 1)); echo "ok - too-few-members-refused (rc=$rc)"
else FAILN=$((FAILN + 1)); echo "FAIL - too-few-members-refused: rc=$rc"; cat "$T/out-min"; fi
echo "lsphp-abi-gate: $PASS passed, $FAILN failed"
[ "$FAILN" = 0 ]