代码 PHP 8.5.12 的 OPcache/JIT 修复:那些不报错、只算错的地方

2026-10-30 21:05:19

PHP 8.5.12 的 OPcache/JIT 修复:那些不报错、只算错的地方

PHP 8.5.12(22 Oct 2026)的 opcache 段几乎全是正确性修复。这批 bug 的共同点:进程不崩、不抛异常、不写 error log,只是结果和解释执行时不一样。下面按“优化器折叠 → JIT 对象语义 → IR 与 DCE → 缓存与内存”的顺序逐条过。

优化器折叠出来的错值

GH-24063:$cond ? -0.0 : 0.0 被折叠成单一 -0.0。 优化器把三元表达式两侧的浮点常量当成同一个值,直接留下 -0.0 常量,0.0 分支消失。负零和正零在 PHP 里是 == 相等但可观察不同的两个值:

  • 1 / $x:除零语义下符号不同,结果分别是 -INF 与 INF;
  • atan2() 的分支判断依赖零的符号;
  • round() / printf() 的格式化输出不同;
  • var_export() 序列化出 -0.0 还是 0.0,json_encode() 输出的文本同样带符号。

走 JIT 时这段代码被折叠,解释执行时不会,对拍能直接看出差异。

GH-23644:常量对常量的比较没被折叠。 优化器漏掉了本该在编译期算完的 constant-vs-constant 比较,把节点留给运行期,VM 最终在 zval_undefined_cv 崩溃。同一类问题在 GH-20890 里表现得更隐蔽:非简单 property hook 配合最小 tracing JIT,同样在 zval_undefined_cv 段错误。

GH-23693:循环不变加法的 guard 判错。 tracing JIT 对循环不变加法上的 guard 给出错误结果——循环体不变,但守卫条件被错误求值。

JIT 对对象布局的误判

GH-23628:tracing JIT 读 lazy proxy 的未定义属性槽。 PHP 8.4 的 lazy ghost/proxy 由反射创建,属性访问本应转发到真实实例。JIT 直接读了代理对象上那个未初始化的槽位,拿到占位值而不是真实值。属性存在、类型对得上,值不对。

GH-23679:父类私有属性写进子类的同名公有属性。 子类用同名属性 shadow 父类私有属性时,JIT 的作用域解析走错,父类私有状态的写入落到了子类可见的公有属性上。两个属性在 zval 层面名字相同、归属不同,这类污染不会报错,只会在后续读取时暴露。

IR 生成与 DCE

GH-23991:多次递归调用生成无效 IR。 同一函数内出现多处递归调用时,JIT 生成的 IR 不合法。

不可靠逃逸分析导致多处错误 DCE。 逃逸分析判断某个对象/值没有逃逸,死代码消除就把它删了,但实际仍有使用点。表现是随机缺失的副作用或读不到的值。同一批修复里还有 SCCP 对非逃逸对象维度上 isset() / empty() 的求值错误。

缓存、流路径与缓冲区

  • OSS-Fuzz #546798343:FCC(first-class callable)加 inlining 时优化器堆缓冲溢出。
  • OSS-Fuzz #5674034779193344:is_cacheable_stream_path() 读未初始化内存。
  • zend_analyze_calls() 的 call_stack 缓冲越界。
  • 文件缓存复用于不同 frameless function 扩展集时崩溃:一次请求的 opcache 文件缓存被另一套声明了 frameless function 的扩展复用,函数帧布局对不上。同类还有 Core 里的 use-after-free——dl() 加载的扩展声明 frameless function,后续请求调用它时释放后使用。

Core 与其它扩展

Core 侧:修复 GHSA-w8hw-w9fx-j79w、GHSA-6wmf-4rv5-p3rp(bailout 导致状态损坏);OSS-Fuzz 532353396(static type 断言失败);GH-23662(print_r() 的 NAN 警告);GH-21999(lazy object、var_dump() 与对象比较下的 GC 不一致);OSS-Fuzz #536440507(immutable class 断言错误);AVX 在 OS 未启用 AVX 状态时被误报为支持;GH-23980(ZEND_INCLUDE_OR_EVAL 上的 ZEND_ASSERT 违反);Windows 上 System V 共享内存模拟在映射末尾之后 attach 段;Fiber 内 GC 触发的析构函数异常没有重抛回触发 GC 的栈帧。

8.5.12 同时修了:BCMath\Number 截断到零仍保留负号、并与零比较为小于零;PDO 持久实例销毁时回滚了另一个实例还在用的事务(GH-23962);PDOStatement::execute() 失败后仍保留上一次结果;ReflectionProperty::skipLazyInitialization() 在 OPcache 下复制无效的常量默认值(GH-23842)。

排查建议

这类问题靠日志抓不到,只能靠对拍。

  1. 开关 JIT 对拍。 同一输入分别用 opcache.jit=tracing 和 opcache.jit=off 跑,diff 输出。opcache.jit 接受 disable、off、tracing、function 以及 1205 这类数字 flags;tracing JIT 在 php.ini-production 里默认关闭,性能敏感的部署常手动打开,所以线上默认配置和本地默认配置可能不是一回事。
  2. 用 opcache.jit_min_lin 缩范围。 提高最小行数阈值,把可疑函数排除出 JIT 覆盖,能定位到具体文件/函数。
  3. CLI 复现。 php -d opcache.enable_cli=1 -d opcache.jit=tracing script.php,绕开 FPM 的进程复用,保证每次都是干净状态。
  4. 升级前后对拍。 固定输入,在 8.5.11 与 8.5.12 上各跑一遍,比较 var_export() / json_encode() 的完整输出,负零、浮点格式化和对象属性顺序都会暴露出来。
  5. 换版本时清文件缓存。 frameless function 和扩展集变化相关的崩溃来自旧 opcache.file_cache 被复用,升级后先清空再建缓存。
复制全文 生成海报 PHP OPcache JIT 性能与调试

推荐文章

程序员茄子在线接单