diff --git a/src/utility.cpp b/src/utility.cpp index 0f6bf728..ccf855f0 100644 --- a/src/utility.cpp +++ b/src/utility.cpp @@ -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); +#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. @@ -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(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) ) diff --git a/src/vld.cpp b/src/vld.cpp index 314c0979..4f5dc9a7 100644 --- a/src/vld.cpp +++ b/src/vld.cpp @@ -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( + InterlockedCompareExchange(&g_arm64TeardownThreadId, 0, 0)); + return teardownThreadId != 0 && teardownThreadId == GetCurrentThreadId(); +} + +struct Arm64TeardownScope +{ + Arm64TeardownScope() + { + InterlockedExchange(&g_arm64TeardownThreadId, static_cast(GetCurrentThreadId())); + } + + ~Arm64TeardownScope() + { + InterlockedExchange(&g_arm64TeardownThreadId, 0); + } +}; #endif #define _DECL_DLLMAIN // for _CRT_INIT @@ -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 @@ -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 @@ -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;