文章摘要
Cloudflare在运行基于Go语言的arm64服务时,发现极少数情况下会出现栈回溯失败的系统崩溃。凭借海量请求规模的优势,他们追踪到一个罕见的Go编译器bug——该编译器在特定条件下会生成存在竞态条件的机器码。经过深入调查,团队最终定位到这是arm64架构编译器优化过程中的一个缺陷。
文章总结
如何在Go语言的arm64编译器中发现一个竞态条件漏洞
背景
Cloudflare每天处理海量HTTP请求,庞大的业务规模使得我们能够发现一些极其罕见的漏洞。最近,我们发现Go语言arm64编译器中的一个竞态条件漏洞,该漏洞会导致生成的代码出现竞态问题。
问题初现
我们在运行一个配置内核处理Magic Transit和Magic WAN流量的服务时,监控系统发现arm64机器上偶尔会出现panic。最初,我们以为这是罕见的栈内存损坏问题,由于影响较小,没有立即跟进。然而,问题持续出现,每天多达30次panic,且无法找到明显原因。
深入调查
我们观察到两种崩溃情况: 1. 致命错误:栈展开未完成,表明栈已损坏。 2. 段错误:在展开栈时访问了无效内存。
进一步分析发现,崩溃均发生在栈展开过程中,且与Go Netlink库的(*NetlinkSocket).Receive函数相关。通过核心转储分析,我们发现goroutine在函数尾声的两条指令之间被抢占,导致栈指针处于不一致状态。
根本原因
在arm64架构中,Go编译器会将大栈帧的调整拆分为多条指令。如果在这些指令之间发生异步抢占,栈指针会处于无效状态,导致后续栈展开失败。这是一个典型的单指令竞态窗口问题。
复现与修复
我们构建了一个最小复现案例,确认了漏洞的存在。Go团队随后修复了该问题,确保栈指针调整以原子方式完成。修复已包含在Go 1.23.12、1.24.6和1.25.0版本中。
总结
这次调试过程极具挑战性,涉及对Go运行时底层机制的深入理解。我们不仅解决了一个罕见的竞态条件问题,还为Go语言的稳定性做出了贡献。Cloudflare始终欢迎喜欢解决此类复杂问题的工程师加入我们的团队。
评论总结
这篇评论主要围绕技术博客内容和相关技术讨论展开,以下是主要观点总结:
- 对文章的赞赏
- 多位用户表达了对文章的喜爱和赞赏 "Really enjoyed reading this. Thanks for writing it!" (javierhonduco) "Excellent article as always from the cloudflare blog" (Agingcoder)
- 技术细节讨论
- 关于ARM汇编和编译器bug的讨论 "it's just that that's where the split was. The IR could've done it" (Neywiny) "Compiler bugs are actually quite common...some of them only appear when you work at a very large scale" (Agingcoder)
- 技术建议
- 提供了关于堆栈指针操作的建议 "Always adjust your stack pointer atomically, kids." (dreamcompiler) "push then pop the stack size maybe?" (Neywiny)
- 实用信息
- 有人分享了问题修复的链接 "here's the fix: https://github.com/golang/go/commit/f7cc61e7d7f77521e073137c..." (pengaru)
- 关于ARM64服务器的疑问
- 询问Cloudflare使用ARM64服务器的情况 "what ARM64 machines are you using and what are they used for?" (riobard)
- 批评观点
- 对信号处理程序中特殊操作提出质疑 "doing crazy shit like swizzling the program counter in a signal handler...is not a good idea" (gok)
- 对技术团队的赞赏
- 赞扬文章叙述清晰和团队技术能力 "description so clear it makes me feel smarter than I am" (renewiltord) "this team is a bunch of hotshots who have the skill to do this on demand" (renewiltord)