Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76fc53358c | ||
|
|
fc60ac5ef3 | ||
|
|
472fdcbc4d | ||
|
|
cb0cecb557 | ||
|
|
2b9e7f302c | ||
|
|
18f65c6d84 | ||
|
|
b2bec5b3ee | ||
|
|
fa7808897f | ||
|
|
42993f42bd | ||
|
|
83c2ee9c2a | ||
|
|
be1540f559 | ||
|
|
af7fd193e5 | ||
|
|
e8e3bfd490 | ||
|
|
c49f456336 | ||
|
|
bcd737a9e2 | ||
|
|
955aec4573 | ||
|
|
998eb17f39 | ||
|
|
f461d22aae | ||
|
|
cfde82ae44 | ||
|
|
132e591503 | ||
|
|
642b97707d | ||
|
|
71ca114529 | ||
|
|
74e12407f8 | ||
|
|
74648ba34b | ||
|
|
86a88c87db | ||
|
|
02d93f4d1a | ||
|
|
94a73ea8ab | ||
|
|
0ffccac4b8 | ||
|
|
eaf616eb30 | ||
|
|
14761597bb | ||
|
|
db19ee24c3 | ||
|
|
3841b18820 | ||
|
|
48b714447f | ||
|
|
ba9650ee45 | ||
|
|
77af001af6 | ||
|
|
cf6936e225 | ||
|
|
b92725d2ec | ||
|
|
5e83c8db3b | ||
|
|
a325615690 | ||
|
|
e28c3dcce4 | ||
|
|
584099ff19 | ||
|
|
8790b027a9 | ||
|
|
9343a56ccf | ||
|
|
3047123f2b | ||
|
|
07378506a7 | ||
|
|
9761157a6b | ||
|
|
61bfdcfaf9 | ||
|
|
fb4946641a | ||
|
|
690ff8738d | ||
|
|
06df1c410b | ||
|
|
a3aa9f2b26 | ||
|
|
83522b00ef | ||
|
|
15e304e0c3 | ||
|
|
da16faaff5 | ||
|
|
03b8f3f730 |
No files matched your search
@@ -4,22 +4,151 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- trunk
|
- trunk
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
Build-and-Push:
|
# Shell gate. Runs FIRST and costs seconds; the images below do not depend on
|
||||||
|
# it (a red job here does not block a push that is otherwise fine), but it is
|
||||||
|
# the only place the ENTRYPOINT logic is executed at all. The .phpt suite and
|
||||||
|
# the Dockerfile's `lsphp -i` probe both test the extension, and both were
|
||||||
|
# green for the release whose entrypoint declared that same extension
|
||||||
|
# missing — see scripts/tests/lsphp-info-probe.test.sh for what went wrong
|
||||||
|
# and why it needed a test outside the image build to catch it.
|
||||||
|
Shell-Checks:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
phpver: [74, 80, 81, 82, 83, 84, 85]
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Runner images differ on whether they are root and whether sudo exists.
|
||||||
|
- name: Install shellcheck
|
||||||
|
run: |
|
||||||
|
if ! command -v shellcheck >/dev/null 2>&1; then
|
||||||
|
(apt-get update && apt-get install -y shellcheck) ||
|
||||||
|
(sudo apt-get update && sudo apt-get install -y shellcheck)
|
||||||
|
fi
|
||||||
|
shellcheck --version
|
||||||
|
|
||||||
|
- name: Syntax check every shell script
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
find scripts ext -name '*.sh' -print0 | xargs -0 -n1 bash -n
|
||||||
|
|
||||||
|
# Deliberately NOT repo-wide. The older scripts (entrypoint.sh,
|
||||||
|
# entrypoint-fpm.sh, create-vhost.sh, create-php-config.sh,
|
||||||
|
# detect-memory.sh) carry pre-existing SC2154/SC2027 findings that predate
|
||||||
|
# this job; listing them here would make the gate red on arrival and
|
||||||
|
# therefore ignored. This is the set that is clean today — the scripts
|
||||||
|
# that run `set -o pipefail` plus the tests. Add files as they are fixed;
|
||||||
|
# do not add one that is not yet clean.
|
||||||
|
- name: shellcheck (warnings and above, on the clean set)
|
||||||
|
run: |
|
||||||
|
shellcheck -S warning \
|
||||||
|
scripts/entrypoint-lsphp.sh \
|
||||||
|
scripts/entrypoint-litespeed.sh \
|
||||||
|
scripts/entrypoint-shared-ols.sh \
|
||||||
|
scripts/render-shared-ols-config.sh \
|
||||||
|
scripts/ols-htaccess-watcher.sh \
|
||||||
|
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-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:
|
||||||
|
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||||||
|
# security builds of the others.
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
phpver: [82, 83, 84, 85]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||||||
|
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||||||
|
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||||||
|
# exactly as before. The repo cache and the registry mirror are probed
|
||||||
|
# separately so either can fail back on its own.
|
||||||
|
- name: Probe package cache
|
||||||
|
id: pkgcache
|
||||||
|
run: |
|
||||||
|
url=""; mirror=""
|
||||||
|
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||||||
|
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||||||
|
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
|
||||||
|
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||||||
|
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||||||
|
# so the mirror is configured on the builder; BuildKit itself still falls
|
||||||
|
# back to Docker Hub if the mirror stops answering mid-run.
|
||||||
|
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||||||
|
if: steps.pkgcache.outputs.mirror != ''
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
buildkitd-config-inline: |
|
||||||
|
[registry."docker.io"]
|
||||||
|
mirrors = ["192.168.1.40:5000"]
|
||||||
|
[registry."192.168.1.40:5000"]
|
||||||
|
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: steps.pkgcache.outputs.mirror == ''
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||||||
|
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
||||||
|
# cached between runs — this keeps it that way if a cache is ever added:
|
||||||
|
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
||||||
|
- name: Cache-bust value
|
||||||
|
id: bust
|
||||||
|
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="$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"
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -35,6 +164,9 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=${{ matrix.phpver }}
|
PHPVER=${{ matrix.phpver }}
|
||||||
|
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||||||
|
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||||
|
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
||||||
tags: |
|
tags: |
|
||||||
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
|
repo.anhonesthost.net/cloud-hosting-platform/cac:php${{ matrix.phpver }}
|
||||||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
|
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac:latest' || '' }}
|
||||||
@@ -42,17 +174,79 @@ jobs:
|
|||||||
Build-FPM-Images:
|
Build-FPM-Images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||||||
|
# security builds of the others.
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
phpver: [74, 80, 81, 82, 83, 84, 85]
|
phpver: [82, 83, 84, 85]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||||||
|
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||||||
|
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||||||
|
# exactly as before. The repo cache and the registry mirror are probed
|
||||||
|
# separately so either can fail back on its own.
|
||||||
|
- name: Probe package cache
|
||||||
|
id: pkgcache
|
||||||
|
run: |
|
||||||
|
url=""; mirror=""
|
||||||
|
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||||||
|
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||||||
|
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
|
||||||
|
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||||||
|
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||||||
|
# so the mirror is configured on the builder; BuildKit itself still falls
|
||||||
|
# back to Docker Hub if the mirror stops answering mid-run.
|
||||||
|
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||||||
|
if: steps.pkgcache.outputs.mirror != ''
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
buildkitd-config-inline: |
|
||||||
|
[registry."docker.io"]
|
||||||
|
mirrors = ["192.168.1.40:5000"]
|
||||||
|
[registry."192.168.1.40:5000"]
|
||||||
|
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: steps.pkgcache.outputs.mirror == ''
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||||||
|
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
||||||
|
# cached between runs — this keeps it that way if a cache is ever added:
|
||||||
|
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
||||||
|
- name: Cache-bust value
|
||||||
|
id: bust
|
||||||
|
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="$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"
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -69,6 +263,9 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=${{ matrix.phpver }}
|
PHPVER=${{ matrix.phpver }}
|
||||||
|
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||||||
|
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||||
|
EXPECT_PHP_MIN=${{ steps.phpmin.outputs.min }}
|
||||||
tags: |
|
tags: |
|
||||||
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
|
repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:php${{ matrix.phpver }}
|
||||||
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
|
${{ matrix.phpver == '85' && 'repo.anhonesthost.net/cloud-hosting-platform/cac-fpm:latest' || '' }}
|
||||||
@@ -76,12 +273,15 @@ jobs:
|
|||||||
Build-LiteSpeed-Images:
|
Build-LiteSpeed-Images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||||||
|
# security builds of the others.
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
|
# PHP 7.4/8.0 deliberately excluded — the LiteSpeed prebuilt base
|
||||||
# images stop at older OLS releases for those PHP versions, and the
|
# images stop at older OLS releases for those PHP versions, and the
|
||||||
# cac-litespeed tier is a paid premium offering: 8.1+ is the
|
# cac-litespeed tier is a paid premium offering: 8.1+ is the
|
||||||
# modernization story we're selling.
|
# modernization story we're selling.
|
||||||
phpver: [81, 82, 83, 84, 85]
|
phpver: [82, 83, 84, 85]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -92,6 +292,38 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||||||
|
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
||||||
|
# cached between runs — this keeps it that way if a cache is ever added:
|
||||||
|
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
||||||
|
- name: Cache-bust value
|
||||||
|
id: bust
|
||||||
|
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
|
||||||
|
# this (build fails if the repo changed mid-build — just re-run) and writes
|
||||||
|
# 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 --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=${php}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -99,6 +331,27 @@ jobs:
|
|||||||
username: ${{ secrets.CI_USER }}
|
username: ${{ secrets.CI_USER }}
|
||||||
password: ${{ secrets.CI_TOKEN }}
|
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
|
||||||
|
env:
|
||||||
|
PHPVER: ${{ matrix.phpver }}
|
||||||
|
NEW_PHP: ${{ steps.lsphp.outputs.php }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
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="$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."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build and Push LiteSpeed Image
|
- name: Build and Push LiteSpeed Image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
@@ -108,10 +361,12 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=${{ matrix.phpver }}
|
PHPVER=${{ matrix.phpver }}
|
||||||
OLS_VERSION=1.8.4
|
OLS_VERSION=1.8.4
|
||||||
# OLS_VERSION pinned to 1.8.4 — only release with prebuilt images
|
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||||||
# for every PHP version we ship (1.8.5 and 1.9.0 don't have an
|
EXPECT_LSPHP_PKG=${{ steps.lsphp.outputs.pkg }}
|
||||||
# lsphp81 variant on Docker Hub). Bump alongside a local rebuild
|
EXPECT_PHP_VERSION=${{ steps.lsphp.outputs.php }}
|
||||||
# test when LiteSpeed publishes lsphp81 on a newer OLS release.
|
# OLS_VERSION pinned to 1.8.4. It was the only release with an lsphp81
|
||||||
|
# variant; 8.1 and older are no longer built (2026-10-21), so the pin
|
||||||
|
# can move to a newer OLS release after a local rebuild test.
|
||||||
# See spec: docs/superpowers/specs/2026-06-01-cac-litespeed-design.md
|
# See spec: docs/superpowers/specs/2026-06-01-cac-litespeed-design.md
|
||||||
tags: |
|
tags: |
|
||||||
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
|
repo.anhonesthost.net/cloud-hosting-platform/cac-litespeed:php${{ matrix.phpver }}
|
||||||
@@ -120,12 +375,15 @@ jobs:
|
|||||||
Build-LSPHP-Images:
|
Build-LSPHP-Images:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
|
# One PHP version failing (e.g. an EOL stream) must not cancel the
|
||||||
|
# security builds of the others.
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
|
# Same PHP matrix as cac-litespeed (81–85): cac-lsphp is the detached
|
||||||
# backend for the shared-ols tier and shares the litespeed prebuilt
|
# backend for the shared-ols tier and shares the litespeed prebuilt
|
||||||
# base, which only ships lsphp for 8.1+. Keep this matrix in lockstep
|
# base, which only ships lsphp for 8.1+. Keep this matrix in lockstep
|
||||||
# with Build-LiteSpeed-Images.
|
# with Build-LiteSpeed-Images.
|
||||||
phpver: [81, 82, 83, 84, 85]
|
phpver: [82, 83, 84, 85]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -136,6 +394,38 @@ jobs:
|
|||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# Per-run cache-bust value. Each job's buildx builder is created fresh by
|
||||||
|
# setup-buildx-action (no cache-from/cache-to here), so today nothing is
|
||||||
|
# cached between runs — this keeps it that way if a cache is ever added:
|
||||||
|
# the package-upgrade layers consume CACHE_BUST and can never be reused.
|
||||||
|
- name: Cache-bust value
|
||||||
|
id: bust
|
||||||
|
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
|
||||||
|
# this (build fails if the repo changed mid-build — just re-run) and writes
|
||||||
|
# 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 --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=${php}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
@@ -143,6 +433,27 @@ jobs:
|
|||||||
username: ${{ secrets.CI_USER }}
|
username: ${{ secrets.CI_USER }}
|
||||||
password: ${{ secrets.CI_TOKEN }}
|
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
|
||||||
|
env:
|
||||||
|
PHPVER: ${{ matrix.phpver }}
|
||||||
|
NEW_PHP: ${{ steps.lsphp.outputs.php }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
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="$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."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build and Push lsphp Image
|
- name: Build and Push lsphp Image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
with:
|
with:
|
||||||
@@ -152,6 +463,9 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=${{ matrix.phpver }}
|
PHPVER=${{ matrix.phpver }}
|
||||||
OLS_VERSION=1.8.4
|
OLS_VERSION=1.8.4
|
||||||
|
CACHE_BUST=${{ steps.bust.outputs.value }}
|
||||||
|
EXPECT_LSPHP_PKG=${{ steps.lsphp.outputs.pkg }}
|
||||||
|
EXPECT_PHP_VERSION=${{ steps.lsphp.outputs.php }}
|
||||||
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
|
# OLS_VERSION pinned to 1.8.4 to match Build-LiteSpeed-Images — same
|
||||||
# prebuilt base, same lsphp binaries. Bump both together.
|
# prebuilt base, same lsphp binaries. Bump both together.
|
||||||
tags: |
|
tags: |
|
||||||
@@ -164,10 +478,40 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||||||
|
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||||||
|
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||||||
|
# exactly as before. The repo cache and the registry mirror are probed
|
||||||
|
# separately so either can fail back on its own.
|
||||||
|
- name: Probe package cache
|
||||||
|
id: pkgcache
|
||||||
|
run: |
|
||||||
|
url=""; mirror=""
|
||||||
|
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||||||
|
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||||||
|
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
|
||||||
|
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||||||
|
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||||||
|
# so the mirror is configured on the builder; BuildKit itself still falls
|
||||||
|
# back to Docker Hub if the mirror stops answering mid-run.
|
||||||
|
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||||||
|
if: steps.pkgcache.outputs.mirror != ''
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
buildkitd-config-inline: |
|
||||||
|
[registry."docker.io"]
|
||||||
|
mirrors = ["192.168.1.40:5000"]
|
||||||
|
[registry."192.168.1.40:5000"]
|
||||||
|
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
if: steps.pkgcache.outputs.mirror == ''
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
@@ -183,6 +527,8 @@ jobs:
|
|||||||
file: ./Dockerfile.shared-httpd
|
file: ./Dockerfile.shared-httpd
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
|
build-args: |
|
||||||
|
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||||
tags: |
|
tags: |
|
||||||
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
|
repo.anhonesthost.net/cloud-hosting-platform/shared-httpd:latest
|
||||||
|
|
||||||
@@ -192,10 +538,40 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
# Internal package cache (CT130 pkg-cache, docs/pkg-cache.md). Used ONLY if
|
||||||
|
# it answers over TLS with our pinned CA; otherwise (down, unreachable,
|
||||||
|
# bad/expired cert) this job builds from the upstream repos and Docker Hub
|
||||||
|
# exactly as before. The repo cache and the registry mirror are probed
|
||||||
|
# separately so either can fail back on its own.
|
||||||
|
- name: Probe package cache
|
||||||
|
id: pkgcache
|
||||||
|
run: |
|
||||||
|
url=""; mirror=""
|
||||||
|
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40/; then url="https://192.168.1.40"; fi
|
||||||
|
if curl -fsS --max-time 5 --cacert keys/pkg-cache/ca.crt -o /dev/null https://192.168.1.40:5000/v2/; then mirror="192.168.1.40:5000"; fi
|
||||||
|
echo "pkg-cache: repos=${url:-<unreachable: upstream>} registry-mirror=${mirror:-<unreachable: Docker Hub>}"
|
||||||
|
[ -n "$url" ] || echo "::warning::pkg-cache unreachable - building from upstream repositories"
|
||||||
|
echo "url=${url}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "mirror=${mirror}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
# BuildKit (docker-container driver) ignores dockerd's registry-mirrors,
|
||||||
|
# so the mirror is configured on the builder; BuildKit itself still falls
|
||||||
|
# back to Docker Hub if the mirror stops answering mid-run.
|
||||||
|
- name: Set up Docker Buildx (Docker Hub via pkg-cache mirror)
|
||||||
|
if: steps.pkgcache.outputs.mirror != ''
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
buildkitd-config-inline: |
|
||||||
|
[registry."docker.io"]
|
||||||
|
mirrors = ["192.168.1.40:5000"]
|
||||||
|
[registry."192.168.1.40:5000"]
|
||||||
|
ca = ["${{ github.workspace }}/keys/pkg-cache/ca.crt"]
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
|
if: steps.pkgcache.outputs.mirror == ''
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Gitea
|
- name: Login to Gitea
|
||||||
@@ -216,5 +592,6 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
PHPVER=83
|
PHPVER=83
|
||||||
OLS_VERSION=1.8.4
|
OLS_VERSION=1.8.4
|
||||||
|
PKG_CACHE=${{ steps.pkgcache.outputs.url }}
|
||||||
tags: |
|
tags: |
|
||||||
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest
|
repo.anhonesthost.net/cloud-hosting-platform/shared-ols:latest
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
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: [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) }}
|
||||||
|
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
|
||||||
|
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[2-5])=([0-9]+\.[0-9]+\.[0-9]+)$ ]] || { echo "::error::bad php_versions entry '$pair' (want NN=X.Y.Z, NN in 82..85)"; exit 1; }
|
||||||
|
[ "${BASH_REMATCH[1]}" = "$PHPVER" ] && v="${BASH_REMATCH[2]}"
|
||||||
|
done
|
||||||
|
echo "prod=$IN_PROD" >> "$GITHUB_OUTPUT"
|
||||||
|
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"
|
||||||
|
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'
|
||||||
|
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
|
||||||
|
env:
|
||||||
|
V: ${{ steps.t.outputs.version }}
|
||||||
|
PHPVER: ${{ matrix.phpver }}
|
||||||
|
RUN_ID: ${{ gitea.run_id }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
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 --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${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${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)-${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) || '' }}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
cloud-hosting-platform/cloud-apache-container
|
||||||
+42
-4
@@ -1,13 +1,36 @@
|
|||||||
FROM almalinux/10-base
|
FROM almalinux/10-base
|
||||||
ARG PHPVER=83
|
ARG PHPVER=83
|
||||||
|
# Per-CI-run value: the first RUN below (dnf update) and everything after it can
|
||||||
|
# never be a layer-cache hit, so every build gets the repos' current packages.
|
||||||
|
# Local builds: --build-arg CACHE_BUST=$(date +%s) or --no-cache.
|
||||||
|
ARG CACHE_BUST=
|
||||||
|
# php.net's current release for this branch (CI); the build fails on older PHP.
|
||||||
|
ARG EXPECT_PHP_MIN=
|
||||||
|
|
||||||
|
# OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40
|
||||||
|
# (CT130 pkg-cache, https only). Unset = upstream repositories. Release RPMs are
|
||||||
|
# GPG-verified against keys/pkg-cache/ either way. Repo files
|
||||||
|
# are restored at the end of every RUN, so no layer keeps the cache address.
|
||||||
|
# See scripts/pkg-cache-repos.sh.
|
||||||
|
ARG PKG_CACHE=
|
||||||
|
COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh
|
||||||
|
COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/
|
||||||
# Install repos, update, install only needed packages, clean up in one layer
|
# Install repos, update, install only needed packages, clean up in one layer
|
||||||
RUN dnf install -y \
|
RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
|
/usr/local/sbin/pkg-cache-repos.sh release-install epel remi && \
|
||||||
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
|
# Use Remi's origin, not the mirrorlist: during the 2026-10-21 PHP security
|
||||||
|
# release the mirrors lagged hours behind (still 8.2.34 when 8.2.35 was out),
|
||||||
|
# so builds shipped unpatched PHP. HTTPS first; plain http only as a fallback
|
||||||
|
# because the origin's HTTPS dropped/timed out under that day's load. A
|
||||||
|
# replayed (old) http index can't ship old PHP: packages stay GPG-checked and
|
||||||
|
# the EXPECT_PHP_MIN gate below fails the build on anything older than php.net.
|
||||||
|
sed -i -e 's|^#baseurl=http://\(.*\)$|baseurl=https://\1 http://\1|' -e 's/^mirrorlist=/#mirrorlist=/' /etc/yum.repos.d/remi*.repo && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
|
printf 'retries=10\ntimeout=60\n' >> /etc/dnf/dnf.conf && \
|
||||||
dnf update -y && \
|
dnf update -y && \
|
||||||
dnf install -y httpd mod_ssl openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
dnf install -y httpd mod_ssl openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
||||||
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix && \
|
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh restore && \
|
||||||
dnf clean all && \
|
dnf clean all && \
|
||||||
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
||||||
|
|
||||||
@@ -16,13 +39,23 @@ COPY ./scripts/ /scripts/
|
|||||||
RUN chmod +x /scripts/*
|
RUN chmod +x /scripts/*
|
||||||
|
|
||||||
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
|
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
|
||||||
RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
|
RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
|
dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh restore && \
|
||||||
dnf clean all
|
dnf clean all
|
||||||
|
|
||||||
# Generate self-signed cert, create needed dirs, install PHP, clean up
|
# Generate self-signed cert, create needed dirs, install PHP, clean up
|
||||||
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt && \
|
RUN openssl req -newkey rsa:2048 -nodes -keyout /etc/pki/tls/private/localhost.key -x509 -days 3650 -subj "/CN=localhost" -out /etc/pki/tls/certs/localhost.crt && \
|
||||||
mkdir -p /run/php-fpm/ && \
|
mkdir -p /run/php-fpm/ && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
/scripts/install-php$PHPVER.sh && \
|
/scripts/install-php$PHPVER.sh && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh restore && \
|
||||||
|
# CI passes php.net's current release for the branch; never ship older PHP.
|
||||||
|
if [ -n "$EXPECT_PHP_MIN" ]; then \
|
||||||
|
php -r 'exit(version_compare(PHP_VERSION, getenv("EXPECT_PHP_MIN"), ">=") ? 0 : 1);' || \
|
||||||
|
{ echo "FATAL: built PHP $(php -r 'echo PHP_VERSION;') is older than php.net $EXPECT_PHP_MIN"; exit 1; }; \
|
||||||
|
else echo "WARNING: no EXPECT_PHP_MIN (local build) - PHP version not gated"; fi && \
|
||||||
|
echo "PHP version report:" && php -v && rpm -q php-cli php-common php-fpm && \
|
||||||
rm -rf /tmp/*
|
rm -rf /tmp/*
|
||||||
|
|
||||||
# Download and install wp-cli (consider pinning version for reproducibility)
|
# Download and install wp-cli (consider pinning version for reproducibility)
|
||||||
@@ -36,10 +69,15 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
|
|||||||
# Copy configs and web files
|
# Copy configs and web files
|
||||||
COPY ./configs/default-index.conf /etc/httpd/conf.d/
|
COPY ./configs/default-index.conf /etc/httpd/conf.d/
|
||||||
COPY ./configs/prod-php.ini /etc/php.ini
|
COPY ./configs/prod-php.ini /etc/php.ini
|
||||||
|
# php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
|
||||||
|
# every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
|
||||||
|
# all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
||||||
|
COPY ./configs/zz-php2shell-mitigation.ini /etc/php.d/zz-php2shell-mitigation.ini
|
||||||
COPY ./configs/phpinfo.php /var/www/html/
|
COPY ./configs/phpinfo.php /var/www/html/
|
||||||
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
||||||
COPY ./configs/index.php /var/www/html/
|
COPY ./configs/index.php /var/www/html/
|
||||||
COPY ./configs/remote_ip.conf /etc/httpd/conf.d/
|
COPY ./configs/remote_ip.conf /etc/httpd/conf.d/
|
||||||
|
COPY ./configs/whp-security.conf /etc/httpd/conf.d/
|
||||||
|
|
||||||
# Set up cron job in a single layer
|
# Set up cron job in a single layer
|
||||||
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
||||||
|
|||||||
+41
-4
@@ -1,13 +1,36 @@
|
|||||||
FROM almalinux/10-base
|
FROM almalinux/10-base
|
||||||
ARG PHPVER=83
|
ARG PHPVER=83
|
||||||
|
# Per-CI-run value: the first RUN below (dnf update) and everything after it can
|
||||||
|
# never be a layer-cache hit, so every build gets the repos' current packages.
|
||||||
|
# Local builds: --build-arg CACHE_BUST=$(date +%s) or --no-cache.
|
||||||
|
ARG CACHE_BUST=
|
||||||
|
# php.net's current release for this branch (CI); the build fails on older PHP.
|
||||||
|
ARG EXPECT_PHP_MIN=
|
||||||
|
|
||||||
|
# OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40
|
||||||
|
# (CT130 pkg-cache, https only). Unset = upstream repositories. Release RPMs are
|
||||||
|
# GPG-verified against keys/pkg-cache/ either way. Repo files
|
||||||
|
# are restored at the end of every RUN, so no layer keeps the cache address.
|
||||||
|
# See scripts/pkg-cache-repos.sh.
|
||||||
|
ARG PKG_CACHE=
|
||||||
|
COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh
|
||||||
|
COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/
|
||||||
# Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer
|
# Install repos, update, install only needed packages (no httpd/mod_ssl), clean up in one layer
|
||||||
RUN dnf install -y \
|
RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm \
|
/usr/local/sbin/pkg-cache-repos.sh release-install epel remi && \
|
||||||
https://rpms.remirepo.net/enterprise/remi-release-10.rpm && \
|
# Use Remi's origin, not the mirrorlist: during the 2026-10-21 PHP security
|
||||||
|
# release the mirrors lagged hours behind (still 8.2.34 when 8.2.35 was out),
|
||||||
|
# so builds shipped unpatched PHP. HTTPS first; plain http only as a fallback
|
||||||
|
# because the origin's HTTPS dropped/timed out under that day's load. A
|
||||||
|
# replayed (old) http index can't ship old PHP: packages stay GPG-checked and
|
||||||
|
# the EXPECT_PHP_MIN gate below fails the build on anything older than php.net.
|
||||||
|
sed -i -e 's|^#baseurl=http://\(.*\)$|baseurl=https://\1 http://\1|' -e 's/^mirrorlist=/#mirrorlist=/' /etc/yum.repos.d/remi*.repo && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
|
printf 'retries=10\ntimeout=60\n' >> /etc/dnf/dnf.conf && \
|
||||||
dnf update -y && \
|
dnf update -y && \
|
||||||
dnf install -y openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
dnf install -y openssl wget procps cronie iproute postgresql-devel microdnf less git \
|
||||||
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix fcgi && \
|
nano rsync unzip zip mariadb bind-utils jq patch nc tree dos2unix fcgi && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh restore && \
|
||||||
dnf clean all && \
|
dnf clean all && \
|
||||||
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
||||||
|
|
||||||
@@ -16,12 +39,22 @@ COPY ./scripts/ /scripts/
|
|||||||
RUN chmod +x /scripts/*
|
RUN chmod +x /scripts/*
|
||||||
|
|
||||||
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
|
# Install ImageMagick from EPEL with HEIC/HEIF/AVIF support
|
||||||
RUN dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
|
RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
|
dnf install -y ImageMagick ImageMagick-libs ImageMagick-heic && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh restore && \
|
||||||
dnf clean all
|
dnf clean all
|
||||||
|
|
||||||
# Create needed dirs, install PHP, clean up (no SSL cert, no httpd)
|
# Create needed dirs, install PHP, clean up (no SSL cert, no httpd)
|
||||||
RUN mkdir -p /run/php-fpm/ && \
|
RUN mkdir -p /run/php-fpm/ && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
/scripts/install-php$PHPVER.sh && \
|
/scripts/install-php$PHPVER.sh && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh restore && \
|
||||||
|
# CI passes php.net's current release for the branch; never ship older PHP.
|
||||||
|
if [ -n "$EXPECT_PHP_MIN" ]; then \
|
||||||
|
php -r 'exit(version_compare(PHP_VERSION, getenv("EXPECT_PHP_MIN"), ">=") ? 0 : 1);' || \
|
||||||
|
{ echo "FATAL: built PHP $(php -r 'echo PHP_VERSION;') is older than php.net $EXPECT_PHP_MIN"; exit 1; }; \
|
||||||
|
else echo "WARNING: no EXPECT_PHP_MIN (local build) - PHP version not gated"; fi && \
|
||||||
|
echo "PHP version report:" && php -v && rpm -q php-cli php-common php-fpm && \
|
||||||
rm -rf /tmp/*
|
rm -rf /tmp/*
|
||||||
|
|
||||||
# Download and install wp-cli
|
# Download and install wp-cli
|
||||||
@@ -34,6 +67,10 @@ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local
|
|||||||
|
|
||||||
# Copy configs (PHP only, no Apache configs)
|
# Copy configs (PHP only, no Apache configs)
|
||||||
COPY ./configs/prod-php.ini /etc/php.ini
|
COPY ./configs/prod-php.ini /etc/php.ini
|
||||||
|
# php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
|
||||||
|
# every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
|
||||||
|
# all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
||||||
|
COPY ./configs/zz-php2shell-mitigation.ini /etc/php.d/zz-php2shell-mitigation.ini
|
||||||
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
COPY ./configs/mariadb.repo /etc/yum.repos.d/
|
||||||
|
|
||||||
# Set up cron job for log rotation
|
# Set up cron job for log rotation
|
||||||
|
|||||||
+63
-2
@@ -22,6 +22,26 @@
|
|||||||
## after FROM for any RUN steps that need it.
|
## after FROM for any RUN steps that need it.
|
||||||
ARG OLS_VERSION=1.8.5
|
ARG OLS_VERSION=1.8.5
|
||||||
ARG PHPVER=83
|
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}
|
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
||||||
ARG PHPVER=83
|
ARG PHPVER=83
|
||||||
ENV PHPVER=${PHPVER}
|
ENV PHPVER=${PHPVER}
|
||||||
@@ -45,14 +65,40 @@ ENV PHPVER=${PHPVER}
|
|||||||
## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs
|
## lsphp${PHPVER}-ldap is the only extra ext we add (everything else WP needs
|
||||||
## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell
|
## ships in the prebuilt base). lsphp84 + lsphp85 don't ship imap or pspell
|
||||||
## from LiteSpeed — customers needing imap should pin to 8.3 or earlier.
|
## from LiteSpeed — customers needing imap should pin to 8.3 or earlier.
|
||||||
RUN apt-get update && \
|
##
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
## SECURITY UPDATES AT BUILD TIME: lsphp-apt-upgrade.sh also upgrades the OS and
|
||||||
|
## sweeps EVERY installed lsphp${PHPVER}* package to the LiteSpeed repo's current
|
||||||
|
## release, failing the build if the repo is unreachable — so a rebuild ships a
|
||||||
|
## PHP security fix as soon as LiteSpeed publishes the .deb, without waiting for
|
||||||
|
## a new litespeedtech/openlitespeed base tag. See the script header.
|
||||||
|
##
|
||||||
|
## CACHE_BUST: CI passes a per-run value so this layer can never be a cache hit
|
||||||
|
## (an ARG's first use in a RUN is part of that layer's cache key). Local builds:
|
||||||
|
## pass --build-arg CACHE_BUST=$(date +%s) or use --no-cache, or you may get the
|
||||||
|
## lsphp from your last build. EXPECT_* are resolved by CI from the repo index
|
||||||
|
## and asserted here; the same values become the image labels below.
|
||||||
|
ARG CACHE_BUST=
|
||||||
|
ARG EXPECT_LSPHP_PKG=
|
||||||
|
ARG EXPECT_PHP_VERSION=
|
||||||
|
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
|
||||||
|
RUN /usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" \
|
||||||
gettext-base sudo cron \
|
gettext-base sudo cron \
|
||||||
ca-certificates curl wget \
|
ca-certificates curl wget \
|
||||||
lsphp${PHPVER}-ldap && \
|
lsphp${PHPVER}-ldap && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
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 —
|
## Composer (matches the wp-cli pattern in the existing CAC Dockerfile —
|
||||||
## phar download, no `php install` pipe since lsphp's CLI mode is fine).
|
## 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 && \
|
RUN curl -fsSL -o /usr/local/bin/composer https://getcomposer.org/download/latest-stable/composer.phar && \
|
||||||
@@ -68,6 +114,10 @@ COPY ./scripts/entrypoint-litespeed.sh \
|
|||||||
/scripts/
|
/scripts/
|
||||||
RUN chmod +x /scripts/*
|
RUN chmod +x /scripts/*
|
||||||
COPY ./configs/litespeed/ /etc/lsws-templates/
|
COPY ./configs/litespeed/ /etc/lsws-templates/
|
||||||
|
## php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
|
||||||
|
## every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
|
||||||
|
## all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
||||||
|
COPY ./configs/zz-php2shell-mitigation.ini /etc/lsws-templates/zz-php2shell-mitigation.ini
|
||||||
|
|
||||||
## Apply our production lsphp ini overrides. Ask lsphp for its scan dir
|
## Apply our production lsphp ini overrides. Ask lsphp for its scan dir
|
||||||
## directly (varies by PHP minor version: 8.3/8.4/8.5 each have their own
|
## directly (varies by PHP minor version: 8.3/8.4/8.5 each have their own
|
||||||
@@ -77,6 +127,7 @@ RUN bash -c 'set -e; \
|
|||||||
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
||||||
mkdir -p "$SCAN_DIR"; \
|
mkdir -p "$SCAN_DIR"; \
|
||||||
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
||||||
|
cp /etc/lsws-templates/zz-php2shell-mitigation.ini "$SCAN_DIR/zz-php2shell-mitigation.ini"; \
|
||||||
echo "wrote overrides to $SCAN_DIR"'
|
echo "wrote overrides to $SCAN_DIR"'
|
||||||
|
|
||||||
## Disable the OLS WebAdmin port for customer-facing containers. Bind admin
|
## Disable the OLS WebAdmin port for customer-facing containers. Bind admin
|
||||||
@@ -87,6 +138,16 @@ RUN sed -i 's|^[[:space:]]*address[[:space:]]\+\*:| address 127.0
|
|||||||
## Cron entry for log rotation (mirrors cac:phpXX).
|
## Cron entry for log rotation (mirrors cac:phpXX).
|
||||||
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
RUN echo "15 */12 * * * root /scripts/log-rotate.sh" >> /etc/crontab
|
||||||
|
|
||||||
|
## Inspect without running the image:
|
||||||
|
## docker buildx imagetools inspect <ref> --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).
|
||||||
|
## 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
|
EXPOSE 80 443
|
||||||
|
|
||||||
## Healthcheck: the entrypoint drops a static /healthz into the customer
|
## Healthcheck: the entrypoint drops a static /healthz into the customer
|
||||||
|
|||||||
+251
-5
@@ -21,6 +21,153 @@
|
|||||||
|
|
||||||
ARG OLS_VERSION=1.8.4
|
ARG OLS_VERSION=1.8.4
|
||||||
ARG PHPVER=83
|
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
|
||||||
|
## ext/cac-path-parity/cac_path_parity.c for why this is an extension and not an
|
||||||
|
## auto_prepend_file.
|
||||||
|
##
|
||||||
|
## WHICH lsphp THE .so IS BUILT AGAINST — read this before touching the apt lines.
|
||||||
|
## `lsphp${PHPVER}-dev` is NOT available at the version the base image ships:
|
||||||
|
## the LiteSpeed apt repo carries only the CURRENT release, and every prebuilt
|
||||||
|
## OLS base image is behind it (measured 2026-08-05 on OLS 1.8.4:
|
||||||
|
## lsphp81 8.1.33 base / 8.1.34 repo, lsphp83 8.3.28 / 8.3.32,
|
||||||
|
## lsphp85 8.5.0 / 8.5.8).
|
||||||
|
## Pinning -dev to the base version fails on all three with
|
||||||
|
## `E: Version '<base>' for 'lsphp<NN>-dev' was not found`.
|
||||||
|
##
|
||||||
|
## So installing -dev necessarily UPGRADES lsphp in this stage. The parity we can
|
||||||
|
## have — and the one this file now guarantees — is the other direction: the
|
||||||
|
## shipped runtime is pinned to whatever version this stage compiled against.
|
||||||
|
## That version is recorded here and consumed by stage 2, so the two apt layers
|
||||||
|
## are cache-locked to each other. Without this, `COPY ./ext` invalidating only
|
||||||
|
## THIS stage while stage 2's apt layer stayed cached produced a real, repeatable
|
||||||
|
## skew (reviewer measured a .so built on 8.3.32 shipped next to an 8.3.30
|
||||||
|
## runtime, with lsphp83-common at 8.3.31 — the vendor family is not always
|
||||||
|
## uniformly versioned either).
|
||||||
|
##
|
||||||
|
## Benign in practice (PHP holds ABI stable across a patch series) but it is the
|
||||||
|
## riskier direction — headers NEWER than the runtime — and the runtime assertion
|
||||||
|
## in stage 2 catches only load failure, never silent struct-layout drift.
|
||||||
|
##
|
||||||
|
## Separate stage on purpose: the compiler + headers (~400MB) stay out of the
|
||||||
|
## shipped image, which gains only the ~40KB .so. Costs ~1-2 min of CI per PHP
|
||||||
|
## version; both stages share the same base layer, so no extra pull.
|
||||||
|
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER} AS ext-build
|
||||||
|
ARG PHPVER=83
|
||||||
|
|
||||||
|
## Toolchain layer, deliberately BEFORE the source COPY so editing the extension
|
||||||
|
## does not re-resolve the PHP version (which is what caused the skew above).
|
||||||
|
## Records the exact lsphp version the headers belong to; stage 2 pins to it.
|
||||||
|
##
|
||||||
|
## lsphp-apt-upgrade.sh (see its header) refuses to run on stale/unreachable
|
||||||
|
## repo lists and sweeps every lsphp${PHPVER}* package to the repo's current
|
||||||
|
## release. CACHE_BUST is per-CI-run so this layer — and therefore the recorded
|
||||||
|
## version stage 2 pins to — is never a cache hit; it is re-declared in stage 2.
|
||||||
|
## --no-os-upgrade: this stage is thrown away, OS updates would only cost time.
|
||||||
|
## EXPECT_* (CI's repo-index lookup) are asserted here too, so a mismatch fails
|
||||||
|
## before the compile rather than after it.
|
||||||
|
ARG CACHE_BUST=
|
||||||
|
ARG EXPECT_LSPHP_PKG=
|
||||||
|
ARG EXPECT_PHP_VERSION=
|
||||||
|
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
|
||||||
|
RUN set -e; \
|
||||||
|
/usr/local/sbin/lsphp-apt-upgrade.sh --no-os-upgrade "${PHPVER}" \
|
||||||
|
build-essential autoconf pkg-config \
|
||||||
|
lsphp${PHPVER}-dev; \
|
||||||
|
RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \
|
||||||
|
DEVV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}-dev); \
|
||||||
|
if [ "$RTV" != "$DEVV" ]; then \
|
||||||
|
echo "FATAL: lsphp${PHPVER}=$RTV but lsphp${PHPVER}-dev=$DEVV — the headers" >&2; \
|
||||||
|
echo " do not belong to the PHP in this stage. Refusing to build." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
mkdir -p /build-out; \
|
||||||
|
printf '%s' "$RTV" > /build-out/lsphp.version; \
|
||||||
|
echo "cac_path_parity will be compiled against lsphp${PHPVER} $RTV"
|
||||||
|
|
||||||
|
## Build, then RUN THE .phpt SUITE as a build gate. Before this, ext/…/tests/
|
||||||
|
## existed but nothing ever executed it — neither this Dockerfile nor
|
||||||
|
## .gitea/workflows/build-push.yaml — so six green tests were documentation. It
|
||||||
|
## costs ~1s per PHP version and it runs against the SAME lsphp build the .so
|
||||||
|
## will ship next to.
|
||||||
|
##
|
||||||
|
## Two guards around `make test`, because its default failure mode is silence:
|
||||||
|
## - if PHP_EXECUTABLE is missing the Makefile prints "Cannot run tests without
|
||||||
|
## CLI sapi." and EXITS 0. The lsphp packages do ship a real CLI
|
||||||
|
## (php-config --php-binary => .../bin/phpN.N), but assert it rather than
|
||||||
|
## trusting it.
|
||||||
|
## - 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; \
|
||||||
|
/usr/local/lsws/lsphp${PHPVER}/bin/phpize; \
|
||||||
|
./configure --enable-cac-path-parity \
|
||||||
|
--with-php-config=/usr/local/lsws/lsphp${PHPVER}/bin/php-config; \
|
||||||
|
make -j"$(nproc)"; \
|
||||||
|
PHP_BIN=$(/usr/local/lsws/lsphp${PHPVER}/bin/php-config --php-binary); \
|
||||||
|
if [ ! -x "$PHP_BIN" ]; then \
|
||||||
|
echo "FATAL: no CLI php at '$PHP_BIN' — \`make test\` would print" >&2; \
|
||||||
|
echo " 'Cannot run tests without CLI sapi.' and exit 0." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
EXPECTED=$(ls tests/*.phpt | wc -l); \
|
||||||
|
if [ "$EXPECTED" -lt 1 ]; then echo "FATAL: no .phpt tests found" >&2; exit 1; fi; \
|
||||||
|
if ! NO_INTERACTION=1 REPORT_EXIT_STATUS=1 make test >/tmp/make-test.log 2>&1; then \
|
||||||
|
cat /tmp/make-test.log >&2; \
|
||||||
|
echo "FATAL: cac_path_parity .phpt suite FAILED — not shipping this .so." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
cat /tmp/make-test.log; \
|
||||||
|
if ! grep -Eq "^Number of tests : +${EXPECTED} +${EXPECTED} *$" /tmp/make-test.log; then \
|
||||||
|
echo "FATAL: expected all ${EXPECTED} .phpt tests to run; the summary above disagrees." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
if ! grep -Eq "^Tests failed +: +0 " /tmp/make-test.log; then \
|
||||||
|
echo "FATAL: run-tests.php reported failures." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
echo "cac_path_parity: ${EXPECTED}/${EXPECTED} .phpt tests passed"; \
|
||||||
|
cp modules/cac_path_parity.so /build-out/
|
||||||
|
|
||||||
|
## ---- stage 2: the shipped sidecar image ------------------------------------
|
||||||
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
||||||
ARG PHPVER=83
|
ARG PHPVER=83
|
||||||
ENV PHPVER=${PHPVER}
|
ENV PHPVER=${PHPVER}
|
||||||
@@ -29,12 +176,64 @@ ENV PHPVER=${PHPVER}
|
|||||||
## base lacks is lsphpNN-ldap. setpriv (util-linux) is already on the Ubuntu
|
## base lacks is lsphpNN-ldap. setpriv (util-linux) is already on the Ubuntu
|
||||||
## base; we add nothing else the sidecar doesn't need. All apt cache cleaned in
|
## base; we add nothing else the sidecar doesn't need. All apt cache cleaned in
|
||||||
## the same layer to keep the image small.
|
## the same layer to keep the image small.
|
||||||
RUN apt-get update && \
|
##
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
## VERSION LOCKSTEP: `apt-get install lsphpNN-ldap` pulls lsphpNN-common forward,
|
||||||
|
## which drags the whole lsphpNN family to the repo's current release — the same
|
||||||
|
## upgrade the ext-build stage gets. Left implicit, the two stages resolve that
|
||||||
|
## independently and Docker caches them independently, so they drift apart (see
|
||||||
|
## the long comment on stage 1). Copying stage 1's recorded version in BEFORE
|
||||||
|
## this layer makes the version part of this layer's cache key: same version =>
|
||||||
|
## cache hit, new version => this layer re-runs and lands on the same one. The
|
||||||
|
## explicit `=$V` pins then make a mid-build repo roll a LOUD apt failure instead
|
||||||
|
## of a silent skew. Verified satisfiable on PHP 8.1/8.3/8.5 (2026-08-05).
|
||||||
|
##
|
||||||
|
## SECURITY UPDATES AT BUILD TIME: lsphp-apt-upgrade.sh upgrades the Ubuntu OS
|
||||||
|
## packages and every other lsphp${PHPVER}* package (extensions such as -msgpack
|
||||||
|
## are NOT dragged along by the -common pin) to the repos' current versions, and
|
||||||
|
## fails the build if the repo is unreachable. CACHE_BUST makes this layer a
|
||||||
|
## guaranteed cache miss per CI run; EXPECT_* are CI's repo-index lookup,
|
||||||
|
## asserted by the script and written into the image labels below.
|
||||||
|
ARG CACHE_BUST=
|
||||||
|
ARG EXPECT_LSPHP_PKG=
|
||||||
|
ARG EXPECT_PHP_VERSION=
|
||||||
|
COPY --from=ext-build /build-out/lsphp.version /etc/cac-lsphp-build.version
|
||||||
|
COPY ./scripts/lsphp-apt-upgrade.sh /usr/local/sbin/lsphp-apt-upgrade.sh
|
||||||
|
RUN set -e; \
|
||||||
|
V=$(cat /etc/cac-lsphp-build.version); \
|
||||||
|
if ! /usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" \
|
||||||
ca-certificates \
|
ca-certificates \
|
||||||
lsphp${PHPVER}-ldap && \
|
lsphp${PHPVER}="$V" lsphp${PHPVER}-common="$V" lsphp${PHPVER}-ldap="$V"; then \
|
||||||
apt-get clean && \
|
echo "FATAL: lsphp-apt-upgrade.sh failed (reason above). If apt said" >&2; \
|
||||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
echo " \"Version '$V' ... was not found\" or refused a downgrade: $V is what" >&2; \
|
||||||
|
echo " cac_path_parity was compiled against, but the LiteSpeed repo (which" >&2; \
|
||||||
|
echo " keeps only the current release) now offers a newer one — either it" >&2; \
|
||||||
|
echo " published mid-build or ext-build was a stale cache hit. Re-run the" >&2; \
|
||||||
|
echo " build (CI) / rebuild with --no-cache (local)." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
apt-get clean; \
|
||||||
|
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*; \
|
||||||
|
RTV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}); \
|
||||||
|
CMV=$(dpkg-query -W -f='${Version}' lsphp${PHPVER}-common); \
|
||||||
|
if [ "$RTV" != "$V" ] || [ "$CMV" != "$V" ]; then \
|
||||||
|
echo "FATAL: cac_path_parity.so was compiled against lsphp${PHPVER} $V but this" >&2; \
|
||||||
|
echo " image would ship lsphp${PHPVER}=$RTV / -common=$CMV." >&2; \
|
||||||
|
echo " Rebuild with --no-cache so both stages resolve the same release." >&2; \
|
||||||
|
exit 1; \
|
||||||
|
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
|
## Scripts + the SHARED production lsphp ini (reused verbatim from the litespeed
|
||||||
## image — same runtime, same tuning). Scripts layer last (they change most).
|
## image — same runtime, same tuning). Scripts layer last (they change most).
|
||||||
@@ -48,18 +247,65 @@ RUN chmod +x /scripts/entrypoint-lsphp.sh /scripts/detect-memory-lsphp.sh /scrip
|
|||||||
## Apply production lsphp ini overrides into lsphp's scan dir (path varies by
|
## Apply production lsphp ini overrides into lsphp's scan dir (path varies by
|
||||||
## PHP minor version; ask lsphp directly — same idiom as Dockerfile.litespeed).
|
## PHP minor version; ask lsphp directly — same idiom as Dockerfile.litespeed).
|
||||||
COPY ./configs/litespeed/lsphp-overrides.ini /etc/lsws-templates/lsphp-overrides.ini
|
COPY ./configs/litespeed/lsphp-overrides.ini /etc/lsws-templates/lsphp-overrides.ini
|
||||||
|
## php2shell mitigation (CVE-2026-109618/-109619). Named zz- so it sorts after
|
||||||
|
## every other scan-dir ini (incl. runtime-written 99-*) and wins. Remove once
|
||||||
|
## all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
||||||
|
COPY ./configs/zz-php2shell-mitigation.ini /etc/lsws-templates/zz-php2shell-mitigation.ini
|
||||||
RUN bash -c 'set -e; \
|
RUN bash -c 'set -e; \
|
||||||
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
||||||
mkdir -p "$SCAN_DIR"; \
|
mkdir -p "$SCAN_DIR"; \
|
||||||
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
cp /etc/lsws-templates/lsphp-overrides.ini "$SCAN_DIR/99-prod-overrides.ini"; \
|
||||||
|
cp /etc/lsws-templates/zz-php2shell-mitigation.ini "$SCAN_DIR/zz-php2shell-mitigation.ini"; \
|
||||||
echo "wrote overrides to $SCAN_DIR"'
|
echo "wrote overrides to $SCAN_DIR"'
|
||||||
|
|
||||||
|
## Install the cac_path_parity extension into lsphp's own extension_dir and load
|
||||||
|
## it unconditionally. It is INERT until the entrypoint writes the per-site
|
||||||
|
## cac_path_parity.from/.to mapping, so it is safe in any context (including
|
||||||
|
## wp-cli runs, where $_SERVER carries no filesystem paths).
|
||||||
|
##
|
||||||
|
## The trailing `lsphp -i | grep` is a BUILD-TIME ASSERTION: if the .so fails to
|
||||||
|
## load (ABI drift after a base-image PHP bump, bad build) the image build fails
|
||||||
|
## here rather than shipping a sidecar that silently lost path parity. Note its
|
||||||
|
## limit: it proves the .so LOADS, not that it was built against these exact
|
||||||
|
## structs — silent layout drift would sail straight through. The version lockstep
|
||||||
|
## above is what actually removes that possibility; this stays as the backstop.
|
||||||
|
## NOTE: probe lsphp with `-i` ONLY. The lsphp binary is the LSAPI SAPI, not the
|
||||||
|
## CLI — it accepts just -[b|c|n|h|i|q|s|v|?] and answers anything else (`-m`,
|
||||||
|
## `-r`) by printing its usage text and exiting 0. A `lsphp -m | grep` check
|
||||||
|
## therefore never matches AND never fails, which is exactly the kind of silent
|
||||||
|
## always-false assertion this whole change exists to eliminate.
|
||||||
|
COPY --from=ext-build /build-out/cac_path_parity.so /tmp/cac_path_parity.so
|
||||||
|
RUN bash -c 'set -e; \
|
||||||
|
LSPHP="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"; \
|
||||||
|
EXT_DIR=$("$LSPHP" -i 2>/dev/null | awk -F" => " "/^extension_dir/ {print \$2; exit}"); \
|
||||||
|
SCAN_DIR=$("$LSPHP" -i 2>/dev/null | awk -F"=> " "/^Scan this dir/ {print \$2; exit}"); \
|
||||||
|
mkdir -p "$EXT_DIR" "$SCAN_DIR"; \
|
||||||
|
mv /tmp/cac_path_parity.so "$EXT_DIR/"; \
|
||||||
|
printf "; installed by Dockerfile.lsphp\nextension=cac_path_parity.so\n" \
|
||||||
|
> "$SCAN_DIR/00-cac-path-parity.ini"; \
|
||||||
|
"$LSPHP" -i 2>/dev/null | grep -q "^cac_path_parity support => enabled$"; \
|
||||||
|
echo "cac_path_parity installed into $EXT_DIR and verified loadable"'
|
||||||
|
|
||||||
## php-lsapi gates .user.ini parsing behind this env var (see entrypoint-lsphp.sh
|
## php-lsapi gates .user.ini parsing behind this env var (see entrypoint-lsphp.sh
|
||||||
## for the full explanation). Set here so the value is visible in `docker inspect`
|
## for the full explanation). Set here so the value is visible in `docker inspect`
|
||||||
## and survives an entrypoint override; the entrypoint re-exports it with the same
|
## and survives an entrypoint override; the entrypoint re-exports it with the same
|
||||||
## default so the runuser exec path can't drop it.
|
## default so the runuser exec path can't drop it.
|
||||||
ENV LSPHP_ENABLE_USER_INI=on
|
ENV LSPHP_ENABLE_USER_INI=on
|
||||||
|
|
||||||
|
## Inspect without running the image:
|
||||||
|
## docker buildx imagetools inspect <ref> --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).
|
||||||
|
## 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
|
EXPOSE 9000
|
||||||
|
|
||||||
## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping).
|
## TCP-connect + lsphp-alive check (LSAPI isn't FastCGI, so no cgi-fcgi ping).
|
||||||
|
|||||||
+14
-2
@@ -1,10 +1,21 @@
|
|||||||
FROM almalinux/10-base
|
FROM almalinux/10-base
|
||||||
|
|
||||||
|
# OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40
|
||||||
|
# (CT130 pkg-cache, https only). Unset = upstream repositories. Release RPMs are
|
||||||
|
# GPG-verified against keys/pkg-cache/ either way. Repo files
|
||||||
|
# are restored at the end of every RUN, so no layer keeps the cache address.
|
||||||
|
# See scripts/pkg-cache-repos.sh.
|
||||||
|
ARG PKG_CACHE=
|
||||||
|
COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh
|
||||||
|
COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/
|
||||||
|
|
||||||
# Install Apache and minimal dependencies (no PHP at all)
|
# Install Apache and minimal dependencies (no PHP at all)
|
||||||
RUN dnf install -y \
|
RUN /usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm && \
|
/usr/local/sbin/pkg-cache-repos.sh release-install epel && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh rpm && \
|
||||||
dnf update -y && \
|
dnf update -y && \
|
||||||
dnf install -y httpd mod_ssl openssl iproute cronie procps curl && \
|
dnf install -y httpd mod_ssl openssl iproute cronie procps curl && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh restore && \
|
||||||
dnf clean all && \
|
dnf clean all && \
|
||||||
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
rm -rf /var/cache/dnf /usr/share/doc /usr/share/man /usr/share/locale/*
|
||||||
|
|
||||||
@@ -13,6 +24,7 @@ COPY ./scripts/detect-memory.sh /scripts/detect-memory.sh
|
|||||||
COPY ./scripts/create-apache-mpm-config.sh /scripts/create-apache-mpm-config.sh
|
COPY ./scripts/create-apache-mpm-config.sh /scripts/create-apache-mpm-config.sh
|
||||||
COPY ./scripts/log-rotate.sh /scripts/log-rotate.sh
|
COPY ./scripts/log-rotate.sh /scripts/log-rotate.sh
|
||||||
COPY ./scripts/entrypoint-shared-httpd.sh /scripts/entrypoint-shared-httpd.sh
|
COPY ./scripts/entrypoint-shared-httpd.sh /scripts/entrypoint-shared-httpd.sh
|
||||||
|
COPY ./scripts/stop-services.sh /scripts/stop-services.sh
|
||||||
COPY ./scripts/tune-mpm.sh /scripts/tune-mpm.sh
|
COPY ./scripts/tune-mpm.sh /scripts/tune-mpm.sh
|
||||||
RUN chmod +x /scripts/*
|
RUN chmod +x /scripts/*
|
||||||
|
|
||||||
|
|||||||
+17
-2
@@ -19,14 +19,29 @@ ARG OLS_VERSION=1.8.4
|
|||||||
ARG PHPVER=83
|
ARG PHPVER=83
|
||||||
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
FROM litespeedtech/openlitespeed:${OLS_VERSION}-lsphp${PHPVER}
|
||||||
|
|
||||||
|
## OPTIONAL internal package cache, e.g. --build-arg PKG_CACHE=https://192.168.1.40
|
||||||
|
## (CT130 pkg-cache, https only). Unset = upstream repositories. Release RPMs are
|
||||||
|
## GPG-verified against keys/pkg-cache/ either way. Repo files
|
||||||
|
## are restored at the end of every RUN, so no layer keeps the cache address.
|
||||||
|
## See scripts/pkg-cache-repos.sh.
|
||||||
|
ARG PKG_CACHE=
|
||||||
|
COPY ./scripts/pkg-cache-repos.sh /usr/local/sbin/pkg-cache-repos.sh
|
||||||
|
COPY ./keys/pkg-cache/ /usr/local/share/pkg-cache/
|
||||||
|
|
||||||
## Tooling the shared tier needs on top of the base:
|
## Tooling the shared tier needs on top of the base:
|
||||||
## - inotify-tools: the .htaccess watcher (spec 5.3)
|
## - inotify-tools: the .htaccess watcher (spec 5.3)
|
||||||
## - gettext-base: envsubst for render-shared-ols-config.sh
|
## - gettext-base: envsubst for render-shared-ols-config.sh
|
||||||
## - openssl: self-signed cert for the :443 listener (HAProxy verifies none)
|
## - openssl: self-signed cert for the :443 listener (HAProxy verifies none)
|
||||||
## - curl/ca-certificates: HEALTHCHECK
|
## - curl/ca-certificates: HEALTHCHECK
|
||||||
RUN apt-get update && \
|
## - procps: provides pgrep, which entrypoint-shared-ols.sh's ols_running()
|
||||||
|
## liveness check depends on. Only transitively present via the base image
|
||||||
|
## today (Ubuntu 24.04 pulls it in) — pin it explicitly so it can't be
|
||||||
|
## pruned as "unused" and silently break the supervisor's crash detection.
|
||||||
|
RUN /usr/local/sbin/pkg-cache-repos.sh apt && \
|
||||||
|
apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
inotify-tools gettext-base openssl ca-certificates curl && \
|
inotify-tools gettext-base openssl ca-certificates curl procps && \
|
||||||
|
/usr/local/sbin/pkg-cache-repos.sh restore && \
|
||||||
apt-get clean && \
|
apt-get clean && \
|
||||||
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
rm -rf /var/lib/apt/lists/* /var/cache/apt/archives/*
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,18 @@ virtualHostConfig {
|
|||||||
addDefaultCharset off
|
addDefaultCharset off
|
||||||
}
|
}
|
||||||
|
|
||||||
|
## SECURITY: never serve config / VCS dotfiles over HTTP. Same named list as
|
||||||
|
## WHP's shared-ols vhconf template (first `context exp:` there) and
|
||||||
|
## shared-httpd's 000-whp-security.conf. Measured on .148: this tier served
|
||||||
|
## /.env even at mode 0640, because OLS runs as the customer uid here. An
|
||||||
|
## `exp:` regex context wins over the `context /` prefix context regardless
|
||||||
|
## of order (measured on OLS 1.8.4 for shared-ols), so a customer .htaccess
|
||||||
|
## cannot reopen it. Named, not blanket "^\.": /.well-known/ (ACME,
|
||||||
|
## security.txt, app-site-association) must stay reachable.
|
||||||
|
context exp:^/(.*/)?\.(user\.ini|env|git|svn|hg|bzr|ht|whp|site-builder) {
|
||||||
|
allowBrowse 0
|
||||||
|
}
|
||||||
|
|
||||||
rewrite {
|
rewrite {
|
||||||
enable 1
|
enable 1
|
||||||
autoLoadHtaccess 1
|
autoLoadHtaccess 1
|
||||||
|
|||||||
@@ -23,10 +23,22 @@ useIpInProxyHeader 1
|
|||||||
|
|
||||||
## LSCache enabled at MODULE scope for the whole tier (dedicated cache volume,
|
## LSCache enabled at MODULE scope for the whole tier (dedicated cache volume,
|
||||||
## ephemeral across rebuilds; OLS auto-keys a per-vhost subdir under storagePath).
|
## ephemeral across rebuilds; OLS auto-keys a per-vhost subdir under storagePath).
|
||||||
## PUBLIC (anonymous) caching ONLY: enableCache 1 + checkPublicCache 1 let OLS
|
## PUBLIC (anonymous) caching ONLY: checkPublicCache 1 lets OLS serve cacheable,
|
||||||
## serve cacheable, non-logged-in responses marked by the LiteSpeed Cache WP
|
## non-logged-in responses that are EXPLICITLY marked by the LiteSpeed Cache WP
|
||||||
## plugin's X-LiteSpeed-Cache-Control headers (ignoreRespCacheCtrl=0 honors them).
|
## plugin's X-LiteSpeed-Cache-Control headers (ignoreRespCacheCtrl=0 honors them).
|
||||||
##
|
##
|
||||||
|
## enableCache is 0 ON PURPOSE. At module scope enableCache 1 means "cache every
|
||||||
|
## cacheable-looking public response by default", INCLUDING ones that carry no
|
||||||
|
## cache header at all. That bit us on Divi: LSCWP's Divi compat calls
|
||||||
|
## litespeed_disable_all for ?et_fb= / ?et_pb_preview= / ?p=N&preview=true, which
|
||||||
|
## switches the plugin off for the request, so it sends NO header (and its own
|
||||||
|
## "Do Not Cache Query Strings" never runs) — and OLS stored the Visual Builder
|
||||||
|
## page under enableCache 1. With enableCache 0 only responses that opt in via
|
||||||
|
## X-LiteSpeed-Cache-Control: public are stored; a plugin-disabled request is
|
||||||
|
## simply not cached. Measured on OLS 1.8.4 (2026-09-21): headerless response =
|
||||||
|
## miss,hit under 1; not cached under 0; explicit `public` header cached under
|
||||||
|
## both. Matches the dedicated cac-litespeed tier (configs/litespeed/site-template.tpl).
|
||||||
|
##
|
||||||
## PRIVATE caching is intentionally OFF (enablePrivateCache 0 + checkPrivateCache 0).
|
## PRIVATE caching is intentionally OFF (enablePrivateCache 0 + checkPrivateCache 0).
|
||||||
## Logged-in / cookie-bearing pages must NEVER be cached at the tier. We previously
|
## Logged-in / cookie-bearing pages must NEVER be cached at the tier. We previously
|
||||||
## left enablePrivateCache=1 assuming "no plugin -> nothing cached," but that was
|
## left enablePrivateCache=1 assuming "no plugin -> nothing cached," but that was
|
||||||
@@ -45,7 +57,7 @@ module cache {
|
|||||||
respCookieCache 1
|
respCookieCache 1
|
||||||
ignoreReqCacheCtrl 0
|
ignoreReqCacheCtrl 0
|
||||||
ignoreRespCacheCtrl 0
|
ignoreRespCacheCtrl 0
|
||||||
enableCache 1
|
enableCache 0
|
||||||
enablePrivateCache 0
|
enablePrivateCache 0
|
||||||
}
|
}
|
||||||
## ---- end shared-ols server append ----
|
## ---- end shared-ols server append ----
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
## SECURITY: never serve config / VCS dotfiles over HTTP (standalone cac).
|
||||||
|
##
|
||||||
|
## Same named list as WHP's shared-httpd drop-in (web-files/configs/
|
||||||
|
## shared-httpd-security.conf, 000-whp-security.conf on every shared-httpd host),
|
||||||
|
## re-rooted from /mnt/users to /home because here the docroot is
|
||||||
|
## /home/<user>/public_html. Standalone cac never got that drop-in, and measured
|
||||||
|
## on .148 it served /.env with contents.
|
||||||
|
##
|
||||||
|
## <FilesMatch> matches the FILENAME resolved from the filesystem path, not the
|
||||||
|
## request URL, so it cannot be side-stepped by URL-encoding. httpd applies
|
||||||
|
## <Directory> sections (and .htaccess) first and <Files>/<FilesMatch> after
|
||||||
|
## them, so this deny wins over create-vhost.sh's `Require all granted`.
|
||||||
|
##
|
||||||
|
## A NAMED LIST, NOT A BLANKET "^\." DENY: /.well-known/ (ACME, security.txt,
|
||||||
|
## apple-app-site-association, assetlinks.json, payment-domain verification)
|
||||||
|
## must stay reachable. FilesMatch tests the final path component, and for
|
||||||
|
## /.well-known/acme-challenge/<token> that is <token>, not a dotfile.
|
||||||
|
##
|
||||||
|
## HTTP only: PHP still reads .user.ini and .env off the filesystem as before.
|
||||||
|
<Directory "/home">
|
||||||
|
<FilesMatch "^\.(user\.ini|env|git|svn|hg|bzr|ht|whp|site-builder)">
|
||||||
|
Require all denied
|
||||||
|
</FilesMatch>
|
||||||
|
</Directory>
|
||||||
|
## Directory form: reaches the files INSIDE .git/ etc. (e.g. /.git/config,
|
||||||
|
## whose own name is not a dotfile).
|
||||||
|
<DirectoryMatch "^/home/.*/\.(git|svn|hg|bzr|whp|site-builder)(/|$)">
|
||||||
|
Require all denied
|
||||||
|
</DirectoryMatch>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
; php2shell mitigation (CVE-2026-109618 / CVE-2026-109619, GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp)
|
||||||
|
; Remove once all images ship PHP >= 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
|
||||||
|
fatal_error_backtraces = 0
|
||||||
|
session.upload_progress.enabled = 0
|
||||||
|
max_input_nesting_level = 16
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
# Decision record: entrypoint boot posture on a failed `lsphp -i` probe, and CI gating for the pipeline-shape check
|
||||||
|
|
||||||
|
Date: 2026-08-05
|
||||||
|
Status: recommendations for sign-off; nothing here is implemented by this document.
|
||||||
|
Background: the SIGPIPE-under-pipefail fix (here-strings replacing `printf | awk` in
|
||||||
|
`scripts/entrypoint-lsphp.sh` and `scripts/entrypoint-litespeed.sh`) removed a boot-killing
|
||||||
|
141 and, as a side effect, changed what happens when `lsphp -i` *genuinely* produces
|
||||||
|
nothing. Two judgement calls were flagged for a human decision. This document is that
|
||||||
|
decision's working-out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Call 1 — boot posture when `lsphp -i` genuinely fails
|
||||||
|
|
||||||
|
### What actually happens today (post-fix, as-written in the repo)
|
||||||
|
|
||||||
|
The two container types are **not** currently symmetric, which matters for the decision:
|
||||||
|
|
||||||
|
- **`cac-lsphp`** (`scripts/entrypoint-lsphp.sh`): fail-open, but *not silent*. An empty
|
||||||
|
`SCAN_DIR` falls to an else-branch that distinguishes "lsphp answered but reported no
|
||||||
|
scan dir" from "`lsphp -i` produced no usable output at all" (`lsphp_info_is_usable`,
|
||||||
|
keyed on the `PHP Version =>` banner), prints a distinct `WARNING:` to stderr for each,
|
||||||
|
and every boot ends with a summary line
|
||||||
|
`entrypoint-lsphp: $_SERVER path parity = <MODE> (...)` where `<MODE>` becomes
|
||||||
|
`none (no scan dir)` or `none (lsphp -i unusable)`. So `docker logs` carries a
|
||||||
|
greppable verdict — but nothing on the platform consumes it.
|
||||||
|
- **`cac-litespeed`** (`scripts/entrypoint-litespeed.sh`, lines 81–102): fail-open and
|
||||||
|
**completely silent**. `if [ -n "$SCAN_DIR" ] ... fi` with no else. An empty probe means
|
||||||
|
the per-site `error_log` ini and the opcache override ini are simply never written, and
|
||||||
|
nothing says so anywhere.
|
||||||
|
- **Shared tiers** (`entrypoint-shared-ols.sh`, `entrypoint-shared-httpd.sh`): no
|
||||||
|
`lsphp -i` probe exists in them at all. The question is moot there today, but the
|
||||||
|
blast-radius argument below still applies if one is ever added.
|
||||||
|
|
||||||
|
What degradation costs the customer, concretely:
|
||||||
|
|
||||||
|
| Lost fragment | cac-lsphp | cac-litespeed | Customer-visible symptom |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `99-user-error-log.ini` | yes | yes | PHP errors go to stderr/`docker logs` instead of `/home/$user/logs/php-fpm/error.log`. Baked `log_errors = On` still applies, so errors are *not lost* — they are somewhere the customer can't see and support doesn't look first. Ticket shape: "my error log is empty." |
|
||||||
|
| `99-user-opcache.ini` | yes | yes | Panel-set opcache overrides silently revert to baked defaults (64 MB / 8000 files). A site tuned up for a big codebase gets cache thrash; hard to attribute. |
|
||||||
|
| `99-cac-path-parity.ini` | yes | n/a | Extension gets no mapping ⇒ `$_SERVER` path parity with cac-fpm is lost entirely in the "no output" case (the auto_prepend fallback is only written in the "extension missing from module list" branch, which requires the probe to have answered). Symptom: Wordfence/path-sensitive plugins misbehave after migration; extremely hard to diagnose from the outside. |
|
||||||
|
|
||||||
|
### The premise to examine first
|
||||||
|
|
||||||
|
The fail-open argument assumes "the site serves either way; only ini fragments differ."
|
||||||
|
That is true when `lsphp -i` answers but lacks a `Scan this dir` line (weird packaging,
|
||||||
|
still a working interpreter). It is **much weaker** when `lsphp -i` produces *nothing*:
|
||||||
|
the same `$LSPHP_BIN` that just failed to print phpinfo is what the entrypoint `exec`s
|
||||||
|
as PID 1 to serve requests. A binary that can't run `-i` because of a missing shared
|
||||||
|
library will not serve LSAPI either — fail-open in that case doesn't buy a serving
|
||||||
|
container, it buys a container that passes `docker ps`, then wedges or crash-loops
|
||||||
|
*after* the entrypoint's diagnostic moment has passed. So the two sub-cases deserve
|
||||||
|
different treatment, and conflating them is the main framing error in a flat
|
||||||
|
"fail-open vs fail-closed" question.
|
||||||
|
|
||||||
|
But there is a counterweight, and it is specific to this fleet: **`lsphp -i` can fail
|
||||||
|
transiently on a healthy image.** These hosts have documented fork-starvation episodes
|
||||||
|
(Committed_AS pinned against CommitLimit during backup windows; it has broken backups
|
||||||
|
before). A `fork()`/`execve` failure at boot time yields exactly "empty `LSPHP_INFO`"
|
||||||
|
through the `|| true`. Under fail-closed, a container (re)starting during a memory-pressure
|
||||||
|
window would refuse to boot **because of the host's state, not its own**, and its restart
|
||||||
|
policy would then add restart churn to a host already under pressure. The previous
|
||||||
|
incident has the same shape: the last time "the probe returned nothing useful" happened
|
||||||
|
in production, the image was *fine* — the probe itself was broken (the SIGPIPE bug), and
|
||||||
|
the fail-closed behavior converted a cosmetic probe defect into a fleet-wide
|
||||||
|
fails-to-boot on busy hosts only. Historically, probe-side failures have outnumbered
|
||||||
|
genuinely-corrupt-image failures **1–0 (or 2–0 counting fork starvation as observed
|
||||||
|
class)**. That base rate is the strongest single argument against pure fail-closed.
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
**A. Fail-open as fixed (status quo).**
|
||||||
|
- Buys: no false boot failures from probe-side defects or host memory pressure; matches
|
||||||
|
the extension's documented posture; sibling entrypoints consistent (once litespeed
|
||||||
|
gains the warning it currently lacks).
|
||||||
|
- Costs: "starts but serves degraded" is precisely the state this platform is worst at
|
||||||
|
noticing — the container watchdog's filter has historically been `exited|dead|created`
|
||||||
|
only, blind to running-but-wrong, and its post-start verify checks `State.Running`, so
|
||||||
|
it would even score a restart of a degraded container as success. Nothing alerts on the
|
||||||
|
stderr WARNINGs. Realistic outcome of a genuine failure under Option A: **weeks of
|
||||||
|
silent degradation**, surfaced eventually as a confusing customer ticket ("where are my
|
||||||
|
errors?" / plugin misbehavior), with the diagnostic clue buried in `docker logs` of a
|
||||||
|
container nobody had reason to inspect.
|
||||||
|
|
||||||
|
**B. Fail-closed (`exit 1`) whenever `lsphp -i` yields nothing usable.**
|
||||||
|
- Buys: loud, immediate, operator-visible failure; watchdog catches `exited`; the
|
||||||
|
diagnostic (the WARNING line) is the *last* thing in `docker logs`, exactly where an
|
||||||
|
on-call looks first. A genuinely broken image cannot masquerade as healthy for weeks.
|
||||||
|
- Costs: converts transient host-side failures (fork starvation) and any future
|
||||||
|
probe-side defect into customer downtime. On a bad-image push, every container that
|
||||||
|
restarts goes down *hard* — arguably correct (they weren't going to serve anyway), but
|
||||||
|
it also means a probe regression like the one just fixed would again be a mass boot
|
||||||
|
outage rather than a mass warning. The customer experience of B's false positive is
|
||||||
|
strictly worse than A's true positive: total outage vs. degraded-but-serving.
|
||||||
|
- Note: B is not "the old behavior restored" — the old behavior also failed to boot on
|
||||||
|
*healthy* images because the probe itself was the broken part. Anyone arguing "the old
|
||||||
|
crash-loop caught real problems" should first account for the fact that the only
|
||||||
|
production firing of that crash was a false positive.
|
||||||
|
|
||||||
|
**C. Fail-open + a machine-detectable degraded marker (middle).**
|
||||||
|
Boot and serve, but make the degraded state a *first-class, monitorable fact* rather than
|
||||||
|
a log line:
|
||||||
|
1. On any degraded branch, write a marker file (e.g. `/run/cac-degraded` containing the
|
||||||
|
mode string) in addition to the existing stderr WARNING, in **both** entrypoints —
|
||||||
|
and add the currently missing warning + summary line to `entrypoint-litespeed.sh` so
|
||||||
|
the two siblings really are consistent (today's "consistency" claim is only half true).
|
||||||
|
2. Retry the probe once or twice with a short sleep before accepting degradation. This
|
||||||
|
is nearly free and specifically absorbs the fork-starvation transient, which is the
|
||||||
|
most likely real-world cause of an empty probe on this fleet.
|
||||||
|
3. Surface the marker: either a periodic platform check (`docker exec ... test -f
|
||||||
|
/run/cac-degraded` across cac containers, or grepping the boot summary line
|
||||||
|
`path parity = none` from `docker logs --tail`) wired into existing monitoring/alerting,
|
||||||
|
or fold it into the health story. **Caution on the healthcheck route:** the
|
||||||
|
HEALTHCHECKs (all seven Dockerfiles have one) are liveness probes; making "degraded
|
||||||
|
config" report unhealthy is tempting but risky — if the watchdog ever learns to
|
||||||
|
restart unhealthy containers, a persistently-degraded-but-serving site would be
|
||||||
|
restart-looped for a condition a restart cannot fix. Keep degraded ≠ unhealthy;
|
||||||
|
alert on the marker through a channel that pages a human instead of triggering an
|
||||||
|
automated restart.
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
**Option C, with one fail-closed carve-out considered and (narrowly) rejected.**
|
||||||
|
|
||||||
|
The carve-out I weighed: fail closed *only* in the `lsphp -i is unusable` sub-case (probe
|
||||||
|
empty), fail open in the `answered but no scan dir` sub-case — on the logic that an
|
||||||
|
interpreter that can't print phpinfo probably can't serve. I recommend against it, for
|
||||||
|
two reasons. First, the fork-starvation transient lands in exactly that sub-case and
|
||||||
|
would cause real, host-pressure-correlated boot failures of healthy sites; the retry in
|
||||||
|
C(2) mitigates but can't eliminate it. Second, if the binary truly cannot execute, the
|
||||||
|
final `exec "$LSPHP_BIN" -b ...` fails on its own a few lines later and PID 1 dies
|
||||||
|
anyway — fail-closed-at-the-probe mostly duplicates a crash the container will have
|
||||||
|
regardless, while adding a new false-positive class. The genuinely dangerous residue —
|
||||||
|
binary runs but is subtly broken — is better caught by the existing HEALTHCHECK
|
||||||
|
(TCP :9000 + `pgrep lsphp`), which *does* fail in that world; the platform gap is that
|
||||||
|
the watchdog ignores unhealthy, and that gap is worth closing on its own merits.
|
||||||
|
|
||||||
|
Per-type answer: same posture for `cac-lsphp` and `cac-litespeed` (their failure domains
|
||||||
|
are equivalent for this probe; the current behavioral gap between them is an oversight,
|
||||||
|
not a design). For **shared-tier** entrypoints the answer is *more* strongly fail-open
|
||||||
|
than for per-site containers, should a similar probe ever appear there: a fail-closed
|
||||||
|
shared tier takes down every site on the host, and the marginal diagnostic value of a
|
||||||
|
crash over a marker does not scale with the blast radius.
|
||||||
|
|
||||||
|
Why C over A, stated so it can be disagreed with: A and C serve the customer identically;
|
||||||
|
they differ only in whether the platform can *notice*. The honest failure-likelihood
|
||||||
|
ranking on this fleet is (1) transient probe failure under memory pressure, (2) probe
|
||||||
|
logic regression, (3) genuinely broken image — and only (3) is the case where B
|
||||||
|
outperforms. C handles (1) with the retry, (2) and (3) with the marker + alert, and its
|
||||||
|
worst case is "degraded for as long as the alerting channel takes to be read" instead of
|
||||||
|
A's "degraded for weeks" or B's "down because the host was busy." If you believe a
|
||||||
|
corrupt image push is *more* likely than a probe/host transient, B becomes defensible —
|
||||||
|
but the recent incident history says otherwise.
|
||||||
|
|
||||||
|
What a customer experiences, per option, when the failure is genuine: A/C — site up,
|
||||||
|
error log empty, opcache defaults, possible plugin path weirdness; C additionally gets it
|
||||||
|
noticed and fixed in hours. B — site down until an operator acts, but the operator is
|
||||||
|
paged immediately by existing machinery. Which failure gets silently tolerated for weeks:
|
||||||
|
A's, unambiguously. B's cannot be (crash-loops are loud); C's only if the marker alert is
|
||||||
|
never actually wired up — which is why C **is not done** until the alerting side ships,
|
||||||
|
and shipping the marker without the consumer would be A with extra steps.
|
||||||
|
|
||||||
|
Before committing, measure:
|
||||||
|
- Fleet scan: does `path parity = none` (or an empty scan dir) occur on any live
|
||||||
|
container today? (`docker logs` grep across cac-lsphp/litespeed containers.) If it
|
||||||
|
already fires, the fix priority changes from "posture" to "why".
|
||||||
|
- Frequency of boot-time fork failures during backup windows (correlate container start
|
||||||
|
timestamps with `MemAvailable` dips) — validates or kills the retry rationale.
|
||||||
|
- Whether the watchdog's unhealthy-blindness fix (in progress platform-side) will ever
|
||||||
|
auto-restart on unhealthy; that decides whether degraded may ever map to unhealthy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Call 2 — should the new CI check gate releases?
|
||||||
|
|
||||||
|
### What actually exists (`.gitea/workflows/build-push.yaml`)
|
||||||
|
|
||||||
|
A `Shell-Checks` job runs on every push to trunk: (1) `bash -n` over every script,
|
||||||
|
(2) `shellcheck -S warning` over an explicit allow-list of the pipefail-era scripts,
|
||||||
|
(3) `scripts/tests/lsphp-info-probe.test.sh`, whose section 5 is the structural scan
|
||||||
|
that outlaws early-exit-reader pipelines in any `pipefail`-enabled script under
|
||||||
|
`scripts/` and `ext/`. None of the six image build jobs has `needs: Shell-Checks`, and
|
||||||
|
the workflow's own comment says so: "a red job here does not block a push that is
|
||||||
|
otherwise fine." So today a red check produces a red run *annotation* while all seven
|
||||||
|
images publish anyway.
|
||||||
|
|
||||||
|
False-positive surface, honestly assessed:
|
||||||
|
- `bash -n` and the test's sections 1–4 are deterministic (the pre-fix pipeline is run
|
||||||
|
only informationally; no pass/fail depends on the race). Near-zero FP risk.
|
||||||
|
- `shellcheck` runs against a hand-curated clean set, so it goes red only when a listed
|
||||||
|
file regresses or a new shellcheck version tightens — low FP risk, nonzero.
|
||||||
|
- The structural scan is a line-oriented awk text scan with flat quote-stripping. Its
|
||||||
|
own header is candid: it can miss multi-line pipelines (false negatives) and could
|
||||||
|
misread exotic quoting (false positives). It requires the reader to be the first word
|
||||||
|
after `|` and special-cases `case a|b)`, `sed "s|x|y|"`, `||`. FP risk is real but
|
||||||
|
bounded — and an FP is a *visible* red on a specific line, trivially triaged.
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
**A. Leave it non-blocking (status quo).**
|
||||||
|
Buys: zero risk that a scanner quirk halts a release. Costs: this platform's own history
|
||||||
|
says non-blocking signals decay into wallpaper — one-shot installers drifted, the
|
||||||
|
watchdog's blind spot sat unnoticed, and this exact bug class shipped to production
|
||||||
|
through a green build gate and **two** code reviews. A check that exists precisely
|
||||||
|
because the build gate can't see entrypoint runtime, but that cannot stop a release,
|
||||||
|
protects nothing on the day it matters; it merely timestamps the moment the regression
|
||||||
|
was ignorable. The failure it would have caught kills PID 1 on busy production hosts
|
||||||
|
only — the single most expensive place to discover it.
|
||||||
|
|
||||||
|
**B. Hard gate now (add `needs: Shell-Checks` to all six build jobs).**
|
||||||
|
Buys: the regression class physically cannot ship again through this pipeline. Costs: an
|
||||||
|
unproven scanner acquires veto power over every release, including emergency ones. Worth
|
||||||
|
sizing that cost concretely rather than abstractly: releases here are operator-driven
|
||||||
|
pushes to trunk, not a high-frequency train; a false red costs the minutes needed to read
|
||||||
|
the offending line, either fix real sloppiness or adjust the scanner, and re-push. There
|
||||||
|
is no mechanism by which a red silently *delays* an unattended release for days, because
|
||||||
|
releases are attended. The scary version of "blocks all releases" mostly doesn't apply to
|
||||||
|
this repo's release model. The one genuinely bad scenario: an urgent security rebuild
|
||||||
|
blocked at 2 a.m. by a scanner FP — mitigable, since the operator can comment out one
|
||||||
|
`needs:` line in the same push, an escape hatch that is itself visible in the diff.
|
||||||
|
|
||||||
|
**C. Staged: soft-fail now, hard gate after N clean runs.**
|
||||||
|
Buys: evidence before enforcement; if the scanner has an FP mode, it is discovered while
|
||||||
|
red is cheap. Costs: N pushes of window during which the bug class can ship again, and a
|
||||||
|
follow-up task of exactly the kind this platform historically forgets (see: one-shot
|
||||||
|
installer drift). If staged, the flip must be a dated calendar/scheduler entry or a
|
||||||
|
tracked ticket, not an intention.
|
||||||
|
|
||||||
|
**D. Split the gate: hard-gate the deterministic steps now, soft-fail the scanner.**
|
||||||
|
Put `bash -n` + the probe regression test (sections 1–4) in a blocking job immediately —
|
||||||
|
they have effectively no FP surface and section 2's marker check alone prevents the
|
||||||
|
specific regression (reintroducing the inline pipelines) — and leave shellcheck + the
|
||||||
|
structural scan non-blocking until proven. Buys: immediate protection against the exact
|
||||||
|
bug that happened, zero new FP-block risk. Costs: two jobs to maintain; the structural
|
||||||
|
scan (the only piece that generalizes beyond this one probe) stays advisory.
|
||||||
|
|
||||||
|
### Recommendation
|
||||||
|
|
||||||
|
**B — make it a hard gate now**, with C's discipline applied only to future *additions*
|
||||||
|
to the check, and one prerequisite: run the full `Shell-Checks` job as-is against the
|
||||||
|
current trunk and the last ~5 release tags first (locally or via a manual dispatch) to
|
||||||
|
confirm it is green on everything already shipped. If that back-test is green, gate. If
|
||||||
|
it is not, fix or scope the scanner, then gate.
|
||||||
|
|
||||||
|
Reasoning, laid out to be disagreeable-with: the decision hinges on comparing the two
|
||||||
|
error costs *as they occur in this repo's actual release process*. A false red costs an
|
||||||
|
attended operator minutes, with a one-line escape hatch, and every FP found makes the
|
||||||
|
scanner better. A false green — or a true red that doesn't block — reproduces a
|
||||||
|
production incident whose failure mode is "customer containers refuse to boot, but only
|
||||||
|
on hosts busy enough to have collapsed pipe buffers," i.e. undetectable in dev by
|
||||||
|
construction. The asymmetry is roughly minutes-of-operator-time vs.
|
||||||
|
production-boot-outage, and the probability of FP red is bounded by a back-test we can
|
||||||
|
run *today* instead of estimating. Choosing C is defensible only if you weight "new check
|
||||||
|
might be flaky in ways a back-test can't reveal" highly — plausible for a check with
|
||||||
|
runtime/timing behavior, but sections 1–4 are deterministic and section 5 is a pure text
|
||||||
|
scan over files that are fully known at back-test time. Flakiness that a back-test can't
|
||||||
|
surface would have to come from the runner environment (shellcheck version drift is the
|
||||||
|
main one — worth pinning the shellcheck version when gating). D is the fallback if the
|
||||||
|
back-test turns up scanner noise that can't be resolved same-day.
|
||||||
|
|
||||||
|
Two obligations come with gating, or the gate rots:
|
||||||
|
1. The shellcheck allow-list must grow with the repo — a new `set -o pipefail` script
|
||||||
|
that never gets listed is invisibly exempt from step (2) (the structural scan, by
|
||||||
|
contrast, auto-discovers pipefail scripts). A one-line CI assertion that every
|
||||||
|
pipefail-enabled script appears in the shellcheck list would close that seam.
|
||||||
|
2. Green must not be oversold: the scanner's documented limits (single-line pipelines,
|
||||||
|
flat quote-stripping) mean a multi-line pipeline reintroduction passes it. The gate
|
||||||
|
raises the floor; it does not replace review. Record that limit where reviewers see
|
||||||
|
it (it already is, in the test header — keep it there).
|
||||||
|
|
||||||
|
Before committing, measure: the back-test above (mandatory); shellcheck version on the
|
||||||
|
Gitea runner vs. pinned (drift is the likeliest future FP source); and after gating,
|
||||||
|
track red-run causes for the first month — if more than ~1 in 10 reds is a scanner FP,
|
||||||
|
revisit with D.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Framing notes (where the question as posed deserves push-back)
|
||||||
|
|
||||||
|
1. "Previously it failed loudly and the watchdog caught it" is only half the history:
|
||||||
|
the *only* production firing of the loud failure was a false positive (the probe's own
|
||||||
|
SIGPIPE), not a broken image. The old posture's loudness was never demonstrated on a
|
||||||
|
genuine failure; pricing it as a proven detection mechanism overstates its record.
|
||||||
|
2. The fail-open "consistency with the sibling" claim is currently aspirational:
|
||||||
|
`entrypoint-litespeed.sh` fails open *silently* while `entrypoint-lsphp.sh` fails open
|
||||||
|
loudly with mode strings. Whatever posture is chosen, making the siblings actually
|
||||||
|
consistent (warning + summary line + marker in both) is part of the work.
|
||||||
|
3. "Fail-open vs fail-closed" flattens two distinct sub-cases — probe empty vs. probe
|
||||||
|
answered without a scan dir — that have different likely causes and different serving
|
||||||
|
prognoses. The entrypoint already distinguishes them in its reporting; the decision
|
||||||
|
should too (and above, does).
|
||||||
@@ -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=<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.
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
# Internal package + image cache (`PKG_CACHE`)
|
||||||
|
|
||||||
|
Opt-in build support for the LAN package cache **CT130 `pkg-cache` at `https://192.168.1.40`** (Proxmox node `pve`, storage `shared-storage-tn`).
|
||||||
|
|
||||||
|
Without `PKG_CACHE`, builds use the upstream repositories. The one change from before is that the EPEL and Remi release RPMs are now **GPG-verified** against `keys/pkg-cache/` before they are installed.
|
||||||
|
|
||||||
|
Why it exists: during the 2026-10-21 PHP security release, builds failed because:
|
||||||
|
- Remi mirrors lagged hours behind origin;
|
||||||
|
- Remi's origin reset or timed out HTTPS under load;
|
||||||
|
- an EPEL mirror had a broken certificate.
|
||||||
|
|
||||||
|
On that day a build without the cache spent **1031 s** in its first dnf step. Through the cache the same step took **40 s cold and 21 s warm**.
|
||||||
|
|
||||||
|
## What the cache serves
|
||||||
|
|
||||||
|
| URL (https, pkg-cache CA) | Upstream tiers, tried in order (stale copy last) |
|
||||||
|
|---|---|
|
||||||
|
| `https://192.168.1.40/remi/` | `https://rpms.remirepo.net`, then `http://rpms.remirepo.net` (origin only, no mirrors) |
|
||||||
|
| `https://192.168.1.40/epel/` | `https://dl.fedoraproject.org/pub/epel`, `https://mirror.math.princeton.edu/pub/epel`, `https://mirrors.kernel.org/fedora-epel` |
|
||||||
|
| `https://192.168.1.40/almalinux/` | `https://repo.almalinux.org/almalinux`, `https://mirror.math.princeton.edu/pub/almalinux` |
|
||||||
|
| `https://192.168.1.40/ubuntu/` | `https://archive.ubuntu.com/ubuntu`, `https://us.archive.ubuntu.com/ubuntu` |
|
||||||
|
| `https://192.168.1.40/ubuntu-security/` | `https://security.ubuntu.com/ubuntu`, `https://archive.ubuntu.com/ubuntu` |
|
||||||
|
| `https://192.168.1.40/litespeed/` | `https://rpms.litespeedtech.com`, then `http://rpms.litespeedtech.com` |
|
||||||
|
| `192.168.1.40:5000` (TLS, pkg-cache CA) | Docker Hub pull-through cache (`registry-1.docker.io`) |
|
||||||
|
|
||||||
|
Plain `http://192.168.1.40/` answers **403** to LAN clients.
|
||||||
|
|
||||||
|
Cache rules:
|
||||||
|
- **Metadata** has a 3-minute TTL. After that it is revalidated upstream (`If-Modified-Since`). Metadata means:
|
||||||
|
- `repomd.xml*`
|
||||||
|
- repodata files whose names don't start with a hash
|
||||||
|
- apt `dists/`, except `by-hash/`
|
||||||
|
- `*-latest*.rpm` and `remi-release-N.rpm`
|
||||||
|
- `RPM-GPG-KEY*`
|
||||||
|
- directory listings
|
||||||
|
- **Packages** (`*.rpm`, `*.deb`, hash-named repodata, `by-hash/`) are cached for 30 days and evicted after 60 days unused. Total size is capped at 110 GB, with 15 GB kept free.
|
||||||
|
- On a connect error, timeout or 5xx, the request falls through to the next tier. Only the **last** tier may answer from a stale copy, so the order is primary, then fallback, then stale.
|
||||||
|
- `proxy_cache_lock` is on.
|
||||||
|
- Responses carry `X-Cache-Status` and `X-Pkgcache-Tier`.
|
||||||
|
|
||||||
|
## Building with the cache
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker buildx build --build-arg PKG_CACHE=https://192.168.1.40 --build-arg PHPVER=85 -f Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
`scripts/pkg-cache-repos.sh` (installed as `/usr/local/sbin/`) and `keys/pkg-cache/` (installed as `/usr/local/share/pkg-cache/`) are wired into `Dockerfile`, `Dockerfile.fpm`, `Dockerfile.shared-httpd` and `Dockerfile.shared-ols`. The script has four modes.
|
||||||
|
|
||||||
|
**`rpm`** handles AlmaLinux, EPEL and Remi:
|
||||||
|
- Each `baseurl` becomes `<cache>/<prefix>/... <origin>/...`. dnf fails over to the origin if the cache dies.
|
||||||
|
- That section's mirrorlist/metalink is commented out.
|
||||||
|
- The section gets `sslcacert=` pointing at a bundle of the system CAs plus the pkg-cache CA. The bundle lives in the state dir and is deleted by `restore`.
|
||||||
|
- It also gets `gpgcheck=1`.
|
||||||
|
- AlmaLinux and Remi also get `repo_gpgcheck=1`, because both sign `repomd.xml` (measured 2026-10-21: `repomd.xml.asc` returns 200). EPEL publishes no repomd signature (404), so its metadata integrity rests on HTTPS end to end.
|
||||||
|
|
||||||
|
**`apt`** handles Ubuntu archive/security and LiteSpeed:
|
||||||
|
- Their URIs are rewritten to the cache.
|
||||||
|
- `/etc/apt/apt.conf.d/99pkg-cache` sets `Acquire::https::192.168.1.40::CaInfo`, so the CA is trusted for the cache host only.
|
||||||
|
- apt verifies the signed `InRelease`/`Release.gpg` as always. It has no failover, so CI must probe the cache first.
|
||||||
|
|
||||||
|
**`release-install epel [remi]`**:
|
||||||
|
- Downloads the release RPM over HTTPS, from the cache or from the origin.
|
||||||
|
- Checks the package name.
|
||||||
|
- Imports the key committed in `keys/pkg-cache/`.
|
||||||
|
- Requires `rpm -K` to report `digests signatures OK`.
|
||||||
|
- Installs with `--setopt=localpkg_gpgcheck=1`.
|
||||||
|
|
||||||
|
**`restore`**:
|
||||||
|
- Puts rewritten files back.
|
||||||
|
- Deletes files the script created.
|
||||||
|
- Deletes any repo/apt file that **appeared during the RUN** and still references the cache.
|
||||||
|
- Then **fails the build** if any file under `/etc/yum.repos.d`, `/etc/dnf` or `/etc/apt` still mentions the cache.
|
||||||
|
|
||||||
|
Every RUN that rewrites ends with `restore`, so no shipped layer carries the LAN address. This was verified on the built images: 0 references, no state dir, no apt drop-in.
|
||||||
|
|
||||||
|
`PKG_CACHE` must start with `https://`. `http://` is refused.
|
||||||
|
|
||||||
|
`Dockerfile.litespeed` and `Dockerfile.lsphp` are **not wired yet**. Two things are needed first:
|
||||||
|
- the multi-stage `lsphp-apt-upgrade.sh` flow needs a `PKG_CACHE` ARG in each stage;
|
||||||
|
- its madison check must accept the cache URL. It currently requires `*rpms.litespeedtech.com*`, and via the cache madison shows `https://192.168.1.40/litespeed/debian`.
|
||||||
|
|
||||||
|
## Trust model
|
||||||
|
|
||||||
|
`keys/pkg-cache/ca.crt` is the *pkg-cache internal CA*:
|
||||||
|
- EC P-256, valid to 2036.
|
||||||
|
- **Name-constrained** to IP `192.168.1.40` and DNS `pkg-cache.internal.cloud-hosting.io` / `pkg-cache.geekhome.local`. Even its private key, which exists only on CT130 at `/etc/pkgcache-ca/ca.key`, cannot mint a certificate that clients would accept for any other host.
|
||||||
|
- It is trusted only by the rewritten repos, the apt per-host option and the BuildKit/dockerd registry config. It is never added to a system trust store.
|
||||||
|
|
||||||
|
The server certificate `/etc/pkgcache-ca/server.crt` **expires 2029-10-20**. `pkgcache-stats` prints the expiry. Renew it with the same `openssl x509 -req ... -extfile srv.cnf` steps in `/etc/pkgcache-ca/`.
|
||||||
|
|
||||||
|
Remaining risks:
|
||||||
|
- **Replay of old but validly signed metadata.** Signatures stop tampering, not freshness. The cache's own plain-http tiers for Remi and LiteSpeed are the leg where an internet MITM could replay. The php.net gates (`EXPECT_PHP_MIN`, `EXPECT_LSPHP_PKG`/`EXPECT_PHP_VERSION`) fail an image build that gets old PHP. **Josh to decide** whether to drop those http tiers: you gain replay resistance and lose availability when Remi's HTTPS is down, as it was all day on 2026-10-21.
|
||||||
|
- **Proposed, not implemented:** the same kind of minimum-version gate for other security-relevant packages:
|
||||||
|
- CI resolves the newest `openssl`/`openssl-libs` and `httpd` from AlmaLinux BaseOS/AppStream, and `ImageMagick` from EPEL, **directly from the origin over HTTPS** (e.g. `dnf repoquery --repofrompath=o,https://repo.almalinux.org/almalinux/10/BaseOS/x86_64/os/ --repo=o --latest-limit=1 --qf '%{evr}' openssl-libs`).
|
||||||
|
- It passes the results as `EXPECT_OPENSSL_MIN` / `EXPECT_HTTPD_MIN` / `EXPECT_IMAGEMAGICK_MIN`.
|
||||||
|
- The Dockerfile asserts `rpm -q --qf '%{EVR}'` is not older (`rpm --eval "%{lua: print(rpm.vercmp('A','B'))}"`).
|
||||||
|
|
||||||
|
## CI integration (APPLIED 2026-10-21)
|
||||||
|
|
||||||
|
1. **Runner network.** Ephemeral runners are NATed and `iptables FORWARD` drops
|
||||||
|
all RFC1918 destinations:
|
||||||
|
- `eph-pve-*` on `pve`, vmbr2, 10.99.98.0/24;
|
||||||
|
- `eph-pve3-*` on `pve3`, vmbr1, 10.99.99.0/24.
|
||||||
|
|
||||||
|
One rule per node now sits **above** those DROPs. It is persistent through a
|
||||||
|
`post-up`/`post-down` pair placed before the first DROP line in
|
||||||
|
`/etc/network/interfaces.d/vmbr2-ci-isolated` (pve) and
|
||||||
|
`/etc/network/interfaces.d/vmbr1-ci-isolated` (pve3). The Proxmox firewall is
|
||||||
|
disabled on both nodes, so its reloads do not touch these chains. Backups:
|
||||||
|
`/root/vmbr{2,1}-ci-isolated.bak-pkgcache-20261021`.
|
||||||
|
```
|
||||||
|
iptables -I FORWARD 1 -i vmbr2 -o vmbr0 -d 192.168.1.40/32 -p tcp -m multiport --dports 443,5000 \
|
||||||
|
-m comment --comment pkg-cache-ct130 -j ACCEPT
|
||||||
|
```
|
||||||
|
Verified from throwaway CTs on vmbr2 and vmbr1:
|
||||||
|
- :443 and :5000 work with TLS verified;
|
||||||
|
- CT130 :22/:80, pve :8006, .193 :22, the router's :53 and 10.10.14.20 stay **blocked**;
|
||||||
|
- internet access is unchanged.
|
||||||
|
|
||||||
|
The LAN runner VM *Git-Builders* (VM103, 192.168.1.126 on vmbr0) needs no
|
||||||
|
rule. `gitea-runner.internal.cloud-hosting.io` resolves to 10.10.14.20, which
|
||||||
|
is unroutable from both the LAN and the DC (stale record?).
|
||||||
|
2. **Workflow** (`.gitea/workflows/build-push.yaml`). Jobs Build-and-Push,
|
||||||
|
Build-FPM-Images, Build-Shared-httpd and Build-Shared-OLS run a
|
||||||
|
**"Probe package cache"** step. It curls `https://192.168.1.40/` and
|
||||||
|
`https://192.168.1.40:5000/v2/` with `--cacert keys/pkg-cache/ca.crt
|
||||||
|
--max-time 5`.
|
||||||
|
- If both answer: `PKG_CACHE=https://192.168.1.40` is passed as a build arg,
|
||||||
|
and buildx is set up with the Docker Hub mirror (`buildkitd-config-inline`,
|
||||||
|
`ca=`).
|
||||||
|
- If either fails: that output is empty, the build uses upstream repos /
|
||||||
|
Docker Hub, and a `::warning::` is emitted.
|
||||||
|
|
||||||
|
Litespeed and lsphp jobs are unchanged (their Dockerfiles are not wired).
|
||||||
|
**Fail-back measured** with the workflow's own step code, from a runner-bridge CT:
|
||||||
|
|
||||||
|
| cache state | probe result | time |
|
||||||
|
|---|---|---|
|
||||||
|
| up | url + mirror set | <1 s |
|
||||||
|
| nginx + registry stopped | both empty, warning | <1 s (connection refused) |
|
||||||
|
| silently dropped | both empty, warning | 10 s (2 x 5 s) |
|
||||||
|
|
||||||
|
Mid-build cache loss is also covered: dnf has the origin as a 2nd baseurl, and
|
||||||
|
BuildKit falls back from the mirror to Docker Hub (measured with the registry stopped).
|
||||||
|
3. **dockerd clients** (plain `docker pull`). Untested, because it needs a daemon restart.
|
||||||
|
Use TLS, no `insecure-registries`:
|
||||||
|
```
|
||||||
|
install -D -m0644 keys/pkg-cache/ca.crt /etc/docker/certs.d/192.168.1.40:5000/ca.crt
|
||||||
|
# /etc/docker/daemon.json: { "registry-mirrors": ["https://192.168.1.40:5000"] } then restart docker
|
||||||
|
```
|
||||||
|
|
||||||
|
## WHP hosts (`scripts/host/pkgcache-host-repos.sh`)
|
||||||
|
|
||||||
|
`pkgcache-host-repos.sh apply https://192.168.1.40` does the following:
|
||||||
|
- backs up every `/etc/yum.repos.d/*.repo` to `/root/pkgcache-repo-backup-<UTC>/`;
|
||||||
|
- installs the name-constrained CA as a system anchor;
|
||||||
|
- rewrites the AlmaLinux, EPEL and Remi sections to `baseurl=<cache>/... <origin>/...`;
|
||||||
|
- comments out mirrorlist/metalink in those sections;
|
||||||
|
- sets `gpgcheck=1`, plus `repo_gpgcheck=1` for AlmaLinux and Remi.
|
||||||
|
|
||||||
|
`revert [DIR]` restores byte-identical files (verified on AL9 and AL10) and
|
||||||
|
removes the anchor. `status` shows the current state.
|
||||||
|
|
||||||
|
**Why not "cache first, mirrorlist as fallback":** measured on AL10, with
|
||||||
|
mirrorlist/metalink left in place dnf **ignores `baseurl` completely** (0 cache
|
||||||
|
requests). The origin as second baseurl is the fallback dnf really performs.
|
||||||
|
|
||||||
|
**Measured on 192.168.1.148** (AlmaLinux 10.2), 2026-10-21:
|
||||||
|
|
||||||
|
| test | result |
|
||||||
|
|---|---|
|
||||||
|
| baseline `dnf makecache --refresh` (original mirrorlists) | rc=0, **1209 s** |
|
||||||
|
| via cache `makecache --refresh` | rc=0, **22 s** (2nd run, non-interactive: 2 s) |
|
||||||
|
| via cache `dnf download php-cli` | rc=0, 1 s |
|
||||||
|
| `dnf -y --downloadonly update php-cli` | "Nothing to do" (already 8.5.12) |
|
||||||
|
| cache **down** (nginx stopped): makecache --refresh / download | rc=0 **16 s** / rc=0 24 s, served by origin |
|
||||||
|
| cache **silently dropped** (iptables DROP): makecache --refresh / download | rc=0 **225 s** / rc=0 59 s (dnf's 30 s connect timeout per attempt) |
|
||||||
|
|
||||||
|
**Datacenter hosts** (whp01, whp02, sdbees, TrueSelfCA) cannot reach
|
||||||
|
192.168.1.40 and are **not configured**. That needs CT130 on NetBird (see the
|
||||||
|
report: setup key / route pending).
|
||||||
|
|
||||||
|
## Operating the cache (CT130)
|
||||||
|
|
||||||
|
- **nginx config.**
|
||||||
|
- `/etc/nginx/conf.d/pkgcache-http.conf` holds the cache zone, maps and log format.
|
||||||
|
- `/etc/nginx/pkgcache/{server-common,meta,pkg}.conf` hold the shared settings.
|
||||||
|
- `/etc/nginx/sites-available/pkgcache` is **generated** by `/usr/local/sbin/pkgcache-gen-nginx`. Edit the `UPSTREAMS` table, then run `pkgcache-gen-nginx > /etc/nginx/sites-available/pkgcache && nginx -t && systemctl reload nginx`.
|
||||||
|
- Listeners: :443 TLS for the LAN, `127.0.0.1:80` for local tools, and :80 for the LAN answers 403.
|
||||||
|
- **Registry.** Config is `/etc/docker/registry/config.yml` (Debian `docker-registry` 2.8.3, the same code as `registry:2`, with TLS). Service `docker-registry`.
|
||||||
|
- **TLS.** Everything is in `/etc/pkgcache-ca/`: `ca.key` (0600), `ca.crt`, `server.key`, `server-chain.crt`, and the `ca.cnf`/`srv.cnf` openssl configs.
|
||||||
|
- **Data.** nginx uses `/srv/cache/nginx`, the registry uses `/srv/cache/registry`. Both live on the 150 GB mount point `mp0` (`backup=0`).
|
||||||
|
- **Security-release day.** `pkgcache-refresh` re-fetches every cached metadata entry right away. Variants:
|
||||||
|
- `pkgcache-refresh remi` refreshes one prefix;
|
||||||
|
- `--list` lists the entries;
|
||||||
|
- `--purge` deletes them instead.
|
||||||
|
|
||||||
|
A refresh bypasses the cache from localhost only and replaces each entry in place. If upstream is down, the old copy stays. Metadata is at most 3 minutes old anyway.
|
||||||
|
- **Monitoring.** `pkgcache-stats` shows:
|
||||||
|
- disk usage;
|
||||||
|
- hit ratio;
|
||||||
|
- fallback-tier use;
|
||||||
|
- the STALE count;
|
||||||
|
- upstream errors;
|
||||||
|
- certificate expiry.
|
||||||
|
|
||||||
|
Logs are `/var/log/nginx/pkgcache.access.log` (fields `cache=`, `tier=`, `up=`) and `journalctl -u docker-registry`. Suggested Uptime Kuma checks are `https://192.168.1.40/` and `https://192.168.1.40:5000/v2/`, with certificate checking against the CA.
|
||||||
|
- **Docker Hub rate limits.** Anonymous pulls are limited per egress IP, and all clients share it. Add `username`/`password` (a Hub access token) under `proxy:` if needed.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# phpize / configure / make artifacts from building this extension locally.
|
||||||
|
# The shipped build happens inside Dockerfile.lsphp's ext-build stage, so
|
||||||
|
# nothing generated here is ever committed.
|
||||||
|
.deps
|
||||||
|
.libs/
|
||||||
|
Makefile
|
||||||
|
Makefile.fragments
|
||||||
|
Makefile.global
|
||||||
|
Makefile.objects
|
||||||
|
acinclude.m4
|
||||||
|
aclocal.m4
|
||||||
|
autom4te.cache/
|
||||||
|
build/
|
||||||
|
config.guess
|
||||||
|
config.h
|
||||||
|
config.h.in
|
||||||
|
config.log
|
||||||
|
config.nice
|
||||||
|
config.status
|
||||||
|
config.sub
|
||||||
|
configure
|
||||||
|
configure.ac
|
||||||
|
include/
|
||||||
|
install-sh
|
||||||
|
libtool
|
||||||
|
ltmain.sh
|
||||||
|
missing
|
||||||
|
mkinstalldirs
|
||||||
|
modules/
|
||||||
|
run-tests.php
|
||||||
|
*.lo
|
||||||
|
*.la
|
||||||
|
*.o
|
||||||
|
*.so
|
||||||
|
tests/*.php
|
||||||
|
tests/*.diff
|
||||||
|
tests/*.exp
|
||||||
|
tests/*.log
|
||||||
|
tests/*.out
|
||||||
|
tests/*.sh
|
||||||
|
!tests/fpm-parity-check.sh
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
/*
|
||||||
|
* cac_path_parity — $_SERVER filesystem-path parity for the shared-ols tier.
|
||||||
|
*
|
||||||
|
* WHAT PROBLEM THIS SOLVES
|
||||||
|
* ------------------------
|
||||||
|
* A site on the standalone tiers (cac / cac-fpm / cac-litespeed) sees:
|
||||||
|
*
|
||||||
|
* $_SERVER['DOCUMENT_ROOT'] = /home/<user>/public_html
|
||||||
|
* $_SERVER['SCRIPT_FILENAME'] = /home/<user>/public_html/index.php
|
||||||
|
*
|
||||||
|
* On the shared-ols tier the webserver is a SHARED OpenLiteSpeed container that
|
||||||
|
* serves every tenant out of one bulk `/docker/users -> /mnt/users:ro` mount, so
|
||||||
|
* its vhost docRoot is /mnt/users/<user>/<domain>/public_html. OLS has no
|
||||||
|
* ProxyFCGISetEnvIf-style remap (unlike shared-httpd -> cac-fpm): it hands lsphp
|
||||||
|
* exactly that path. The cac-lsphp sidecar symlinks /mnt/users/<user>/<domain>
|
||||||
|
* -> /home/<user>, so every file OPERATION resolves and PHP's own __FILE__ /
|
||||||
|
* __DIR__ / realpath() / getcwd() already report /home/<user>/... — but the RAW
|
||||||
|
* strings OLS put in $_SERVER still read /mnt/users. Moving a site from cac-fpm
|
||||||
|
* to cac-lsphp therefore changed two $_SERVER values, which is exactly the kind
|
||||||
|
* of difference that surfaces later as a broken plugin path or a mismatched
|
||||||
|
* absolute path stored in the database.
|
||||||
|
*
|
||||||
|
* WHY THIS IS AN EXTENSION AND NOT AN auto_prepend_file
|
||||||
|
* -----------------------------------------------------
|
||||||
|
* The first fix was an `auto_prepend_file` drop-in that realpath()'d the two
|
||||||
|
* keys. `auto_prepend_file` is PHP_INI_PERDIR, so ANY site with its own
|
||||||
|
* .user.ini auto_prepend_file silently displaces it and the normaliser never
|
||||||
|
* runs — the state 7 live shared_ols sites are actually in today (Wordfence and
|
||||||
|
* cPanel imports). PHP resolves a single winning value for auto_prepend_file
|
||||||
|
* after the .user.ini chain is parsed, so there is no way to "chain" from the
|
||||||
|
* losing side either.
|
||||||
|
*
|
||||||
|
* The obvious hardening — `php_admin_value auto_prepend_file` — is WORSE, not
|
||||||
|
* better: making our prepend un-overridable makes the customer's prepend
|
||||||
|
* un-runnable, which would disable those same 7 Wordfence WAFs. The two goals
|
||||||
|
* are irreconcilable as long as the mechanism IS the prepend hook.
|
||||||
|
*
|
||||||
|
* An extension sidesteps that entirely. RINIT runs before any userland code and
|
||||||
|
* cannot be displaced by .user.ini, and it consumes no userland hook — so the
|
||||||
|
* customer's auto_prepend_file remains the only prepend in play and keeps
|
||||||
|
* working untouched. Both constraints are satisfied at once.
|
||||||
|
*
|
||||||
|
* The mapping comes from two PHP_INI_SYSTEM entries. PHP_INI_SYSTEM is NOT
|
||||||
|
* settable from .user.ini (which honours only PHP_INI_PERDIR/PHP_INI_USER), nor
|
||||||
|
* from ini_set(), nor from .htaccess — so a customer cannot point the rewrite
|
||||||
|
* somewhere else or switch it off. The cac-lsphp entrypoint writes them from the
|
||||||
|
* same `user`/`domain` env the symlink is built from, so the two can't drift.
|
||||||
|
*
|
||||||
|
* WHY A STRING PREFIX SWAP AND NOT realpath()
|
||||||
|
* -------------------------------------------
|
||||||
|
* The old normaliser called realpath(), which worked only because the sidecar
|
||||||
|
* symlinks /mnt/users/<user>/<domain> -> /home/<user>. A plain prefix swap is
|
||||||
|
* better on every axis that matters here:
|
||||||
|
*
|
||||||
|
* - It is byte-identical to cac-fpm BY CONSTRUCTION. realpath() resolves ALL
|
||||||
|
* symlinks, so a customer who makes public_html itself a symlink would get
|
||||||
|
* some third path — cac-fpm reports the literal /home/<user>/public_html.
|
||||||
|
* - It cannot fail. realpath() returns false for a path that does not exist
|
||||||
|
* (and is constrained by open_basedir), leaving the value half-normalised.
|
||||||
|
* - It costs no syscall. realpath() is an lstat chain on every request.
|
||||||
|
*
|
||||||
|
* The prefix is this site's FULL mount path (/mnt/users/<user>/<domain>), not
|
||||||
|
* the bare bulk-mount root, and it only matches on a path-component boundary —
|
||||||
|
* so a value pointing at another tenant, or one that is already canonical, is
|
||||||
|
* never touched.
|
||||||
|
*
|
||||||
|
* FAILURE MODES — every one of them leaves $_SERVER untouched and lets the
|
||||||
|
* request proceed. Nothing here can warn, throw, or 500 a customer site:
|
||||||
|
* - mapping unset/empty (any tier that is not shared-ols) -> RINIT returns
|
||||||
|
* immediately, extension is inert.
|
||||||
|
* - either side of the mapping not an ABSOLUTE path -> inert. Nothing
|
||||||
|
* the entrypoint writes is anything else, and a relative prefix cannot
|
||||||
|
* usefully match a SAPI-supplied path, so a malformed mapping is treated
|
||||||
|
* exactly like an absent one.
|
||||||
|
* - $_SERVER absent or not an array -> return.
|
||||||
|
* - key absent from $_SERVER -> skip that key.
|
||||||
|
* - key present but not a string -> skip that key.
|
||||||
|
* - value shorter than the prefix / prefix mismatch -> skip that key.
|
||||||
|
* - value matches the prefix mid-component -> skip that key.
|
||||||
|
* There is no error path, no userland-visible diagnostic, and no dependency on
|
||||||
|
* the filesystem being readable.
|
||||||
|
*
|
||||||
|
* The one thing this CANNOT detect is a well-formed but WRONG mapping: it will
|
||||||
|
* confidently rewrite to a wrong path and say nothing. That is accepted by
|
||||||
|
* construction rather than overlooked — entrypoint-lsphp.sh derives from/to from
|
||||||
|
* the same two variables it builds the compatibility symlink from, so a wrong
|
||||||
|
* mapping means the symlink is wrong too and the site is already broken in a far
|
||||||
|
* louder way. The only runtime signal is `lsphp -i`, which prints
|
||||||
|
* "Rewriting => active" alongside the live from/to values.
|
||||||
|
*
|
||||||
|
* That row is what the post-deploy fleet canary greps, so MINFO's "active" test
|
||||||
|
* must stay a mirror of the conditions RINIT actually rewrites under — see
|
||||||
|
* PHP_MINFO_FUNCTION below, which shares cacpp_mapping_active() with RINIT
|
||||||
|
* precisely so the two cannot drift. A MINFO that reported "active" for a
|
||||||
|
* mapping RINIT treats as inert would mask exactly the failure the canary
|
||||||
|
* exists to catch.
|
||||||
|
*
|
||||||
|
* SCOPE / KNOWN LIMITS
|
||||||
|
* --------------------
|
||||||
|
* Only $_SERVER is rewritten. LSAPI also answers getenv('DOCUMENT_ROOT') from
|
||||||
|
* the request environment, and that still returns the /mnt/users string. That is
|
||||||
|
* a deliberate limit: touching the SAPI environment risks the request env lsphp
|
||||||
|
* itself reads. Real-world PHP (WordPress and its plugin ecosystem) reads
|
||||||
|
* $_SERVER, not getenv(), for these.
|
||||||
|
*
|
||||||
|
* The sidecar's compatibility symlink is still REQUIRED and is not replaced by
|
||||||
|
* this extension: it is what makes the path OLS sends actually resolve on disk.
|
||||||
|
* This extension only corrects the strings.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifdef HAVE_CONFIG_H
|
||||||
|
#include "config.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include "php.h"
|
||||||
|
#include "php_ini.h"
|
||||||
|
#include "ext/standard/info.h"
|
||||||
|
#include "SAPI.h"
|
||||||
|
#include "zend_compile.h" /* zend_is_auto_global_str() */
|
||||||
|
#include "php_cac_path_parity.h"
|
||||||
|
|
||||||
|
ZEND_DECLARE_MODULE_GLOBALS(cac_path_parity)
|
||||||
|
|
||||||
|
#define CACPP_G(v) ZEND_MODULE_GLOBALS_ACCESSOR(cac_path_parity, v)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The $_SERVER keys that carry a FILESYSTEM path. URI-derived keys (PHP_SELF,
|
||||||
|
* SCRIPT_NAME, REQUEST_URI) are already identical across tiers and are left
|
||||||
|
* alone. PATH_TRANSLATED and CONTEXT_DOCUMENT_ROOT are usually absent under
|
||||||
|
* OLS; rewriting them is a no-op when they are, and correct when they are not.
|
||||||
|
*/
|
||||||
|
static const struct {
|
||||||
|
const char *name;
|
||||||
|
size_t len;
|
||||||
|
} cacpp_keys[] = {
|
||||||
|
{ ZEND_STRL("DOCUMENT_ROOT") },
|
||||||
|
{ ZEND_STRL("SCRIPT_FILENAME") },
|
||||||
|
{ ZEND_STRL("PATH_TRANSLATED") },
|
||||||
|
{ ZEND_STRL("CONTEXT_DOCUMENT_ROOT") },
|
||||||
|
};
|
||||||
|
|
||||||
|
/* clang-format off */
|
||||||
|
PHP_INI_BEGIN()
|
||||||
|
/*
|
||||||
|
* PHP_INI_SYSTEM is load-bearing: it is the reason a customer's .user.ini
|
||||||
|
* cannot reach these. Do not relax to PERDIR.
|
||||||
|
*/
|
||||||
|
STD_PHP_INI_ENTRY("cac_path_parity.from", "", PHP_INI_SYSTEM, OnUpdateString,
|
||||||
|
from, zend_cac_path_parity_globals, cac_path_parity_globals)
|
||||||
|
STD_PHP_INI_ENTRY("cac_path_parity.to", "", PHP_INI_SYSTEM, OnUpdateString,
|
||||||
|
to, zend_cac_path_parity_globals, cac_path_parity_globals)
|
||||||
|
PHP_INI_END()
|
||||||
|
/* clang-format on */
|
||||||
|
|
||||||
|
static PHP_GINIT_FUNCTION(cac_path_parity)
|
||||||
|
{
|
||||||
|
#if defined(COMPILE_DL_CAC_PATH_PARITY) && defined(ZTS)
|
||||||
|
ZEND_TSRMLS_CACHE_UPDATE();
|
||||||
|
#endif
|
||||||
|
cac_path_parity_globals->from = NULL;
|
||||||
|
cac_path_parity_globals->to = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* THE MAPPING PREDICATE — one definition, two callers.
|
||||||
|
*
|
||||||
|
* RINIT uses it to decide whether to rewrite; MINFO uses it to REPORT whether
|
||||||
|
* rewriting is live. Those two tests were written out longhand in two places
|
||||||
|
* and promptly drifted: the absolute-path guard was added to RINIT only, so
|
||||||
|
* `lsphp -i` went on printing "Rewriting => active" for a mapping RINIT had
|
||||||
|
* already decided to ignore. That row is the fleet canary's signal, so the lie
|
||||||
|
* masked precisely the failure the canary looks for. Keep them sharing this.
|
||||||
|
*
|
||||||
|
* Pure predicates over two NUL-terminated strings: no allocation, no side
|
||||||
|
* effect, no way to fail — MINFO gains no error path by calling them, and the
|
||||||
|
* fail-open invariant is untouched.
|
||||||
|
*/
|
||||||
|
static int cacpp_mapping_configured(const char *from, const char *to)
|
||||||
|
{
|
||||||
|
return from != NULL && *from != '\0' && to != NULL && *to != '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
static int cacpp_mapping_active(const char *from, const char *to)
|
||||||
|
{
|
||||||
|
return cacpp_mapping_configured(from, to) && *from == '/' && *to == '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Trailing slashes would defeat the component-boundary test below. */
|
||||||
|
static size_t cacpp_trim(const char *s, size_t len)
|
||||||
|
{
|
||||||
|
while (len > 1 && s[len - 1] == '/') {
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void cacpp_rewrite_key(zval *server, const char *key, size_t key_len,
|
||||||
|
const char *from, size_t from_len,
|
||||||
|
const char *to, size_t to_len)
|
||||||
|
{
|
||||||
|
zval *val = zend_hash_str_find(Z_ARRVAL_P(server), key, key_len);
|
||||||
|
if (val == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ZVAL_DEREF(val);
|
||||||
|
if (Z_TYPE_P(val) != IS_STRING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *s = Z_STRVAL_P(val);
|
||||||
|
size_t len = Z_STRLEN_P(val);
|
||||||
|
|
||||||
|
if (len < from_len || memcmp(s, from, from_len) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
* Only replace on a PATH-COMPONENT boundary. Without this,
|
||||||
|
* from=/mnt/users/bob/site.com would also match a sibling directory
|
||||||
|
* /mnt/users/bob/site.com.bak and silently rewrite another site's path
|
||||||
|
* into this site's /home.
|
||||||
|
*/
|
||||||
|
if (len != from_len && s[from_len] != '/') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t tail_len = len - from_len;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* to="/" is the one absolute prefix that survives cacpp_trim() as a bare
|
||||||
|
* separator, and the tail always starts with one — splicing both would give
|
||||||
|
* "//public_html". Drop it when there IS a tail; keep it when there is not
|
||||||
|
* (value == from exactly, where "/" is the correct answer). Unreachable from
|
||||||
|
* the entrypoint, which always writes to=/home/<user>.
|
||||||
|
*/
|
||||||
|
size_t eff_to_len = to_len;
|
||||||
|
if (tail_len > 0 && eff_to_len == 1 && to[0] == '/') {
|
||||||
|
eff_to_len = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
zend_string *out = zend_string_alloc(eff_to_len + tail_len, 0);
|
||||||
|
|
||||||
|
memcpy(ZSTR_VAL(out), to, eff_to_len);
|
||||||
|
memcpy(ZSTR_VAL(out) + eff_to_len, s + from_len, tail_len);
|
||||||
|
ZSTR_VAL(out)[eff_to_len + tail_len] = '\0';
|
||||||
|
|
||||||
|
zval nv;
|
||||||
|
ZVAL_STR(&nv, out);
|
||||||
|
/*
|
||||||
|
* Update the track_vars array IN PLACE. $_SERVER in the symbol table and
|
||||||
|
* PG(http_globals)[TRACK_VARS_SERVER] are two references to the SAME
|
||||||
|
* zend_array, which is why this is visible to userland. Do NOT
|
||||||
|
* SEPARATE_ARRAY() here: that would copy the array and leave the symbol
|
||||||
|
* table pointing at the original, i.e. silently do nothing. This is the
|
||||||
|
* same in-place pattern php_register_variable_ex() uses.
|
||||||
|
*/
|
||||||
|
zend_hash_str_update(Z_ARRVAL_P(server), key, key_len, &nv);
|
||||||
|
}
|
||||||
|
|
||||||
|
PHP_RINIT_FUNCTION(cac_path_parity)
|
||||||
|
{
|
||||||
|
#if defined(ZTS) && defined(COMPILE_DL_CAC_PATH_PARITY)
|
||||||
|
ZEND_TSRMLS_CACHE_UPDATE();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const char *from = CACPP_G(from);
|
||||||
|
const char *to = CACPP_G(to);
|
||||||
|
|
||||||
|
/* Unconfigured (any tier that isn't shared-ols) => completely inert. */
|
||||||
|
if (!cacpp_mapping_configured(from, to)) {
|
||||||
|
return SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Both sides must be ABSOLUTE. The entrypoint only ever writes absolute
|
||||||
|
* paths; a relative prefix would be a typo or a mangled ini, and matching it
|
||||||
|
* against a SAPI-supplied path could only ever produce nonsense. Treat it
|
||||||
|
* like an absent mapping — inert, no diagnostic, request proceeds.
|
||||||
|
*/
|
||||||
|
if (!cacpp_mapping_active(from, to)) {
|
||||||
|
return SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* With auto_globals_jit=On (the default) $_SERVER is not built yet at
|
||||||
|
* RINIT — php_hash_environment() only MARKED it for lazy creation. Reading
|
||||||
|
* PG(http_globals)[TRACK_VARS_SERVER] here without this call finds IS_UNDEF
|
||||||
|
* and the extension silently does nothing, which is precisely the failure
|
||||||
|
* the auto_prepend approach had. Forcing the auto-global now builds it (via
|
||||||
|
* the SAPI's register_server_variables) so there is something to rewrite,
|
||||||
|
* and the later userland access gets the corrected array.
|
||||||
|
*/
|
||||||
|
zend_is_auto_global_str(ZEND_STRL("_SERVER"));
|
||||||
|
|
||||||
|
zval *server = &PG(http_globals)[TRACK_VARS_SERVER];
|
||||||
|
if (Z_TYPE_P(server) != IS_ARRAY) {
|
||||||
|
return SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t from_len = cacpp_trim(from, strlen(from));
|
||||||
|
size_t to_len = cacpp_trim(to, strlen(to));
|
||||||
|
|
||||||
|
for (size_t i = 0; i < sizeof(cacpp_keys) / sizeof(cacpp_keys[0]); i++) {
|
||||||
|
cacpp_rewrite_key(server, cacpp_keys[i].name, cacpp_keys[i].len,
|
||||||
|
from, from_len, to, to_len);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
PHP_MINIT_FUNCTION(cac_path_parity)
|
||||||
|
{
|
||||||
|
REGISTER_INI_ENTRIES();
|
||||||
|
return SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
PHP_MSHUTDOWN_FUNCTION(cac_path_parity)
|
||||||
|
{
|
||||||
|
UNREGISTER_INI_ENTRIES();
|
||||||
|
return SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
PHP_MINFO_FUNCTION(cac_path_parity)
|
||||||
|
{
|
||||||
|
const char *from = CACPP_G(from);
|
||||||
|
const char *to = CACPP_G(to);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Report what RINIT would ACTUALLY do, by asking the same predicates RINIT
|
||||||
|
* asks — never a longhand copy of them (see cacpp_mapping_active above for
|
||||||
|
* what that cost last time). Three distinct answers, because "configured but
|
||||||
|
* ignored" is a different operational problem from "not configured" and the
|
||||||
|
* canary must be able to tell them apart.
|
||||||
|
*/
|
||||||
|
const char *state;
|
||||||
|
if (cacpp_mapping_active(from, to)) {
|
||||||
|
state = "active";
|
||||||
|
} else if (cacpp_mapping_configured(from, to)) {
|
||||||
|
state = "inactive (mapping not absolute)";
|
||||||
|
} else {
|
||||||
|
state = "inactive (unconfigured)";
|
||||||
|
}
|
||||||
|
|
||||||
|
php_info_print_table_start();
|
||||||
|
php_info_print_table_header(2, "cac_path_parity support", "enabled");
|
||||||
|
php_info_print_table_row(2, "Version", PHP_CAC_PATH_PARITY_VERSION);
|
||||||
|
/*
|
||||||
|
* The canary greps for this row: "active" proves the mapping is live — and,
|
||||||
|
* since the predicate is shared with RINIT, proves the request path agrees.
|
||||||
|
*/
|
||||||
|
php_info_print_table_row(2, "Rewriting", state);
|
||||||
|
php_info_print_table_end();
|
||||||
|
|
||||||
|
DISPLAY_INI_ENTRIES();
|
||||||
|
}
|
||||||
|
|
||||||
|
zend_module_entry cac_path_parity_module_entry = {
|
||||||
|
STANDARD_MODULE_HEADER,
|
||||||
|
"cac_path_parity",
|
||||||
|
NULL, /* no userland functions — by design */
|
||||||
|
PHP_MINIT(cac_path_parity),
|
||||||
|
PHP_MSHUTDOWN(cac_path_parity),
|
||||||
|
PHP_RINIT(cac_path_parity),
|
||||||
|
NULL, /* RSHUTDOWN */
|
||||||
|
PHP_MINFO(cac_path_parity),
|
||||||
|
PHP_CAC_PATH_PARITY_VERSION,
|
||||||
|
PHP_MODULE_GLOBALS(cac_path_parity),
|
||||||
|
PHP_GINIT(cac_path_parity),
|
||||||
|
NULL, /* GSHUTDOWN */
|
||||||
|
NULL, /* post-deactivate */
|
||||||
|
STANDARD_MODULE_PROPERTIES_EX
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef COMPILE_DL_CAC_PATH_PARITY
|
||||||
|
#if defined(ZTS)
|
||||||
|
ZEND_TSRMLS_CACHE_DEFINE()
|
||||||
|
#endif
|
||||||
|
ZEND_GET_MODULE(cac_path_parity)
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
dnl config.m4 for the cac_path_parity extension.
|
||||||
|
dnl Built out-of-tree against the image's own lsphp:
|
||||||
|
dnl phpize && ./configure --with-php-config=/usr/local/lsws/lsphpNN/bin/php-config
|
||||||
|
dnl No external libraries, no optional features — pure core API.
|
||||||
|
|
||||||
|
PHP_ARG_ENABLE([cac_path_parity],
|
||||||
|
[whether to enable cac_path_parity support],
|
||||||
|
[AS_HELP_STRING([--enable-cac-path-parity],
|
||||||
|
[Enable cac_path_parity ($_SERVER path parity for the shared-ols tier)])],
|
||||||
|
[no])
|
||||||
|
|
||||||
|
if test "$PHP_CAC_PATH_PARITY" != "no"; then
|
||||||
|
AC_DEFINE(HAVE_CAC_PATH_PARITY, 1, [Have cac_path_parity support])
|
||||||
|
PHP_NEW_EXTENSION(cac_path_parity, cac_path_parity.c, $ext_shared)
|
||||||
|
fi
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* cac_path_parity — $_SERVER filesystem-path parity for the shared-ols tier.
|
||||||
|
*
|
||||||
|
* See cac_path_parity.c for the full rationale.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PHP_CAC_PATH_PARITY_H
|
||||||
|
#define PHP_CAC_PATH_PARITY_H
|
||||||
|
|
||||||
|
extern zend_module_entry cac_path_parity_module_entry;
|
||||||
|
#define phpext_cac_path_parity_ptr &cac_path_parity_module_entry
|
||||||
|
|
||||||
|
#define PHP_CAC_PATH_PARITY_VERSION "1.0.0"
|
||||||
|
|
||||||
|
#if defined(ZTS) && defined(COMPILE_DL_CAC_PATH_PARITY)
|
||||||
|
ZEND_TSRMLS_CACHE_EXTERN()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
ZEND_BEGIN_MODULE_GLOBALS(cac_path_parity)
|
||||||
|
char *from;
|
||||||
|
char *to;
|
||||||
|
ZEND_END_MODULE_GLOBALS(cac_path_parity)
|
||||||
|
|
||||||
|
#endif /* PHP_CAC_PATH_PARITY_H */
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: rewrites the configured prefix on a filesystem $_SERVER key
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
cac_path_parity.from=/mnt/users/bob/site.com
|
||||||
|
cac_path_parity.to=/home/bob
|
||||||
|
variables_order=EGPCS
|
||||||
|
--ENV--
|
||||||
|
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||||
|
HTTP_HOST=site.com
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// NOTE: the CLI SAPI overwrites DOCUMENT_ROOT (to "") and SCRIPT_FILENAME /
|
||||||
|
// PATH_TRANSLATED (to the script path) AFTER the env import, so those three
|
||||||
|
// cannot be driven from --ENV-- here. They go through the identical code path
|
||||||
|
// as CONTEXT_DOCUMENT_ROOT (one loop over one key table); the real web-SAPI
|
||||||
|
// proof for them is tests/fpm-parity-check.sh.
|
||||||
|
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||||
|
// Non-path vars must be untouched.
|
||||||
|
var_dump($_SERVER['HTTP_HOST']);
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
string(21) "/home/bob/public_html"
|
||||||
|
string(8) "site.com"
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: a sibling dir sharing the prefix is NOT rewritten
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
cac_path_parity.from=/mnt/users/bob/site.com
|
||||||
|
cac_path_parity.to=/home/bob
|
||||||
|
variables_order=EGPCS
|
||||||
|
--ENV--
|
||||||
|
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com.bak/public_html
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// Replacement happens only on a path-COMPONENT boundary. Without that guard a
|
||||||
|
// neighbouring directory would be folded into this container's /home.
|
||||||
|
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
string(39) "/mnt/users/bob/site.com.bak/public_html"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: an exact prefix match (no trailing component) is rewritten
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
cac_path_parity.from=/mnt/users/bob/site.com/
|
||||||
|
cac_path_parity.to=/home/bob/
|
||||||
|
variables_order=EGPCS
|
||||||
|
--ENV--
|
||||||
|
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// Also covers trailing slashes in the configured values being tolerated.
|
||||||
|
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
string(9) "/home/bob"
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: completely inert when no mapping is configured
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
variables_order=EGPCS
|
||||||
|
--ENV--
|
||||||
|
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// cac-fpm / cac-litespeed never configure a mapping, so the extension must be
|
||||||
|
// a no-op there. This is the no-regression guarantee for the other tiers.
|
||||||
|
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
string(35) "/mnt/users/bob/site.com/public_html"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: works with auto_globals_jit=On (lazy $_SERVER, the default)
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
auto_globals_jit=1
|
||||||
|
cac_path_parity.from=/mnt/users/bob/site.com
|
||||||
|
cac_path_parity.to=/home/bob
|
||||||
|
variables_order=EGPCS
|
||||||
|
--ENV--
|
||||||
|
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// With auto_globals_jit=On, $_SERVER does not exist yet when extension RINIT
|
||||||
|
// runs. The extension forces the auto-global so there is something to rewrite;
|
||||||
|
// drop that call and this test prints the /mnt/users path.
|
||||||
|
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
string(21) "/home/bob/public_html"
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: mapping is PHP_INI_SYSTEM — userland cannot change it
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
cac_path_parity.from=/mnt/users/bob/site.com
|
||||||
|
cac_path_parity.to=/home/bob
|
||||||
|
variables_order=EGPCS
|
||||||
|
--ENV--
|
||||||
|
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// PHP_INI_SYSTEM entries are not modifiable at runtime, and .user.ini honours
|
||||||
|
// only PHP_INI_PERDIR|PHP_INI_USER — so a customer cannot redirect or disable
|
||||||
|
// the rewrite the way they can displace an auto_prepend_file.
|
||||||
|
var_dump(ini_set('cac_path_parity.from', '/tmp'));
|
||||||
|
var_dump(ini_set('cac_path_parity.to', '/tmp'));
|
||||||
|
var_dump(ini_get('cac_path_parity.from'));
|
||||||
|
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
bool(false)
|
||||||
|
bool(false)
|
||||||
|
string(23) "/mnt/users/bob/site.com"
|
||||||
|
string(21) "/home/bob/public_html"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: a non-absolute mapping is inert, not applied
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
cac_path_parity.from=mnt/users/bob/site.com
|
||||||
|
cac_path_parity.to=/home/bob
|
||||||
|
variables_order=EGPCS
|
||||||
|
--ENV--
|
||||||
|
CONTEXT_DOCUMENT_ROOT=mnt/users/bob/site.com/public_html
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// The value here is deliberately relative TOO, so the prefix would match and be
|
||||||
|
// rewritten if the absolute-path guard in RINIT were removed. Nothing the
|
||||||
|
// entrypoint writes looks like this; the guard exists so a mangled ini degrades
|
||||||
|
// to "inert" rather than to "confidently wrong".
|
||||||
|
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
string(34) "mnt/users/bob/site.com/public_html"
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: to=/ does not produce a doubled separator
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
cac_path_parity.from=/mnt/users/bob/site.com
|
||||||
|
cac_path_parity.to=/
|
||||||
|
variables_order=EGPCS
|
||||||
|
--ENV--
|
||||||
|
CONTEXT_DOCUMENT_ROOT=/mnt/users/bob/site.com/public_html
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// Degenerate mapping, unreachable from entrypoint-lsphp.sh (which always writes
|
||||||
|
// to=/home/<user>). Before the eff_to_len collapse this returned
|
||||||
|
// "//public_html". A path with a doubled leading slash is not the same string as
|
||||||
|
// the cac-fpm value, which is the entire point of this extension.
|
||||||
|
var_dump($_SERVER['CONTEXT_DOCUMENT_ROOT']);
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
string(12) "/public_html"
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: MINFO reports a non-absolute mapping as INACTIVE, not active
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
cac_path_parity.from=mnt/users/bob/site.com
|
||||||
|
cac_path_parity.to=/home/bob
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// `lsphp -i | grep Rewriting` is the signal the post-deploy fleet canary uses to
|
||||||
|
// confirm parity is live on a host. When the absolute-path guard was added to
|
||||||
|
// RINIT, MINFO was left testing only "both values non-empty" — so this exact
|
||||||
|
// mapping (relative `from`, silently INERT since 007) still printed
|
||||||
|
// "Rewriting => active". A canary that reports healthy for a dead mapping hides
|
||||||
|
// precisely the failure it was deployed to find.
|
||||||
|
//
|
||||||
|
// MINFO and RINIT now share cacpp_mapping_active(); revert MINFO to the
|
||||||
|
// non-empty test and this prints "active".
|
||||||
|
ob_start();
|
||||||
|
phpinfo(INFO_MODULES);
|
||||||
|
$info = ob_get_clean();
|
||||||
|
|
||||||
|
// Also assert the row is unique, so the match below cannot be some other
|
||||||
|
// module's identically-named row.
|
||||||
|
var_dump(preg_match_all('/^Rewriting => (.+)$/m', $info, $m));
|
||||||
|
var_dump(rtrim($m[1][0]));
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
int(1)
|
||||||
|
string(31) "inactive (mapping not absolute)"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
--TEST--
|
||||||
|
cac_path_parity: MINFO reports a well-formed mapping as ACTIVE
|
||||||
|
--EXTENSIONS--
|
||||||
|
cac_path_parity
|
||||||
|
--INI--
|
||||||
|
cac_path_parity.from=/mnt/users/bob/site.com
|
||||||
|
cac_path_parity.to=/home/bob
|
||||||
|
--FILE--
|
||||||
|
<?php
|
||||||
|
// The other half of 009. Tightening MINFO must not overshoot into the opposite
|
||||||
|
// lie: a canary that reports "inactive" on a perfectly good mapping would page
|
||||||
|
// the fleet for nothing and, worse, train us to ignore the row. This mapping is
|
||||||
|
// the exact shape entrypoint-lsphp.sh writes (/mnt/users/<user>/<domain> ->
|
||||||
|
// /home/<user>), and 001 proves RINIT really does rewrite under it.
|
||||||
|
ob_start();
|
||||||
|
phpinfo(INFO_MODULES);
|
||||||
|
$info = ob_get_clean();
|
||||||
|
|
||||||
|
var_dump(preg_match_all('/^Rewriting => (.+)$/m', $info, $m));
|
||||||
|
var_dump(rtrim($m[1][0]));
|
||||||
|
?>
|
||||||
|
--EXPECT--
|
||||||
|
int(1)
|
||||||
|
string(6) "active"
|
||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
## fpm-parity-check.sh — end-to-end proof under a REAL web SAPI.
|
||||||
|
##
|
||||||
|
## WHY NOT .phpt: the CLI SAPI overwrites DOCUMENT_ROOT / SCRIPT_FILENAME /
|
||||||
|
## PATH_TRANSLATED after importing the environment, and the cli-server SAPI does
|
||||||
|
## not process .user.ini at all — so neither can exercise the two things that
|
||||||
|
## actually matter here.
|
||||||
|
##
|
||||||
|
## WHY PHP-FPM: php-fpm takes DOCUMENT_ROOT and SCRIPT_FILENAME as caller-
|
||||||
|
## supplied FastCGI params and honours .user.ini — structurally the same shape as
|
||||||
|
## OpenLiteSpeed handing a detached lsphp its LSAPI params. It is the closest
|
||||||
|
## analogue available without an OLS runtime.
|
||||||
|
##
|
||||||
|
## Asserts:
|
||||||
|
## 1. CONTROL — no mapping => PHP reports the raw /mnt/users paths, i.e. the
|
||||||
|
## test reproduces the bug before claiming to fix it.
|
||||||
|
## 2. FIX — mapping => both keys read /home/<user>/... .
|
||||||
|
## 3. WORDFENCE — mapping AND a customer .user.ini auto_prepend_file (the state
|
||||||
|
## 7 live shared_ols sites are in): paths are STILL corrected
|
||||||
|
## AND the customer's prepend STILL runs. This is the case the
|
||||||
|
## old auto_prepend_file normaliser silently lost.
|
||||||
|
## 4. OLD — for the record: the previous auto_prepend mechanism, with the
|
||||||
|
## same customer .user.ini, does NOT run. This is the evidence
|
||||||
|
## that hardening the prepend hook could not have worked.
|
||||||
|
##
|
||||||
|
## Exit codes: 0 = all assertions passed, 1 = an assertion FAILED, 2 = the
|
||||||
|
## harness could not run (missing binary, php-fpm refused to start, .so would not
|
||||||
|
## load). 2 is deliberately distinct from 1: a startup problem previously
|
||||||
|
## surfaced as all nine assertions failing with an empty `got:`, which reads like
|
||||||
|
## nine parity bugs and is the opposite of the truth.
|
||||||
|
##
|
||||||
|
## Usage: ./fpm-parity-check.sh [ROOT] [PHP_FPM_BIN] [EXT_SO]
|
||||||
|
## ROOT defaults to /mnt/users (falls back to a temp dir if not creatable).
|
||||||
|
## PHP_FPM_BIN is auto-detected; every packaging of php-fpm this repo touches
|
||||||
|
## uses a different name (`php-fpm` in the official docker images,
|
||||||
|
## `php-fpm8.N` on Debian/Ubuntu, /usr/sbin/... unlinked from PATH), so a
|
||||||
|
## single hardcoded default is guaranteed to be wrong somewhere and its only
|
||||||
|
## symptom was a silent `SKIP`.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
find_fpm() {
|
||||||
|
local c
|
||||||
|
for c in php-fpm php-fpm8.5 php-fpm8.4 php-fpm8.3 php-fpm8.2 php-fpm8.1; do
|
||||||
|
if command -v "$c" >/dev/null 2>&1; then command -v "$c"; return 0; fi
|
||||||
|
done
|
||||||
|
for c in /usr/local/sbin/php-fpm /usr/sbin/php-fpm /usr/sbin/php-fpm8.*; do
|
||||||
|
if [ -x "$c" ]; then echo "$c"; return 0; fi
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
ROOT="${1:-/mnt/users}"
|
||||||
|
FPM_BIN="${2:-$(find_fpm || true)}"
|
||||||
|
EXT_SO="${3:-$HERE/../modules/cac_path_parity.so}"
|
||||||
|
PORT="${PORT:-9001}"
|
||||||
|
|
||||||
|
command -v cgi-fcgi >/dev/null || { echo "SKIP: cgi-fcgi not installed (apt install libfcgi-bin)"; exit 0; }
|
||||||
|
[ -n "$FPM_BIN" ] && [ -x "$FPM_BIN" ] || { echo "SKIP: php-fpm not found (pass it as \$2)"; exit 0; }
|
||||||
|
[ -f "$EXT_SO" ] || { echo "SKIP: $EXT_SO not built (run phpize && ./configure && make)"; exit 0; }
|
||||||
|
|
||||||
|
## `${VAR%%$'\n'*}` rather than `| head -1`: same first line, no pipeline, so
|
||||||
|
## nothing here can be decided by a SIGPIPE race under the pipefail on line 39.
|
||||||
|
## This one only ever fed an echo, so it could not have misled anyone — it is
|
||||||
|
## changed so that "no pipefail script in this repo pipes into an early-exit
|
||||||
|
## reader" stays a rule with no exceptions to remember.
|
||||||
|
FPM_VERSION=$("$FPM_BIN" -n -v 2>/dev/null || true)
|
||||||
|
echo "php-fpm: $FPM_BIN (${FPM_VERSION%%$'\n'*})"
|
||||||
|
echo "extension: $EXT_SO"
|
||||||
|
|
||||||
|
## Pre-flight. If the .so will not load into THIS php-fpm (PHP API mismatch is
|
||||||
|
## the usual cause) every assertion below would fail identically and blame the
|
||||||
|
## extension's logic. Say what actually happened instead.
|
||||||
|
## Captured into a variable and matched with a here-string, not piped into
|
||||||
|
## `grep -qx`. `grep -q` exits on its first match, and with `set -o pipefail`
|
||||||
|
## (line 39) a writer still writing at that moment dies 141 and the pipeline
|
||||||
|
## reads FALSE — announcing "cannot load the extension" *because* the extension
|
||||||
|
## was listed. The reason to change it is structural, not that `php-fpm -m` is
|
||||||
|
## small: there is no payload size that makes this shape safe (41 KB SIGPIPEs
|
||||||
|
## about 11% of the time into a 64 KB pipe — see the note over the probe helpers
|
||||||
|
## in scripts/entrypoint-lsphp.sh), and this pre-flight exists precisely to stop
|
||||||
|
## a harness malfunction being reported as an extension fault, so it must not
|
||||||
|
## have one of its own. (The same construct on 40 KB of `lsphp -i` is what broke
|
||||||
|
## entrypoint-lsphp.sh in production.)
|
||||||
|
##
|
||||||
|
## A here-string, not the `[[ ]]` form those helpers use, on purpose: `<<<`
|
||||||
|
## spills to /tmp/sh-thd.XXXXXX above ~4-64 KB depending on the bash build, so
|
||||||
|
## it is a writable-temp-dir precondition, which is unacceptable on a boot path
|
||||||
|
## and irrelevant here — `php-fpm -m` is ~1 KB, and this harness has already
|
||||||
|
## created a docroot and a pool config by the time it runs.
|
||||||
|
FPM_MODULES=$("$FPM_BIN" -n -d "extension=$EXT_SO" -m 2>/dev/null || true)
|
||||||
|
if ! grep -qx 'cac_path_parity' <<<"$FPM_MODULES"; then
|
||||||
|
echo "HARNESS FAILURE: $FPM_BIN cannot load $EXT_SO" >&2
|
||||||
|
"$FPM_BIN" -n -d "extension=$EXT_SO" -m 2>&1 | grep -i 'unable\|warning\|error' >&2
|
||||||
|
echo " The .so must be built against the same PHP as this php-fpm binary." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$ROOT" 2>/dev/null || ROOT="$(mktemp -d)/mnt/users"
|
||||||
|
USER_NAME=bob
|
||||||
|
SITE="$ROOT/$USER_NAME/site.com"
|
||||||
|
DOCROOT="$SITE/public_html"
|
||||||
|
HOME_PATH="/home/$USER_NAME"
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
## php-fpm REFUSES to start as root unless the pool names a non-root user/group,
|
||||||
|
## and the pool this script generates had neither — so as shipped it never got
|
||||||
|
## past startup in any root context (which is every container in this repo).
|
||||||
|
## Resolve a real unprivileged account rather than assuming www-data exists.
|
||||||
|
POOL_USER=""
|
||||||
|
POOL_GROUP=""
|
||||||
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
|
for u in www-data nobody daemon; do
|
||||||
|
if id -u "$u" >/dev/null 2>&1; then POOL_USER="$u"; break; fi
|
||||||
|
done
|
||||||
|
for g in www-data nogroup nobody daemon; do
|
||||||
|
if getent group "$g" >/dev/null 2>&1; then POOL_GROUP="$g"; break; fi
|
||||||
|
done
|
||||||
|
[ -n "$POOL_USER" ] && [ -n "$POOL_GROUP" ] || {
|
||||||
|
echo "HARNESS FAILURE: running as root but found no unprivileged user/group for the pool" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$DOCROOT" || { echo "cannot create $DOCROOT"; exit 1; }
|
||||||
|
## The pool worker is not root: it has to be able to read the fixtures under
|
||||||
|
## $TMP (mktemp -d is 0700) and walk down to $DOCROOT.
|
||||||
|
chmod 755 "$TMP"
|
||||||
|
trap 'rm -rf "$TMP"; rm -f "$DOCROOT/.user.ini"' EXIT
|
||||||
|
|
||||||
|
cat > "$DOCROOT/probe.php" <<'PHP'
|
||||||
|
<?php
|
||||||
|
echo "DOCUMENT_ROOT=" . $_SERVER['DOCUMENT_ROOT'] . "\n";
|
||||||
|
echo "SCRIPT_FILENAME=" . $_SERVER['SCRIPT_FILENAME'] . "\n";
|
||||||
|
echo "PREPEND_RAN=" . (defined('CUSTOMER_PREPEND_RAN') ? 'yes' : 'no') . "\n";
|
||||||
|
PHP
|
||||||
|
|
||||||
|
## Stand-in for the customer's wordfence-waf.php.
|
||||||
|
cat > "$SITE/customer-waf.php" <<'PHP'
|
||||||
|
<?php
|
||||||
|
define('CUSTOMER_PREPEND_RAN', 1);
|
||||||
|
PHP
|
||||||
|
|
||||||
|
## Stand-in for the OLD mechanism (scripts/cac-lsphp-normalize.php).
|
||||||
|
cat > "$TMP/old-normalize.php" <<'PHP'
|
||||||
|
<?php
|
||||||
|
foreach (array('DOCUMENT_ROOT', 'SCRIPT_FILENAME') as $k) {
|
||||||
|
if (!empty($_SERVER[$k]) && strncmp($_SERVER[$k], '/mnt/users/', 11) === 0) {
|
||||||
|
$r = realpath($_SERVER[$k]);
|
||||||
|
if ($r !== false) { $_SERVER[$k] = $r; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PHP
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "[global]"
|
||||||
|
echo "error_log = $TMP/fpm-error.log"
|
||||||
|
echo "daemonize = no"
|
||||||
|
echo "[www]"
|
||||||
|
echo "listen = 127.0.0.1:$PORT"
|
||||||
|
echo "pm = static"
|
||||||
|
echo "pm.max_children = 2"
|
||||||
|
## Only when we are root: php-fpm hard-errors on a root pool, and warns
|
||||||
|
## (harmlessly, but noisily) if a non-root master names a user at all.
|
||||||
|
if [ -n "$POOL_USER" ]; then
|
||||||
|
echo "user = $POOL_USER"
|
||||||
|
echo "group = $POOL_GROUP"
|
||||||
|
fi
|
||||||
|
} > "$TMP/fpm.conf"
|
||||||
|
|
||||||
|
## Returns non-zero when php-fpm never answered. Callers MUST distinguish that
|
||||||
|
## from an assertion failure — an unstarted php-fpm makes every expect() below
|
||||||
|
## fail with an empty `got:`, which looks like nine parity bugs.
|
||||||
|
run_case() {
|
||||||
|
: > "$TMP/fpm.out"
|
||||||
|
"$FPM_BIN" -n -y "$TMP/fpm.conf" -F -d user_ini.cache_ttl=0 "$@" \
|
||||||
|
>"$TMP/fpm.out" 2>&1 &
|
||||||
|
local pid=$! out=""
|
||||||
|
for _ in $(seq 1 40); do
|
||||||
|
sleep 0.15
|
||||||
|
## SC1007: `QUERY_STRING=` IS the intent — an empty FastCGI param in the
|
||||||
|
## per-command environment prefix, exactly as a webserver sends it for a
|
||||||
|
## URL with no query string. Not a truncated assignment.
|
||||||
|
# shellcheck disable=SC1007
|
||||||
|
out=$(SCRIPT_FILENAME="$DOCROOT/probe.php" DOCUMENT_ROOT="$DOCROOT" \
|
||||||
|
SCRIPT_NAME=/probe.php REQUEST_METHOD=GET QUERY_STRING= \
|
||||||
|
cgi-fcgi -bind -connect "127.0.0.1:$PORT" 2>/dev/null)
|
||||||
|
[ -n "$out" ] && break
|
||||||
|
## Master already gone => it will never answer; stop waiting 6s for it.
|
||||||
|
kill -0 "$pid" 2>/dev/null || break
|
||||||
|
done
|
||||||
|
kill "$pid" 2>/dev/null; wait "$pid" 2>/dev/null
|
||||||
|
printf '%s' "$out"
|
||||||
|
[ -n "$out" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
die_startup() {
|
||||||
|
echo
|
||||||
|
echo "HARNESS FAILURE: php-fpm never answered for case '$1'." >&2
|
||||||
|
echo " This is a STARTUP/environment failure, NOT a parity assertion failure." >&2
|
||||||
|
echo " php-fpm: $FPM_BIN" >&2
|
||||||
|
echo " pool user/group: ${POOL_USER:-<none, master is not root>}/${POOL_GROUP:-}" >&2
|
||||||
|
echo " --- php-fpm output ---" >&2
|
||||||
|
sed 's/^/ /' "$TMP/fpm.out" >&2
|
||||||
|
echo " --- pool error_log ---" >&2
|
||||||
|
[ -s "$TMP/fpm-error.log" ] && sed 's/^/ /' "$TMP/fpm-error.log" >&2
|
||||||
|
echo " ----------------------" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
expect() {
|
||||||
|
local label="$1" got="$2" want="$3"
|
||||||
|
if [ "$got" = "$want" ]; then
|
||||||
|
echo " PASS $label"
|
||||||
|
else
|
||||||
|
echo " FAIL $label"
|
||||||
|
echo " want: $want"
|
||||||
|
echo " got: $got"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
field() { printf '%s' "$1" | sed -n "s/^$2=//p"; }
|
||||||
|
|
||||||
|
EXT=( -d "extension=$EXT_SO" )
|
||||||
|
MAP=( -d "cac_path_parity.from=$SITE" -d "cac_path_parity.to=$HOME_PATH" )
|
||||||
|
USERINI_LINE="auto_prepend_file = $SITE/customer-waf.php"
|
||||||
|
|
||||||
|
echo "== 1. CONTROL: extension loaded, no mapping (reproduces the bug) =="
|
||||||
|
rm -f "$DOCROOT/.user.ini"
|
||||||
|
out=$(run_case "${EXT[@]}") || die_startup "1. CONTROL"
|
||||||
|
expect "DOCUMENT_ROOT is the raw OLS path" "$(field "$out" DOCUMENT_ROOT)" "$DOCROOT"
|
||||||
|
expect "SCRIPT_FILENAME is the raw OLS path" "$(field "$out" SCRIPT_FILENAME)" "$DOCROOT/probe.php"
|
||||||
|
|
||||||
|
echo "== 2. FIX: mapping configured =="
|
||||||
|
out=$(run_case "${EXT[@]}" "${MAP[@]}") || die_startup "2. FIX"
|
||||||
|
expect "DOCUMENT_ROOT == cac-fpm value" "$(field "$out" DOCUMENT_ROOT)" "$HOME_PATH/public_html"
|
||||||
|
expect "SCRIPT_FILENAME == cac-fpm value" "$(field "$out" SCRIPT_FILENAME)" "$HOME_PATH/public_html/probe.php"
|
||||||
|
|
||||||
|
echo "== 3. WORDFENCE: customer .user.ini auto_prepend_file present =="
|
||||||
|
printf '%s\n' "$USERINI_LINE" > "$DOCROOT/.user.ini"
|
||||||
|
out=$(run_case "${EXT[@]}" "${MAP[@]}") || die_startup "3. WORDFENCE"
|
||||||
|
expect "DOCUMENT_ROOT still corrected" "$(field "$out" DOCUMENT_ROOT)" "$HOME_PATH/public_html"
|
||||||
|
expect "SCRIPT_FILENAME still corrected" "$(field "$out" SCRIPT_FILENAME)" "$HOME_PATH/public_html/probe.php"
|
||||||
|
expect "customer auto_prepend_file still ran" "$(field "$out" PREPEND_RAN)" "yes"
|
||||||
|
|
||||||
|
echo "== 4. OLD MECHANISM (why the prepend hook could not be hardened) =="
|
||||||
|
out=$(run_case -d "auto_prepend_file=$TMP/old-normalize.php") || die_startup "4. OLD MECHANISM"
|
||||||
|
expect "auto_prepend normaliser is displaced by the customer's .user.ini" \
|
||||||
|
"$(field "$out" DOCUMENT_ROOT)" "$DOCROOT"
|
||||||
|
expect "customer's prepend is the one that ran" "$(field "$out" PREPEND_RAN)" "yes"
|
||||||
|
|
||||||
|
rm -f "$DOCROOT/.user.ini"
|
||||||
|
if [ "$fail" -eq 0 ]; then echo "ALL PASS"; else echo "FAILURES"; fi
|
||||||
|
exit "$fail"
|
||||||
@@ -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.
|
||||||
|
# <url> <sha256>
|
||||||
|
https://www.litespeedtech.com/packages/lsapi/php-litespeed-8.3.tgz cd616b82cc853e5e3187fccd060228b9cd1a2009ce72d8da5950eef950ac29b1
|
||||||
@@ -0,0 +1,804 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
=SWGM
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# PHP release-manager signing keys, per branch: <branch> <PRIMARY fingerprint> <who>
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
Trust material for the OPT-IN internal package cache (scripts/pkg-cache-repos.sh,
|
||||||
|
docs/pkg-cache.md). Copied into images at /usr/local/share/pkg-cache/; nothing
|
||||||
|
here is added to the system trust store.
|
||||||
|
|
||||||
|
ca.crt "pkg-cache internal CA (CT130)", EC P-256, valid to 2036-10-18.
|
||||||
|
NAME-CONSTRAINED to IP 192.168.1.40 and DNS
|
||||||
|
pkg-cache.internal.cloud-hosting.io / pkg-cache.geekhome.local.
|
||||||
|
SHA-256 fingerprint BB:5B:9A:08:05:ED:2F:58:83:92:D0:99:9D:11:0A:30:14:88:68:F0:F0:88:65:5E:94:0B:75:EB:C2:A2:BD:F3
|
||||||
|
(private key lives only on CT130: /etc/pkgcache-ca/ca.key)
|
||||||
|
RPM-GPG-KEY-EPEL-10 Fedora (epel10) 7D8D 15CB FC4E 6268 8591 FB26 33D9 8517 E37E D158
|
||||||
|
fetched 2026-10-21 over HTTPS from dl.fedoraproject.org and
|
||||||
|
mirror.math.princeton.edu (identical); fingerprint matches
|
||||||
|
https://fedoraproject.org/security/
|
||||||
|
RPM-GPG-KEY-remi2024 Remi's RPM repository 2024 CF1D F005 7CE8 5DFF 5B2F 2A37 C2FD 3B2C 2A09 48E4
|
||||||
|
fetched 2026-10-21 over HTTPS from rpms.remirepo.net; byte-identical
|
||||||
|
to the key shipped in remi-release-10.2-1.el10.remi. Not found on
|
||||||
|
keys.openpgp.org / keyserver.ubuntu.com - confirm out of band.
|
||||||
|
Remi rotates keys (remi2025, ...): when remi-release starts being
|
||||||
|
signed by a new key, `release-install remi` fails loudly - add the
|
||||||
|
new key here (and to pkg-cache-repos.sh).
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGV4X6kBEAC3eQxgiWuo08uc3mHo4ELux++uqTnYz/tJzEf9Ou3h36WnhumA
|
||||||
|
Nvs+Ts5h8PBx879Y9/aIX1Z20p1kf6tBCinZnEJu59n+TAAsph0+XQlV1l5YkleK
|
||||||
|
Z2ff/Fg65k8QcLXWaIGykA/FaKznRiSurGuD6tRGhJw7DawEwBJr8QZSkRUpnH1L
|
||||||
|
URW97Q/iKrRPiE5VEayE0y8eAL28jIIiFvR+4oJMzvCsRRB/2wYZ2MlJOW91hcYf
|
||||||
|
mbUoXKOBD5UzsJylu7kj25K/ge8rEJ7KicOOwcdYddxsU3DxGSSfwF8AMagENcm2
|
||||||
|
XROeXknjm84A8sNlUkFZBJwfuc7eRTiZGJrnQQVYLrkKj8Mxpq9Ts7hU51TqAWNI
|
||||||
|
uvGDlJdYNE3D2RMqjMEsZ8ej08Thrib6xslu4NzTBkt+6QNnXL4E3hEgYtoyio60
|
||||||
|
GswSz2ulogKg7X4JrNdJYE8/qNowyF3hoVgj5TG1/wQRq+5HlMMOLjgGu9wzLUix
|
||||||
|
fnVfEUnzaofbrUf4/GabCaeY8xRe4tFQrvzigQ4g+kgwKKnfAeqBmPov0yljkw9z
|
||||||
|
BYJWR5zvaw0ffg9Ing00KUSaXBXA5jSlgk1603Y+LefY1SlXsTyqohiRvGH6FI77
|
||||||
|
HNMo72DwoJfFcYjncZUzKgXWJECR4nhVsdj6pKoOjcQ4aSuyVxtsR86ASQARAQAB
|
||||||
|
tChGZWRvcmEgKGVwZWwxMCkgPGVwZWxAZmVkb3JhcHJvamVjdC5vcmc+iQJOBBMB
|
||||||
|
CAA4FiEEfY0Vy/xOYmiFkfsmM9mFF+N+0VgFAmV4X6kCGw8FCwkIBwIGFQoJCAsC
|
||||||
|
BBYCAwECHgECF4AACgkQM9mFF+N+0Vhv/A/+PlhPLSctGRCUEahE+cN4764Acc3p
|
||||||
|
l40ZYzXRhqR0/Tc1/cSDjlA3qVTc8SPohi5OJXwCyr9EiMqKoyoDN097euqbYpyp
|
||||||
|
yN/Pj0lBjsXwcpdDtZ21WGeQU0Khb04N68bMtJbDaxeBciTvDDQravZuPPh0m4Rg
|
||||||
|
Z6myEoa6Aa6EK0hI1Qwi1qIWeRiuEkVT671IaKVETBW5XiUpNBXDAB/L+6DzUF9u
|
||||||
|
scBzfsUDiPO6NrpYDtV3jwq22y6gWluIct/Ka8brwPbqK2sBfFzrHboRhfqlTGjs
|
||||||
|
7F9qUGwIQZn/A8iozXZYQ0+JG1bhQyvjA8eN1GOcRpT+O7H7JXN49o6IG2As4+iK
|
||||||
|
F04+qjqAu2sVfpD8mzM2VubFNllcKKiyCzRYHhSbObRCPzsudDL9GPiXeGGaCuWg
|
||||||
|
sDkiA1MESvf2tLETAGBs/TziO4GwmXUtlKbRiq1FYm90mVq9mBxPZ/Idn+yZusNB
|
||||||
|
0O5SXIbI8lYZw5n4XTK4b+byHRBYsOTHiTsGvjTF2Y7oSwW2CVUmL6RZ23mI4qoY
|
||||||
|
1p5kzRS+GjT1acnTei/FTsOlIKCsjfeHx7uxCkX6xpAD8P3UtLQqfsgH0CL4vSZt
|
||||||
|
TGO6L1InQlp4ZG3OYIomTKbD3/R0wod3U3dTqdulQMXL895u6OLTY3spY2m2MO2k
|
||||||
|
p9Dfd2pKuxK9Mys=
|
||||||
|
=mhQZ
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
mQINBGWdEG0BEADV9c8aSIITQmoWKN0ac4c8AEhZcdzpWSRPwHIS8syVRw/gj30W
|
||||||
|
Mn1OoxqFn2UECh2jjqYaQFECXNrM2/lzO4GhvcuoPn6p0rXFMvYvwtiOJ+rmDvTe
|
||||||
|
UOwRxkZhYehpvsRPFoVhlYJh3U1BM0UHm2iE/TIqVusY9FHQuCMZVl0BHk81YGDa
|
||||||
|
qE3cyu+9zOs8z1bYlsYBgLIbtb4+loQTRexFTfZkUcCWgAaNyur8Wy7Msdm5LOqN
|
||||||
|
ZZu6bNKBOv0Cr+BOwGNoFRbi4IM+5H+0eI1n7V5if4CuXfdYHp+Pm+PQvftm/nqB
|
||||||
|
jRLKqioRKzHjk7xNMsi5lM+SU4WyfU77pr3RXuJR6fkymLVAeu5sy2U6ySj0SXIO
|
||||||
|
/I3oPp4/zJ8/e94jG5iDqh92b5IWyXSE+EGw4kkpUvZLrK8vyyEfk8h9Gf8qA5uL
|
||||||
|
yGfw0vnSVjYb+RdnfZFVdPaIEmeY8tnobS87duj8JESZiSWo5sU4filkciW5e+MJ
|
||||||
|
w9wohgl+Cqm2J5Ljqus6DRm4ptKbZD8QZRnmGcSk6O2VopBKt2XLat4eIJLr/EiN
|
||||||
|
0CJ1EH/oJmEeZbYtUjxjsGsbPXicwDH6ZIeIMSaiDgqRxPm1LTGVLjSHAk+k0e5l
|
||||||
|
ulEmaoA7t/p3U2peGc7ONvozXiyNHZdGiRkqFC1mA9/JpeWw4KFH6JSgmQARAQAB
|
||||||
|
tEtSZW1pJ3MgUlBNIHJlcG9zaXRvcnkgMjAyNCAoaHR0cHM6Ly9ycG1zLnJlbWly
|
||||||
|
ZXBvLm5ldC8pIDxyZW1pQHJlbWlyZXBvLm5ldD6JAlEEEwEIADsWIQTPHfAFfOhd
|
||||||
|
/1svKjfC/TssKglI5AUCZZ0QbQIbAwULCQgHAgIiAgYVCgkICwIEFgIDAQIeBwIX
|
||||||
|
gAAKCRDC/TssKglI5Na7EACDTGjTcuTyudfuu/JC41BSurclCcwZyPsGaClyc0qx
|
||||||
|
Tk8Rs4rbyVw1u494fiFG/yEVl7LGGja84UJOlMdw82KgVUmr4fGLaBBFWm+KnZ4W
|
||||||
|
4vvFcCIxUshENOaDrkhby9F5P379gVOK9I3n9l4Q2xt+sWUNwE74CNfCTUeYMIm/
|
||||||
|
/AHbb1DTM9FuIT6FssgvILMTlksiyrw8U8bj+QUzPZrFCpvv+PnFA7ZDMEyCf2Tz
|
||||||
|
/zawxoP3a3b+3/g61U9SVIkfToonnorqh9lkQYhsdKTFHW6eESqe7zcqMBZv/sSc
|
||||||
|
3kFzl22KczSdU8eYxdujv9BmlW2SOrQsnuMYxvoD1LzhwSB/nOapNrWU+djhJCof
|
||||||
|
ueZ+rBW4QWysmG0dEp5Wpz85TstAnQ+j5aCOMSAkiAVsrJ0Ff3JXakIiL7rEtccS
|
||||||
|
dhgBT06LJtJy1NGIC0Sotn45n6dtfi6IuI8LnELCKeHxhdFSJcAPB8y95u0uHymy
|
||||||
|
sqMfzjq1VcYtzC5II6Yd5Gtb2+QzyQwORslwXhUdXwZKUc5i6X5xFMSxU523fH9n
|
||||||
|
i+kQlUTPkOUr66v4fmYDj+ZlJQSFoPylcHCke/vOo/rH5nj+YMPa6LdohsdzHKpd
|
||||||
|
DkKrk+Udj5aDXtXGIFYwoNKO4BnCsz/ImZfB4CQ+ekJsY1pFill3+MRTM6IvSA+P
|
||||||
|
7LkCDQRlnRBtARAAq8k4FM9euMMsXz0buxxyr0ew0ecWNhZGy24a6DXORVTEJbgI
|
||||||
|
T3HpV/C2PWkwgEBswYzREm+en61jhWr5unCiytO2klT+K2WNcIUNW+z2ciL/JNaJ
|
||||||
|
OcKwjkiY/A+KtDStzllCQ/XnpgAXGvFZjMjhHrNGQoAwNnPE/cMtYwcuVdW3Ipm3
|
||||||
|
cOOtzxRmfnhP8YUEyHxdos/7AZowaiLIUkaL5RUaygGrfPGyUXPl1aXS1oiOm37W
|
||||||
|
RiKCGUxCHSlpBqeh1WQPOaps8y+ME9jMzF9SLvqiXlKPllJIx9i8tHUOnX6Y2U2x
|
||||||
|
5sXsCEiA+zNVMU3YQ04++Yx2Rp6sDhNEnAbTCqqequqIzaXHZWwszAeG6pj1G90m
|
||||||
|
8PwDeHy641ySgDMwVPpnH5rHWmmT96XvF9QNg07OBtW9/phryzwAuHogrBY4DyFD
|
||||||
|
pUxDkTv9TxfBaS/s1uJxEGknIgnKzQo79mRy206waGKRA2AV36ORSfU12sK8pl3r
|
||||||
|
N95iIDh/OIuMl2STaXilhwb1hRpGFvwRlq3Hvvan4SY1aO219Q+VxMGXeqC5F/rm
|
||||||
|
Anh7mRVMP2qu0rj7L8xJ/OD/bPfuWL72/PvviBr/jdRM9I+Vj8dPGjhUwhK1dpEG
|
||||||
|
Q6yvoVXLHXucVHfIVR+ALLBmK8oHXNIFeIXhiUbgmRAilPHRz6Rf5pgbrC0AEQEA
|
||||||
|
AYkCNgQYAQgAIBYhBM8d8AV86F3/Wy8qN8L9OywqCUjkBQJlnRBtAhsMAAoJEML9
|
||||||
|
OywqCUjkQ2kP/iwqvt1sX85KM4OSE9ftqemwnvh19NizMKHMYGgikMAvy2CndEOI
|
||||||
|
wzKVeE1iK9/ivZQQIbD5l0G9hzVZjIxbgu9dAcxM+jdmaPyUj7yn6AX2yeGAeHyA
|
||||||
|
qzNKZaTLJPieRzdXk/uKPVyWUuzbR0vwR7n8N5N0pe/FEfO9O0uX5iz2kf5+b10c
|
||||||
|
WqvzLmO+czuyCjTHnjhKfE73iIktYE3MnM8qFHPiKwm+jeTkUnmInoR3qIKR/SCV
|
||||||
|
u2vk/q5uvpNaUSPSDKji6wh3/hbNbPocLSGIjugfWNm/4LhlSHPNnRJ2dIvSxeiI
|
||||||
|
qtSWxhULEyHGBPMLDriVumCV7Rb/byJwiMbmCIpxB5XLtbgt7nIqWQTko+6ci71S
|
||||||
|
4rDXolM3bytYbpqq4b/Xc1bhBuTX3omwrd0kxmGqad8eia6O6iBsP7GAl4QkySm4
|
||||||
|
MfKQfohjXTnT60wqEzQwLGI4NKN9bHRSsGINHX5p+Kz9HLf87fTY4uglbFxg0/II
|
||||||
|
Y3nko5KBw/gfe0WpwZY0sWjK20cXJn3xLQjoG8OjUNUCUYTRsiAvn4AiDRt++qtl
|
||||||
|
bvJjJqOU1k4GrGoCVpEUiOc9Cw1YZREcFe8H2at3pGnxJ1DFlYA916kNAxYANp2s
|
||||||
|
r3N3agwevgIzP78hynxjm6a5H2w0vkg189pZn2RR9zX9gmrvlqg1o0P1
|
||||||
|
=meaU
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICPzCCAeagAwIBAgIUJI0QJZ+PCrSQ/nKZFENwwcp8Zr0wCgYIKoZIzj0EAwIw
|
||||||
|
TDEmMCQGA1UEAwwdcGtnLWNhY2hlIGludGVybmFsIENBIChDVDEzMCkxIjAgBgNV
|
||||||
|
BAoMGWNsb3VkLWhvc3RpbmcuaW8gaW50ZXJuYWwwHhcNMjYxMDIxMTkzNjIyWhcN
|
||||||
|
MzYxMDE4MTkzNjIyWjBMMSYwJAYDVQQDDB1wa2ctY2FjaGUgaW50ZXJuYWwgQ0Eg
|
||||||
|
KENUMTMwKTEiMCAGA1UECgwZY2xvdWQtaG9zdGluZy5pbyBpbnRlcm5hbDBZMBMG
|
||||||
|
ByqGSM49AgEGCCqGSM49AwEHA0IABBJh75sC5vSzTnkYl0CYYEA/TKUHEXhbtanV
|
||||||
|
OVNW4NlD3za5+bu5YuKjpbFAva4TCZqWsaS9IMd/JfsaFbf5XVejgaUwgaIwEgYD
|
||||||
|
VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFP7UCTf/
|
||||||
|
v7KU1KBrs1vqa71/TZl0MF0GA1UdHgEB/wRTMFGgTzAKhwjAqAEo/////zAlgiNw
|
||||||
|
a2ctY2FjaGUuaW50ZXJuYWwuY2xvdWQtaG9zdGluZy5pbzAaghhwa2ctY2FjaGUu
|
||||||
|
Z2Vla2hvbWUubG9jYWwwCgYIKoZIzj0EAwIDRwAwRAIgOod4wPHu0kAzYAw8KeGg
|
||||||
|
cIrFMwQ4ATKVTkDioiSFOqICIEhY+sGj/YZ4oBMst3b/AAz9zMEJAXpnCkSTI6tM
|
||||||
|
YAIl
|
||||||
|
-----END CERTIFICATE-----
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* cac-lsphp $_SERVER path normaliser (auto_prepend).
|
* cac-lsphp $_SERVER path normaliser (auto_prepend) — DEGRADED FALLBACK ONLY.
|
||||||
|
*
|
||||||
|
* SUPERSEDED by the cac_path_parity PHP extension (ext/cac-path-parity/), which
|
||||||
|
* does this from RINIT where a customer's .user.ini cannot displace it. The
|
||||||
|
* entrypoint only wires this file up when that extension is not loadable in the
|
||||||
|
* running image, and logs a WARNING when it does. Do not extend this script —
|
||||||
|
* fix the extension instead.
|
||||||
|
*
|
||||||
|
* It is kept because the flaw documented at the bottom of this docblock is
|
||||||
|
* exactly why the extension exists, and because an image where the extension
|
||||||
|
* failed to load should degrade to the old behaviour rather than to nothing.
|
||||||
*
|
*
|
||||||
* The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
|
* The shared-ols container serves from its bulk /docker/users->/mnt/users mount,
|
||||||
* so OLS sends lsphp $_SERVER['DOCUMENT_ROOT'] / ['SCRIPT_FILENAME'] under
|
* so OLS sends lsphp $_SERVER['DOCUMENT_ROOT'] / ['SCRIPT_FILENAME'] under
|
||||||
|
|||||||
+58
-24
@@ -14,8 +14,18 @@ export FPM_LISTEN=${FPM_LISTEN:-9000}
|
|||||||
|
|
||||||
adduser -u $uid $user
|
adduser -u $uid $user
|
||||||
|
|
||||||
mkdir -p /home/$user/public_html
|
# Root never follows customer symlinks under /home/$user: see safe-paths.sh.
|
||||||
mkdir -p /home/$user/logs/php-fpm
|
source /scripts/safe-paths.sh
|
||||||
|
secure_home
|
||||||
|
|
||||||
|
user_mkdir /home/$user/public_html /home/$user/logs/php-fpm
|
||||||
|
# Create the pool's error_log (php_admin_value[error_log] in
|
||||||
|
# create-php-config.sh) AS THE CUSTOMER, before php-fpm starts. It used to be
|
||||||
|
# `touch`ed as root at the very end, after the chown -R, so on a site's first
|
||||||
|
# start the pool (customer uid) could not open it and PHP errors fell back to
|
||||||
|
# FastCGI stderr -> shared-httpd's error log until the next restart's chown -R
|
||||||
|
# fixed the owner. user_touch never writes or chowns through a symlink.
|
||||||
|
user_touch /home/$user/logs/php-fpm/error.log
|
||||||
|
|
||||||
ln -sf /home/$user/logs/php-fpm /var/log/php-fpm
|
ln -sf /home/$user/logs/php-fpm /var/log/php-fpm
|
||||||
|
|
||||||
@@ -25,13 +35,27 @@ echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP-FPM pm=${PHP_FPM_PM} max_
|
|||||||
/scripts/create-php-config.sh
|
/scripts/create-php-config.sh
|
||||||
|
|
||||||
mkdir -p /run/php-fpm/
|
mkdir -p /run/php-fpm/
|
||||||
|
# php-fpm opens its slowlog under /home/$user/logs as root: last fail-closed
|
||||||
|
# check immediately before it starts (safe-paths.sh).
|
||||||
|
require_safe_logs /home/$user/logs/php-fpm
|
||||||
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
||||||
|
# GNU chown -R defaults to -P: symlinks are lchown()ed, never followed.
|
||||||
chown -R $user:$user /home/$user
|
chown -R $user:$user /home/$user
|
||||||
chmod -R 755 /home/$user
|
# Do NOT `chmod -R 755` the docroot here. /home/$user is the customer's
|
||||||
|
# bind-mounted site directory; a recursive 755 on every start made every file
|
||||||
|
# (wp-config.php included) world-readable AND executable, and reverted any
|
||||||
|
# tighter permissions the customer had set. Apache (shared-httpd, or the
|
||||||
|
# bundled httpd) runs as `apache`, not the customer, so it only needs to
|
||||||
|
# traverse directories to serve static files. Normalise that and nothing else:
|
||||||
|
# open the docroot itself, and add o+x to directories that lack it. Regular
|
||||||
|
# files are never touched, so execute is never added and wp-config.php is
|
||||||
|
# never loosened. Runs as the customer (safe-paths.sh) so a symlink can only
|
||||||
|
# reach what the customer already owns.
|
||||||
|
open_dir_traversal
|
||||||
|
|
||||||
if [[ $environment == 'DEV' ]]; then
|
if [[ $environment == 'DEV' ]]; then
|
||||||
echo "Starting Dev Deployment (FPM-only mode)"
|
echo "Starting Dev Deployment (FPM-only mode)"
|
||||||
mkdir -p /home/$user/_db_backups
|
user_mkdir /home/$user/_db_backups
|
||||||
if ! command -v microdnf &> /dev/null; then
|
if ! command -v microdnf &> /dev/null; then
|
||||||
echo "microdnf not found, installing with dnf..."
|
echo "microdnf not found, installing with dnf..."
|
||||||
dnf install -y microdnf && dnf clean all
|
dnf install -y microdnf && dnf clean all
|
||||||
@@ -49,13 +73,12 @@ if [[ $environment == 'DEV' ]]; then
|
|||||||
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
|
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
|
||||||
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
|
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
|
||||||
mysql -e "FLUSH PRIVILEGES;"
|
mysql -e "FLUSH PRIVILEGES;"
|
||||||
echo "# User crontab for $user" > /home/$user/crontab
|
# Written AS THE CUSTOMER (as_user tee), never by root through a customer path.
|
||||||
echo "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" >> /home/$user/crontab
|
drop_symlink /home/$user/crontab
|
||||||
chown $user:$user /home/$user/crontab
|
printf '%s\n' "# User crontab for $user" "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" \
|
||||||
echo "MySQL User: "$mysql_user > /home/$user/mysql_creds
|
| as_user tee /home/$user/crontab >/dev/null
|
||||||
echo "MySQL Password: "$mysql_password >> /home/$user/mysql_creds
|
printf '%s\n' "MySQL User: $mysql_user" "MySQL Password: $mysql_password" "MySQL Database: devdb_$mysql_db" \
|
||||||
echo "MySQL Database: devdb_"$mysql_db >> /home/$user/mysql_creds
|
| as_user tee /home/$user/mysql_creds
|
||||||
cat /home/$user/mysql_creds
|
|
||||||
fi
|
fi
|
||||||
/usr/bin/memcached -d -u $user
|
/usr/bin/memcached -d -u $user
|
||||||
fi
|
fi
|
||||||
@@ -66,21 +89,32 @@ if [[ $environment == 'PROD' ]]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up user crontab
|
# Set up + load the user crontab, created and read as the customer
|
||||||
if [ ! -f /home/$user/crontab ]; then
|
# (safe-paths.sh). Root reading/writing/chowning it followed symlinks.
|
||||||
echo "# User crontab for $user" > /home/$user/crontab
|
load_user_crontab
|
||||||
echo "# Add your cron jobs here" >> /home/$user/crontab
|
|
||||||
echo "# Example: */5 * * * * /home/$user/scripts/my-script.sh" >> /home/$user/crontab
|
|
||||||
chown $user:$user /home/$user/crontab
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Load user crontab
|
|
||||||
crontab -u $user /home/$user/crontab
|
|
||||||
|
|
||||||
/usr/sbin/crond
|
/usr/sbin/crond
|
||||||
|
|
||||||
# Tail PHP-FPM logs (becomes PID 1 process)
|
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
||||||
touch /home/$user/logs/php-fpm/error.log
|
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
||||||
tail -f /home/$user/logs/php-fpm/*
|
# docker waited the full 10s and SIGKILLed php-fpm mid-request (exit 137).
|
||||||
|
# php-fpm QUIT = graceful (workers finish their current request), then crond.
|
||||||
|
source /scripts/stop-services.sh
|
||||||
|
TAIL_PID=""
|
||||||
|
shutdown() {
|
||||||
|
trap - TERM INT
|
||||||
|
echo "entrypoint: received stop signal, shutting down"
|
||||||
|
stop_service php-fpm /run/php-fpm/php-fpm.pid QUIT 6 'php-fpm: master'
|
||||||
|
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
||||||
|
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap shutdown TERM INT
|
||||||
|
|
||||||
|
# Tail PHP-FPM logs to docker logs. Backgrounded + `wait` (instead of a
|
||||||
|
# foreground tail) so bash can run the trap as soon as a signal arrives.
|
||||||
|
tail -f /home/$user/logs/php-fpm/* &
|
||||||
|
TAIL_PID=$!
|
||||||
|
wait "$TAIL_PID"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
@@ -67,7 +67,46 @@ fi
|
|||||||
## see lsphp's PHP errors in the exact same file on the new image.
|
## see lsphp's PHP errors in the exact same file on the new image.
|
||||||
## Rendered as a tiny ini in lsphp's scan dir; PHP merges it after the
|
## Rendered as a tiny ini in lsphp's scan dir; PHP merges it after the
|
||||||
## production-tuning overrides at startup.
|
## production-tuning overrides at startup.
|
||||||
SCAN_DIR=$(/usr/local/lsws/lsphp${PHPVER}/bin/lsphp -i 2>/dev/null | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
|
## Captured, then matched in the shell. As a single pipeline this was
|
||||||
|
## `lsphp -i | awk '…{print;exit}'`: awk stops at the "Scan this dir" line,
|
||||||
|
## which is near the top of the output, so lsphp can still be writing when awk
|
||||||
|
## closes the pipe. lsphp then dies 141, `set -o pipefail` (line 12) makes that
|
||||||
|
## the pipeline's status, and because this is a BARE ASSIGNMENT `set -e` KILLS
|
||||||
|
## PID 1 — the container never starts. (Its twin in entrypoint-lsphp.sh chose a
|
||||||
|
## degraded fallback instead; this one just exits.) That is a race on whether
|
||||||
|
## the reader closes before the writer's last write() returns, not a function of
|
||||||
|
## how big the output is: see the long note over the probe helpers in
|
||||||
|
## entrypoint-lsphp.sh for the measurements. The rule is simply that no
|
||||||
|
## `writer | early-exiting-reader` belongs in a pipefail script.
|
||||||
|
##
|
||||||
|
## A here-string would remove the pipeline, but bash spills a here-string to
|
||||||
|
## /tmp/sh-thd.XXXXXX above a build-dependent size (65536 for the bash 5.2.21 in
|
||||||
|
## this image, between 4096 and 16384 for Debian's 5.2.15) — and on this line,
|
||||||
|
## a bare assignment, a temp file it cannot create is again `set -e` killing
|
||||||
|
## PID 1: `docker run --read-only` reproduces exactly that. So the extraction is
|
||||||
|
## done with parameter expansion, which allocates nothing.
|
||||||
|
##
|
||||||
|
## Same answer as the awk it replaces: first line starting "Scan this dir", then
|
||||||
|
## the text between the FIRST and SECOND '=> ' on it (awk's $2 under -F'=> '),
|
||||||
|
## empty if the line carries no separator, empty if there is no such line.
|
||||||
|
## `|| true` on the capture keeps a genuinely failing lsphp as an empty
|
||||||
|
## SCAN_DIR — which the `-n` test below already handles — not a boot failure.
|
||||||
|
LSPHP_INFO=$(/usr/local/lsws/lsphp"${PHPVER}"/bin/lsphp -i 2>/dev/null || true)
|
||||||
|
SCAN_DIR=""
|
||||||
|
## The leading newline is what makes a match on LINE 1 behave like every other
|
||||||
|
## line, exactly as awk's `^` anchor does.
|
||||||
|
scan_rest=$'\n'"$LSPHP_INFO"
|
||||||
|
if [[ $scan_rest == *$'\nScan this dir'* ]]; then
|
||||||
|
## `#` takes the SHORTEST prefix, i.e. the FIRST matching line — awk's `exit`.
|
||||||
|
scan_rest=${scan_rest#*$'\nScan this dir'}
|
||||||
|
scan_line="Scan this dir${scan_rest%%$'\n'*}"
|
||||||
|
if [[ $scan_line == *'=> '* ]]; then
|
||||||
|
SCAN_DIR=${scan_line#*'=> '}
|
||||||
|
SCAN_DIR=${SCAN_DIR%%'=> '*}
|
||||||
|
fi
|
||||||
|
unset scan_line
|
||||||
|
fi
|
||||||
|
unset scan_rest
|
||||||
if [ -n "$SCAN_DIR" ]; then
|
if [ -n "$SCAN_DIR" ]; then
|
||||||
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
|
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
|
||||||
; rendered at container start by entrypoint-litespeed.sh
|
; rendered at container start by entrypoint-litespeed.sh
|
||||||
@@ -194,7 +233,34 @@ trap term_handler TERM INT
|
|||||||
## down). We match the running message specifically — a bare grep for "running"
|
## down). We match the running message specifically — a bare grep for "running"
|
||||||
## would also match "not running". (This image keeps the pidfile under
|
## would also match "not running". (This image keeps the pidfile under
|
||||||
## /tmp/lshttpd, not logs/, so we never hard-code a pidfile path.)
|
## /tmp/lshttpd, not logs/, so we never hard-code a pidfile path.)
|
||||||
ols_running() { /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi 'running with pid'; }
|
##
|
||||||
|
## Read into a variable and match with a here-string rather than piping into
|
||||||
|
## `grep -qi`: `grep -q` closes the pipe on its first match, and under the
|
||||||
|
## `set -o pipefail` at the top of this file a writer that is still writing when
|
||||||
|
## that happens dies 141 and the pipeline reports FALSE — i.e. "OLS is down"
|
||||||
|
## precisely because the "running" line matched, which here means a spurious
|
||||||
|
## relaunch of a healthy OLS, five of which trip the crash-loop cap and exit
|
||||||
|
## PID 1. (Same defect that shipped in entrypoint-lsphp.sh's cac_path_parity
|
||||||
|
## probe.) The reason to change it is STRUCTURAL — a pipefail script must not
|
||||||
|
## pipe into an early-exit reader, whatever the payload — because "lswsctrl
|
||||||
|
## prints one short line so it always wins" is a size argument, and size
|
||||||
|
## arguments about this race are wrong: see the measurements over the probe
|
||||||
|
## helpers in entrypoint-lsphp.sh, where 41 KB SIGPIPEd 11% of the time into a
|
||||||
|
## 64 KB pipe. A non-zero `lswsctrl` still means "not running", exactly as
|
||||||
|
## pipefail made it mean before.
|
||||||
|
##
|
||||||
|
## A here-string is the right shape HERE, where those helpers use `[[ ]]`: the
|
||||||
|
## reason to avoid `<<<` there is that bash spills a large here-string to
|
||||||
|
## /tmp/sh-thd.XXXXXX and so makes a writable temp dir a boot precondition. The
|
||||||
|
## threshold is 65536 bytes in this image's bash 5.2.21 and no lower than 4096
|
||||||
|
## in any bash this repo has met; `lswsctrl status` prints well under 100 bytes
|
||||||
|
## and cannot approach it, so no temp file is ever created and the case-
|
||||||
|
## insensitive match stays a plain `grep -i` instead of a hand-rolled glob.
|
||||||
|
ols_running() {
|
||||||
|
local st
|
||||||
|
st=$(/usr/local/lsws/bin/lswsctrl status 2>/dev/null) || return 1
|
||||||
|
grep -qi 'running with pid' <<<"$st"
|
||||||
|
}
|
||||||
|
|
||||||
## Crash-loop cap: if OLS can't stay up, bail out so Docker's restart policy and
|
## Crash-loop cap: if OLS can't stay up, bail out so Docker's restart policy and
|
||||||
## the site-health monitor escalate instead of us hot-looping forever.
|
## the site-health monitor escalate instead of us hot-looping forever.
|
||||||
|
|||||||
+367
-17
@@ -22,6 +22,14 @@
|
|||||||
## /home/$user/public_html files. PHP canonicalises the symlink, so
|
## /home/$user/public_html files. PHP canonicalises the symlink, so
|
||||||
## __FILE__/__DIR__/realpath all report /home/$user/public_html (verified
|
## __FILE__/__DIR__/realpath all report /home/$user/public_html (verified
|
||||||
## 2026-06-10) — the customer never sees the /mnt/users path.
|
## 2026-06-10) — the customer never sees the /mnt/users path.
|
||||||
|
##
|
||||||
|
## THE $_SERVER STRINGS: the symlink makes paths RESOLVE, but the raw strings OLS
|
||||||
|
## put in $_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] still read /mnt/users.
|
||||||
|
## The cac_path_parity extension (baked into the image, configured per-site
|
||||||
|
## below) rewrites those two at request start, so a site moved from cac-fpm to
|
||||||
|
## cac-lsphp sees byte-identical values. It replaced an auto_prepend_file
|
||||||
|
## normaliser that any site's own .user.ini silently displaced — see
|
||||||
|
## ext/cac-path-parity/cac_path_parity.c.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -60,8 +68,33 @@ SAFE_DOMAIN="$domain"
|
|||||||
case "$domain" in
|
case "$domain" in
|
||||||
\*.*) SAFE_DOMAIN="wildcard.${domain#\*.}" ;;
|
\*.*) SAFE_DOMAIN="wildcard.${domain#\*.}" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
## Both of these get interpolated into generated php.ini fragments below. They
|
||||||
|
## are panel-validated and both already feed `ln -sfn` and the shared-ols vhost
|
||||||
|
## config, so a hostile value is not reachable today — this is the belt to that
|
||||||
|
## brace. A newline in $domain is an INI-DIRECTIVE INJECTION into the generated
|
||||||
|
## fragment (measured against the pre-fix script: domain=$'evil.com\nprecision =
|
||||||
|
## 7\n; ' put that directive in 99-cac-path-parity.ini and lsphp reported
|
||||||
|
## `precision => 7`); `$(...)` yields an ini parse error and `"` an empty value,
|
||||||
|
## and BOTH of those leave the
|
||||||
|
## path-parity extension INERT — the exact silent parity loss this whole change
|
||||||
|
## exists to eliminate. Quoting the emitted values (done below) neutralises
|
||||||
|
## newlines and quotes; it does NOT neutralise php.ini's own `${VAR}`
|
||||||
|
## interpolation, which is why the character class is checked as well.
|
||||||
|
INI_TOKENS_OK=yes
|
||||||
|
case "$user" in ''|*[!A-Za-z0-9._-]*) INI_TOKENS_OK=no ;; esac
|
||||||
|
case "$SAFE_DOMAIN" in ''|*[!A-Za-z0-9._-]*) INI_TOKENS_OK=no ;; esac
|
||||||
|
if [ "$INI_TOKENS_OK" != yes ]; then
|
||||||
|
echo "WARNING: entrypoint-lsphp: user/domain contain characters outside [A-Za-z0-9._-] — refusing to generate php.ini fragments from them, so the \$_SERVER path-parity mapping and the per-site error_log are BOTH skipped (the extension stays inert, log_errors stays On from the image defaults and PHP logs to stderr i.e. \`docker logs\`; requests are unaffected). user=$(printf '%q' "$user") domain=$(printf '%q' "$domain")" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
## The exact path prefix the shared-ols container serves this site from — the
|
||||||
|
## string OLS puts in SCRIPT_FILENAME/DOCUMENT_ROOT. Used twice: for the symlink
|
||||||
|
## that makes it RESOLVE, and for the cac_path_parity mapping that makes it READ
|
||||||
|
## like cac-fpm. Deriving both from one variable keeps them in lockstep.
|
||||||
|
OLS_SITE_PATH="/mnt/users/$user/$SAFE_DOMAIN"
|
||||||
mkdir -p "/mnt/users/$user"
|
mkdir -p "/mnt/users/$user"
|
||||||
ln -sfn "/home/$user" "/mnt/users/$user/$SAFE_DOMAIN"
|
ln -sfn "/home/$user" "$OLS_SITE_PATH"
|
||||||
|
|
||||||
## ---- detached-lsphp pool sizing ----
|
## ---- detached-lsphp pool sizing ----
|
||||||
# shellcheck source=/dev/null
|
# shellcheck source=/dev/null
|
||||||
@@ -76,6 +109,32 @@ export PHP_LSAPI_MAX_REQUESTS="${PHP_LSAPI_MAX_REQUESTS:-500}"
|
|||||||
export LSAPI_MAX_IDLE="${LSAPI_MAX_IDLE:-30}"
|
export LSAPI_MAX_IDLE="${LSAPI_MAX_IDLE:-30}"
|
||||||
export LSAPI_EXTRA_CHILDREN="${LSAPI_EXTRA_CHILDREN:-5}"
|
export LSAPI_EXTRA_CHILDREN="${LSAPI_EXTRA_CHILDREN:-5}"
|
||||||
export LSAPI_AVOID_FORK="${LSAPI_AVOID_FORK:-0}"
|
export LSAPI_AVOID_FORK="${LSAPI_AVOID_FORK:-0}"
|
||||||
|
## LSAPI_KEEP_LISTEN=2 works around a leak in lsphp's own bookkeeping — not a
|
||||||
|
## setting we're tuning for taste. The master keeps a `busy` worker counter in
|
||||||
|
## a MAP_SHARED page it shares with its children; measured live on whp01,
|
||||||
|
## that counter drifts NEGATIVE over days of uptime (arclightcourt.com-01 was
|
||||||
|
## at busy=-8 after 6.9 days; a healthy sibling sat at 0..9). php-src
|
||||||
|
## sapi/litespeed/lsapilib.c computes each child's idle-exit grace period as
|
||||||
|
## `10 + busy*10` seconds (capped by LSAPI_MAX_IDLE) INSIDE
|
||||||
|
## `if (s_keep_listener == 1)` — with busy=-8 that's `wait_time = -70`, so
|
||||||
|
## workers exit after ~1s idle instead of 10-30s. No worker then lingers in
|
||||||
|
## accept(), so the master's "an idle worker is already accepting, don't
|
||||||
|
## fork" guard never fires and it forks for every single connection —
|
||||||
|
## observed slamming the hard child ceiling under bot traffic
|
||||||
|
## (`Reached max children process limit`) and, on rejection, leaving the
|
||||||
|
## pending connection to rot in the kernel backlog as a 503. Confirmed
|
||||||
|
## asymmetry: the affected site logged 306 OLS-side `ExtConn timed out` /
|
||||||
|
## deadlock / `oops! 503` errors where an identically-configured healthy
|
||||||
|
## sibling logged 0. Restarting the container resets the counter to 0 (it's
|
||||||
|
## initialised at master start) but it drifts negative again over about a
|
||||||
|
## week — a reset, not a cure. LSAPI_KEEP_LISTEN=2 skips the `== 1` branch
|
||||||
|
## entirely, so idle-exit timing is never derived from the leaked counter and
|
||||||
|
## instead falls straight back to LSAPI_MAX_IDLE above. The is_enough_free_mem()
|
||||||
|
## memory guard sits immediately above that branch in lsapilib.c and is NOT
|
||||||
|
## part of it, so it still applies at =2 — this does not trade away the
|
||||||
|
## memory-pressure protection LSAPI_MAX_IDLE exists for. Still overridable
|
||||||
|
## (e.g. back to 1) per-container as an escape hatch.
|
||||||
|
export LSAPI_KEEP_LISTEN="${LSAPI_KEEP_LISTEN:-2}"
|
||||||
LSPHP_BIND="${LSPHP_BIND:-0.0.0.0:9000}"
|
LSPHP_BIND="${LSPHP_BIND:-0.0.0.0:9000}"
|
||||||
|
|
||||||
## ---- .user.ini support ----
|
## ---- .user.ini support ----
|
||||||
@@ -97,40 +156,331 @@ LSPHP_BIND="${LSPHP_BIND:-0.0.0.0:9000}"
|
|||||||
## .user.ini has not been remediated yet.
|
## .user.ini has not been remediated yet.
|
||||||
export LSPHP_ENABLE_USER_INI="${LSPHP_ENABLE_USER_INI:-on}"
|
export LSPHP_ENABLE_USER_INI="${LSPHP_ENABLE_USER_INI:-on}"
|
||||||
|
|
||||||
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP_LSAPI_CHILDREN=${PHP_LSAPI_CHILDREN} | LSAPI_MAX_IDLE=${LSAPI_MAX_IDLE} | PHPVER=${PHPVER} | bind=${LSPHP_BIND} | user_ini=${LSPHP_ENABLE_USER_INI}"
|
echo "Container memory: ${CONTAINER_MEMORY_MB}MB | PHP_LSAPI_CHILDREN=${PHP_LSAPI_CHILDREN} | LSAPI_MAX_IDLE=${LSAPI_MAX_IDLE} | LSAPI_KEEP_LISTEN=${LSAPI_KEEP_LISTEN} | PHPVER=${PHPVER} | bind=${LSPHP_BIND} | user_ini=${LSPHP_ENABLE_USER_INI}"
|
||||||
|
|
||||||
|
## Validate a numeric value destined for a generated php.ini fragment.
|
||||||
|
## Sets INI_NUM to the value when it is acceptable, and to "" (plus a WARNING)
|
||||||
|
## when it is not. Never fatal: a rejected override just leaves the image
|
||||||
|
## default in place, and the site serves either way.
|
||||||
|
##
|
||||||
|
## Digits-only is what closes the injection: no newline, quote, `$` or `{` can
|
||||||
|
## survive it, so neither an ini-directive injection nor php.ini's `${VAR}`
|
||||||
|
## interpolation is reachable regardless of what the caller sent.
|
||||||
|
##
|
||||||
|
## The range bound is a separate, weaker concern: it is a sanity check, NOT a
|
||||||
|
## guarantee that the value works. Measured — with `99-prod-overrides.ini`
|
||||||
|
## setting `opcache.interned_strings_buffer = 16`, a memory_consumption of 8 or
|
||||||
|
## 16 is ACCEPTED here and still aborts opcache at startup ("Insufficient shared
|
||||||
|
## memory for interned strings buffer"), loading no opcache at all. The floor is
|
||||||
|
## not raised to cover that because doing so would forfeit the superset property
|
||||||
|
## below; the panel clamps at 32, well clear of it.
|
||||||
|
validate_ini_num() {
|
||||||
|
local name="$1" val="$2" min="$3" max="$4"
|
||||||
|
INI_NUM=""
|
||||||
|
case "$val" in
|
||||||
|
''|*[!0-9]*) ;;
|
||||||
|
*)
|
||||||
|
## Length-cap first: `[ -lt ]` on a 25-digit string is an arithmetic
|
||||||
|
## error, not a comparison. 7 digits covers every max below.
|
||||||
|
if [ "${#val}" -le 7 ] && [ "$val" -ge "$min" ] && [ "$val" -le "$max" ]; then
|
||||||
|
INI_NUM="$val"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "WARNING: entrypoint-lsphp: ${name}=$(printf '%q' "$val") is not a plain integer in ${min}-${max} — ignoring it; the image default from 99-prod-overrides.ini applies." >&2
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
## ---- per-site ini drop-ins (identical mechanism to entrypoint-litespeed.sh) ----
|
## ---- per-site ini drop-ins (identical mechanism to entrypoint-litespeed.sh) ----
|
||||||
## error_log → the same customer-visible path cac:phpNN / cac-litespeed use, so
|
## error_log → the same customer-visible path cac:phpNN / cac-litespeed use, so
|
||||||
## "where's my PHP error log?" is answered identically across all site types.
|
## "where's my PHP error log?" is answered identically across all site types.
|
||||||
SCAN_DIR=$("$LSPHP_BIN" -i 2>/dev/null | awk -F'=> ' '/^Scan this dir/ {print $2; exit}')
|
## Capture lsphp's own info once and read both answers out of it. Probe with
|
||||||
|
## `-i` ONLY: lsphp is the LSAPI SAPI, not the CLI — it accepts just
|
||||||
|
## -[b|c|n|h|i|q|s|v|?] and answers `-m`/`-r` by printing usage and exiting 0, so
|
||||||
|
## a `lsphp -m | grep` test never matches and never errors either.
|
||||||
|
##
|
||||||
|
## ---- CAC-TEST: probe helpers BEGIN ----
|
||||||
|
## Everything between these two markers is extracted verbatim and executed by
|
||||||
|
## scripts/tests/lsphp-info-probe.test.sh — the markers are inert comments with
|
||||||
|
## no runtime effect, and they exist so the test exercises THE SHIPPED CODE
|
||||||
|
## rather than a copy of it that can drift away from it. Keep BOTH markers: the
|
||||||
|
## extractor refuses to emit anything unless it sees the END one, so a half-
|
||||||
|
## deleted pair is reported there as a marker error instead of silently
|
||||||
|
## sourcing the rest of this file.
|
||||||
|
##
|
||||||
|
## WHY THESE MATCH `$1` IN THE SHELL AND NEVER PIPE IT INTO A READER.
|
||||||
|
##
|
||||||
|
## What broke. Both probes used to be `printf '%s\n' "$LSPHP_INFO" | <reader>`,
|
||||||
|
## and both readers stop early — `grep -q` at its first match, `awk` at `exit`.
|
||||||
|
## When the reader closes the pipe with the writer still writing, the writer
|
||||||
|
## takes SIGPIPE and dies 141; `set -o pipefail` (line 34) adopts 141 as the
|
||||||
|
## PIPELINE's status; and the branch reads FALSE **because the thing it was
|
||||||
|
## looking for was present early enough to stop the reader**. Measured on whp02
|
||||||
|
## against the published cac-lsphp:php83: 5/5 runs status=141 with pipefail,
|
||||||
|
## 0 without.
|
||||||
|
##
|
||||||
|
## THE RULE, and it is not about size. ANY `writer | early-exiting-reader`
|
||||||
|
## under pipefail is a latent 141. PAYLOAD SIZE IS NOT A SAFETY ARGUMENT. Each
|
||||||
|
## run is decided by a race — whether the reader's close lands before the
|
||||||
|
## writer's final write() returns — and the payload only sets how many write()
|
||||||
|
## syscalls the writer has to lose. Measured here against a default
|
||||||
|
## 65536-byte pipe (confirmed with F_GETPIPE_SZ):
|
||||||
|
## 41144 bytes -> 141 in 32/300 runs (11%) — well UNDER capacity
|
||||||
|
## 65012 bytes -> 141 in 25/30 runs — not 100% even AT capacity
|
||||||
|
## 500 KB into a 1 MiB pipe -> 200/200 SIGPIPE written 4096 bytes at a
|
||||||
|
## time, 0/200 written as one 500 KB write
|
||||||
|
## and strace caught printf dying having written 12086 of 40406 bytes into a
|
||||||
|
## 65536-byte pipe, i.e. losing with 53 KB of room to spare. The reason the same
|
||||||
|
## image failed 5/5 on whp02 and 10/10 clean in a dev container is the WRITER's
|
||||||
|
## syscall size: bash <= 5.2.15 pushes ~37 KB per write, bash >= 5.2.21 pushes
|
||||||
|
## 80-160 bytes, so the newer shell needs hundreds of chances to lose the race
|
||||||
|
## and the older one needs a couple. (An earlier draft of this comment blamed
|
||||||
|
## pipe capacity and fs.pipe-user-pages-soft. Both were wrong: that soft limit
|
||||||
|
## clamps new pipes to two pages rather than one, applies only once a single uid
|
||||||
|
## holds more than 1024 pipes, and is skipped entirely for CAP_SYS_RESOURCE.)
|
||||||
|
##
|
||||||
|
## The only SOUND reasons a call site is safe are structural:
|
||||||
|
## * the file does not set pipefail; or
|
||||||
|
## * the reader provably consumes to EOF (no `q`, `-q`, `-l`, `-m`, `exit`,
|
||||||
|
## `break`); or
|
||||||
|
## * the pipeline's status is discarded.
|
||||||
|
## The reader's implementation is not a defence either: at 248 KB, mawk, gawk,
|
||||||
|
## `grep -q` and `head -1` each returned 141 on 10/10, and these images have
|
||||||
|
## already drifted between mawk 1.3.4 (cac-lsphp) and gawk 5.2.1
|
||||||
|
## (cac-litespeed:php83) — not a property this repo controls.
|
||||||
|
##
|
||||||
|
## WHY PURE-BASH MATCHING RATHER THAN A HERE-STRING. `<<<` does remove the
|
||||||
|
## pipeline, but it is not free: above a build-dependent size bash materialises
|
||||||
|
## the string as /tmp/sh-thd.XXXXXX, so it makes a writable temp dir a
|
||||||
|
## PRECONDITION OF BOOTING. Measured in this image (bash 5.2.21) the switch is
|
||||||
|
## at exactly 65536 bytes and `lsphp -i` is 39934, so the here-string form was
|
||||||
|
## not hitting disk here — but Debian's bash 5.2.15 switches somewhere between
|
||||||
|
## 4096 and 16384, where the same payload would. What that costs is not
|
||||||
|
## theoretical:
|
||||||
|
## docker run --read-only ... 'SCAN_DIR=$(awk ... <<<"$BIG")'
|
||||||
|
## -> bash: cannot create temp file for here-document: Read-only file
|
||||||
|
## system ... and the script is dead: exit 1, PID 1 gone.
|
||||||
|
## which is the exact boot failure this branch exists to remove, re-acquired
|
||||||
|
## from a different direction and gated on which bash the base image ships.
|
||||||
|
## `[[ ]]` and `${...}` allocate nothing and cannot fail that way. Where the
|
||||||
|
## subject is a couple of hundred bytes and provably cannot approach the
|
||||||
|
## threshold, a here-string is still fine — see `ols_running` in
|
||||||
|
## entrypoint-litespeed.sh, which says so at the call site.
|
||||||
|
##
|
||||||
|
## THE PATTERNS ARE THE OLD ONES RE-EXPRESSED, NOT APPROXIMATED.
|
||||||
|
## grep -q '^cac_path_parity support => enabled$' — an anchored whole-line
|
||||||
|
## match, so the subject is wrapped in a newline at BOTH ends and the glob
|
||||||
|
## matches \n<line>\n; the wrapping is what keeps the first line and an
|
||||||
|
## unterminated last line matching exactly as grep matched them.
|
||||||
|
## grep -q '^PHP Version => ' — anchored at the start
|
||||||
|
## only, so only a leading newline is added.
|
||||||
|
## awk -F'=> ' '/^Scan this dir/ {print $2; exit}' — first matching line,
|
||||||
|
## then the text between the FIRST and SECOND '=> ' on it ($2), or empty if
|
||||||
|
## there is no separator. `${x#*'=> '}` then `${y%%'=> '*}` is that, exactly.
|
||||||
|
## scripts/tests/lsphp-info-probe.test.sh asserts this equivalence against the
|
||||||
|
## grep/awk originals over the edge cases (match on the first line, on the last
|
||||||
|
## line with no trailing newline, decoy substrings, a second separator, an empty
|
||||||
|
## value, a missing key), so "same answer as before" is checked, not asserted.
|
||||||
|
##
|
||||||
|
## Statuses are unchanged: a genuinely-absent extension is still a clean 1, and
|
||||||
|
## a genuinely-missing "Scan this dir" line is still empty output with status 0.
|
||||||
|
lsphp_info_has_parity_ext() {
|
||||||
|
[[ $'\n'"$1"$'\n' == *$'\ncac_path_parity support => enabled\n'* ]]
|
||||||
|
}
|
||||||
|
lsphp_info_scan_dir() {
|
||||||
|
local rest line val
|
||||||
|
rest=$'\n'"$1"
|
||||||
|
[[ $rest == *$'\nScan this dir'* ]] || return 0
|
||||||
|
## `#` takes the SHORTEST prefix, i.e. the FIRST matching line — awk's `exit`.
|
||||||
|
rest=${rest#*$'\nScan this dir'}
|
||||||
|
line="Scan this dir${rest%%$'\n'*}"
|
||||||
|
val=""
|
||||||
|
if [[ $line == *'=> '* ]]; then
|
||||||
|
val=${line#*'=> '}
|
||||||
|
val=${val%%'=> '*}
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$val"
|
||||||
|
}
|
||||||
|
## Did `lsphp -i` answer at all? Separates "the extension is not there" from
|
||||||
|
## "our probe produced nothing to look in", so neither gets reported as the
|
||||||
|
## other. Keyed on the phpinfo banner, which is line 2 of every `lsphp -i`
|
||||||
|
## (verified against lsphp83 8.3.32) and is not something LSPHP_INFO could
|
||||||
|
## contain from any other source.
|
||||||
|
lsphp_info_is_usable() {
|
||||||
|
[[ $'\n'"$1" == *$'\nPHP Version => '* ]]
|
||||||
|
}
|
||||||
|
## ---- CAC-TEST: probe helpers END ----
|
||||||
|
|
||||||
|
PATH_PARITY_MODE="none"
|
||||||
|
LSPHP_INFO=$("$LSPHP_BIN" -i 2>/dev/null || true)
|
||||||
|
SCAN_DIR=$(lsphp_info_scan_dir "$LSPHP_INFO")
|
||||||
|
## `|| true` above is what keeps a broken probe survivable: fail-open is
|
||||||
|
## deliberate here and below — the site serves either way, only the $_SERVER
|
||||||
|
## strings differ. What the failure gets REPORTED as is handled at each of the
|
||||||
|
## two places it changes the outcome (the parity branch, and the no-scan-dir
|
||||||
|
## else at the bottom of this block).
|
||||||
if [ -n "$SCAN_DIR" ]; then
|
if [ -n "$SCAN_DIR" ]; then
|
||||||
mkdir -p "$SCAN_DIR"
|
mkdir -p "$SCAN_DIR"
|
||||||
cat > "$SCAN_DIR/99-user-error-log.ini" <<EOF
|
## Values emitted double-quoted via printf rather than interpolated into an
|
||||||
; rendered at container start by entrypoint-lsphp.sh
|
## unquoted heredoc — see the INI_TOKENS_OK note above for what that prevents.
|
||||||
error_log = /home/${user}/logs/php-fpm/error.log
|
##
|
||||||
log_errors = On
|
## Gated on INI_TOKENS_OK for the same reason the mapping below is: this
|
||||||
EOF
|
## fragment interpolates $user into generated ini too. Leaving it ungated was
|
||||||
## Normalise \$_SERVER['DOCUMENT_ROOT']/['SCRIPT_FILENAME'] from the OLS-sent
|
## an INCONSISTENCY, not a live hole — a newline is inert inside the quotes,
|
||||||
## /mnt/users path back to /home/<user> so cac-lsphp is byte-for-byte 1:1 with
|
## and a `${`-bearing $user cannot exist because the useradd above would have
|
||||||
## cac-fpm. Customer sites have no auto_prepend by default, so this is safe; a
|
## failed under `set -euo pipefail`. But "this particular unvetted value
|
||||||
## site that sets its own .user.ini auto_prepend overrides it (paths still
|
## happens to be contained" is the reasoning this branch already rejected one
|
||||||
## resolve via the symlink either way).
|
## screenful up, so it is not the reasoning that guards this line either.
|
||||||
|
##
|
||||||
|
## Rejecting costs such a user nothing it needs: `log_errors = On` is already
|
||||||
|
## baked in by 99-prod-overrides.ini, so PHP still logs — to stderr, i.e.
|
||||||
|
## `docker logs`, which is MORE visible than a per-site file, not less. No
|
||||||
|
## legitimate user reaches this branch (verified fleet-wide: 30 shared_ols
|
||||||
|
## sites across 4 hosts, none rejected by the charset check).
|
||||||
|
if [ "$INI_TOKENS_OK" = yes ]; then
|
||||||
|
{
|
||||||
|
echo '; rendered at container start by entrypoint-lsphp.sh'
|
||||||
|
printf 'error_log = "%s"\n' "/home/$user/logs/php-fpm/error.log"
|
||||||
|
echo 'log_errors = On'
|
||||||
|
} > "$SCAN_DIR/99-user-error-log.ini"
|
||||||
|
else
|
||||||
|
## The container filesystem survives `docker restart`, so a fragment an
|
||||||
|
## earlier boot wrote from a different env must not outlive the rejection.
|
||||||
|
rm -f "$SCAN_DIR/99-user-error-log.ini"
|
||||||
|
fi
|
||||||
|
## ---- $_SERVER path parity with cac-fpm ----
|
||||||
|
## Point the cac_path_parity extension at THIS site's mapping. Same two
|
||||||
|
## values the compatibility symlink above is built from, so the rewrite and
|
||||||
|
## the symlink can never disagree.
|
||||||
|
##
|
||||||
|
## Both settings are PHP_INI_SYSTEM: a customer's .user.ini (PHP_INI_PERDIR /
|
||||||
|
## PHP_INI_USER only) cannot redirect or disable them, and the extension
|
||||||
|
## occupies no userland hook — so the customer's own auto_prepend_file (the
|
||||||
|
## Wordfence WAF on several live sites) keeps working untouched. That
|
||||||
|
## combination is why this is an extension: the previous auto_prepend_file
|
||||||
|
## normaliser was itself PHP_INI_PERDIR and any site with its own prepend
|
||||||
|
## silently displaced it, while making OUR prepend win would have disabled
|
||||||
|
## THEIRS. See ext/cac-path-parity/cac_path_parity.c.
|
||||||
|
if [ "$INI_TOKENS_OK" != yes ]; then
|
||||||
|
## Already warned above. Write NOTHING: neither the mapping (we will not
|
||||||
|
## generate ini from an unvetted string) nor the auto_prepend fallback (which
|
||||||
|
## would not be correct for such a site either). The extension stays inert,
|
||||||
|
## the request path is unaffected.
|
||||||
|
rm -f "$SCAN_DIR/99-cac-path-parity.ini" "$SCAN_DIR/99-cac-lsphp-normalize.ini"
|
||||||
|
PATH_PARITY_MODE="none (user/domain rejected)"
|
||||||
|
elif lsphp_info_has_parity_ext "$LSPHP_INFO"; then
|
||||||
|
{
|
||||||
|
echo '; rendered at container start by entrypoint-lsphp.sh'
|
||||||
|
printf 'cac_path_parity.from = "%s"\n' "$OLS_SITE_PATH"
|
||||||
|
printf 'cac_path_parity.to = "%s"\n' "/home/$user"
|
||||||
|
} > "$SCAN_DIR/99-cac-path-parity.ini"
|
||||||
|
## Drop the pre-extension fallback if an older image left one here — the
|
||||||
|
## container filesystem survives a "docker restart", so an in-place upgrade
|
||||||
|
## must not keep a stale auto_prepend pointing at the old normaliser.
|
||||||
|
rm -f "$SCAN_DIR/99-cac-lsphp-normalize.ini"
|
||||||
|
PATH_PARITY_MODE="extension"
|
||||||
|
else
|
||||||
|
## Degraded fallback for an image built before the extension existed (or one
|
||||||
|
## where it failed to load). Restores the old, .user.ini-defeatable
|
||||||
|
## behaviour rather than losing normalisation entirely — but say so loudly,
|
||||||
|
## because in this mode parity is NOT guaranteed.
|
||||||
|
##
|
||||||
|
## FAIL-OPEN, DELIBERATELY: a probe that cannot answer must never stop the
|
||||||
|
## container. The site serves either way; only the $_SERVER strings differ.
|
||||||
cat > "$SCAN_DIR/99-cac-lsphp-normalize.ini" <<'EOF'
|
cat > "$SCAN_DIR/99-cac-lsphp-normalize.ini" <<'EOF'
|
||||||
; rendered at container start by entrypoint-lsphp.sh
|
; rendered at container start by entrypoint-lsphp.sh (DEGRADED FALLBACK)
|
||||||
auto_prepend_file = /scripts/cac-lsphp-normalize.php
|
auto_prepend_file = /scripts/cac-lsphp-normalize.php
|
||||||
EOF
|
EOF
|
||||||
|
## ...but do not DIAGNOSE more than was established. The old wording said
|
||||||
|
## "extension not loadable in this image" for EVERY reason this branch is
|
||||||
|
## reached — including the probe breaking on its own, which is exactly what
|
||||||
|
## happened (see the SIGPIPE note on the helpers above): a false verdict
|
||||||
|
## that sent operators to rebuild an image whose extension was fine and
|
||||||
|
## whose build gate had passed. The claim now carries its evidence, and the
|
||||||
|
## evidence is real: reaching here at all means SCAN_DIR was parsed out of
|
||||||
|
## this same output, so `lsphp -i` did answer and its module list is
|
||||||
|
## authoritative. The case where it did NOT answer never gets here — it is
|
||||||
|
## caught and reported honestly at the `lsphp_info_is_usable` check above.
|
||||||
|
PATH_PARITY_MODE="auto_prepend (DEGRADED)"
|
||||||
|
echo "WARNING: entrypoint-lsphp: cac_path_parity extension not loadable in this image — '${LSPHP_BIN} -i' answered (${#LSPHP_INFO} bytes, scan dir ${SCAN_DIR}) and does not list it — falling back to the auto_prepend normaliser, which a site's own .user.ini auto_prepend_file will silently displace. Rebuild/repull cac-lsphp:php${PHPVER}." >&2
|
||||||
|
fi
|
||||||
## Per-site opcache override (panel: Advanced Tuning → OpCache size); falls
|
## Per-site opcache override (panel: Advanced Tuning → OpCache size); falls
|
||||||
## back to the baked lsphp-overrides.ini defaults when unset.
|
## back to the baked lsphp-overrides.ini defaults when unset.
|
||||||
if [ -n "${OPCACHE_MEMORY_MB:-}" ] || [ -n "${OPCACHE_MAX_FILES:-}" ]; then
|
##
|
||||||
|
## SAME INJECTION CLASS AS THE MAPPING ABOVE, and it was left open when that
|
||||||
|
## one was closed. These two lines interpolated the raw env into an UNQUOTED
|
||||||
|
## `echo`, so (measured against the pre-fix script on this branch's image)
|
||||||
|
## OPCACHE_MEMORY_MB=$'128\nprecision = 7\n; ' put `precision = 7` into
|
||||||
|
## 99-user-opcache.ini and lsphp duly reported `precision => 7`.
|
||||||
|
##
|
||||||
|
## WHP does cast (int) and clamp these before setting the env
|
||||||
|
## (web-files/libs/site-pool-env.php: 32-512 MB, 2000-32000 files) — but
|
||||||
|
## "the panel validates it" is exactly the argument this branch rejected for
|
||||||
|
## `domain`, and the panel is a different repo on a different release cadence.
|
||||||
|
## Validate at the point of use, where the ini is actually generated.
|
||||||
|
##
|
||||||
|
## The accepted ranges below are deliberately a strict SUPERSET of the panel's
|
||||||
|
## clamps (32-512 and 2000-32000), so widening a panel clamp later can never
|
||||||
|
## start silently rejecting real sites here.
|
||||||
|
##
|
||||||
|
## Provenance, stated honestly: max_accelerated_files [200, 1000000] IS PHP's
|
||||||
|
## own clamp. For memory_consumption, 8 is PHP's documented floor but 4096 is
|
||||||
|
## OURS — PHP imposes no upper bound on that directive. It is a typo guard, not
|
||||||
|
## a vendor limit. An out-of-range value is not merely ignored: PHP resets the
|
||||||
|
## directive to its COMPILED default, discarding the image's own
|
||||||
|
## `99-prod-overrides` value, which is a further reason to reject rather than
|
||||||
|
## pass such a value through.
|
||||||
|
OPCACHE_LINES=()
|
||||||
|
if [ -n "${OPCACHE_MEMORY_MB:-}" ]; then
|
||||||
|
validate_ini_num OPCACHE_MEMORY_MB "$OPCACHE_MEMORY_MB" 8 4096
|
||||||
|
if [ -n "$INI_NUM" ]; then
|
||||||
|
OPCACHE_LINES+=("$(printf 'opcache.memory_consumption = "%s"' "$INI_NUM")")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -n "${OPCACHE_MAX_FILES:-}" ]; then
|
||||||
|
validate_ini_num OPCACHE_MAX_FILES "$OPCACHE_MAX_FILES" 200 1000000
|
||||||
|
if [ -n "$INI_NUM" ]; then
|
||||||
|
OPCACHE_LINES+=("$(printf 'opcache.max_accelerated_files = "%s"' "$INI_NUM")")
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ "${#OPCACHE_LINES[@]}" -gt 0 ]; then
|
||||||
{
|
{
|
||||||
echo "; rendered at container start by entrypoint-lsphp.sh"
|
echo "; rendered at container start by entrypoint-lsphp.sh"
|
||||||
echo "; per-site override from WHP whp.sites.opcache_*_override"
|
echo "; per-site override from WHP whp.sites.opcache_*_override"
|
||||||
[ -n "${OPCACHE_MEMORY_MB:-}" ] && echo "opcache.memory_consumption = ${OPCACHE_MEMORY_MB}"
|
printf '%s\n' "${OPCACHE_LINES[@]}"
|
||||||
[ -n "${OPCACHE_MAX_FILES:-}" ] && echo "opcache.max_accelerated_files = ${OPCACHE_MAX_FILES}"
|
|
||||||
} > "$SCAN_DIR/99-user-opcache.ini"
|
} > "$SCAN_DIR/99-user-opcache.ini"
|
||||||
|
else
|
||||||
|
## Nothing valid to say. Remove rather than leave whatever a previous boot
|
||||||
|
## wrote. Defensive only — do not read this as fixing a reachable bug: the
|
||||||
|
## writable layer does outlive a `docker restart`, but so does the
|
||||||
|
## environment, and changing these vars requires a RECREATE, which starts
|
||||||
|
## from a fresh layer with no stale fragment. Kept because it is free, and
|
||||||
|
## because it makes "no valid override" mean the same thing on every boot
|
||||||
|
## regardless of how the container got here.
|
||||||
|
rm -f "$SCAN_DIR/99-user-opcache.ini"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
## No scan dir means none of the per-site ini drop-ins land — including the
|
||||||
|
## path-parity mapping. Previously this failed silently; it must not, because
|
||||||
|
## the tier's cac-fpm parity guarantee is one of the things lost.
|
||||||
|
##
|
||||||
|
## Two different things land here and they are not the same report. "lsphp
|
||||||
|
## reports no additional-ini scan dir" ASSERTS that lsphp answered us, which
|
||||||
|
## is false when the probe produced nothing at all — and that was the wrong
|
||||||
|
## half of the same mistake the parity branch above made: describing a probe
|
||||||
|
## that could not answer as a finding about the image. Say which one it was.
|
||||||
|
if lsphp_info_is_usable "$LSPHP_INFO"; then
|
||||||
|
echo "WARNING: entrypoint-lsphp: lsphp reports no additional-ini scan dir — per-site error_log, opcache and \$_SERVER path-parity settings were NOT applied." >&2
|
||||||
|
PATH_PARITY_MODE="none (no scan dir)"
|
||||||
|
else
|
||||||
|
echo "WARNING: entrypoint-lsphp: '${LSPHP_BIN} -i' produced no usable phpinfo output (${#LSPHP_INFO} bytes) — per-site error_log, opcache and \$_SERVER path-parity settings were NOT applied. This is a PROBE failure and establishes nothing about what the image contains; run '${LSPHP_BIN} -i' in this container before concluding anything about it." >&2
|
||||||
|
PATH_PARITY_MODE="none (lsphp -i unusable)"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "entrypoint-lsphp: \$_SERVER path parity = ${PATH_PARITY_MODE} (${OLS_SITE_PATH} -> /home/${user})"
|
||||||
|
|
||||||
## ---- ownership ----
|
## ---- ownership ----
|
||||||
## Ensure the dirs we created + the log file are customer-owned so lsphp (running
|
## Ensure the dirs we created + the log file are customer-owned so lsphp (running
|
||||||
## as $user) can read code and write logs. Customer content is already
|
## as $user) can read code and write logs. Customer content is already
|
||||||
|
|||||||
@@ -59,11 +59,30 @@ fi
|
|||||||
# Start cron for log rotation
|
# Start cron for log rotation
|
||||||
/usr/sbin/crond
|
/usr/sbin/crond
|
||||||
|
|
||||||
# Tail Apache logs (becomes PID 1 process)
|
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
||||||
|
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
||||||
|
# docker waited the full 10s and SIGKILLed apache mid-request (exit 137), for
|
||||||
|
# every site on the host at once. graceful-stop (WINCH) lets in-flight requests
|
||||||
|
# finish; after 7s it falls back to TERM. Timeouts sum to < 10s so this
|
||||||
|
# completes inside docker's default stop timeout.
|
||||||
|
source /scripts/stop-services.sh
|
||||||
|
TAIL_PID=""
|
||||||
|
SLEEP_PID=""
|
||||||
|
shutdown() {
|
||||||
|
trap - TERM INT
|
||||||
|
echo "entrypoint-shared-httpd: received stop signal, shutting down"
|
||||||
|
stop_service httpd /etc/httpd/run/httpd.pid WINCH 7 '^/usr/sbin/httpd'
|
||||||
|
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
||||||
|
[ -n "$SLEEP_PID" ] && kill "$SLEEP_PID" 2>/dev/null
|
||||||
|
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap shutdown TERM INT
|
||||||
|
|
||||||
|
# Tail Apache logs to docker logs.
|
||||||
# Use a loop to pick up new log files as vhosts are added.
|
# Use a loop to pick up new log files as vhosts are added.
|
||||||
# tail -f only watches files that exist at start time.
|
# tail -f only watches files that exist at start time.
|
||||||
touch /var/log/httpd/error_log
|
touch /var/log/httpd/error_log
|
||||||
TAIL_PID=""
|
|
||||||
while true; do
|
while true; do
|
||||||
LOG_FILES=$(find /var/log/httpd/ -name '*.log' -o -name '*_log' 2>/dev/null | sort)
|
LOG_FILES=$(find /var/log/httpd/ -name '*.log' -o -name '*_log' 2>/dev/null | sort)
|
||||||
if [ -n "$TAIL_PID" ]; then
|
if [ -n "$TAIL_PID" ]; then
|
||||||
@@ -72,6 +91,10 @@ while true; do
|
|||||||
fi
|
fi
|
||||||
tail -f $LOG_FILES &
|
tail -f $LOG_FILES &
|
||||||
TAIL_PID=$!
|
TAIL_PID=$!
|
||||||
# Re-check for new log files every 60 seconds
|
# Re-check for new log files every 60 seconds. Background + `wait` (not a
|
||||||
sleep 60
|
# foreground sleep) so a stop signal runs the trap immediately instead of
|
||||||
|
# after up to 60s.
|
||||||
|
sleep 60 &
|
||||||
|
SLEEP_PID=$!
|
||||||
|
wait "$SLEEP_PID"
|
||||||
done
|
done
|
||||||
@@ -75,7 +75,48 @@ term_handler() {
|
|||||||
}
|
}
|
||||||
trap term_handler TERM INT
|
trap term_handler TERM INT
|
||||||
|
|
||||||
ols_running() { /usr/local/lsws/bin/lswsctrl status 2>/dev/null | grep -qi 'running with pid'; }
|
## NOT `lswsctrl status` (unlike the otherwise-identical function in
|
||||||
|
## entrypoint-litespeed.sh). `lswsctrl` appends a timestamped line to
|
||||||
|
## logs/lsrestart.log on EVERY invocation it makes, including `status` — and
|
||||||
|
## this loop polls every 3s forever. Measured on whp01: lsrestart.log is 96 MB,
|
||||||
|
## holding 1,819,286 `status` lines against 2,429 real `restart` lines; at one
|
||||||
|
## poll per 3s that's ~63 days of continuous polling, which is exactly the
|
||||||
|
## file's age, and it isn't rotated on any host (whp01/whp02/sdbees all growing
|
||||||
|
## at ~1.5 MB/day). So: check liveness directly instead of shelling out to a
|
||||||
|
## tool whose logging is a side effect we don't want on a fixed timer.
|
||||||
|
##
|
||||||
|
## Verified (docker run litespeedtech/openlitespeed:1.8.4-lsphp83, the exact
|
||||||
|
## base this image is built FROM — see Dockerfile.shared-ols): the running main
|
||||||
|
## process shows in `ps` as `openlitespeed (lshttpd - main)`, one PID, always
|
||||||
|
## present while OLS is up and absent the instant it is killed (checked via
|
||||||
|
## `ps aux` immediately after `kill -9` on the main PID). `pgrep -f` matches
|
||||||
|
## against the full command line, and no other process on this image's `ps`
|
||||||
|
## output contains that string, so this cannot cross-match an unrelated
|
||||||
|
## process. It also cannot self-match: pgrep excludes its own PID by default,
|
||||||
|
## and the invoking process here is bash executing this script file, whose own
|
||||||
|
## argv never contains the pattern text (only the *source lines* of this script
|
||||||
|
## do, which `pgrep -f` never sees).
|
||||||
|
##
|
||||||
|
## Deliberately NOT the pidfile (/tmp/lshttpd/lshttpd.pid, confirmed present in
|
||||||
|
## the same probe): pidfiles are known to go stale across a crash (verified —
|
||||||
|
## after `kill -9` the file still held the dead PID), and treating a stale PID
|
||||||
|
## as "alive" if the kernel ever reuses that number is a false positive this
|
||||||
|
## supervisor cannot afford (see below). `pgrep -f` reads the live process
|
||||||
|
## table, so there is no staleness window to reason about.
|
||||||
|
##
|
||||||
|
## Conservative on both failure directions, which matters because this is a
|
||||||
|
## supervisor predicate, not a metric: a false negative makes start_ols() run
|
||||||
|
## `lswsctrl start` against an already-running OLS — verified against the same
|
||||||
|
## probe base image, that is NOT a no-op, it sends SIGUSR1 to the live main
|
||||||
|
## process, i.e. the same graceful self-restart QUIC.cloud IP refreshes trigger
|
||||||
|
## (see entrypoint-litespeed.sh's note on that handoff) — a brief, zero-
|
||||||
|
## downtime blip at worst. A false positive is worse: it leaves a genuinely
|
||||||
|
## dead OLS un-revived until some later poll happens to notice. So if this
|
||||||
|
## predicate is ever in doubt it should err toward reporting "not running", not
|
||||||
|
## "running".
|
||||||
|
ols_running() {
|
||||||
|
pgrep -f 'lshttpd - main' >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
MAX_STARTS=5
|
MAX_STARTS=5
|
||||||
WINDOW=60
|
WINDOW=60
|
||||||
|
|||||||
+57
-23
@@ -10,8 +10,11 @@ fi
|
|||||||
|
|
||||||
adduser -u $uid $user
|
adduser -u $uid $user
|
||||||
|
|
||||||
mkdir -p /home/$user/public_html
|
# Root never follows customer symlinks under /home/$user: see safe-paths.sh.
|
||||||
mkdir -p /home/$user/logs/{apache,php-fpm}
|
source /scripts/safe-paths.sh
|
||||||
|
secure_home
|
||||||
|
|
||||||
|
user_mkdir /home/$user/public_html /home/$user/logs/apache /home/$user/logs/php-fpm
|
||||||
|
|
||||||
mv /var/log/httpd /var/log/httpd.bak
|
mv /var/log/httpd /var/log/httpd.bak
|
||||||
|
|
||||||
@@ -34,14 +37,28 @@ if [ -f /etc/httpd/conf.d/ssl.conf ]; then
|
|||||||
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
|
mv /etc/httpd/conf.d/ssl.conf /etc/httpd/conf.d/ssl.conf.bak
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# httpd and php-fpm open their logs under /home/$user/logs as root: last
|
||||||
|
# fail-closed check immediately before they start (safe-paths.sh).
|
||||||
|
require_safe_logs /home/$user/logs/apache /home/$user/logs/php-fpm
|
||||||
/usr/sbin/httpd -k start
|
/usr/sbin/httpd -k start
|
||||||
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
/usr/sbin/php-fpm -y /etc/php-fpm.conf
|
||||||
|
# GNU chown -R defaults to -P: symlinks are lchown()ed, never followed.
|
||||||
chown -R $user:$user /home/$user
|
chown -R $user:$user /home/$user
|
||||||
chmod -R 755 /home/$user
|
# Do NOT `chmod -R 755` the docroot here. /home/$user is the customer's
|
||||||
|
# bind-mounted site directory; a recursive 755 on every start made every file
|
||||||
|
# (wp-config.php included) world-readable AND executable, and reverted any
|
||||||
|
# tighter permissions the customer had set. Apache (shared-httpd, or the
|
||||||
|
# bundled httpd) runs as `apache`, not the customer, so it only needs to
|
||||||
|
# traverse directories to serve static files. Normalise that and nothing else:
|
||||||
|
# open the docroot itself, and add o+x to directories that lack it. Regular
|
||||||
|
# files are never touched, so execute is never added and wp-config.php is
|
||||||
|
# never loosened. Runs as the customer (safe-paths.sh) so a symlink can only
|
||||||
|
# reach what the customer already owns.
|
||||||
|
open_dir_traversal
|
||||||
|
|
||||||
if [[ $environment == 'DEV' ]]; then
|
if [[ $environment == 'DEV' ]]; then
|
||||||
echo "Starting Dev Deployment"
|
echo "Starting Dev Deployment"
|
||||||
mkdir -p /home/$user/_db_backups
|
user_mkdir /home/$user/_db_backups
|
||||||
# Ensure microdnf is available for installing MariaDB and memcached in DEV mode
|
# Ensure microdnf is available for installing MariaDB and memcached in DEV mode
|
||||||
if ! command -v microdnf &> /dev/null; then
|
if ! command -v microdnf &> /dev/null; then
|
||||||
echo "microdnf not found, installing with dnf..."
|
echo "microdnf not found, installing with dnf..."
|
||||||
@@ -60,14 +77,13 @@ if [[ $environment == 'DEV' ]]; then
|
|||||||
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
|
mysql -e "CREATE USER '"$mysql_user"'@'localhost' IDENTIFIED BY '"$mysql_password"';"
|
||||||
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
|
mysql -e "GRANT ALL PRIVILEGES ON *.* TO '"$mysql_user"'@'localhost' WITH GRANT OPTION;"
|
||||||
mysql -e "FLUSH PRIVILEGES;"
|
mysql -e "FLUSH PRIVILEGES;"
|
||||||
# Create user crontab with MySQL backup job
|
# Create user crontab with MySQL backup job. Written AS THE CUSTOMER
|
||||||
echo "# User crontab for $user" > /home/$user/crontab
|
# (as_user tee), never by root through a customer path.
|
||||||
echo "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" >> /home/$user/crontab
|
drop_symlink /home/$user/crontab
|
||||||
chown $user:$user /home/$user/crontab
|
printf '%s\n' "# User crontab for $user" "*/15 * * * * /scripts/mysql-backup.sh $user devdb_$mysql_db" \
|
||||||
echo "MySQL User: "$mysql_user > /home/$user/mysql_creds
|
| as_user tee /home/$user/crontab >/dev/null
|
||||||
echo "MySQL Password: "$mysql_password >> /home/$user/mysql_creds
|
printf '%s\n' "MySQL User: $mysql_user" "MySQL Password: $mysql_password" "MySQL Database: devdb_$mysql_db" \
|
||||||
echo "MySQL Database: devdb_"$mysql_db >> /home/$user/mysql_creds
|
| as_user tee /home/$user/mysql_creds
|
||||||
cat /home/$user/mysql_creds
|
|
||||||
|
|
||||||
fi
|
fi
|
||||||
/usr/bin/memcached -d -u $user
|
/usr/bin/memcached -d -u $user
|
||||||
@@ -78,19 +94,37 @@ if [[ $environment == 'PROD' ]]; then
|
|||||||
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
|
sed -r -i 's/;session.save_path="localhost:11211/session.save_path="memcache:11211/' /etc/php.d/50-memcached.ini
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Set up user crontab
|
# Set up + load the user crontab, created and read as the customer
|
||||||
if [ ! -f /home/$user/crontab ]; then
|
# (safe-paths.sh). Root reading/writing/chowning it followed symlinks.
|
||||||
echo "# User crontab for $user" > /home/$user/crontab
|
load_user_crontab
|
||||||
echo "# Add your cron jobs here" >> /home/$user/crontab
|
|
||||||
echo "# Example: */5 * * * * /home/$user/scripts/my-script.sh" >> /home/$user/crontab
|
|
||||||
chown $user:$user /home/$user/crontab
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Load user crontab
|
|
||||||
crontab -u $user /home/$user/crontab
|
|
||||||
|
|
||||||
/usr/sbin/crond
|
/usr/sbin/crond
|
||||||
tail -f /var/log/httpd/*
|
|
||||||
|
# Graceful shutdown. bash is PID 1 here, and PID 1 only receives signals it
|
||||||
|
# has a handler for, so without this trap `docker stop`'s SIGTERM was dropped:
|
||||||
|
# docker waited the full 10s and SIGKILLed apache and php-fpm mid-request
|
||||||
|
# (exit 137). Stop apache first (graceful-stop: finish in-flight requests,
|
||||||
|
# accept no new ones), then php-fpm (QUIT: workers finish their current
|
||||||
|
# request), then crond. Timeouts sum to < 10s so this completes inside
|
||||||
|
# docker's default stop timeout.
|
||||||
|
source /scripts/stop-services.sh
|
||||||
|
TAIL_PID=""
|
||||||
|
shutdown() {
|
||||||
|
trap - TERM INT
|
||||||
|
echo "entrypoint: received stop signal, shutting down"
|
||||||
|
stop_service httpd /etc/httpd/run/httpd.pid WINCH 4 '^/usr/sbin/httpd'
|
||||||
|
stop_service php-fpm /run/php-fpm/php-fpm.pid QUIT 3 'php-fpm: master'
|
||||||
|
stop_service crond /run/crond.pid TERM 1 '^/usr/sbin/crond'
|
||||||
|
[ -n "$TAIL_PID" ] && kill "$TAIL_PID" 2>/dev/null
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
trap shutdown TERM INT
|
||||||
|
|
||||||
|
# Stream apache logs to docker logs. Backgrounded + `wait` (instead of a
|
||||||
|
# foreground tail) so bash can run the trap as soon as a signal arrives.
|
||||||
|
tail -f /var/log/httpd/* &
|
||||||
|
TAIL_PID=$!
|
||||||
|
wait "$TAIL_PID"
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
## pkgcache-host-repos.sh — point a WHP HOST's dnf repos (AlmaLinux 9/10 BaseOS/
|
||||||
|
## AppStream/CRB/extras/..., EPEL, Remi) at the internal package cache, with the
|
||||||
|
## ORIGIN as automatic fallback. Not used by image builds (see pkg-cache-repos.sh).
|
||||||
|
##
|
||||||
|
## pkgcache-host-repos.sh apply https://192.168.1.40 # backs up, then rewrites
|
||||||
|
## pkgcache-host-repos.sh status
|
||||||
|
## pkgcache-host-repos.sh revert [BACKUP_DIR] # default: newest backup
|
||||||
|
##
|
||||||
|
## Fallback design (measured 2026-10-21 on AlmaLinux 10.2):
|
||||||
|
## * each rewritten section gets baseurl=<cache>/<prefix>/<path> <origin>/<path>
|
||||||
|
## — dnf/librepo treats the second URL as a failover mirror.
|
||||||
|
## * the section's mirrorlist=/metalink= is COMMENTED OUT. Measured: with a
|
||||||
|
## mirrorlist/metalink present dnf ignores baseurl entirely (0 cache requests),
|
||||||
|
## so "cache first, mirrorlist as fallback" is not something dnf can do.
|
||||||
|
## * gpgcheck=1 everywhere; repo_gpgcheck=1 for AlmaLinux + Remi (both sign
|
||||||
|
## repomd.xml); EPEL has no repomd signature and relies on HTTPS end to end.
|
||||||
|
## * trust: the name-constrained pkg-cache CA is installed as a system anchor
|
||||||
|
## (/etc/pki/ca-trust/source/anchors/pkg-cache-ca.crt). Its nameConstraints
|
||||||
|
## (IP 192.168.1.40, pkg-cache.* DNS) are enforced by OpenSSL — a cert it
|
||||||
|
## signs for any other name fails ("permitted subtree violation").
|
||||||
|
## Every changed file is copied to /root/pkgcache-repo-backup-<UTC>/ first.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODE="${1:-status}"
|
||||||
|
CA_SRC="$(dirname "$0")/pkg-cache-ca.crt"
|
||||||
|
ANCHOR=/etc/pki/ca-trust/source/anchors/pkg-cache-ca.crt
|
||||||
|
die() { echo "FATAL(pkgcache-host-repos): $*" >&2; exit 1; }
|
||||||
|
same() { [ "$(sha256sum < "$1")" = "$(sha256sum < "$2")" ]; } # cmp/diffutils is not on minimal hosts
|
||||||
|
|
||||||
|
rewrite() { # $1 file $2 cache url
|
||||||
|
python3 - "$1" "$2" <<'PY'
|
||||||
|
import re, sys
|
||||||
|
f, C = sys.argv[1], sys.argv[2].rstrip('/')
|
||||||
|
maps = [ # regex on a (commented or live) baseurl, cache prefix, origin base, repomd signed?
|
||||||
|
(r'^#?\s*baseurl\s*=\s*https?://repo\.almalinux\.org/almalinux/(\S+)', 'almalinux', 'https://repo.almalinux.org/almalinux/', True),
|
||||||
|
(r'^#?\s*baseurl\s*=\s*https?://download\.example/pub/epel/(\S+)', 'epel', 'https://dl.fedoraproject.org/pub/epel/', False),
|
||||||
|
(r'^#?\s*baseurl\s*=\s*https?://rpms\.remirepo\.net/(\S+)', 'remi', 'https://rpms.remirepo.net/', True),
|
||||||
|
]
|
||||||
|
out, sec = [], []
|
||||||
|
state = {'hit': False, 'sig': False}
|
||||||
|
def flush():
|
||||||
|
for x in sec:
|
||||||
|
if state['hit']:
|
||||||
|
if re.match(r'^(mirrorlist|metalink)\s*=', x):
|
||||||
|
x = '#' + x
|
||||||
|
elif re.match(r'^(gpgcheck|repo_gpgcheck)\s*=', x):
|
||||||
|
continue
|
||||||
|
out.append(x)
|
||||||
|
if state['hit']:
|
||||||
|
# keep blank-line layout: insert before trailing blanks
|
||||||
|
tail = []
|
||||||
|
while out and out[-1].strip() == '':
|
||||||
|
tail.append(out.pop())
|
||||||
|
out.append('gpgcheck=1')
|
||||||
|
if state['sig']:
|
||||||
|
out.append('repo_gpgcheck=1')
|
||||||
|
out.extend(tail)
|
||||||
|
sec.clear(); state['hit'] = state['sig'] = False
|
||||||
|
for line in open(f).read().split('\n'):
|
||||||
|
if line.startswith('['):
|
||||||
|
flush()
|
||||||
|
for rx, prefix, origin, sig in maps:
|
||||||
|
m = re.match(rx, line)
|
||||||
|
if m:
|
||||||
|
p = m.group(1)
|
||||||
|
line = f'baseurl={C}/{prefix}/{p} {origin}{p}'
|
||||||
|
state['hit'] = True; state['sig'] = state['sig'] or sig
|
||||||
|
break
|
||||||
|
sec.append(line)
|
||||||
|
flush()
|
||||||
|
open(f, 'w').write('\n'.join(out))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
apply)
|
||||||
|
C="${2:?usage: apply https://<cache>}"; C="${C%/}"
|
||||||
|
case "$C" in https://*) ;; *) die "cache URL must be https://" ;; esac
|
||||||
|
[ -s "$CA_SRC" ] || die "missing $CA_SRC (copy keys/pkg-cache/ca.crt next to this script)"
|
||||||
|
B=/root/pkgcache-repo-backup-$(date -u +%Y%m%dT%H%M%SZ)
|
||||||
|
mkdir -p "$B/yum.repos.d"
|
||||||
|
cp -a /etc/yum.repos.d/*.repo "$B/yum.repos.d/"
|
||||||
|
[ -e "$ANCHOR" ] && cp -a "$ANCHOR" "$B/" || touch "$B/.no-anchor-before"
|
||||||
|
install -m 0644 "$CA_SRC" "$ANCHOR" && update-ca-trust
|
||||||
|
for f in /etc/yum.repos.d/almalinux*.repo /etc/yum.repos.d/epel*.repo /etc/yum.repos.d/remi*.repo; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
grep -q -E '^#?\s*baseurl\s*=\s*https?://(repo\.almalinux\.org|download\.example|rpms\.remirepo\.net)/' "$f" && rewrite "$f" "$C"
|
||||||
|
done
|
||||||
|
echo "backup: $B"
|
||||||
|
echo "changed files:"; for f in "$B"/yum.repos.d/*.repo; do same "$f" "/etc/yum.repos.d/$(basename "$f")" || echo " /etc/yum.repos.d/$(basename "$f")"; done
|
||||||
|
;;
|
||||||
|
revert)
|
||||||
|
B="${2:-$(ls -d /root/pkgcache-repo-backup-* 2>/dev/null | sort | tail -1)}"
|
||||||
|
[ -d "$B/yum.repos.d" ] || die "no backup dir ($B)"
|
||||||
|
for f in "$B"/yum.repos.d/*.repo; do same "$f" "/etc/yum.repos.d/$(basename "$f")" || { cp -a "$f" /etc/yum.repos.d/; echo "restored /etc/yum.repos.d/$(basename "$f")"; }; done
|
||||||
|
if [ -e "$B/.no-anchor-before" ]; then rm -f "$ANCHOR"; update-ca-trust; echo "removed $ANCHOR"; fi
|
||||||
|
dnf clean metadata -q || true
|
||||||
|
echo "reverted from $B"
|
||||||
|
;;
|
||||||
|
status)
|
||||||
|
grep -l -E '^baseurl=https://[^ ]+/(almalinux|epel|remi)/' /etc/yum.repos.d/*.repo 2>/dev/null | sed 's/^/via cache: /' || echo "no repo uses the cache"
|
||||||
|
ls -d /root/pkgcache-repo-backup-* 2>/dev/null | sed 's/^/backup: /' || true
|
||||||
|
[ -e "$ANCHOR" ] && echo "CA anchor: $ANCHOR" || echo "CA anchor: absent"
|
||||||
|
;;
|
||||||
|
*) die "usage: apply URL | status | revert [DIR]" ;;
|
||||||
|
esac
|
||||||
Executable
+121
@@ -0,0 +1,121 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
## lsphp-abi-gate.sh — may PHP extensions compiled against OLD_INC be loaded into
|
||||||
|
## a PHP built with NEW_INC? Used by lsphp-src-build.sh (emergency php.net build)
|
||||||
|
## before it keeps LiteSpeed's packaged PECL .so files (redis, imagick,
|
||||||
|
## memcached, igbinary, msgpack) next to a newer PHP patch release.
|
||||||
|
##
|
||||||
|
## WHY: PHP refuses a module whose ZEND_MODULE_API_NO / build-id differ (loud),
|
||||||
|
## but a changed struct layout under the SAME API number loads fine and corrupts
|
||||||
|
## memory (silent). Patch releases are not supposed to change layouts, yet the
|
||||||
|
## php2shell fix (8.x.y, 2026-10-21) did add fields — so this is measured, not
|
||||||
|
## assumed.
|
||||||
|
##
|
||||||
|
## HOW: compile one TU including the public PHP headers (each header in the list
|
||||||
|
## below that exists in BOTH trees) with -g against each tree, dump every struct/
|
||||||
|
## union with pahole (incl. anonymous typedef'd ones), and compare:
|
||||||
|
## * every pre-existing member keeps its offset and size (else FAIL)
|
||||||
|
## * a new member may only use bytes no old member used (else FAIL)
|
||||||
|
## (padding, or appended at the end)
|
||||||
|
## * a struct may change size only if it is *_globals — engine/module globals
|
||||||
|
## are singletons behind one exported symbol (executor_globals, ps_globals);
|
||||||
|
## no extension embeds or allocates them (else FAIL)
|
||||||
|
## A member that is renamed or retyped counts as removed: a false alarm that fails
|
||||||
|
## loud, never a silent pass.
|
||||||
|
##
|
||||||
|
## Usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR (each *_INC contains php/)
|
||||||
|
## Env: ABI_MIN_MEMBERS (default 1000) — fewer members extracted from OLD_INC
|
||||||
|
## means the probe itself is broken; refuse rather than pass vacuously.
|
||||||
|
## Exit 0 = compatible (report on stdout), 1 = incompatible (report on stderr).
|
||||||
|
## Tested by scripts/tests/lsphp-abi-gate.test.sh.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OLD_INC="${1:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
|
||||||
|
NEW_INC="${2:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
|
||||||
|
WORK="${3:?usage: lsphp-abi-gate.sh OLD_INC NEW_INC WORKDIR}"
|
||||||
|
MIN="${ABI_MIN_MEMBERS:-1000}"
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
|
||||||
|
HDRS=(php.h php_ini.h SAPI.h ext/standard/info.h ext/standard/php_var.h
|
||||||
|
ext/standard/php_string.h ext/standard/basic_functions.h ext/standard/url.h
|
||||||
|
ext/standard/php_incomplete_class.h ext/standard/php_smart_string.h
|
||||||
|
ext/session/php_session.h ext/json/php_json.h ext/date/php_date.h
|
||||||
|
ext/hash/php_hash.h ext/random/php_random.h ext/spl/spl_exceptions.h
|
||||||
|
ext/pcre/php_pcre.h zend_exceptions.h zend_interfaces.h zend_smart_str.h
|
||||||
|
zend_closures.h zend_generators.h zend_observer.h zend_attributes.h
|
||||||
|
zend_enum.h zend_weakrefs.h zend_fibers.h zend_extensions.h)
|
||||||
|
|
||||||
|
: >"$WORK/abi.c"
|
||||||
|
for h in "${HDRS[@]}"; do
|
||||||
|
for t in "$OLD_INC/php" "$NEW_INC/php"; do
|
||||||
|
[ -e "$t/$h" ] || [ -e "$t/main/$h" ] || [ -e "$t/Zend/$h" ] || continue 2
|
||||||
|
done
|
||||||
|
printf '#include "%s"\n' "$h" >>"$WORK/abi.c"
|
||||||
|
done
|
||||||
|
[ -s "$WORK/abi.c" ] || { echo "ABI gate: none of the probe headers exist in both trees" >&2; exit 1; }
|
||||||
|
|
||||||
|
dump() { # $1 = include root, $2 = tag
|
||||||
|
local inc="$1/php" o="$WORK/abi-$2.o"
|
||||||
|
gcc -g -O0 -c -fno-eliminate-unused-debug-types \
|
||||||
|
-I"$inc" -I"$inc/main" -I"$inc/TSRM" -I"$inc/Zend" -I"$inc/ext" -I"$inc/ext/date/lib" \
|
||||||
|
-o "$o" "$WORK/abi.c"
|
||||||
|
## M <struct> <decl>#<n> <offset[:bit]> <size> and S <struct> <size>
|
||||||
|
## Named: "struct _zend_object {" ... "};" /* size: N ... */
|
||||||
|
## Anonymous: "typedef struct {" ... "} zend_fcall_info_cache;"
|
||||||
|
pahole -a -F dwarf "$o" | awk '
|
||||||
|
/^(typedef )?(struct|union) ([A-Za-z_0-9]+ )?\{/ {
|
||||||
|
depth = 1; nm = 0; sz = ""; delete seen
|
||||||
|
name = ($1 == "typedef") ? "" : $2; next }
|
||||||
|
depth == 0 { next }
|
||||||
|
depth == 1 && /^\}/ {
|
||||||
|
if (name == "") { n = $0; sub(/^\}[ \t]*/, "", n); sub(/;.*$/, "", n); name = n }
|
||||||
|
for (i = 1; i <= nm; i++) print "M", name, mem[i]
|
||||||
|
if (sz != "") print "S", name, sz
|
||||||
|
depth = 0; next }
|
||||||
|
depth == 1 && /\/\* size: [0-9]+/ { t = $0; sub(/^.*size: /, "", t); sub(/[^0-9].*$/, "", t); sz = t; next }
|
||||||
|
/\{[ \t]*$/ { depth++; next }
|
||||||
|
/^[ \t]+\}/ { depth-- }
|
||||||
|
/;[ \t]*\/\*[ \t]+[0-9]+(:[ \t]*[0-9]+)?[ \t]+[0-9]+[ \t]+\*\// {
|
||||||
|
d = $0; sub(/;.*$/, "", d); gsub(/[ \t]+/, " ", d); sub(/^ /, "", d); gsub(/ /, "_", d)
|
||||||
|
c = $0; sub(/^.*\/\*[ \t]+/, "", c); sub(/[ \t]+\*\/.*$/, "", c); gsub(/:[ \t]+/, ":", c)
|
||||||
|
k = split(c, w, /[ \t]+/)
|
||||||
|
seen[d]++
|
||||||
|
mem[++nm] = d "#" seen[d] " " w[1] " " w[k] }
|
||||||
|
' >"$WORK/abi-$2.txt"
|
||||||
|
}
|
||||||
|
dump "$OLD_INC" old
|
||||||
|
dump "$NEW_INC" new
|
||||||
|
|
||||||
|
NOLD=$(awk '$1 == "M"' "$WORK/abi-old.txt" | wc -l)
|
||||||
|
NSTRUCT=$(awk '$1 == "S"' "$WORK/abi-old.txt" | wc -l)
|
||||||
|
if [ "$NOLD" -lt "$MIN" ]; then
|
||||||
|
echo "ABI gate: only $NOLD struct members extracted from $OLD_INC (< $MIN) — the probe is broken; refusing to call that compatible." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPORT=$(awk '
|
||||||
|
FNR == NR { if ($1 == "M") { old[$2 " " $3] = $4 " " $5; ost[$2] = 1 } else if ($1 == "S") osz[$2] = $3; next }
|
||||||
|
$1 == "M" { new[$2 " " $3] = $4 " " $5; nst[$2] = 1 }
|
||||||
|
$1 == "S" { nsz[$2] = $3 }
|
||||||
|
END {
|
||||||
|
for (k in old) { split(k, p, " "); if (!(p[1] in nst)) continue
|
||||||
|
if (!(k in new)) print "BAD removed/renamed member " k " (was " old[k] ")"
|
||||||
|
else if (old[k] != new[k]) print "BAD moved member " k ": " old[k] " -> " new[k] }
|
||||||
|
for (k in new) { if (k in old) continue; split(k, p, " "); if (!(p[1] in ost)) continue
|
||||||
|
split(new[k], q, " "); s = q[1] + 0; e = s + q[2]
|
||||||
|
hit = ""
|
||||||
|
for (o in old) { split(o, r, " "); if (r[1] != p[1]) continue; split(old[o], z, " ")
|
||||||
|
if (s < z[1] + z[2] && z[1] + 0 < e) { hit = o; break } }
|
||||||
|
if (hit != "") print "BAD new member " k " at " new[k] " overlaps old " hit
|
||||||
|
else print "OK new member " k " at " new[k] " (padding/appended)" }
|
||||||
|
for (st in osz) { if (!(st in nsz) || osz[st] == nsz[st]) continue
|
||||||
|
if (st ~ /_globals$/) print "OK size " st " " osz[st] " -> " nsz[st] " (*_globals singleton)"
|
||||||
|
else print "BAD size " st " " osz[st] " -> " nsz[st] }
|
||||||
|
}' "$WORK/abi-old.txt" "$WORK/abi-new.txt" | sort)
|
||||||
|
|
||||||
|
if [[ $'\n'"$REPORT" == *$'\nBAD '* ]]; then
|
||||||
|
printf '%s\n' "$REPORT" >&2
|
||||||
|
echo "ABI gate: INCOMPATIBLE ($NOLD members / $NSTRUCT structs compared)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ -z "$REPORT" ] || printf '%s\n' "$REPORT"
|
||||||
|
echo "ABI gate: compatible ($NOLD members / $NSTRUCT structs compared)"
|
||||||
Executable
+102
@@ -0,0 +1,102 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
## lsphp-apt-upgrade.sh — BUILD-TIME ONLY. Brings every installed lsphp${PHPVER}*
|
||||||
|
## package (and, by default, the Ubuntu OS packages) up to what the repos offer
|
||||||
|
## RIGHT NOW, then proves it.
|
||||||
|
##
|
||||||
|
## Why this exists: cac-litespeed / cac-lsphp start FROM the LiteSpeed prebuilt
|
||||||
|
## image litespeedtech/openlitespeed:<OLS>-lsphpNN, which LiteSpeed republishes
|
||||||
|
## rarely (the 1.8.4 tags were built 2025-12-16 and ship lsphp85 8.5.0 /
|
||||||
|
## lsphp84 8.4.15, measured 2026-10-21, while rpms.litespeedtech.com/debian
|
||||||
|
## offers 8.5.11 / 8.4.26). The base image already carries that apt repo, so a
|
||||||
|
## PHP security release reaches us the moment LiteSpeed publishes the .deb —
|
||||||
|
## provided every build actually asks for it. Before this script the lsphp core
|
||||||
|
## only moved forward as a SIDE EFFECT of `apt-get install lsphpNN-ldap` (whose
|
||||||
|
## exact-version dependency on lsphpNN-common drags the family along); lsphp
|
||||||
|
## extension packages without that coupling (e.g. lsphpNN-msgpack) and every
|
||||||
|
## Ubuntu package stayed at the base image's December versions.
|
||||||
|
##
|
||||||
|
## Fails LOUDLY — never ships the base image's PHP by accident:
|
||||||
|
## * `apt-get update --error-on=any`: plain `apt-get update` only WARNS when a
|
||||||
|
## repo is unreachable and carries on with no/old lists, exit 0.
|
||||||
|
## * the LiteSpeed repo must be the candidate source for lsphp${PHPVER}.
|
||||||
|
## * after upgrading, no lsphp${PHPVER}* package may remain upgradable.
|
||||||
|
## * if CI passed the version it resolved from the repo index (EXPECT_LSPHP_PKG,
|
||||||
|
## EXPECT_PHP_VERSION), the installed package / running PHP must match it;
|
||||||
|
## that same value becomes the image LABEL, so the label cannot lie.
|
||||||
|
##
|
||||||
|
## Usage: lsphp-apt-upgrade.sh [--no-os-upgrade] PHPVER [extra apt packages / pkg=ver pins...]
|
||||||
|
## Extra packages are installed in the same transaction set (after the OS
|
||||||
|
## upgrade, before the lsphp sweep). Does NOT clean apt lists — the caller
|
||||||
|
## decides (multi-stage builders want them; shipped layers delete them).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
OS_UPGRADE=1
|
||||||
|
if [ "${1:-}" = "--no-os-upgrade" ]; then OS_UPGRADE=0; shift; fi
|
||||||
|
PHPVER="${1:?usage: lsphp-apt-upgrade.sh [--no-os-upgrade] PHPVER [pkgs...]}"
|
||||||
|
shift
|
||||||
|
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
APT=(apt-get -y -o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold)
|
||||||
|
LSPHP="/usr/local/lsws/lsphp${PHPVER}/bin/lsphp"
|
||||||
|
PHPCLI="/usr/local/lsws/lsphp${PHPVER}/bin/php"
|
||||||
|
|
||||||
|
die() { echo "FATAL(lsphp-apt-upgrade): $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
echo "lsphp-apt-upgrade: PHPVER=${PHPVER} os_upgrade=${OS_UPGRADE} cache_bust=${CACHE_BUST:-<none>}"
|
||||||
|
echo "lsphp-apt-upgrade: base image lsphp${PHPVER}=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}" 2>/dev/null || echo none)"
|
||||||
|
|
||||||
|
"${APT[@]}" update --error-on=any || die "apt-get update failed (repo unreachable?). Refusing to build with stale package lists."
|
||||||
|
|
||||||
|
# The candidate for lsphpNN must come from the LiteSpeed repo — not "none", and
|
||||||
|
# not merely whatever is already installed.
|
||||||
|
# Read each output into a variable and match in the shell: piping into an
|
||||||
|
# early-exit reader (grep -q, awk ... exit) under pipefail can SIGPIPE the writer
|
||||||
|
# and report "missing" for a package that is there (scripts/tests/lsphp-info-probe.test.sh).
|
||||||
|
MADISON=$(apt-cache madison "lsphp${PHPVER}")
|
||||||
|
if [[ "$MADISON" != *rpms.litespeedtech.com* ]]; then
|
||||||
|
apt-cache policy "lsphp${PHPVER}" >&2 || true
|
||||||
|
die "the LiteSpeed apt repo does not offer lsphp${PHPVER} (repo missing from sources, or its index is empty)."
|
||||||
|
fi
|
||||||
|
POLICY=$(apt-cache policy "lsphp${PHPVER}")
|
||||||
|
CAND=$(awk '/Candidate:/ {print $2; exit}' <<<"$POLICY")
|
||||||
|
echo "lsphp-apt-upgrade: repo candidate lsphp${PHPVER}=${CAND}"
|
||||||
|
|
||||||
|
if [ "$OS_UPGRADE" = 1 ]; then
|
||||||
|
# --with-new-pkgs: let an upgrade pull in a new dependency instead of being
|
||||||
|
# silently "kept back". Never removes packages (that would be dist-upgrade).
|
||||||
|
"${APT[@]}" upgrade --with-new-pkgs
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $# -gt 0 ]; then
|
||||||
|
"${APT[@]}" install --no-install-recommends "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sweep EVERY installed lsphpNN* package (core + all extensions) to the candidate.
|
||||||
|
mapfile -t PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' "lsphp${PHPVER}*" 2>/dev/null | awk '$1 == "ii" {print $2}')
|
||||||
|
[ "${#PKGS[@]}" -gt 0 ] || die "no lsphp${PHPVER}* packages installed — wrong base image?"
|
||||||
|
"${APT[@]}" install --only-upgrade --no-install-recommends "${PKGS[@]}"
|
||||||
|
|
||||||
|
LEFT=$(apt list --upgradable 2>/dev/null | grep "^lsphp${PHPVER}" || true)
|
||||||
|
[ -z "$LEFT" ] || die "lsphp packages still upgradable after the sweep:
|
||||||
|
$LEFT"
|
||||||
|
|
||||||
|
INST=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}")
|
||||||
|
RUNV=$("$PHPCLI" -r 'echo PHP_VERSION;') || die "lsphp${PHPVER} CLI does not run"
|
||||||
|
|
||||||
|
if [ -n "${EXPECT_LSPHP_PKG:-}" ] && [ "$INST" != "$EXPECT_LSPHP_PKG" ]; then
|
||||||
|
die "installed lsphp${PHPVER}=$INST but CI resolved $EXPECT_LSPHP_PKG from the repo index (repo changed mid-build? re-run)."
|
||||||
|
fi
|
||||||
|
if [ -n "${EXPECT_PHP_VERSION:-}" ] && [ "$RUNV" != "$EXPECT_PHP_VERSION" ]; then
|
||||||
|
die "lsphp${PHPVER} reports PHP $RUNV but the image will be labelled $EXPECT_PHP_VERSION."
|
||||||
|
fi
|
||||||
|
if [ -z "${EXPECT_PHP_VERSION:-}" ]; then
|
||||||
|
echo "lsphp-apt-upgrade: WARNING no EXPECT_PHP_VERSION build-arg — image label will read 'unverified' (fine for local builds; CI always sets it)." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$RUNV" > /etc/cac-php-version
|
||||||
|
|
||||||
|
echo "lsphp-apt-upgrade: ===== version report ====="
|
||||||
|
dpkg-query -W -f=' ${Package} ${Version}\n' "lsphp${PHPVER}*"
|
||||||
|
echo " lsphp -v: $("$LSPHP" -v | head -n1)"
|
||||||
|
echo " OS packages still upgradable: $(apt list --upgradable 2>/dev/null | grep -vc '^Listing' || true)"
|
||||||
|
echo "lsphp-apt-upgrade: OK lsphp${PHPVER}=${INST} (PHP ${RUNV})"
|
||||||
Executable
+304
@@ -0,0 +1,304 @@
|
|||||||
|
#!/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}"
|
||||||
|
[[ "$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."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
BRANCH="${PHPVER:0:1}.${PHPVER:1}" # 85 -> 8.5
|
||||||
|
[[ "$PHP_SRC_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "PHP_SRC_VERSION='$PHP_SRC_VERSION' is not X.Y.Z (RCs/alphas are refused)."
|
||||||
|
[ "${PHP_SRC_VERSION%.*}" = "$BRANCH" ] || die "PHP_SRC_VERSION=$PHP_SRC_VERSION does not belong to PHPVER=$PHPVER (branch $BRANCH)."
|
||||||
|
[[ "${PHP_SRC_SHA256:-}" =~ ^[0-9a-f]{64}$ ]] || die "PHP_SRC_SHA256 must be the 64-hex sha256 of php-${PHP_SRC_VERSION}.tar.xz from the php.net releases JSON."
|
||||||
|
|
||||||
|
P="/usr/local/lsws/lsphp${PHPVER}"
|
||||||
|
START=$(date +%s)
|
||||||
|
|
||||||
|
## ---- 1. toolchain + every -dev library the packaged configure line needs -----
|
||||||
|
## Same script as the shipped stage: refuses stale/unreachable repo lists and
|
||||||
|
## brings lsphp${PHPVER}* (core + -dev) to the repo's current release, so the
|
||||||
|
## Configure Command we read is the one the shipped stage's packages carry
|
||||||
|
## (lsphp-src-overlay.sh re-checks that the versions match).
|
||||||
|
## -dev packages: one per library in the packaged Configure Command. Extras are
|
||||||
|
## harmless; a missing one makes configure fail, which is the loud outcome.
|
||||||
|
DEVPKGS=(
|
||||||
|
build-essential autoconf pkg-config file dwarves
|
||||||
|
curl ca-certificates gpg gpgv xz-utils
|
||||||
|
libxml2-dev libssl-dev libcurl4-openssl-dev libenchant-2-dev libgmp-dev
|
||||||
|
libonig-dev libedit-dev libbz2-dev libxslt1-dev libzip-dev libsodium-dev
|
||||||
|
libargon2-dev libjpeg-dev libxpm-dev libpng-dev libwebp-dev libfreetype-dev
|
||||||
|
libldap-dev libsasl2-dev libpq-dev libsnmp-dev libsqlite3-dev freetds-dev
|
||||||
|
libtidy-dev libicu-dev zlib1g-dev libkrb5-dev
|
||||||
|
)
|
||||||
|
case "$PHPVER" in
|
||||||
|
81) DEVPKGS+=(libc-client2007e-dev libpspell-dev) ;; # --with-imap, --with-pspell
|
||||||
|
82|83) DEVPKGS+=(libpspell-dev) ;; # --with-pspell (gone in 8.4)
|
||||||
|
esac
|
||||||
|
/usr/local/sbin/lsphp-apt-upgrade.sh "${PHPVER}" "${DEVPKGS[@]}" "lsphp${PHPVER}-dev"
|
||||||
|
PKGV=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}")
|
||||||
|
DEVV=$(dpkg-query -W -f='${Version}' "lsphp${PHPVER}-dev")
|
||||||
|
[ "$PKGV" = "$DEVV" ] || die "lsphp${PHPVER}=$PKGV but lsphp${PHPVER}-dev=$DEVV — packaged headers do not match the packaged binary."
|
||||||
|
PKG_PHPV=$("$P/bin/php" -r 'echo PHP_VERSION;')
|
||||||
|
log "packaged reference: lsphp${PHPVER} ${PKGV} (PHP ${PKG_PHPV}); building php.net ${PHP_SRC_VERSION}"
|
||||||
|
if [ "$(printf '%s\n%s\n' "$PKG_PHPV" "$PHP_SRC_VERSION" | sort -V | tail -n1)" != "$PHP_SRC_VERSION" ] \
|
||||||
|
|| [ "$PKG_PHPV" = "$PHP_SRC_VERSION" ]; then
|
||||||
|
die "PHP_SRC_VERSION=$PHP_SRC_VERSION is not NEWER than the packaged $PKG_PHPV — the emergency path is pointless (or a downgrade). Use the normal build."
|
||||||
|
fi
|
||||||
|
|
||||||
|
## ---- 2. fetch + verify -------------------------------------------------------
|
||||||
|
WORK=/usr/src/php-src
|
||||||
|
mkdir -p "$WORK"
|
||||||
|
cd "$WORK"
|
||||||
|
TARBALL="php-${PHP_SRC_VERSION}.tar.xz"
|
||||||
|
for f in "$TARBALL" "$TARBALL.asc"; do
|
||||||
|
curl -fsSL --retry 3 --max-time 600 -o "$f" "https://www.php.net/distributions/$f" \
|
||||||
|
|| die "download of https://www.php.net/distributions/$f failed."
|
||||||
|
done
|
||||||
|
|
||||||
|
GOT=$(sha256sum "$TARBALL" | awk '{print $1}')
|
||||||
|
[ "$GOT" = "$PHP_SRC_SHA256" ] || die "sha256 mismatch for $TARBALL: got $GOT, expected $PHP_SRC_SHA256 (php.net releases JSON)."
|
||||||
|
log "sha256 OK $GOT"
|
||||||
|
|
||||||
|
[ -s "$KEYDIR/php-keyring.asc" ] || die "pinned keyring $KEYDIR/php-keyring.asc missing."
|
||||||
|
## The committed file is php.net's keyring byte-for-byte (ASCII-armoured); gpgv
|
||||||
|
## only reads binary keyrings.
|
||||||
|
gpg --batch --yes --dearmor -o "$WORK/php-keyring.gpg" "$KEYDIR/php-keyring.asc" || die "could not dearmor $KEYDIR/php-keyring.asc."
|
||||||
|
[ -s "$KEYDIR/release-managers.txt" ] || die "pinned fingerprint list $KEYDIR/release-managers.txt missing."
|
||||||
|
STATUS=$(gpgv --status-fd 1 --keyring "$WORK/php-keyring.gpg" "$TARBALL.asc" "$TARBALL" 2>"$WORK/gpgv.err") || {
|
||||||
|
cat "$WORK/gpgv.err" >&2; die "OpenPGP signature verification of $TARBALL FAILED."; }
|
||||||
|
cat "$WORK/gpgv.err"
|
||||||
|
## [GNUPG:] VALIDSIG <signing-key-fpr> <date> <ts> <expire> <ver> <rsv> <pk-algo> <hash> <class> <PRIMARY-fpr>
|
||||||
|
PRIMARY=$(awk '$1 == "[GNUPG:]" && $2 == "VALIDSIG" {print $NF}' <<<"$STATUS")
|
||||||
|
[ -n "$PRIMARY" ] || die "gpgv reported no VALIDSIG for $TARBALL."
|
||||||
|
ALLOWED=$(awk -v b="$BRANCH" '$1 == b {print $2}' "$KEYDIR/release-managers.txt")
|
||||||
|
[ -n "$ALLOWED" ] || die "no release-manager fingerprints pinned for PHP $BRANCH in release-managers.txt."
|
||||||
|
OK=0
|
||||||
|
while read -r fp; do [ "$fp" = "$PRIMARY" ] && OK=1; done <<<"$ALLOWED"
|
||||||
|
[ "$OK" = 1 ] || die "$TARBALL is validly signed by $PRIMARY, which is NOT a pinned PHP $BRANCH release manager (release-managers.txt)."
|
||||||
|
log "signature OK: primary key $PRIMARY is a pinned PHP $BRANCH release manager"
|
||||||
|
|
||||||
|
tar -xJf "$TARBALL"
|
||||||
|
SRC="$WORK/php-${PHP_SRC_VERSION}"
|
||||||
|
grep -F "PHP ${PHP_SRC_VERSION}" "$SRC/NEWS" >/dev/null || die "NEWS in $TARBALL has no 'PHP ${PHP_SRC_VERSION}' entry."
|
||||||
|
|
||||||
|
## ---- 2b. LiteSpeed's LSAPI SAPI, as the packages use (keys/php-src/litespeed-sapi.txt)
|
||||||
|
read -r SAPI_URL SAPI_SHA < <(awk '!/^#/ && NF == 2 {print; exit}' "$KEYDIR/litespeed-sapi.txt")
|
||||||
|
[ -n "${SAPI_URL:-}" ] && [[ "${SAPI_SHA:-}" =~ ^[0-9a-f]{64}$ ]] || die "no pinned LiteSpeed SAPI in $KEYDIR/litespeed-sapi.txt."
|
||||||
|
curl -fsSL --retry 3 --max-time 300 -o "$WORK/lsapi.tgz" "$SAPI_URL" || die "download of $SAPI_URL failed."
|
||||||
|
GOT=$(sha256sum "$WORK/lsapi.tgz" | awk '{print $1}')
|
||||||
|
[ "$GOT" = "$SAPI_SHA" ] || die "LiteSpeed SAPI $SAPI_URL sha256 $GOT != pinned $SAPI_SHA (LiteSpeed republished it? re-measure against the packaged lsphp before re-pinning)."
|
||||||
|
mkdir -p "$WORK/lsapi"
|
||||||
|
tar -xzf "$WORK/lsapi.tgz" -C "$WORK/lsapi"
|
||||||
|
## Per its README: copy only the *.c/*.h over php-src's sapi/litespeed and keep
|
||||||
|
## php-src's config.m4 / Makefile.frag / arginfo.
|
||||||
|
N=0
|
||||||
|
for f in "$WORK"/lsapi/*/*.c "$WORK"/lsapi/*/*.h; do
|
||||||
|
[ -f "$f" ] || continue
|
||||||
|
install -m 0644 "$f" "$SRC/sapi/litespeed/$(basename "$f")"; N=$((N + 1))
|
||||||
|
done
|
||||||
|
[ "$N" -ge 4 ] || die "LiteSpeed SAPI archive had only $N .c/.h files."
|
||||||
|
log "LiteSpeed SAPI $(basename "$SAPI_URL") (sha256 OK) replaces php-src sapi/litespeed sources ($N files)"
|
||||||
|
|
||||||
|
## ---- 2c. compiled-in ini defaults LiteSpeed patched ----------------------------
|
||||||
|
## Measured 2026-10-21 with `php -n`: LiteSpeed's lsphp82/83 carry Debian's
|
||||||
|
## session.save_path patch (default '/var/lib/php/sessions' instead of php-src's
|
||||||
|
## ''); lsphp85 does not. Reproduce whatever the packaged binary reports. Any
|
||||||
|
## OTHER compiled-in default that differs is caught by the gate in step 4b.
|
||||||
|
PKG_SAVE_PATH=$("$P/bin/php" -n -r 'echo ini_get("session.save_path");')
|
||||||
|
if [ -n "$PKG_SAVE_PATH" ]; then
|
||||||
|
[[ "$PKG_SAVE_PATH" =~ ^[A-Za-z0-9/._-]+$ ]] || die "unexpected packaged session.save_path default '$PKG_SAVE_PATH'."
|
||||||
|
sed -i -E 's|(STD_PHP_INI_ENTRY\("session\.save_path",[[:space:]]*)""|\1"'"$PKG_SAVE_PATH"'"|' "$SRC/ext/session/session.c"
|
||||||
|
grep -F "\"session.save_path\"" "$SRC/ext/session/session.c" | grep -F "\"$PKG_SAVE_PATH\"" >/dev/null \
|
||||||
|
|| die "could not apply the packaged session.save_path default '$PKG_SAVE_PATH' to ext/session/session.c."
|
||||||
|
log "compiled-in session.save_path default set to '$PKG_SAVE_PATH' (as the packaged lsphp${PHPVER})"
|
||||||
|
fi
|
||||||
|
|
||||||
|
## ---- 3. replay the packaged configure line -----------------------------------
|
||||||
|
CFG_LINE=$("$P/bin/php" -i | awk -F' => ' '$1 == "Configure Command " || $1 == "Configure Command" {sub(/^[^=]*=> /, ""); print}')
|
||||||
|
[ -n "$CFG_LINE" ] || die "could not read 'Configure Command' from the packaged $P/bin/php -i."
|
||||||
|
## xargs does the shell-style single-quote splitting without an eval.
|
||||||
|
mapfile -t CFG_ARGS < <(printf '%s\n' "$CFG_LINE" | xargs -n1 printf '%s\n')
|
||||||
|
[ "${CFG_ARGS[0]}" = "./configure" ] || die "unexpected Configure Command shape (first word '${CFG_ARGS[0]}')."
|
||||||
|
CFG_ARGS=("${CFG_ARGS[@]:1}")
|
||||||
|
|
||||||
|
ARGS=()
|
||||||
|
PKG_CFLAGS=""
|
||||||
|
for a in "${CFG_ARGS[@]}"; do
|
||||||
|
case "$a" in
|
||||||
|
CFLAGS=*) PKG_CFLAGS="${a#CFLAGS=}" ;;
|
||||||
|
*) ARGS+=("$a") ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
[ -n "$PKG_CFLAGS" ] || die "packaged Configure Command carries no CFLAGS — refusing to guess."
|
||||||
|
CFLAGS=""
|
||||||
|
for w in $PKG_CFLAGS; do
|
||||||
|
case "$w" in -ffile-prefix-map=*|-fdebug-prefix-map=*) continue ;; esac
|
||||||
|
CFLAGS+="${CFLAGS:+ }$w"
|
||||||
|
done
|
||||||
|
CFLAGS+=" -ffile-prefix-map=${SRC}=."
|
||||||
|
export DEB_BUILD_MAINT_OPTIONS=hardening=+all
|
||||||
|
CPPFLAGS=$(dpkg-buildflags --get CPPFLAGS)
|
||||||
|
LDFLAGS=""
|
||||||
|
for w in $(dpkg-buildflags --get LDFLAGS); do
|
||||||
|
## CFLAGS ends in -fno-lto (LiteSpeed's choice); keep the link step consistent.
|
||||||
|
case "$w" in -flto*|-ffat-lto-objects) continue ;; esac
|
||||||
|
LDFLAGS+="${LDFLAGS:+ }$w"
|
||||||
|
done
|
||||||
|
CXXFLAGS="$CFLAGS"
|
||||||
|
export CFLAGS CXXFLAGS CPPFLAGS LDFLAGS
|
||||||
|
log "configure args (from packaged lsphp${PHPVER} ${PKGV}): ${ARGS[*]}"
|
||||||
|
log "CFLAGS=$CFLAGS"
|
||||||
|
log "CPPFLAGS=$CPPFLAGS"
|
||||||
|
log "LDFLAGS=$LDFLAGS"
|
||||||
|
|
||||||
|
## Build-environment shim (builder stage only, never shipped): ext/pdo_dblib's
|
||||||
|
## config.m4 looks for libsybdb in $prefix/lib, not the multiarch dir Ubuntu's
|
||||||
|
## freetds-dev uses. pdo_dblib is built but not shipped (the packaged image has
|
||||||
|
## no lsphpNN-dblib), exactly as in the packaged build.
|
||||||
|
MA=$(dpkg-architecture -qDEB_HOST_MULTIARCH)
|
||||||
|
[ -e /usr/lib/libsybdb.so ] || ln -s "/usr/lib/$MA/libsybdb.so" /usr/lib/libsybdb.so
|
||||||
|
|
||||||
|
cd "$SRC"
|
||||||
|
if ! ./configure "${ARGS[@]}" "CFLAGS=$CFLAGS" >"$WORK/configure.log" 2>&1; then
|
||||||
|
tail -n 60 "$WORK/configure.log" >&2
|
||||||
|
die "configure FAILED with the packaged flags — most likely a -dev package missing from DEVPKGS in this script (see the error above)."
|
||||||
|
fi
|
||||||
|
## Unknown options are expected for a handful of historic flags LiteSpeed still
|
||||||
|
## passes (--enable-gd-native-ttf, --with-vpx-dir, ...); they are just as unknown
|
||||||
|
## to the packaged build. Print them so a NEW unknown flag is visible in the log.
|
||||||
|
grep -E "^configure: WARNING: unrecognized options" "$WORK/configure.log" || true
|
||||||
|
|
||||||
|
T0=$(date +%s)
|
||||||
|
make -j"$(nproc)" >"$WORK/make.log" 2>&1 || { tail -n 80 "$WORK/make.log" >&2; die "make FAILED."; }
|
||||||
|
log "make took $(( $(date +%s) - T0 ))s on $(nproc) CPUs"
|
||||||
|
|
||||||
|
STAGE="$WORK/stage"
|
||||||
|
make install INSTALL_ROOT="$STAGE" >"$WORK/install.log" 2>&1 || { tail -n 40 "$WORK/install.log" >&2; die "make install FAILED."; }
|
||||||
|
SP="$STAGE$P"
|
||||||
|
[ -x "$SP/bin/lsphp${BRANCH}" ] || die "make install produced no $P/bin/lsphp${BRANCH} (did --enable-litespeed vanish from the packaged flags?)."
|
||||||
|
[ -x "$SP/bin/php${BRANCH}" ] || die "make install produced no $P/bin/php${BRANCH}."
|
||||||
|
## --program-suffix names everything <tool>8.M; LiteSpeed's packages ship the
|
||||||
|
## SAPI as bin/lsphp and the CLI as both bin/php and bin/php8.M (two identical
|
||||||
|
## regular files, measured). Reproduce those names; the overlay copies only the
|
||||||
|
## files the packaged image actually has.
|
||||||
|
mv "$SP/bin/lsphp${BRANCH}" "$SP/bin/lsphp"
|
||||||
|
cp -p "$SP/bin/php${BRANCH}" "$SP/bin/php"
|
||||||
|
|
||||||
|
## Strip like dh_strip does for the packaged binaries (measured: no .comment /
|
||||||
|
## debug sections in the packaged lsphp).
|
||||||
|
find "$SP" -type f \( -name '*.so' -o -path "*/bin/*" \) -print0 | while IFS= read -r -d '' f; do
|
||||||
|
case "$(file -b "$f")" in
|
||||||
|
ELF*shared\ object*|ELF*pie\ executable*|ELF*executable*)
|
||||||
|
strip --remove-section=.comment --remove-section=.note --strip-unneeded "$f" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
NEWV=$("$SP/bin/php${BRANCH}" -n -r 'echo PHP_VERSION;' 2>/dev/null || true)
|
||||||
|
[ "$NEWV" = "$PHP_SRC_VERSION" ] || die "built php reports '$NEWV', expected $PHP_SRC_VERSION."
|
||||||
|
|
||||||
|
## ---- 4b. compiled-in ini defaults must match the packaged binary ---------------
|
||||||
|
## `php -n` = no php.ini, no scan dir, no shared extensions: what is left is
|
||||||
|
## what the binary itself defaults to. New directives (a security release may
|
||||||
|
## add one, e.g. memory_hard_limit_percent in 8.x.y php2shell fix) are allowed
|
||||||
|
## and printed; a removed or changed default fails.
|
||||||
|
ini_defaults() { "$1" -n -r 'foreach (ini_get_all(null, false) as $k => $v) echo $k, "=", var_export($v, true), "\n";' | sort; }
|
||||||
|
INI_PKG=$(ini_defaults "$P/bin/php")
|
||||||
|
INI_NEW=$(ini_defaults "$SP/bin/php${BRANCH}")
|
||||||
|
INI_BAD=$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") | grep '^<' || true)
|
||||||
|
INI_ADD=$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") | grep '^>' || true)
|
||||||
|
[ -z "$INI_BAD" ] || die "compiled-in ini defaults differ from the packaged lsphp${PHPVER} (< packaged, > php.net build):
|
||||||
|
$(diff <(printf '%s\n' "$INI_PKG") <(printf '%s\n' "$INI_NEW") || true)
|
||||||
|
LiteSpeed patched a default this script does not reproduce (see step 2c)."
|
||||||
|
log "compiled-in ini defaults identical to packaged ($(printf '%s\n' "$INI_PKG" | wc -l) directives)${INI_ADD:+; new in $PHP_SRC_VERSION: $(printf '%s' "$INI_ADD" | tr '\n' ' ')}"
|
||||||
|
|
||||||
|
## Same LSAPI SAPI as the packaged binary? Compare the LSAPI_*/LSCAPI_* names
|
||||||
|
## (its env knobs + feature switches) compiled into each. A difference means
|
||||||
|
## LiteSpeed moved to a SAPI release we have not pinned (or the copy failed).
|
||||||
|
lsapi_names() { grep -aoE 'LS(C)?API_[A-Z0-9_]+' "$1" | sort -u; }
|
||||||
|
LSAPI_DIFF=$(diff <(lsapi_names "$P/bin/lsphp") <(lsapi_names "$SP/bin/lsphp") || true)
|
||||||
|
[ -z "$LSAPI_DIFF" ] || die "LSAPI SAPI differs from the packaged lsphp (< packaged, > php.net build):
|
||||||
|
$LSAPI_DIFF"
|
||||||
|
log "LSAPI SAPI matches the packaged lsphp ($(lsapi_names "$SP/bin/lsphp" | wc -l) LSAPI_* names identical)"
|
||||||
|
|
||||||
|
## ---- 4. ABI gate: may LiteSpeed's packaged PECL .so files load into this? -----
|
||||||
|
## See scripts/lsphp-abi-gate.sh (and its test). Measured for 8.5.11 -> 8.5.12:
|
||||||
|
## the php2shell fix adds CG(interned_strings_fallible) in padding and appends
|
||||||
|
## EG(delayed_oom) / PS(in_rfc1867_update); no existing member moved.
|
||||||
|
ABI_OUT=$(bash "$HERE/lsphp-abi-gate.sh" "$P/include" "$SP/include" "$WORK/abi" 2>&1) || {
|
||||||
|
printf '%s\n' "$ABI_OUT" >&2
|
||||||
|
die "ABI gate: PHP $PHP_SRC_VERSION changed struct layouts relative to the packaged $PKG_PHPV headers. LiteSpeed's packaged PECL .so files (redis, imagick, memcached, igbinary, msgpack) were compiled against the old layout and must NOT be loaded into this build — they would have to be rebuilt from source, which this emergency path does not do (docs/lsphp-source-emergency.md)."
|
||||||
|
}
|
||||||
|
printf '%s\n' "$ABI_OUT" | sed 's/^/lsphp-src-build: /'
|
||||||
|
|
||||||
|
## ---- 5. hand-off ----------------------------------------------------------------
|
||||||
|
mkdir -p "$OUT/root"
|
||||||
|
cp -a "$STAGE/." "$OUT/root/"
|
||||||
|
cat >"$OUT/meta.env" <<EOF
|
||||||
|
LSPHP_SRC_PHP_VERSION=$PHP_SRC_VERSION
|
||||||
|
LSPHP_SRC_SHA256=$PHP_SRC_SHA256
|
||||||
|
LSPHP_SRC_SIGNER=$PRIMARY
|
||||||
|
LSPHP_SRC_PKG_REFERENCE=$PKGV
|
||||||
|
LSPHP_SRC_PKG_PHP_VERSION=$PKG_PHPV
|
||||||
|
EOF
|
||||||
|
printf '%s\n' "${ARGS[@]}" >"$OUT/configure-args.txt"
|
||||||
|
log "done in $(( $(date +%s) - START ))s: php.net ${PHP_SRC_VERSION} staged at $OUT/root (reference lsphp${PHPVER} ${PKGV})"
|
||||||
Executable
+203
@@ -0,0 +1,203 @@
|
|||||||
|
#!/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
|
||||||
|
[[ "$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]: $*"; }
|
||||||
|
|
||||||
|
LSPHP_SOURCE="${LSPHP_SOURCE:-litespeed-apt}"
|
||||||
|
if [ ! -f "$OUT/meta.env" ]; then
|
||||||
|
[ -z "${PHP_SRC_VERSION:-}" ] || die "PHP_SRC_VERSION=$PHP_SRC_VERSION but no staged php.net build in $OUT (lsphp-src stage did not run?)."
|
||||||
|
[ "$LSPHP_SOURCE" = "litespeed-apt" ] || die "LSPHP_SOURCE=$LSPHP_SOURCE requires PHP_SRC_VERSION."
|
||||||
|
log "no php.net build staged — packaged lsphp${PHPVER} kept as is."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
[ "$LSPHP_SOURCE" = "php.net" ] || die "a php.net build is staged but LSPHP_SOURCE=$LSPHP_SOURCE (must be 'php.net' so the image label tells the truth)."
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
. "$OUT/meta.env"
|
||||||
|
[ "${PHP_SRC_VERSION:-}" = "$LSPHP_SRC_PHP_VERSION" ] || die "staged build is $LSPHP_SRC_PHP_VERSION but PHP_SRC_VERSION=${PHP_SRC_VERSION:-<empty>}."
|
||||||
|
|
||||||
|
BRANCH="${PHPVER:0:1}.${PHPVER:1}"
|
||||||
|
P="/usr/local/lsws/lsphp${PHPVER}"
|
||||||
|
SRCROOT="$OUT/root"
|
||||||
|
PROBE=/usr/local/share/lsphp-src-probe.php
|
||||||
|
[ -f "$PROBE" ] || die "$PROBE missing."
|
||||||
|
|
||||||
|
CORE="lsphp${PHPVER}"
|
||||||
|
COREV=$(dpkg-query -W -f='${Version}' "$CORE")
|
||||||
|
[ "$COREV" = "$LSPHP_SRC_PKG_REFERENCE" ] || die "this stage has $CORE $COREV but the php.net build was configured from $LSPHP_SRC_PKG_REFERENCE (the LiteSpeed repo moved between stages? rebuild)."
|
||||||
|
|
||||||
|
probe() { # $1 = tag; writes $OUT/probe-<tag>-{cli,lsapi}.txt
|
||||||
|
"$P/bin/php" "$PROBE" >"$OUT/probe-$1-cli.txt" || die "php probe failed ($1)."
|
||||||
|
"$P/bin/lsphp" "$PROBE" >"$OUT/probe-$1-lsapi.txt" || die "lsphp probe failed ($1)."
|
||||||
|
local first
|
||||||
|
first=$(head -n1 "$OUT/probe-$1-lsapi.txt")
|
||||||
|
[ "$first" = "SAPI litespeed" ] || die "lsphp did not run the probe under the litespeed SAPI ($1): '$first'."
|
||||||
|
}
|
||||||
|
## Configure Command minus CFLAGS, whitespace-normalised (the CFLAGS argument
|
||||||
|
## sits in a different position in our ./configure invocation).
|
||||||
|
cfgcmd() { "$P/bin/php" -i | awk -F' => ' '$1 ~ /^Configure Command/ {print $2}' | sed -E "s/'CFLAGS=[^']*'//; s/[[:space:]]+/ /g; s/^ //; s/ $//"; }
|
||||||
|
|
||||||
|
probe before
|
||||||
|
CFG_BEFORE=$(cfgcmd)
|
||||||
|
OLDV=$(awk '$1 == "VERSION" {print $2}' "$OUT/probe-before-cli.txt")
|
||||||
|
[ "$OLDV" = "$LSPHP_SRC_PKG_PHP_VERSION" ] || die "packaged PHP is $OLDV, build meta says $LSPHP_SRC_PKG_PHP_VERSION."
|
||||||
|
|
||||||
|
## ---- which files are php-src output? (dpkg decides) ---------------------------
|
||||||
|
mapfile -t SRC_PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' "lsphp${PHPVER}*" \
|
||||||
|
| awk -v v="$COREV" '$1 == "ii" && $3 == v {print $2}')
|
||||||
|
mapfile -t PECL_PKGS < <(dpkg-query -W -f='${db:Status-Abbrev} ${Package} ${Version}\n' "lsphp${PHPVER}*" \
|
||||||
|
| awk -v v="$COREV" '$1 == "ii" && $3 != v {print $2 "=" $3}')
|
||||||
|
if [ "$MODE" = runtime ]; then
|
||||||
|
for p in "${SRC_PKGS[@]}"; do
|
||||||
|
[ "$p" != "lsphp${PHPVER}-dev" ] || die "runtime mode but lsphp${PHPVER}-dev is installed in this stage."
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
log "php-src packages at $COREV: ${SRC_PKGS[*]}"
|
||||||
|
log "kept as packaged (own versions): ${PECL_PKGS[*]:-none}"
|
||||||
|
|
||||||
|
REPLACED=0
|
||||||
|
KEPT=()
|
||||||
|
while IFS= read -r f; do
|
||||||
|
[ -f "$f" ] && [ ! -L "$f" ] || continue
|
||||||
|
rel="${f#/}"
|
||||||
|
is_elf=0
|
||||||
|
if [ "$(head -c4 "$f" | od -An -c | tr -d ' ')" = '177ELF' ]; then is_elf=1; fi
|
||||||
|
case "$f" in
|
||||||
|
"$P/bin/phar${BRANCH}.phar"|"$P"/include/php/*|"$P"/lib/php/build/*|"$P/bin/php-config"|"$P/bin/php-config${BRANCH}"|"$P/bin/phpize"|"$P/bin/phpize${BRANCH}")
|
||||||
|
## text artefacts generated by the php-src build
|
||||||
|
[ "$MODE" = dev ] || [[ "$f" == "$P/bin/phar${BRANCH}.phar" ]] || { KEPT+=("$f"); continue; }
|
||||||
|
src="$SRCROOT/$rel"
|
||||||
|
case "$f" in
|
||||||
|
"$P/bin/php-config") src="$SRCROOT$P/bin/php-config${BRANCH}" ;;
|
||||||
|
"$P/bin/phpize") src="$SRCROOT$P/bin/phpize${BRANCH}" ;;
|
||||||
|
esac
|
||||||
|
if [ -f "$src" ]; then
|
||||||
|
install -m "$(stat -c %a "$f")" "$src" "$f"; REPLACED=$((REPLACED + 1))
|
||||||
|
elif [[ "$f" == "$P"/include/php/* ]]; then
|
||||||
|
log "header $f no longer exists in php.net $LSPHP_SRC_PHP_VERSION — removing the stale copy"
|
||||||
|
rm -f "$f"
|
||||||
|
else
|
||||||
|
die "php.net build has no counterpart for packaged $f."
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if [ "$is_elf" = 1 ]; then
|
||||||
|
src="$SRCROOT/$rel"
|
||||||
|
[ "$f" = "$P/bin/php" ] && src="$SRCROOT$P/bin/php${BRANCH}"
|
||||||
|
[ -f "$src" ] || die "packaged ELF $f (php-src output, owned by a $COREV package) has NO php.net counterpart at $src — refusing to ship an old binary next to the new PHP."
|
||||||
|
install -m "$(stat -c %a "$f")" "$src" "$f"; REPLACED=$((REPLACED + 1))
|
||||||
|
else
|
||||||
|
KEPT+=("$f")
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done < <(for p in "${SRC_PKGS[@]}"; do dpkg -L "$p"; done | sort -u)
|
||||||
|
|
||||||
|
if [ "$MODE" = dev ]; then
|
||||||
|
## headers that are NEW in the php.net release (none for most patch releases)
|
||||||
|
while IFS= read -r -d '' h; do
|
||||||
|
dst="${h#"$SRCROOT"}"
|
||||||
|
[ -e "$dst" ] || { mkdir -p "$(dirname "$dst")"; install -m 0644 "$h" "$dst"; log "added new header $dst"; }
|
||||||
|
done < <(find "$SRCROOT$P/include/php" -type f -print0)
|
||||||
|
CFV=$("$P/bin/php-config" --version)
|
||||||
|
[ "$CFV" = "$LSPHP_SRC_PHP_VERSION" ] || die "php-config reports $CFV after overlay."
|
||||||
|
fi
|
||||||
|
log "replaced $REPLACED php-src files; kept ${#KEPT[@]} packaging files (ini templates, helper scripts, docs, man pages)"
|
||||||
|
|
||||||
|
## ---- no unresolved shared libraries -------------------------------------------
|
||||||
|
while IFS= read -r -d '' f; do
|
||||||
|
if [ "$(head -c4 "$f" | od -An -c | tr -d ' ')" = '177ELF' ]; then
|
||||||
|
LDD=$(ldd "$f" 2>&1 || true)
|
||||||
|
[[ "$LDD" != *"not found"* ]] || die "$f has unresolved libraries:
|
||||||
|
$LDD"
|
||||||
|
fi
|
||||||
|
done < <(find "$P/bin" "$P/lib" -type f -print0)
|
||||||
|
|
||||||
|
## ---- the "same support" gate ----------------------------------------------------
|
||||||
|
probe after
|
||||||
|
CFG_AFTER=$(cfgcmd)
|
||||||
|
[ "$CFG_BEFORE" = "$CFG_AFTER" ] || die "Configure Command differs from the packaged build beyond CFLAGS:
|
||||||
|
packaged: $CFG_BEFORE
|
||||||
|
php.net : $CFG_AFTER"
|
||||||
|
|
||||||
|
FAIL=0
|
||||||
|
for sapi in cli lsapi; do
|
||||||
|
B="$OUT/probe-before-$sapi.txt"; A="$OUT/probe-after-$sapi.txt"
|
||||||
|
NEWV=$(awk '$1 == "VERSION" {print $2}' "$A")
|
||||||
|
[ "$NEWV" = "$LSPHP_SRC_PHP_VERSION" ] || { echo "[$sapi] reports PHP $NEWV, expected $LSPHP_SRC_PHP_VERSION" >&2; FAIL=1; }
|
||||||
|
## extension sets + versions
|
||||||
|
R=$(awk -v o="$OLDV" -v n="$LSPHP_SRC_PHP_VERSION" '
|
||||||
|
FNR == NR { if ($1 == "EXT" || $1 == "ZEXT") b[$1" "$2] = $3; next }
|
||||||
|
($1 == "EXT" || $1 == "ZEXT") { a[$1" "$2] = $3 }
|
||||||
|
END {
|
||||||
|
for (k in b) {
|
||||||
|
if (!(k in a)) { print "MISSING " k; continue }
|
||||||
|
want = (b[k] == o) ? n : b[k]
|
||||||
|
if (a[k] != want) print "VERSION " k " " b[k] " -> " a[k] " (want " want ")"
|
||||||
|
}
|
||||||
|
for (k in a) if (!(k in b)) print "EXTRA " k " " a[k]
|
||||||
|
}' "$B" "$A" | sort)
|
||||||
|
if [ -n "$R" ]; then echo "[$sapi] extension surface changed:"$'\n'"$R" >&2; FAIL=1; fi
|
||||||
|
## feature lines must be byte-identical
|
||||||
|
R=$(diff <(grep '^FEAT ' "$B") <(grep '^FEAT ' "$A") || true)
|
||||||
|
if [ -n "$R" ]; then echo "[$sapi] feature surface changed:"$'\n'"$R" >&2; FAIL=1; fi
|
||||||
|
## effective ini (packaged php.ini + mods-available/*.ini): no directive may
|
||||||
|
## vanish or change value; directives NEW in the release are printed.
|
||||||
|
R=$(awk 'FNR == NR { if ($1 == "INI") b[$2] = $0; next } $1 == "INI" { a[$2] = $0 }
|
||||||
|
END { for (k in b) { if (!(k in a)) print "REMOVED " b[k]; else if (a[k] != b[k]) print "CHANGED " b[k] " -> " a[k] } }' "$B" "$A" | sort)
|
||||||
|
if [ -n "$R" ]; then echo "[$sapi] effective ini changed:"$'\n'"$R" >&2; FAIL=1; fi
|
||||||
|
R=$(awk 'FNR == NR { if ($1 == "INI") b[$2] = 1; next } $1 == "INI" && !($2 in b) { print " " $0 }' "$B" "$A")
|
||||||
|
[ -z "$R" ] || log "[$sapi] ini directives new in $LSPHP_SRC_PHP_VERSION (informational):"$'\n'"$R"
|
||||||
|
log "[$sapi] $(grep -c '^EXT ' "$A") extensions + $(grep -c '^ZEXT ' "$A") zend extensions, identical to packaged; $(grep -c '^FEAT ' "$A") feature lines identical"
|
||||||
|
done
|
||||||
|
[ "$FAIL" = 0 ] || die "the php.net build does NOT offer the same support as packaged lsphp${PHPVER} $COREV (details above)."
|
||||||
|
|
||||||
|
printf '%s\n' "$LSPHP_SRC_PHP_VERSION" >/etc/cac-php-version
|
||||||
|
cat >/etc/cac-lsphp-source <<EOF
|
||||||
|
source=php.net
|
||||||
|
php_version=$LSPHP_SRC_PHP_VERSION
|
||||||
|
tarball_sha256=$LSPHP_SRC_SHA256
|
||||||
|
signer_primary_fpr=$LSPHP_SRC_SIGNER
|
||||||
|
replaces_packaged=$CORE=$COREV (PHP $OLDV)
|
||||||
|
pecl_kept=${PECL_PKGS[*]:-none}
|
||||||
|
EOF
|
||||||
|
log "OK: lsphp${PHPVER} is now php.net $LSPHP_SRC_PHP_VERSION (was packaged $OLDV); /etc/cac-lsphp-source written"
|
||||||
|
## $OUT is a BuildKit bind mount (rw = writes discarded); nothing to clean up.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
// lsphp-src-probe.php — prints the runtime "support surface" of the PHP that
|
||||||
|
// runs it, one fact per line, for lsphp-src-overlay.sh to diff before/after the
|
||||||
|
// php.net overlay. Run under BOTH SAPIs: `bin/php probe.php` and
|
||||||
|
// `bin/lsphp probe.php` (lsphp executes a script argument under the litespeed
|
||||||
|
// SAPI — measured; it is only `-m`/`-r` it does not understand).
|
||||||
|
// Line kinds: SAPI <name> | VERSION <v> | EXT <name> <version> | ZEXT <name> <version>
|
||||||
|
// INI <name> <value> | FEAT <key> <value>
|
||||||
|
// The overlay script relies on the stable "<KIND> <key> <rest>" shape; keep it.
|
||||||
|
echo 'SAPI ', PHP_SAPI, "\n";
|
||||||
|
echo 'VERSION ', PHP_VERSION, "\n";
|
||||||
|
echo 'FEAT ZTS ', PHP_ZTS, "\n";
|
||||||
|
echo 'FEAT DEBUG ', PHP_DEBUG, "\n";
|
||||||
|
echo 'FEAT INT_SIZE ', PHP_INT_SIZE, "\n";
|
||||||
|
foreach (get_loaded_extensions() as $e) {
|
||||||
|
echo 'EXT ', str_replace(' ', '_', $e), ' ', phpversion($e), "\n";
|
||||||
|
}
|
||||||
|
foreach (get_loaded_extensions(true) as $e) {
|
||||||
|
echo 'ZEXT ', str_replace(' ', '_', $e), ' ', phpversion($e), "\n";
|
||||||
|
}
|
||||||
|
foreach (ini_get_all(null, false) as $k => $v) {
|
||||||
|
echo 'INI ', $k, ' ', var_export($v, true), "\n";
|
||||||
|
}
|
||||||
|
if (function_exists('gd_info')) {
|
||||||
|
foreach (gd_info() as $k => $v) {
|
||||||
|
echo 'FEAT gd:', str_replace(' ', '_', $k), ' ', var_export($v, true), "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (function_exists('curl_version')) {
|
||||||
|
$c = curl_version();
|
||||||
|
echo 'FEAT curl:version ', $c['version'], "\n";
|
||||||
|
echo 'FEAT curl:ssl ', $c['ssl_version'], "\n";
|
||||||
|
echo 'FEAT curl:protocols ', implode(',', $c['protocols']), "\n";
|
||||||
|
}
|
||||||
|
echo 'FEAT openssl ', defined('OPENSSL_VERSION_TEXT') ? OPENSSL_VERSION_TEXT : 'none', "\n";
|
||||||
|
if (defined('INTL_ICU_VERSION')) echo 'FEAT icu ', INTL_ICU_VERSION, "\n";
|
||||||
|
if (class_exists('Imagick')) echo 'FEAT imagick:formats ', count(Imagick::queryFormats()), "\n";
|
||||||
|
if (function_exists('sodium_crypto_aead_aes256gcm_is_available')) echo 'FEAT sodium:aes256gcm ', var_export(sodium_crypto_aead_aes256gcm_is_available(), true), "\n";
|
||||||
|
echo 'FEAT password_algos ', implode(',', password_algos()), "\n";
|
||||||
|
echo 'FEAT hash_algos ', count(hash_algos()), "\n";
|
||||||
|
echo 'FEAT stream_wrappers ', implode(',', stream_get_wrappers()), "\n";
|
||||||
|
echo 'FEAT stream_filters ', implode(',', stream_get_filters()), "\n";
|
||||||
|
if (function_exists('mb_list_encodings')) echo 'FEAT mb_encodings ', count(mb_list_encodings()), "\n";
|
||||||
|
if (function_exists('opcache_get_status')) echo 'FEAT opcache:loaded 1', "\n";
|
||||||
|
if (class_exists('Redis')) echo 'FEAT redis:serializers ', implode(',', array_filter(['IGBINARY', 'MSGPACK', 'JSON'], fn($s) => defined("Redis::SERIALIZER_$s"))), "\n";
|
||||||
|
if (class_exists('Redis')) echo 'FEAT redis:compression ', implode(',', array_filter(['LZF', 'ZSTD', 'LZ4'], fn($s) => defined("Redis::COMPRESSION_$s"))), "\n";
|
||||||
|
if (class_exists('Memcached')) echo 'FEAT memcached:igbinary ', var_export(Memcached::HAVE_IGBINARY, true), ' msgpack ', var_export(Memcached::HAVE_MSGPACK, true), ' sasl ', var_export(Memcached::HAVE_SASL, true), "\n";
|
||||||
@@ -15,6 +15,20 @@
|
|||||||
## runs it and the panel monitors it (check-ols-htaccess-watcher.php).
|
## runs it and the panel monitors it (check-ols-htaccess-watcher.php).
|
||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
|
## WATCH_ROOT is deliberately left as the host-wide /mnt/users, not narrowed to
|
||||||
|
## the shared-OLS tenant set, even though that set IS derivable in-container
|
||||||
|
## (render-shared-ols-config.sh's $SITES_ROOT/*/site.meta VHROOT= is exactly
|
||||||
|
## that list). Narrowing it would mean handing inotifywait a fixed argv list of
|
||||||
|
## VHROOT dirs at process start — and inotifywait cannot be told to watch a NEW
|
||||||
|
## directory once running. The panel provisions sites onto this container live,
|
||||||
|
## between renders; a site added after the watcher started would then sit
|
||||||
|
## outside every watch until the next container restart, i.e. exactly the
|
||||||
|
## silent-failure mode (spec 7) this script exists to prevent, now for brand
|
||||||
|
## new tenants instead of none. Doing this safely needs a reload path (SIGHUP
|
||||||
|
## re-exec off the current site.meta list, coordinated with
|
||||||
|
## render-shared-ols-config.sh) that does not exist yet and is its own change.
|
||||||
|
## So: WATCH_ROOT stays broad, and correctness comes entirely from the path
|
||||||
|
## match below, which is sufficient on its own.
|
||||||
WATCH_ROOT="${OLS_WATCH_ROOT:-/mnt/users}"
|
WATCH_ROOT="${OLS_WATCH_ROOT:-/mnt/users}"
|
||||||
DEBOUNCE="${OLS_HTACCESS_DEBOUNCE:-15}" # coalesce window (s)
|
DEBOUNCE="${OLS_HTACCESS_DEBOUNCE:-15}" # coalesce window (s)
|
||||||
FLOOR="${OLS_HTACCESS_FLOOR:-60}" # min seconds between restarts
|
FLOOR="${OLS_HTACCESS_FLOOR:-60}" # min seconds between restarts
|
||||||
@@ -24,16 +38,17 @@ last_restart=0
|
|||||||
log() { echo "ols-htaccess-watcher: $*" >&2; }
|
log() { echo "ols-htaccess-watcher: $*" >&2; }
|
||||||
|
|
||||||
do_restart() {
|
do_restart() {
|
||||||
|
path="$1"
|
||||||
now=$(date +%s)
|
now=$(date +%s)
|
||||||
if [ $((now - last_restart)) -lt "$FLOOR" ]; then
|
if [ $((now - last_restart)) -lt "$FLOOR" ]; then
|
||||||
log "within ${FLOOR}s floor — coalescing, skipping restart"
|
log "within ${FLOOR}s floor — coalescing, skipping restart ($path)"
|
||||||
return
|
return
|
||||||
fi
|
fi
|
||||||
if "$LSWSCTRL" restart >/dev/null 2>&1; then
|
if "$LSWSCTRL" restart >/dev/null 2>&1; then
|
||||||
last_restart=$now
|
last_restart=$now
|
||||||
log "graceful restart issued (.htaccess change)"
|
log "graceful restart issued — $path changed"
|
||||||
else
|
else
|
||||||
log "WARNING: lswsctrl restart failed"
|
log "WARNING: lswsctrl restart failed ($path)"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,18 +56,34 @@ if ! command -v inotifywait >/dev/null 2>&1; then
|
|||||||
log "FATAL: inotifywait not installed (inotify-tools)"; exit 1
|
log "FATAL: inotifywait not installed (inotify-tools)"; exit 1
|
||||||
fi
|
fi
|
||||||
mkdir -p "$WATCH_ROOT"
|
mkdir -p "$WATCH_ROOT"
|
||||||
log "watching $WATCH_ROOT for .htaccess changes (debounce=${DEBOUNCE}s floor=${FLOOR}s)"
|
log "watching $WATCH_ROOT for docroot (public_html) .htaccess changes (debounce=${DEBOUNCE}s floor=${FLOOR}s)"
|
||||||
|
|
||||||
## -m monitor, -r recursive. We filter to .htaccess in the read loop rather than
|
## -m monitor, -r recursive. We filter in the read loop rather than --include
|
||||||
## --include so this works on older inotify-tools too. modify/create/delete/move
|
## so this works on older inotify-tools too. modify/create/delete/move all
|
||||||
## all matter (delete of .htaccess also changes rewrite behavior).
|
## matter (delete of .htaccess also changes rewrite behavior).
|
||||||
inotifywait -m -r -e modify,create,delete,move "$WATCH_ROOT" --format '%f' 2>/dev/null |
|
##
|
||||||
while read -r fname; do
|
## --format '%w%f' (full path), NOT '%f' (basename only). OLS reads .htaccess
|
||||||
case "$fname" in
|
## (RewriteFile) only from a vhost's DOCROOT — VHROOT, i.e.
|
||||||
.htaccess) ;;
|
## /mnt/users/<user>/<domain>/public_html (see render-shared-ols-config.sh /
|
||||||
|
## entrypoint-lsphp.sh) — never anything below it. A basename-only match fires
|
||||||
|
## for ANY .htaccess anywhere under a tenant, at any depth, and WordPress
|
||||||
|
## plugins write plenty of those that OLS never opens: measured on whp01 over
|
||||||
|
## 24h, this watcher fired 63 restarts, of which the docroot .htaccess actually
|
||||||
|
## changed in 0. All 28 distinct files behind those 63 were plugin guard files
|
||||||
|
## — Wordfence self-healing waf/views/vendor/tmp/models/lib/.htaccess, W3 Total
|
||||||
|
## Cache writing one per cached URL under wp-content/cache/page_enhanced/, plus
|
||||||
|
## WPForms/Gravity Forms/UpdraftPlus/Groundhogg/WP Staging upload guards — and
|
||||||
|
## most of those tenants are on the shared Apache tier (cac-fpm), not this OLS
|
||||||
|
## tier at all, so their cache churn was restarting the OLS serving 15 unrelated
|
||||||
|
## tenants for no reason. Matching the full path down to /public_html/.htaccess
|
||||||
|
## is what actually ties a change to something OLS will reread.
|
||||||
|
inotifywait -m -r -e modify,create,delete,move "$WATCH_ROOT" --format '%w%f' 2>/dev/null |
|
||||||
|
while read -r path; do
|
||||||
|
case "$path" in
|
||||||
|
*/public_html/.htaccess) ;;
|
||||||
*) continue ;;
|
*) continue ;;
|
||||||
esac
|
esac
|
||||||
## A tenant .htaccess changed. Coalesce the save-burst, then restart ONCE.
|
## A tenant DOCROOT .htaccess changed. Coalesce the save-burst, then restart ONCE.
|
||||||
##
|
##
|
||||||
## The coalesce is HARD-BOUNDED to DEBOUNCE seconds: a previous version blocked
|
## The coalesce is HARD-BOUNDED to DEBOUNCE seconds: a previous version blocked
|
||||||
## on `read -t DEBOUNCE` which, on a busy multi-tenant server, never timed out
|
## on `read -t DEBOUNCE` which, on a busy multi-tenant server, never timed out
|
||||||
@@ -69,5 +100,5 @@ while read -r fname; do
|
|||||||
break # ~2s of total quiet — the burst has settled
|
break # ~2s of total quiet — the burst has settled
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
do_restart
|
do_restart "$path"
|
||||||
done
|
done
|
||||||
Executable
+227
@@ -0,0 +1,227 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
## pkg-cache-repos.sh — BUILD-TIME ONLY, OPT-IN. Points dnf/apt at the internal
|
||||||
|
## package cache (nginx proxy_cache, CT130 "pkg-cache" on the Proxmox cluster)
|
||||||
|
## when PKG_CACHE is set; without it the repo files are left alone and builds
|
||||||
|
## use the upstream repositories as before.
|
||||||
|
##
|
||||||
|
## PKG_CACHE=https://192.168.1.40 pkg-cache-repos.sh rpm # AlmaLinux/EPEL/Remi
|
||||||
|
## PKG_CACHE=https://192.168.1.40 pkg-cache-repos.sh apt # Ubuntu + LiteSpeed
|
||||||
|
## pkg-cache-repos.sh release-install epel [remi] # GPG-verified release RPMs
|
||||||
|
## pkg-cache-repos.sh restore # undo everything above
|
||||||
|
##
|
||||||
|
## Every RUN that rewrites must `restore` before it ends: it puts rewritten files
|
||||||
|
## back, deletes files this script created, deletes any NEW repo/apt file that
|
||||||
|
## still references the cache, then FAILS if any reference is left — so no shipped
|
||||||
|
## layer carries the cache's LAN address (images run on hosts that can't reach it).
|
||||||
|
##
|
||||||
|
## Why: on 2026-10-21 (PHP security release) builds failed on Remi mirrors lagging
|
||||||
|
## hours behind origin, Remi's origin timing out over HTTPS, and an EPEL mirror
|
||||||
|
## with a broken certificate. The cache fetches from the ORIGINS over HTTPS (with
|
||||||
|
## origin/mirror fallbacks and stale-on-error), keeps metadata ~3 minutes and
|
||||||
|
## immutable .rpm/.deb files long-term.
|
||||||
|
##
|
||||||
|
## Integrity (security review 2026-10-21):
|
||||||
|
## * PKG_CACHE must be https://. The cache's certificate is issued by the
|
||||||
|
## name-constrained "pkg-cache internal CA" (keys/pkg-cache/ca.crt, valid only
|
||||||
|
## for 192.168.1.40 / pkg-cache.*). That CA is trusted ONLY by the rewritten
|
||||||
|
## repos (dnf sslcacert= bundle, apt per-host CaInfo) and only during the RUN —
|
||||||
|
## never added to the system trust store.
|
||||||
|
## * gpgcheck=1 is forced on every rewritten repo; repo_gpgcheck=1 on Remi and
|
||||||
|
## AlmaLinux, which sign repomd.xml (measured: repomd.xml.asc 200). EPEL
|
||||||
|
## publishes no repomd signature (404), so its metadata relies on HTTPS end
|
||||||
|
## to end (client->cache->dl.fedoraproject.org/mirrors, all verified).
|
||||||
|
## * release-install downloads epel-release / remi-release (from the cache, or
|
||||||
|
## from the HTTPS origin without PKG_CACHE), checks the package name, imports
|
||||||
|
## the signing keys committed in keys/pkg-cache/ and requires
|
||||||
|
## `rpm -K` "signatures OK" before installing with localpkg_gpgcheck=1.
|
||||||
|
## * The php.net version gates (EXPECT_PHP_MIN / EXPECT_PHP_VERSION) still fail
|
||||||
|
## a build that gets old PHP (replay of old-but-signed metadata).
|
||||||
|
##
|
||||||
|
## rpm: each rewritten baseurl keeps the upstream origin as a SECOND url, so if
|
||||||
|
## the cache dies mid-build dnf fails over to the origin by itself.
|
||||||
|
## apt: apt has no per-source failover; CI only sets PKG_CACHE after probing the
|
||||||
|
## cache (with the CA), and a build without PKG_CACHE uses upstream sources.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
MODE="${1:?usage: pkg-cache-repos.sh rpm|apt|release-install NAME...|restore}"
|
||||||
|
shift || true
|
||||||
|
STATE=/var/lib/pkg-cache-repos # saved originals + manifest + created files
|
||||||
|
SHARE="${PKG_CACHE_SHARE:-/usr/local/share/pkg-cache}" # ca.crt + RPM-GPG-KEY-*
|
||||||
|
C="${PKG_CACHE:-}"
|
||||||
|
C="${C%/}"
|
||||||
|
|
||||||
|
die() { echo "FATAL(pkg-cache-repos): $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
if [ -n "$C" ]; then
|
||||||
|
case "$C" in
|
||||||
|
https://*) ;;
|
||||||
|
*) die "PKG_CACHE must be an https:// URL (plain http is refused), got '$C'" ;;
|
||||||
|
esac
|
||||||
|
HOSTPORT="${C#https://}"; HOSTPORT="${HOSTPORT%%/*}"
|
||||||
|
[ -s "$SHARE/ca.crt" ] || die "PKG_CACHE set but $SHARE/ca.crt (pkg-cache CA) is missing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Files that may hold repo configuration; restore re-checks all of them.
|
||||||
|
repo_files() {
|
||||||
|
find /etc/yum.repos.d /etc/dnf /etc/apt -type f 2>/dev/null | sort || true
|
||||||
|
}
|
||||||
|
|
||||||
|
start_session() {
|
||||||
|
[ -d "$STATE" ] && return 0
|
||||||
|
mkdir -p "$STATE/orig"
|
||||||
|
repo_files > "$STATE/manifest" # what existed before this RUN touched anything
|
||||||
|
: > "$STATE/created"
|
||||||
|
printf '%s\n' "$HOSTPORT" > "$STATE/host"
|
||||||
|
}
|
||||||
|
save() { # keep the first-seen (pre-rewrite) copy
|
||||||
|
[ -e "$STATE/orig$1" ] && return 0
|
||||||
|
mkdir -p "$STATE/orig$(dirname "$1")"
|
||||||
|
cp -p "$1" "$STATE/orig$1"
|
||||||
|
}
|
||||||
|
created() { printf '%s\n' "$1" >> "$STATE/created"; }
|
||||||
|
|
||||||
|
if [ "$MODE" = restore ]; then
|
||||||
|
[ -d "$STATE" ] || exit 0
|
||||||
|
host=$(cat "$STATE/host")
|
||||||
|
# mapfile from a process substitution, not `cmd | while read`: no pipeline,
|
||||||
|
# so nothing for pipefail to misreport (scripts/tests/lsphp-info-probe.test.sh).
|
||||||
|
mapfile -t saved < <(cd "$STATE/orig" && find . -type f)
|
||||||
|
for rel in "${saved[@]}"; do
|
||||||
|
cat "$STATE/orig/$rel" > "/${rel#./}"
|
||||||
|
done
|
||||||
|
while read -r f; do [ -n "$f" ] && rm -f -- "$f"; done < "$STATE/created"
|
||||||
|
# A repo/apt file that appeared during this RUN and still names the cache
|
||||||
|
# (e.g. written by a package from the rewritten repos) is removed too.
|
||||||
|
mapfile -t current < <(repo_files)
|
||||||
|
for f in "${current[@]}"; do
|
||||||
|
if ! grep -qxF -- "$f" "$STATE/manifest" && grep -qF -- "$host" "$f"; then
|
||||||
|
echo "pkg-cache-repos: removing new file that references the cache: $f"
|
||||||
|
rm -f -- "$f"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
left=$(repo_files | xargs -r grep -lF -- "$host" 2>/dev/null || true)
|
||||||
|
rm -rf "$STATE"
|
||||||
|
[ -z "$left" ] || die "cache address $host still present after restore in: $left"
|
||||||
|
echo "pkg-cache-repos: original repository files restored (no reference to $host left)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$MODE" = release-install ]; then
|
||||||
|
[ $# -gt 0 ] || die "release-install needs: epel and/or remi"
|
||||||
|
tmp=$(mktemp -d)
|
||||||
|
curl_opts=(-fsS --proto '=https' --retry 5 --retry-all-errors --max-time 120)
|
||||||
|
if [ -n "$C" ]; then
|
||||||
|
start_session
|
||||||
|
cat /etc/pki/tls/certs/ca-bundle.crt "$SHARE/ca.crt" > "$STATE/ca-bundle.pem"
|
||||||
|
curl_opts+=(--cacert "$STATE/ca-bundle.pem")
|
||||||
|
fi
|
||||||
|
files=()
|
||||||
|
for name in "$@"; do
|
||||||
|
case "$name" in
|
||||||
|
epel) pkg=epel-release key=RPM-GPG-KEY-EPEL-10
|
||||||
|
path=/epel/epel-release-latest-10.noarch.rpm
|
||||||
|
origin=https://dl.fedoraproject.org/pub/epel/epel-release-latest-10.noarch.rpm ;;
|
||||||
|
remi) pkg=remi-release key=RPM-GPG-KEY-remi2024
|
||||||
|
path=/remi/enterprise/remi-release-10.rpm
|
||||||
|
origin=https://rpms.remirepo.net/enterprise/remi-release-10.rpm ;;
|
||||||
|
*) die "unknown release package '$name'" ;;
|
||||||
|
esac
|
||||||
|
url="$origin"; [ -n "$C" ] && url="$C$path"
|
||||||
|
f="$tmp/$pkg.rpm"
|
||||||
|
curl "${curl_opts[@]}" -o "$f" "$url" || die "download failed: $url"
|
||||||
|
rpm --import "$SHARE/$key" || die "cannot import $SHARE/$key"
|
||||||
|
got=$(rpm -qp --nosignature --qf '%{NAME}' "$f" 2>/dev/null || true)
|
||||||
|
[ "$got" = "$pkg" ] || die "$url is '$got', expected $pkg"
|
||||||
|
chk=$(rpm -K "$f" 2>&1 || true)
|
||||||
|
[[ "$chk" == *": digests signatures OK" ]] || die "signature check failed for $url: $chk"
|
||||||
|
echo "pkg-cache-repos: $pkg verified ($chk) from $url"
|
||||||
|
files+=("$f")
|
||||||
|
done
|
||||||
|
dnf install -y --setopt=localpkg_gpgcheck=1 "${files[@]}"
|
||||||
|
rm -rf "$tmp"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$C" ]; then
|
||||||
|
echo "pkg-cache-repos($MODE): PKG_CACHE not set - using upstream repositories"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
rewrite_rpm() {
|
||||||
|
local f="$1"
|
||||||
|
grep -q -E '^#? ?baseurl=https?://(repo\.almalinux\.org|download\.example|rpms\.remirepo\.net)/' "$f" || return 0
|
||||||
|
save "$f"
|
||||||
|
# Per [section]: a (commented or live) baseurl on a known origin becomes
|
||||||
|
# "baseurl=<cache>/<prefix>/<path> <origin>/<path>"; that section's
|
||||||
|
# mirrorlist/metalink is commented out (dnf prefers them over baseurl) and it
|
||||||
|
# gets sslcacert=<system bundle + pkg-cache CA>, gpgcheck=1 and, for repos
|
||||||
|
# that sign repomd.xml, repo_gpgcheck=1. Other sections are untouched.
|
||||||
|
awk -v C="$C" -v BUNDLE="$STATE/ca-bundle.pem" '
|
||||||
|
function flush( i) {
|
||||||
|
for (i = 1; i <= n; i++) {
|
||||||
|
if (hit && buf[i] ~ /^(mirrorlist|metalink)=/) buf[i] = "#" buf[i]
|
||||||
|
if (hit && buf[i] ~ /^(sslcacert|gpgcheck|repo_gpgcheck)[ \t]*=/) continue
|
||||||
|
print buf[i]
|
||||||
|
}
|
||||||
|
if (hit) {
|
||||||
|
print "sslcacert=" BUNDLE
|
||||||
|
print "gpgcheck=1"
|
||||||
|
if (signed) print "repo_gpgcheck=1"
|
||||||
|
}
|
||||||
|
n = 0; hit = 0; signed = 0
|
||||||
|
}
|
||||||
|
function rw(line, re, prefix, origin, sig, p) {
|
||||||
|
if (match(line, re)) {
|
||||||
|
p = substr(line, RSTART + RLENGTH); sub(/[ \t].*$/, "", p)
|
||||||
|
hit = 1; if (sig) signed = 1
|
||||||
|
return "baseurl=" C "/" prefix "/" p " " origin "/" p
|
||||||
|
}
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
/^\[/ { flush() }
|
||||||
|
{
|
||||||
|
l = $0
|
||||||
|
if (l ~ /^#? ?baseurl=/) {
|
||||||
|
l = rw(l, "^#? ?baseurl=https?://repo\\.almalinux\\.org/almalinux/", "almalinux", "https://repo.almalinux.org/almalinux", 1)
|
||||||
|
l = rw(l, "^#? ?baseurl=https?://download\\.example/pub/epel/", "epel", "https://dl.fedoraproject.org/pub/epel", 0)
|
||||||
|
l = rw(l, "^#? ?baseurl=https?://rpms\\.remirepo\\.net/", "remi", "https://rpms.remirepo.net", 1)
|
||||||
|
}
|
||||||
|
buf[++n] = l
|
||||||
|
}
|
||||||
|
END { flush() }
|
||||||
|
' "$f" > "$f.pkgcache" && cat "$f.pkgcache" > "$f" && rm -f "$f.pkgcache"
|
||||||
|
}
|
||||||
|
|
||||||
|
rewrite_apt() {
|
||||||
|
local f="$1"
|
||||||
|
grep -q -E 'https?://([a-z]{2}\.)?(archive|security)\.ubuntu\.com/|https?://rpms\.litespeedtech\.com/' "$f" || return 0
|
||||||
|
save "$f"
|
||||||
|
sed -i -E \
|
||||||
|
-e "s%https?://(archive|[a-z]{2}\.archive)\.ubuntu\.com/ubuntu/?%$C/ubuntu/%g" \
|
||||||
|
-e "s%https?://security\.ubuntu\.com/ubuntu/?%$C/ubuntu-security/%g" \
|
||||||
|
-e "s%https?://rpms\.litespeedtech\.com/%$C/litespeed/%g" \
|
||||||
|
"$f"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$MODE" in
|
||||||
|
rpm)
|
||||||
|
start_session
|
||||||
|
cat /etc/pki/tls/certs/ca-bundle.crt "$SHARE/ca.crt" > "$STATE/ca-bundle.pem"
|
||||||
|
for f in /etc/yum.repos.d/*.repo; do [ -f "$f" ] && rewrite_rpm "$f"; done
|
||||||
|
echo "pkg-cache-repos(rpm): repos now via $C:"
|
||||||
|
grep -h '^baseurl=' /etc/yum.repos.d/*.repo | grep -F "$C/" | sort -u | sed 's/^/ /'
|
||||||
|
;;
|
||||||
|
apt)
|
||||||
|
start_session
|
||||||
|
conf=/etc/apt/apt.conf.d/99pkg-cache
|
||||||
|
# Trust the pkg-cache CA for the cache host ONLY (apt per-host option).
|
||||||
|
printf 'Acquire::https::%s::CaInfo "%s";\n' "${HOSTPORT%%:*}" "$SHARE/ca.crt" > "$conf"
|
||||||
|
created "$conf"
|
||||||
|
for f in /etc/apt/sources.list /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources; do
|
||||||
|
[ -f "$f" ] && rewrite_apt "$f"
|
||||||
|
done
|
||||||
|
echo "pkg-cache-repos(apt): sources now via $C:"
|
||||||
|
grep -h -E '^(deb|URIs:)' /etc/apt/sources.list /etc/apt/sources.list.d/* 2>/dev/null | grep -F "$C/" | sort -u | sed 's/^/ /'
|
||||||
|
;;
|
||||||
|
*) die "unknown mode '$MODE'" ;;
|
||||||
|
esac
|
||||||
@@ -82,18 +82,61 @@ awk '
|
|||||||
} >> "$TMP"
|
} >> "$TMP"
|
||||||
|
|
||||||
## --- 4. emit per-site vhost stanzas + collect listener map lines ---
|
## --- 4. emit per-site vhost stanzas + collect listener map lines ---
|
||||||
|
##
|
||||||
|
## First value of KEY= in a site.meta, as plain data. This replaces
|
||||||
|
## `sed -n 's/^KEY=//p' "$meta" | head -1`, which was a pipeline whose reader
|
||||||
|
## (`head -1`) exits after one line while the writer (`sed`) may still be
|
||||||
|
## flushing: the writer then dies 141, and `set -euo pipefail` (line 22) makes
|
||||||
|
## the whole ASSIGNMENT fail, which aborts this script mid-render. A truncated
|
||||||
|
## httpd_config.conf is never written (the render is atomic), but the effect is
|
||||||
|
## that a site the panel just provisioned silently never appears in the config
|
||||||
|
## and every subsequent render fails the same way.
|
||||||
|
##
|
||||||
|
## Measured in this image, `sed -n 's/^DOMAINS=//p' | head -1`:
|
||||||
|
## 400 matching lines (~6 KB of sed output) -> 0 0 0 0 0
|
||||||
|
## 6000 matching lines (~90 KB of sed output) -> 141 141 141
|
||||||
|
## Do not read a threshold into those two rows. There is no size below which
|
||||||
|
## this is safe: each run is a RACE on whether `head` closes the pipe before
|
||||||
|
## `sed`'s final write() returns, and the payload only decides how many write()
|
||||||
|
## syscalls sed has to lose. Measured against a default 65536-byte pipe
|
||||||
|
## (F_GETPIPE_SZ), 41144 bytes SIGPIPEd on 32 of 300 runs — 11%, well under
|
||||||
|
## capacity — and 65012 bytes still only on 25 of 30, so it is neither safe
|
||||||
|
## below capacity nor certain at it; strace caught a writer dying having put
|
||||||
|
## 12086 of 40406 bytes into a 65536-byte pipe. What actually separated a host
|
||||||
|
## that failed 5/5 from one that passed 10/10 was the WRITER's syscall size
|
||||||
|
## (bash <= 5.2.15 writes ~37 KB at a time, >= 5.2.21 writes 80-160 bytes), not
|
||||||
|
## the host's pipe capacity. (An earlier draft of this comment blamed
|
||||||
|
## fs.pipe-user-pages-soft; that limit clamps new pipes to two pages, not one,
|
||||||
|
## only past 1024 pipes for one uid, and never for CAP_SYS_RESOURCE.)
|
||||||
|
##
|
||||||
|
## So the rule this file follows is structural, not statistical: under pipefail,
|
||||||
|
## a writer piped into a reader that can stop early (`head`, `grep -q`/`-l`/`-m`,
|
||||||
|
## `sed q`, `awk ... exit`, `read`) is a latent 141 — full stop. A call site is
|
||||||
|
## only sound when the file does not set pipefail, or the reader provably runs
|
||||||
|
## to EOF, or the status is thrown away. "A site.meta would never be that big"
|
||||||
|
## was never one of those, least of all for panel-written input we do not
|
||||||
|
## validate.
|
||||||
|
##
|
||||||
|
## awk reads the FILE directly and stops at the first hit: no pipeline, so
|
||||||
|
## nothing for pipefail to adopt. Same semantics as before, verified against
|
||||||
|
## the old form on duplicate keys, decoy keys (`notVHNAME=`), empty values and
|
||||||
|
## missing keys: first match wins, the rest of the line is the value, verbatim.
|
||||||
|
meta_value() {
|
||||||
|
awk -v k="$1" 'index($0, k "=") == 1 { print substr($0, length(k) + 2); exit }' "$2"
|
||||||
|
}
|
||||||
|
|
||||||
maps=""
|
maps=""
|
||||||
site_count=0
|
site_count=0
|
||||||
for meta in "$SITES_ROOT"/*/site.meta; do
|
for meta in "$SITES_ROOT"/*/site.meta; do
|
||||||
[ -e "$meta" ] || continue
|
[ -e "$meta" ] || continue
|
||||||
sdir=$(dirname "$meta")
|
sdir=$(dirname "$meta")
|
||||||
## PARSE site.meta with sed — do NOT `source` it. The panel writes these values
|
## EXTRACT from site.meta — do NOT `source` it. The panel writes these values
|
||||||
## (derived from DB domains), so they should be safe, but sourcing paneldata as
|
## (derived from DB domains), so they should be safe, but sourcing paneldata as
|
||||||
## shell would execute any metacharacters as root in this container if a value
|
## shell would execute any metacharacters as root in this container if a value
|
||||||
## ever slipped validation. sed extraction treats them as plain data.
|
## ever slipped validation. meta_value treats them as plain data.
|
||||||
VHNAME=$(sed -n 's/^VHNAME=//p' "$meta" | head -1)
|
VHNAME=$(meta_value VHNAME "$meta")
|
||||||
VHROOT=$(sed -n 's/^VHROOT=//p' "$meta" | head -1)
|
VHROOT=$(meta_value VHROOT "$meta")
|
||||||
DOMAINS=$(sed -n 's/^DOMAINS=//p' "$meta" | head -1)
|
DOMAINS=$(meta_value DOMAINS "$meta")
|
||||||
if [ -z "$VHNAME" ] || [ -z "$VHROOT" ] || [ -z "$DOMAINS" ] || [ ! -f "$sdir/vhconf.conf" ]; then
|
if [ -z "$VHNAME" ] || [ -z "$VHROOT" ] || [ -z "$DOMAINS" ] || [ ! -f "$sdir/vhconf.conf" ]; then
|
||||||
echo "render-shared-ols: skipping $sdir (incomplete: VHNAME/VHROOT/DOMAINS/vhconf.conf)" >&2
|
echo "render-shared-ols: skipping $sdir (incomplete: VHNAME/VHROOT/DOMAINS/vhconf.conf)" >&2
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# shellcheck disable=SC2154 # $uid / $user come from the container environment
|
||||||
|
## safe-paths.sh — sourced by entrypoint.sh and entrypoint-fpm.sh. Not executed directly.
|
||||||
|
##
|
||||||
|
## /home/$user is the customer's bind-mounted site directory: the customer can
|
||||||
|
## create anything in it, including symlinks, from SFTP/FTP, cron, or PHP. The
|
||||||
|
## entrypoints run as root, and root following a customer symlink is a
|
||||||
|
## privilege escalation: `touch`/`chown`/`>` on logs/php-fpm/error.log ->
|
||||||
|
## /etc/shadow hands the customer /etc/shadow (container root).
|
||||||
|
##
|
||||||
|
## Rules the entrypoints follow:
|
||||||
|
## * /home/$user itself is the bind-mount point and must be a real directory.
|
||||||
|
## * logs/ is WHP-owned and apache / php-fpm open files in it as root, so it
|
||||||
|
## must be a real directory and every symlink under it is removed + logged.
|
||||||
|
## * Files and dirs are CREATED as the customer (as_user), so a symlink or a
|
||||||
|
## swapped path can only reach what the customer could reach anyway.
|
||||||
|
## * Ownership changes use `chown -h` / `chown -R` (GNU default -P: never
|
||||||
|
## follows symlinks, lchown()s them), never a dereferencing chown.
|
||||||
|
## * Customer files are READ as the customer (crontab), never as root.
|
||||||
|
|
||||||
|
# as_user CMD... : run CMD as the customer uid/gid (no root, no caps).
|
||||||
|
as_user() {
|
||||||
|
if command -v setpriv >/dev/null 2>&1; then
|
||||||
|
setpriv --reuid="$uid" --regid="$uid" --init-groups -- "$@"
|
||||||
|
else
|
||||||
|
runuser -u "$user" -- "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# die MSG : refuse to start. Only the customer who planted the path is hurt,
|
||||||
|
# which is preferred to root following it.
|
||||||
|
die() {
|
||||||
|
echo "entrypoint: FATAL: $* - refusing to start" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# drop_symlink PATH : if PATH is a symlink, unlink it AS THE CUSTOMER (rm on
|
||||||
|
# the final component never touches the link target, and if a parent dir is
|
||||||
|
# swapped for a symlink mid-way the customer can only delete what they already
|
||||||
|
# could) and log it. Fails closed: still a symlink afterwards -> refuse to start.
|
||||||
|
drop_symlink() {
|
||||||
|
if [ -L "$1" ]; then
|
||||||
|
echo "entrypoint: SECURITY: $1 was a symlink to '$(readlink "$1")' - removing" >&2
|
||||||
|
as_user rm -f -- "$1"
|
||||||
|
[ -L "$1" ] && die "could not remove symlink $1"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# secure_home : run once, right after adduser, before anything else touches
|
||||||
|
# /home/$user.
|
||||||
|
secure_home() {
|
||||||
|
local h="/home/$user"
|
||||||
|
if [ -L "$h" ] || [ ! -d "$h" ]; then
|
||||||
|
die "$h is not a real directory (symlink or missing)"
|
||||||
|
fi
|
||||||
|
# The mount point itself: owned by the customer so as_user can create in it.
|
||||||
|
chown -h "$user:$user" "$h"
|
||||||
|
drop_symlink "$h/logs"
|
||||||
|
# public_html may legitimately be a symlink (release-dir deploys); it is left
|
||||||
|
# alone and every root-side operation on it runs as the customer instead.
|
||||||
|
if [ -d "$h/logs" ]; then
|
||||||
|
# As the customer: find without -L never follows symlinks, -delete unlinks
|
||||||
|
# the link itself, and a raced parent swap can only reach what the customer
|
||||||
|
# could already delete. Errors are not fatal HERE: require_safe_logs
|
||||||
|
# re-checks immediately before the daemons start and fails closed.
|
||||||
|
as_user find "$h/logs" -xdev -type l \
|
||||||
|
-printf 'entrypoint: SECURITY: %p was a symlink to %l - removed\n' -delete >&2 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# user_mkdir DIR... : create directories as the customer.
|
||||||
|
user_mkdir() {
|
||||||
|
as_user mkdir -p -- "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# user_touch FILE : create FILE as the customer (a symlink there is dropped
|
||||||
|
# first, fail-closed). Root never touches/chowns it: if it already exists
|
||||||
|
# root-owned (left by an older image) the touch fails quietly and the
|
||||||
|
# entrypoint's chown -R, which never follows symlinks, fixes the owner. Must end
|
||||||
|
# up a regular file, or refuse to start.
|
||||||
|
user_touch() {
|
||||||
|
drop_symlink "$1"
|
||||||
|
as_user touch -- "$1" 2>/dev/null || true
|
||||||
|
[ -f "$1" ] && [ ! -L "$1" ] || die "$1 is not a regular file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# require_safe_logs DIR... : fail-closed gate, run immediately before starting
|
||||||
|
# the daemons that open files under /home/$user/logs AS ROOT (httpd, php-fpm).
|
||||||
|
# Each DIR must be a real directory, and nothing under logs/ may be anything
|
||||||
|
# but a regular file or directory (no symlink, fifo, socket, device).
|
||||||
|
require_safe_logs() {
|
||||||
|
local d bad
|
||||||
|
for d in "/home/$user/logs" "$@"; do
|
||||||
|
[ -d "$d" ] && [ ! -L "$d" ] || die "$d is not a real directory"
|
||||||
|
done
|
||||||
|
bad=$(find "/home/$user/logs" -xdev ! -type f ! -type d -print -quit 2>&1)
|
||||||
|
[ -z "$bad" ] || die "unsafe entry under /home/$user/logs: $bad"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# load_user_crontab : install /home/$user/crontab, read AS THE CUSTOMER. Root
|
||||||
|
# reading it (`crontab -u $user /home/$user/crontab`) would follow a symlink to
|
||||||
|
# any root-readable file and install it where the customer can `crontab -l` it.
|
||||||
|
load_user_crontab() {
|
||||||
|
local f="/home/$user/crontab"
|
||||||
|
if [ ! -e "$f" ] && [ ! -L "$f" ]; then
|
||||||
|
as_user sh -c 'printf "%s\n" "# User crontab for $1" "# Add your cron jobs here" "# Example: */5 * * * * /home/$1/scripts/my-script.sh" > "$2"' sh "$user" "$f"
|
||||||
|
fi
|
||||||
|
# No temp file: root writing a predictable /tmp path is the same bug class.
|
||||||
|
local content
|
||||||
|
if content=$(as_user cat -- "$f" 2>/dev/null); then
|
||||||
|
printf '%s\n' "$content" | crontab -u "$user" -
|
||||||
|
else
|
||||||
|
echo "entrypoint: WARNING: $f not readable as $user - user crontab not loaded" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# open_dir_traversal : chmod 755 the home dir, then add o+x to every directory
|
||||||
|
# lacking it (apache/nginx run as their own user and must traverse). Run AS THE
|
||||||
|
# CUSTOMER: `find -type d` without -L skips symlinks, but chmod would follow a
|
||||||
|
# dir swapped for a symlink between find and chmod; as the customer that can
|
||||||
|
# only reach what the customer already owns.
|
||||||
|
open_dir_traversal() {
|
||||||
|
chmod 755 "/home/$user"
|
||||||
|
as_user find "/home/$user" -type d ! -perm -o=x -exec chmod o+x {} + 2>/dev/null || true
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
## stop-services.sh — sourced by the bash PID-1 entrypoints (entrypoint.sh,
|
||||||
|
## entrypoint-fpm.sh, entrypoint-shared-httpd.sh). Not executed directly.
|
||||||
|
##
|
||||||
|
## Why this exists: those entrypoints end with bash as PID 1 tailing logs.
|
||||||
|
## The kernel never delivers a signal to PID 1 unless PID 1 installed a handler
|
||||||
|
## for it, so `docker stop`'s SIGTERM was silently dropped. Docker waited the
|
||||||
|
## full stop timeout (10s) and then SIGKILLed the whole container, killing
|
||||||
|
## apache / php-fpm mid-request (exit 137). The entrypoints now trap TERM/INT
|
||||||
|
## and call stop_service for each daemon they started, then exit 0.
|
||||||
|
##
|
||||||
|
## Budget: docker's default stop timeout is 10s. Callers pass per-service
|
||||||
|
## timeouts that sum to < 10s so the graceful path always completes before
|
||||||
|
## docker gives up and SIGKILLs.
|
||||||
|
|
||||||
|
# A pid counts as alive if it exists and is not a zombie. bash (PID 1) reaps
|
||||||
|
# the daemons it adopted, but between exit and reap a zombie still answers
|
||||||
|
# `kill -0`, so check /proc state too.
|
||||||
|
_svc_alive() {
|
||||||
|
[ -n "$1" ] || return 1
|
||||||
|
kill -0 "$1" 2>/dev/null || return 1
|
||||||
|
! grep -qs '^State:[[:space:]]*Z' "/proc/$1/status"
|
||||||
|
}
|
||||||
|
|
||||||
|
# stop_service NAME PIDFILE GRACEFUL_SIGNAL GRACE_SECONDS [PGREP_PATTERN]
|
||||||
|
# Sends GRACEFUL_SIGNAL, waits up to GRACE_SECONDS for the process to exit,
|
||||||
|
# then sends TERM and waits 1s more, then KILL. Pid comes from PIDFILE, or
|
||||||
|
# from `pgrep -o -f PGREP_PATTERN` when the pidfile is missing/stale.
|
||||||
|
stop_service() {
|
||||||
|
local name=$1 pidfile=$2 sig=$3 grace=$4 pat=${5:-} pid="" i
|
||||||
|
[ -r "$pidfile" ] && pid=$(tr -dc '0-9' < "$pidfile")
|
||||||
|
if ! _svc_alive "$pid" && [ -n "$pat" ]; then
|
||||||
|
pid=$(pgrep -o -f "$pat" 2>/dev/null)
|
||||||
|
fi
|
||||||
|
if ! _svc_alive "$pid"; then
|
||||||
|
echo "entrypoint: $name not running"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "entrypoint: stopping $name (pid $pid) with SIG$sig"
|
||||||
|
kill -s "$sig" "$pid" 2>/dev/null
|
||||||
|
for ((i = 0; i < grace * 10; i++)); do
|
||||||
|
_svc_alive "$pid" || { echo "entrypoint: $name stopped"; return 0; }
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
echo "entrypoint: $name still running after ${grace}s, sending SIGTERM"
|
||||||
|
kill -TERM "$pid" 2>/dev/null
|
||||||
|
for ((i = 0; i < 10; i++)); do
|
||||||
|
_svc_alive "$pid" || { echo "entrypoint: $name stopped"; return 0; }
|
||||||
|
sleep 0.1
|
||||||
|
done
|
||||||
|
echo "entrypoint: $name still running, sending SIGKILL"
|
||||||
|
kill -KILL "$pid" 2>/dev/null
|
||||||
|
return 0
|
||||||
|
}
|
||||||
Executable
+99
@@ -0,0 +1,99 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
## lsphp-abi-gate.test.sh — proves scripts/lsphp-abi-gate.sh can FAIL.
|
||||||
|
##
|
||||||
|
## The gate is what lets the emergency php.net lsphp build keep LiteSpeed's
|
||||||
|
## packaged PECL .so files. A gate that always says "compatible" would be worse
|
||||||
|
## than none, so every rule is exercised against synthetic header trees whose
|
||||||
|
## outcome is known: three layouts that must pass (unchanged; new member in
|
||||||
|
## padding; member appended to a *_globals struct) and five that must fail
|
||||||
|
## (member inserted mid-struct; member removed; member retyped/resized; a
|
||||||
|
## non-globals struct grown at the end; a struct embedded in another growing).
|
||||||
|
## Plus: a probe that extracts too few members must refuse, not pass.
|
||||||
|
##
|
||||||
|
## Needs gcc + pahole (dwarves). Exits 0 only if every case behaves.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HERE=$(cd "$(dirname "$0")/.." && pwd)
|
||||||
|
GATE="$HERE/lsphp-abi-gate.sh"
|
||||||
|
for t in gcc pahole; do
|
||||||
|
command -v "$t" >/dev/null 2>&1 || { echo "SKIP-FAIL: $t not installed (apt-get install gcc dwarves)" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
T=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$T"' EXIT
|
||||||
|
|
||||||
|
## $1 = dir, $2 = body of struct _zend_object, $3 = body of _zend_executor_globals,
|
||||||
|
## $4 = extra declarations
|
||||||
|
mk() {
|
||||||
|
mkdir -p "$1/php/main"
|
||||||
|
cat >"$1/php/main/php.h" <<EOF
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
struct _zend_object { $2 };
|
||||||
|
struct _zend_executor_globals { $3 };
|
||||||
|
struct _holder { struct _zend_object obj; int after; };
|
||||||
|
typedef struct { int a; void *b; } anon_cache;
|
||||||
|
$4
|
||||||
|
struct _zend_executor_globals executor_globals;
|
||||||
|
struct _holder holder_sym;
|
||||||
|
anon_cache anon_sym;
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
BASE_OBJ='uint32_t gc; uint32_t handle; bool flag; void *ce; void *handlers;'
|
||||||
|
BASE_EG='void *a; int b; void *reserved[4];'
|
||||||
|
mk "$T/old" "$BASE_OBJ" "$BASE_EG" ''
|
||||||
|
|
||||||
|
PASS=0; FAILN=0
|
||||||
|
expect() { # $1 = pass|fail, $2 = name, $3 = new dir
|
||||||
|
local rc=0
|
||||||
|
ABI_MIN_MEMBERS=5 bash "$GATE" "$T/old" "$3" "$T/w-$2" >"$T/out-$2" 2>&1 || rc=$?
|
||||||
|
if { [ "$1" = pass ] && [ "$rc" = 0 ]; } || { [ "$1" = fail ] && [ "$rc" != 0 ]; }; then
|
||||||
|
PASS=$((PASS + 1)); echo "ok - $2 ($1, rc=$rc)"
|
||||||
|
else
|
||||||
|
FAILN=$((FAILN + 1)); echo "FAIL - $2: expected $1, got rc=$rc"; sed 's/^/ /' "$T/out-$2"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
mk "$T/same" "$BASE_OBJ" "$BASE_EG" ''
|
||||||
|
expect pass unchanged "$T/same"
|
||||||
|
|
||||||
|
## bool after `flag` lands in the 3 padding bytes before the pointer `ce`
|
||||||
|
mk "$T/pad" 'uint32_t gc; uint32_t handle; bool flag; bool newflag; void *ce; void *handlers;' "$BASE_EG" ''
|
||||||
|
expect pass new-member-in-padding "$T/pad"
|
||||||
|
|
||||||
|
mk "$T/eg" "$BASE_OBJ" 'void *a; int b; void *reserved[4]; void *delayed_oom;' ''
|
||||||
|
expect pass globals-appended "$T/eg"
|
||||||
|
|
||||||
|
mk "$T/mid" 'uint32_t gc; void *inserted; uint32_t handle; bool flag; void *ce; void *handlers;' "$BASE_EG" ''
|
||||||
|
expect fail member-inserted-mid-struct "$T/mid"
|
||||||
|
|
||||||
|
mk "$T/rm" 'uint32_t gc; uint32_t handle; bool flag; void *handlers;' "$BASE_EG" ''
|
||||||
|
expect fail member-removed "$T/rm"
|
||||||
|
|
||||||
|
mk "$T/retype" 'uint32_t gc; uint64_t handle; bool flag; void *ce; void *handlers;' "$BASE_EG" ''
|
||||||
|
expect fail member-resized "$T/retype"
|
||||||
|
|
||||||
|
## appended to a NON-globals struct: no old member moves, but sizeof grows —
|
||||||
|
## extensions that allocate sizeof(zend_object)+x would under-allocate
|
||||||
|
mk "$T/grow" 'uint32_t gc; uint32_t handle; bool flag; void *ce; void *handlers; void *extra;' "$BASE_EG" ''
|
||||||
|
expect fail non-globals-struct-grew "$T/grow"
|
||||||
|
|
||||||
|
## same change seen from the embedding struct: the report must name _holder.after
|
||||||
|
## as MOVED, i.e. the container check works on its own, not only via the size rule
|
||||||
|
if grep -E '^BAD moved member _holder int_after#1' "$T/out-non-globals-struct-grew" >/dev/null; then
|
||||||
|
PASS=$((PASS + 1)); echo "ok - embedded-struct-shifts-container (reported _holder.after moved)"
|
||||||
|
else
|
||||||
|
FAILN=$((FAILN + 1)); echo "FAIL - embedded-struct-shifts-container: _holder.after not reported"; sed 's/^/ /' "$T/out-non-globals-struct-grew"
|
||||||
|
fi
|
||||||
|
|
||||||
|
## the anonymous typedef is tracked too
|
||||||
|
mkdir -p "$T/anon/php/main"
|
||||||
|
sed 's/typedef struct { int a; void \*b; } anon_cache;/typedef struct { int a; int z; void *b; long c; } anon_cache;/' "$T/old/php/main/php.h" >"$T/anon/php/main/php.h"
|
||||||
|
expect fail anonymous-typedef-changed "$T/anon"
|
||||||
|
|
||||||
|
## a probe that finds (almost) nothing must refuse
|
||||||
|
rc=0; ABI_MIN_MEMBERS=1000 bash "$GATE" "$T/old" "$T/same" "$T/w-min" >"$T/out-min" 2>&1 || rc=$?
|
||||||
|
if [ "$rc" != 0 ] && grep 'probe is broken' "$T/out-min" >/dev/null; then PASS=$((PASS + 1)); echo "ok - too-few-members-refused (rc=$rc)"
|
||||||
|
else FAILN=$((FAILN + 1)); echo "FAIL - too-few-members-refused: rc=$rc"; cat "$T/out-min"; fi
|
||||||
|
|
||||||
|
echo "lsphp-abi-gate: $PASS passed, $FAILN failed"
|
||||||
|
[ "$FAILN" = 0 ]
|
||||||
Executable
+564
@@ -0,0 +1,564 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
## lsphp-info-probe.test.sh — regression test for the SIGPIPE-under-pipefail
|
||||||
|
## class of bug that shipped in cac-lsphp (trunk 9343a56) and silently turned
|
||||||
|
## $_SERVER path parity off on every shared-ols site whose host lost the race.
|
||||||
|
##
|
||||||
|
## THE BUG. entrypoint-lsphp.sh asked "is cac_path_parity loaded?" with
|
||||||
|
##
|
||||||
|
## printf '%s\n' "$LSPHP_INFO" | grep -q '^cac_path_parity support => enabled$'
|
||||||
|
##
|
||||||
|
## under `set -euo pipefail`. `grep -q` exits the instant it matches; printf is
|
||||||
|
## still pushing the rest of `lsphp -i` into the pipe, takes SIGPIPE, and exits
|
||||||
|
## 141; pipefail prefers that over grep's 0. So the test read FALSE **because
|
||||||
|
## the extension was present** — present early enough in the output to stop the
|
||||||
|
## reader — and the container fell back to the degraded auto_prepend normaliser
|
||||||
|
## the extension exists to replace, while telling the operator the extension was
|
||||||
|
## "not loadable in this image".
|
||||||
|
##
|
||||||
|
## THE RULE THIS FILE ENFORCES, stated so nobody re-derives a wrong one: ANY
|
||||||
|
## `writer | early-exiting-reader` under pipefail is a latent 141. PAYLOAD SIZE
|
||||||
|
## IS NOT A SAFETY ARGUMENT — each run is a race on whether the reader's close
|
||||||
|
## lands before the writer's final write() returns, and size only sets how many
|
||||||
|
## write() syscalls the writer must survive. Measured against a default
|
||||||
|
## 65536-byte pipe: 41144 bytes SIGPIPEd on 32/300 runs (11%, well UNDER
|
||||||
|
## capacity) and 65012 bytes on 25/30 (not certain even AT capacity); 500 KB
|
||||||
|
## into a 1 MiB pipe was 200/200 when written 4096 bytes at a time and 0/200 as
|
||||||
|
## a single write. A call site is sound only for a STRUCTURAL reason: the file
|
||||||
|
## does not set pipefail, or the reader provably consumes to EOF (no `q`, `-q`,
|
||||||
|
## `-l`, `-m`, `exit`, `break`), or the pipeline's status is discarded. Section 4
|
||||||
|
## below prints this race happening at ~41 KB; if the numbers there ever read as
|
||||||
|
## "small payloads are fine", the numbers are right and the reading is wrong.
|
||||||
|
##
|
||||||
|
## WHY THE EXISTING SUITE DID NOT CATCH IT. The .phpt suite and
|
||||||
|
## fpm-parity-check.sh both test the EXTENSION; nothing executed the
|
||||||
|
## ENTRYPOINT's branch logic, and the Dockerfile's own `lsphp -i | grep -q`
|
||||||
|
## build gate runs under `bash -c 'set -e'` with NO pipefail, so it reported the
|
||||||
|
## extension present in the very image whose entrypoint declared it missing.
|
||||||
|
##
|
||||||
|
## WHAT THIS ASSERTS.
|
||||||
|
## 1. behaviour — the SHIPPED probe helpers (extracted verbatim from
|
||||||
|
## entrypoint-lsphp.sh, never copied, so they cannot drift) return the
|
||||||
|
## right answer AND a clean exit status under `set -euo pipefail` with a
|
||||||
|
## realistic ~40 KB phpinfo body.
|
||||||
|
## 2. structure — no script in this repo that enables pipefail pipes into a
|
||||||
|
## reader that can exit before its writer finishes. This is the check that
|
||||||
|
## fails deterministically against trunk; assertion 1's *old* form is a
|
||||||
|
## RACE (measured 141 on 3 of 5 runs here, 5 of 5 on whp02, and 0 of 5
|
||||||
|
## under a different Docker daemon), so no behavioural assertion about the
|
||||||
|
## broken code could be trusted to fail on every machine. Section 4 runs
|
||||||
|
## the old form anyway and prints what it did, for the record.
|
||||||
|
## 3. equivalence — the helpers' pure-bash matching answers exactly what the
|
||||||
|
## grep/awk patterns they replaced answer, checked case by case against
|
||||||
|
## those same patterns reading a FILE (a file, so the reference itself
|
||||||
|
## cannot SIGPIPE). Section 6.
|
||||||
|
## 4. the scan's own coverage — every reader shape section 5 claims to catch
|
||||||
|
## is caught, and a matched set of safe shapes is NOT flagged. Section 7.
|
||||||
|
## Without this the scan's regexes are unfalsified and can quietly stop
|
||||||
|
## matching; `grep -l` and `sed q` were both missed until section 7 existed.
|
||||||
|
##
|
||||||
|
## SCOPE LIMITS, stated rather than hidden. The structural scan is a text scan,
|
||||||
|
## so it under-reports in known ways:
|
||||||
|
## - it reads one line at a time. The repo's only multi-line pipeline
|
||||||
|
## (ols-htaccess-watcher.sh's `inotifywait … |` / `while read`) is invisible
|
||||||
|
## to it and was reviewed by hand: that reader loops until EOF, i.e. until
|
||||||
|
## the writer has already gone, so it cannot produce this failure.
|
||||||
|
## - its quote stripping is flat, so a pipe nested inside a command
|
||||||
|
## substitution inside a quoted string (`echo "x ($(a | head -1))"`) is read
|
||||||
|
## as quoted text and skipped.
|
||||||
|
## - the reader list is an ENUMERATION, not a proof. It knows `grep`
|
||||||
|
## (-q/-l/-L/-m and their long forms), `head`, `read`, `awk … exit` and
|
||||||
|
## `sed` with a q/Q command; it does not know an early exit hidden in
|
||||||
|
## `perl -ne '… last'`, `python -c`, `jq`, `head -c`, or any project-local
|
||||||
|
## program that stops reading. A reader not on the list is not thereby safe.
|
||||||
|
## - it only inspects the FIRST word after a pipe, so `foo | LC_ALL=C grep -q`
|
||||||
|
## or `foo | { grep -q x; }` reads as an unknown reader and is skipped.
|
||||||
|
## It is a guard against reintroducing the shape, not a proof of its absence.
|
||||||
|
##
|
||||||
|
## Usage: scripts/tests/lsphp-info-probe.test.sh [REPO_ROOT]
|
||||||
|
## Exit: 0 all assertions passed, 1 an assertion FAILED, 2 the test could not run.
|
||||||
|
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
ROOT="${1:-$(cd "$HERE/../.." && pwd)}"
|
||||||
|
ENTRYPOINT="$ROOT/scripts/entrypoint-lsphp.sh"
|
||||||
|
TMP="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TMP"' EXIT
|
||||||
|
|
||||||
|
pass=0; fail=0
|
||||||
|
ok() { pass=$((pass+1)); echo " ok — $1"; }
|
||||||
|
bad() { fail=$((fail+1)); echo " FAIL — $1" >&2; }
|
||||||
|
die() { echo "HARNESS FAILURE: $*" >&2; exit 2; }
|
||||||
|
|
||||||
|
[ -f "$ENTRYPOINT" ] || die "no entrypoint at $ENTRYPOINT (pass REPO_ROOT as \$1)"
|
||||||
|
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
## 1. Extract the real probe helpers. Not a copy: whatever the shipped
|
||||||
|
## entrypoint says between the markers is what runs below, so a future edit
|
||||||
|
## that reintroduces a pipeline is tested, not narrated.
|
||||||
|
##
|
||||||
|
## A missing block is an assertion FAILURE, not a harness error, and it does
|
||||||
|
## not stop the run: against a pre-fix checkout the probes are still inline
|
||||||
|
## pipelines, and section 5 below is what names them. Bailing out here would
|
||||||
|
## have replaced that report with "could not run".
|
||||||
|
##
|
||||||
|
## BOUNDED ON PURPOSE. The extraction buffers and emits NOTHING until it has
|
||||||
|
## seen the END marker, so deleting or mistyping that one line is a marker
|
||||||
|
## error (exit 4) rather than a slurp of every line after BEGIN into a file
|
||||||
|
## this harness then `source`s. That is not hypothetical: it happened during
|
||||||
|
## development and only failed loudly by luck — `set -u` tripped over an
|
||||||
|
## unbound variable two statements into the entrypoint's real boot code. An
|
||||||
|
## extractor whose failure mode is "execute arbitrary parts of the program
|
||||||
|
## under test" is not a safe thing to leave lying around, however careful the
|
||||||
|
## markers are today.
|
||||||
|
## exit 0 = both markers, block on stdout
|
||||||
|
## exit 3 = no BEGIN marker at all (pre-fix checkout, or block removed)
|
||||||
|
## exit 4 = BEGIN seen, END missing — refuse to emit, refuse to source
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
HAVE_HELPERS=yes
|
||||||
|
XRC=0
|
||||||
|
awk '
|
||||||
|
/^## ---- CAC-TEST: probe helpers BEGIN ----$/ { f = 1 }
|
||||||
|
f { buf = buf $0 ORS }
|
||||||
|
f && /^## ---- CAC-TEST: probe helpers END ----$/ { printf "%s", buf; found = 1; exit 0 }
|
||||||
|
END { if (found) exit 0; else if (f) exit 4; else exit 3 }
|
||||||
|
' "$ENTRYPOINT" > "$TMP/helpers.sh" || XRC=$?
|
||||||
|
|
||||||
|
case "$XRC" in
|
||||||
|
0)
|
||||||
|
for fn in lsphp_info_has_parity_ext lsphp_info_scan_dir lsphp_info_is_usable; do
|
||||||
|
if ! grep -q "^${fn}()" "$TMP/helpers.sh"; then
|
||||||
|
HAVE_HELPERS=no
|
||||||
|
bad "the probe-helper block in ${ENTRYPOINT#"$ROOT"/} defines no ${fn}()"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
HAVE_HELPERS=no
|
||||||
|
: > "$TMP/helpers.sh"
|
||||||
|
bad "${ENTRYPOINT#"$ROOT"/} has a 'CAC-TEST: probe helpers BEGIN' marker with no matching END marker. Nothing was extracted — an unterminated block would otherwise have pulled the whole rest of the entrypoint into a file this test sources and runs."
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
HAVE_HELPERS=no
|
||||||
|
bad "no probe-helper markers in ${ENTRYPOINT#"$ROOT"/} — either they were removed, or this is a pre-fix checkout where the probes are still inline pipelines (trunk 9343a56). Section 5 says which lines."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
HAVE_HELPERS=no
|
||||||
|
bad "extracting the probe-helper block from ${ENTRYPOINT#"$ROOT"/} failed (awk exit $XRC)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
## 2. Fixtures. Shaped like real `lsphp -i`: the two lines the probes look for
|
||||||
|
## sit near the TOP (that is what lets a reader stop early), with tens of KB
|
||||||
|
## of body after them. A fixture whose marker is near the end could not
|
||||||
|
## SIGPIPE at all and would make this whole test vacuous, so both properties
|
||||||
|
## are asserted before anything else runs.
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
SCAN_PATH='/usr/local/lsws/lsphp83/etc/php/8.3/mods-available'
|
||||||
|
make_fixture() { # $1=outfile $2=yes|no (include the cac_path_parity line)
|
||||||
|
{
|
||||||
|
printf 'phpinfo()\nPHP Version => 8.3.27\n\n'
|
||||||
|
printf 'System => Linux 6ecb4e0a1c1f 5.15.0 #1 SMP x86_64\n'
|
||||||
|
printf 'Server API => LiteSpeed V8.3\n'
|
||||||
|
printf 'Configuration File (php.ini) Path => /usr/local/lsws/lsphp83/etc/php/8.3/litespeed\n'
|
||||||
|
printf 'Scan this dir for additional .ini files => %s\n' "$SCAN_PATH"
|
||||||
|
printf 'PHP API => 20230831\nDebug Build => no\nThread Safety => disabled\n\n'
|
||||||
|
printf 'bcmath\n\nBCMath support => enabled\n\n'
|
||||||
|
printf 'calendar\n\nCalendar support => enabled\n\n'
|
||||||
|
[ "$2" = yes ] && printf 'cac_path_parity\n\ncac_path_parity support => enabled\nRewriting => active\n\n'
|
||||||
|
printf 'Core\n\nPHP Version => 8.3.27\n\n'
|
||||||
|
## ~40 KB of directive rows, exactly the shape phpinfo() prints them in.
|
||||||
|
local i=0
|
||||||
|
while [ "$i" -lt 700 ]; do
|
||||||
|
printf 'some.directive_%03d => local_value_%03d => master_value_%03d\n' "$i" "$i" "$i"
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
} > "$1"
|
||||||
|
}
|
||||||
|
make_fixture "$TMP/info-present.txt" yes
|
||||||
|
make_fixture "$TMP/info-absent.txt" no
|
||||||
|
: > "$TMP/info-empty.txt"
|
||||||
|
|
||||||
|
echo "== fixture sanity =="
|
||||||
|
FIX_BYTES=$(wc -c < "$TMP/info-present.txt")
|
||||||
|
MATCH_OFF=$(grep -b -m1 '^cac_path_parity support => enabled$' "$TMP/info-present.txt" | cut -d: -f1)
|
||||||
|
if [ "$FIX_BYTES" -ge 32768 ]; then
|
||||||
|
ok "fixture is ${FIX_BYTES} bytes (realistic 'lsphp -i' is ~40 KB)"
|
||||||
|
else
|
||||||
|
bad "fixture is only ${FIX_BYTES} bytes — too small to reproduce the failure"
|
||||||
|
fi
|
||||||
|
if [ "$MATCH_OFF" -lt $((FIX_BYTES / 4)) ]; then
|
||||||
|
ok "match sits at byte ${MATCH_OFF} of ${FIX_BYTES} — most of the body is still unwritten when a reader could stop"
|
||||||
|
else
|
||||||
|
bad "match at byte ${MATCH_OFF} of ${FIX_BYTES} leaves too small a tail; the test would be vacuous"
|
||||||
|
fi
|
||||||
|
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
## 3. Behaviour, under the REAL option set (`set -euo pipefail`, as line 34 of
|
||||||
|
## the entrypoint sets it). Each case runs in its own bash process so the
|
||||||
|
## options and the exit status are the genuine article, not something this
|
||||||
|
## harness simulated.
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
cat > "$TMP/runner.sh" <<'RUNNER'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$1" # the extracted probe helpers
|
||||||
|
LSPHP_INFO=$(cat "$2")
|
||||||
|
case "$3" in
|
||||||
|
has_ext) lsphp_info_has_parity_ext "$LSPHP_INFO" ;;
|
||||||
|
scan_dir) lsphp_info_scan_dir "$LSPHP_INFO" ;;
|
||||||
|
usable) lsphp_info_is_usable "$LSPHP_INFO" ;;
|
||||||
|
legacy) printf '%s\n' "$LSPHP_INFO" | grep -q '^cac_path_parity support => enabled$' ;;
|
||||||
|
*) echo "unknown case $3" >&2; exit 99 ;;
|
||||||
|
esac
|
||||||
|
RUNNER
|
||||||
|
|
||||||
|
## NOT `out=$(run …)`: command substitution runs the function in a subshell,
|
||||||
|
## where an exit status assigned to RC would be thrown away with it. Run in this
|
||||||
|
## shell, capture stdout through a file, and RC is the real thing.
|
||||||
|
RC=0; out=""
|
||||||
|
run() { # $1=fixture $2=case -> sets RC and out
|
||||||
|
RC=0
|
||||||
|
bash "$TMP/runner.sh" "$TMP/helpers.sh" "$1" "$2" >"$TMP/out" 2>&1 || RC=$?
|
||||||
|
out=$(cat "$TMP/out")
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "== the shipped probes under set -euo pipefail =="
|
||||||
|
if [ "$HAVE_HELPERS" = no ]; then
|
||||||
|
echo " (skipped — no probe helpers to run; see the failure above)"
|
||||||
|
fi
|
||||||
|
if [ "$HAVE_HELPERS" = yes ]; then
|
||||||
|
|
||||||
|
run "$TMP/info-present.txt" has_ext
|
||||||
|
if [ "$RC" -eq 0 ]; then
|
||||||
|
ok "extension present => branch TAKEN (exit 0)"
|
||||||
|
elif [ "$RC" -eq 141 ]; then
|
||||||
|
bad "exit 141 (SIGPIPE): the probe is a pipeline again — this is the original bug"
|
||||||
|
else
|
||||||
|
bad "extension present => exit $RC (expected 0). Output: $out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run "$TMP/info-absent.txt" has_ext
|
||||||
|
if [ "$RC" -eq 1 ]; then
|
||||||
|
ok "extension genuinely absent => branch NOT taken (exit 1, a clean 'no')"
|
||||||
|
else
|
||||||
|
bad "extension absent => exit $RC (expected 1). Output: $out"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run "$TMP/info-present.txt" scan_dir
|
||||||
|
if [ "$RC" -eq 0 ] && [ "$out" = "$SCAN_PATH" ]; then
|
||||||
|
ok "scan-dir probe returns '$out' (exit 0)"
|
||||||
|
elif [ "$RC" -eq 141 ]; then
|
||||||
|
bad "scan-dir probe exit 141 (SIGPIPE) — as a bare assignment under set -e that KILLS PID 1"
|
||||||
|
else
|
||||||
|
bad "scan-dir probe => exit $RC, got '$out', expected '$SCAN_PATH'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run "$TMP/info-present.txt" usable
|
||||||
|
if [ "$RC" -eq 0 ]; then ok "usability probe: real phpinfo body => usable (exit 0)"
|
||||||
|
else bad "usability probe on real body => exit $RC (expected 0)"; fi
|
||||||
|
|
||||||
|
run "$TMP/info-empty.txt" usable
|
||||||
|
if [ "$RC" -eq 1 ]; then ok "usability probe: empty body => NOT usable (exit 1)"
|
||||||
|
else bad "usability probe on empty body => exit $RC (expected 1)"; fi
|
||||||
|
|
||||||
|
## The two failure causes must be reported as the different things they are.
|
||||||
|
## A probe that cannot answer has established nothing about the image, and the
|
||||||
|
## old message asserted the opposite of that for every reason it fired.
|
||||||
|
## SC2016: these patterns are the entrypoint's literal text, `$` and all.
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
if grep -q 'if lsphp_info_is_usable "$LSPHP_INFO"; then' "$ENTRYPOINT" &&
|
||||||
|
grep -q 'This is a PROBE failure and establishes nothing' "$ENTRYPOINT"; then
|
||||||
|
ok "a probe that produced nothing is reported as OUR failure, not as a verdict on the image"
|
||||||
|
else
|
||||||
|
bad "entrypoint no longer reports an unusable 'lsphp -i' as a probe failure"
|
||||||
|
fi
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
if grep -q 'not loadable in this image' "$ENTRYPOINT" &&
|
||||||
|
grep -q 'answered (${#LSPHP_INFO} bytes, scan dir ${SCAN_DIR}) and does not list it' "$ENTRYPOINT"; then
|
||||||
|
ok "the 'extension not loadable' verdict now ships the evidence it rests on"
|
||||||
|
else
|
||||||
|
bad "the 'extension not loadable' message no longer states what it observed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
fi # HAVE_HELPERS
|
||||||
|
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
## 4. The old form, for the record. NOT asserted: it is a race, and asserting a
|
||||||
|
## race would make this suite flap on whichever machine happens to win it.
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
echo "== the pre-fix pipeline form on the same input (informational) =="
|
||||||
|
if [ "$HAVE_HELPERS" = no ]; then
|
||||||
|
echo " (skipped — the runner needs the helper block to source)"
|
||||||
|
fi
|
||||||
|
legacy_rcs=""
|
||||||
|
if [ "$HAVE_HELPERS" = yes ]; then
|
||||||
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
run "$TMP/info-present.txt" legacy
|
||||||
|
legacy_rcs="$legacy_rcs $RC"
|
||||||
|
done
|
||||||
|
echo " 10 runs of 'printf | grep -q' under pipefail:${legacy_rcs}"
|
||||||
|
echo " (0 = happened to finish writing, 141 = writer SIGPIPEd and pipefail reported the"
|
||||||
|
echo " extension MISSING because it was present. Either value is expected here.)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
## 5. Structural scan — the deterministic guard, and the part of this file that
|
||||||
|
## fails against trunk on EVERY machine. Any script that turns on pipefail
|
||||||
|
## and pipes into a reader that can stop early has this bug latent in it
|
||||||
|
## whether or not today's buffer sizes expose it, so the shape is what gets
|
||||||
|
## outlawed, not the symptom.
|
||||||
|
##
|
||||||
|
## It is a text scan, so it is honest about its limits: it strips quoted
|
||||||
|
## spans and comments (that is what keeps `case a|b)`, `sed "s|x|y|"` and
|
||||||
|
## `echo "a | b"` from being reported), it requires the reader to be the
|
||||||
|
## FIRST word after a pipe, and it reads one line at a time. This file is
|
||||||
|
## skipped because section 4 runs the broken form on purpose.
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
echo "== structural scan: early-exit readers in pipefail scripts =="
|
||||||
|
mapfile -t PIPEFAIL_FILES < <(grep -rl --include='*.sh' -E '^[[:space:]]*set[[:space:]]+-[a-zA-Z]*[[:space:]]*o?[[:space:]]*pipefail|^[[:space:]]*set[[:space:]]+-o[[:space:]]+pipefail' "$ROOT/scripts" "$ROOT/ext" 2>/dev/null | sort)
|
||||||
|
[ "${#PIPEFAIL_FILES[@]}" -gt 0 ] || die "found no pipefail-enabled scripts under $ROOT — the scan would be vacuous"
|
||||||
|
|
||||||
|
cat > "$TMP/scan.awk" <<'AWKPROG'
|
||||||
|
# Does this `grep …` invocation stop reading before EOF? -q/-l/-L/-m do; -c,
|
||||||
|
# -i, -v, -o, -n and the rest read the whole input and are none of our business.
|
||||||
|
# Walked option by option rather than pattern-matched in one go, because the
|
||||||
|
# letters have to be read the way grep reads them: a cluster like -im1 stops
|
||||||
|
# early, -e/-f/-A/-B/-C/-d/-D swallow the rest of their token as an ARGUMENT
|
||||||
|
# (so `grep -eq` is the pattern "q", not --quiet), and the first non-option word
|
||||||
|
# is the pattern, after which nothing is a flag any more.
|
||||||
|
function grep_stops_early(s, a, k, j, t, c, m) {
|
||||||
|
if (s !~ /^[[:space:]]*grep([[:space:];&)]|$)/) return 0
|
||||||
|
sub(/^[[:space:]]*grep([[:space:]]+|$)/, "", s)
|
||||||
|
m = split(s, a, /[[:space:]]+/)
|
||||||
|
for (k = 1; k <= m; k++) {
|
||||||
|
t = a[k]
|
||||||
|
if (t == "") continue
|
||||||
|
if (t == "--") return 0
|
||||||
|
if (t ~ /^--/) {
|
||||||
|
if (t ~ /^--(quiet|silent|max-count|files-with-match|files-without-match)/) return 1
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (t !~ /^-/) return 0 # the pattern; options are over
|
||||||
|
sub(/^-/, "", t)
|
||||||
|
for (j = 1; j <= length(t); j++) {
|
||||||
|
c = substr(t, j, 1)
|
||||||
|
if (c == "q" || c == "l" || c == "L" || c == "m") return 1
|
||||||
|
if (c ~ /[efABCdD]/) break # rest of the token is its argument
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
{
|
||||||
|
raw = $0
|
||||||
|
l = raw
|
||||||
|
gsub(/\\"/, "X", l); gsub(/\\'/, "X", l) # escaped quotes are not delimiters
|
||||||
|
while (match(l, /'[^']*'/)) l = substr(l, 1, RSTART-1) "SQ" substr(l, RSTART+RLENGTH)
|
||||||
|
while (match(l, /"[^"]*"/)) l = substr(l, 1, RSTART-1) "DQ" substr(l, RSTART+RLENGTH)
|
||||||
|
if (l ~ /^[[:space:]]*#/) next # whole-line comment
|
||||||
|
sub(/[[:space:]]#.*/, "", l) # trailing comment
|
||||||
|
|
||||||
|
# `u` = the line with quote CHARACTERS dropped but their contents KEPT, used
|
||||||
|
# only to read a command's script argument. `l` cannot serve for that: it
|
||||||
|
# replaces a whole quoted span with a placeholder, so `sed 'q'` and
|
||||||
|
# `awk '{exit}'` lose the very token that makes them early-exit readers.
|
||||||
|
u = raw
|
||||||
|
gsub(/["']/, "", u)
|
||||||
|
sub(/[[:space:]]#.*/, "", u)
|
||||||
|
|
||||||
|
gsub(/\|\|/, " ", l) # || is not a pipeline
|
||||||
|
if (l !~ /\|/) next
|
||||||
|
n = split(l, seg, "|")
|
||||||
|
for (i = 2; i <= n; i++) {
|
||||||
|
r = seg[i]
|
||||||
|
if (grep_stops_early(r)) { print NR ": " raw; next }
|
||||||
|
# The `[[:space:];&)]|$` tail rather than a bare `[[:space:]]|$`: a reader
|
||||||
|
# can be the last word of a compound command (`… | head; }`), which the
|
||||||
|
# whitespace-only form silently skipped.
|
||||||
|
if (r ~ /^[[:space:]]*head([[:space:];&)]|$)/) { print NR ": " raw; next }
|
||||||
|
if (r ~ /^[[:space:]]*(while[[:space:]]+)?read([[:space:];&)]|$)/) { print NR ": " raw; next }
|
||||||
|
if (r ~ /^[[:space:]]*awk([[:space:];&)]|$)/ && u ~ /exit/) { print NR ": " raw; next }
|
||||||
|
# sed: a q/Q command anywhere in the script — `/re/q`, `2q`, `$q`, `1p;q`,
|
||||||
|
# `{…;q}`, and the bare `sed q` / `sed 'q'` that the earlier pattern missed
|
||||||
|
# (it required a `;`, `{` or `/` in front of the q, which a lone script has
|
||||||
|
# none of). Anchored on what may PRECEDE the q and what may FOLLOW it, so a
|
||||||
|
# q inside a replacement — `sed s/a/q/` — is not read as the command (which
|
||||||
|
# is what the trailing `/` exclusion buys: a command q is never followed by
|
||||||
|
# another delimiter, a replacement q always is).
|
||||||
|
if (r ~ /^[[:space:]]*sed([[:space:];&)]|$)/ &&
|
||||||
|
(u ~ /[;{\/][[:space:]]*[qQ]([^[:alnum:]\/]|$)/ ||
|
||||||
|
u ~ /[[:space:]]([0-9]+|\$)?[qQ]([[:space:];}]|$)/)) { print NR ": " raw; next }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AWKPROG
|
||||||
|
|
||||||
|
offenders=0
|
||||||
|
for f in "${PIPEFAIL_FILES[@]}"; do
|
||||||
|
[ "$(basename "$f")" = "$(basename "${BASH_SOURCE[0]}")" ] && continue
|
||||||
|
while IFS= read -r hit; do
|
||||||
|
offenders=$((offenders+1))
|
||||||
|
echo " FAIL — ${f#"$ROOT"/}:${hit}" >&2
|
||||||
|
done < <(awk -f "$TMP/scan.awk" "$f")
|
||||||
|
done
|
||||||
|
if [ "$offenders" -eq 0 ]; then
|
||||||
|
ok "${#PIPEFAIL_FILES[@]} pipefail-enabled scripts, no pipeline whose reader can outrun its writer"
|
||||||
|
else
|
||||||
|
bad "$offenders pipeline(s) above pipe into an early-exit reader under pipefail."
|
||||||
|
echo " Read the value into a variable and match it in the shell (see" >&2
|
||||||
|
echo " lsphp_info_has_parity_ext in scripts/entrypoint-lsphp.sh), or give the" >&2
|
||||||
|
echo " reader the file directly. Neither is a pipeline, so there is no second" >&2
|
||||||
|
echo " exit status for pipefail to prefer. A here-string also works, but it" >&2
|
||||||
|
echo " spills to a temp file above a build-dependent size, so it is not the" >&2
|
||||||
|
echo " right shape on a boot path." >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
## 6. Equivalence. The helpers answer with `[[ ]]` and `${…}` what they used to
|
||||||
|
## answer with grep and awk, and "same patterns, different plumbing" is a
|
||||||
|
## claim that has to be checked rather than asserted — an anchored line match
|
||||||
|
## re-expressed as a glob is exactly where an off-by-one lives.
|
||||||
|
##
|
||||||
|
## The reference runs the ORIGINAL grep/awk patterns over a FILE, so the
|
||||||
|
## reference itself cannot SIGPIPE and cannot be accused of the bug it is
|
||||||
|
## refereeing. The file is written the way the old pipeline fed them,
|
||||||
|
## `printf '%s\n' "$SUBJECT"`, so the comparison is against the pre-fix
|
||||||
|
## behaviour byte for byte and not against a tidier reading of it.
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
echo "== pure-bash matching vs the grep/awk patterns it replaced =="
|
||||||
|
if [ "$HAVE_HELPERS" = no ]; then
|
||||||
|
echo " (skipped — no probe helpers to compare)"
|
||||||
|
else
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
source "$TMP/helpers.sh"
|
||||||
|
|
||||||
|
NL=$'\n'
|
||||||
|
eq_fail=0
|
||||||
|
eq_case() { # $1 = label, $2 = subject
|
||||||
|
local label="$1" subj="$2" f="$TMP/eq.txt"
|
||||||
|
local ref_has ref_use new_has new_use ref_scan new_scan
|
||||||
|
printf '%s\n' "$subj" > "$f"
|
||||||
|
|
||||||
|
ref_has=0; grep -q '^cac_path_parity support => enabled$' "$f" || ref_has=$?
|
||||||
|
new_has=0; lsphp_info_has_parity_ext "$subj" || new_has=$?
|
||||||
|
ref_use=0; grep -q '^PHP Version => ' "$f" || ref_use=$?
|
||||||
|
new_use=0; lsphp_info_is_usable "$subj" || new_use=$?
|
||||||
|
ref_scan=$(awk -F'=> ' '/^Scan this dir/ {print $2; exit}' "$f")
|
||||||
|
new_scan=$(lsphp_info_scan_dir "$subj")
|
||||||
|
|
||||||
|
if [ "$ref_has" = "$new_has" ] && [ "$ref_use" = "$new_use" ] && [ "$ref_scan" = "$new_scan" ]; then
|
||||||
|
ok "equivalent on: $label"
|
||||||
|
else
|
||||||
|
eq_fail=$((eq_fail+1))
|
||||||
|
bad "NOT equivalent on: $label — has_ext grep=$ref_has bash=$new_has; usable grep=$ref_use bash=$new_use; scan_dir awk='$ref_scan' bash='$new_scan'"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
SD='Scan this dir for additional .ini files'
|
||||||
|
PARITY='cac_path_parity support => enabled'
|
||||||
|
eq_case "empty subject" ""
|
||||||
|
eq_case "match on the first line" "${PARITY}${NL}tail line"
|
||||||
|
eq_case "match on the last line" "head line${NL}${PARITY}"
|
||||||
|
eq_case "match is the only line" "${PARITY}"
|
||||||
|
eq_case "match sandwiched" "a${NL}${PARITY}${NL}b"
|
||||||
|
eq_case "not at line start (decoy)" "x ${PARITY}${NL}b"
|
||||||
|
eq_case "trailing space defeats the \$" "${PARITY} ${NL}b"
|
||||||
|
eq_case "prefix-only line (decoy)" "cac_path_parity support => enabled but no${NL}b"
|
||||||
|
eq_case "genuinely absent" "a${NL}b${NL}c"
|
||||||
|
eq_case "embedded blank lines" "a${NL}${NL}${PARITY}${NL}${NL}b"
|
||||||
|
eq_case "banner first, scan dir second" "PHP Version => 8.3.27${NL}${SD} => /a/b"
|
||||||
|
eq_case "scan dir on the first line" "${SD} => /a/b${NL}PHP Version => 8.3.27"
|
||||||
|
eq_case "scan dir on the last line" "PHP Version => 8.3.27${NL}${SD} => /a/b"
|
||||||
|
eq_case "scan dir, second separator" "${SD} => /a => /b${NL}x"
|
||||||
|
eq_case "scan dir, empty value" "${SD} => ${NL}x"
|
||||||
|
eq_case "scan dir, no separator" "Scan this dir is broken${NL}x"
|
||||||
|
eq_case "scan dir, first of two wins" "${SD} => /first${NL}${SD} => /second"
|
||||||
|
eq_case "scan dir, value has spaces" "${SD} => /a b/c ${NL}x"
|
||||||
|
eq_case "scan-dir line is a prefix" "Scan this directory => /a/b${NL}x"
|
||||||
|
eq_case "banner not at line start" "x PHP Version => 8.3.27${NL}b"
|
||||||
|
eq_case "banner without trailing space" "PHP Version =>${NL}b"
|
||||||
|
eq_case "CR-terminated lines" $'PHP Version => 8.3.27\r'"${NL}${PARITY}"$'\r'
|
||||||
|
eq_case "glob metacharacters in body" "*${NL}?${NL}[a-z]${NL}${PARITY}${NL}]["
|
||||||
|
eq_case "realistic 40 KB body" "$(cat "$TMP/info-present.txt")"
|
||||||
|
[ "$eq_fail" -eq 0 ] || echo " (an inequivalence here means the shipped probe now answers something the old grep/awk did not)" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
## 7. The scan's own coverage. Section 5 only proves something if its regexes
|
||||||
|
## actually match the shapes it claims to outlaw — an unfalsified scanner
|
||||||
|
## reports "clean" just as loudly when it has stopped matching anything.
|
||||||
|
## `grep -l foo` and `sed q` were both silently missed until this section
|
||||||
|
## existed; both are asserted below, alongside the safe forms that must NOT
|
||||||
|
## be reported, because a scanner that flags everything is no better.
|
||||||
|
## ---------------------------------------------------------------------------
|
||||||
|
echo "== the structural scan catches what it claims to =="
|
||||||
|
cat > "$TMP/scan-bad.sh" <<'BADSH'
|
||||||
|
set -euo pipefail
|
||||||
|
a() { producer | grep -q needle; }
|
||||||
|
b() { producer | grep -qx needle; }
|
||||||
|
c() { producer | grep -m1 needle; }
|
||||||
|
d() { producer | grep -im1 needle; }
|
||||||
|
e() { producer | grep -l foo; }
|
||||||
|
f() { producer | grep -L foo; }
|
||||||
|
g() { producer | grep --quiet foo; }
|
||||||
|
h() { producer | grep --files-with-matches foo; }
|
||||||
|
i() { producer | head -1; }
|
||||||
|
j() { producer | head; }
|
||||||
|
k() { producer | read -r x; }
|
||||||
|
l() { producer | while read -r x; do :; done; }
|
||||||
|
m() { producer | awk '/x/ {print; exit}'; }
|
||||||
|
n() { producer | sed q; }
|
||||||
|
o() { producer | sed 'q'; }
|
||||||
|
p() { producer | sed 2q; }
|
||||||
|
q() { producer | sed -n '1p;q'; }
|
||||||
|
r() { producer | sed -n '/x/{p;q}'; }
|
||||||
|
s() { producer | sed '$q'; }
|
||||||
|
BADSH
|
||||||
|
cat > "$TMP/scan-good.sh" <<'GOODSH'
|
||||||
|
set -euo pipefail
|
||||||
|
a() { producer | grep -c needle; }
|
||||||
|
b() { producer | grep -i needle; }
|
||||||
|
c() { producer | grep -v needle; }
|
||||||
|
d() { producer | grep -o needle; }
|
||||||
|
e() { producer | awk '{print $1}'; }
|
||||||
|
f() { producer | sed -n 's/^K=//p'; }
|
||||||
|
g() { producer | sed 's/a/q/'; }
|
||||||
|
h() { producer | sed -e 'y/abc/xqz/'; }
|
||||||
|
i() { producer | wc -l; }
|
||||||
|
j() { producer | sort -u; }
|
||||||
|
k() { producer | tail -1; }
|
||||||
|
l() { case $x in a|b) : ;; esac; }
|
||||||
|
m() { echo "a | head -1"; }
|
||||||
|
n() { echo 'x | grep -q y'; }
|
||||||
|
o() { grep -q needle <<<"$1"; }
|
||||||
|
p() { grep -q needle "$file"; }
|
||||||
|
# q() { producer | grep -q commented-out; }
|
||||||
|
r() { producer | grep -eq foo; }
|
||||||
|
GOODSH
|
||||||
|
missed=""; falsely=""; hits_bad=""
|
||||||
|
while IFS= read -r line; do
|
||||||
|
fn=${line#*: }; fn=${fn%%(*}
|
||||||
|
hits_bad="$hits_bad $fn"
|
||||||
|
done < <(awk -f "$TMP/scan.awk" "$TMP/scan-bad.sh")
|
||||||
|
for want in a b c d e f g h i j k l m n o p q r s; do
|
||||||
|
case " ${hits_bad:-} " in *" $want "*) ;; *) missed="$missed $want" ;; esac
|
||||||
|
done
|
||||||
|
mapfile -t good_hits < <(awk -f "$TMP/scan.awk" "$TMP/scan-good.sh")
|
||||||
|
if [ -z "$missed" ]; then
|
||||||
|
ok "all 19 early-exit reader shapes are reported (incl. grep -l/-L/--quiet and bare 'sed q')"
|
||||||
|
else
|
||||||
|
bad "the scan misses these shapes in scan-bad.sh:$missed"
|
||||||
|
awk -f "$TMP/scan.awk" "$TMP/scan-bad.sh" >&2
|
||||||
|
fi
|
||||||
|
if [ "${#good_hits[@]}" -eq 0 ]; then
|
||||||
|
ok "18 read-to-EOF / quoted / non-pipeline forms are not reported"
|
||||||
|
else
|
||||||
|
falsely=$(printf '%s; ' "${good_hits[@]}")
|
||||||
|
bad "the scan false-positives on: $falsely"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "passed: $pass failed: $fail"
|
||||||
|
[ "$fail" -eq 0 ] || exit 1
|
||||||
|
exit 0
|
||||||
Reference in new issue
Block a user