OpenAIOpenAI NewsJun 30, 2026, 12:00 AM

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

A condensed section focused on the key takeaways first.

Original Post

Quick Digest

Summary

A condensed section focused on the key takeaways first.

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

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

Key Points

  • Two distinct bugs: bad Azure host and libunwind race
  • Automated core-dump pipeline revealed population patterns
  • Decommissioned hardware and patched libunwind

Summary

A set of mysterious post-return crashes in Rockset (C++) were resolved by shifting from case-by-case debugging to a population-level analysis of all core dumps. The investigation revealed two distinct causes that had been conflated: silent hardware corruption on a single Azure host and an 18-year-old race condition in GNU libunwind. The team built an automated core-processing pipeline to classify and correlate crashes, which enabled precise isolation and remediation.

Key Points

  • Problem symptoms: functions returning to bogus addresses, NULL saved return addresses, and occasional %rsp misalignment observed in cores.
  • Initial local debugging (doctor mode) was misleading because multiple independent failure modes were mixed together.
  • Built an automated pipeline to download prefixes of cores, extract registers/frames, and label crashes (return-to-null, misaligned-stack, other) at scale.
  • Population analysis (epidemiology mode) exposed two clusters: widespread return-to-null crashes vs. region-localized misaligned-stack crashes tied to a single failing physical host.
  • Root causes:
    • Bug #1: silent CPU/hardware corruption on a single Azure host producing misaligned-stack crashes that affected whichever VM scheduled there.
    • Bug #2: an 18-year-old race in GNU libunwind that could mask as ordinary bad returns and triggered under a narrow single-instruction race window.
  • Practical diagnostic techniques used: red-zone inspection (128-byte ABI red zone), frame-pointer chains (-fno-omit-frame-pointer), bulk core labeling, correlating by node/region/timestamp, and filtering log false positives.
  • Fixes and mitigations: decommissioned the faulty host, patched or updated libunwind to remove the race, and added an automated core-analysis pipeline and better crash classification to prevent future conflation.

Practical takeaways for engineers

  • When crashes look impossible, gather population-level data before deep single-case forensics; distinct root causes are often mixed.
  • Invest early in automated core processing (even core prefixes) to extract registers/frames and produce reliable labels at scale.
  • Use ABI guarantees (red zone) and frame-pointers to recover information from partially clobbered stacks.
  • Correlate crashes by physical host, VM lifetime, region, kernel, and timestamps to spot hardware- vs. software-patterns.
  • Audit commonly used low-level libraries (like libunwind); long-lived open-source code can contain subtle race windows that surface only under scale.

Outcome

The combined approach led to concrete remediation: removal of the faulty Azure host, fixing or upgrading libunwind, and shipping instrumentation and an automated pipeline to detect and triage similar crash populations faster.

Full Translation

Translations

A translation section that keeps the flow of the original article.

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

Core dump epidemiology: fixing an 18-year-old bug | OpenAI News | DocsDigest