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:
1 parent
be1540f559
commit
83c2ee9c2a
13 files changed
+2021
-3
No files matched your search
Executable
+99
@@ -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 ]
|
||||
Reference in new issue
Block a user