Windows上の謎のクラッシュバグを追跡する話 #14

公開日: 2012-02-01


デバッグの話はおもしろい。手探りの状態の中、一歩一歩、小さなヒントを手繰って問題の核心に近づいていく過程はミステリー小説のようである。私も何か自分のデバッグ話でも披露しようと思ったが、しょぼい話しか思いつかなかったので、代わりに、少し前に読んでおもしろかった話を紹介したい。

Tracking down a mysterious Windows crash
http://neugierig.org/software/chromium/notes/2011/08/windows-hookers.html

これは Chrome の Windows 版のクラッシュバグを追跡する話である。

このバグはクラッシュの中でも上位のものだったが、長らく原因不明であった。0番地にジャンプしようとしてクラッシュしているようなのだが、なぜそんなことが起きるのか誰にもわからなかった。というのも、クラッシュが起きるのは Chrome のイベントループのメカニズムの中でも非常に一般的なところで起きるので、ありとあらゆるコードが同じ場所を通る可能性があるのだ。

さらに、イベントドリブンのプログラムなので、スタックトレースはイベントループの先からしかわからず、元のコールバックの呼び出し元がわからない(イベントドリブンのプログラムはこれがしんどいですね)。

しかし、クラッシュダンプのファイル (minidump) からわかったのは、クラッシュが起きるスレッドは Child process launcher というスレッドで起きるらしいということだ。これを検証するコードを入れてみたら、確かにそうだということがわかった。このスレッドで動くコードは小さいので、すべてのコードを精査すればわかるかと思ったが、残念ながら手がかりなしであった。

しばらくして、たまたま、あるエンジニアがこのバグの担当として任命された。まもなく、氏は名案を思いついた。コールバックの呼び出し元がわからないのが問題だったら、わかるようにすればいいじゃないか?

コールバック(タスクとも呼ばれる)をイベントループに投げるときに、呼び出し元のプログラムカウンタ(実行中のコードのアドレス)を一緒に登録するようにした。そして、タスクを実行する直前に、このプログラムカウンタの値をローカル変数にコピーしてスタック上に含める。これだけだとコンパイラに最適化されて消えてしまうかもしれないので、最適化できない関数呼び出しをして、必ずスタックの上に値が残るようにする。

クラッシュ時に生成されてサーバに送られる minidump ファイルにはスタックの値が保存されているので、このようにしておけば、minidump の内容から呼び出し元のアドレスがわかり、関数名もわかるのである。このアイディアは大成功して、問題のタスクの正体は ChildProcessLauncher::Context::Terminate() から投げられる、ChildProcessLauncher::Context::TerminateInternal() という関数であることがわかった。

が、この問題の関数を見ても、何が原因かまだわからない。タスク関連の user-after-free かと当たりをつけて検証するコードをいれてみたが、違う。仕方がないので、TerminateInternal を逆アセンブルしてみたが、たいしたことはやっていない。TerminateProcess という Win32 API を呼んでいるくらいだ。この関数呼び出しは DLL の import table 上のアドレスにジャンプすることで実現している。

もしかしたらこの TerminateProcess のアドレスが何らかのプログラムにより書き換えられて、それによってスタックが壊されているのが原因じゃないか?この推理を検証するコードをいれてみたが、特に新しい手がかりは得られず。

もうひとつ、 CloseHandle という Win32 API も同様に呼び出しているで、こっちもあやしい。いっそのこと TerminateProcess と CloseHandle の両方とも、import table 経由で呼び出すのではなく、ロードされているDLLから本物のアドレスを取り出して(本物の名前はそれぞれ NtTerminateProcess, NtClose というらしい)、直接呼び出したらいいんじゃないか?ということでやってみたら、大成功。問題のクラッシュバグは消えて、Windows のクラッシュの10%近くが削減できた。めでたし!

というのが大まかな話の内容。詳しくは元の記事を読んで欲しい。

ちなみに、犯人は TerminateProcess の方だったようで、NtClose を直接呼び出すコードは http://crrev.com/97407 で消されていた。不要になったコードをちゃんと消しているところも偉い。

Satoru Takabayashi