HeadlinesBriefing HeadlinesBriefing.com

Goランタイムバグ:32ビット組み込みLinuxクラッシュ

Hacker News •
×

私たちの日常業務は通常、Linuxとセキュリティのトピックを中心に展開し、ソフトウェアスタックの深部にまで及んでいます。それにもかかわらず、あなたが考えているよりもはるかに頻繁に、私たちはスタックのはるかに上位で動作しているアプリケーションのデバッグに終わります。時々、そのような問題の根源はLinuxカーネルにあり、時々それは別のところにあります。このブログ投稿では、私たちがGoランタイム内部のバグを見つけて修正した方法を示します。最近、あるお客様が、Goで書かれたアプリケーションが彼らの組み込みLinuxシステムの1つで時折クラッシュすることを報告しました。クラッシュはいつも同じ致命的エラーで、署名は次の通りです:runtime: netpoll: eventfd ready for 5。最初は、アプリケーション自体にバグがあると考えました。しかし、エラーを詳しく調べてみると、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)を受け取りました。なぜコードがEPOLLINのみを要求しているにもかかわらず、epollが突然EPOLLINより多くのイベントを報告するようになったのでしょうか。より深く調査する前に、私たちはこのエラーメッセージを検索エンジンに入力し、同じ問題に直面した人がいないかを調べました。これにより、Goプロジェクトのイシュートラッカーにある次のレポートに直接たどり着きました:runtime: netpoll: eventfd ready for something unexpected。このイシューは、私たちが観察したまさにその致命的エラーを説明しています。さらに、これは32ビットARM組み込みLinuxシステムでも発生しました!報告者たちはまた、このクラッシュが長時間実行されるアプリケーションで発生することにも言及しました。これは、私たちのお客様の説明とも一致しました。バンゴー!このイシューは2025年3月からオープンかつ未解決の状態でした。Goのメンテナーも、この問題を修正しようとした1回の試みを却下していました。このイシューのコメントから、この致命的エラーが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バイトのクッキーを保存し、それを発火イベントの一部としてユーザースペースに返します。アプリケーションはこのクッキーを使ってイベントにメタデータを付加し、例えば異なるイベントソースを区別するために使用します。struct epoll_event { __poll_t events; /* ev.

Events in Go netpoll */ __u64 data; /* ev. Data in Go netpoll */ }。Goランタイムの奥深くでは、メインイベントハンドラーは、イベントがイベントファイルディスクリプターに属するか、ソケットファイルディスクリプターに属するかを判断する必要があります。これは、ev. Dataとその内部イベントファイルディスクリプターオブジェクトのアドレスを比較することで行われます:if *(**uintptr)(unsafe.

Pointer(&ev. Data)) == &netpoll Event Fd {...}。コードをさらに読み進めると、ev. Dataはnetpoll Event Fdへの生ポインタか、またはソケットごとのpoll Descオブジェクトへのタグ付きポインタのいずれかを保持していることがわかりました。ポインタのタグは、fdseqと呼ばれるカウンターで、再利用されたpoll Descオブジェクトを区別するために使用されます。ここまでは問題ありません。同じフィールドに生ポインタとタグ付きポインタを混在させるのは疑わしいと感じました。しかし、これがクラッシュとどのように関連しているかはまだはっきりしていませんでした。Goがメモリ内でタグ付きポインタをどのように配置しているかを調べた結果、ついに根本原因が明らかになりました。32ビットシステムでは、ポインタは4バイトを占有しますが、dataフィールドは8バイトを期待しています。このサイズの不一致により、ポインタ値を保存するときに切り捨てが発生します。上位4バイトは変換中にゼロに設定されます。ランタイムがその後、切り捨てられたポインタを元のイベントファイルディスクリプターのアドレスと比較するとき、...

出典: Hacker News · 要約:HeadlinesBriefing