PHP 长驻 worker 的 OOM 排障:8.6 换掉 longjmp,多了 memory_hard_limit_percent
先升级:两个 9.2 的 RCE 已带修
PHP 自身存在两个严重漏洞,评分都是 9.2,攻击者不用登录就能在服务器上跑代码。修复版本为 PHP 8.5.12、8.4.27、8.3.36、8.2.35;8.6.0 RC4(2026-10-21)同样包含。CVE 编号:CVE-2026-109618、CVE-2026-109619。
这不是理论风险。发现者 Calif 称,2026 年 8 月已向 PHP 团队提交可用的 exploit,团队确认可运行。第一个漏洞的 GitHub advisory 写明,完整远程代码执行“已在默认 PHP 输入与内存限制下,通过 HTTP 对 mod_php 验证”——默认限制,无需登录。
8.6.0 后续节点:RC5 计划 2026-11-05,GA 2026-11-19。
成因:longjmp 停在还没接完的数据上
两个 bug 都处在内存管理与错误处理的交界。
PHP 触发内存上限时,历来用 longjmp 直接跳回安全点,把正在执行的每个函数就地丢弃。问题在于触发时机:有几处 PHP 已建好部分请求数据、尚未完成关联;此时内存耗尽,longjmp 一走了之,留下未完成的数据。随后清理阶段会信任这块数据——“may then treat the invalid data as valid”。
第二个 bug 在 worker 层。一个 worker 连续处理请求,某些内部状态本应在请求之间清掉。恰好在错误时机发生 fatal error 会跳过清理,留下仍指向刚结束请求的指针。该请求内存已被释放,下一个请求落到同一 worker,在重置前读到悬垂指针。8.5 上最清楚的两个例子:fatal error 保存的 backtrace(默认开启),以及 session upload progress(也默认开启)。团队说前者就是已确认的 mod_php 攻击的基础。
修复:置标记,到安全点再报错
单次提交,推到所有分支,标题 "Avoid state corruption via bailout"。作者 Arnaud Le Blanc,协作 Ilija Tovilo、Jakub Zelenka、Nora Dossche、Nicolas Grekas。涉及 389 个文件、约 249 个新测试。
核心是 OOM 不再跳出:越过正常内存上限时,不在操作中途 bail,而是置标记,完成手上安全的部分,在下一个安全点报告错误,清理阶段不会再看到未完成的数据。
再高一层有硬上限兜底:如果还是冲破,PHP 当场退出进程,不冒险带着坏内存继续跑。这是新增 ini 设置 memory_hard_limit_percent。
针对悬垂状态,补丁把 worker 全局变量提前重置,赶在下一个请求能触达之前;同时让 session upload progress 干活时不唤醒应用对象。
OOM 能被优雅处理而非跳出后,一次内存分配就被允许直接失败、返回空——引擎里每个会分配内存的角落都要学会检查返回值并干净回退。窄 bug,全代码库级修复。
这一改动随补丁进入所有受支持分支,因此 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 上也会看到新行为,不只是 8.6。
对 FPM / FrankenPHP 长驻 worker 的影响
所有处理 Web 请求的 PHP 安装都受影响:PHP-FPM、Apache mod_php、php-cgi、FrankenPHP、LiteSpeed。mod_php 风险最高,已有演示中未认证请求能在应用代码执行前触发。
对 FPM 和 FrankenPHP 这类长驻 worker,第二类 bug 尤其贴近:worker 复用、请求间状态没清干净,下一个请求就踩上去。FrankenPHP worker 模式本就常驻脚本循环,状态清理责任更重。
时间线:内存问题最早在 2024 年一次外部审计中浮现,2026 年 8 月证明可利用。CVE 编号提前泄露,附带错误评分和发布时间;发布因此提前一天;Chainguard 提前六小时推了修补镜像。发布时 PHP 团队与 CISA 均未确认野外已有可用 exploit。
memory_limit 与 memory_hard_limit_percent 怎么配
memory_limit 仍是第一道线,是“正常报错”阈值,决定何时收到 Allowed memory size exhausted 的 fatal error。
memory_hard_limit_percent 是第二道线,相对 memory_limit 的百分比。设成 100 意味着两道线重合,硬上限失去兜底意义;留出余量,让第一道线报错、清理、worker 继续服务,只有异常情况(比如错误处理路径自己还在吃内存)才落到硬上限。比例按单请求峰值的波动幅度定,没有通用值。
硬上限语义是“直接退出进程”。FPM 下由 master 拉起新 worker;FrankenPHP worker 模式下,进程退出会打断常驻循环。监控上要区分:普通 OOM fatal error 是请求级故障,硬上限退出是进程级故障。
验证:复现 OOM、看重启、对日志
- 复现 OOM:把
memory_limit调小,跑持续分配内存的脚本,确认得到正常 fatal error,请求结束而 worker 存活,不是进程消失、连接被掐断。 - 触发硬上限:把
memory_hard_limit_percent设到略高于 100,再用更激进的分配顶穿,观察进程是否按设计退出。该路径会让 worker 死掉,最好在预发环境做。 - 看 worker 是否重启:FPM 看进程号是否变化、进程池 worker 数量是否补齐;容器看 restart count 与 PID;FrankenPHP 看 worker 是否被重新拉起。把 OOM 复现时间点与这些事件对齐。
- 记日志:应用日志应有内存耗尽的 fatal error;进程级退出另记一条(退出信号、退出码、时间)。两边都留,才能区分“请求失败”与“worker 崩了”。
另外,依赖“分配失败会直接跳出”的假设要重看。引擎里分配现在可能直接失败返回空,自定义扩展、FFI、自己实现分配回调的地方,都要显式检查返回值。
来源: