Skip to content

[cDAC] Fix bug in GetThreadData#126592

Open
rcj1 wants to merge 7 commits intodotnet:mainfrom
rcj1:fix-thread-state-bug
Open

[cDAC] Fix bug in GetThreadData#126592
rcj1 wants to merge 7 commits intodotnet:mainfrom
rcj1:fix-thread-state-bug

Conversation

@rcj1
Copy link
Copy Markdown
Contributor

@rcj1 rcj1 commented Apr 6, 2026

We have to ensure that the thread is reported as dead when either Dead or ReportDead are set. There are, however, certain cases where it is useful to distinguish between the two, for example GetThreadOwningMonitorLock.

Copilot AI review requested due to automatic review settings April 6, 2026 23:14
@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.

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 fixes cDAC thread “dead” semantics by treating ThreadState.ReportDead as dead for enumeration and IsThreadMarkedDead, while still allowing callers to distinguish Dead vs ReportDead when needed (e.g., monitor-lock ownership queries).

Changes:

  • Update thread enumeration and IsThreadMarkedDead to consider ReportDead as dead.
  • Extend the managed thread contract to surface ReportDead and convert runtime state bits into contract flags.
  • Adjust Thread contract documentation to reflect that raw runtime state requires conversion to the contract enum.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs Treat ReportDead threads as dead for enumeration and IsThreadMarkedDead.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs Add mapping from runtime Thread::State bits (incl. TS_ReportDead) into contract ThreadState.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs Add ThreadState.ReportDead to the public contract enum.
docs/design/datacontracts/Thread.md Note that Thread::State should be converted to the contract enum rather than used as a raw integer.

Background = 0x00000200, // Thread is a background thread
Unstarted = 0x00000400, // Thread has never been started
Dead = 0x00000800, // Thread is dead
ReportDead = 0x00001000, // Thread is dead for the purposes of the debugger
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What does it mean "for the purpose of the debugger"?

Copy link
Copy Markdown
Contributor Author

@rcj1 rcj1 Apr 7, 2026

Choose a reason for hiding this comment

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

We set ReportDead before Dead to indicate that a thread is done with useful work and is in cleanup - that is, not interesting to a debugger.

th->SetThreadState(Thread::TS_ReportDead);

Do you think we should or can combine the two under Dead? Aside from the debugger, we use the flag mostly for GC-related checks, and I'm not 100% sure if it'd be safe to do so.

Copy link
Copy Markdown
Member

@jkotas jkotas Apr 8, 2026

Choose a reason for hiding this comment

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

I would like to see the flag to be described in terms of invariants that it guarantees.

I understand Dead. it means that the thread is not going to run any managed code (nor native runtime code - except the very small amount of non-interesting unmanaged runtime code between this flag being set and return from the thread exit callback).

What are the invariants for ReportDead? I do not see anything that prevents ReportDead threads running managed code. If it has a fuzzy meaning "the thread is probably not going to do any interesting work and it is probably going to exit soon", we should document it as such. I wish we can come up with something more precise. I would be fine with adjusting the implementation to fit the more precise definition.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Do you think we should or can combine the two under Dead?

Or delete the ReportDead flag. I am not sure.

You can give it a try and see what breaks to understand what the ReportFlag flag is actually used for.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ReportDead is set in three places; immediately before thread destruction

th->SetThreadState(Thread::TS_ReportDead);
, "on the main thread of an EXE, after that thread has finished doing useful work" before process termination
pCurThread->SetThreadState(Thread::TS_ReportDead);
, and on thread detach
SetThreadState((Thread::ThreadState)(Thread::TS_Detached | Thread::TS_ReportDead));
. I am hesitant to remove it because this would mean the debugger would see these threads that are potentially in the middle of cleanup, and try to make sense of their structures. I believe we can document ReportDead as "this thread will not do more interesting work" and leave it at that.

Copy link
Copy Markdown
Member

@jkotas jkotas Apr 10, 2026

Choose a reason for hiding this comment

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

How about this:

  • Delete the confusing remapping of TS_ReportDead to TS_Dead from GetSnapshotState and adjust callers accordingly. It should be fine to assume that any TS_Dead thread is TS_ReportedDead as well, so it should be sufficient to check for TS_ReportedDead instead of both bits. It is possible that cDAC won't actually need TS_Dead after this cleanup, I am not able to tell.

  • Description for TS_ReportDead: TS_ReportDead is set when the thread starts shutting down and it is not expected to run managed code anymore. It corresponds to ThreadState.Stopped reported by the managed Thread.ThreadState API. Would it make sense to rename TS_ReportDead to TS_Stopped to make it clear that it is same as managed ThreadState.Stopped?

  • Description for TS_Dead: TS_Dead is set after the .NET runtime finished shutting down the thread, and the OS is about to terminate the thread.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This sounds good.

Copy link
Copy Markdown
Member

@max-charlamb max-charlamb left a comment

Choose a reason for hiding this comment

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

lgtm

Copilot AI review requested due to automatic review settings April 10, 2026 17:43
@rcj1 rcj1 force-pushed the fix-thread-state-bug branch from 87ae76b to 6561442 Compare April 10, 2026 17:46
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

Copilot reviewed 16 out of 16 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

src/coreclr/vm/comsynchronizable.cpp:408

  • ThreadNative_GetThreadState now reads the raw m_State via GetState(), but still only treats TS_Dead as "stopped". Previously GetSnapshotState() ORed TS_Dead when the reporting bit was set (now TS_Stopped), so this will fail to report shutting-down threads as stopped. Update the condition to treat TS_Stopped (and likely TS_Dead as well) as ThreadStopped to preserve behavior.
    // grab a snapshot
    Thread::ThreadState state = thread->GetState();

    if (state & Thread::TS_Dead)
        res |= ThreadNative::ThreadStopped;

Copilot AI review requested due to automatic review settings April 10, 2026 21:22
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

Copilot reviewed 17 out of 17 changed files in this pull request and generated 1 comment.

// So ensure we mark the thread as dead before we send the tool notifications.
// The TS_ReportDead bit will cause the debugger to view this as TS_Dead.
_ASSERTE(HasThreadState(TS_ReportDead));
// The TS_Stopped bit will cause the debugger to view this as TS_Dead.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
// The TS_Stopped bit will cause the debugger to view this as TS_Dead.

Debugger sees TS_Stopped as TS_Stopped

Copy link
Copy Markdown
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

Thanks!

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.

4 participants