-
Notifications
You must be signed in to change notification settings - Fork 275
Description
C++ objects with static duration (e.g. globals) are typically destroyed by the CRT from DllMain (in library projects) [1]. However, destruction is best avoided on process termination because the state is compromised [2]. C++ objects destructors can use wil::ProcessShutdownInProgress to determine if the library is being unloaded normally, in which case resources must be freed, or if instead the process is terminating and any work can be skipped.
So wil::ProcessShutdownInProgress should be DllMain-safe. However turns out that it acquires a lock indirectly via GetModuleHandle, namely the LdrpSnapsLock. This lock can be orphaned on process shutdown (unlike the loader lock LdrpLoaderLock, acquired by ExitProcess before terminating all other threads).
This shows that SHCore.dll, which uses wil::ProcessShutdownInProgress, can acquire an orphaned LdrpSnapsLock on termination:
> ntdll.dll!NtTerminateProcess() Unknown
ntdll.dll!RtlpWaitOnCriticalSection() Unknown
ntdll.dll!RtlpEnterCriticalSectionContended() Unknown
ntdll.dll!RtlEnterCriticalSection() Unknown
ntdll.dll!LdrpAddUnicodeStringToSnapsBuffer() Unknown
ntdll.dll!LdrpLogInternal() Unknown
ntdll.dll!LdrGetDllHandle() Unknown
KERNELBASE.dll!GetModuleHandleW() Unknown
SHCore.dll!wil_details_GetNtDllModuleHandle(void) Unknown
SHCore.dll!wil::details::RtlDllShutdownInProgress(void) Unknown
SHCore.dll!wil::ProcessShutdownInProgress(void) Unknown
SHCore.dll!wil::details::`dynamic atexit destructor for 'g_enabledStateManager''() Unknown
ucrtbase.dll!<lambda>(void)() Unknown
ucrtbase.dll!__crt_seh_guarded_call<int>::operator()<<lambda_7777bce6b2f8c936911f934f8298dc43>,<lambda>(void) &,<lambda_3883c3dff614d5e0c5f61bb1ac94921c>>() Unknown
ucrtbase.dll!_execute_onexit_table() Unknown
ucrtbase.dll!__crt_state_management::wrapped_invoke<int (*)(int *),int *,int>(int (*)(int *),int *) Unknown
SHCore.dll!dllmain_crt_process_detach() Unknown
SHCore.dll!dllmain_dispatch() Unknown
ntdll.dll!LdrpCallInitRoutineInternal() Unknown
ntdll.dll!LdrpCallInitRoutine() Unknown
ntdll.dll!LdrShutdownProcess() Unknown
ntdll.dll!RtlExitUserProcess() Unknown
kernel32.dll!ExitProcessImplementation() Unknown
ucrtbase.dll!common_exit() Unknown
This leads to instant termination [3] and other DllMain do not have a chance to run.
The issue can be reproduced with the following sample:
#include <windows.h>
#include <stdio.h>
#include <process.h>
static unsigned int __stdcall
thread_main(void* user_data)
{
for (;;)
{
GetModuleHandle(L"ntdll.dll");
}
return 0;
}
int
main(void)
{
for (int i = 0; i < 16; i++)
_beginthreadex(NULL, 0, thread_main, NULL, 0, NULL);
/* By default, stdout is unbuffered when output goes to
* an interactive device. Call setvbuf to make this test
* effective regardless of the output device.
*/
setvbuf(stdout, NULL, _IOFBF, 4096);
printf("hello world!\n");
LoadLibraryEx(L"shcore.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32);
}Since the CRT flushes FILE streams from its DllMain, when instant termination occurs due to the orphaned lock acquisition you won't see any output.
The test above should be run in a loop:
While ($true) { echo "launching"; .\test.exe; Start-Sleep -Seconds 1 }