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