Files
cloud-apache-container/docs/lsphp-source-emergency.md
shadowdaoandClaude Opus 5.5 83c2ee9c2a 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>
2026-10-21 09:46:22 -07:00

7.9 KiB

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

# 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:

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

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=<sha256 of php-8.5.12.tar.xz from https://www.php.net/releases/index.php?json&version=8.5.12> \
  --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 https://www.php.net/gpg-keys.php and https://www.php.net/distributions/php-keyring.gpg.
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.