没有不值得去解决的问题,也没有不值得去学习的技术!

紧急升级 PHP 8.5.12:一次耗时 2.5 小时的生产环境安全更新

【图15:第二次生产切换成功,PHP 8.5.12、PHP-FPM、FastCGI 和 WordPress 全部验证通过】

作者:

在

,

OneinStack 生产运维实践

【图 3:OneinStack 原生方案与 oneinstack-custom 的完整对比表】

(1) OneinStack 生产定制分支正式公开:从 PHP 8.5 与 Nginx/OpenSSL 实战到 GitHub 持续维护

【图 1:阿里云 ECS 系统盘使用率超过 80% 的告警邮件】

(2) 阿里云 ECS 系统盘占用超过 80%:清理 PHP、Nginx、Redis 升级残留后从 81% 降至 42%

【图15:第二次生产切换成功,PHP 8.5.12、PHP-FPM、FastCGI 和 WordPress 全部验证通过】

(3) 紧急升级 PHP 8.5.12:一次耗时 2.5 小时的生产环境安全更新

2026 年 10 月 21 日晚,我开始了一次 PHP 安全更新,将网站生产环境从 PHP 8.5.9 升级到 PHP 8.5.12。

原本以为,同一个 PHP 8.5 分支内的补丁版本升级,应该不会花费太多时间。

然而,从开始准备到最终上线,整个过程大约花了 2.5 个小时。

期间经历了源码编译、扩展重新构建、生产配置迁移、Python 兼容性调整、独立 PHP-FPM 验证,以及一次让人紧张的生产切换失败。

好在最终顺利完成了升级,WordPress 网站恢复正常,原有配置也完整保留。

这篇文章记录整个过程,包括成功的步骤、遇到的问题,以及我对下一次升级流程的思考。

一、为什么必须立即升级 PHP?

事情要从 PHP 官方发布的一则安全预告说起。

2026 年 10 月 20 日,PHP 官方提前宣布,将于 10 月 21 日 14:00 UTC 发布一次重要安全更新。

预告中特别提到,这次更新涉及两个严重漏洞,CVSS v4.0 评分均为 9.2。

官方建议 PHP 用户提前做好准备,以便新版本发布后尽快升级。

【图1:PHP 官方提前发布安全更新预告,两个漏洞的 CVSS v4.0 评分均为 9.2】
【图1:PHP 官方提前发布安全更新预告,两个漏洞的 CVSS v4.0 评分均为 9.2】

到了 10 月 21 日,官方正式公布了漏洞详情,并发布多个 PHP 分支的安全修复版本:

  • PHP 8.5.12
  • PHP 8.4.27
  • PHP 8.3.36
  • PHP 8.2.35

这次修复涉及两个可能导致未经身份验证即可远程执行代码的严重漏洞:

CVE-2026-109618:与内存不足处理过程中的内部请求数据状态有关。

CVE-2026-109619:与内存释放后残留的全局状态有关,后续请求可能使用这些未正确重置的数据。

官方特别指出,风险不仅涉及 Apache 的 PHP 模块,也包括 PHP-FPM、php-cgi 等处理 Web 请求的 PHP 运行环境。

我的网站使用 Nginx + PHP-FPM,自然也需要重视这次更新。

【图2:PHP 官方正式公布两个严重远程代码执行漏洞,并建议用户立即更新】
【图2:PHP 官方正式公布两个严重远程代码执行漏洞,并建议用户立即更新】

当时生产环境运行的是 PHP 8.5.9,因此升级目标很明确:尽快更新至 PHP 8.5.12,同时保留现有网站配置和运行能力。

PHP 官方公告:

https://www.php.net/archive/2026.php

二、为什么没有直接覆盖现有 PHP?

网站服务器运行的是 Alibaba Cloud Linux 3,PHP 使用源码编译安装,安装位置为:

/usr/local/php

这套环境通过 OneinStack 相关脚本管理,并且已经有一些长期使用的配置调整。

如果直接把新版本编译安装到原目录,一旦出现配置覆盖、扩展不兼容或者 PHP-FPM 启动失败,就可能影响正在运行的网站。

因此,我没有一开始就覆盖生产 PHP,而是先在独立目录中编译和验证新版本。

1. 编译 PHP 8.5.12

首先获取 PHP 官方源码包,并核对 SHA-256。

接着参考当前生产环境的编译参数,使用 PHP 8.5.12 源码重新编译。

编译过程中还需要处理构建环境中的 iconv 头文件冲突问题。最终通过隔离的头文件覆盖方式完成构建,避免直接修改系统头文件。

编译完成后,验证命令行程序和 PHP-FPM 程序均为 PHP 8.5.12。

【图3:PHP 8.5.12 源码编译成功,原有生产 PHP 8.5.9 仍正常运行】
【图3:PHP 8.5.12 源码编译成功,原有生产 PHP 8.5.9 仍正常运行】

2. 安装到隔离目录

为了确保新版本不会提前影响网站,安装时使用了 INSTALL_ROOT。

安装完成后,新 PHP 位于:

/usr/local/src/php85-security-20261021/stage/usr/local/php

这一阶段只是准备新版本程序,并没有修改生产目录。

【图4:PHP 8.5.12 成功安装到隔离目录,CLI 与 PHP-FPM 版本验证通过】
【图4:PHP 8.5.12 成功安装到隔离目录,CLI 与 PHP-FPM 版本验证通过】

不过,这里还有一个容易忽略的问题。

隔离安装目录不等于 PHP 的运行时编译前缀也会自动改变。

新 PHP 的编译前缀仍然是 /usr/local/php。

也就是说,在最终切换以前,使用隔离程序进行测试时,需要明确指定对应的配置、扩展加载位置等参数。

这个细节也影响了后续的扩展编译和配置验证。

三、重新编译三个 PHP 扩展

生产环境使用了三个需要保留的动态扩展:

LDAP、Imagick 和 Redis。

虽然这次只是 PHP 8.5 分支内的补丁升级,但我仍决定使用新编译的 PHP 环境重新构建这些扩展,而不是直接复制旧版本中的模块文件。

这样可以验证扩展与目标 PHP 运行环境是否兼容。

1. LDAP

首先处理 LDAP。

为了让扩展构建工具指向隔离安装的 PHP 8.5.12,需要准备相应的 phpize 和 php-config 工具副本。

随后完成 LDAP 扩展的配置、编译与加载测试。

【图5:LDAP 扩展重新编译完成,并成功由 PHP 8.5.12 加载】
【图5:LDAP 扩展重新编译完成,并成功由 PHP 8.5.12 加载】

2. Imagick

接下来重新编译 Imagick 3.8.1。

这里不仅需要确认 PHP 扩展本身能够加载,还要检查它依赖的 ImageMagick 库。

测试结果显示,Imagick 可以在 PHP 8.5.12 环境中正常加载,使用的 ImageMagick 版本为 7.1.1-10。

【图6:Imagick 3.8.1 重新编译成功,ImageMagick 依赖和扩展加载验证通过】
【图6:Imagick 3.8.1 重新编译成功,ImageMagick 依赖和扩展加载验证通过】

3. Redis

最后处理 Redis 6.3.0。

与前两个扩展一样,先完成编译,再通过新 PHP 验证模块加载情况。

【图7:Redis 6.3.0 扩展重新编译并成功加载】
【图7:Redis 6.3.0 扩展重新编译并成功加载】

至此,三个动态扩展均通过测试。

不过,我并没有立即部署新 PHP。

因为还有一个更麻烦的问题:如何保留已经调整过的生产配置?

四、生产配置迁移比预期复杂

这次升级,我不希望简单地把旧版 php.ini 整份复制到新版中。

原因并不是旧配置一定存在问题,而是这种做法可能把不再支持的参数、历史默认值和旧版本配置一起带到新环境。

反过来,如果完全使用新版默认配置,也可能丢失生产环境的调优参数。

例如,当前生产环境包含:

  • memory_limit = 512M
  • max_execution_time = 600
  • upload_max_filesize = 128M
  • post_max_size = 128M
  • opcache.memory_consumption = 192
  • opcache.interned_strings_buffer = 32
  • pm.max_children = 14

这些配置并不是升级之后可以随意丢失的。

1. 先备份生产配置

我先保存当前 PHP、OPcache 和 PHP-FPM 的配置文件。

最终生成了 7 个文件的生产配置快照,并通过 SHA-256 记录其内容。

【图8:生产 PHP、OPcache 与 PHP-FPM 配置快照创建成功,共包含 7 个文件】
【图8:生产 PHP、OPcache 与 PHP-FPM 配置快照创建成功,共包含 7 个文件】

2. 审核 151 项配置

为了避免直接覆盖配置,我使用了一个配置迁移工具,整理旧配置来源,并逐项决定如何处理。

最终共识别出 151 项参数:

分类数量处理原则
默认配置86使用 PHP 8.5.12 官方模板
生产配置30保留现有调优值
部署配置34保留现有部署要求
已删除配置1审核后不再迁移

另外,还发现 9 处不同值的重复赋值,需要确认实际生效的是哪一个。

例如,旧配置中的 opcache.consistency_checks 已经不再受 PHP 8.5 支持,因此不能原样迁移。

经过审核,最终生成了正式批准的配置迁移策略。

【图9:151 项配置迁移分类审核完成,9 处覆盖优先级和三个扩展产物均已确认】
【图9:151 项配置迁移分类审核完成,9 处覆盖优先级和三个扩展产物均已确认】

这一过程比最初预计的复杂得多。

但至少让我确认:新版本上线时,真正需要保留的生产参数没有被遗漏。

3. 迁移工具自身也需要修复

接下来又出现了两个问题。

首先,服务器的系统 Python 为 3.6.8,而迁移工具最初使用了部分需要更新版本 Python 才支持的写法。

我曾考虑安装 Python 3.11。

但检查安装计划后发现,除了新增 Python 软件包,还会升级系统的 chkconfig。

为了执行一次 PHP 配置迁移,就额外修改系统组件,我认为没有必要。

因此,最终选择让迁移工具兼容 Python 3.6.8。

随后,在配置生成阶段,又遇到了另一处阻断:

php/engine: unknown/removed/unavailable target directive

进一步排查发现,engine 虽然存在于 PHP 8.5.12 官方配置模板中,但当前运行时并没有注册这个参数。

同类情况不止一项。

所以问题不是简单地删除 engine 就能解决,而是迁移工具的判断条件需要更准确。

最终调整为:对于已明确审核为默认配置、并且能够与目标官方模板中的有效赋值完全对应的参数,可以沿用新版模板;其他未知参数依然需要阻断。

修改完成后,回归测试结果为 33 项测试执行,其中 28 项通过、5 项跳过,没有失败。

【图10:修复配置迁移工具后,回归测试完成,没有测试失败】
【图10:修复配置迁移工具后,回归测试完成,没有测试失败】

之后,再使用服务器实际的 Python 3.6.8 验证配置清单,并完成 PHP 8.5.12 候选配置生成。

151 项参数全部通过校验,三个扩展产物也完成了完整性检查。

【图11:PHP 8.5.12 候选配置生成与验证通过,151 项参数均得到处理】
【图11:PHP 8.5.12 候选配置生成与验证通过,151 项参数均得到处理】

五、独立验证 PHP-FPM,而不只是检查版本号

完成编译和配置迁移,并不意味着 PHP-FPM 一定能够正常提供 Web 服务。

为了进一步验证,我使用独立的 Unix Socket 启动 PHP 8.5.12 的 PHP-FPM。

这一阶段不接管生产请求,也不会替换正在运行的 PHP 8.5.9。

随后向新 PHP-FPM 发送真实的 FastCGI 请求,检查:

  • PHP 版本是否为 8.5.12。
  • 请求是否由 www 用户执行。
  • 生产调优参数是否正确加载。
  • OPcache 是否可用。
  • LDAP、Imagick、Redis 是否正常加载。
  • 配置目录中的 INI 文件是否全部读取。

最终全部通过。

【图12:独立 PHP-FPM worker 成功处理 FastCGI 请求,配置和三个扩展验证通过】
【图12:独立 PHP-FPM worker 成功处理 FastCGI 请求,配置和三个扩展验证通过】

到这里,我已经比较有信心了。

但在真正切换生产版本之前,仍然需要给自己留一条退路。

六、备份 PHP 8.5.9,准备正式部署

除了先前保存的配置快照,我还备份了完整的旧版 PHP 安装文件。

最终生成的 PHP 8.5.9 回滚归档约为 376MB,并完成 SHA-256 和关键文件校验。

【图13:生产 PHP 8.5.9 回滚备份创建成功,关键文件与归档完整性验证通过】
【图13:生产 PHP 8.5.9 回滚备份创建成功,关键文件与归档完整性验证通过】

接着,将 PHP 8.5.12 的程序、已审核配置以及三个扩展整理成完整的待部署目录。

过程中还发现,新目录中的两个配置子目录权限为 700,而生产环境使用 755。

修正权限后,再以真正的 www 用户执行 PHP 8.5.12 加载测试,结果正常。

至此,编译、扩展、配置、PHP-FPM 和回滚备份都已经完成验证。

随后便开始正式切换。

七、第一次生产切换失败,自动回滚也报告失败

第一次切换采用的流程并不复杂:

先停止 PHP 8.5.9 的 PHP-FPM,保留旧版安装目录,再将 PHP 8.5.12 放到 /usr/local/php,最后启动新 PHP-FPM。

前面的目录切换顺利完成。

但在启动新版 PHP-FPM 后,脚本进入了自动回滚。

终端显示:

Plaintext
FAILED_STAGE=START_NEW_FPM
RESULT=CRITICAL
ROLLBACK=FAILED
ACTION=MANUAL_RECOVERY_REQUIRED
【图14:第一次生产切换在启动新 PHP-FPM 阶段报告失败,自动回滚也返回失败】
【图14:第一次生产切换在启动新 PHP-FPM 阶段报告失败,自动回滚也返回失败】

看到 RESULT=CRITICAL,确实有些紧张。

因为此时最需要确认的并不是新版本为什么失败,而是网站是否已经恢复正常。

于是,我没有立即重新执行部署,而是检查实际服务器状态。

结果发现:

  • /usr/local/php 已经恢复为 PHP 8.5.9。
  • PHP 8.5.12 被保留在失败版本目录中。
  • PHP-FPM 服务处于正常运行状态。
  • 实际 FastCGI 请求通过。
  • WordPress 源站 /wp-json/ 返回 HTTP 200。

也就是说,脚本报告自动回滚失败,但实际生产服务已经恢复正常。

这也说明,自动化脚本报告的状态并不一定等于系统最终状态。尤其在生产切换失败时,不能只依靠最后一行结果判断网站是否真的不可访问。

问题可能出在哪里?

随后查看 systemd 日志,发现切换期间的新旧 PHP-FPM 都出现了启动记录,而且操作发生在非常接近的时间内。

结合切换脚本的实现,我怀疑问题与服务启动后的检查时机有关。

原脚本在 systemctl start php-fpm 返回后,立即检查服务和 Socket 状态。

但服务管理器认为启动操作完成,并不必然代表 PHP-FPM 已经能够立即处理请求。

如果检查发生得太早,就可能产生误判。

不过,这只是根据现有证据作出的判断,并没有完全证明第一次失败的根因。

因此,后续修改的重点不是放宽所有验证条件,而是让启动检查真正等待服务就绪。

八、第二次切换:等待真实请求成功

第一次失败后,我没有重新编译 PHP,也没有重新生成配置。

因为此前已经确认,PHP 8.5.12 的部署文件完整性校验仍然通过,旧版 PHP 8.5.9 也已经正常恢复。

因此只调整切换流程。

最重要的变化,是在启动新 PHP-FPM 后增加有限时间的等待,并使用真实 FastCGI 请求进行验证。

只有当新版本实际能够处理请求,而且返回的 PHP 版本、扩展和配置均符合预期时,才认为服务已经就绪。

第二次切换依然保留失败回滚能力,但没有再重复前面的大量准备工作。

这一次,结果顺利通过:

Plaintext
RESULT=PASS
STAGE=PHP8512_PRODUCTION_CUTOVER
PRODUCTION_PHP=8.5.12
FPM=ACTIVE
FASTCGI=PASS
WORDPRESS=PASS
EXTENSIONS=3/3
【图15:第二次生产切换成功,PHP 8.5.12、PHP-FPM、FastCGI 和 WordPress 全部验证通过】
【图15:第二次生产切换成功,PHP 8.5.12、PHP-FPM、FastCGI 和 WordPress 全部验证通过】

升级完成后,我又独立检查了一次生产环境。

结果显示:

  • PHP 版本已经更新至 8.5.12。
  • PHP-FPM 主进程正常运行。
  • 原有 512M 内存限制仍然生效。
  • LDAP、Imagick、Redis 均正常加载。
  • WordPress 源站动态接口返回 HTTP 200。
  • PHP 8.5.9 的完整安装目录仍然保留,可供必要时恢复。

到这里,这次安全更新才算真正完成。

九、为什么一次补丁升级花了 2.5 个小时?

回顾整个过程,我认为原因不只是 PHP 源码编译和扩展构建耗时。

更主要的是,这次并没有一套已经完整验证、可以直接重复使用的升级流程。

不少问题都是在实际执行时才逐步发现和解决的。

例如:

配置迁移工具与服务器 Python 版本不兼容,需要临时调整。

官方配置模板中的部分参数无法直接通过运行时注册表判断,需要修复迁移规则。

扩展需要重新编译和验证,并正确处理隔离安装路径。

待部署目录还需要检查权限,确保 www 用户能够读取配置。

第一次正式切换又因为就绪检查问题而中断,随后还需要确认旧版服务确实恢复正常。

这些工作单独来看,都有其必要性。

但如果下一次 PHP 补丁升级还要重新走一遍,就显得太浪费时间了。

尤其对于一台已经正常运行的网站服务器而言,我更希望把工作量集中在真正有价值的地方,而不是不断补充复杂的验证脚本。

十、下一次升级应该怎么改进?

这次升级虽然花了 2.5 个小时,但也留下了不少可以复用的成果。

例如:

已经完成的 PHP 配置迁移工具,可以继续处理新版默认配置和原有生产调优之间的关系。

扩展构建与验证过程已经有了实践记录。

部署前的完整性检查、真实 FastCGI 验证、旧版目录保留和失败回滚,也都经过了实际使用。

下一步,我打算回到 oneinstack-custom 仓库,整理并完善这些脚本。

不过,这次经历也让我进一步考虑网站可用性。

当前成功采用的仍然是停止旧 PHP-FPM、切换目录、启动新 PHP-FPM 的方式,因此切换期间可能出现短暂请求中断。

对于未来的升级,我更希望能够让新旧 PHP-FPM 暂时并行运行,先验证新版服务,再通过 Nginx 将请求平滑切换过去。

如果能够在现有工具基础上,以较小的改动实现这种方式,就有机会同时减少升级时间和网站中断。

但这部分目前还只是后续改进目标,并不是本次已经完成的功能。

我也不打算为了追求理论上最完善的方案,再花大量时间进行复杂重构。

能复用的就复用,能简化的就简化,优先解决真正影响升级效率和可靠性的问题。

十一、总结

这次 PHP 8.5.9 到 PHP 8.5.12 的升级,最终顺利完成。

从官方发布严重安全漏洞公告,到完成源码编译、扩展验证、生产配置迁移和最终部署,整个过程持续约 2.5 个小时。

中途第一次切换失败,自动回滚还报告了严重错误,但后续检查确认生产环境已经恢复。

调整服务就绪验证方式后,第二次切换成功,网站正式运行 PHP 8.5.12。

这次经历让我更加确定一件事:

升级流程的价值,不只在于能不能成功升级,还在于下一次执行时,能否减少重复劳动和不必要的复杂度。

对于严重安全漏洞,及时更新当然是首要目标。

但从长期维护的角度来看,可靠、简单、可重复的升级流程,同样值得投入精力。

接下来,我会继续完善仓库中的升级工具。

等真正完成改进并经过验证后,再单独记录这次升级流程的优化成果。


相关链接:

PHP 官方网站:https://www.php.net/

PHP 2026 年安全公告:https://www.php.net/archive/2026.php

OneinStack 定制仓库:https://github.com/shuijingwan/oneinstack-custom

阿里云 ECS 系统盘占用超过 80%:清理 PHP、Nginx、Redis 升级残留后从 81% 降至 42%

关于作者 · 持续学习与技术实践

我是一名拥有 15 年以上开发经验的技术从业者,自 2013 年起持续运营个人技术博客,记录工作与个人项目中遇到的真实问题,以及解决问题过程中的探索、实践和思考。

除了技术写作,我也在持续开发和维护自己的独立项目,探索新技术和新工具在实际场景中的应用。我始终相信,没有不值得去解决的问题,也没有不值得去学习的技术。

如果这篇文章对你有所帮助,欢迎浏览博客中的其他文章,也欢迎交流技术经验与想法。

关于我  ·  GitHub  ·  邮件联系