- Ripgrep 15.2.0 の x86_64-unknown-linux-musl バイナリが、大規模なファイルツリーを高い並行性で検索する際に、断続的に
SIGSEGVとともに終了する - クラッシュは
opendirが呼び出したcalloc内部で発生し、musl mallocng のヒープメタデータ整合性チェック地点がスタックトレースの最上位に現れる - 再現環境は約 20GiB・180万ファイルで構成されたツリーで、存在しない文字列を
rgで繰り返し検索する - 24コアシステムで、検索ツリーがカーネルのブロックキャッシュに収まるだけの RAM を確保すると、通常 約1分以内に問題が発生する
- OpenAI Codex に含まれる
rgだけでなく、公式リリースとバイト単位で同一のバイナリでも独立して再現され、Codex の依存関係とは無関係な問題であることが確認された
発生環境
- 使用バージョンは ripgrep 15.2.0 rev
e89fff8で、+pcre2機能を含む- コンパイル時 SIMD:
+SSE2,-SSSE3,-AVX2 - 実行時 SIMD:
+SSE2,+SSSE3,+AVX2 - PCRE2 10.45 と JIT を使用可能
- コンパイル時 SIMD:
- オペレーティングシステムは OpenSUSE Tumbleweed Linux x86_64
- 最初に発見された OpenAI Codex 同梱の
rgは、公式 x86_64-unknown-linux-musl リリース とバイト単位で同一 - Codex とは別に公式バイナリでも再現され、解析用バイナリは次のコマンドでデバッグシンボルを含めてビルドした
CROSS_CONTAINER_ENGINE=podman CARGO_PROFILE_RELEASE_DEBUG=true ~/.cargo/bin/cross build --release --target x86_64-unknown-linux-musl
再現手順
- generate_repro_tree.py は、もともと問題が発生したリポジトリの統計を模倣したランダムなファイルツリーを生成する
- このプログラムは LLM で作成された
- 生成結果は約 20GiB、180万ファイル規模
- 生成されたツリーのルートで、存在しない任意の文字列を繰り返し検索する
while true; do rg tnoheueunotshisnthukoethnsueothnsiuothonesuioseuinth; done
- 再現には十分に大きな検索ツリーが必須であると観察されている
- 24コアシステムで、ツリー全体がカーネルのブロックキャッシュに収まるだけの空き RAM がある場合、通常は約1分後にクラッシュする
クラッシュ地点
- 実際の結果は コアダンプを残す
SIGSEGV - スタックトレース最上位は musl mallocng の
get_metaで、ヒープメタデータ整合性チェック地点でクラッシュする - 呼び出しフローは、
opendirがcallocを呼び出し、Rust 標準ライブラリのディレクトリ走査と ripgrep のignore::walkワーカーへ続くget_meta→__malloc_allzerop→calloc→opendirstd::fs::read_dir→ignore::walk::Work::read_dir→ignore::walk::Worker::run
- 解析資料としてコアダンプと該当する rg バイナリが添付されている
期待される動作と現在の状態
- 期待される動作は、大規模・高並行性の検索でも セグメンテーションエラーなしに実行されること
- 提供された内容には、原因の確定、修正案、レビュー結果、または最終的な解決状況は含まれていない
1件のコメント
Hacker News のコメント
カーネルパッチに面白い一節がある: https://lore.kernel.org/all/CALCETrXbj__SFQMzPZhES5y6-sh4np-...
2年前ならこういう分析はコミュニティに時間を気前よく寄付した結果だと受け取られただろうが、今では出所とトークンコストがたった0.06ドルだと分かっているので読む気になれない。これから起こることを予感させる点も一因だ
数年後には、人間が自力でバグを掘り下げるのは最後の手段になり、別の AI エージェントが報告書を読んで修正を検証するようになるだろう。コンパイラ生成のアセンブリや公共の場での携帯電話利用を無視するように、嘲笑の段階から無関心の段階へ移るのだと思う
musl のデフォルトアロケータを便利だからそのまま使うのは理解できるが、速度そのものが目的のアプリケーションでより高速なアロケータに替えていないのは不思議だ
mallocng はマルチスレッド競合に弱い。普段は I/O ボトルネックだったアプリケーションも musl でビルドするとスレッド 8 本だけで
mallocボトルネックが発生し、mimalloc に替えると性能が20倍向上して glibc のデフォルト構成にかなり近づき、glibc+mimalloc よりは少し遅かった実際に興味深い問題ではあるが、そもそもこんな形で表面化すべきではなかった
opendirで発生している。Rust のアロケータ差し替え方式はプロセス全体のアロケータを置き換えるのではなく、Rust コードが呼ぶアロケータだけを置き換えるただしグローバルロックを使うアロケータを通る必要があるなら、ripgrep は libc の
opendirを避けたほうがよさそうだHPC クラスタで大規模クラスタファイルシステム相手に ripgrep を走らせているなら、今すぐ止めてワークフローを設計し直すべきだ。こうした作業は大量の小さな I/O を発生させるが、これは大規模クラスタファイルシステムのアキレス腱だ
クラスタの高帯域メモリ階層で処理すべき仕事をファイルシステムのメタデータ階層に押しつけることになり、数人のユーザーが同時に実行するだけで高帯域ファイルシステム全体が麻痺しかねない
https://isolveproblems.substack.com/p/how-microsoft-vaporize... の内容が少しでも正しいなら、Azure のファイルシステム抽象化で最適化されていない経路が1つあるだけで、利用急増が巨大な障害半径に広がり得る
カーネルバグ分析へ直接リンクしたほうがよいかもしれない: https://github.com/dfoxfranke/ripgrep-3494-analysis
Headline段落で断念した「新たにページフォルトした匿名ページにスレッドが保存した値が、約10命令後の同じスレッドによる再読み込みで消え、関数実行中にページの backing が置き換わる」「フォルト時点で pagemap を読むと backing はカーネルの zero page である」「メカニズムは VMA ごとのロックの匿名フォルト高速経路と同時
munmapの TLB shootdown の相互作用へ localize される」といった文が続くページの
backingとは何か、freshly-faultedや「約10命令後」が何を意味するのか、メカニズムがどう「localize」されるのかも分かりにくい。技術説明というより単語をつなぎ合わせた文章のように見えるちゃんとした技術文書の例はこちら: https://yifan.lu/2019/01/11/the-first-f00d-exploit/
結論としてはおおむねLinux 7.0 と musl 1.2.5 の組み合わせに問題があるようだが、再現は依然として同じ物理 Threadripper CPU に強い負荷をかけたときに断続的に成功するだけで、ハードウェア問題を排除できていない
不自然なタイミングで CPU がマイグレーションして発生した複雑な競合状態か、ページテーブル削除経路が誤った PTE を一時的に露出させたバグに見える。zero page の PFN が 0 だとも思わない
推測するに、直接のページテーブル削除処理の中で、CPU が上位ページング構造のキャッシュ済みエントリ経由で、すでに解放・再利用されたテーブルを読めるようになっていた可能性がある。以前こういう問題をデバッグしたことがあるが、本当にひどかった
なぜ他の libc ではなくmusl libc でだけバグが発生するのか?
普段なら musl のスレッドスタックサイズを疑うところだが、カーネルバグだと確認されたのか気になる