From 83c2ee9c2aaa2145f4be4c0e978367472371c4b5 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Wed, 21 Oct 2026 09:43:45 -0700 Subject: [PATCH 1/2] 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 --- .gitea/workflows/build-push.yaml | 53 +- .gitea/workflows/lsphp-source-emergency.yaml | 132 +++ Dockerfile.litespeed | 35 +- Dockerfile.lsphp | 54 +- docs/lsphp-source-emergency.md | 139 ++++ keys/php-src/litespeed-sapi.txt | 13 + keys/php-src/php-keyring.asc | 804 +++++++++++++++++++ keys/php-src/release-managers.txt | 22 + scripts/lsphp-abi-gate.sh | 121 +++ scripts/lsphp-src-build.sh | 303 +++++++ scripts/lsphp-src-overlay.sh | 202 +++++ scripts/lsphp-src-probe.php | 47 ++ scripts/tests/lsphp-abi-gate.test.sh | 99 +++ 13 files changed, 2021 insertions(+), 3 deletions(-) create mode 100644 .gitea/workflows/lsphp-source-emergency.yaml create mode 100644 docs/lsphp-source-emergency.md create mode 100644 keys/php-src/litespeed-sapi.txt create mode 100644 keys/php-src/php-keyring.asc create mode 100644 keys/php-src/release-managers.txt create mode 100755 scripts/lsphp-abi-gate.sh create mode 100755 scripts/lsphp-src-build.sh create mode 100755 scripts/lsphp-src-overlay.sh create mode 100644 scripts/lsphp-src-probe.php create mode 100755 scripts/tests/lsphp-abi-gate.test.sh diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 1ee2a83..5953a5a 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -52,12 +52,27 @@ jobs: scripts/create-vhost-litespeed.sh \ scripts/install-lscache-wp.sh \ scripts/lsphp-apt-upgrade.sh \ + scripts/lsphp-src-build.sh \ + scripts/lsphp-src-overlay.sh \ + scripts/lsphp-abi-gate.sh \ scripts/tune-mpm.sh \ - scripts/tests/lsphp-info-probe.test.sh + scripts/tests/lsphp-info-probe.test.sh \ + scripts/tests/lsphp-abi-gate.test.sh - name: lsphp probe regression test run: ./scripts/tests/lsphp-info-probe.test.sh + # The emergency php.net lsphp path (lsphp-source-emergency.yaml) keeps + # LiteSpeed's PECL .so files only if this gate says the Zend ABI is + # unchanged; prove here, on every push, that the gate can still FAIL. + - name: lsphp ABI gate regression test + run: | + if ! command -v pahole >/dev/null 2>&1 || ! command -v gcc >/dev/null 2>&1; then + (apt-get update && apt-get install -y gcc dwarves) || + (sudo apt-get update && sudo apt-get install -y gcc dwarves) + fi + ./scripts/tests/lsphp-abi-gate.test.sh + Build-and-Push: runs-on: ubuntu-latest strategy: @@ -231,6 +246,24 @@ jobs: username: ${{ secrets.CI_USER }} password: ${{ secrets.CI_TOKEN }} + # A php.net emergency build (lsphp-source-emergency.yaml) may be what + # :phpNN points at right now. While LiteSpeed still lags, a normal build + # would silently put the VULNERABLE packaged lsphp back. Refuse instead; + # the job goes green again by itself once the LiteSpeed repo catches up. + # Fail-open if the tag cannot be inspected (first build, registry blip). + - name: Do not roll back a newer php.net emergency image + run: | + set -euo pipefail + ref=repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }} + src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true) + cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true) + new='${{ steps.lsphp.outputs.php }}' + echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new" + if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then + echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed." + exit 1 + fi + - name: Build and Push LiteSpeed Image uses: docker/build-push-action@v6 with: @@ -304,6 +337,24 @@ jobs: username: ${{ secrets.CI_USER }} password: ${{ secrets.CI_TOKEN }} + # A php.net emergency build (lsphp-source-emergency.yaml) may be what + # :phpNN points at right now. While LiteSpeed still lags, a normal build + # would silently put the VULNERABLE packaged lsphp back. Refuse instead; + # the job goes green again by itself once the LiteSpeed repo catches up. + # Fail-open if the tag cannot be inspected (first build, registry blip). + - name: Do not roll back a newer php.net emergency image + run: | + set -euo pipefail + ref=repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${{ matrix.phpver }} + src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true) + cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true) + new='${{ steps.lsphp.outputs.php }}' + echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new" + if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then + echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed." + exit 1 + fi + - name: Build and Push lsphp Image uses: docker/build-push-action@v6 with: diff --git a/.gitea/workflows/lsphp-source-emergency.yaml b/.gitea/workflows/lsphp-source-emergency.yaml new file mode 100644 index 0000000..5015b80 --- /dev/null +++ b/.gitea/workflows/lsphp-source-emergency.yaml @@ -0,0 +1,132 @@ +name: lsphp from php.net source (EMERGENCY) +run-name: EMERGENCY lsphp php.net build by ${{ gitea.actor }} + +## NEVER runs on push. Use only when a PHP security release is out and the +## LiteSpeed apt repo (rpms.litespeedtech.com/debian, noble) still ships the +## vulnerable lsphp. Runbook + exact trigger command: docs/lsphp-source-emergency.md +## +## What it builds: the normal Dockerfile.lsphp / Dockerfile.litespeed with +## PHP_SRC_VERSION / PHP_SRC_SHA256 / LSPHP_SOURCE=php.net +## so lsphp${PHPVER} is compiled from the php.net tarball (sha256 from the +## php.net releases JSON + OpenPGP signature against keys/php-src/), configured +## from the packaged lsphp's own Configure Command, and the image build FAILS +## unless the result has the same extensions / ini / features as the packaged +## lsphp (scripts/lsphp-src-overlay.sh). +## +## Tags: always :phpNN-phpnet-X.Y.Z (immutable, for staging / rollback) +## and :phpNN only when push_production_tags = true. +on: + workflow_dispatch: + inputs: + php_versions: + description: "Space-separated PHPVER=X.Y.Z pairs, e.g. '82=8.2.35 83=8.3.36 84=8.4.27 85=8.5.12'" + required: true + type: string + images: + description: "Which images: both | lsphp | litespeed" + required: true + default: both + type: string + push_production_tags: + description: "Also push :phpNN (what WHP pulls). false = only :phpNN-phpnet-X.Y.Z for staging." + required: true + default: false + type: boolean + +jobs: + Build-lsphp-from-source: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + phpver: [81, 82, 83, 84, 85] + image: [lsphp, litespeed] + steps: + - name: Select this job's target (skip if not requested) + id: t + # inputs via env, never spliced into the script text + env: + IN_VERSIONS: ${{ inputs.php_versions }} + IN_IMAGES: ${{ inputs.images }} + IN_PROD: ${{ format('{0}', inputs.push_production_tags) }} + run: | + set -euo pipefail + case "$IN_IMAGES" in both|lsphp|litespeed) ;; *) echo "::error::images must be both|lsphp|litespeed, got '$IN_IMAGES'"; exit 1 ;; esac + case "$IN_PROD" in true|false) ;; *) echo "::error::push_production_tags must be true|false, got '$IN_PROD'"; exit 1 ;; esac + v="" + for pair in $IN_VERSIONS; do + [[ "$pair" =~ ^(8[1-5])=([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "::error::bad php_versions entry '$pair' (want NN=X.Y.Z, NN in 81..85)"; exit 1; } + [ "${BASH_REMATCH[1]}" = '${{ matrix.phpver }}' ] && v="${BASH_REMATCH[2]}" + done + echo "prod=$IN_PROD" >> "$GITHUB_OUTPUT" + if [ -z "$v" ] || { [ "$IN_IMAGES" != both ] && [ "$IN_IMAGES" != '${{ matrix.image }}' ]; }; then + echo "not requested: php${{ matrix.phpver }} / ${{ matrix.image }}"; echo "skip=1" >> "$GITHUB_OUTPUT"; exit 0 + fi + echo "skip=0" >> "$GITHUB_OUTPUT" + echo "version=$v" >> "$GITHUB_OUTPUT" + echo "image_name=cac-${{ matrix.image }}" >> "$GITHUB_OUTPUT" + echo "dockerfile=./Dockerfile.${{ matrix.image }}" >> "$GITHUB_OUTPUT" + + - name: Checkout + if: steps.t.outputs.skip == '0' + uses: actions/checkout@v4 + + # sha256 of php-X.Y.Z.tar.xz from php.net's own releases JSON. The build + # re-verifies it AND the OpenPGP signature; this step only refuses early + # if the release does not exist or is not newer than what LiteSpeed ships. + - name: Resolve php.net release + packaged lsphp + if: steps.t.outputs.skip == '0' + id: v + run: | + set -euo pipefail + v='${{ steps.t.outputs.version }}' + json=$(curl -fsS --retry 3 --max-time 60 "https://www.php.net/releases/index.php?json&version=$v") + sha=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); v=sys.argv[1]; print(next((s["sha256"] for s in d.get("source", []) if s.get("filename")=="php-%s.tar.xz"%v), ""))' "$v") + [ -n "$sha" ] || { echo "::error::php.net has no release php-$v.tar.xz (EOL branch? typo?): $json"; exit 1; } + [[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { echo "::error::no sha256 for php-$v.tar.xz in the php.net releases JSON"; exit 1; } + idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages) + pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1) + [ -n "$pkg" ] || { echo "::error::lsphp${{ matrix.phpver }} not in the LiteSpeed repo index"; exit 1; } + pphp=${pkg%%-*} + if [ "$(printf '%s\n%s\n' "$pphp" "$v" | sort -V | tail -n1)" != "$v" ] || [ "$pphp" = "$v" ]; then + echo "::error::LiteSpeed already ships lsphp${{ matrix.phpver }} $pkg (PHP $pphp) >= $v — use the normal build (push to trunk / dispatch build-push.yaml)." + exit 1 + fi + echo "php.net $v sha256=$sha ; LiteSpeed lsphp${{ matrix.phpver }}=$pkg" + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "pkg=$pkg" >> "$GITHUB_OUTPUT" + echo "pkgphp=$pphp" >> "$GITHUB_OUTPUT" + echo "bust=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + if: steps.t.outputs.skip == '0' + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea + if: steps.t.outputs.skip == '0' + uses: docker/login-action@v3 + with: + registry: repo.anhonesthost.net + username: ${{ secrets.CI_USER }} + password: ${{ secrets.CI_TOKEN }} + + - name: Build and push (php.net lsphp) + if: steps.t.outputs.skip == '0' + uses: docker/build-push-action@v6 + with: + file: ${{ steps.t.outputs.dockerfile }} + platforms: linux/amd64 + push: true + build-args: | + PHPVER=${{ matrix.phpver }} + OLS_VERSION=1.8.4 + CACHE_BUST=${{ steps.v.outputs.bust }} + EXPECT_LSPHP_PKG=${{ steps.v.outputs.pkg }} + EXPECT_PHP_VERSION=${{ steps.v.outputs.pkgphp }} + PHP_SRC_VERSION=${{ steps.t.outputs.version }} + PHP_SRC_SHA256=${{ steps.v.outputs.sha }} + LSPHP_SOURCE=php.net + tags: | + repo.anhonesthost.net/cloud-hosting-platform/${{ steps.t.outputs.image_name }}:php${{ matrix.phpver }}-phpnet-${{ steps.t.outputs.version }} + ${{ steps.t.outputs.prod == 'true' && format('repo.anhonesthost.net/cloud-hosting-platform/{0}:php{1}', steps.t.outputs.image_name, matrix.phpver) || '' }} + ${{ steps.t.outputs.prod == 'true' && matrix.phpver == 85 && format('repo.anhonesthost.net/cloud-hosting-platform/{0}:latest', steps.t.outputs.image_name) || '' }} diff --git a/Dockerfile.litespeed b/Dockerfile.litespeed index a08aea8..f362f9f 100644 --- a/Dockerfile.litespeed +++ b/Dockerfile.litespeed @@ -22,6 +22,26 @@ ## after FROM for any RUN steps that need it. ARG OLS_VERSION=1.8.5 ARG PHPVER=83 + +## ---- EMERGENCY ONLY: lsphp compiled from php.net source ---------------------- +## Idle unless PHP_SRC_VERSION is set (then /lsphp-src-out stays empty and the +## overlay step below is a no-op). Same stage as Dockerfile.lsphp — see there and +## docs/lsphp-source-emergency.md. +FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} AS lsphp-src +ARG PHPVER=83 +ARG CACHE_BUST= +ARG EXPECT_LSPHP_PKG= +ARG EXPECT_PHP_VERSION= +ARG PHP_SRC_VERSION= +ARG PHP_SRC_SHA256= +RUN --mount=type=bind,source=scripts,target=/mnt/scripts \ + --mount=type=bind,source=keys/php-src,target=/usr/local/share/php-src-keys \ + set -e; \ + if [ -n "$PHP_SRC_VERSION" ]; then \ + install -m 0755 /mnt/scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh; \ + fi; \ + PHPVER="${PHPVER}" bash /mnt/scripts/lsphp-src-build.sh + FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} ARG PHPVER=83 ENV PHPVER=${PHPVER} @@ -68,6 +88,17 @@ RUN /usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" \ apt-get clean && \ rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/* +## EMERGENCY PATH: overlay the php.net lsphp (no-op on normal builds) and prove +## it has the same extension/feature surface as the packaged one just installed +## — see scripts/lsphp-src-overlay.sh. Bind mounts: no extra image layer content. +ARG PHP_SRC_VERSION= +ARG LSPHP_SOURCE=litespeed-apt +RUN --mount=type=bind,from=lsphp-src,source=/lsphp-src-out,target=/lsphp-src-out,rw \ + --mount=type=bind,source=scripts,target=/mnt/scripts \ + install -m 0644 /mnt/scripts/lsphp-src-probe.php /usr/local/share/lsphp-src-probe.php && \ + bash /mnt/scripts/lsphp-src-overlay.sh runtime "${PHPVER}" && \ + rm -f /usr/local/share/lsphp-src-probe.php + ## Composer (matches the wp-cli pattern in the existing CAC Dockerfile — ## phar download, no `php install` pipe since lsphp's CLI mode is fine). RUN curl -fsSL -o /usr/local/bin/composer https://getcomposer.org/download/latest-stable/composer.phar && \ @@ -111,8 +142,10 @@ RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab ## docker buildx imagetools inspect --format '{{json .Image.Config.Labels}}' ## Values come from CI's repo-index lookup and were asserted against the ## installed packages by lsphp-apt-upgrade.sh above ("unverified" = local build). -LABEL org.whp.php-version="${EXPECT_PHP_VERSION:-unverified}" \ +## Emergency (php.net) builds: see the same LABEL block in Dockerfile.lsphp. +LABEL org.whp.php-version="${PHP_SRC_VERSION:-${EXPECT_PHP_VERSION:-unverified}}" \ org.whp.lsphp-package-version="${EXPECT_LSPHP_PKG:-unverified}" \ + org.whp.lsphp-source="${LSPHP_SOURCE}" \ org.whp.php-flavour="lsphp${PHPVER}" EXPOSE 80 443 diff --git a/Dockerfile.lsphp b/Dockerfile.lsphp index e05084d..9820a31 100644 --- a/Dockerfile.lsphp +++ b/Dockerfile.lsphp @@ -22,6 +22,29 @@ ARG OLS_VERSION=1.8.4 ARG PHPVER=83 +## ---- stage 0 (EMERGENCY ONLY): lsphp compiled from php.net source ----------- +## Idle unless PHP_SRC_VERSION is set: lsphp-src-build.sh then exits at once and +## /lsphp-src-out stays empty, so every normal build is unchanged (the overlay +## steps below are no-ops). Set PHP_SRC_VERSION + PHP_SRC_SHA256 + LSPHP_SOURCE=php.net +## ONLY when a PHP security release is out and the LiteSpeed apt repo still lags +## — CI: .gitea/workflows/lsphp-source-emergency.yaml; runbook: +## docs/lsphp-source-emergency.md. The tarball is sha256- AND OpenPGP-verified +## against keys/php-src/; configure flags are replayed from the packaged lsphp. +FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} AS lsphp-src +ARG PHPVER=83 +ARG CACHE_BUST= +ARG EXPECT_LSPHP_PKG= +ARG EXPECT_PHP_VERSION= +ARG PHP_SRC_VERSION= +ARG PHP_SRC_SHA256= +RUN --mount=type=bind,source=scripts,target=/mnt/scripts \ + --mount=type=bind,source=keys/php-src,target=/usr/local/share/php-src-keys \ + set -e; \ + if [ -n "$PHP_SRC_VERSION" ]; then \ + install -m 0755 /mnt/scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh; \ + fi; \ + PHPVER="${PHPVER}" bash /mnt/scripts/lsphp-src-build.sh + ## ---- stage 1: build the cac_path_parity extension -------------------------- ## $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] parity with cac-fpm, enforced ## from RINIT so a customer's .user.ini cannot displace it — see @@ -101,6 +124,17 @@ RUN set -e; \ ## - a run that executes ZERO tests also exits 0, so assert the summary shows ## every .phpt in the directory both ran and passed. Same reasoning as the ## `lsphp -i` probe below: an assertion that cannot fail is worse than none. +## EMERGENCY PATH: swap the packaged lsphp + its -dev files for the php.net build +## (no-op when stage 0 was idle), so cac_path_parity below compiles against, and +## its .phpt suite runs on, the PHP this image will actually ship. +ARG PHP_SRC_VERSION= +ARG LSPHP_SOURCE=litespeed-apt +RUN --mount=type=bind,from=lsphp-src,source=/lsphp-src-out,target=/lsphp-src-out,rw \ + --mount=type=bind,source=scripts,target=/mnt/scripts \ + install -m 0644 /mnt/scripts/lsphp-src-probe.php /usr/local/share/lsphp-src-probe.php && \ + bash /mnt/scripts/lsphp-src-overlay.sh dev "${PHPVER}" && \ + rm -f /usr/local/share/lsphp-src-probe.php + COPY ./ext/cac-path-parity /usr/src/cac-path-parity RUN set -e; \ cd /usr/src/cac-path-parity; \ @@ -189,6 +223,18 @@ RUN set -e; \ fi; \ echo "runtime lsphp${PHPVER} pinned to $V (the version cac_path_parity was built against)" +## EMERGENCY PATH: overlay the php.net lsphp (no-op on normal builds) and prove +## it has the same extension/feature surface as the packaged one just installed +## — see scripts/lsphp-src-overlay.sh. Bind mounts, so neither the staged build +## nor the scripts add a layer to the image. +ARG PHP_SRC_VERSION= +ARG LSPHP_SOURCE=litespeed-apt +RUN --mount=type=bind,from=lsphp-src,source=/lsphp-src-out,target=/lsphp-src-out,rw \ + --mount=type=bind,source=scripts,target=/mnt/scripts \ + install -m 0644 /mnt/scripts/lsphp-src-probe.php /usr/local/share/lsphp-src-probe.php && \ + bash /mnt/scripts/lsphp-src-overlay.sh runtime "${PHPVER}" && \ + rm -f /usr/local/share/lsphp-src-probe.php + ## Scripts + the SHARED production lsphp ini (reused verbatim from the litespeed ## image — same runtime, same tuning). Scripts layer last (they change most). COPY ./scripts/entrypoint-lsphp.sh \ @@ -250,8 +296,14 @@ ENV LSPHP_ENABLE_USER_INI=on ## docker buildx imagetools inspect --format '{{json .Image.Config.Labels}}' ## Values come from CI's repo-index lookup and were asserted against the ## installed packages by lsphp-apt-upgrade.sh above ("unverified" = local build). -LABEL org.whp.php-version="${EXPECT_PHP_VERSION:-unverified}" \ +## Emergency (php.net) builds: org.whp.php-version is the php.net version the +## overlay step asserted; org.whp.lsphp-package-version still names the LiteSpeed +## packages underneath (their ini/PECL files are what the image keeps); +## org.whp.lsphp-source says which one is running. /etc/cac-lsphp-source has the +## tarball sha256 + signer. +LABEL org.whp.php-version="${PHP_SRC_VERSION:-${EXPECT_PHP_VERSION:-unverified}}" \ org.whp.lsphp-package-version="${EXPECT_LSPHP_PKG:-unverified}" \ + org.whp.lsphp-source="${LSPHP_SOURCE}" \ org.whp.php-flavour="lsphp${PHPVER}" EXPOSE 9000 diff --git a/docs/lsphp-source-emergency.md b/docs/lsphp-source-emergency.md new file mode 100644 index 0000000..0d6c456 --- /dev/null +++ b/docs/lsphp-source-emergency.md @@ -0,0 +1,139 @@ +# Emergency lsphp from php.net source + +Use this only when PHP has published a security release and the LiteSpeed apt +repo (`rpms.litespeedtech.com/debian`, noble) still ships the vulnerable lsphp. +The emergency build replaces the PHP inside `cac-lsphp` and `cac-litespeed` with +the same PHP version compiled from the php.net tarball. Everything else in the +image stays as LiteSpeed packages it. The build fails unless the result offers +the same support as the packaged lsphp. + +This path was first built for php2shell (CVE-2026-109618/-109619, 2026-10-21): +php.net released 8.2.35, 8.3.36, 8.4.27 and 8.5.12, and LiteSpeed was still on +8.2.34, 8.3.35, 8.4.26 and 8.5.11. + +## Trigger + +```bash +# 1. Staging tags only (:phpNN-phpnet-X.Y.Z). Test these on .148 first. +curl -fsS -X POST -H "Authorization: token $TEA_TOKEN" -H 'Content-Type: application/json' \ + https://repo.anhonesthost.net/api/v1/repos/cloud-hosting-platform/cloud-apache-container/actions/workflows/lsphp-source-emergency.yaml/dispatches \ + -d '{"ref":"trunk","inputs":{"php_versions":"82=8.2.35 83=8.3.36 84=8.4.27 85=8.5.12","images":"both","push_production_tags":"false"}}' + +# 2. Production tags (:phpNN, :latest for 85). WHP pulls these on recreate. +# The JSON is the same as step 1 except push_production_tags is "true". +``` + +Inputs: + +- `php_versions`: space-separated `NN=X.Y.Z` pairs, with NN between 81 and 85. +- `images`: `both`, `lsphp` or `litespeed`. +- `push_production_tags`: `true` or `false`. + +The job for a given PHP version refuses to run if php.net has no such release. +It also refuses if LiteSpeed already ships that version or a newer one; in that +case, run the normal build instead. + +After pushing, check the labels on the pushed image: + +```bash +docker buildx imagetools inspect repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php85 \ + --format '{{json .Image.Config.Labels}}' +# org.whp.lsphp-source = php.net, org.whp.php-version = 8.5.12 +``` + +Inside the container, `/etc/cac-lsphp-source` records the tarball sha256, the +signer fingerprint and the packaged release that was replaced. + +Running containers do not change until they are recreated. Follow the normal +deploy order: test.148, whp02, sdbees, then whp01. + +## Getting back to the packaged lsphp + +When LiteSpeed publishes the fixed lsphp, run the normal `build-push.yaml` +build, either with a push to trunk or a dispatch. It overwrites `:phpNN` with +the packaged build once the packaged version is the same as or newer than the +php.net version. + +Until that happens, the normal build refuses to overwrite a newer php.net image. +It fails at the "Do not roll back a newer php.net emergency image" step. This is +intentional, so a routine push during the emergency does not put the vulnerable +PHP back. That step lets the build through if it cannot inspect the published +tag. + +## Local build + +```bash +docker build -f Dockerfile.lsphp --build-arg PHPVER=85 --build-arg OLS_VERSION=1.8.4 \ + --build-arg CACHE_BUST=$(date +%s) \ + --build-arg PHP_SRC_VERSION=8.5.12 \ + --build-arg PHP_SRC_SHA256= \ + --build-arg LSPHP_SOURCE=php.net -t localhost/cac-lsphp-src:php85-test . +``` + +The same build args work with `Dockerfile.litespeed`. Without +`PHP_SRC_VERSION`, both Dockerfiles build exactly as before: the `lsphp-src` +stage and the overlay steps do nothing. + +## What the build does and what makes it fail + +The build has four parts. + +1. The `lsphp-src` stage runs `scripts/lsphp-src-build.sh`, FROM the same + `litespeedtech/openlitespeed` base as the shipped image. +2. In `Dockerfile.lsphp`, the ext-build stage runs `scripts/lsphp-src-overlay.sh dev`. + This builds `cac_path_parity` and runs its `.phpt` suite against the php.net + PHP. +3. The shipped stage runs `scripts/lsphp-src-overlay.sh runtime`. +4. The labels record the source: `org.whp.php-version` is the php.net version + and `org.whp.lsphp-source` is `php.net`. `org.whp.lsphp-package-version` + still names the LiteSpeed packages underneath. + +Each of the following fails the build: + +| Check | Fails when | +|---|---| +| Tarball sha256 | It does not match `PHP_SRC_SHA256`, which CI takes from the php.net releases JSON. | +| OpenPGP signature | `gpgv` rejects the `.asc` against `keys/php-src/php-keyring.asc`, or the signer's primary fingerprint is not listed for that branch in `keys/php-src/release-managers.txt`. | +| Version sanity | The version is not X.Y.Z, is not in the PHPVER branch, or is not newer than the packaged version. | +| Configure | Replaying the packaged lsphp's own `Configure Command` fails. The usual cause is a `-dev` package missing from `DEVPKGS`. | +| LSAPI SAPI | `php-litespeed-8.3.tgz` does not match its pinned sha256, or the built lsphp's set of `LSAPI_*` names differs from the packaged binary's. | +| Compiled-in ini defaults | Any `php -n` ini default differs from the packaged binary. Directives that are new in the release are allowed and printed. | +| ABI gate | A struct member's offset or size changed, or a non-`*_globals` struct changed size. See `scripts/lsphp-abi-gate.sh`. | +| File replacement | A php-src ELF file owned by a package at the core version has no php.net counterpart. | +| Same-support gate (both SAPIs) | Loaded extensions differ, a PECL or Zend extension changes version, a php-src extension is not at the new version, an effective ini value changes or disappears, or a gd/curl/openssl/icu/imagick/redis/memcached/password/hash/stream feature line changes. | +| Build-env consistency | `Configure Command` differs beyond CFLAGS, an ELF file has an unresolved library, or the packaged release in the builder stage differs from the one in the shipped stage. | + +## Differences from the packaged build + +These were measured on 2026-10-21 for php85 and php82 (`cac-lsphp`) and php83 +(`cac-litespeed`): + +- Version strings, build date and build host. +- `memory_hard_limit_percent` exists. The php2shell release added this ini + directive. +- The bundled Olson timezone DB is 2026.5 instead of 2026.3 for 8.5. It comes + from the newer tarball. +- LDAP "Vendor Version" is 20610 instead of 20607. This is the version of the + OpenLDAP headers it was compiled against; noble's libldap-dev is newer now. + The runtime library is the same Ubuntu libldap. +- CFLAGS has no `-ffile-prefix-map` or `-fdebug-prefix-map` pointing at + LiteSpeed's build directory. +- One more `_chk` (FORTIFY) symbol is used. Shared extensions get BIND_NOW, + which is stricter than the packaged build. +- The image is about 100 MB larger. The overlay layer holds a second copy of + lsphp, php and php8.M. + +## Maintenance: what must stay in sync + +| Item | When it needs attention | How you find out | +|---|---|---| +| `keys/php-src/release-managers.txt` and `php-keyring.asc` | A new release manager, or a new PHP branch (8.6). | The build fails because the signer is not pinned or there is no key. Refresh from and . | +| `keys/php-src/litespeed-sapi.txt` | LiteSpeed publishes a new php-litespeed release and builds lsphp with it. | The build fails on the sha256 mismatch or on the LSAPI name-set difference. Re-measure the packaged binary, then re-pin. | +| `DEVPKGS` in `lsphp-src-build.sh` | LiteSpeed adds a configure flag that needs a new `-dev` library. | configure fails and prints the error. | +| session.save_path patch (step 2c) | LiteSpeed patches some other compiled-in default. | The compiled-in ini defaults check fails and shows a diff. | +| PECL reuse | A PHP patch release changes a struct layout. | The ABI gate fails. PECL extensions would then have to be rebuilt from source, which this path does not do. Without that, the emergency path cannot be used for that release. | +| 8.1 | php.net no longer publishes 8.1 (EOL). | The workflow refuses because there is no release. | + +No configure flags, extension lists or PECL versions are kept in this repo. +They are read from the packaged lsphp in the same build, so they cannot go out +of date unnoticed. diff --git a/keys/php-src/litespeed-sapi.txt b/keys/php-src/litespeed-sapi.txt new file mode 100644 index 0000000..13446e6 --- /dev/null +++ b/keys/php-src/litespeed-sapi.txt @@ -0,0 +1,13 @@ +# LiteSpeed's own LSAPI SAPI source, which the LiteSpeed lsphp packages compile +# INSTEAD of php-src's bundled sapi/litespeed (measured 2026-10-21 on packaged +# lsphp81..85: the binaries carry LSAPI_MAX_CMD_SCRIPT_PATH_LEN, report +# "Copyright (c) 1997-2016" and phpversion('litespeed') = '' — all three are +# php-litespeed-8.3, none is php-src 8.5.12's sapi/litespeed). +# LiteSpeed publishes no signature for it; this sha256 was taken on 2026-10-21 +# (Last-Modified: Sun, 07 Sep 2025 20:35:32 GMT) and lsphp-src-build.sh refuses +# any other file. lsphp-src-build.sh additionally checks that the resulting +# binary has the same LSAPI_* feature/env-name set as the packaged lsphp, so a +# LiteSpeed SAPI upgrade we have not pinned yet fails the build instead of +# shipping an older SAPI. +# +https://www.litespeedtech.com/packages/lsapi/php-litespeed-8.3.tgz cd616b82cc853e5e3187fccd060228b9cd1a2009ce72d8da5950eef950ac29b1 diff --git a/keys/php-src/php-keyring.asc b/keys/php-src/php-keyring.asc new file mode 100644 index 0000000..0c6cf79 --- /dev/null +++ b/keys/php-src/php-keyring.asc @@ -0,0 +1,804 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +=SWGM +-----END PGP PUBLIC KEY BLOCK----- diff --git a/keys/php-src/release-managers.txt b/keys/php-src/release-managers.txt new file mode 100644 index 0000000..6798a80 --- /dev/null +++ b/keys/php-src/release-managers.txt @@ -0,0 +1,22 @@ +# PHP release-manager signing keys, per branch: +# Transcribed from https://www.php.net/gpg-keys.php (2026-10-21). The key material +# is php-keyring.asc in this directory, the official keyring from +# https://www.php.net/distributions/php-keyring.gpg (saved unmodified; it is ASCII-armoured despite its .gpg name). +# lsphp-src-build.sh accepts a tarball only if gpgv validates its .asc against +# php-keyring.asc AND the signing key's PRIMARY fingerprint is listed here for +# the tarball's branch. A new release manager = a reviewed commit to both files. +8.1 528995BFEDFBA7191D46839EF9BA0ADA31CBD89E Joe Watkins +8.1 39B641343D8C104B2B146DC3F9C39DC0B9698544 Ben Ramsey +8.1 F1F692238FBC1666E5A5CCD4199F9DFEF6FFBAFD Patrick Allaert +8.2 1198C0117593497A5EC5C199286AF1F9897469DC Pierrick Charron +8.2 39B641343D8C104B2B146DC3F9C39DC0B9698544 Ben Ramsey +8.2 E60913E4DF209907D8E30D96659A97C9CF2A795A Sergey Panteleev +8.3 1198C0117593497A5EC5C199286AF1F9897469DC Pierrick Charron +8.3 AFD8691FDAEDF03BDF6E460563F15A9B715376CA Eric A Mann +8.3 C28D937575603EB4ABB725861C0779DC5C0A9DE4 Jakub Zelenka +8.4 AFD8691FDAEDF03BDF6E460563F15A9B715376CA Eric A Mann +8.4 9D7F99A0CB8F05C8A6958D6256A97AF7600A39A6 Calvin Buckley +8.4 0616E93D95AF471243E26761770426E17EBBB3DD Saki Takamachi +8.5 1198C0117593497A5EC5C199286AF1F9897469DC Pierrick Charron +8.5 49D9AF6BC72A80D6691719C8AA23F5BE9C7097D4 Volker Dusch +8.5 D95C03BC702BE9515344AE3374E44BC9067701A5 Daniel Scherzer diff --git a/scripts/lsphp-abi-gate.sh b/scripts/lsphp-abi-gate.sh new file mode 100755 index 0000000..4795662 --- /dev/null +++ b/scripts/lsphp-abi-gate.sh @@ -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 # and S + ## 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)" diff --git a/scripts/lsphp-src-build.sh b/scripts/lsphp-src-build.sh new file mode 100755 index 0000000..e6b158b --- /dev/null +++ b/scripts/lsphp-src-build.sh @@ -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 +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 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" <"$OUT/configure-args.txt" +log "done in $(( $(date +%s) - START ))s: php.net ${PHP_SRC_VERSION} staged at $OUT/root (reference lsphp${PHPVER} ${PKGV})" diff --git a/scripts/lsphp-src-overlay.sh b/scripts/lsphp-src-overlay.sh new file mode 100755 index 0000000..f90bfad --- /dev/null +++ b/scripts/lsphp-src-overlay.sh @@ -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:-}." + +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--{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 < | VERSION | EXT | ZEXT +// INI | FEAT +// The overlay script relies on the stable " " 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"; diff --git a/scripts/tests/lsphp-abi-gate.test.sh b/scripts/tests/lsphp-abi-gate.test.sh new file mode 100755 index 0000000..8cc97e3 --- /dev/null +++ b/scripts/tests/lsphp-abi-gate.test.sh @@ -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" < +#include +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 ] -- 2.52.0 From 42993f42bdf4fc9ff9f30156d9fda4811f888f0d Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Wed, 21 Oct 2026 09:47:44 -0700 Subject: [PATCH 2/2] ci(security): no ${{ }} in run:, validate resolved versions, LiteSpeed index over HTTPS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security review of #30: step outputs derived from the LiteSpeed apt index (fetched over plain HTTP) were spliced into shell via ${{ }} in jobs that hold registry push secrets — a MITM'd "version" would be shell injection. - every run: block in build-push.yaml and lsphp-source-emergency.yaml now receives matrix values, step outputs, inputs and run_id via env: only (incl. the pre-existing php.net / LiteSpeed resolve steps from #24/#29) - resolved versions are regex-checked before GITHUB_OUTPUT: PHP ^[0-9]+\.[0-9]+\.[0-9]+$, deb ^[0-9][0-9A-Za-z.+~:-]*$ - LiteSpeed Packages index fetched over HTTPS (--proto =https); measured rpms.litespeedtech.com serves it (HTTP/2 200) - lsphp-src-build.sh / lsphp-src-overlay.sh validate PHPVER too Co-Authored-By: Claude Opus 5.5 --- .gitea/workflows/build-push.yaml | 78 +++++++++++++++----- .gitea/workflows/lsphp-source-emergency.yaml | 34 ++++++--- scripts/lsphp-src-build.sh | 1 + scripts/lsphp-src-overlay.sh | 1 + 4 files changed, 82 insertions(+), 32 deletions(-) diff --git a/.gitea/workflows/build-push.yaml b/.gitea/workflows/build-push.yaml index 5953a5a..e6d8a20 100644 --- a/.gitea/workflows/build-push.yaml +++ b/.gitea/workflows/build-push.yaml @@ -97,19 +97,26 @@ jobs: # the package-upgrade layers consume CACHE_BUST and can never be reused. - name: Cache-bust value id: bust - run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT" + env: + RUN_ID: ${{ gitea.run_id }} + run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT" # The minimum PHP version this build must ship: php.net's current release for # the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older, # so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP. - name: Resolve current PHP release from php.net id: phpmin + # Values reach the script via env, never via ${{ }} inside run: (a + # crafted string would otherwise be shell); outputs are regex-checked. + env: + PHPVER: ${{ matrix.phpver }} run: | set -euo pipefail - v="${{ matrix.phpver }}"; branch="${v:0:1}.${v:1}" + v="$PHPVER"; branch="${v:0:1}.${v:1}" json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}") min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")') if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi + [[ "$min" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::php.net returned a malformed version for ${branch}"; exit 1; } echo "php.net current ${branch}: ${min}" echo "min=${min}" >> "$GITHUB_OUTPUT" @@ -157,19 +164,26 @@ jobs: # the package-upgrade layers consume CACHE_BUST and can never be reused. - name: Cache-bust value id: bust - run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT" + env: + RUN_ID: ${{ gitea.run_id }} + run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT" # The minimum PHP version this build must ship: php.net's current release for # the branch (over HTTPS). The Dockerfile fails if Remi gave us anything older, # so lagging mirrors or replayed repo metadata can't ship a vulnerable PHP. - name: Resolve current PHP release from php.net id: phpmin + # Values reach the script via env, never via ${{ }} inside run: (a + # crafted string would otherwise be shell); outputs are regex-checked. + env: + PHPVER: ${{ matrix.phpver }} run: | set -euo pipefail - v="${{ matrix.phpver }}"; branch="${v:0:1}.${v:1}" + v="$PHPVER"; branch="${v:0:1}.${v:1}" json=$(curl -fsS --retry 5 --max-time 60 "https://www.php.net/releases/index.php?json&max=1&version=${branch}") min=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(iter(d)) if isinstance(d,dict) and d else "")') if [ -z "$min" ]; then echo "::error::no php.net release found for ${branch}"; exit 1; fi + [[ "$min" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::php.net returned a malformed version for ${branch}"; exit 1; } echo "php.net current ${branch}: ${min}" echo "min=${min}" >> "$GITHUB_OUTPUT" @@ -222,7 +236,9 @@ jobs: # the package-upgrade layers consume CACHE_BUST and can never be reused. - name: Cache-bust value id: bust - run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT" + env: + RUN_ID: ${{ gitea.run_id }} + run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT" # Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the # CURRENT lsphp version. The Dockerfile asserts the installed package equals @@ -230,14 +246,21 @@ jobs: # it into the image labels org.whp.php-version / org.whp.lsphp-package-version. - name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo id: lsphp + # HTTPS (rpms.litespeedtech.com serves it; measured 2026-10-21), values via + # env, and both outputs regex-checked before they reach GITHUB_OUTPUT. + env: + PHPVER: ${{ matrix.phpver }} run: | set -euo pipefail - idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages) - pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1) - if [ -z "$pkg" ]; then echo "::error::lsphp${{ matrix.phpver }} not found in the LiteSpeed repo index"; exit 1; fi - echo "lsphp${{ matrix.phpver }} in repo: $pkg (PHP ${pkg%%-*})" + idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages) + pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1) + if [ -z "$pkg" ]; then echo "::error::lsphp${PHPVER} not found in the LiteSpeed repo index"; exit 1; fi + php="${pkg%%-*}" + [[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; } + [[ "$php" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '${php}' derived from lsphp${PHPVER} ${pkg}"; exit 1; } + echo "lsphp${PHPVER} in repo: $pkg (PHP ${php})" echo "pkg=$pkg" >> "$GITHUB_OUTPUT" - echo "php=${pkg%%-*}" >> "$GITHUB_OUTPUT" + echo "php=${php}" >> "$GITHUB_OUTPUT" - name: Login to Gitea uses: docker/login-action@v3 @@ -252,12 +275,15 @@ jobs: # the job goes green again by itself once the LiteSpeed repo catches up. # Fail-open if the tag cannot be inspected (first build, registry blip). - name: Do not roll back a newer php.net emergency image + env: + PHPVER: ${{ matrix.phpver }} + NEW_PHP: ${{ steps.lsphp.outputs.php }} run: | set -euo pipefail - ref=repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }} + ref="repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${PHPVER}" src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true) cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true) - new='${{ steps.lsphp.outputs.php }}' + new="$NEW_PHP" echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new" if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed." @@ -313,7 +339,9 @@ jobs: # the package-upgrade layers consume CACHE_BUST and can never be reused. - name: Cache-bust value id: bust - run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT" + env: + RUN_ID: ${{ gitea.run_id }} + run: echo "value=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT" # Ask the LiteSpeed apt repo (the same one the base image's apt uses) for the # CURRENT lsphp version. The Dockerfile asserts the installed package equals @@ -321,14 +349,21 @@ jobs: # it into the image labels org.whp.php-version / org.whp.lsphp-package-version. - name: Resolve current lsphp${{ matrix.phpver }} from the LiteSpeed repo id: lsphp + # HTTPS (rpms.litespeedtech.com serves it; measured 2026-10-21), values via + # env, and both outputs regex-checked before they reach GITHUB_OUTPUT. + env: + PHPVER: ${{ matrix.phpver }} run: | set -euo pipefail - idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages) - pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1) - if [ -z "$pkg" ]; then echo "::error::lsphp${{ matrix.phpver }} not found in the LiteSpeed repo index"; exit 1; fi - echo "lsphp${{ matrix.phpver }} in repo: $pkg (PHP ${pkg%%-*})" + idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages) + pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1) + if [ -z "$pkg" ]; then echo "::error::lsphp${PHPVER} not found in the LiteSpeed repo index"; exit 1; fi + php="${pkg%%-*}" + [[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; } + [[ "$php" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '${php}' derived from lsphp${PHPVER} ${pkg}"; exit 1; } + echo "lsphp${PHPVER} in repo: $pkg (PHP ${php})" echo "pkg=$pkg" >> "$GITHUB_OUTPUT" - echo "php=${pkg%%-*}" >> "$GITHUB_OUTPUT" + echo "php=${php}" >> "$GITHUB_OUTPUT" - name: Login to Gitea uses: docker/login-action@v3 @@ -343,12 +378,15 @@ jobs: # the job goes green again by itself once the LiteSpeed repo catches up. # Fail-open if the tag cannot be inspected (first build, registry blip). - name: Do not roll back a newer php.net emergency image + env: + PHPVER: ${{ matrix.phpver }} + NEW_PHP: ${{ steps.lsphp.outputs.php }} run: | set -euo pipefail - ref=repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${{ matrix.phpver }} + ref="repo.anhonesthost.net/cloud-hosting-platform/cac-lsphp:php${PHPVER}" src=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.lsphp-source" }}' 2>/dev/null || true) cur=$(docker buildx imagetools inspect "$ref" --format '{{ index .Image.Config.Labels "org.whp.php-version" }}' 2>/dev/null || true) - new='${{ steps.lsphp.outputs.php }}' + new="$NEW_PHP" echo "published $ref: source=${src:-?} php=${cur:-?}; this build: packaged $new" if [ "$src" = "php.net" ] && [ "$cur" != "$new" ] && [ "$(printf '%s\n%s\n' "$cur" "$new" | sort -V | tail -n1)" = "$cur" ]; then echo "::error::$ref is a php.net emergency build of PHP $cur; LiteSpeed still ships $new. Not overwriting it with an older PHP. Re-run lsphp-source-emergency.yaml for a newer fix, or wait for LiteSpeed." diff --git a/.gitea/workflows/lsphp-source-emergency.yaml b/.gitea/workflows/lsphp-source-emergency.yaml index 5015b80..e483156 100644 --- a/.gitea/workflows/lsphp-source-emergency.yaml +++ b/.gitea/workflows/lsphp-source-emergency.yaml @@ -49,6 +49,8 @@ jobs: IN_VERSIONS: ${{ inputs.php_versions }} IN_IMAGES: ${{ inputs.images }} IN_PROD: ${{ format('{0}', inputs.push_production_tags) }} + PHPVER: ${{ matrix.phpver }} + IMG: ${{ matrix.image }} run: | set -euo pipefail case "$IN_IMAGES" in both|lsphp|litespeed) ;; *) echo "::error::images must be both|lsphp|litespeed, got '$IN_IMAGES'"; exit 1 ;; esac @@ -56,16 +58,17 @@ jobs: v="" for pair in $IN_VERSIONS; do [[ "$pair" =~ ^(8[1-5])=([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "::error::bad php_versions entry '$pair' (want NN=X.Y.Z, NN in 81..85)"; exit 1; } - [ "${BASH_REMATCH[1]}" = '${{ matrix.phpver }}' ] && v="${BASH_REMATCH[2]}" + [ "${BASH_REMATCH[1]}" = "$PHPVER" ] && v="${BASH_REMATCH[2]}" done echo "prod=$IN_PROD" >> "$GITHUB_OUTPUT" - if [ -z "$v" ] || { [ "$IN_IMAGES" != both ] && [ "$IN_IMAGES" != '${{ matrix.image }}' ]; }; then - echo "not requested: php${{ matrix.phpver }} / ${{ matrix.image }}"; echo "skip=1" >> "$GITHUB_OUTPUT"; exit 0 + if [ -z "$v" ] || { [ "$IN_IMAGES" != both ] && [ "$IN_IMAGES" != "$IMG" ]; }; then + echo "not requested: php${PHPVER} / ${IMG}"; echo "skip=1" >> "$GITHUB_OUTPUT"; exit 0 fi echo "skip=0" >> "$GITHUB_OUTPUT" echo "version=$v" >> "$GITHUB_OUTPUT" - echo "image_name=cac-${{ matrix.image }}" >> "$GITHUB_OUTPUT" - echo "dockerfile=./Dockerfile.${{ matrix.image }}" >> "$GITHUB_OUTPUT" + case "$IMG" in lsphp|litespeed) ;; *) echo "::error::unexpected matrix image '$IMG'"; exit 1 ;; esac + echo "image_name=cac-${IMG}" >> "$GITHUB_OUTPUT" + echo "dockerfile=./Dockerfile.${IMG}" >> "$GITHUB_OUTPUT" - name: Checkout if: steps.t.outputs.skip == '0' @@ -77,26 +80,33 @@ jobs: - name: Resolve php.net release + packaged lsphp if: steps.t.outputs.skip == '0' id: v + env: + V: ${{ steps.t.outputs.version }} + PHPVER: ${{ matrix.phpver }} + RUN_ID: ${{ gitea.run_id }} run: | set -euo pipefail - v='${{ steps.t.outputs.version }}' + v="$V" + [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed version '$v'"; exit 1; } json=$(curl -fsS --retry 3 --max-time 60 "https://www.php.net/releases/index.php?json&version=$v") sha=$(printf '%s' "$json" | python3 -c 'import json,sys; d=json.load(sys.stdin); v=sys.argv[1]; print(next((s["sha256"] for s in d.get("source", []) if s.get("filename")=="php-%s.tar.xz"%v), ""))' "$v") [ -n "$sha" ] || { echo "::error::php.net has no release php-$v.tar.xz (EOL branch? typo?): $json"; exit 1; } [[ "$sha" =~ ^[0-9a-f]{64}$ ]] || { echo "::error::no sha256 for php-$v.tar.xz in the php.net releases JSON"; exit 1; } - idx=$(curl -fsS --retry 3 --max-time 60 http://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages) - pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${{ matrix.phpver }}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1) - [ -n "$pkg" ] || { echo "::error::lsphp${{ matrix.phpver }} not in the LiteSpeed repo index"; exit 1; } + idx=$(curl -fsS --proto '=https' --retry 3 --max-time 60 https://rpms.litespeedtech.com/debian/dists/noble/main/binary-amd64/Packages) + pkg=$(printf '%s\n' "$idx" | awk -v p="lsphp${PHPVER}" '/^Package: /{n=$2} /^Version: / && n==p {print $2}' | sort -V | tail -n1) + [ -n "$pkg" ] || { echo "::error::lsphp${PHPVER} not in the LiteSpeed repo index"; exit 1; } + [[ "$pkg" =~ ^[0-9][0-9A-Za-z.+~:-]*$ ]] || { echo "::error::malformed lsphp${PHPVER} package version in the repo index"; exit 1; } pphp=${pkg%%-*} + [[ "$pphp" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "::error::malformed PHP version '$pphp' from lsphp${PHPVER} $pkg"; exit 1; } if [ "$(printf '%s\n%s\n' "$pphp" "$v" | sort -V | tail -n1)" != "$v" ] || [ "$pphp" = "$v" ]; then - echo "::error::LiteSpeed already ships lsphp${{ matrix.phpver }} $pkg (PHP $pphp) >= $v — use the normal build (push to trunk / dispatch build-push.yaml)." + echo "::error::LiteSpeed already ships lsphp${PHPVER} $pkg (PHP $pphp) >= $v — use the normal build (push to trunk / dispatch build-push.yaml)." exit 1 fi - echo "php.net $v sha256=$sha ; LiteSpeed lsphp${{ matrix.phpver }}=$pkg" + echo "php.net $v sha256=$sha ; LiteSpeed lsphp${PHPVER}=$pkg" echo "sha=$sha" >> "$GITHUB_OUTPUT" echo "pkg=$pkg" >> "$GITHUB_OUTPUT" echo "pkgphp=$pphp" >> "$GITHUB_OUTPUT" - echo "bust=$(date -u +%Y%m%dT%H%M%SZ)-${{ gitea.run_id }}" >> "$GITHUB_OUTPUT" + echo "bust=$(date -u +%Y%m%dT%H%M%SZ)-${RUN_ID}" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx if: steps.t.outputs.skip == '0' diff --git a/scripts/lsphp-src-build.sh b/scripts/lsphp-src-build.sh index e6b158b..e5d390e 100755 --- a/scripts/lsphp-src-build.sh +++ b/scripts/lsphp-src-build.sh @@ -59,6 +59,7 @@ die() { echo "FATAL(lsphp-src-build): $*" >&2; exit 1; } log() { echo "lsphp-src-build: $*"; } PHPVER="${PHPVER:?PHPVER required}" +[[ "$PHPVER" =~ ^[0-9]{2}$ ]] || die "PHPVER='$PHPVER' is not NN." 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." diff --git a/scripts/lsphp-src-overlay.sh b/scripts/lsphp-src-overlay.sh index f90bfad..ce69695 100755 --- a/scripts/lsphp-src-overlay.sh +++ b/scripts/lsphp-src-overlay.sh @@ -37,6 +37,7 @@ 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 +[[ "$PHPVER" =~ ^[0-9]{2}$ ]] || { echo "FATAL(lsphp-src-overlay): PHPVER='$PHPVER' is not NN" >&2; exit 1; } die() { echo "FATAL(lsphp-src-overlay): $*" >&2; exit 1; } log() { echo "lsphp-src-overlay[$MODE]: $*"; } -- 2.52.0