Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions src/utility.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
extern ReportHookSet* g_pReportHooks;
extern VisualLeakDetector g_vld;
extern ImageDirectoryEntries g_Ide;
#if defined(_M_ARM64)
extern "C" bool VldArm64InTeardown(void);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

VldArm64InTeardown

we should have an int test that

  1. would hang on "previous" SW version. Would be awesome to have the test deterministic.

  2. would not hang with the proposed.

Probably the bot can write it, there are such bugs happening which were the trigger for this fix.

#endif

// Global variables.
static BOOL s_reportDelay = FALSE; // If TRUE, we sleep for a bit after calling OutputDebugString to give the debugger time to catch up.
Expand Down Expand Up @@ -1510,6 +1513,26 @@ HMODULE GetCallingModule(UINT_PTR pCaller )
{
HMODULE hModule = NULL;

#if defined(_M_ARM64)
if (VldArm64InTeardown())
{
// During ARM64 process-exit teardown the loader lock is held and the
// QueryVirtualMemoryInformation / VirtualQuery calls below reach
// NtQueryVirtualMemory, which intermittently livelocks (aka.ms/AA10dvw4).
// Use a loader-based lookup that walks the loaded-module list instead and
// cannot wedge. For a code address (every call-stack frame) this returns
// the same module base; for a non-module address it returns NULL, which
// all callers already treat as "not a tracked module".
HMODULE hMod = NULL;
if (GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(pCaller), &hMod))
{
return hMod;
}
return NULL;
}
#endif

// Try QueryVirtualMemoryInformation first (Windows 10 1607+)
WIN32_MEMORY_REGION_INFORMATION memoryRegionInfo;
if ( QueryVirtualMemoryInformation(GetCurrentProcess(), (LPCVOID)pCaller, MemoryRegionInfo, &memoryRegionInfo, sizeof(memoryRegionInfo), NULL) )
Expand Down
77 changes: 77 additions & 0 deletions src/vld.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,31 @@ struct Arm64TeardownReportScope
Arm64TeardownReportScope() { InterlockedIncrement(&g_arm64InTeardownReport); }
~Arm64TeardownReportScope() { InterlockedDecrement(&g_arm64InTeardownReport); }
};

// The ARM64 teardown thread uses a loader-based module lookup during the entire
// leak report, including the pre-warm GetLeaksCount(). Other threads must keep
// using the normal lookup because they do not hold the loader lock.
static volatile LONG g_arm64TeardownThreadId = 0;

extern "C" bool VldArm64InTeardown(void)
{
DWORD teardownThreadId = static_cast<DWORD>(
InterlockedCompareExchange(&g_arm64TeardownThreadId, 0, 0));
return teardownThreadId != 0 && teardownThreadId == GetCurrentThreadId();
}

struct Arm64TeardownScope
{
Arm64TeardownScope()
{
InterlockedExchange(&g_arm64TeardownThreadId, static_cast<LONG>(GetCurrentThreadId()));
}

~Arm64TeardownScope()
{
InterlockedExchange(&g_arm64TeardownThreadId, 0);
}
};
#endif

#define _DECL_DLLMAIN // for _CRT_INIT
Expand Down Expand Up @@ -651,6 +676,32 @@ VisualLeakDetector::~VisualLeakDetector ()
g_vldDllNotificationCookie = NULL;
}
#if defined(_M_ARM64)
// [ARM64 process-exit teardown hang fix] Route GetCallingModule() through a
// loader-based module lookup for the whole leak report. Set BEFORE the
// pre-warm below because the pre-warm's call-stack resolution asks for the
// module of every frame; on ARM64 the QueryVirtualMemoryInformation /
// VirtualQuery path that GetCallingModule() normally uses reaches
// NtQueryVirtualMemory, which intermittently livelocks under the loader
// lock at process exit (aka.ms/AA10dvw4). (This is separate from
// Arm64TeardownReportScope, which only suppresses dbghelp symbolization and
// must stay OFF during the pre-warm so suppression names can be resolved.)
Arm64TeardownScope arm64TeardownGetCallingModuleScope;

// [ARM64 process-exit teardown hang fix] Disable leak detection on THIS
// (teardown) thread across the pre-warm and the leak report below. Both
// allocate: dbghelp allocates internally during symbol resolution, and
// VLD's own ReportLeaks() bookkeeping allocates through the heap hooks.
// With detection disabled those transient allocations bypass the
// allocation hooks instead of being tracked; tracking them would call
// GetCallingModule() -> QueryVirtualMemoryInformation() per allocation,
// which intermittently wedges forever on ARM64 under the loader lock at
// process exit (aka.ms/AA10dvw4). Restored right after ReportLeaks(),
// while thread-local storage is still valid (it is torn down later in
// this destructor). Non-weakening: the report only reads the block map
// already populated during the run; nothing user-relevant allocates
// after main returns.
DisableLeakDetection();

// Pre-warm the suppression cache while dbghelp is still usable. The ARM64
// teardown scope below disables dbghelp symbol resolution because the
// bundled ARM64 dbghelp spins under the loader lock at shutdown; but the
Expand Down Expand Up @@ -706,6 +757,13 @@ VisualLeakDetector::~VisualLeakDetector ()
}
}

#if defined(_M_ARM64)
// Re-enable leak detection now that the ARM64 leak report is complete and
// before this destructor tears down thread-local storage below (pairs
// with the DisableLeakDetection() before the pre-warm above).
RestoreLeakDetectionState();
#endif

// Free resources used by the symbol handler.
DbgTrace(L"dbghelp32.dll %i: SymCleanup\n", GetCurrentThreadId());
// SymCleanup is safe to call on ARM64 even during teardown: empirically
Expand Down Expand Up @@ -3263,6 +3321,25 @@ CaptureContext::~CaptureContext() {
if (!m_bFirst)
return;

#if defined(_M_ARM64)
// [ARM64 process-exit teardown hang fix] Mirror the allocation-hook guards
// (see VisualLeakDetector::_HeapAlloc): when leak detection is disabled on
// this thread, or when this thread already holds the DbgHelp lock (a
// re-entrant allocation made from inside VLD's own dbghelp symbol
// resolution), the inner heap / RtlAllocateHeap hooks never recorded a block
// (m_tls->blockWithoutGuard stays NULL), so there is nothing to map here.
// Skip IsExcludedModule() as well: it calls GetCallingModule() ->
// QueryVirtualMemoryInformation(), a per-allocation memory-region query that
// intermittently wedges forever on ARM64 under the loader lock at process
// teardown (aka.ms/AA10dvw4; see the ARM64 report handling in
// ~VisualLeakDetector). Such allocations are never user leaks, so this
// reports no different leak -- it only avoids the wedge-prone query.
if (!g_vld.enabled() || g_DbgHelp.IsLockedByCurrentThread()) {
Reset();
return;
}
#endif

BOOL excluded = IsExcludedModule();
if ((m_tls->blockWithoutGuard) && (!excluded)) {
blockinfo_t* pblockInfo = NULL;
Expand Down
Loading