Skip to content

[clr-interp] Release DbgTransportTarget RSLock on static destruction#127754

Open
kotlarmilos wants to merge 1 commit intodotnet:mainfrom
kotlarmilos:di-shutdown-leak-fix
Open

[clr-interp] Release DbgTransportTarget RSLock on static destruction#127754
kotlarmilos wants to merge 1 commit intodotnet:mainfrom
kotlarmilos:di-shutdown-leak-fix

Conversation

@kotlarmilos
Copy link
Copy Markdown
Member

@kotlarmilos kotlarmilos commented May 4, 2026

Description

The static g_DbgTransportTarget instance is destroyed after the host process's main() returns. On unix (desktop interpreter) the dynamic loader does not invoke DbgDllMain(DLL_PROCESS_DETACH) for mscordbi.dylib, so DbgTransportTarget::Shutdown() may never run and the embedded RSLock m_sLock is left in its initialized state. In debug builds the RSLock destructor asserts that the lock was Destroy()'d before destruction, so the host process aborts.

This affects every debugger test that exits cleanly through the static destructor path. Locally on osx-arm64 Debugger.Tests --clrinterpreter the xunit fail count drops from 230 to 65:

  • Inspection.BasicInspection, Inspection.EnumNames, Inspection.GenericInterfaces, Inspection.GenericSharedStatic, Inspection.GenericStatics, Inspection.NestedGenerics, Inspection.inspect_SetVars
  • Stackwalking.ClrMethods
  • Stepping.setIPTestwithUnvaildPos, Stepping.stepin, Stepping.stepover
  • Async.AsyncStepInto

@dotnet-policy-service
Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@kotlarmilos kotlarmilos changed the title [debug/di] Release DbgTransportTarget RSLock on static destruction [interp] Release DbgTransportTarget RSLock on static destruction May 4, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a defensive DbgTransportTarget destructor so the right-side debug transport can clean up its lock and tracked sessions during static destruction, covering Unix teardown paths where DbgDllMain(DLL_PROCESS_DETACH) may not run.

Changes:

  • Declares an explicit destructor for DbgTransportTarget.
  • Adds destructor logic that calls Shutdown() when the transport lock appears initialized.
  • Keeps the fix localized to the debug transport manager used by the CoreCLR debugger transport layer.
Show a summary per file
File Description
src/coreclr/debug/di/dbgtransportmanager.h Declares the new DbgTransportTarget destructor.
src/coreclr/debug/di/dbgtransportmanager.cpp Implements the destructor and conditionally invokes Shutdown() during static destruction.

Copilot's findings

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment thread src/coreclr/debug/di/dbgtransportmanager.cpp
@kotlarmilos kotlarmilos added this to the 11.0.0 milestone May 4, 2026
@kotlarmilos kotlarmilos changed the title [interp] Release DbgTransportTarget RSLock on static destruction [clr-interp][debug/di] Release DbgTransportTarget RSLock on static destruction May 4, 2026
@kotlarmilos kotlarmilos changed the title [clr-interp][debug/di] Release DbgTransportTarget RSLock on static destruction [clr-interp] Release DbgTransportTarget RSLock on static destruction May 4, 2026
The static g_DbgTransportTarget instance is destroyed after the host process's main() returns. On Unix the dynamic loader does not invoke DbgDllMain(DLL_PROCESS_DETACH) for mscordbi.dylib, so DbgTransportTarget::Shutdown() may never run and the embedded RSLock m_sLock is left in its initialized state. In debug builds the RSLock destructor asserts that the lock was Destroy()'d before destruction, so the host process aborts with 'Leaking Critical section for RS Lock DbgTransportTarget Lock' and exits 134. The test harness then marks every otherwise successful debugger test as a failure during teardown.

Add a defensive ~DbgTransportTarget() that invokes Shutdown() when the lock is still initialized.

This affects every debugger test that exits cleanly through the static destructor path. Locally on osx-arm64 Debugger.Tests --clrinterpreter the xunit fail count drops from 230 to 65, with the following tests no longer reported as failing on top of the existing baseline:
- Inspection.BasicInspection, Inspection.EnumNames, Inspection.GenericInterfaces, Inspection.GenericSharedStatic, Inspection.GenericStatics, Inspection.NestedGenerics, Inspection.inspect_SetVars
- Stackwalking.ClrMethods
- Stepping.setIPTestwithUnvaildPos, Stepping.stepin, Stepping.stepover
- Async.AsyncStepInto

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@kotlarmilos
Copy link
Copy Markdown
Member Author

As @janvorli noted, this is nice-to-have. The tests are intended to run primarily against release where the assert does not fire, so this only cleans up checked-build noise. The downside is that the order in which destructors of statics are executed is undefined, so one destructor can easily depend on something that was already destroyed by another.

@janvorli
Copy link
Copy Markdown
Member

janvorli commented May 5, 2026

I would prefer fixing the issue by not using a static instance of the DbgTransportTarget instead. When the ~DbgTransportTarget is invoked, there is no guarantee that the Shutdown() call would succeed. The call can in the future rely on something that would be already torn down at the point when the C runtime calls the destructor.

However, I can see that @AaronRobinsonMSFT has introduced this static destructor recently and removed dynamic allocation of the transport, so I'd like to know his opinion first.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants