HeadlinesBriefing HeadlinesBriefing.com

Go 运行时错误:32 位嵌入式 Linux 崩溃分析

Hacker News •
×

我们的日常工作通常围绕 Linux 和安全主题展开,深入软件栈底层。尽管如此,比你想象的更频繁的是,我们最终会调试那些运行在更高层的应用程序。有时这样的问题源自 Linux 内核,有时则不然。在这篇博客文章中,我们展示了我们如何发现并修复了 Go 运行时内部的一个错误。最近,一位客户报告称,他们用 Go 编写的应用程序在其嵌入式 Linux 系统上时不时崩溃。崩溃总是表现为完全相同的致命错误,签名为:runtime: netpoll: eventfd ready for 5。起初我们认为应用程序本身有 bug。但仔细检查错误后,这更像是 Go 的 netpoll 机制内部假设不再成立。该错误消息来自 src/runtime/netpoll_epoll.go 中的 netpoll():if ev. Events != linux. EPOLLIN { throw("runtime: netpoll: eventfd ready for something unexpected") }。在这条代码路径上,netpoll 代码期望 EPOLLIN 是唯一触发的事件,但它收到了 5,即 EPOLLIN|EPOLLOUT。为什么 epoll 突然报告超过 EPOLLIN 的事件,而代码只请求了 EPOLLIN?在深入调查之前,我们将错误信息输入搜索引擎,希望有人之前遇到过同样的问题。这直接引导我们找到了 Go 项目问题追踪器中的一条报告:runtime: netpoll: eventfd ready for something unexpected。该问题描述了我们看到的确切致命错误。此外,它也发生在 32 位 ARM 嵌入式 Linux 系统上!报告者还指出,崩溃发生在长时间运行的应用程序中。这也符合我们客户的描述。太棒了!该问题自 2025 年 3 月起就一直未解决。Go 维护者也曾驳回过一次修复尝试。从该问题的评论中,我们得知该致命错误仅在 32 位 ARM 和 i386 Linux 系统上出现,内核版本各不相同。一些内核较旧,有些较新。没有任何报告者在 x86_64 或 arm64 系统上看到过此问题。最初我们怀疑 epoll 在 32 位 ARM 或 i386 上行为不同。我们很快否定了这个想法,因为 epoll 是内核中的通用核心代码。为什么它只会在 32 位 ARM 或 i386 上返回伪造的事件集?尽管如此,错误仅在 32 位系统上出现这一事实仍然困扰着我们。作为下一步,我们审查了 Go netpoll 代码中的 epoll 使用情况。在 LLM 的帮助下,我们通读了 src/runtime/netpoll_epoll.go,重点关注 32 位陷阱,如整数转换。审查揭示了 Go 的 netpoll 代码使用了 struct epoll_event 的 data 字段。Linux epoll 可以在内核中存储一个 8 字节的 cookie,并将其作为触发事件的一部分返回到用户空间。应用程序使用此 cookie 来为事件附加元数据,例如区分不同的事件源。struct epoll_event { __poll_t events; /* ev.

Events in Go netpoll */ __u64 data; /* ev. Data in Go netpoll */ }; 深入 Go 运行时,主事件处理器需要判断一个事件是属于事件 fd 还是 socket fd。它通过将 ev. Data 与其内部事件 fd 对象的地址进行比较来决定:if *(**uintptr)(unsafe.

Pointer(&ev. Data)) == &netpoll Event Fd {...}。继续阅读代码发现,ev. Data 要么持有 netpoll Event Fd 的原始指针,要么持有每个 socket 对象的 poll Desc 的带标签指针。指针标签是一个计数器 fdseq,用于区分被回收的 poll Desc 对象。到目前为止一切正常。在同一个字段中混合原始指针和带标签的指针看起来有些可疑。但这如何导致崩溃尚不明确。检查 Go 在内存中如何布局带标签的指针终于揭示了根本原因。在 32 位系统上,指针占用 4 字节,而 data 字段期望 8 字节。这种大小不匹配导致在存储指针值时发生截断。上位 4 字节在转换过程中被清零。当运行时后来将截断后的指针与原始事件 fd 地址进行比较时,...

来源: Hacker News · 由HeadlinesBriefing整理摘要