2026-10-21 已釋出 PHP 的修正版 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12。這些版本修正了 2 個 critical 等級的漏洞(CVE-2026-109618 與 CVE-2026-109619)。不過,在我撰寫本文時,Amazon Linux 2023(以下簡稱 AL2023)的官方套件庫中,尚未提供修正版套件。
本文介紹的是,直接沿用 AL2023 發行的 SRPM,只把裡面的 PHP 版本升級到上游修正版後重新建置的作法。這個方法有以下優點:
dnf upgrade 自動換回官方版本適用對象是,在 EC2 上使用 AL2023 官方套件庫的 php8.2 / php8.3 / php8.5 套件的使用者。本文的步驟已在 8.2 / 8.3 / 8.5 上確認過。8.4 尚未驗證。
免責聲明
php-src 的 GitHub Security Advisory 於 2026-10-21 13:34 UTC 公開了以下 2 項。詳細內容請務必閱讀連結中的原文。
ID名稱嚴重性概要GHSA-w8hw-w9fx-j79w / CVE-2026-109618RCE via OOM corruptioncriticalPHP 會用 longjmp() 處理記憶體不足(OOM)錯誤。此時,請求用的記憶體可能會以不正確的狀態殘留,並在請求結束處理時被讀取。透過精心構造 multipart 解析,可以在各種限制範圍內觸發 OOM。GHSA-6wmf-4rv5-p3rp / CVE-2026-109619RCE via stale global statecritical工作程序保留的全域狀態會一直指向已釋放的請求記憶體。當同一個工作程序處理後續請求時,就會發生 use-after-free。
根據公告,CVE-2026-109618 具有以下特性:
受影響版本與修正版如下所示(PHP 8 ChangeLog)。
分支受影響版本修正版8.28.2.35 以下8.2.358.38.3.36 以下8.3.368.48.4.27 以下8.4.278.58.5.12 以下8.5.12
各分支的修正都是 1 個 commit,標題為 "Avoid state corruption via bailout"。
分支commit8.531c0ea7d7ff18.33c832b523ee18.201a40e89fbfc
變更規模很大,達到 +11,556 / −1,017 行,並修改了 Zend/zend_alloc.c 等檔案。
在撰稿當下,AL2023 最新套件庫中的版本如下,全部都屬於受影響版本。
套件版本php8.28.2.34-1.amzn2023.0.1php8.38.3.35-1.amzn2023.0.1php8.58.5.11-1.amzn2023.0.1
你在閱讀本文時的狀況,可以用下列指令確認。如果修正版已經釋出,本文的步驟就不需要了。
dnf repoquery -q --releasever=latest --latest-limit=1 \
--qf '%{name}-%{version}-%{release}' php8.2-cli php8.3-cli php8.5-cli
一開始,我嘗試在 AL2023 的 SRPM 上直接加入修正 commit 當作 patch。這樣差異最小,但最後沒有成功。
--fuzz=0 時,會有 patch 套用失敗的 hunk。8.2 / 8.3:patch 雖然能套上去,但連結時失敗。
undefined reference to `php_var_unserialize_set_incomplete_classes_only'
undefined reference to `php_url_scanner_early_rinit'
修正 commit 變更了 re2c 的原始碼(.re)。但發行 tarball 中包含的是已生成的 .c,建置時不會重新生成。因此,變更不會反映到已生成的程式碼中。
因此,我改成 把 SRPM 參照的 tarball 換成 php.net 的修正版(php-8.x.y.tar.xz)。修正版 tarball 內包含正確生成的原始碼。AL2023 特有的 patch 群組也可以順利套用到修正版上(後面 %prep 會確認)。
升級版本時,除了漏洞修正之外,也會帶入其他變更。
版號提升漏洞修正以外的變更8.2.34 → 8.2.35、8.3.35 → 8.3.36僅有 CI 與 Windows 相關變更,以及內建 PCRE2 的修正。AL2023 會使用系統的 pcre2 進行建置,因此內建 PCRE2 的修正與你無關8.5.11 → 8.5.12也包含其他上游錯誤修正。請確認 ChangeLog,並在你的應用程式上做實際測試。
建置是在 public.ecr.aws/amazonlinux/amazonlinux:2023 容器內進行。容器架構必須與你要安裝套件的目標主機相同。
目標建置環境(作者使用的環境)Graviton(aarch64)在 Graviton EC2(c7g.4xlarge)上的 Dockerx86_64x86 主機上的 Docker(--platform linux/amd64)也可以在 x86 主機上用 QEMU 模擬建置 arm64,但速度非常慢,不建議。
編譯選項會從 SRPM 與 redhat-rpm-config 繼承,不需要自行指定,與官方建置使用相同選項。
架構主要選項x86_64-march=x86-64-v2 -mtune=genericaarch64-march=armv8.2-a+crypto -mtune=neoverse-n1
包含 %check(測試)的建置時間大約如下:
版本環境時間8.5c7g.4xlarge約 12 分鐘8.3c7g.4xlarge約 9~11 分鐘8.26 核心 Intel 桌機約 14~15 分鐘
以下以 php8.5 為例。若是 8.3 / 8.2,請自行替換版本號。
目標SRPM修正版php8.5php8.5-8.5.11-1.amzn2023.0.18.5.12php8.3php8.3-8.3.35-1.amzn2023.0.18.3.36php8.2php8.2-8.2.34-1.amzn2023.0.18.2.35### 1. 啟動建置用容器
mkdir -p out
docker run -d --name php-rebuild -v "$PWD/out:/out" \
public.ecr.aws/amazonlinux/amazonlinux:2023 sleep infinity
docker exec -it php-rebuild bash
如果是在 x86 主機上為 x86_64 建置,docker run 請加上 --platform linux/amd64。
後續指令都在容器內執行。
dnf install -y dnf-plugins-core rpm-build rpmdevtools
# %prep 會用 gpgv2 驗證 tarball 簽章;gnupg2-minimal 不夠
dnf -y swap gnupg2-minimal gnupg2
useradd -m builder
rpmbuild 不要以 root 執行,請改用一般使用者 builder。
mkdir -p /src && cd /src
dnf download --source php8.5-8.5.11-1.amzn2023.0.1
rpm -K *.src.rpm # 確認輸出 "digests signatures OK"
dnf builddep -y /src/*.src.rpm
cp /src/*.src.rpm /home/builder/ && chown builder /home/builder/*.src.rpm
su - builder -c 'rpm -ivh --define "_topdir /home/builder/rb" /home/builder/php8.5-*.src.rpm'
%check 測試中有些項目會依環境而失敗(後文會說明)。為了判斷版本升級後是否新出現失敗測試,你需要一個比較基準。因此,建議在修改 spec 之前先建置一次並保留日誌。
T=/home/builder/rb; S=$(ls $T/SPECS/*.spec)
su - builder -c "rpmbuild --define '_topdir $T' -ba $S" 2>&1 | tee /src/build-baseline.log
grep -A30 'FAILED TEST SUMMARY' /src/build-baseline.log
# 為避免混入發行物,刪除基準建置產生的 RPM
rm -rf $T/RPMS/* $T/SRPMS/*
基準建置產生的 RPM 會輸出到與修正版 RPM 相同的 RPMS/ 目錄。如果不刪除,會讓未修正的 RPM 混進發行物中。
T=/home/builder/rb; S=$(ls $T/SPECS/*.spec)
sed -i 's/^%global upver .*/%global upver 8.5.12/' $S
sed -i 's/^Release: .*/Release: 0.1.local%{?dist}/' $S
# 不刪掉的話,Release 末尾會多出 .0.1
sed -i '/^%define _trivial/d; /^%define _buildid/d' $S
0.1.local 這段可以改成你喜歡的字串,但務必要比官方的 1.amzn2023.0.1 小。原因會在「版本命名與切回官方版」說明。
AL2023 的 spec 中,tarball 是 Source0,簽章(.asc)是 Source21。由於 spectool 同時指定多個 -s 時只會使用最後一個,所以要分開執行。
su - builder -c "cd $T/SOURCES && spectool -g -s 0 $S && spectool -g -s 21 $S"
Source 的編號可能會隨 spec 版本改變。不放心的話,先用 spectool -l $S 確認清單再執行。
接著與 php.net 公布的 sha256 做比對。
echo "08bc2eba2a588654ade1357174aa646b761556c862dda0678345ee29ed481209 $T/SOURCES/php-8.5.12.tar.xz" | sha256sum -c
撰稿當下,php.net 上列出的 tar.xz sha256 如下。
檔案sha256php-8.5.12.tar.xz``08bc2eba2a588654ade1357174aa646b761556c862dda0678345ee29ed481209``php-8.3.36.tar.xz``dd54693aa334c5c4d72e9aee96b082c2473750a18a6c1d105789c93a3b1692fd``php-8.2.35.tar.xz``5737b98601fe5ab1b8c5db0c82ed3158acb9283ed3081ffe2faffb9ac81455e6
不要直接照抄這個表,請以你從 php.net 直接取得的值為準。
curl -s 'https://www.php.net/releases/index.php?json&version=8.5'
輸出中,請使用對應 php-8.5.12.tar.xz 的 sha256 值。
%prepsu - builder -c "rpmbuild --define '_topdir $T' -bp $S"
在 %prep 中會做兩件事:
只要這裡沒有錯誤,就表示簽章驗證與 patch 套用都成功了。
su - builder -c "rpmbuild --define '_topdir $T' -ba $S" 2>&1 | tee /src/build.log
grep -A30 'FAILED TEST SUMMARY' /src/build.log
%check 結果與基準建置比對這個 spec 會忽略 %check 的結束碼。因此,即使測試失敗,建置本身也可能成功。 請務必檢查 FAILED TEST SUMMARY。
作者在自己的環境中失敗的測試如下,且在版本升級前後結果相同。
版本失敗數失敗的測試8.54個pcre gh11956、openssl ecc_custom_params、opcache preload_006、preload_0118.32個gh11956、ecc_custom_params8.21個gh11956
失敗測試可能會因環境不同而改變,因此請拿步驟 4 取得的基準建置日誌來比對。只要版本升級後沒有新增失敗測試即可。
cd /out
find $T/RPMS -name '*local*.rpm' ! -name '*debuginfo*' ! -name '*debugsource*' -exec cp {} /out/ \;
sha256sum *.rpm > SHA256SUMS
ls -1
RPM 與 SHA256SUMS 會輸出到主機端的 ./out。
用 Release: 0.1.local%{?dist} 建置後,版本會變成 8.5.12-0.1.local.amzn2023。用 rpmdev-vercmp 比較時,順序如下:
8.5.11-1.amzn2023.0.1 (目前的官方版本)
< 8.5.12-0.1.local.amzn2023 (自行建置)
< 8.5.12-1.amzn2023.0.1 (未來預期的官方版本)
rpmdev-vercmp 8.5.12-0.1.local.amzn2023 8.5.11-1.amzn2023.0.1
rpmdev-vercmp 8.5.12-0.1.local.amzn2023 8.5.12-1.amzn2023.0.1
從這個順序可以得到以下結論:
dnf upgrade 會安裝自行建置版本。8.5.12-1.amzn2023.0.1 舊,因此官方版本釋出後,dnf upgrade 會自動替換回去。如果 AWS 不升版、而是直接回補修正(例如 8.5.11-1.amzn2023.0.2),那麼自行建置版本會被判定為「較新」。此時 dnf upgrade 不會自動切回官方版。請改用 dnf distro-sync,或重新建立 AMI(參見「切回」)。
不要直接上正式環境,請先在模擬正式主機的容器中驗證。
在正式主機上先確認以下項目:
rpm -qa 'php*' | sort # 已安裝的 PHP 套件
rpm -q --qf '%{version}\n' system-release # AL2023 的發行版本(格式為 2023.x.YYYYMMDD)
php -m > /tmp/php-m.before # 擴充模組清單(比對用)
基底映像檔請使用與正式主機發行版本相符的標籤。 如果在最新版映像檔中安裝舊 releasever 的 PHP,會發生 openssl 3.5 與 3.0 的衝突而失敗。
<release> 請替換成步驟 1 中確認到的正式主機 system-release 版本。
docker run -it --name prodlike \
public.ecr.aws/amazonlinux/amazonlinux:<release>.0 bash
# --- 容器內 ---
# 將 releasever 固定成正式環境一致(不要加尾端的 .0)
echo <release> > /etc/dnf/vars/releasever
dnf install -y php8.5-cli php8.5-fpm ... # 安裝與正式環境 rpm -qa 'php*' 相同的套件
exit
docker commit prodlike prodlike:snapshot
後續驗證都在由快照啟動的一次性容器中進行,失敗了重建即可。
docker run --rm -it -v "$PWD/out:/rpms" prodlike:snapshot bash
cd /rpms
sha256sum -c SHA256SUMS
REL=8.5.12-0.1.local.amzn2023
ARCH=$(uname -m) # aarch64 或 x86_64
# 只選擇與已安裝的 php8.5* 同名的自行建置版本(PECL 擴充套件未重新建置,因此排除)
pkgs=""
for n in $(rpm -qa --qf '%{name}\n' "php8.5*" | grep -v pecl); do
pkgs="$pkgs ./$n-$REL.$ARCH.rpm"
done
dnf upgrade -y --best --disablerepo='*' $pkgs
dnf upgrade 的每個選項都有其理由:
--disablerepo='*':只使用本機 RPM,避免第三方套件庫干擾。--best:必要。 如果不加,dnf 會默默略過無法解決相依性的套件,但仍顯示 Complete!,並以 0 結束碼結束(詳見「踩雷點」)。如果正式環境中安裝了對應檔案不在 out/ 裡的套件,dnf 會因找不到檔案而停止。這時請比對已建置套件清單與正式環境中的套件。
# 如果沒有任何輸出,表示全部都已替換成自行建置版本
rpm -qa --qf '%{name}-%{release}\n' 'php8.5*' | grep -v pecl | grep -v local
作者在更新後檢查了以下項目:
php -v # 應顯示 8.5.12
php -m | diff /tmp/php-m.before - # 擴充模組清單應與更新前相同
php-fpm -t # 確認 FPM 設定有效
rpm -Va 'php*' # 除設定檔外不應有差異
find /etc -name '*.rpmnew' -o -name '*.rpmsave' # 不應有任何輸出
種類結果AL2023 以套件形式提供的 PECL 擴充套件(php8.5-pecl-igbinary / msgpack / redis6)不需要重新建置。因為在 8.5.x 之間 Zend API 相同,更新後仍可直接使用pecl install 手動安裝的擴充套件作者在 8.2.x 上測試過的擴充套件,更新後也仍可載入其他擴充模組尚未驗證,請用 php -m 的比對結果確認。
spec 裡的 %transfiletriggerin fpm 會在交易結束時執行 systemctl try-restart php-fpm。因此,執行中的 php-fpm 會自動重新啟動。重新啟動的那幾秒內,正在處理的請求可能會中斷。
請確認重新啟動後的 master process 是使用新的 binary。
# 如果輸出中不含 "(deleted)",就表示正在使用新的 binary
readlink /proc/$(systemctl show -p MainPID --value php-fpm)/exe
由 cron 啟動的長時間工作、queue worker 等常駐 PHP CLI 程序,會一直使用舊 binary,直到你重新啟動它們為止,請自行重啟。
--best 不加時,相依性有問題的套件會被默默略過如果執行 dnf upgrade 時不加 --best,它會顯示以下訊息並略過套件:
Skipping packages with broken dependencies
但最後仍會顯示 Complete!,結束碼也是 0。也就是說,從腳本執行時看起來像是成功了。請務必加上 --best,並在套用後確認 grep -v local 的結果。
在安裝了 libsodium 1.0.18 的舊版 AL2023 發行版上,如果主機使用 php8.2,就會發生相依性錯誤。
libsodium自建版 php8.2-sodium 所需libsodium.so.26舊主機上安裝的libsodium 1.0.18(libsodium.so.23)2023.11 之後的發行版已經內含 1.0.19。
解法是從最新版套件庫取得 libsodium 1.0.19,並在同一個交易中安裝。先確認是否還有其他套件依賴舊版 libsodium。若除了 php8.2-sodium 之外還有其他套件出現,也要確認那些套件是否能在 libsodium 1.0.19 上正常運作。
rpm -q --whatrequires 'libsodium.so.23()(64bit)'
在建置環境(最新版 AL2023 容器)中取得它。
dnf download --arch x86_64 libsodium-1.0.19-5.amzn2023
在目標主機上,與 PHP 套件一起傳入。
dnf upgrade -y --best --disablerepo='*' $pkgs ./libsodium-1.0.19-5.amzn2023.x86_64.rpm
如同「在類正式環境中的驗證」所說,如果在最新版映像檔中安裝舊 releasever 的 PHP,會發生 openssl 3.5 與 3.0 衝突。基底映像檔的標籤必須與正式環境的發行版一致。寫入 /etc/dnf/vars/releasever 的值,請去掉標籤尾端的 .0。
gnupg2-minimal 會讓 %prep 失敗在 %prep 中,tarball 簽章是用 gpgv2 驗證的。容器內預設安裝的 gnupg2-minimal 沒有 gpgv2,因此會失敗。請用 dnf -y swap gnupg2-minimal gnupg2 替換。
spectool -s 指定多個時,只會使用最後一個像 spectool -g -s 0 -s 21 這樣寫時,最後只會抓到最後一個來源。請逐一執行。
_trivial / _buildid 會讓 Release 尾端多出字串AL2023 的 spec 中有 %define _trivial 與 %define _buildid。如果保留它們,Release 尾端會多出 .0.1。想要得到預期的版本,請刪除這兩行。
基準建置的 RPM 會輸出到與修正版相同的 RPMS/ 目錄。請在建置修正版前刪除它們。
%check 失敗時,建置仍可能成功因為 spec 忽略 %check 的結束碼,所以即使測試失敗,建置也可能成功。請檢查 FAILED TEST SUMMARY,並與基準建置比對。
dnf distro-sync -y 'php8.5*'
這個步驟已在類正式容器中確認過。
如果舊版 php8.2 主機也一併更換了 libsodium,那就要一起切回。
dnf distro-sync -y 'php8.2*' libsodium
切回後,會回到有漏洞的版本。
以下是一般性的部署範例。
S3Download 取得 RPM,再以 dnf upgrade --best --disablerepo='*' 進行替換,並檢查 Release 與 PHP_VERSION 的流程,在 8.5.12 與 8.3.36 都有通過。將 RPM 與 SHA256SUMS 放到 S3。
aws s3 cp out/ s3://<your-bucket>/php-rebuild/8.5.12-0.1.local/ --recursive
讓執行個體取得檔案的方法有兩種:
s3:GetObject 權限。若加密方式是 SSE-S3,就不需要 KMS 權限。從 staging 環境開始,一台一台套用。加上 --max-concurrency 1 --max-errors 0 後,只要有一台失敗就會停止。
aws ssm send-command \
--document-name AWS-RunShellScript \
--targets 'Key=tag:<your-tag-key>,Values=<your-tag-value>' \
--max-concurrency 1 --max-errors 0 \
--parameters 'commands=["bash /path/to/apply-php-rebuild.sh"]'
apply-php-rebuild.sh 的內容,就是把「套用」步驟寫成腳本:從 S3 下載、sha256sum -c、dnf upgrade --best --disablerepo='*'、檢查是否漏裝、確認 readlink 等。
只更新運行中的執行個體還不夠。Auto Scaling 新起的執行個體仍會從有漏洞的 AMI 啟動。請把 RPM 也燒錄進 AMI,並更新 Launch Template。
如果你在 CloudFormation 中,將 Launch Template 的 ImageId 設成 SSM 參數型別(AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>),那麼即使只把 SSM 的值改成新的 AMI ID,Launch Template 仍可能指向舊 AMI。因為 SSM 值只會在建立或更新 stack 時被解析,所以在 AMI 燒錄完成後,請再更新一次 stack,讓 Launch Template 套用變更。如果 ASG 有 UpdatePolicy(例如 AutoScalingRollingUpdate),也要注意這次 stack 更新會觸發執行個體替換。
在 EC2 Image Builder 中,可以用以下類似的 component 來實作:
S3Download 步驟取得 RPMExecuteBash 步驟執行相同的 dnf upgrade --best --disablerepo='*'PHP_VERSION 是修正版name: php-rebuild-upgrade
schemaVersion: 1.0
phases:
- name: build
steps:
- name: Download
action: S3Download
inputs:
- source: s3://<your-bucket>/php-rebuild/8.5.12-0.1.local/*
destination: /tmp/php-rebuild/
- name: Upgrade
action: ExecuteBash
inputs:
commands:
- |
set -euo pipefail
cd /tmp/php-rebuild
sha256sum -c SHA256SUMS
REL=8.5.12-0.1.local.amzn2023
ARCH=$(uname -m)
pkgs=""
for n in $(rpm -qa --qf '%{name}\n' "php8.5*" | grep -v pecl); do
pkgs="$pkgs ./$n-$REL.$ARCH.rpm"
done
dnf upgrade -y --best --disablerepo='*' $pkgs
# 若仍有未替換成自行建置版本的套件,就讓流程失敗
if rpm -qa --qf '%{name}-%{release}\n' 'php8.5*' | grep -v pecl | grep -v local; then
echo "some php packages were not upgraded" >&2
exit 1
fi
if [ "$(php -r 'echo PHP_VERSION;')" != "8.5.12" ]; then
echo "unexpected PHP_VERSION" >&2
exit 1
fi
在 bash 中,對命令使用 ! 否定時,即使該命令失敗,set -e 也不會觸發。舉例來說,! rpm -qa ... | grep -v local 這種寫法,在檢查失敗時不一定會讓腳本停止。請把檢查寫成 if ...; then exit 1; fi 的形式。
在 set -o pipefail 下,如果寫成 php -m | grep -qi 'Zend OPcache',即使擴充模組已載入,也可能判定失敗。因為 grep -q 一旦找到相符內容就會停止讀取,這會讓仍在輸出的 php 因 SIGPIPE 異常結束,而 pipefail 會把這視為整個管線失敗。
作者的環境中就發生過這種情況:前一個 php -v 明明顯示 with Zend OPcache,後續檢查卻誤判成「OPcache 沒有載入」。輸出越大越容易發生,有時在本機測試又不容易重現。請先把輸出存入變數,再進行檢查。
mods=$(php -m)
if ! grep -qi 'Zend OPcache' <<<"$mods"; then
echo "Zend OPcache is not loaded" >&2
exit 1
fi
像 grep -v 這種會把輸入讀到底的命令,就不會有這個問題。head -1 也會因為和 grep -q 類似的原因而避免使用。
當 ALAS 公開修正版套件後,就切換回官方版本。
dnf upgrade 就會自動替換。local 的 Release。dnf upgrade -y 'php8.5*'
# 如果沒有任何輸出,就表示已切回官方版本
rpm -qa --qf '%{name}-%{version}-%{release}\n' 'php*' | grep local
如果官方修正不是升版,而是以 backport 方式釋出,那麼 dnf upgrade 不會自動替換。請參考「版本命名與切回官方版」中的說明,改用 dnf distro-sync。
原文出處:https://qiita.com/amu-kuroneko/items/46f00fbe8b5bac009c1c