一个打包超时,把 CPU 拉进审讯室
2026 年 2 月,Debian 13 龙架构移植版 loong13 维护者王邈在给 normaliz 做交叉编译时遇到一件怪事:这个数学软件的自带测试在 LA664 核心的龙芯服务器上跑着跑着就卡死在循环里出不来。一开始怀疑是 normaliz 自己用了弱内存序,或者 OpenMP 实现的 pragma omp atomic 有 bug。但把代码读到底、把 normaliz 简化到极限,问题依然在。
简单复现程序测不出来,意味着这不是软件问题。把目光转向底层,反汇编里 pragma omp atomic 被 LLVM 生成了 LoongArch64 的 amadd.d,而这条指令本来就是 LoongArch 用来实现原子加的“原子性”保障。
半年后,王邈和 jia.je 一起换了思路:让 AI 去找最小复现,他们负责指挥 AI 的调查方向。两天后,AI 把目光锁定到 normaliz 处理函数里的一段 memcpy,进而指向 glibc 在 LA664 上启用的 LASX 向量化内存搬运路径。
触发条件的三条腿
把这个 CPU bug 的触发条件拆开看,其实非常具体:
- 两个线程必须跑在不同的物理核上,同一个物理核上的 SMT 逻辑线程之间不会触发;
- 线程对同一地址执行不带数据屏障的原子操作,例如
amadd.d而非amadd_db.d; - 至少有一个线程在原子操作之间穿插向量化的内存读取。
在 jia.je 团队的最小复现里,normaliz 的每个数据点正好是 2208 字节、276 个 uint64_t,刚好是 LASX 的 256 位寄存器一次能装下的 1/8 区间。两个线程各自分片遍历,先做向量化 memcpy,再做 relaxed 原子加,一轮结束比对计数器。复现稳定性高到吓人:在 3C6000/S 两个物理核上跑 30 次试验,"双方都做 LASX 读"的触发概率几乎达到 100%;带 _db 后缀的 amcas_db.d 在相同条件下失败率是 0%。
Rust 也没躲过
更尴尬的是 Rust 标准库。std::sync::Arc 的引用计数、std::sync::mpsc 的 Sender 克隆,都用了不带数据屏障的 amadd。作者构造了一个 safe Rust 程序,用 Arc 或 mpsc 都能让程序崩溃,表现为 SIGABRT 或 glibc 报告堆被破坏——即典型的 use-after-free 模式。
但工程意义上的可利用面很窄:两个线程必须在同一进程内对同一计数器做 relaxed 原子操作,至少一方做向量化内存读取。这种触发条件天然被进程隔离挡住,不太可能被单方面远程利用。
龙芯的 14 天响应
8 月 26 日邮件反馈,9 月 9 日拿到测试固件。修复方法出人意料地轻:把 CPU 内部一个未文档化的 CSR MCSR24 的 bit 13 置 1。单核性能没变化,多核性能也只是略微下降,完全够用。
软件层面其实也有规避路径:编译器可以改生成 am<OP>_db.*,或者改用 LL/SC 循环。但代价是把所有现存的 LA664 二进制重新编译一遍。这条路在 14 天内显然走不通,而 CSR 一行命令就能修好。
受影响范围限定在使用 LA664 核心的 3C6000/S 和 3A6000 系列;更早的 LA464(3A5000)没有这个问题。龙芯承诺在国庆(10 月 1 日)之前公开发布固件。
AI 到底干了什么
回过头看,这个故事最有意思的部分不是龙芯的 14 天响应,而是 AI 在六个月的排查里到底起了什么作用。
第一轮排查中,人类已经基本能判断是 amadd 在丢更新,但他们没办法把 normaliz 整个代码库裁剪到一个最小复现。AI 在第二轮里做的事,本质上是把代码搜索范围从"编译原理/内存序/OpenMP 实现"收缩到"glibc memcpy 的实现选择",然后基于 xvld 指令的特性,直接拼出了一个稳定复现的最小程序。这是普通程序员不愿意干的"代码考古"活儿。
但 AI 也没有自动把所有事做完。明确告知 AI 原子加有问题、要求它聚焦 memcpy 这一关键提示,仍然来自开发者。换句话说,AI 干的是"把已知信息在大规模代码里快速验证并定位到具体一行",而"判断这是 CPU bug 不是软件 bug"这关键一跳,依然是人类工程师的活儿。
一个老话题的新注脚
CPU erratum 这件事,ARM 公版核的 Software Developer Errata Notice 里有一堆,Intel/AMD 各自有几十上百条文档化的 erratum,真正影响日常使用的很少。LoongArch 是新架构,生态还在长,这次能从一个普通的打包超时一路追到 CSR 修复,说明两件事:一是用户社区在认真测试,二是厂商在认真响应。
下一次,你在 LA664 上看到一个诡异的多线程 crash,先看一眼 glibc memcpy 是不是开了 LASX、编译器是不是在生成无 _db 后缀的原子指令。这两点组合到一起,大概能解释 9 成 LoongArch 上"明明代码看起来对但就是不稳"的玄学问题。