diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index 9c5920af..20058a8b 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -409,6 +409,16 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/dtor_suppression_test/dtor_suppression_tes FAIL_REGULAR_EXPRESSION "Visual Leak Detector detected [0-9]+ memory leak") endif() +# ============================================================================ +# ARM64 process-exit teardown hang regression test +# ============================================================================ +if(VLD_PLATFORM STREQUAL "ARM64") + add_vld_test(arm64_teardown_hang_test + SOURCES arm64_teardown_hang_test/arm64_teardown_hang_test.cpp + ) + set_property(TEST arm64_teardown_hang_test PROPERTY TIMEOUT 15) +endif() + # ============================================================================ # Dynamically loaded module symbol resolution regression test # diff --git a/src/tests/arm64_teardown_hang_test/arm64_teardown_hang_test.cpp b/src/tests/arm64_teardown_hang_test/arm64_teardown_hang_test.cpp new file mode 100644 index 00000000..f6ff0c95 --- /dev/null +++ b/src/tests/arm64_teardown_hang_test/arm64_teardown_hang_test.cpp @@ -0,0 +1,153 @@ +#include + +#include +#include + +#include "vld.h" + +namespace +{ +using QueryVirtualMemoryInformationFunction = decltype(&QueryVirtualMemoryInformation); + +volatile LONG g_blockQueryVirtualMemoryInformation = 0; +QueryVirtualMemoryInformationFunction g_originalQueryVirtualMemoryInformation = nullptr; +void* volatile g_leak = nullptr; + +BOOL WINAPI BlockingQueryVirtualMemoryInformation( + HANDLE process, + const VOID* virtualAddress, + WIN32_MEMORY_INFORMATION_CLASS memoryInformationClass, + PVOID memoryInformation, + SIZE_T memoryInformationSize, + PSIZE_T returnSize) +{ + // A teardown regression becomes a bounded CTest timeout instead of relying + // on the intermittent OS livelock that originally exposed this path. + if (InterlockedCompareExchange(&g_blockQueryVirtualMemoryInformation, 0, 0) != 0) + { + Sleep(INFINITE); + } + + return g_originalQueryVirtualMemoryInformation( + process, + virtualAddress, + memoryInformationClass, + memoryInformation, + memoryInformationSize, + returnSize); +} + +bool ReplaceVldQueryVirtualMemoryInformationImport() +{ + HMODULE vldModule; + if (!GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&VLDEnable), + &vldModule)) + { + return false; + } + + auto* imageBase = reinterpret_cast(vldModule); + auto* dosHeader = reinterpret_cast(imageBase); + if (dosHeader->e_magic != IMAGE_DOS_SIGNATURE || dosHeader->e_lfanew <= 0) + { + return false; + } + + auto* ntHeaders = reinterpret_cast( + imageBase + static_cast(dosHeader->e_lfanew)); + if (ntHeaders->Signature != IMAGE_NT_SIGNATURE) + { + return false; + } + + const IMAGE_DATA_DIRECTORY& importDirectory = + ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + if (importDirectory.VirtualAddress == 0) + { + return false; + } + + auto* importDescriptor = reinterpret_cast( + imageBase + importDirectory.VirtualAddress); + for (; importDescriptor->Name != 0; ++importDescriptor) + { + if (importDescriptor->OriginalFirstThunk == 0) + { + continue; + } + + auto* nameThunk = reinterpret_cast( + imageBase + importDescriptor->OriginalFirstThunk); + auto* addressThunk = reinterpret_cast( + imageBase + importDescriptor->FirstThunk); + + for (; nameThunk->u1.AddressOfData != 0; ++nameThunk, ++addressThunk) + { + if (IMAGE_SNAP_BY_ORDINAL(nameThunk->u1.Ordinal)) + { + continue; + } + + auto* importByName = reinterpret_cast( + imageBase + nameThunk->u1.AddressOfData); + if (std::strcmp( + reinterpret_cast(importByName->Name), + "QueryVirtualMemoryInformation") != 0) + { + continue; + } + + DWORD oldProtection; + if (!VirtualProtect( + &addressThunk->u1.Function, + sizeof(addressThunk->u1.Function), + PAGE_READWRITE, + &oldProtection)) + { + return false; + } + + g_originalQueryVirtualMemoryInformation = + reinterpret_cast( + addressThunk->u1.Function); + InterlockedExchangePointer( + reinterpret_cast(&addressThunk->u1.Function), + reinterpret_cast(&BlockingQueryVirtualMemoryInformation)); + + DWORD ignoredProtection; + if (!VirtualProtect( + &addressThunk->u1.Function, + sizeof(addressThunk->u1.Function), + oldProtection, + &ignoredProtection)) + { + return false; + } + return true; + } + } + + return false; +} +} + +int main() +{ + VLDEnable(); + g_leak = std::malloc(64); + if (g_leak == nullptr) + { + return 1; + } + + if (!ReplaceVldQueryVirtualMemoryInformationImport()) + { + return 2; + } + + InterlockedExchange(&g_blockQueryVirtualMemoryInformation, 1); + ExitProcess(0); +} 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;