スタックオーバーフロー

こんにちは、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