适用场景:从二进制层关门,把官方补丁编进 PHP,而不是靠可被覆盖的运行期开关。
核心事实(已对 php-src 核实):
- 完整根治补丁 = commit
3584a98bda0「Replace bailout on OOM with UnwindExit; properly reset various globals」,已发版8.2.35 / 8.3.36 / 8.4.27 / 8.5.12。- 7.x 与 ≤8.1 没有官方发版 tag,官方只给了一个 EOL 源码分支
PHP-8.0-CVE-2026-109618-109619(commit82f8f31b16a,最小化缓解版,非完整根治)。
1. 根因与两种修法的区别
致命错误(OOM)时 PHP 调 zend_bailout() 做 longjmp,native 清理被打断,残留的半初始化/已释放状态被后续请求或复用 worker 读到。
官方给了两档修法,这决定了不同版本能做到哪一步:
| 档位 | commit | 做了什么 | 能否移植到老引擎 |
|---|---|---|---|
| 完整根治 | 3584a98bda0 | 用 UnwindExit 替换 OOM 时的 bailout,让异常路径走正常栈回退;并在 startup 前正确复位各全局(EG/PG/SAPI)。附带大量 oom_*.phpt 回归测试 | 仅 Zend Engine 4(PHP 8)原生;引擎差异大,不能机械 cherry-pick 到 7.x/5.x |
| 最小缓解(EOL) | 82f8f31b16a | ① ext/session/session.c:if (!PS(rfc1867_enabled) || true) 永久关 upload_progress 回调;② main/SAPI.c:efree 后把 auth_*/content_type_dup/current_user 置 NULL;③ main/main.c、Zend/zend_execute_API.c:复位 http_globals / EG 若干字段 | ①② 思路可移植;③ 需按各引擎改写 |
要点:完整根治改的是 Zend 引擎的 bailout 机制,这套只对 PHP 8 的引擎成立。越老的分支,越只能退回"最小缓解"这一档。
2. PHP 8.x(8.2 / 8.3 / 8.4 / 8.5)——直接编官方修复版
最省事,也最彻底:拉已含 3584a98bda0 的发版 tag,原样编译。
# 以 8.3.36 为例;其余换成 8.2.35 / 8.4.27 / 8.5.12
curl -fsSLO https://www.php.net/distributions/php-8.3.36.tar.gz
tar xf php-8.3.36.tar.gz && cd php-8.3.36
# 关键:沿用你现网二进制的 configure 参数,避免编出来缺扩展
# 现网机器上执行 `php -i | grep 'Configure Command'` 拿到原始参数,原样贴到下面
./configure $(php -i | sed -n 's/^Configure Command => //p' | tr -d "'")
make -j"$(nproc)"
make test # 应能看到 oom_*.phpt 这批回归用例通过 = 根治补丁在起作用
sudo make install
8.0 / 8.1(无发版 tag):把 EOL 分支 PHP-8.0-CVE-2026-109618-109619 的差异 git format-patch 出来,应用到对应 8.0/8.1 源码再编(它们引擎同代,最小缓解可用,但拿不到 UnwindExit 的完整根治)。
3. PHP 7.x(7.0–7.4)——移植最小缓解后自编译
无官方任何发版。7.x 是 Zend Engine 3,比 5.x 接近 8.0,EG/PG 复位那部分比 5.x 好移植,但 UnwindExit 那套仍不适用。可落地的是最小缓解三件套:
/* ext/session/session.c —— php_session_rfc1867_callback 内,核心一行 */
- if (!PS(rfc1867_enabled)) {
+ if (!PS(rfc1867_enabled) || 1) { /* CVE-2026-109618: 永久关 upload_progress 回调 */
return retval;
}
/* main/SAPI.c —— sapi_deactivate 内,每个 efree 后补一行置 NULL,防 bailout 后二次释放 */
if (SG(request_info).auth_user) { efree(SG(request_info).auth_user); SG(request_info).auth_user = NULL; }
if (SG(request_info).auth_password) { efree(SG(request_info).auth_password); SG(request_info).auth_password = NULL; }
if (SG(request_info).content_type_dup){ efree(SG(request_info).content_type_dup);SG(request_info).content_type_dup = NULL; }
/* auth_digest / current_user 同理 */
第三件(main/main.c 请求关闭时复位 http_globals、Zend/zend_execute_API.c 复位 EG 字段)——7.x 要按本分支的 zval/EG 定义改写(7.x 用 ZVAL_UNDEF 的方式与 8.0 接近,可对照 82f8f31b16a 逐行翻译)。
编译流程同第 2 节(换 7.x 源码 + 现网 configure 参数)。叠加运行期 pm.max_requests=1 兜住 109619 的 worker 复用面。
4. PHP 5.x(5.5 / 5.6)——本项目线上,只能做到最小缓解
诚实结论:5.x 是 Zend Engine 2.6,zval 结构、分配器、VM 与 PHP 8 完全不同,UnwindExit 的完整根治无法移植,也没有任何上游参照。能干净落地的只有:
ext/session/session.c的|| 1(同第 3 节那行)——这正是官方给 EOL 用的同一招,直接堵死 109618 的主泄露路径,且因为编进了二进制,比 ini 的enabled=0更硬(改不掉、不能被逐请求覆盖)。main/SAPI.c的 efree 后置 NULL——5.x 的SAPI.c结构一致,可照搬,防 SAPI 请求信息指针的 UAF/二次释放。- 运行期
pm.max_requests=1打断 worker 复用,压 109619。
残余风险:以上组合关死了 109618 的已知利用面,并大幅收窄 109619,但不等于像 8.x 那样的引擎级根治。5.x 的最终出路仍是升级框架 + PHP,这篇只是把"不能升级的这段时间"的风险降到工程上可接受。
本项目的落地路径(remi SCL,不是官方 tarball)
线上用的是 /opt/remi/php56、/opt/remi/php55,不能直接 make install 覆盖,要走 SRPM 重打包,否则会和 yum 管理冲突:
# 在一次性构建容器里做,绝不在生产机编译(红线:实验不碰真实资产)
# 1. 装对应 SRPM(版本要和现网完全一致:php 5.6.37 / 5.5.30)
rpmbuild --rebuild php56-php-*.src.rpm # 先解包拿到 SPEC + 源码 tarball
# 2. 把上面 ①② 的改动做成 patch,塞进 SPEC 的 PatchNNN + %patchNNN
# 3. 重新 rpmbuild -ba SPECS/php56.spec,产出带补丁的 php56-php-* 子包
# 4. 灰度:先在一台(如当前没对外的机器)yum localinstall,验证后再滚动
替换 RPM = 停服级变更。按红线:先在隔离容器编译验证,线上替换和重启须逐台确认。
5. 编译后怎么验证"补丁真的生效"(良性,不碰攻击)
用 upload_progress 这个文档化功能本身反证回调已被关闭,不触发 OOM、不做内存回显、不打 shell:
; probe.ini
session.upload_progress.enabled=1
session.upload_progress.cleanup=0
session.save_path=/tmp
// target.php:上传完成后读 session,看回调有没有写入
<?php session_start();
$hit = array_filter(array_keys($_SESSION), function($k){ return strpos($k,'upload_progress_')===0; });
echo 'upload_progress_keys=' . ($hit ? implode(',',$hit) : 'NONE') . "\n";
对同一个带 PHP_SESSION_UPLOAD_PROGRESS 字段的 multipart 上传:
- 未打补丁:输出
upload_progress_...(回调是活的) - 打补丁后:输出
NONE(回调已被编译期关闭)→ 证明 109618 路径已关。
参考
- 完整根治 commit
3584a98bda0;EOL 最小缓解分支PHP-8.0-CVE-2026-109618-109619/ commit82f8f31b16a - 已发版:
php-8.2.35 / 8.3.36 / 8.4.27 / 8.5.12(7.x、8.0、8.1 无发版 tag) - GHSA-w8hw-w9fx-j79w(CVE-2026-109618)/ GHSA-6wmf-4rv5-p3rp(CVE-2026-109619)
本文针对PHP中由OOM触发的严重漏洞CVE-2026-109618与CVE-2026-109619,提出基于源码编译的二进制层修复方案。核心在于移植官方UnwindExit机制(8.x完整根治)或最小缓解补丁(7.x/5.x),覆盖PHP 5.5–8.5全版本。方案规避运行期开关风险,强调SRPM重打包、worker复用拦截、zval/EG状态复位及efree后置NULL等关键技术点,并提供非攻击式验证方法。

150

被折叠的 条评论
为什么被折叠?



