Driver VerifierのSpecial Pool

こんにちは。株式会社SHIFT、自動化エンジニアの水谷です。

かなり前の話になるのですが、Windows用のデバイスドライバーのテストを行った経験が何年かあります。デバイスドライバーのテスト環境はやや特殊で、「Driver Verifier」というOS自身が持つデバイスドライバーのチェック機能がほぼすべての場面で使われていました。

Driver Verifierは、デバイスドライバーの動作を様々な面からチェックしてくれる便利な機能なのですが、その中でも「Special Pool」機能について知った時にはちょっとした衝撃、というか感動のようなものを覚えましたので、今回はこれについて書いてみたいと思います。


ドライバーのクラッシュのほとんどはメモリ関連のバグが原因

意外と知られていないことなのですが、Windowsがブルースクリーンを表示して止まってしまう原因のほとんどがデバイスドライバーのバグにあります。
Windowsのカーネルは(新しく追加された部分もありますが、多くの部分は)開発されてからすでに30年以上経っていてバグは狩りつくされた状態のため、ここが原因でブルースクリーンになるケースはあまりないと言われています。

つまり、Windowsがクラッシュするかどうかは、使っているデバイスドライバーの出来次第、と言っても過言ではありません(この問題を解決するためにWHQLやHCKなどが生まれてきたのですが、これはまた別の機会にでも)。そこで、Windowsにはデバイスドライバーのバグを見つけ出し、堅牢性を高めるための補助機能としてDriver Verifierが作りこまれています。

Vierifer.exeを起動してみる

Driver Verifierはたくさんの視点からドライバーをチェックしてくれるのですが、どのドライバーにどのようなチェックを有効にするかは、GUIで指定できます。そのためのツールが「verifier.exe」です。
”Windows”キー+”R”で、”verifier.exe”とタイプして起動してみると、以下のようなウィンドウが開きます。

画像1


ここで、上から2番目の「カスタム設定を作成する(コード開発者用)」を選択して「次へ」ボタンを押すと、どのようなチェックを行うかを選択する画面に進みます。

画像2

ここで1番上に表示されている「特別なプール」が、今回お話ししたい「Special Pool」です(なんで「特別なプール」と訳しちゃったんだろうな、まだ「スペシャルプール」の方がよかったのに、とか思いつつw)。

メモリアクセス問題の難しさ

C言語でよくプログラミングされている方は、メモリアクセス問題に悩まされた経験が何度もあることと思います。
C言語では、低レベルなメモリ操作ができるため、より高速で効率的なプログラムが書ける反面、これが原因でデバッグが難しいバグを生んでしまうことがあります。デバイスドライバーも一般的にC言語で記述することが多いため、メモリアクセスに関するバグが混入することが良くあります。

中でもバッファーオーバーラン(バッファーオーバーフロー)系のバグは、やっかいです。
一見うまく動いているようでも、特定の条件が重なった時だけエラーとなったりすることがあってバグの発見が遅れたり、再現性が低いがためにバグ修正に時間がかかったりすることも多々あります。

また、バッファーオーバーランによって別の変数や他プロセスが使っているメモリ領域を破壊してしまうことで、まったく関係ないと思われる動作に不具合を生じさせてしまったりすることもあります。不具合が起きた動作に関するコードを何度見てもどこにも間違いがないのになぁ……と、デバッグに時間がかかったりすることもあります。

これがユーザーモードのアプリケーションであれば、最悪でもアプリのクラッシュですむことが多いですが、これがデバイスドライバーであれば(他のデバイスドライバーやOSカーネルが確保したメモリ領域にもアクセスできてしまうため)、OS全体の動きに影響を与えることがあります。つまり、“最悪”のレベルが桁違いにヤバイものとなり、たとえばOSの起動不可、とかディスク破壊なんてことも可能性が0とは言い切れない状態になってしまいます。

そこで、このSpecial Poolでは、このバッファーオーバーフローや、その逆のバッファーアンダーフローを検出する手助けをしてくれます。

Special Poolのスぺシェルたる所以

Driver VerifierでSpecial Poolを有効にした状態で、デバイスドライバーがExAllocatePoolWithTag等のメモリ確保系カーネルモードAPIをコールすると、たとえそのサイズが小さくても、1ページ(通常4Kbytes)のメモリが用意され、その最後部に要求したメモリ領域が確保されます(下図は16バイトを確保した場合の例、本来なら4バイトのタグが付きますが、ここでは省略)。

画像3

そして、この際ページ内の残りの領域は特定のパターンで埋め尽くされます。さらに、続くページはあえて非アサインページとして、別のメモリ確保系APIが呼ばれても、ここにはアサインしないような動作を行います。

これによって、もし、1バイトでも確保した領域より先のメモリにアクセス(書き込みだけでなく読み込みも)すると、これは“次のページへのアクセス”=“非アサインページへのアクセス”となり、確実にページフォルトが発生することになります。デバイスドライバーですので、ページフォルトは即ブルースクリーンの状態となり、カーネルデバッガーを接続していれば、開発者やテスターはバグチェックコード(この場合は0xC1)とパラメーター、それからスタックトレースなどからどのようなメモリアクセス問題があったのかを見て、デバッグを行うことになります。また、カーネルデバッガーが接続されていなければ、ダンプファイルが生成されますので、それをデバッガーに読み込んで調査することになります。

また、バッファーアンダーランについても、部分的に検出ができるようになっています。このメモリがアサインされたページの上部は特定のバイトパターンで埋め尽くされており、もし確保したメモリ領域の上部(マイナスオフセット部分)に書き込みを行った場合はそのパターンが崩れることになります。メモリが解放される際に、OSはそのパターンが崩れていないかチェックが自動的に行われ、もし崩れていたら同じくブルースクリーンとなります。

また、より効率的にバッファーアンダーランを検出するために、Windowsのカーネルはあえてページの先頭にメモリをアサインする場合もあります。

画像4

この場合は、前ページは非アサインとし、1バイトでもアサインされたメモリ領域より前にアクセス(リードおよびライト)を行うとページフォルトが発生してブルースクリーンが表示されます。
さらに、メモリを解放した後にその領域にアクセスしてしまう問題もよくあるのですが、これについても、メモリ解放後はこのページを非アサイン状態に保つことで、メモリ解放後のアクセスを確実に見つけ出す機能を備えています。

なんとも贅沢なメモリの使い方ですし、かなりパフォーマンスも低下しますが、”確かにこれは効率よくメモリアクセスバグを検出できるな”、とそのアイディアに感心しましたし、それと同時にドライバーの不具合検出のためだけにここまでの機能をカーネルに作りこんでいるのか!? という驚きを覚えた記憶があります。

Driver Verifierには、これ以外にもさまざまな機能があり、昔も今もデバイスドライバーのテストには必須のアイテムとなっているます。その機能の中にはSpecial Poolと同様に素晴らしいアイディアが詰め込まれたものもありますので、もし興味を持たれたら調べてみると面白いかもしれません。

――――――――――――――――――――――――――――――――――

執筆者プロフィール:水谷 裕一
大手外資系IT企業で15年間テストエンジニアとして、多数のプロジェクトでテストの自動化作業を経験。その後画像処理系ベンチャーを経てSHIFTに入社。
SHIFTでは、テストの自動化案件を2件こなした後、株式会社リアルグローブ・オートメーティッド(RGA)にPMとして出向中。RGAでは主にAnsibleに関する案件をプレーイングマネジャーとして担当している。

お問合せはお気軽に
https://service.shiftinc.jp/contact/

SHIFTについて(コーポレートサイト)
https://www.shiftinc.jp/

SHIFTのサービスについて(サービスサイト)
https://service.shiftinc.jp/

SHIFTの導入事例
https://service.shiftinc.jp/case/

お役立ち資料はこちら
https://service.shiftinc.jp/resources/

SHIFTの採用情報はこちら
https://recruit.shiftinc.jp/career/