2007年9月11日 星期二

When operator new and delete are overwritten

上個禮拜, 本 team 的強者查理大大發現了一個 Vista 上的 Crash... 其 Dump 可謂匪夷所思

我們首次到手的 Dump, call stack 長這樣:

# 1 Id: d30.fd4 Suspend: 1 Teb: 7ffde000 Unfrozen
ChildEBP RetAddr Args to Child
029fc564 773a1d37 01390000 006d5748 029fc60c ntdll!RtlpCoalesceFreeBlocks+0x26d
029fc65c 773a1c21 006d5748 006d5750 006d5750 ntdll!RtlpFreeHeap+0x1e2
029fc678 75d97a7e 01390000 00000000 006d5748 ntdll!RtlFreeHeap+0x14e
029fc68c 1000d343 01390000 00000000 006d5750 kernel32!HeapFree+0x14
029fc6cc 1000961b 006d5750 1bdb284a 029fc798 libXXXXXSecure!free+0x6e
029fc73c 10008c5a 46e56fb6 00000d30 00000fd4 libXXXXXSecure!LogWriteW+0xeb
029fe778 10008d7f 00000008 01390c08 00000154 libXXXXXSecure!vdprintfW+0x1da
029fe7a0 10008edf 00000008 01390c08 00000154 libXXXXXSecure!dprintfW+0x2f
029fe7b8 100090a8 1bdb0952 75d97b60 773a2f1d libXXXXXSecure!CDebugLogW::EnterDebugLog+0x1f
029fe7e4 10025a05 1002f274 00000154 1002f398 libXXXXXSecure!CDebugLogW::CDebugLogW+0x158
029fec40 0044bcd6 1bc66186 00000000 006ad738 libXXXXXSecure!TSECURE::clearTGPTempDir+0x45

...

3 Id: d30.51c Suspend: 1 Teb: 7ffdc000 Unfrozen
ChildEBP RetAddr Args to Child
03ebf92c 773a06a0 7737b18c 00000338 00000000 ntdll!KiFastSystemCallRet
03ebf930 7737b18c 00000338 00000000 00000000 ntdll!NtWaitForSingleObject+0xc
03ebf994 7737b071 00000000 00000000 01390000 ntdll!RtlpWaitOnCriticalSection+0x154
03ebf9bc 77368f4a 01390130 03eb9ea4 01390000 ntdll!RtlEnterCriticalSection+0x152
03ebfaa0 773a214c 00000214 00000220 0139039c ntdll!RtlpAllocateHeap+0x15c
03ebfb18 10018acb 01390000 00000008 00000214 ntdll!RtlAllocateHeap+0x1e3
03ebfb58 1001133a 00000214 00000214 00000000 libXXXXXSecure!_calloc_impl+0xc4
03ebfb70 1000daf3 00000001 00000214 00000000 libXXXXXSecure!_calloc_crt+0x13
03ebfb90 1000dbaa 10000000 00000002 00000000 libXXXXXSecure!_CRT_INIT+0x17b
03ebfbd0 1000dc64 10000000 773aa604 10000000 libXXXXXSecure!__DllMainCRTStartup+0x59
03ebfbd8 773aa604 10000000 00000002 00000000 libXXXXXSecure!_DllMainCRTStartup+0x1d
03ebfbf8 7737ab3b 1000dc47 10000000 00000002 ntdll!LdrpCallInitRoutine+0x14
03ebfc98 7737a954 03ebfd24 03eb9904 00000000 ntdll!LdrpInitializeThread+0x149
03ebfd00 7737a980 03ebfd24 77340000 00000000 ntdll!_LdrpInitialize+0x21d
03ebfd10 00000000 03ebfd24 77340000 00000000 ntdll!LdrInitializeThunk+0x10

列出這兩個 Thread 主要是因為, 我們一度以為 Vista 環境中竟然平行執行 DllMainCRTStartup 及 mainCRTStartup...

但後來發現_CRT_INIT 的 dwReason 參數, 它並非我們所想像的 DLL_PROCESS_ATTACH(1) 而是 DLL_THREAD_ATTACH(2).

03ebfb90 1000dbaa 10000000 00000002 00000000 libXXXXXSecure!_CRT_INIT+0x17b

也就是說, 這根 Thread 根本是來擾亂視聽的, 它其實正常運作中, 只不過被 crashed thread 所 own 住的 Heap lock 給卡住了而已.

所以我們似乎可以把重心全部放在 fd4 這根 thread 上.

此外, 感謝查理大的指示, 我們還擁有一個很重要的線索, 就是該 crash 的 process 會間接 include 到一個

overwrite 掉 operator new 及 delete 的 header file:

static inline void* operator new(size_t size)
{
void* ptr = ::HeapAlloc(::GetProcessHeap(), 0, size);
if (!ptr)
{
_THROW_NCEE(std::bad_alloc, "Insufficient memory");
}
return ptr;
}
static inline void operator delete(void* ptr) { ::HeapFree(::GetProcessHeap(), 0, ptr); }

我們幾乎可以確定問題一定是由它造成的, 但接下來我們想知道的是:

問題到底是怎麼造成的呢? 它真的會 overwrite 掉 CRT 當中對 operator new 及 delete 的實作嗎?

於是我們追進 crashed thread 所呼叫的 free() 當中, 可以看到它操作的是 _crtheap (由 _head_init@CRT 所建立)

libXXXXXSecure!free:
1000d2d5 6a0c push 0Ch
1000d2d7 68f8220310 push offset libXXXXXSecure!_CT??_R0?AVout_of_rangestd+0x2b8 (100322f8)
1000d2dc e837910000 call libXXXXXSecure!__SEH_prolog4 (10016418)
1000d2e1 8b7508 mov esi,dword ptr [ebp+8]
1000d2e4 85f6 test esi,esi
1000d2e6 7475 je libXXXXXSecure!free+0x88 (1000d35d)
1000d2e8 833da08d031003 cmp dword ptr [libXXXXXSecure!__active_heap (10038da0)],3
1000d2ef 7543 jne libXXXXXSecure!free+0x5f (1000d334)
1000d2f1 6a04 push 4
1000d2f3 e8ed850000 call libXXXXXSecure!_lock (100158e5)

...

1000d337 ff35f4880310 push dword ptr [libXXXXXSecure!_crtheap (100388f4)]
1000d33d ff1514b00210 call dword ptr [libXXXXXSecure!_imp__HeapFree (1002b014)]

由此可見, 該 header file 並沒有 overwrite 掉 CRT 當中的 operator delete 實作.

那 operator new 呢? 我們追進 basic_string::_Copy > basic_string::allocate > std::_Allocate > operator new,

可以看到其實作如下:

libXXXXXSecure!operator new:
10025d10 55 push ebp
10025d11 8bec mov ebp,esp
10025d13 83ec10 sub esp,10h
10025d16 8b4508 mov eax,dword ptr [ebp+8]
10025d19 50 push eax
10025d1a 6a00 push 0
10025d1c ff1518b00210 call dword ptr [libXXXXXSecure!_imp__GetProcessHeap (1002b018)]
10025d22 50 push eax
10025d23 ff1544b00210 call dword ptr [libXXXXXSecure!_imp__HeapAlloc (1002b044)]
...

很明顯地, 它是被 overwrite 掉之後的結果.

平平都是 include 那個 overwrite header file, 為啥 operator new 被蓋掉了, 但 operator delete 卻沒有?

我們展開呼叫了 operator delete 的 LogWriteW:

100095fc e8fffaffff call libXXXXXSecure!Send (10009100)
10009601 397c2448 cmp dword ptr [esp+48h],edi
10009605 8ad8 mov bl,al
10009607 c744241044bc0210 mov dword ptr [esp+10h],offset libXXXXXSecure!CCmdProtocol::`vftable' (1002bc44)
1000960f 720d jb libXXXXXSecure!LogWriteW+0xee (1000961e)
10009611 8b442434 mov eax,dword ptr [esp+34h]
10009615 50 push eax
10009616 e864350000 call libXXXXXSecure!operator delete (1000cb7f)

它理應要呼叫 CCmdProtocol::~CCmdProtocol() 來做 cleanup 動作, 但因為被 optimize 的緣故, 此處只出現了一個 delete,

而且這個 delete 忽略掉 overwrite function, 仍舊與 CRT 的實作相連結, 這應該算是個 optimization error 吧...


所謂禍不單行, 取消掉 libLogClient 的 optimization 之後, 程式仍然會 crash...

這次的 Dump, crashed thread 的 call stack 如下:

# 1 Id: 820.f94 Suspend: 1 Teb: 7ffdd000 Unfrozen
ChildEBP RetAddr Args to Child
02bfe684 773a1d37 002f0000 01670c38 02bfe72c ntdll!RtlpCoalesceFreeBlocks+0x35
02bfe77c 773a1c21 01670c38 01670c40 773a2f1d ntdll!RtlpFreeHeap+0x1e2
02bfe798 75d97a7e 002f0000 00000000 01670c38 ntdll!RtlFreeHeap+0x14e
02bfe7ac 10027f56 002f0000 00000000 01670c40 kernel32!HeapFree+0x14
02bfe7c0 10002ffa 01670c40 02bfea20 01670c40 libXXXXXSecure!operator delete+0x16
02bfe7d4 10002f7d 67c8f9a4 02bfea0c 02bfec34 libXXXXXSecure!std::auto_ptr::~auto_ptr+0x1a
02bfe7f0 10025d18 67c8f214 003a0043 0050005c libXXXXXSecure!CDebugLogW::~CDebugLogW+0x4d
02bfec40 0044bcd6 67dbc520 00000000 0031d738 libXXXXXSecure!TSECURE::clearTGPTempDir+0x1a8

出現了 auto_ptr object... 顯然又是個 new/delete 不相符造成的 crash

以同樣的方法追進去, 可以看到這次 operator delete 被 overwrite 掉了, 但 operator new 並沒有

原來是因為 CDebugLogW::~CDebugLogW 的實作放在 header file 裡, 而 CDebugLogW::CDebugLogW 的實作放在另一個 library 內.

從這個例子我們可以看到, 這種寫法會造成建解構函式分別採用到不同的 CRT function 實作.

沒有留言: