1 ポイント 投稿者 GN⁺ 3 시간 전 | 1件のコメント | WhatsAppで共有
  • 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 を使用可能
  • オペレーティングシステムは 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 で、ヒープメタデータ整合性チェック地点でクラッシュする
  • 呼び出しフローは、opendircalloc を呼び出し、Rust 標準ライブラリのディレクトリ走査と ripgrep の ignore::walk ワーカーへ続く
    • get_meta__malloc_allzeropcallocopendir
    • std::fs::read_dirignore::walk::Work::read_dirignore::walk::Worker::run
  • 解析資料としてコアダンプ該当する rg バイナリが添付されている

期待される動作と現在の状態

  • 期待される動作は、大規模・高並行性の検索でも セグメンテーションエラーなしに実行されること
  • 提供された内容には、原因の確定、修正案、レビュー結果、または最終的な解決状況は含まれていない

1件のコメント

 
GN⁺ 3 시간 전
Hacker News のコメント
  • カーネルパッチに面白い一節がある: https://lore.kernel.org/all/CALCETrXbj__SFQMzPZhES5y6-sh4np-...

    ripgrep の面白いバグ報告と、誠実ではあるがかなりひどい AI 生成分析を見た
    これは https://github.com/dfoxfranke/ripgrep-3494-analysis を指しているが、自分も人間が書いたにしては長すぎると思った。しかもこのスレッドはまさに今日投稿されたようだ

    • 読むのが苦痛だったし、あの冗長な文章のどこにも、lore.kernel で実際の人間が見つけたのと同じコードや領域を指摘している箇所が見当たらない。Claude に詳しい人に聞きたいのだが、実際の原因を特定した部分はあるのか?
    • 2000年ごろまでは公共の場で携帯電話を使う人は気取った嫌なやつのように扱われていて、今の AI もそれに近い不快な拒否の段階を通っている
      2年前ならこういう分析はコミュニティに時間を気前よく寄付した結果だと受け取られただろうが、今では出所とトークンコストがたった0.06ドルだと分かっているので読む気になれない。これから起こることを予感させる点も一因だ
      数年後には、人間が自力でバグを掘り下げるのは最後の手段になり、別の AI エージェントが報告書を読んで修正を検証するようになるだろう。コンパイラ生成のアセンブリや公共の場での携帯電話利用を無視するように、嘲笑の段階から無関心の段階へ移るのだと思う
  • musl のデフォルトアロケータを便利だからそのまま使うのは理解できるが、速度そのものが目的のアプリケーションでより高速なアロケータに替えていないのは不思議だ
    mallocng はマルチスレッド競合に弱い。普段は I/O ボトルネックだったアプリケーションも musl でビルドするとスレッド 8 本だけで malloc ボトルネックが発生し、mimalloc に替えると性能が20倍向上して glibc のデフォルト構成にかなり近づき、glibc+mimalloc よりは少し遅かった
    実際に興味深い問題ではあるが、そもそもこんな形で表面化すべきではなかった

    • ripgrep は 64ビット musl でビルドする際、実際にjemalloc をグローバルアロケータとして指定している: https://github.com/BurntSushi/ripgrep/blob/435f59fc4b43af3ab...
    • セグメンテーション違反のスタックを見ると、割り当ては musl libc の opendir で発生している。Rust のアロケータ差し替え方式はプロセス全体のアロケータを置き換えるのではなく、Rust コードが呼ぶアロケータだけを置き換える
      ただしグローバルロックを使うアロケータを通る必要があるなら、ripgrep は libc の opendir を避けたほうがよさそうだ
    • これはカーネルバグだ。libc アロケータが大した理由もなくひどいという点には同意するが、この問題は mimalloc や glibc を含む他のアプリケーションコードでも同程度の確率で起こり得るように見える
    • ほとんどのプログラムは割り当てを再利用して高速化する。そもそも割り当てをしなければ高速なアロケータも不要で、ripgrep の作業自体も頻繁な割り当てを本質的には必要としていない
    • むしろ musl の mallocng が持つハードニング機能のおかげでカーネルバグを発見できた。そうでなければ数か月にわたって静かにメモリを破壊し、気づかれなかったかもしれない
  • HPC クラスタで大規模クラスタファイルシステム相手に ripgrep を走らせているなら、今すぐ止めてワークフローを設計し直すべきだ。こうした作業は大量の小さな I/O を発生させるが、これは大規模クラスタファイルシステムのアキレス腱
    クラスタの高帯域メモリ階層で処理すべき仕事をファイルシステムのメタデータ階層に押しつけることになり、数人のユーザーが同時に実行するだけで高帯域ファイルシステム全体が麻痺しかねない

    • これは HPC クラスタではなく、自分のワークステーションで使っているbtrfsにすぎない
    • 最近 GitHub が不安定になっている根本原因も似たようなものではないかと思っていた。AI 利用によって増幅された何十億もの小さなファイル操作が突然発生しており、オブジェクトグラフは本質的に断片化しているので、1ページを先読みして通常の Git 操作がそのページ内のオブジェクトだけを触るようにするのも難しい
      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/
    • 「オーバーフローは依然としてオーバーフローし、use-after-free は依然として use-after-free であり、musl マスク競合は依然として競合する」という一節はコンピュータ詩みたいで笑ってしまう
      結論としてはおおむねLinux 7.0 と musl 1.2.5 の組み合わせに問題があるようだが、再現は依然として同じ物理 Threadripper CPU に強い負荷をかけたときに断続的に成功するだけで、ハードウェア問題を排除できていない
    • AI 生成バグ報告を読むのはつらい
    • 追跡自体は素晴らしいが、説明は意味をなしていない。追加の TLB フラッシュがエラーになることはなく、CPU は好きなときにいつでもフラッシュできる。実際のエラーは、存在してはいけないときにzero-page PTEが存在していたことにあるようだ
      不自然なタイミングで CPU がマイグレーションして発生した複雑な競合状態か、ページテーブル削除経路が誤った PTE を一時的に露出させたバグに見える。zero page の PFN が 0 だとも思わない
      推測するに、直接のページテーブル削除処理の中で、CPU が上位ページング構造のキャッシュ済みエントリ経由で、すでに解放・再利用されたテーブルを読めるようになっていた可能性がある。以前こういう問題をデバッグしたことがあるが、本当にひどかった
    • 典型的な冗長なLLM の残滓分析だ。分析自体は正しいのかもしれないが、詳しく読むのがつらく、人間が同じ分析をしたなら分量は5分の1になっていただろう
  • なぜ他の libc ではなくmusl libc でだけバグが発生するのか?

    • 単なる偶然で、1台のマシンでしか起きていない可能性もある
    • musl のアロケータは、新たにページフォルトした単一ページをそのままアプリケーションに露出するためだろう。他のアロケータは通常、複数ページをまとめて事前確保するので、競合状態の時間窓がより狭くなる
  • 普段なら musl のスレッドスタックサイズを疑うところだが、カーネルバグだと確認されたのか気になる