liandu2024:Open-Box v0.1.223 修复 FakeIP UDP 缓存竞态导致的崩溃

作者说明补丁统一 ReadPacket 的原子占用逻辑,并报告竞态检测与回归用例结果;v0.1.223 会连同 1.14.1-openbox-tcp8 内核下载,仍建议继续观察。

作者原文

你这份栈直接把问题指出来了,已定位并修好,v0.1.223 发出去了,内核版本 1.14.1-openbox-tcp8

是什么

问题在依赖库 github.com/sagernet/sing v0.9.4 的 CachedPacketConn:同一个 UDP 缓存包有两条"取走"的路,却用了两套不同的同步——

  • ReadCachedPacket() 用原子标志 taken.CompareAndSwap
  • ReadPacket() 只是裸检查 if c.buffer != nil

并发时 ReadPacket 检查完、还没来得及 DecRef,缓存就被另一边置成 nil 了,于是 refs.Add(-1) 打在空指针上——正是你栈里的

sync/atomic.(*Int32).Add
buf.(*Buffer).DecRef                    buffer.go:297
bufio.(*CachedPacketConn).ReadPacket    cache.go:187

addr=0x30 正好是 Buffer.refs 的偏移。你那次 slice bounds out of range [:1232] with capacity 0 是同一个竞态的另一面:读到了已经 Release 回收的内存。

你的栈里有 route.(*fakeIPNATPacketConn).ReadPacket,说明你开着 FakeIP,走 FakeIP 的 UDP 流量最容易撞上这条路径(不开 FakeIP 也有机会,只是概率低)。

反过来还有一份:ReadPacket 先取走时不会置 taken,随后的 ReadCachedPacket 仍会赢下 CAS,把一个 Buffer 已经是 nil 的包交给调用方——那是另一个空指针入口。

怎么修的

ReadPacket 也走同一个 taken 原子标志,谁抢到谁处理。

上游 main 分支我今天核对过,至今仍是裸检查,所以这个补丁是 Open-Box 自己打的(也是我们第一个改依赖库而不是 sing-box 本体的补丁)。验证:

  • Go 竞态检测器:打补丁前稳定报 DATA RACE(读 cache.go:182 / 写 cache.go:200),补丁后干净;
  • 三条回归用例进了内核构建,两个架构都跑;
  • 反向验证:拿未打补丁的库跑,用例当场失败并打印出那个 Buffer:<nil> 的包。

你需要做的

面板里点升级到 v0.1.223这一版会连内核一起下载(约 16 MB),不像前几版只下面板;升级过程中内核重启一次。

升完请留意一两天。如果还崩,麻烦再贴一次 logread -e sing-box | tail -n 80 —— 那就是另一条路径,我接着查。感谢你把完整栈贴出来,没有这个定位不到。