[Bug 52017] New: LLDB keeps DLL loaded after FreeLibrary is called.

Bug ID 52017
Summary LLDB keeps DLL loaded after FreeLibrary is called.
Product lldb
Version 12.0
Hardware PC
OS Windows NT
Status NEW
Severity normal
Priority P
Component All Bugs
Assignee lldb-dev@lists.llvm.org
Reporter george.owen@savoch.net
CC jdevlieghere@apple.com, llvm-bugs@lists.llvm.org

Created attachment 25307 [details]
Test case

Repro:
 * Build a shared library DLL
 * Create an executable that does not link that DLL
 * Load a DLL in code using LoadLibrary
 * Unload the same DLL using FreeLibrary on the HMODULE obtained from the first
step
 * Observe that LLDB does not release its lock on the DLL file, until LLDB is
closed.

LLDB keeps DLL loaded (and therefore the file locked) even after the
FreeLibrary is called. This means that the DLL, and PDB, both remain locked and
unable to be modified, even after they are no longer required by LLDB.

This can be observed by building the attached test case and running it through
LLDB. Once the program is either breakpointed or paused after the FreeLibrary
call, you can either attempt to delete the files and observe they cannot be
deleted, or use a program such as Process Explorer
([https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer](https://docs.microsoft.com/en-us/sysinternals/downloads/process-explorer)) to
see the processes that keep a handle to the file. This is only released when
LLDB is closed.

Debugging the same test case with Visual Studio 2019 (Version 16.9.4) shows
that Visual Studio does not keep the DLL or PDB locked after FreeLibrary is
called.

After SBDebugger::Destroy(), Terminate() there are still file mappings are active for loaded modules. MapViewOfFile was called, but UnmapViewOfFile not/or DuplicateHandler is still alive. (it’s in Windows\Path.inc).

Anyway, after unmapping all that stuff and closing duplicated handles - modules were unlocked.
LLVM 13.0.1

lldb caches the modules it has read between runs in so that subsequent reruns are fast. This can be a significant speedup for repeat debugging sessions with apps that load lots of shared libraries. If you want to force all un-referenced modules to be discarded after a session, you can call SBDebugger.MemoryPressureDetected. If that doesn’t free up all these file mappings, then we should fix that. But the fact that lldb keeps references to the previous shared sessions loaded libraries otherwise is by design.

MemoryPressureDetected() does nothing about this issue. I tried “target delete -ac” instead (it sets flag “true” to force removing shared modules from memory), but also without success.
It would be nice to have some configuration flag - to keep modules in memory or not after SBDebugger::Destroy()/Terminate().

That’s the bug, right? Either of these methods should have freed these modules and closed the file handles to them, but they didn’t. Another configuration flag would just call into the same non-working code…

Found additional info about this problem in lldb\source\Core\ModuleList.cpp:

static SharedModuleListInfo &GetSharedModuleListInfo()
{
static SharedModuleListInfo *g_shared_module_list_info = nullptr;
static llvm::once_flag g_once_flag;
llvm::call_once(g_once_flag, {
// NOTE: Intentionally leak the module list so a program doesn’t have to
// cleanup all modules and object files as it exits. This just wastes time
// doing a bunch of cleanup that isn’t required.

I checked - after Destoroy/Terminate g_shared_module_list_info really contains element.
It seems if some references weren’t cleaned and/or you have smthing in your code which can use shared_ptr => destructors aren’t called, so modules aren’t unloaded.
So, this part of code should be reworked.

This seems to be specific to Windows. If I try the same thing on macOS, with a little program like:

#include <dlfcn.h>
#include <stdio.h>

int
main()
{
  void *handle = dlopen("/tmp/libmylib.dylib", 0);
  if (!handle) {
    printf("Failed to dlopen file.\n");
    return 0;
  }
  printf("dlopen succeeds.\n");
  dlclose(handle);

  return 0;
}

If I run to the “dlopen succeeds” printf, I can see libmylib in the image list output. When I run to the final “return”, it isn’t in the image list output - which is correct - but it is in image list -g because we cache parsed shared libraries. If I run script lldb.debugger.MemoryPressureDetected() to clear that cache, then image list -g no longer shows libmylib.dylib, and it doesn’t show up in the “open files” for lldb either; I ran an external tool on the lldb process to check this.

Something in the Windows DynamicLoader code must be holding an extra reference to the dylib file, the behavior of the macOS side shows that the accounting in the generic code is good.