English | 日本語
Intended readers: people using Docker and GitHub Actions who wait for official image updates when a CVE drops. Reading time: 1 minute for "0. At a glance", 10 minutes for the whole thing.
On 2026-10-21, two unauthenticated RCE vulnerabilities in PHP were disclosed: CVE-2026-109618 (state corruption on OOM) and CVE-2026-109619 (state left over between requests) (PHP Foundation post).
This repository is a demo that, on the day the fixes were released, published fixed images with signatures and SBOMs (software bills of materials) for 7 versions (7.4 to 8.5) to ghcr.io.
How this document is organized
This document shows that, without waiting for official images or distribution updates, we were able to ship verifiable fixed images ourselves on the same day (and the procedure is kept in this repository).
| Section | Purpose | What it shows |
|---|---|---|
| 0. At a glance | One table of what becomes possible | Key points |
| 1. Ship without waiting | Speed, and why we don't have to wait | CI durations, pipeline diagram |
| 2. The same procedure down to EOL 7.4 | Fixed images even for unsupported versions | Where the patches come from, render.sh output |
| 3. Verifiable even when self-built | An answer to "self-built images are suspicious" | SBOM contents, how to verify signatures |
| 4. Minimal contents, small attack surface | The image contains as little as possible | nonroot, no shell, multi-arch |
| 5. Light to operate | Only rebuild what changed | Input hashes and caching |
| 6. What is not guaranteed | Limits | EOL patches and scanner limitations |
The output blocks in this document are regenerated and checked with the commands in checks.json. CI durations are the values from the linked runs and were not re-run. The intended reader is described in personas/demo-audience.md (a hypothetical persona).
| What you can do | Details | Evidence |
|---|---|---|
| Ship without waiting | From php.net's fixed tarball, signed images are published about 7 minutes after push | Section 1 |
| Ship for EOL versions too | 7.4 / 8.0 / 8.1 get the PHP security team's official backports, through the same procedure | Section 2 |
| Verifiable even when self-built | Source sha256 check, apk signatures, SBOM inside the image, cosign signatures, attached scan results | Section 3 |
| Small attack surface | distroless (no shell), runs as uid 65532, amd64 and arm64 | Section 4 |
| Light to operate | PHP is not recompiled when inputs are unchanged. A PR with a warm cache took 1 min 16 s | Section 5 |
All images live at ghcr.io/yowatari/php-oct2026-hotfix and can be pulled without authentication.
docker run --rm ghcr.io/yowatari/php-oct2026-hotfix:8.5 -v
docker run -d -p 9000:9000 ghcr.io/yowatari/php-oct2026-hotfix:8.5-fpm # php-fpm (listen 9000)Once the official fixed tarball is on php.net, you bump the version number and push, and signed images appear on ghcr.io. On the first run, it took about 7 minutes from push to a signed 8.5.12 image.
| Run | What it did | Duration |
|---|---|---|
| run #1 | First run. push → build → publish → smoke test → cosign signing (8.5.12, x86_64) | About 6 min 40 s (17:27:46 → 17:34:28 UTC) |
| run #5 | Build and test 7 versions × amd64/arm64 with no cache | About 9 min |
| run #7 | Same inputs again (with cache) | 1 min 16 s |
| run #12 | Publish 7 versions × amd64/arm64 on main, through smoke test, signing, verify and scan (with cache) | About 4 min |
We don't have to wait because we build PHP ourselves from the php.net tarball rather than using Wolfi's packages. When work started (around 16:00 UTC on 2026-10-21), the published Wolfi php-8.5 was 8.5.11 and php-8.4 was 8.4.26, both still unfixed (these are the values observed at that time; they may have been updated since).
Three tools are used. Wolfi is a Linux distribution for containers published by Chainguard; the libraries in the image come from it. melange builds PHP as a Wolfi apk package instead of using a Dockerfile, and apko assembles the image from just those apks.
The flow from build to image is as follows (the diagram follows the job order in .github/workflows/build.yml).
In CI, the Actions published by Chainguard (melange-build, apko-publish and vul-scans) are used as is.
PHP 8.1 and earlier are out of support, so no official fixed tarballs are released. Instead, the PHP security team published minimal backports for 7.4 / 8.0 / 8.1 on php-src branches (php.net announcement).
This repository puts those commits in patches/<branch>/ and applies them to the final tarball when building. The patches are the official fix commits themselves.
Subject: [PATCH] Avoid state corruption via bailout
Fix GHSA-w8hw-w9fx-j79w (CVE-2026-109618)
Fix GHSA-6wmf-4rv5-p3rp (CVE-2026-109619)
scripts/render.sh fetches the tarball's sha256 from php.net, checks whether patches exist, and generates the configuration. Patched builds set melange's epoch to 1. epoch is the package revision number — the r1 in 7.4.33-r1 — which distinguishes it from plain 7.4.33 (-r0).
version=7.4.33
epoch=1
sha256=924846abf93bc613815c55dd3f5809377813ac62a9ec4eb3778675b82a27b927
patches=1
legacy=1
legacy=1 means that because current Wolfi's OpenSSL and libxml2 are too new for 7.4, OpenSSL 1.1.1w and libxml2 2.11.9 are built in the same pipeline and used instead. This is how images for all 7 versions are shipped, as shown below.
8.5 -> PHP 8.5.12 uid=65532
8.4 -> PHP 8.4.27 uid=65532
8.3 -> PHP 8.3.36 uid=65532
8.2 -> PHP 8.2.35 uid=65532
8.1 -> PHP 8.1.34 uid=65532
8.0 -> PHP 8.0.30 uid=65532
7.4 -> PHP 7.4.33 uid=65532
The concern with self-built images is whether you can check "what's inside and who built it". This pipeline builds those checks into each stage.
| Stage | What is checked |
|---|---|
| Source | Verified against the sha256 published by php.net before extraction |
| apk | melange signs them, and apko only installs apks whose signatures it can verify |
| Image | apko puts a per-package SBOM (SPDX format) in /var/lib/db/sbom/ inside the image |
| Distribution | Signed with cosign. No keys are held: "keyless signing" obtains a short-lived certificate using the identity (OIDC token) issued by GitHub Actions |
| Scan | Chainguard's vul-scans Action attaches the results of the vulnerability scanners Trivy and Grype to the image as signed attestations |
The SBOM records not only the PHP source but also where the bundled OpenSSL and libxml2 came from. For the 7.4 image, the sources listed in the php-hotfix SBOM are:
https://www.php.net/distributions/php-7.4.33.tar.xz
https://www.openssl.org/source/openssl-1.1.1w.tar.gz
https://download.gnome.org/sources/libxml2/2.11/libxml2-2.11.9.tar.xz
You can confirm that the signature was made on GitHub Actions by this repository's workflow with the following command.
cosign verify ghcr.io/yowatari/php-oct2026-hotfix:8.5 \
--certificate-identity-regexp '^https://github.com/yowatari/php-oct2026-hotfix/.github/workflows/build.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.comVerification for ghcr.io/yowatari/php-oct2026-hotfix:8.5 --
- The cosign claims were validated
- The code-signing certificate was verified using trusted certificate authority certificates
Signatures are attached to the multi-arch index and to each amd64 and arm64 image beneath it. CI on main also runs this verify on every publish and fails if it doesn't pass.
The image contains only PHP and the libraries it needs to run. There is no shell, and the process runs as an unprivileged user (uid 65532, nonroot).
stat /bin/sh: no such file or directory
The CLI image (:8.5) and the FPM image (:8.5-fpm) are separate, and the CLI image contains neither php-fpm nor headers. Both are multi-arch for amd64 and arm64.
Platform: linux/amd64
Platform: linux/arm64
arm64 is built natively on CI's ubuntu-24.04-arm runners, without emulation.
Because there is no shell, RUN can't be used when this image is the FROM of a Dockerfile. Add your app with COPY.
FROM ghcr.io/yowatari/php-oct2026-hotfix:8.5
COPY app.php /app/app.php
CMD ["/app/app.php"]hello from 8.5.12
To add extensions, change the configure options in melange.tmpl.yaml, or make a separate melange package and add it to apko's packages. This procedure has not been tried yet (unverified).
CI only rebuilds what has changed inputs. scripts/render.sh computes melange_hash from the melange configuration and patches, and image_hash from those plus the apko configuration. CI uses the two as follows.
| Situation | Behavior |
|---|---|
melange_hash unchanged |
Restore apks from cache; don't compile PHP |
The marker tag build-<hash> for image_hash exists on ghcr |
Already published, so nothing is done for that version (main only) |
| PR | Build and test only. Nothing is pushed to ghcr |
| Daily schedule | Run only apko, picking up fixes in Wolfi's base packages |
The patches applied to EOL 7.4 / 8.0 / 8.1 are minimal mitigations addressing only these two issues. Other unfixed vulnerabilities remain, and the bundled OpenSSL 1.1.1w is also out of upstream support. Use them as a stopgap until you move to 8.2 or later.
Whether a vulnerability scanner reports "fixed" depends on the scanner's data. PHP in this image uses its own package name (php-hotfix), so scanner results are not proof of the fix. Confirm the fix through the version, the patches, and the SBOM.
No tests were run that actually exploit the vulnerabilities to confirm they are fixed.
This repository is a demo, and the images are hosted on a personal account's (yowatari) ghcr. If you want to use them as a production FROM, fork the repository into your own organization and publish to your own ghcr. The workflow publishes to ghcr.io/<repository name>, so it works as is.
| Situation | What to do |
|---|---|
| A fixed tarball is out on php.net | Bump DEFAULT_VERSIONS in .github/workflows/build.yml and merge to main. render.sh fetches the sha256 |
| No tarball yet, only a patch | Put patches/<major.minor>/NNNN-*.patch in place and merge to main. The build becomes -r1 |
| Need to ship just one version urgently | Pass JSON such as ["8.5.13"] to versions in workflow_dispatch |
| Branch | Source | Tags (CLI / FPM) |
|---|---|---|
| 8.5 / 8.4 / 8.3 / 8.2 | php.net fixed tarballs (8.5.12 / 8.4.27 / 8.3.36 / 8.2.35) | 8.5.12, 8.5.12-r0, 8.5 / 8.5.12-fpm, 8.5-fpm, etc. |
| 8.1 / 8.0 / 7.4 (EOL) | Final tarballs (8.1.34 / 8.0.30 / 7.4.33) + official backports | 7.4.33-r1, 7.4 / 7.4.33-r1-fpm, 7.4-fpm, etc. |
Extensions are a minimal set (mbstring, openssl, curl, zlib, mysqli / pdo_mysql, pdo_sqlite, bcmath, sockets, pcntl, etc. 8.5 has OPcache built in; 8.4 and earlier build opcache.so but leave it disabled by default). Edit the configure options in melange.tmpl.yaml if you need more.
| Path | Contents |
|---|---|
melange.tmpl.yaml |
PHP build definition. php-hotfix / -fpm / -dev subpackages and tests |
legacy-deps.inc.yaml |
OpenSSL 1.1.1w and libxml2 2.11.9 for 7.4 / 8.0 / 8.1 |
apko.tmpl.yaml |
Image definitions (CLI and FPM) |
patches/<branch>/ |
Official backports for EOL branches |
scripts/render.sh |
Generates melange and apko configurations and hashes from a version |
scripts/smoke.sh |
Image smoke tests |
examples/copy-app/ |
Example of adding an app to this image with COPY |
.github/workflows/build.yml |
Build, publish, signing, scanning |
checks.json, figures/ |
Files used to verify the outputs and diagrams in this README |
scripts/render.sh 8.5.12
docker run --rm -v "$PWD":/work -w /work cgr.dev/chainguard/melange keygen melange.rsa
docker run --privileged --rm -v "$PWD":/work -w /work cgr.dev/chainguard/melange \
build build/8.5.12/melange.yaml --arch x86_64 --signing-key melange.rsa \
--source-dir build/8.5.12/src --out-dir build/8.5.12/packages
docker run --rm -v "$PWD":/work -w /work cgr.dev/chainguard/apko \
build build/8.5.12/apko.yaml php-hotfix:8.5.12 build/8.5.12/image.tar \
-r /work/build/8.5.12/packages -k /work/melange.rsa.pub # for FPM, use apko-fpm.yaml
docker load < build/8.5.12/image.tar