Repository navigation
[MrBot] ARM64: fix intermittent process-exit hang in VLD teardown leak report [Ready] - #65
Conversation
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
|
@microsoft-github-policy-service rerun |
2 similar comments
|
@microsoft-github-policy-service rerun |
|
@microsoft-github-policy-service rerun |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
• High — process-global teardown flag can deadlock other threads. g_arm64InTeardown ( src/vld.cpp:243-252 ) affects every thread. While the teardown thread holds the loader lock, other threads calling GetCallingModule() ( src/utility.cpp:1516-1533 ) switch to GetModuleHandleExW and block on that lock. waitForAllVLDThreads() can then time out and free VLD state before those threads resume, causing deadlock or use-after-free. Scope the fallback to the teardown thread, e.g. store its thread ID and require GetCurrentThreadId() to match. No other actionable findings. |
|
🤖 MrBot: Addressed in e0aeff7 by replacing the process-wide teardown flag with the teardown thread ID. |
|
/azp run |
|
Commenter does not have sufficient privileges for PR 65 in repo Azure/vld |
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Under concurrent ctest -j load on ARM64, a unit-test process can wedge forever
after main returns. Full-memory dumps of the wedged *_ut_exe_ebs.exe show
thread 0 (single-threaded post-main) stuck in NtQueryVirtualMemory <-
KERNELBASE!QueryVirtualMemoryInformation, reached from VLD's leak report that
runs under the loader lock at DLL_PROCESS_DETACH:
~VisualLeakDetector -> leak report (pre-warm GetLeaksCount + ReportLeaks)
-> GetCallingModule() -> QueryVirtualMemoryInformation -> NtQueryVirtualMemory
On ARM64 that per-address memory-region query intermittently livelocks under
the loader lock at process exit (aka.ms/AA10dvw4); on x64 it does not. It can
strike ANY VLD-linked exe under concurrent -j load, not a specific test. The
existing Arm64TeardownReportScope only suppressed dbghelp symbolization, not the
GetCallingModule -> QueryVirtualMemoryInformation path that actually wedges.
GetCallingModule() is reached during teardown from two kinds of caller:
(a) VLD's allocation hooks -> CaptureContext::~CaptureContext ->
IsExcludedModule() (for the report's own transient allocations), and
(b) CallStack::resolve()/resolveFunction() -> GetCallingModule() per frame,
driven by the pre-warm's call-stack resolution.
Fix (ARM64-only, non-weakening), all in src/vld.cpp + src/utility.cpp:
1. GetCallingModule(): during the ARM64 teardown report, look the module up via
the loader (GetModuleHandleExW FROM_ADDRESS) instead of
QueryVirtualMemoryInformation/VirtualQuery. The loader walk cannot reach
NtQueryVirtualMemory and is safe under the (already-held) loader lock. For a
code address this returns the same module base; for a non-module address it
returns NULL, which every caller already treats as 'not a tracked module'.
Gated by a new broad g_arm64InTeardown flag set BEFORE the pre-warm (separate
from g_arm64InTeardownReport, which must stay off during the pre-warm so
suppression names can still be resolved). This covers caller (b) and (a).
2. ~CaptureContext(): when leak detection is disabled on this thread or the
thread holds the DbgHelp lock, skip IsExcludedModule() (the inner heap hooks
already recorded no block, so nothing is mapped either way). Mirrors the
existing _HeapAlloc guard; removes redundant GetCallingModule calls -- caller (a).
3. ~VisualLeakDetector(): disable leak detection on the teardown thread across
the pre-warm + ReportLeaks(), restoring it before TLS is torn down, so the
report's own transient allocations are not tracked.
The reported leak set is unchanged (guarded allocations were never going to be
mapped; the loader lookup returns the same module base for code addresses).
x64/x86 behavior is untouched. Verified on ARM64 (Cobalt) hardware by looping
the gate's full unit-test ctest -j16 suite: unfixed wedges on pass 1, fixed
stays green.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e0aeff7 to
80003f5
Compare
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
|
we should have an int test that
|
|
Mr. Bot, as Andrei suggests, create a minimal repro. Locally prove that it hangs deterministically with ARM64 without the fix and then ensure it passes with this fixed version. The repro int test must be run as part of the build yml on ARM64. |
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
🤖 MrBot: Addressed in 3aba07c. I added an ARM64-only CTest integration repro that patches VLD's The test source cross-compiles for ARM64 with |
This was just while we fix the "require two human reviewers" bug
|
/azp run |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Matt Durak (mattdurak)
left a comment
There was a problem hiding this comment.
sign off as driver, waiting on second review
Pull request was closed
|
Azure Pipelines: There may be pipelines that require an authorized user to comment /azp run to run. |
Summary
On ARM64, a process that links VLD can hang forever after
mainreturns —inside VLD's leak report at
DLL_PROCESS_DETACH, under the loader lock. Thissurfaced in Azure-MessagingStore's gated ARM64 unit-test leg: under
concurrent
ctest -jload,*_ut_exe_ebs.exeprocesses intermittently wedgedat process exit and stalled the whole leg until the job timeout. It is not
specific to any one test — three different test exes wedged simultaneously in
one prior repro.
Addresses ADO Task 38853991
(https://msazure.visualstudio.com/One/_workitems/edit/38853991).
Root cause (confirmed from crash dumps + source)
Full-memory dumps of the wedged processes (single-threaded post-
main) showthread 0 stuck in:
~VisualLeakDetectorruns the leak report under the loader lock.GetCallingModule()resolves the owning module of an address using
QueryVirtualMemoryInformation(and
VirtualQueryas a fallback) — both of which reachNtQueryVirtualMemory.On ARM64, under the loader lock at process exit, that per-address memory-region
query intermittently livelocks (documented dbghelp/ARM64 interaction,
aka.ms/AA10dvw4); on x64 it does not.
During teardown
GetCallingModule()is reached from two kinds of caller, soguarding only one is not enough:
CaptureContext::~CaptureContext→IsExcludedModule(), for allocations the report itself makes (dbghelp'sinternal allocations during symbol resolution; VLD's report bookkeeping).
CallStack::resolve()/resolveFunction()→GetCallingModule()per frame, driven by the ARM64 pre-warm
GetLeaksCount()that resolvescall stacks to decide leak suppression.
The pre-existing
Arm64TeardownReportScopeonly suppressed dbghelpsymbolization; it did nothing about either
GetCallingModulepath.The fix and regression coverage
GetCallingModule()uses a loader-based lookup on the ARM64 teardownthread. The teardown thread ID is published before the pre-warm (so it
covers caller (b), unlike
Arm64TeardownReportScopewhich must stay off duringthe pre-warm so suppression names can be resolved).
GetCallingModule()usesthe fallback only when the current thread matches that ID, so concurrently
active threads retain the normal non-loader lookup. On the teardown thread it
resolves the module with
GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | …_UNCHANGED_REFCOUNT)— a loader-list walk that cannot reach
NtQueryVirtualMemoryand is safeunder the already-held loader lock. For a code address (every call-stack frame)
it returns the same module base; for a non-module address it returns
NULL,which every caller already treats as “not a tracked module.” This is the
change that removes the hang.
~CaptureContext(): when leak detection is disabled on this thread or thethread already holds the DbgHelp lock, skip
IsExcludedModule()— the innerheap hooks already recorded no block, so nothing is mapped either way. Mirrors
the existing
_HeapAllocguard; removes redundantGetCallingModulecalls(caller (a)).
~VisualLeakDetector(): disable leak detection on the teardown threadacross the pre-warm +
ReportLeaks(), restoring it before thread-local storageis torn down later in the same destructor, so the report’s own transient
allocations are not tracked.
QueryVirtualMemoryInformationentry with a blocking function immediatelybefore process exit. The unfixed teardown path deterministically times out;
the fixed loader-based path never reaches the hook and exits normally. CMake
registers the test only for ARM64 with a 15-second timeout, so the existing
ARM64
cteststeps run it in both Debug and RelWithDebInfo builds.Non-weakening / same reported leaks: the loader lookup returns the same module
base for code addresses; the guarded allocations in (2)/(3) were never going to be
mapped. No assertion or leak check is disabled or relaxed. x64/x86 behavior is
untouched (every change is under
#if defined(_M_ARM64)).Verification — native ARM64 hardware (Azure Cobalt
Standard_D16plds_v6)Using the gate's own published ARM64 unit-test binaries (build 172760233,
273 test exes) run with the gate command
ctest -C Debug -j 16(a per-test--timeoutreaps a stuck-at-exit process so a wedge shows up as a Timeout).Baseline and fix were both built from source with the identical
RelWithDebInfoconfig (same dll size; only this change differs), so the fix isthe only variable:
master@a5e71c7: wedged on pass 1 (a testprocess failed to exit and was reaped by
--timeout). Matches the captureddumps.
ctest -j16passes were green — 100% of the 273 tests passed on every pass, ~161 s per
pass, zero exit-time wedges. (Run as a 3-pass warm-up plus 17 more,
back-to-back with the same fixed dll.)
warnings enabled. Its import hook also deterministically blocks the equivalent
unfixed lookup path in a local x64 harness, while the PR's ARM64 pipeline runs
the fixed-path assertion.