ハクソク

世界を動かす技術を、日本語で。

カーネルのバグは平均で2年間隠れ続ける。中には20年隠れるものもある

概要

  • Linuxカーネルのバグは平均2.1年発見されずに残存
  • CAN busSCTPなど一部サブシステムでバグ寿命が特に長い
  • バグ検出は年々高速化しているが、古いバグのバックログが存在
  • 自己修正(バグ導入者自身が修正)も多く、コード所有意識が影響
  • 長寿命バグは参照カウントミスレースコンディションなどが多い

Linuxカーネルのバグ寿命と発見の傾向

  • Fixes:タグ付きのバグ修正ペアを125,183件分析
  • 有効な記録は123,696件(寿命0~27年)
  • 平均バグ寿命は2.1年、最長は20.7年(ethtoolのバッファオーバーフロー)
  • 2010年では1年以内発見率0%、2022年には**69%**に向上
  • VulnBERTによるバグ検出精度は92.2%、偽陽性率は1.2%(CodeBERTは48%)

バグ発見までの時間とその変化

  • 2025年のみのデータでは平均寿命2.8年、全履歴では2.1年に短縮
  • 5年以上発見されないバグは全体の13.5%(全履歴)、19.4%(2025年のみ)
  • バグ発見までの速度は年々向上傾向
    • 2010年導入バグ:平均寿命9.9年、1年以内発見0%
    • 2022年導入バグ:平均寿命0.8年、1年以内発見69%
  • SyzkallerKASANなどのサニタイザ、静的解析、レビュー体制強化が寄与

バグの発見と修正のパターン

  • 新規バグは早期発見、古いバグはバックログとして残存
    • 2024-2025年修正バグの60%が直近2年で導入
    • 18%が5-10年前、6.5%が10年以上前に導入
  • カーネルのFixes:タグを活用したバグ追跡手法
    • git logによるFixes:タグ抽出
    • 修正・導入コミットの日付取得
    • サブシステム分類・バグ種別分類
    • バグ寿命計算

サブシステムごとのバグ寿命

  • CAN bus(4.2年)、SCTP(4.0年)、USB(3.5年)などが長寿命
  • BPF(1.1年)、GPU(1.4年)は短寿命
  • ニッチなプロトコルテストカバレッジ不足が長寿命の要因
  • GPUBPFは専用のファジングインフラ整備により短期間で発見

バグ種別ごとの寿命

  • レースコンディションは平均5.1年と最も発見が遅い
  • 整数オーバーフロー(3.9年)、use-after-free(3.2年)、メモリリーク(3.1年)なども長め
  • null参照デッドロックは比較的早期に発見

長寿命バグの特徴と事例

  • 参照カウント漏れNULLチェック漏れ整数オーバーフロー状態機械のレースが典型
  • 19年間修正されなかったnetfilterのrefcountリーク事例
    • テスト条件が特殊で再現困難
    • メモリリークとして長期間放置
  • 不完全な修正も多く、"未定義動作"や"再現できなかった"旨のコミットは要注意

なぜ一部バグが長期間隠れるのか

  • ファジングカバレッジ不足
    • Syzkallerはシステムコールには強いが状態遷移が必要なプロトコルに弱い
  • トリガー条件が複雑
    • 特定のパケットシーケンスや同時実行、メモリ圧迫などが必要
  • 古いコードのレビュー不足
    • "安定"している部分は開発者の目が届きにくい

長寿命バグのアナトミー

  • 参照カウントミス
    • 例:kref_get後にエラーパスでkref_putし忘れ
  • NULLチェックタイミングの誤り
    • 取得直後にデリファレンスし、その後でNULLチェック
  • 整数オーバーフロー
    • ユーザー入力からのサイズ計算ミス
  • 状態機械のレース
    • ロック解除直後の状態変化タイミングの競合

今後の課題と展望

  • 検出ツールのさらなる精度向上
    • VulnBERTのようなAI活用による検出率向上
  • テストカバレッジの拡大
    • ニッチサブシステムや複雑な状態遷移への対応
  • コード所有意識とレビュー体制の強化
    • バグ導入者自身による修正の促進
  • 不完全修正の早期発見
    • "未定義動作"や"再現できなかった"コミットへの追加レビュー

Hackerたちの意見

Firefoxのバグって、そんなに長く放置されるんだね。
俺が好きなFirefoxのバグの一つは、詳細はあんまり覚えてないけど、こんな感じだったかな。「この設定ファイルを使ってるとクラッシュする。」もうちょっと複雑だったけど、結局は何らかのクラッシュ。20年後、やっとそのバグが閉じられたんだ。見て、彼らは設定パーサーをRustで書き直して、今はこれが修正されたんだって。クールだけど、俺が覚えてるのはそこじゃない。いつも考えるのは、バグがオープンされた直後に「ごめん、バグを直す前に自分たちのプログラミング言語を書かなきゃいけないんだ。心配しないで、戻ってくるから、ちょっと時間がかかるけど。」って返答することを想像してみて。誰も信じないよね。でも、実際に起こったことなんだ。
どんなソフトウェアにも長生きするバグがあるよ。存在する限りバグがないものはないから、ほぼ避けられない。Windowsのバグトラッカー見たことある?アンチFirefoxの連中は本当にそれを攻撃しようとしてる。この記事のポイントはLinuxを批判することじゃなくて、もっと生産的なコードレビューにつながる分析なんだ。
もしかしたら俺のシステムだけかもしれないけど、時間がハイパーリンクみたいに見えるのに、なぜかリンクになってないんだよね。特に、コミットハッシュがカーネルリポジトリの実際のコミットにリンクしてないのが残念。
CSSでホバーすると色が#79635cになるタグなんだよね。確かに変なスタイル選択だけど、意味的にはリンクじゃないんだよ。
「Rustで書き直せ」ってコメントがスレッドを占拠する前に言っとくけど、ここで説明されてるバグのクラス(高並行状態機械の論理エラーや不正確なハードウェアの仮定)は、必ずしも借用チェッカーで捕まるわけじゃないよ。Rustはメモリ安全性に関しては素晴らしいけど、ネットワークカードの仕様を誤解したり、DMAとやり取りする不安全なロジックでレースコンディションを書くのを防いではくれない。とはいえ、メモリ安全性の問題を占める70%のバグを排除できれば、こういう深い論理バグを見つけるためのSNR比は劇的に改善されるはず。セグフォルトを追跡するのに時間をかけすぎて、微妙な破損バグを見逃しちゃうんだよね。
> ここで説明されてるバグのクラス(高並行状態機械の論理エラーや不正確なハードウェアの仮定)は、必ずしも借用チェッカーで捕まるわけじゃないよ。Rustはメモリ安全性に関しては素晴らしいけど、ネットワークカードの仕様を誤解したり、DMAとやり取りする不安全なロジックでレースコンディションを書くのを防いではくれない。Rustはメモリ安全性だけじゃなくて、代数データ型やRAIIなどもあって、こういう馬鹿げた論理バグを捕まえるのに大いに役立つよ。
> ここで説明されてるバグのクラス(高並行状態機械の論理エラーや不正確なハードウェアの仮定)は、必ずしも借用チェッカーで捕まるわけじゃないよ。あなたが説明しているバグは確かにRustの借用チェッカーでは直接対処されないものだけど、記事はあなたのコメントが示唆するよりも広い範囲をカバーしていると思うよ。例えば、記事のかなりの部分(ほとんど?)は、収集したデータを分析することに費やされていて、バグをサブシステムごとにグループ化しているんだ。サブシステム バグ数 平均寿命 drivers/can 446 4.2年 networking/sctp 279 4.0年 networking/ipv4 1,661 3.6年 usb 2,505 3.5年 tty 1,033 3.5年 netfilter 1,181 2.9年 networking 6,079 2.9年 memory 2,459 1.8年 gpu 5,212 1.4年 bpf 959 1.1年 またはバグのタイプごとに: バグタイプ 数 平均寿命 中央値 race-condition 1,188 5.1年 2.6年 integer-overflow 298 3.9年 2.2年 use-after-free 2,963 3.2年 1.4年 memory-leak 2,846 3.1年 1.4年 buffer-overflow 399 3.1年 1.5年 refcount 2,209 2.8年 1.3年 null-deref 4,931 2.2年 0.7年 deadlock 1,683 2.2年 0.8年 さらに、10年以上続くバグの一般的なパターンを説明するセクションには、以下のように書かれている: > 1. 参照カウントエラー > 2. デリファレンス後のNULLチェックの欠如 > 3. サイズ計算における整数オーバーフロー > 4. 状態機械におけるレースコンディション これらはすべて、あなたのコメントに書かれているよりも広い範囲をカバーしているよ。さらに、19年前のバグのケーススタディは、高並行状態機械やハードウェアの仮定とは関係ない参照カウントエラーなんだ。
これは、批判者にも支持者にもあまり評価されていない部分だと思う。Rustを使うと、普通のメモリ安全性について深く考えずに「リスク」をもっと取れるんだ。コードの構造を整えて、ロジックが明らかに正しいようにすることを優先してる。C++では、メモリ安全性を超シンプルに保つことが最重要で、例えばstd::string_viewをどこかに保存することはほとんどないよ。Rustでは、好きなところに&strを置くだけ。間違えたらコンパイル時に分かるしね。
同時状態機械の例は、ロックのエラーみたいに見える?その間に変更されないという前提なら、ロックは保持し続けるべきじゃない?その場合、Rustのロックが役立つよ。データを埋め込むことができるから、保持されていないと触ることすらできないんだ。
Rustは多くのバグを防げるし、状態機械の保証もモデル化できる。全部をRustで書き直すのは非常にコストがかかるから、すぐには実現しないだろうね。
他のトップレベルのコメントではRustについて言及されていないし、TFAもRustやメモリ安全性のようなトピックには触れていない。ただのバグの話だよ。Rustのファントム熱心さは残念ながら本物だね。[1] ああ、でもRIRのコメントを投稿前に却下することの冷却効果があるよね…
あなたがそんなに恐れているRustの人たちに備えて、物語を先取りしてコントロールしようとするのが面白いね。これは非合理的な恐れなのかな?政治的な議論で使われる手法を思い出すよ。
空気と戦ってるだけだよ。
バグの70%がメモリ安全性の問題だとは思わないな。私の経験では、もっと5%に近いよ。
> DMAと相互作用するunsafeなロジックのレースコンディションについて もしメモリ安全なコードを書いても、DMA転送を誤ってプログラムしたり、PCIeデバイスでバグを引き起こしたりすると、ハードウェアが無効なデータを本来別のものが入るべき領域にぶちまけることで、メモリ安全性の問題を引き起こす可能性があるってことは覚えておく価値があるね。
「データセットの制限」に関するセクションには、研究が「Fixes:タグが付いたバグのみをキャッチしている(修正コミットの約28%)」と書いてある。修正コミットの「28%」だけから平均が2年だと推測するのは、かなりの外挿だと思う。
明らかかもしれないけど、ここには確実にデータのバイアスがあるよ。避けられないんだ。例えば、多くのバグは検出されないけど、コードを書き直すときに取り除かれる。だから、リファクタリングが頻繁に行われるコードは、修正されたバグの年齢が低くなる。よく使われるコンポーネントやサブシステムは、バグを早く検出することができる。一部のサブシステムはその性質上、バグをより許容できるけど、必要に応じてもっと正確である必要があるものもある(例えばbpfみたいに)。
これが言ってるカーネルはたぶんLinuxのことだよね。Windowsにも似たような現象あるのかな?
grsecurityプロジェクトは多くのセキュリティバグを修正したけど、パッチセットを販売して利益を上げているから、貢献はしていない。彼らが見つけたバグが6~7年後に再発見されるのは珍しくないよ。https://xcancel.com/spendergrsec
> パッチセットを販売して利益を上げているから Profiting from selling their patchsetは全体の話じゃないよ。grsecは長い間公開されていて無料だったし、カーネルがそれを採用するのを妨げる多くの要因があったんだ。
でも、パッチセットは元のコードと同じライセンスを使うべきじゃない?
ステートマシンのレースパターンは、カーネルの仕事を超えて共鳴するよ。アプリケーションコードにも似たようなバグが隠れているのを何年も見てきた。特定のユーザーアクションのシーケンスでしか発生しないトランザクションステートのエッジケースとかね。中央値の寿命が面白いよ。レースコンディションは5.1年、ヌルデリファレンスは2.2年ってのは直感的に理解できる。前者は特定のタイミングが必要だけど、後者はコードパスに入った瞬間に明らかにクラッシュするから。珍しい条件で発生するバグほど長生きするんだよね。
iOS 26のCore Audioバグ(CVE-2025-31200)は、二つの異なる配列を同期させることに関するもので、ユーザーから来るかもしれない次元情報を信頼してしまったことによる仮定ミスがあるんだ。https://youtu.be/nTO3TRBW00E
深いバグ、特にカーネル内のバグは、何年も気づかれないことがあるって分析をよく見るよ。時には数十年も。最初は怖い感じがするけど、考えれば考えるほど…予測可能に思えてくる。私が役立つと思うメンタルモデルは、ユーザーが表面的なバグを発見するってこと。深いバグは稀な組み合わせでしか現れない。あるバグが現れるには新しいコンテキストが必要なんだ。いくつかのパターンを観察してきたけど、未定義の動作に関連するバグは永久に隠れている。論理エラーは珍しいハードウェアやタイミング条件より重要じゃない。悪用される前から、セキュリティの欠陥は頻繁に存在するんだ。これについて他の人はどう思ってるのかな?持続的なバグは安定性を示すのか、それとも失敗を示すのか?通常、どんなことが発見につながるんだろう?「十分にテストされた」コードをどの程度信頼してる?
> 「十分にテストされた」コードをどの程度信頼してる? 信じてないよ。だから、私はコンパートメンタリゼーションを通じてセキュリティを提供するQubes OSを使ってるんだ。
著者の意図は、バグが「隠れている」年数をカーネルのコードベースの品質やメンテナのパフォーマンスの指標として使うことなのかな?「どんどん速くなってる」って記事のどこかに書いてあったから、気になって。個人的には、バグが何年も隠れているのは、そのバグが重大性が低いか優先度が低いことを示していて、全体的な品質がすごく良いってことかもしれないと思う。もしその時間が既知のバグを再現して解決するのにかかる時間を表しているなら、カーネルの中で「バグが隠れている」とは言わないけどね。
> 個人的には、バグが何年も隠れているのは、そのバグが重大性が低いか優先度が低いことを示していて、全体的な品質がすごく良いってことかもしれない。 それはそうは思えないね。バグがテストされたコードにないか、あまり頻繁に遭遇しないだけかもしれない。まだ非常に深刻なバグである可能性もある。長生きするバグの問題は、誰かがそれを長い間利用している可能性があることだよ。
> 個人的には、バグが何年も隠れているのは、そのバグが重大性が低いか優先度が低いことを示していて、全体的な品質がすごく良いってことかもしれない。 それはあまり正しくないよ。非常に深刻なバグが何年も、さらには数十年も隠れていることは多いから。Heartbleedが思い浮かぶね。こういうバグが長く隠れている理由は、パニックを引き起こさないことが多いからで、見つけるのが本当に難しいんだ。例えば、use after freeのバグは本当に危険だよ。でも、ほとんどのコードでは、use after freeが発生しても危険なことは起こらないっていうのがかなり安全な予測だね。特に、ポインタがfreeの直後に使われて、すぐに死んじゃう場合はね。多くの場合、誤った読み書きが何かを壊すことはないし、レースコンディションの問題も同じことが言える(これらは最も長生きするバグの一部)。多くの場合、レースコンディションがあることに気づかないことが多いのは、ロックの競合が低いからで、レースが露呈しないからなんだ。そして、たとえ露呈しても、再現するのがすごく難しいことがある。なぜなら、レースが同じ方法で2回行われる可能性は低いから。
面白いね!少し前にChromeとFirefoxのContent Security Policyバグについて似たような分析をしたことがあって、その時のバグから報告までの平均時間はそれぞれ約3年と1年だったんだ。 https://www.usenix.org/conference/usenixsecurity23/presentat... でも、うちのバグデータセットはかなり小さかったんだよね。残念ながら、すべてのバグの発生を特定しなきゃいけなかったから。Linuxプロジェクトがちゃんと「Fixes: 」タグを使ってるのを見るのはいいね。