debuggingwinapidlldllimportloadlibrary

Why the `LoadLibrary()` function and delay-load use different DLL search path?


I have a case when the LoadLibrary() function and the delay-load DLL use different DLL search path. Specifically:

LoadLibrary("DLL2.dll"); // succeeds

while:

FunctionFromDLL1(); // fails to delay-import a function from DLL2

Running some diagnostic reveals both LoadLibrary() and delay-import call the LdrpComputeLazyDllPath() function to calculate the DLL search path:

Why is that?


Solution

  • It is unforeseen consequences of the LOAD_LIBRARY_SEARCH_USER_DIRS, LOAD_LIBRARY_SEARCH_APPLICATION_DIR, LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR flags. These flags are applied not only to the loaded DLL itself, but also to all delay-loaded DLLs - even if these DLLs are loaded outside of the LoadLibraryEx() call.

    In other words, if some code loads the DLL1.dll via the LoadLibraryEx() function with one of the above flags, the system will remember these flags and will use them later to resolve delay loads from the DLL1.dll.

    For example, if your application have plugins that are located in their own folders:

    App\Plugins\Plugin1\Plugin1.dll // statically linked to DLL1
    App\Plugins\Plugin1\DLL1.dll // delay-imports from DLL2 - a system DLL
    

    You may load the Plugin1.dll via LoadLibraryEx() with the LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR flag, so the Plugin1.dll could find/load the statically linked DLL1.dll. The simple LoadLibrary() would not work here, as the DLL's folder is not in default DLL search path.

    Now, even if your app loads the DLL1.dll file via the LoadLibrary() function - the system will still remember that the DLL1.dll was loaded with custom search paths.

    Next, if you call a function from the DLL1.dll that delay-imports a function from, say, some system DLL (DLL2.dll), the system will use the saved custom DLL search path to find the DLL2.dll. And since the LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR flag overrides DLL search path, the System32 folder will not be in custom DLL search path. This will cause the DLL2.dll to be searched only in the App\Plugins\Plugin1\ folder - which will fail.

    I don't think this behaviour is documented anywhere, but I may be wrong. I looked for it, but can't find any mentions. Perhaps, I looked in wrong places or used bad search terms.

    People on the internet use workarounds like: loading the DLL2.dll explicitly via LoadLibrary() before calling functions from the DLL1.dll. Loading the DLL explicitly will ensure it is loaded by the time delay-import is executed. Since the DLL2.dll will be already loaded, there is no need to search for it, so delay-import will succeed. While this will work, but it is a giant hack, as it requires you to know implementation details of the DLL1.dll - which may be not under your control.

    I don't think there is a way to tell the system to use some specific search paths for delay-loads. So the only/real solution here is to load the Plugin1.dll with something like LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR | LOAD_LIBRARY_SEARCH_DEFAULT_DIRS. The LOAD_LIBRARY_SEARCH_DLL_LOAD_DIR flag will allow the Plugin1.dll to pick up the DLL1.dll from its folder, while the LOAD_LIBRARY_SEARCH_DEFAULT_DIRS will include System32 folder - which will allow delay-load to succeed later.

    Please note LOAD_LIBRARY_SEARCH_DEFAULT_DIRS does not include %PATH%. So if your DLL delay-imports from DLL on %PATH% - this will not work.