著者:コーリー・ミニヤード
信頼性とLinux
前回の記事では、安全性が極めて重要なソフトウェアにおける信頼性問題の深刻さについて述べました。今回の記事では、Linuxにおけるバグに焦点を当てます。
私はMontaVistaのカーネルアーキテクトとして働いており、その過程でカーネル内の数多くの難解なバグに取り組んできました。簡単なバグにも取り組みましたが、それらは主に他の人が担当しています。他の人が進展できなかった後に、私が担当することになるのです。これらのバグのいくつかを例に挙げ、現在の分析手法ではカーネルが安全性が極めて重要なシステムには適さないと考える理由を説明したいと思います。
記憶を踏みにじる者
まず最初に取り上げるバグは、メモリを不正に操作するバグです。カーネル内で実際にメモリを不正に操作するバグはこれが唯一で、私の経験と観察によれば、非常に稀です。静的解析やレビューで簡単に見つけることができます。安全性の観点からは、特に問題視していません。
この状況では、ページテーブル(struct page)の一部がランダムに上書きされていました。常にページテーブル内に存在していたため、少なくともある程度の一貫性は保たれていました。そこで、カーネルを修正してページテーブルを読み取り専用にし、ページテーブルへの書き込みを行うすべての処理の周りにコードを追加して、書き込み中は書き込み対象のページのみを書き込み可能にするようにしました。このかなり難しい修正とレビュー、テストの後、パッチをお客様に送りました。お客様はこれらのパッチを適用し、問題の原因となっていたページテーブルへの書き込みを検出しました。実際には、問題はお客様が作成したカーネルモジュールにあったことが判明しました。このモジュールは、以前のバージョンの製品では正常に動作していました。根本原因に関する情報は得られていません。問題の原因となっている関数を特定した後、お客様からそれ以上の情報は提供されませんでした。
この種のバグ自体はそれほど気にならないものの、今回の経験を通して、カーネルはカーネルを深く理解していない人には向かない場所だと痛感しました。カーネルエンジニアである私にとって自明の理と思えることも、他の人には全く理解できない場合があるのです。多くのお客様が独自のニーズに合わせてカーネルを改変していますが、そうした改変には大きなリスクが伴います。
ファームウェアのバグ
不思議なことに、カーネルのバグすべてがカーネルに起因するわけではない。ある顧客が研究室でLinuxを搭載したカードを使用していたところ、起動時または起動直後にカードがクラッシュすることが頻繁に発生した。彼らはカーネルコアダンプを取得し、私に送ってくれた。
この分析は実に簡単でした。カーネルがクラッシュした時に何が起こっていたのかを調べ、いろいろと調べてみたところ、メモリ内で実行されていたマシンコードが、本来メモリ内に存在するべきものと一致していないことに気づきました。そこで、誤ったメモリを抽出して顧客に送り返し、何らかの手がかりが得られることを期待しました。顧客はダンプの中に自社のラボのIPアドレスを見つけ、それがARPパケットだったことが判明しました。ファームウェアがカーネル起動前にイーサネットデバイスを無効化していなかったことが原因だったのです。カーネルがイーサネットデバイスをリセットする前にパケットを受信すると、メモリ経由でDMA転送が行われていました。
レースコンディション
現在、私が開発したプロジェクトであるgensioのテストスイートで検出されたバグの修正に取り組んでいます。自分のコードのどこに問題があるのかを突き止めるのに多くの時間を費やしました。結局のところ、カーネルのせいにするのはコンパイラのせいにするのと同じようなものです。本当にそうなのか、確信が持てなければいけません。
しかし、頭を悩ませた末、簡単な再現手順を作成したところ、やはりカーネルに問題があることが判明しました。マスターptyに書き込みを行い、ptyを閉じると、微妙な競合状態によって、データの途中にデータの一部が欠落してしまうことが時折発生します。この問題はカーネルに長期間存在していましたが、誰も気づいていませんでした。ttyコードは非常に複雑なため、メンテナーたちも修正方法を完全に把握できていないのです。
解放後使用バグ
最後の例として、最近私が取り組んだバグについてお話しします。お客様はネットワークネイバーコード、つまりARPなどを処理する汎用コードに非常に大きな負荷をかけていました。時折カーネルがクラッシュし、通常はタイマーコードかそれに関連する部分で発生していました。タイマーデータは全くの偽物に見え、分析とデバッグパッチを適用した結果、解放済みメモリの使用が原因であることが分かりました。タイマー内のデータは破壊されていたため、タイマーがどこから来たのか特定できませんでした。当時、それがネイバーコードと関係があるとは実際には分かっていませんでした。ただ、タイマーに関連する何かがクラッシュしていることだけは分かっていました。
この問題を突き止めるために、実行中のタイマーすべてをデータ構造で追跡するコードを作成し、メモリ解放ルーチンに、既知の実行中のタイマーがそのメモリ領域内に存在する場合にパニックを起こすコードを追加しました。
そしてもちろん、問題は発生しなくなりました。ハイゼンバグです。おそらく、フリーコード内の余分な時間によってタイミングがずれ、問題が隠蔽されたのでしょう。顧客はデバッグコードをそのまま残しました。それはシステムの動作に影響を与えないほど効率的だったからです。数か月後、ついに問題が発生しました。この問題は後のカーネルパッチで修正されたと思われます(これはごく最近のことなので、まだ100%確信は持てませんが)、パッチのヘッダーにはこの種の競合状態についての記述はありませんでした。
だから何?
次回の投稿では、なぜこれらのバグが分かりやすい例だと私が考えるのかについてお話しします。
Please reach out to discuss your particular scenario today.