OpenAIOpenAI News2026/06/30 0:00

Core dump epidemiology: fixing an 18-year-old bug

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

openaijamodel: gpt-5-mini-2025-08-07

コアダンプ疫学:18年越しのバグを修正

Key Points

  • 原因は二重だった
  • コアを集計して分離
  • libunwind競合を発見

Summary

OpenAI の Rockset サービスで発生した説明不能なクラッシュを、個別解析から「集団解析(疫学)」に切り替えて解決した事例。大量のコアダンプを自動で解析・分類して相関を調べた結果、原因は別々の二つの問題であることが判明した。

Key Points

  • 問題の症状
    • 関数が戻った直後に命令ポインタが無効になり SIGSEGV。戻り先アドレスが NULL になっているケースや %rsp が 8 バイトずれているケースが観察された。
  • 調査手法の転換
    • 個別のコア解析(医者モード)から、全コアを対象にした自動解析パイプライン(疫学モード)へ変更。
    • コアの先頭データからレジスタやフレーム情報を抽出し、return-to-null / misaligned-stack / other に自動ラベリングして集計した。
  • 発見された原因(2系統)
    • バグ1: 特定の Azure ホスト上のサイレントなハードウェア破損に起因する misaligned-stack(地理的・ノード限定、短期間で発生)。
    • バグ2: GNU libunwind の長年存在する競合(single-instruction race window)で、クラッシュが普通の戻り失敗に隠蔽されていた(広域に分布)。
  • 技術的知見
    • -fno-omit-frame-pointer と x86_64 の red zone を活用して、戻った直後のフレーム断片を再現できる。
    • 頻繁なシグナル配送(coarse_thread_cputime_clock)を使っているコードパスは、カーネルやシグナル周りの仮説を誘導するが、集団データで切り分けが可能。

実務的な推奨(エンジニア向け)

  • コアダンプ運用
    • 本番コアを自動で集め、並列でメタデータ抽出・ラベリングするパイプラインを用意する。
  • 分類と相関解析
    • クラッシュカテゴリごとに時間・ノード・リージョン・カーネル・バイナリ版で相関を取る。局所的ならハードウェア、分散的ならライブラリや共有コードを疑う。
  • デバッグ短縮のためのチェックリスト
    • -fno-omit-frame-pointer を有効にしてフレーム情報を確保。
    • red zone やシグナルハンドラによる情報保存の特性を理解して解析に使う。
    • 既存のオープンソースランタイム(例: libunwind)に古典的なレースや既知のバグがないか監査し、パッチやアップストリーム修正を適用する。
  • モニタと運用
    • ノード単位の時系列集計で異常ホストを速やかに隔離・再起動・交換する運用フローを持つ。

結論

少数ケースの深掘りだけでは「複数原因の混在」を見落とす。大量のコアダンプを自動解析してクラスタリングすることで、ハードウェア故障とライブラリ競合の両方を切り分け、短期間で修正に結びつけられた。エンジニアはコアの自動分類・相関解析を標準手順に組み込むべきである。

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

openaijamodel: gpt-5-mini-2025-08-07

コアダンプ疫学:18年前のバグを修正する

OpenAI のモデルやエージェントは、推論時(モデルがあなたの質問について考えているとき)に関連データを検索するために、スケーラブルなデータインフラにますます依存しています。これらのサービスの一部は C++ で書かれており、低レベルでシステムを制御できるためパフォーマンスを最大化し、メモリ使用量を最小化できます。その効率上の利点はスケールにとって重要ですが、C++ がメモリ安全性を欠くため、誤ったまたは存在しないメモリアドレスへの書き込みによりクラッシュが発生することがあります。

数か月前、ChatGPT のデータインフラの重要部分である Rockset サービス内から発生するいくつかのクラッシュを観測しました。これらは多くのデータプラグインや会話検索にとって重要なカスタムサービスです。各クラッシュでは、通常の C++ 関数が完了してから無効なアドレスに戻ろうとし、命令ポインタがコードを指さなくなったためカーネルがプログラムを停止させていました。場合によってはスタックフレーム内の戻り先アドレスのスロットが NULL でした。別の場合ではスタックポインタの CPU レジスタ自体が 8 バイトずれているように見え、実行中に %rsp がなんらかの形で減算されたかのようでした。どちらの場合もクラッシュは関数のリターン時に起きていました。

これらはアプリケーションコードの通常の失敗モードではありません。保存された戻り先アドレスだけに到達する迷い書き込み(stray write)が発生する可能性はありますが極めて稀です。また、インラインアセンブリ、setcontextlongjmp(我々はこれらを使っていません)を介さずに %rsp を 8 バイトずらすバグはさらに奇妙です。コンパイル済みコードは通常、そのレジスタに対する直接の調整を関数プロローグとエピローグのみで行うからです。われわれ(あるいは ChatGPT)が思いつくあらゆる仮説には強い反証があり、バグはほとんど不可能に思えました。

最初に我々が「たぶんこれだ」と思っていた一つの問題は、結果的に同時に発見された互いに無関係な二つのバグであることが判明しました。第一に、ある Azure ホスト上での静かなハードウェア破損 — CPU がただ正しく計算をしなかったケース。第二に、GNU libunwind にある 18 年前からの競合状態のバグで、広く使われるオープンソースライブラリに見落とされていたものです。本稿は、疫学者のように考え、クラッシュ全体の高品質なデータセットを構築することで、説明不能に見えるクラッシュをどのように特定・修正したかの物語です。

目次

  • First debugging attempt: carefully examining a few core dumps
  • Clues from the stack
  • Doctor or epidemiologist?
  • Cleaning the data
  • Bug #1: the bad host
  • Exception handling is a dynamic control transfer
  • Undoing one last assumption
  • Bug #2: the libunwind bug
  • Why the cores masked as ordinary bad returns
  • A single-instruction race window
  • Why did the libunwind bug appear now?
  • The power of a population-level diagnosis

First debugging attempt: carefully examining a few core dumps

まず、Rockset についてもう少し深掘りします。Rockset はクラウドネイティブな検索とリアルタイム分析のためのデータシステムで、OpenAI の多くの社内ユースケース(同期コネクタなど)で使われています(Rockset は 2024 年に OpenAI に買収されました)。ストリーミング更新はワークスペースのナレッジベースの最新インデックスを維持するために用いられ、ChatGPT が質問に答えたりアクションを実行する際に関連情報を検索できるようにします。Rockset の実行レイヤーは C++ で書かれています。C++ は CPU への低レベルアクセスを提供するためパフォーマンスと効率に優れますが、アプリケーションのバグが無効なメモリアクセスやセグフォルトを引き起こす可能性があります。

これらを追跡するために、我々は folly の致命的シグナルハンドラを使ってクラッシュ発生時にスタックトレースをログし、対応するコアダンプ(クラッシュ時のプログラム状態のスナップショット)を Azure Blob Storage にアップロードして後で解析できるようにしていました。Rockset のクエリ処理のリーフはすべてレプリケートされており、クラッシュがクライアントに与える影響を最小化します。それでも、各セグフォルトは信頼性と品質目標を満たすために修正が必要なバグに対応しています。

当初のアプローチは、これらのコアを従来のデバッグ問題のように扱うことでした。少数のコアダンプを注意深く調べ、仮説を立てて一つずつ潰していくやり方です。ほとんどのクラッシュは DocumentTree::updateDocument というメソッド内で発生していました。これらのクラッシュでは updateDocument が未知の関数 X を呼び出し、その間にスタックが破損し、X が実行中に戻り先が実行可能コードでないアドレスに戻ってしまったように見えました。場合によっては X の直前にポップされたフレームは有効に見えましたが、保存された戻りアドレスが NULL でした。別の場合ではスタックポインタ自体が間違っているように見えましたが、次の有効なフレームはまだ updateDocument のままでした。

スタックがいつ破損したかは分からず、探索空間は非常に大きくなりました。updateDocument は大きなメソッドで多くのインライン化が行われているため、X の候補は圧倒的でした。これは我々の C++ コードのバグですか?コンパイラやリンク時の問題?ランタイムライブラリのどれかの問題?シグナル配信やコンテキストスイッチに関する Linux カーネルのバグ?さらに珍しい何か?もし stray write なら、なぜ ASAN のステージング環境で検出されなかったのか?

アプリケーションレベルのログを使って問題の発生を特定しようとしましたが、スタック破損バグはログだけでは分類が難しく、ログに記録されたスタックトレース自体が破損または欠落していることが多く、偽陽性と偽陰性の両方を伴わないログクエリを構築できませんでした。さらにコアを手動でより多く調べて追加の例を見つけましたが、そのプロセスは労力がかかりすぎて信頼できるデータセットを作るには至りませんでした。

この時点で我々は(誤って)ハードウェアバグを除外していました。複数のリージョンと複数のハードウェアタイプでクラッシュを見たため、ソフトウェアだけが原因だと考えていたのです。数日間、我々は misaligned-%rsp の単一ケースに非常に深く入り込み、スタックとレジスタの内容からクラッシュ直前の履歴を再構築しました。いくつかの手がかりは得られましたが、すべてのクラッシュが同じ原因だという初期結論を手放さなかったために、そこから脱却できませんでした。

Clues from the stack

調査の転換点に入る前に、コアファイルからどのような情報を抽出していたかを説明することが重要です。Rockset は -fno-omit-frame-pointer でコンパイルされているため、アクティブなスタックフレームは常に %rbp を通じて辿ることができ、呼び出し元はフレームポインタの連結リストを形成します。Linux x86_64 上の AMD64 System V ABI では %rsp の下に 128 バイトのレッドゾーンが予約されています。その領域はユーザ空間コードが利用可能で、重要なことに、カーネルはシグナルを配信する際にそれを上書きしないことを ABI の契約として約束します。

このレッドゾーンは戻り後クラッシュのデバッグで中心的役割を果たしました。なぜなら、そこにはリターン前の情報の一部が保持されるからです。SIGSEGV がトリガーされると、folly の致命的シグナルハンドラはクラッシュしたスレッドのスタック上で実行されます。既にアクティブでない(関数が戻ったため)スタックフレームはシグナルハンドラにより上書きされますが、最後の 128 バイトだけは保護されます。だから我々は「X の直前にポップされたスタックフレームは有効に見えたが戻りアドレスは NULL だった」といったことが言えるのです。レッドゾーンは非アクティブフレームの一部、あるいは単にその一端を保存します。

ある misaligned-stack のクラッシュでは関与している関数が非常に小さかったため、実行中に %rsp がずれてしまい、その後さらに呼び出しが成功していることが観測できました。プログラムは最終的にアクティブな関数が戻ろうとしたときにのみクラッシュしました。それらのコードパスは例外、インラインアセンブリ、setcontextlongjmp を使用していなかったので、もし core が示すようにスタックポインタが本当にそのように変化しているなら、ユーザ空間コード内のもっともらしいバグでは説明がつきませんでした。これは我々をカーネルへと押しやりました。

Rockset は多くのプログラムよりも積極的にシグナルを使います。クエリ実行は多くの軽量タスクに分割されデータを交換します。これは高 QPS ワークロードを効率的に処理するために重要ですが、多数のクエリの仕事が同じスレッドプールへ多重化されるため、クエリ単位の CPU 会計が厄介になります。我々の解決策は coarse_thread_cputime_clock と呼んでいるもので、clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...) を各タスク境界でサンプリングできるほど安価に近似します。

timer_create API は CPU 時間の蓄積を含むいくつかの時間概念に基づく定期的なシグナル配信をスケジュールするために使えます。我々は数ミリ秒の CPU 時間ごとにシグナル(SIGUSR2)を配信するようにスケジュールし、その時点でシグナルハンドラがスレッドローカルの値を更新します。多くのタスクは実行中に粗いクロックの進みを見ないことがあっても、すべてのデルタを合計することでクエリの実際の CPU 時間の無偏推定量を得られます。シグナルを非常に頻繁に配信しているため、コンテキストスイッチやシグナル配信周りの稀なカーネルバグはもっともらしく見えました。我々はバグレポート、カーネルソース、Azure 特有のカーネルパッチを読み、ストレステストを行いましたが、関連しそうなものは見つかりませんでした。

その時点で我々は一歩下がって別のアプローチを試すことにしました。

Doctor or epidemiologist?

この種の問題をデバッグする方法は大きく二つあります。一つはある種の「医者」的アプローチ:一人の患者に集中して多数のテストを行い、詳細な証拠から単一事例を診断する方法。もう一つは「疫学者」的アプローチ:母集団全体を見て、単一事例では明らかにならないパターンがないかを問う方法です。

バグは特定リリースで発生し始めたのか?特定のハードウェア SKU(CPU とサーバーモデル)やリージョン、カーネルバージョンと相関しているか?一見一つの症候に見える中に複数の異なるクラスタが隠れていないか?我々はこれまで主に医者モードでした。重要な転換は、高品質な母集団データを集める必要があると決めたことでした。

Cleaning the data

以前の自動化試行が失敗したのは、ログのテキスト検索で問題のすべてのインスタンスを見つけようとしていたからです。コアダンプ自身にははるかに多くの情報が含まれているのに、手動で見るとスケールしません。そこで我々はコアダンプを自動解析できるパイプラインを構築する努力に投資することにしました。

ChatGPT にスクリプトを書かせ、各コアファイルのプレフィックスをダウンロードしてレジスタを抽出し、既知の偽陽性をログでフィルタし、クラッシュを return-to-nullmisaligned-stack、または other に自動ラベル付けするようにしました。そしてそのスクリプトを過去 1 年間の本番 Rockset コアダンプのすべてに並列で実行しました。これが転換点でした。クリーンなデータセットを手に入れると相関関係が即座に現れました。

我々が一つの奇妙なバグだと扱っていたものは、実際には別々のクラッシュ母集団が二つ存在していました。return-to-null のコアは多くのクラスタと地理的リージョンに広がっていました。頻度は最近増加していましたが、明確な開始日や明瞭なインフラ境界はありませんでした。一方で misaligned-stack のクラッシュは全く異なって見えました。これらはすべて一つのリージョンから来ており、明確な開始日があり、長期間稼働しているノードでは決して発生していませんでした。複数の Azure VM(クラウド上でホストされる仮想マシン)をまたいでいたにもかかわらず、このパターンはどの VM が割り当てられても問題を起こす一つの物理マシンのハードウェア不良のように見えました。

この瞬間、我々は二つのバグを心理的に混同していたことに気づきました。両方のバグから来る反例を混ぜていたために単一の一貫した説明を見つけられなかったのです。

Bug #1: the bad host

Armed with a clean list of Kubernetes nodes and timestamps, we were a