zlib-rs 会不会梦到 Raptor Lake

背景:Firefox 为什么要换 zlib-rs

从 Firefox 151.0.0 开始,gzip 的压缩/解压实现从传统的 C 语言 zlib 切换为 zlib-rs——由荷兰非营利组织 Trifecta Tech Foundation 维护的 Rust 实现。Trifecta Tech 在其官方博客中回忆,他们与 Mozilla 工程师从 2024 年夏天就开始接触,但这次集成足足花了两年时间才真正落地生产环境[1]

表面上看,zlib-rs 作为"插入式替代品"并不复杂:

  • 它在不同压缩等级下使用的算法与 zlib-ng 保持一致,但与原版 stock zlib 不完全相同,因此压缩输出的具体字节和长度会有细微差异,导致 Firefox 测试套件里大量基于"精确输出字节"或"输出长度"的断言需要更新[1:1]
  • Firefox 给所有符号加前缀(比如 inflate 变成 MOZ_Z_inflate)以避免符号冲突,zlib-rs 本身已支持符号前缀配置,这部分工作量可控[1:2]

按 Trifecta Tech 的说法,这些都只是"直来直去"的工程活——直到诡异的崩溃开始出现。

神秘崩溃:一个"逻辑上不可能失败"的边界检查

集成上线后,Firefox 这边开始收到崩溃报告(Bugzilla #1950764)。日志显示,失败点是一个 Rust 的数组边界检查(bounds check)——而这个检查按代码逻辑根本不应该失败[1:3]

Trifecta Tech 团队在博客里坦言这次很"幸运":Rust 的边界检查机制让错误暴露成了一次可观测的 panic,而如果是传统 C 语言实现,同样的内存错误很可能只是悄无声息的数据损坏,根本不会被发现[1:4]

问题是,团队在本地完全无法复现。随着越来越多崩溃报告涌入,一个规律逐渐浮现:几乎全部集中在 Intel Raptor Lake(13代/14代)CPU 上——也就是那批已经因"不稳定与性能退化"问题而臭名昭著的桌面处理器[1:5]

平行时空:RAD Game Tools 与 Oodle 的两年血泪史

巧的是,在 Mozilla/zlib-rs 团队摸不着头脑的同一时期,游戏行业里另一支团队也在和同一个硬件 bug 死磕——只是他们摸索的时间更长。这条线索值得单独展开,因为它最终成了破案的关键。

RAD Game Tools(Epic Games 旗下,Oodle 压缩库的开发商)早在 2023 年 3 月就开始收到《堡垒之夜》以及其他 Unreal 引擎/Oodle 客户的崩溃报告,主要表现为着色器(shader)解压失败[2]。负责 Oodle 的工程师 Fabian Giesen 在 2025 年 5 月发布的复盘文章里,详细记录了这段排查史:

  • 2023 年春:第一批报告全部来自同一家主板厂商的同一种主板+CPU组合,后来发现是音频驱动损坏了用户态代码里向量寄存器的内容——一个彻头彻尾的误导线索[2:1]
  • 后续报告里,有客户反映在 BIOS 里关闭 AVX 支持后崩溃消失,团队因此把 Oodle Data 里所有 SIMD 代码筛查了一遍,但没找到问题;甚至试过整体关闭解码器里的向量化代码,问题依然存在[2:2]
  • 2023 年 7 月前后:论坛上开始流传"关闭 E-core 能缓解"的说法,同时确认问题不止出现在一家主板厂商的产品上[2:3]
  • 2023 年 10 月:终于在 Epic 内部一台个人电脑上复现——关键条件是"着色器编译"与"Oodle 解压"同时在不同线程上运行,这个组合至今仍是复现该问题的核心配方[2:4]。即便如此,团队当时找到的复现方式仍然极不稳定,且耗时极长。
  • 与此同时,团队注意到很多主板厂商的默认 BIOS 设置堪称离谱:额定 253W TDP 的芯片,BIOS 默认给到 500W 功率墙、500A 电流墙[2:5]。一度怀疑问题主要源于主板厂商激进的默认超频设定。
  • 2024 年下半年:Intel 正式确认,症状的根源是 CPU 内部时钟树电路(clock tree circuitry)的物理性退化[2:6]

转折点出现在 2025 年春天:一位客户报告了一种"看起来像竞态或内存踩踏"的随机解码失败,但有一个反常特征——失败后用同一份输入数据立即重试,往往能够成功[2:7]。Giesen 指出,这个特征本身就排除了"数据真的损坏"的可能(因为重试时数据大概率来自有强 ECC 保护的 L2/L3 缓存),而更像是 CPU 在执行过程中出了错。这位客户的复现环境恰好也是 13/14 代 Intel CPU,并且还轻微超频了约 5%[2:8]

拿到失败时刻的解码器工作内存快照后,团队发现了几个反常规律[2:9]:

  1. 损坏的字节从不引发"比特流失步"那种典型的级联错误——译码器从头到尾都顺利走完了比特流读取,问题出现在更后面的环节。
  2. 损坏总是孤立的单字节,前后几千到上万字节完全正确——这不符合典型内存踩踏(通常一踩就是 4/8 字节对齐的整块)的特征。
  3. 被破坏的字节数值,永远落在 1 到 11 之间。

这第三点是真正的突破口。Oodle 大量使用 Huffman 编码,其译码表的每一项是一个 {len: u8, sym: u8} 结构——len 字段(码长,通常限制在 11 bit 以内)恰好落在 1~11 的范围。Giesen 意识到:被错误写出的不是符号值 sym,而是码长 len[2:10]

继续往下看 x86-64(配合 BMI2 指令集)的 Huffman 译码核心循环,问题最终定位在这一条指令上[2:11]:

mov   [rDest + <index>], ch   ; 本该写出符号值(RCX 的 bits 15:8)

ch 对应的是 RCX 寄存器的 bits 15:8,而紧邻它的 cl(bits 7:0)恰好存的就是码长。Giesen 团队的结论是:在受影响的 CPU 上,大约每解码 1 万字节会出现一次,这条指令本该写出 ch 的内容,实际却写出了 cl 的内容[2:12]

针对这个发现,RAD 给出的临时规避方案极其简单:多加一条移位指令,把数据显式挪到低字节寄存器再存,从而完全绕开"高字节寄存器直接写内存"这种写法:

shr   ecx, 8
mov   [rDest + <index>], cl   ; 改为从 cl 写出

这个修复随 Oodle 2.9.14(2025 年 5 月)发布,带来的性能损失在典型数据上大约只有 0.5%[2:13]

值得一提的是,Giesen 在文章末尾坦诚地标注了这部分是"推测":他认为 CPU 内部必然存在一组多路选择器(multiplexer),负责决定一次字节写入到底取寄存器的高字节还是低字节,而这组选择信号的时序余量(timing slack)在部分芯片上已经不足——超频或处于 turbo boost 高频区间时,该控制信号有时来不及到位,于是写出了寄存器低字节的内容[2:14]。这与 Intel 后续公布的官方根因高度吻合。

Intel 官方根因:Vmin Shift Instability

2024 年下半年,Intel 在其官方社区博客中正式公布了根因诊断,把这个问题命名为 Vmin Shift Instability[3]。核心结论是:

该问题被定位到 IA 核心内部的一个时钟树电路(clock tree circuit),这个电路在高电压、高温条件下,对"可靠性老化"(reliability aging)格外敏感;Intel 观测到这类条件会导致时钟的占空比(duty cycle)发生偏移,并进而引发系统不稳定[3:1]

更具体地,Intel 列出了四种会触发 Vmin 偏移的运行场景,并对应给出了微码层面的缓解措施[3:2]:

# 触发场景 对应缓解措施
1 主板电源交付设置超出 Intel 官方功耗指导值 采用 Intel Default Settings 推荐配置
2 eTVB(Enhanced Thermal Velocity Boost)微码算法,允许 i9 处理器即便在高温下也维持高性能状态 微码 0x125(2024 年 6 月)修复
3 SVID 微码算法以过高频率/时长请求高电压 微码 0x129(2024 年 8 月)修复
4 微码与 BIOS 代码在空闲/轻载期间仍请求偏高的核心电压 微码 0x12B(整合前两项并补充修复)

需要强调的是,Intel 明确指出 13/14 代移动端处理器(包括 HX 系列)以及之后的 Lunar Lake、Arrow Lake 架构均不受此问题影响——这是桌面端 Raptor Lake/Raptor Lake Refresh 特有的问题[3:3]

更关键、也更令人无奈的一点是:微码更新只能阻止退化进一步恶化,无法逆转已经发生的物理损伤。Giesen 在文章里直言,一旦机器表现出这些症状,说明硬件已经发生了真实的物理损伤,需要更换处理器;软件/微码层面能做的,最多是防止"换新之后又在几个月内退化出同样的问题"[2:15]。也正因如此,RAD 和 Mozilla 都不得不在软件层面"绕开"问题指令,而不是指望厂商修复就完事。

回到 Firefox:zlib-rs 里的 push_dist 函数

理解了 Oodle 那边的根因之后,zlib-rs 这边的 bug 就豁然开朗了。问题函数是 push_dist,逻辑非常直白——往一个缓冲区连续写三个字节[1:6]:

pub fn push_dist(&mut self, dist: u16, len: u8) {
    let buf = &mut self.buf.as_mut_slice()[self.filled..][..3];
    let [dist1, dist2] = dist.to_le_bytes();

    buf[0] = dist1;
    buf[1] = dist2;
    buf[2] = len;

    self.filled += 3;
}

但用 LLVM 22 编译出来的汇编里,其中一句恰好是:

mov     byte ptr [rsi + rdi + 1], ch

和 Oodle 案例里的指令形态几乎一模一样——都是把 RCX(或等价寄存器)的高字节 ch 直接写入内存。在受影响的 Raptor Lake 芯片上,这条指令偶发性地写成了低字节 cl 的内容,导致 zlib-rs 内部的 Huffman 编码数据被悄悄破坏,最终撞上了 Rust 的边界检查而 panic[1:7]

Trifecta Tech 给出的修复思路与 RAD 异曲同工,但用的是另一种手段——通过一小段 unsafe 代码,强制让 LLVM 生成"非对齐写入两字节"的指令,从根本上避免编译器选择高字节寄存器直写这种形态:

pub fn push_dist(&mut self, dist: u16, len: u8) {
    let buf = &mut self.buf.as_mut_slice()[self.filled..][..3];

    let bytes = dist.to_le_bytes();
    unsafe { buf.as_mut_ptr().cast::<[u8; 2]>().write_unaligned(bytes) }
    buf[2] = len;

    self.filled += 3;
}

这个修复由 Mozilla 工程师 Mike Hommey 实现并合并进 Firefox(对应 commit),随后也被上游合并进 zlib-rs 本体[1:8]。Trifecta Tech 表示会长期保留这段 unsafe 代码——团队认为这是"用一小段容易审查的 unsafe,换取在各种硬件平台上稳定运行"的合理取舍[1:9]

有意思的是,Trifecta Tech 还提到一个细节:LLVM 23 已经不再生成这种问题指令了,但作者自己也说,这很可能只是编译器代码生成策略变化带来的"误打误撞",并非 LLVM 项目有意识地修复了这个硬件适配问题[1:10]。等 zlib-rs 的最低支持 Rust 版本(MSRV)将来升级到要求 LLVM 23 的版本时,这段 workaround 才可能被移除。

为什么"压缩/解压代码"总能率先"抓到"硬件 bug?

把 Oodle 和 zlib-rs 两个故事放在一起看,会发现一个很有意思的共性:两次都是压缩/解压库率先暴露了硬件问题,而不是其他更常见的应用代码

原因并不是压缩代码更容易触发硬件错误,而是它更容易暴露硬件错误:

  • 解压算法天然需要处理"可能已损坏"的输入数据(磁盘损坏、网络传输错误等),因此普遍内置了大量一致性校验逻辑。
  • Rust 实现的 zlib-rs 还额外带有内存安全的边界检查。

这意味着,当硬件偶发性地写错一个字节时,压缩/解压代码大概率会在校验阶段就报错并 panic/返回错误码;而绝大多数其他业务代码遇到同样的单字节错误,往往只是默默产生一个错误结果,或者干脆没有任何可观察的异常——根本没人能把锅甩到 CPU 头上。这也是为什么 Raptor Lake 这个硬件问题,最早是从游戏的 shader 解压崩溃以及现在的浏览器 gzip 解压崩溃里被发现的,而不是从某个更"主流"的代码路径里被发现。

zlib-rs 带来的性能提升

抛开这场惊心动魄的硬件 bug 排查,Trifecta Tech 强调换用 zlib-rs 本身是非常值得的——尤其在 Linux x86_64 上,速度提升相当夸张。引用自他们博客里基于 zlib-py 项目的基准测试(对比 CPython 自带 zlib 与 zlib-rs 绑定)[1:11]:

  • 一次性解压 64KB 数据(level 1):提速约 32.5 倍
  • 一次性解压 1MB 数据(level 6):提速约 27.3 倍
  • 流式解压 1MB 数据(level 6):提速约 10.9 倍

压缩侧也有提速,但因为压缩比本身存在差异,横向对比没有解压那么直观[1:12]。另外团队提到,在 aarch64(尤其是 macOS)上提速幅度相对较小,原因是苹果自带的 zlib 动态库已经针对部分性能热点用内联汇编做了专门优化,这也提醒了 zlib-rs 团队后续还有继续优化的空间[1:13]

小结

这是一个挺难得的案例:一次看似普通的"把 C 库换成 Rust 实现"的工程升级,意外撞上了真实存在的硬件物理退化问题,并因为 Rust 自带的边界检查机制而被快速暴露出来,最终通过对照另一个行业(游戏开发)里独立但高度相似的排查经验,定位到了根因,并用一种"绕开编译器生成特定指令"的取巧方式完成了软件层面的规避。

它也提醒我们:在 13/14 代 Raptor Lake 桌面 CPU 已经大规模出货、且部分芯片已经发生不可逆物理退化的背景下,即便用户已经更新了 Intel 提供的微码(0x125/0x129/0x12B),应用层仍然有必要对"偏门但合法"的指令形态(比如这里的高字节寄存器直写)保持警惕——尤其是在数据完整性要求较高的场景里。

参考资料

延伸阅读:


  1. Folkert de Vries. “zlib-rs in Firefox”. Trifecta Tech Foundation Blog, 2026-06-16. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. Fabian Giesen. “Oodle 2.9.14 and Intel 13th/14th gen CPUs”. The ryg blog, 2025-05-21. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  3. Thomas Hannaford (Intel). “Intel Core 13th and 14th Gen Desktop Instability Root Cause Update”. Intel Community, 2024. ↩︎ ↩︎ ↩︎ ↩︎