How to deal with the fact that dynamically loaded DLL can be unloaded by Windows even if reference count is not zero?

Viewed 443

The documentation says "The system unloads a module when its reference count reaches zero or when the process terminates (regardless of the reference count)."

This causes a significant problem and it's unclear how to solve it. Now about the problem. Suppose we have an executable E that explicitly depends on a DLL library L1 that dynamically loads a DLL library L2 via LoadLibrary. When E finishes and the process starts terminating L2 can be unloaded before L1. This can be problematic if L1 has an object with static storage duration which contains some of the resources obtained from from L2 because those resources will start destroying at L1 unloading time which means a call to already unloaded L2 will result in a crash.

One of the possible solutions (of even the only one) is not to destroy the object with static storage duration. The drawback of this solution is that L1 can potentially be loaded/unloaded dynamically multiple times, which increases the memory leak. This can be mitigated by using DllMain where we could check whether the process terminates or the library is unloaded via FreeLibrary.

So far everything seems to work just fine. But there is more. Suppose we want L1 to be a static library that links with a DLL library named L_SHIM. So now the executable E is linked with the L_SHIM and the whole DllMain trick doesn't work anymore. Actually, nothing seems to work if we are not allowed to modify L_SHIM library.

Has anybody had to solve a problem like this? I'd appreciate any thoughts about possible solutions.

1 Answers

When handling DLL_PROCESS_DETACH, a DLL should free resources such as heap memory only if the DLL is being unloaded dynamically (the lpReserved parameter is NULL). If the process is terminating (the lpvReserved parameter is non-NULL), all threads in the process except the current thread either have exited already or have been explicitly terminated by a call to the ExitProcess function, which might leave some process resources such as heaps in an inconsistent state. In this case, it is not safe for the DLL to clean up the resources. Instead, the DLL should allow the operating system to reclaim the memory.

so you need check lpvReserved inside DllMain:

let you have global variable:

bool g_is_terminating = false;

and in DllMain

case DLL_PROCESS_DETACH: g_is_terminating = (reserved != nullptr);

destructors of objects with global/static called after DllMain. so you can check in destructor g_is_terminating;

if (!g_is_terminating) { do_something(); }

another and always useful and very simply way, use undocumented api

//This routine returns the status of process shutdown.

EXTERN_C
DECLSPEC_IMPORT
BOOLEAN
NTAPI
RtlDllShutdownInProgress();

this api exported from ntdll.dll - so you need use or ntdll.lib or ntdllp.lib (in case you not use crt)

simply call RtlDllShutdownInProgress(); from destructror and if return true - not access another dll - this mean process is terminating

if (!RtlDllShutdownInProgress()) { do_something(); }

internally, in ntdll exist global variable

BOOLEAN LdrpShutdownInProgress = FALSE;

and when ExitProcess called - than LdrShutdownProcess called from ntdll.dll and at begin it set LdrpShutdownInProgress = TRUE; the RtlDllShutdownInProgress() simply return value of LdrShutdownProcess


and about "The system unloads a module when the process terminates (regardless of the reference count)."

this is false. system not unload any dll after process begin terminates. even if direct call FreeLibrary/LdrUnloadDll at this time (after LdrpShutdownInProgress set to TRUE) - dll will be not unloaded. really - no big sense do this - because soon all process will be destroyed.

..which means a call to already unloaded L2 will result in a crash. really L2 never will eb unloaded after call ExitProcess. but DLL_PROCESS_DETACH will be called inside L2. and this will be always before L1 DLL_PROCESS_DETACH notification (this notify send in LIFO order. so if L1 load L2 - DLL_PROCESS_ATTACH called in L1 before in L2 - L1 before L2 in InInitializationOrderLinks list. and on process exit - this list (InInitializationOrderLinks) processed in reverse order .

so L2 , despite still in memory, will be already in "uninit" state (after DLL_PROCESS_DETACH ) call some api inside L2 can lead to undefined results. it may work without any problem, or crash or random result. this even more worse, compare stable crash

Related