Skip to content

ProcessShutdownInProgress may acquire orphaned lock on process termination #567

@lb90

Description

@lb90

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 }

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions