著者: コーリー・ミニヤード

信頼性とLinux

前回の投稿では、安全性が重要なソフトウェアにおける信頼性問題の重大性についてお話ししました。今回は、Linuxのバグに焦点を当てます。

私はMontaVistaのカーネルアーキテクトとして働いており、これまでカーネルの数多くの難解なバグに取り組んできました。簡単なバグもいくつか手がけましたが、それらはほとんどが他の人が担当しています。他の人が作業を進められなくなった後に、私が担当することになります。これらのバグのいくつかを例に挙げ、現在の解析技術ではカーネルがセーフティクリティカルなシステムに適さないと考える理由を説明したいと思います。

記憶を踏みにじる者

最初にお話しするバグは、メモリトランプラーです。これはカーネル内で私が実際に目にした唯一のメモリトランプラーです。私の経験と観察では、非常に稀です。静的解析やレビューで簡単に発見できます。安全性の観点からは、特に問題にはなりません。

この状況では、ページ テーブルの一部 (struct page) がランダムに上書きされていました。その部分は常時ページ テーブル内に存在していたため、少なくともある程度の一貫性は保たれていました。そこでカーネルを修正し、ページ テーブルを読み取り専用にしました。次に、ページ テーブルへの書き込みに関連するすべてのコードを追加し、特定のページのみが書き込み時にのみ書き込み可能になるようにしました。この非常に難しい修正とレビューおよびテストを行った後、パッチをお客様に送信しました。お客様はこれらのパッチを適用し、問題の原因となっていたページ テーブルへの書き込みを検出しました。問題は実際にはお客様が作成したカーネル モジュールに存在していたことが判明しました。このモジュールは、以前のバージョンの当社製品では問題なく動作していました。根本的な原因に関する情報は持ち合わせていません。問題の原因となっている関数を指摘した後、お客様からはそれ以上の情報提供がありませんでした。

この種のバグは全体的には気になりませんが、今回の経験から、カーネルはカーネルを深く理解していない人が使うべき場所ではないということを学びました。カーネルエンジニアとして、私が自明だと思っていることが、他の人には全く自明ではないこともあります。多くのお客様が独自のニーズに合わせてカーネルを改変しており、そのような改変には大きなリスクが伴います。

ファームウェアのバグ

不思議なことに、カーネルのバグのすべてがカーネルに起因するわけではありません。あるお客様はラボのカードでLinuxを動作させていましたが、起動時やカードの起動直後にクラッシュすることがよくありました。彼らはカーネルのコアダンプを取得して私に送ってくれました。

この件の分析は実に簡単でした。カーネルがクラッシュした時に何が起こっていたのかを調べ、いろいろと調べた結果、メモリ上で実行されていたマシンコードが、メモリ内にあるべきものと一致していないことに気づきました。間違ったメモリを抜き取り、顧客に送り返しました。顧客にとって何か意味のある情報になることを期待していました。ダンプの中にラボのIPアドレスが含まれ、それがARPパケットであることが分かりました。ファームウェアはカーネルを起動する前にイーサネットデバイスを無効化していなかったことが判明しました。カーネルがイーサネットデバイスをリセットする前にパケットを受信した場合、メモリを介してDMAが実行されることになります。

競合状態

現在、私が開発したプロジェクトであるGensioのテストスイートで発見されたバグの修正に取り組んでいます。コードのどこが間違っているのかを突き止めるのにかなりの時間を費やしました。結局のところ、カーネルのせいにするのはコンパイラのせいにするのと同じです。しっかり確認した方がいいでしょう。

しかし、頭を悩ませた後、小さな再現ツールを書いてみたら、案の定カーネルに原因がありました。マスターptyに書き込みをしてptyを閉じると、微妙な競合状態によって、データの途中でデータが欠落してしまうことが時々ありました。これはカーネルに長年存在していたのですが、誰も気づいていませんでした。ttyのコードは非常に複雑なので、メンテナーも修正方法がまだよく分かっていません。

使用後解放バグ

最後に、最近私が取り組んだバグについてお話しします。お客様はネットワークネイバーコード、つまりARPなどを扱う汎用コードに深刻なダメージを与えていました。時折、カーネルがクラッシュすることがあり、通常はタイマーコードかそれに関連する部分で発生していました。タイマーデータは完全に偽物であることが判明し、解析とデバッグパッチ適用の結果、メモリの解放後使用(use-after-free)が原因であることが分かりました。タイマーデータは破壊されていたため、タイマーの出所を特定できませんでした。実際、当時はネイバーコードと何らかの関係があるとは認識していませんでした。ただ、タイマーに関連する何かがクラッシュしていることは分かっていました。

これを突き止めるために、データ構造内で実行中のすべてのタイマーを追跡するコードをいくつか書き、そのメモリ チャンク内に既知の実行中のタイマーがある場合にパニックを起こすコードをメモリ解放ルーチンに追加しました。

その後、当然ながら、問題は発生しなくなりました。ハイゼンバグです。フリーコードに余分な時間があったため、タイミングがずれて問題が隠れてしまったのだと思います。顧客はデバッグコードを残したままにしていました。デバッグコードはシステムの動作に影響を与えないほど効率的だったからです。数ヶ月後、ついにトリップが発生しました。この問題はその後のカーネルパッチで修正されたと思われます(かなり最近のことなので、まだ100%確実ではありません)。しかし、パッチのヘッダーにはこの種の競合に関する記述はありませんでした。

だから何

次の投稿では、これらのバグが説明に役立つと思う理由について説明します。