概要
- Cloudflareは Goのarm64コンパイラのバグ を大規模運用中に発見
- Magic Transit/Magic WAN向け制御サービス で断続的なpanicを観測
- バグの根本原因は Goランタイムのスタックアンワインド処理 と判明
- Netlinkライブラリ と非同期プリエンプションの絡みで顕在化
- 詳細な調査とデバッガ解析で コンパイラの競合状態 が明らかに
Cloudflareの大規模運用でGo arm64コンパイラバグを発見
- 毎秒 8,400万件のHTTPリクエスト を330都市のデータセンターで処理
- 極めて稀なバグ でも高頻度で顕在化する規模
- Magic TransitやMagic WAN向けの カーネル制御サービス で異常なpanicを検知
- エラーメッセージ「 traceback did not unwind completely」を観測、 スタック破損 の疑い
- 当初は 稀なスタックメモリ破損 と推測し、深追いせず
異常パターンの再発と調査再開
- 一時的に panic/recoverの利用停止 で致命的panicが消失
- しかし 1か月後に再発し、1日最大30件の致命的panic を観測
- 既存のパターンマッチングや推測が通用せず、 根本原因調査 を決断
2種類のバグパターンを特定
-
無効メモリアクセス時のクラッシュ
-
明示的な致命的エラー検出時のpanic
-
いずれも *(unwinder).next でスタックアンワインド時に発生
-
Goランタイムの スケジューラ構造体(g, m, p) の理解が調査の鍵
- g :goroutine
- m :カーネルスレッド(machine)
- p :物理実行コンテキスト(processor)
調査の行き詰まりと突破口
- スタックアンワインド自体は正常、だが 無効なスタック を処理している
- Go Netlinkライブラリ の古いバージョンがクラッシュ時に必ず関与
- ログ調査で 全てのセグメンテーションフォルトがNetlinkSocket.Receiveのプリエンプト時 に発生と判明
Goの非同期プリエンプションの影響
- Go 1.14以降、 非同期プリエンプション (async preemption)を導入
- sysmonスレッド が10ms以上実行中のgoroutineをSIGURGで強制プリエンプト
- signal handlerで スタック・プログラムカウンタを書き換え、asyncPreemptを模倣
根本原因の特定
- 2つの仮説
- Go Netlinkのunsafe.Pointer利用による未定義動作
- GoランタイムのバグでNetlinkSocket.Receiveでのみ顕在化
- コード監査では決定打が得られず
- プロダクション環境のcoredumpをデバッガで解析
- asyncPreempt2 で停止、 NetlinkSocket.Receive 呼び出し中にプリエンプト
- スタックアンワインド時に スケジューラ構造体mの不正参照 でセグメンテーションフォルト
まとめ
- Goのarm64コンパイラが生成するコードに競合状態が存在
- 非同期プリエンプションのタイミングとNetlinkライブラリの組み合わせ でスタック破損が発生
- Cloudflareの大規模運用が 極めて稀なバグを発見・報告 するきっかけに
- 問題の根本解決には Goランタイム・コンパイラの修正 が必要
補足
- 詳細なスタックトレースやメモリアドレスは割愛
- Goランタイムやプリエンプションの詳細は 公式ドキュメント を参照