スタックオーバーフロー
こんにちは、K里です。
今回は Windows OS がクラッシュする最多要因の一つであるカーネルスタックオーバーフローについてお話したいと思います。
カーネルスタックは、関数間でやりとりする引数、戻り値や関数内で使用するローカル変数を保持するためのストレージ領域で、そのサイズはプロセッサアーキテクチャに依存して x86 では 12 KB、x64 (amd64/EM64T 含) では 24KB、Itanium では 32 KB と固定で割り当てられます。また、カーネルスタックは、Windows OS の各プロセスで動作するスレッド単位で割り当てられますので、スレッド内で呼び出される全ての関数で使用するスタック消費サイズが割り当てサイズを超えた場合スタックオーバーフローが発生します。その結果、OS はシステム内で致命的なエラーが発生したと判断し、下記のいずれかの Bugcheck を発生させることになります。
Bug Check Codes
https://msdn.microsoft.com/en-us/library/ff542347(VS.85).aspx
STOP 0x1E: KMODE_EXCEPTION_NOT_HANDLED
STOP 0x2B: PANIC_STACK_SWITCH
STOP 0x7E: SYSTEM_THREAD_EXCEPTION_NOT_HANDLED
STOP 0x7F: UNEXPECTED_KERNEL_MODE_TRAP
STOP 0x8E: KERNEL_MODE_EXCEPTION_NOT_HANDLED
以下は x86 での典型的なスタックオーバーフローです。
1: kd> !analyze -v
*******************************************************************************
* *
* Bugcheck Analysis *
* *
*******************************************************************************
UNEXPECTED_KERNEL_MODE_TRAP (7f)
This means a trap occurred in kernel mode, and it's a trap of a kind
that the kernel isn't allowed to have/catch (bound trap) or that
is always instant death (double fault). The first number in the
bugcheck params is the number of the trap (8 = double fault, etc)
Consult an Intel x86 family manual to learn more about what these
traps are. Here is a *portion* of those codes:
If kv shows a taskGate
use .tss on the part before the colon, then kv.
Else if kv shows a trapframe
use .trap on that value
Else
.trap on the appropriate frame will show where the trap was taken
(on x86, this will be the ebp that goes with the procedure KiTrap)
Endif
kb will then show the corrected stack.
Arguments:
Arg1: 00000008, EXCEPTION_DOUBLE_FAULT
Arg2: 807c6750
Arg3: 00000000
Arg4: 00000000
Debugging Details:
------------------
BUGCHECK_STR: 0x7f_8
TSS: 00000028 -- (.tss 0x28)
eax=00fc13d9 ebx=00000000 ecx=00000000 edx=00000000 esi=871e6ef8 edi=863094c0
eip=961cd3b5 esp=96e0099c ebp=96e011e4 iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010202
foo!FilterConsumeStackPhase5+0x15:
961cd3b5 c685b8f7ffff00 mov byte ptr [ebp-848h],0 ss:0010:96e0099c=??
Resetting default scope
DEFAULT_BUCKET_ID: VISTA_DRIVER_FAULT
PROCESS_NAME: test.exe
CURRENT_IRQL: 0
LAST_CONTROL_TRANSFER: from 961cd37e to 961cd3b5
STACK_TEXT:
96e011e4 961cd37e 00000000 00000000 00000000 foo!FilterConsumeStackPhase5+0x15
96e01d84 961cd31e 00000000 00000000 00000000 foo!FilterConsumeStackPhase4+0x3e
96e023b4 961cd2c2 00000000 00000000 00000000 foo!FilterConsumeStackPhase3+0x3e
96e033c4 961cd25e 00000000 00000000 00000000 foo!FilterConsumeStackPhase2+0x42
96e03bd4 961cd194 874fa288 00220000 0000000e foo!FilterConsumeStackPhase1+0x3e
96e03bfc 83085593 871e6ef8 86324348 86324348 foo!FilterDispatchIo+0x174
96e03c14 8327999f 863094c0 86324348 863243b8 nt!IofCallDriver+0x63
96e03c34 8327cb71 871e6ef8 863094c0 00000000 nt!IopSynchronousServiceTail+0x1f8
96e03cd0 832c33f4 871e6ef8 86324348 00000000 nt!IopXxxControlFile+0x6aa
96e03d04 8308c1ea 0000001c 00000000 00000000 nt!NtDeviceIoControlFile+0x2a
96e03d04 778870b4 0000001c 00000000 00000000 nt!KiFastCallEntry+0x12a
0024f92c 77885864 75a6989d 0000001c 00000000 ntdll!KiFastSystemCallRet
0024f930 75a6989d 0000001c 00000000 00000000 ntdll!NtDeviceIoControlFile+0xc
0024f990 75d2a671 0000001c 00220000 00000000 KERNELBASE!DeviceIoControl+0xf6
0024f9bc 0134126f 0000001c 00220000 00000000 kernel32!DeviceIoControlImplementation+0x80
0024fa00 0134141a 00000001 000d0f00 000d1508 test!main+0x5a
0024fa44 75d33c45 7ffdf000 0024fa90 778a37f5 test!__mainCRTStartup+0x102
0024fa50 778a37f5 7ffdf000 77b9bfb9 00000000 kernel32!BaseThreadInitThunk+0xe
0024fa90 778a37c8 0134154b 7ffdf000 00000000 ntdll!__RtlUserThreadStart+0x70
0024faa8 00000000 0134154b 7ffdf000 00000000 ntdll!_RtlUserThreadStart+0x1b
STACK_COMMAND: .tss 0x28 ; kb
FOLLOWUP_IP:
foo!FilterConsumeStackPhase5+15
961cd3b5 c685b8f7ffff00 mov byte ptr [ebp-848h],0
SYMBOL_STACK_INDEX: 0
SYMBOL_NAME: foo!FilterConsumeStackPhase5+15
FOLLOWUP_NAME: MachineOwner
MODULE_NAME: foo
IMAGE_NAME: foo.sys
DEBUG_FLR_IMAGE_TIMESTAMP: 4e1ac464
FAILURE_BUCKET_ID: 0x7f_8_foo!FilterConsumeStackPhase5+15
BUCKET_ID: 0x7f_8_foo!FilterConsumeStackPhase5+15
Followup: MachineOwner
---------
このような場合、上記 !analyze 結果での STACK_COMMAND からまずは k コマンドでコールスタック上の各関数を見ていくことになりますが、その際に f オプションを付けることで、各関数で消費しているスタックサイズ (下記出力結果の赤字部分の内 KiFastCallEntry から呼び出される関数以降がカーネルスタックサイズになります) を表示することが可能です。例えば関数 FilterConsumeStackPhase2 では、4112 バイトものスタックを消費しており、スレッドで割り当てられる全スタックサイズの 3 分の 1 をこの関数で消費していることになります。ソフトウェアデザインにも依存しますが、ドライバは各関数内で消費するスタックを最小限に留める必要があります。一般的にはローカルで宣言するのはポインタやシンプルなカウンタのみとし、比較的大きいサイズの変数や構造体などはシステムメモリ (ページ/非ページ) を割り当てそのポインタを操作し、また再起関数は呼び出し回数を制限します。
1: kd> .tss 0x28
eax=00fc13d9 ebx=00000000 ecx=00000000 edx=00000000 esi=871e6ef8 edi=863094c0
eip=961cd3b5 esp=96e0099c ebp=96e011e4 iopl=0 nv up ei pl nz na po nc
cs=0008 ss=0010 ds=0023 es=0023 fs=0030 gs=0000 efl=00010202
foo!FilterConsumeStackPhase5+0x15:
961cd3b5 c685b8f7ffff00 mov byte ptr [ebp-848h],0 ss:0010:96e0099c=??
1: kd> kfL
*** Stack trace for last set context - .thread/.cxr resets it
Memory ChildEBP RetAddr
96e011e4 961cd37e foo!FilterConsumeStackPhase5+0x15
ba0 96e01d84 961cd31e foo!FilterConsumeStackPhase4+0x3e
630 96e023b4 961cd2c2 foo!FilterConsumeStackPhase3+0x3e
1010 96e033c4 961cd25e foo!FilterConsumeStackPhase2+0x42
810 96e03bd4 961cd194 foo!FilterConsumeStackPhase1+0x3e
28 96e03bfc 83085593 foo!FilterDispatchIo+0x174
18 96e03c14 8327999f nt!IofCallDriver+0x63
20 96e03c34 8327cb71 nt!IopSynchronousServiceTail+0x1f8
9c 96e03cd0 832c33f4 nt!IopXxxControlFile+0x6aa
34 96e03d04 8308c1ea nt!NtDeviceIoControlFile+0x2a
0 96e03d04 778870b4 nt!KiFastCallEntry+0x12a
0024f92c 77885864 ntdll!KiFastSystemCallRet
4 0024f930 75a6989d ntdll!NtDeviceIoControlFile+0xc
60 0024f990 75d2a671 KERNELBASE!DeviceIoControl+0xf6
2c 0024f9bc 0134126f kernel32!DeviceIoControlImplementation+0x80
44 0024fa00 0134141a test!main+0x5a
44 0024fa44 75d33c45 test!__mainCRTStartup+0x102
c 0024fa50 778a37f5 kernel32!BaseThreadInitThunk+0xe
40 0024fa90 778a37c8 ntdll!__RtlUserThreadStart+0x70
18 0024faa8 00000000 ntdll!_RtlUserThreadStart+0x1b
しかしながら、通常 1 スレッドで呼び出される関数は、ご自身で開発されているドライバ内関数だけではなく、Windows OS の各モジュールや他の皆様が開発されたドライバなど様々な関数呼び出しで構成されます。そのため、たとえご自身のドライバのスタックサイズが少量であっても他で消費されるスタックサイズが大きければ、その結果オーバーフローが発生してしまうケースがあるので注意が必要です。具体的には、I/O Manager から発行される I/O 要求を受信し、独自の処理や情報の追加などを行い、下位ドライバへ I/O を通知するようなフィルタドライバが対象となります。フィルタドライバが多数インストールされること自体は問題ではありませんが、ドライバの数に応じて否応にも呼び出される関数は増え、その分スタックも消費されますのでオーバーフローが発生する可能性が高まることになります。特にファイルシステムやディスク関連のフィルタドライバなどでこのような問題が発生しやすい傾向にあります。
そこで、ドライバ内で予め現行スタック消費量を求めておき、その消費量が大きい場合には、その後の処理を別スレッドで実施することでオーバーフローを防ぐことが可能です。具体的には IoGetRemainingStackSize 関数を呼び出して残りのスタックサイズを計算し、十分なスタック領域が残っているか (IoGetRemainingStackSize の呼び出しタイミングから以降の処理で消費するスタックを計算し、オーバーフローが発生しないサイズで調整) を判断します。そのサイズが十分ではない場合、以降の I/O 処理を IoQueueWorkItem 関数でキューイングし、元スレッドでは work item が完了するまで待ち状態とし同期を図ります。
このようにドライバ内でスタック消費量をチェックし、別スレッドで処理を行うことでスタックオーバーフローを防ぐことは可能ですが、ドライバの数や追加機能を増やせばやはりスタックを使い果たすことになります。そのため、まずは必要ではないフィルタドライバがインストールされていないか、また皆様が開発されているドライバでスタックを大量に消費してしまうような宣言、処理がないかをご確認いただくことをお奨めいたします。
それではまた。
Appendix
How do I keep my driver from running out of kernel-mode stack?
https://msdn.microsoft.com/en-us/windows/hardware/gg463190.aspx
IoGetRemainingStackSize Routine
https://msdn.microsoft.com/en-us/library/ff549286(VS.85).aspx