Skip to content

Don't dlclose the legacy DAC on non-Windows - #1499

Merged
Max Charlamb (max-charlamb) merged 1 commit into
microsoft:mainfrom
hoyosjs:juhoyosa/dont-unload-dac
Jul 21, 2026
Merged

Don't dlclose the legacy DAC on non-Windows#1499
Max Charlamb (max-charlamb) merged 1 commit into
microsoft:mainfrom
hoyosjs:juhoyosa/dont-unload-dac

Conversation

@hoyosjs

Copy link
Copy Markdown
Member

Intermittent (~15-20%) SIGSEGV in the test host on Linux, surfacing as Test host process crashed / aborted runs rather than test failures. Faulting thread:

#6  0x...21edc0 in ?? ()
#7  __GI___nptl_deallocate_tsd () at nptl_deallocate_tsd.c:73
#8  start_thread

The DAC embeds the PAL. ClrMD brings the PAL up by calling the DAC's exported DllMain, which initializes TLS, registering a pthread key with a destructor in the DAC. That destructor pointer lives in glibc's per-thread key table, not in the DAC. RefCountedFreeLibrary.Release dlcloses the DAC on DataTarget dispose, but the pthread key is never deleted. Any host thread that entered the DAC's PAL and later exits jumps into the now-unmapped destructor from __nptl_deallocate_tsd.

The existing suppression only covered inspecting the current process, so dump-file targets still unloaded the DAC.

Why not tear the PAL down instead of leaking?

There is no in-process, reversible PAL teardown to call before dlclose:

  • The DAC's own DllMain(DLL_PROCESS_DETACH) does not touch the PAL - it only destroys the dac mutex (daccess.cpp DllMain2). It never deletes the pthread key.
  • pthread_key_delete only runs via TLSCleanup, which is reachable only on PAL init-failure unwind or the process-terminate path.
  • The only public teardown entrypoint, PAL_TerminateEx, ends in exit(exitCode) - it shuts down and exits the whole process. Its sole caller is createdump, which is exiting anyway and passes an exit code. A long-lived host that uses ClrMD cannot call it.

Fix

Never free the DAC on non-Windows so the destructor remains mapped. Effectively leaks one mapping per unique DAC path.

Validation

Root cause proven by logging the DAC base + addr2line on the fault offset -> InternalEndCurrentThreadWrapper.

@max-charlamb
Max Charlamb (max-charlamb) merged commit 153fe21 into microsoft:main Jul 21, 2026
10 checks passed
Pedro Sakuma Travi (pedrosakuma) added a commit to pedrosakuma/dotnet-diagnostics that referenced this pull request Jul 24, 2026
* chore: bump Microsoft.Diagnostics.Runtime (ClrMD) 3.1.512801 -> 4.0.732401

Upgrades to the latest published ClrMD release so we can pick up a fix
faster once one ships. microsoft/clrmd#1499 ("Don't dlclose the legacy
DAC on non-Windows", merged 2026-07-21) plausibly fixes our Linux CI
host-crash flake (issue #147 / dotnet/runtime#128525): its root cause
(TLS destructor left dangling after dlclose-ing the DAC, crashing an
unrelated thread in __nptl_deallocate_tsd on exit) matches our own
findings (unmanaged runtime thread, deterministic low-offset fault in
an unmapped thread-stack region).

No published NuGet package includes the fix yet (latest is 4.0.732401,
published 2026-06-25, before the merge), so this is purely a version
bump to the current major to shorten the upgrade path later — no
functional change expected, and the flake still reproduces locally on
this version as expected.

Verified: dotnet build DotnetDiagnostics.slnx -c Release (0 errors) and
dotnet test tests/DotnetDiagnostics.Core.Tests (825 passed, 1 skipped,
same known host-crash flake, no real failures).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test: tolerate ClrMD 4.0 taking the direct ThreadPool path on Windows

ClrMD 4.0.732401's runtime.ThreadPool now succeeds during live attach on
Windows (previously null with 3.1.512801, forcing the lightweight
thread-snapshot fallback). ThreadSnapshot_InspectLive_CapturesThreadPoolSnapshot
hardcoded the fallback-only note ("heap-wide walks"); on Windows CI the
direct path now runs instead, which still avoids a heap-wide local-queue
owner walk during live suspended capture and says so via a different note
("heap-wide scans", see ClrMdThreadSnapshotInspector.cs CaptureLocalQueues).

Widen the assertion to accept either note — which code path ClrMD takes is
a runtime/ClrMD-version implementation detail, not part of this test's
contract. No production code changed; both paths were already correct.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hoyosjs
Juan Hoyos (hoyosjs) deleted the juhoyosa/dont-unload-dac branch September 3, 2026 22:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants