From 7e88aa5a327b0981244776e86b23a225efa7063f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:05:40 +0000 Subject: [PATCH 1/4] Implement DacDbi thread APIs via Thread contract data and descriptors Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ebc56d23-ae7d-4d2c-b83d-d86d74c51e29 Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/ebc56d23-ae7d-4d2c-b83d-d86d74c51e29 Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- docs/design/datacontracts/Thread.md | 7 ++ .../vm/datadescriptor/datadescriptor.inc | 4 +- src/coreclr/vm/threads.h | 2 + .../Contracts/IThread.cs | 6 +- .../CorDbHResults.cs | 1 + .../Contracts/Thread_1.cs | 27 +++++- .../Data/Thread.cs | 6 ++ .../Dbi/DacDbiImpl.cs | 89 +++++++++++++++++- .../cdac/tests/ClrDataExceptionStateTests.cs | 8 ++ .../DumpTests/DacDbi/DacDbiThreadDumpTests.cs | 92 +++++++++++++++++-- .../MockDescriptors/MockDescriptors.Thread.cs | 24 +++++ 11 files changed, 249 insertions(+), 17 deletions(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index f1345105acb6aa..95c78c831e9938 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -37,7 +37,11 @@ record struct ThreadData ( TargetPointer Frame; TargetPointer FirstNestedException; TargetPointer TEB; + TargetPointer ExposedObjectHandle; TargetPointer LastThrownObjectHandle; + TargetPointer CurrentCustomDebuggerNotificationHandle; + bool LastThrownObjectIsUnhandled; + bool HasUnhandledException; TargetPointer NextThread; ); ``` @@ -98,7 +102,10 @@ The contract additionally depends on these data descriptors | `Thread` | `CachedStackBase` | Pointer to the base of the stack | | `Thread` | `CachedStackLimit` | Pointer to the limit of the stack | | `Thread` | `TEB` | Thread Environment Block pointer | +| `Thread` | `ExposedObject` | Handle to the managed `Thread` object exposed to the debugger | | `Thread` | `LastThrownObject` | Handle to last thrown exception object | +| `Thread` | `LastThrownObjectIsUnhandled` | Whether `LastThrownObject` should be treated as unhandled | +| `Thread` | `CurrentCustomDebuggerNotification` | Handle to the current custom debugger notification object | | `Thread` | `LinkNext` | Pointer to get next thread | | `Thread` | `ExceptionTracker` | Pointer to exception tracking information | | `Thread` | `RuntimeThreadLocals` | Pointer to some thread-local storage | diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 0ee65734c32da2..3c6308b072198f 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -48,10 +48,12 @@ CDAC_TYPE_FIELD(Thread, T_POINTER, DebuggerFilterContext, cdac_data::Deb #ifdef PROFILING_SUPPORTED CDAC_TYPE_FIELD(Thread, T_POINTER, ProfilerFilterContext, cdac_data::ProfilerFilterContext) #endif // PROFILING_SUPPORTED -CDAC_TYPE_FIELD(Thread, TYPE(GCHandle), GCHandle, cdac_data::ExposedObject) +CDAC_TYPE_FIELD(Thread, TYPE(GCHandle), ExposedObject, cdac_data::ExposedObject) CDAC_TYPE_FIELD(Thread, TYPE(GCHandle), LastThrownObject, cdac_data::LastThrownObject) +CDAC_TYPE_FIELD(Thread, T_UINT32, LastThrownObjectIsUnhandled, cdac_data::LastThrownObjectIsUnhandled) CDAC_TYPE_FIELD(Thread, T_POINTER, LinkNext, cdac_data::Link) CDAC_TYPE_FIELD(Thread, T_POINTER, ThreadLocalDataPtr, cdac_data::ThreadLocalDataPtr) +CDAC_TYPE_FIELD(Thread, TYPE(GCHandle), CurrentCustomDebuggerNotification, cdac_data::CurrentCustomDebuggerNotification) #ifndef TARGET_UNIX CDAC_TYPE_FIELD(Thread, T_POINTER, TEB, cdac_data::TEB) CDAC_TYPE_FIELD(Thread, T_POINTER, UEWatsonBucketTrackerBuckets, cdac_data::UEWatsonBucketTrackerBuckets) diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index e120e0b3bdddc2..9220531f5b4998 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -3775,8 +3775,10 @@ struct cdac_data static constexpr size_t CachedStackLimit = offsetof(Thread, m_CacheStackLimit); static constexpr size_t ExposedObject = offsetof(Thread, m_ExposedObject); static constexpr size_t LastThrownObject = offsetof(Thread, m_LastThrownObjectHandle); + static constexpr size_t LastThrownObjectIsUnhandled = offsetof(Thread, m_ltoIsUnhandled); static constexpr size_t Link = offsetof(Thread, m_Link); static constexpr size_t ThreadLocalDataPtr = offsetof(Thread, m_ThreadLocalDataPtr); + static constexpr size_t CurrentCustomDebuggerNotification = offsetof(Thread, m_hCurrNotification); static_assert(std::is_same().m_ExceptionState), ThreadExceptionState>::value, "Thread::m_ExceptionState is of type ThreadExceptionState"); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs index 98d33c30a9254a..2fce01344d30dc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IThread.cs @@ -26,6 +26,7 @@ public enum ThreadState Unstarted = 0x00000400, // Thread has never been started Stopped = 0x00010000, // Thread has started to shut down ThreadPoolWorker = 0x01000000, // Thread is a thread pool worker thread + Detached = unchecked((int)0x80000000), // Thread was detached } public record struct ThreadData( @@ -39,7 +40,11 @@ public record struct ThreadData( TargetPointer Frame, TargetPointer FirstNestedException, TargetPointer TEB, + TargetPointer ExposedObjectHandle, TargetPointer LastThrownObjectHandle, + TargetPointer CurrentCustomDebuggerNotificationHandle, + bool LastThrownObjectIsUnhandled, + bool HasUnhandledException, TargetPointer NextThread); public interface IThread : IContract @@ -55,7 +60,6 @@ void GetStackLimitData(TargetPointer threadPointer, out TargetPointer stackBase, TargetPointer IdToThread(uint id) => throw new NotImplementedException(); TargetPointer GetThreadLocalStaticBase(TargetPointer threadPointer, TargetPointer tlsIndexPtr) => throw new NotImplementedException(); TargetPointer GetCurrentExceptionHandle(TargetPointer threadPointer) => throw new NotImplementedException(); - TargetPointer GetThrowableObject(TargetPointer threadPointer) => throw new NotImplementedException(); byte[] GetWatsonBuckets(TargetPointer threadPointer) => throw new NotImplementedException(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/CorDbHResults.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/CorDbHResults.cs index c152faa61e4f1a..34846f9fdaf8ce 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/CorDbHResults.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/CorDbHResults.cs @@ -6,6 +6,7 @@ namespace Microsoft.Diagnostics.DataContractReader; public static class CorDbgHResults { public const int CORDBG_E_NOTREADY = unchecked((int)0x80131c10); + public const int CORDBG_E_BAD_THREAD_STATE = unchecked((int)0x8013132d); public const int CORDBG_E_READVIRTUAL_FAILURE = unchecked((int)0x80131c49); public const int ERROR_BUFFER_OVERFLOW = unchecked((int)0x8007006F); // HRESULT_FROM_WIN32(ERROR_BUFFER_OVERFLOW) public const int CORDBG_E_CLASS_NOT_LOADED = unchecked((int)0x80131303); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs index fe2aa710303490..cf081b26b802df 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs @@ -26,7 +26,15 @@ private enum ThreadState_1 Background = 0x200, Unstarted = 0x400, Stopped = 0x10000, - ThreadPoolWorker = 0x1000000 + ThreadPoolWorker = 0x1000000, + Detached = unchecked((int)0x80000000) + } + + [Flags] + private enum ExceptionFlags + { + DebuggerInterceptInfo = 0x00000200, + IsUnhandled = 0x00000800, } internal Thread_1(Target target) @@ -73,6 +81,8 @@ private static Contracts.ThreadState GetThreadState(ThreadState_1 state) result |= Contracts.ThreadState.Stopped; if (state.HasFlag(ThreadState_1.ThreadPoolWorker)) result |= Contracts.ThreadState.ThreadPoolWorker; + if (state.HasFlag(ThreadState_1.Detached)) + result |= Contracts.ThreadState.Detached; return result; } @@ -82,12 +92,23 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) TargetPointer address = _target.ReadPointer(thread.ExceptionTracker); TargetPointer firstNestedException = TargetPointer.Null; + bool hasUnhandledException = false; if (address != TargetPointer.Null) { Data.ExceptionInfo exceptionInfo = _target.ProcessedData.GetOrAdd(address); firstNestedException = exceptionInfo.PreviousNestedInfo; + + if (exceptionInfo.ThrownObjectHandle != TargetPointer.Null) + { + uint exceptionFlags = exceptionInfo.ExceptionFlags; + hasUnhandledException = (exceptionFlags & (uint)ExceptionFlags.IsUnhandled) != 0 + && (exceptionFlags & (uint)ExceptionFlags.DebuggerInterceptInfo) == 0; + } } + if (thread.LastThrownObjectIsUnhandled != 0) + hasUnhandledException = true; + return new ThreadData( threadPointer, thread.Id, @@ -99,7 +120,11 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) thread.Frame, firstNestedException, thread.TEB, + thread.ExposedObject, thread.LastThrownObject.Handle, + thread.CurrentCustomDebuggerNotification, + thread.LastThrownObjectIsUnhandled != 0, + hasUnhandledException, GetThreadFromLink(thread.LinkNext)); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs index 8cb0fe073b957d..357e93a62ff12c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs @@ -25,8 +25,10 @@ public Thread(Target target, TargetPointer address) // TEB does not exist on certain platforms TEB = target.ReadPointerFieldOrNull(address, type, nameof(TEB)); + ExposedObject = target.ReadPointerField(address, type, nameof(ExposedObject)); LastThrownObject = target.ProcessedData.GetOrAdd( target.ReadPointerField(address, type, nameof(LastThrownObject))); + LastThrownObjectIsUnhandled = target.ReadField(address, type, nameof(LastThrownObjectIsUnhandled)); LinkNext = target.ReadPointerField(address, type, nameof(LinkNext)); // Address of the exception tracker @@ -36,6 +38,7 @@ public Thread(Target target, TargetPointer address) ThreadLocalDataPtr = target.ReadPointerField(address, type, nameof(ThreadLocalDataPtr)); DebuggerFilterContext = target.ReadPointerField(address, type, nameof(DebuggerFilterContext)); ProfilerFilterContext = target.ReadPointerFieldOrNull(address, type, nameof(ProfilerFilterContext)); + CurrentCustomDebuggerNotification = target.ReadPointerField(address, type, nameof(CurrentCustomDebuggerNotification)); } public uint Id { get; init; } @@ -47,11 +50,14 @@ public Thread(Target target, TargetPointer address) public TargetPointer CachedStackBase { get; init; } public TargetPointer CachedStackLimit { get; init; } public TargetPointer TEB { get; init; } + public TargetPointer ExposedObject { get; init; } public ObjectHandle LastThrownObject { get; init; } + public uint LastThrownObjectIsUnhandled { get; init; } public TargetPointer LinkNext { get; init; } public TargetPointer ExceptionTracker { get; init; } public TargetPointer UEWatsonBucketTrackerBuckets { get; init; } public TargetPointer ThreadLocalDataPtr { get; init; } public TargetPointer DebuggerFilterContext { get; init; } public TargetPointer ProfilerFilterContext { get; init; } + public TargetPointer CurrentCustomDebuggerNotification { get; init; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index 95fdef60493597..8ab61365aaa786 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -413,7 +413,33 @@ public int GetThreadHandle(ulong vmThread, nint pRetVal) => _legacy is not null ? _legacy.GetThreadHandle(vmThread, pRetVal) : HResults.E_NOTIMPL; public int GetThreadObject(ulong vmThread, ulong* pRetVal) - => _legacy is not null ? _legacy.GetThreadObject(vmThread, pRetVal) : HResults.E_NOTIMPL; + { + *pRetVal = 0; + int hr = HResults.S_OK; + try + { + Contracts.ThreadData threadData = _target.Contracts.Thread.GetThreadData(new TargetPointer(vmThread)); + if ((threadData.State & (Contracts.ThreadState.Stopped | Contracts.ThreadState.Unstarted | Contracts.ThreadState.Detached)) != 0) + throw Marshal.GetExceptionForHR(CorDbgHResults.CORDBG_E_BAD_THREAD_STATE)!; + + *pRetVal = threadData.ExposedObjectHandle.Value; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + ulong retValLocal; + int hrLocal = _legacy.GetThreadObject(vmThread, &retValLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pRetVal == retValLocal, $"cDAC: {*pRetVal:x}, DAC: {retValLocal:x}"); + } +#endif + return hr; + } public int GetThreadAllocInfo(ulong vmThread, DacDbiThreadAllocInfo* pThreadAllocInfo) => _legacy is not null ? _legacy.GetThreadAllocInfo(vmThread, pThreadAllocInfo) : HResults.E_NOTIMPL; @@ -422,7 +448,30 @@ public int SetDebugState(ulong vmThread, int debugState) => _legacy is not null ? _legacy.SetDebugState(vmThread, debugState) : HResults.E_NOTIMPL; public int HasUnhandledException(ulong vmThread, Interop.BOOL* pResult) - => _legacy is not null ? _legacy.HasUnhandledException(vmThread, pResult) : HResults.E_NOTIMPL; + { + *pResult = Interop.BOOL.FALSE; + int hr = HResults.S_OK; + try + { + Contracts.ThreadData threadData = _target.Contracts.Thread.GetThreadData(new TargetPointer(vmThread)); + *pResult = threadData.HasUnhandledException ? Interop.BOOL.TRUE : Interop.BOOL.FALSE; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + Interop.BOOL resultLocal; + int hrLocal = _legacy.HasUnhandledException(vmThread, &resultLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pResult == resultLocal, $"cDAC: {*pResult}, DAC: {resultLocal}"); + } +#endif + return hr; + } public int GetUserState(ulong vmThread, int* pRetVal) => _legacy is not null ? _legacy.GetUserState(vmThread, pRetVal) : HResults.E_NOTIMPL; @@ -525,8 +574,15 @@ public int GetCurrentException(ulong vmThread, ulong* pRetVal) int hr = HResults.S_OK; try { - TargetPointer throwable = _target.Contracts.Thread.GetThrowableObject(new TargetPointer(vmThread)); - *pRetVal = throwable.Value; + TargetPointer threadPtr = new TargetPointer(vmThread); + TargetPointer exceptionHandle = _target.Contracts.Thread.GetCurrentExceptionHandle(threadPtr); + if (exceptionHandle == TargetPointer.Null) + { + ThreadData data = _target.Contracts.Thread.GetThreadData(threadPtr); + if (data.LastThrownObjectIsUnhandled) + exceptionHandle = data.LastThrownObjectHandle; + } + *pRetVal = exceptionHandle.Value; } catch (System.Exception ex) { @@ -549,7 +605,30 @@ public int GetObjectForCCW(ulong ccwPtr, ulong* pRetVal) => _legacy is not null ? _legacy.GetObjectForCCW(ccwPtr, pRetVal) : HResults.E_NOTIMPL; public int GetCurrentCustomDebuggerNotification(ulong vmThread, ulong* pRetVal) - => _legacy is not null ? _legacy.GetCurrentCustomDebuggerNotification(vmThread, pRetVal) : HResults.E_NOTIMPL; + { + *pRetVal = 0; + int hr = HResults.S_OK; + try + { + Contracts.ThreadData threadData = _target.Contracts.Thread.GetThreadData(new TargetPointer(vmThread)); + *pRetVal = threadData.CurrentCustomDebuggerNotificationHandle.Value; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + ulong retValLocal; + int hrLocal = _legacy.GetCurrentCustomDebuggerNotification(vmThread, &retValLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pRetVal == retValLocal, $"cDAC: {*pRetVal:x}, DAC: {retValLocal:x}"); + } +#endif + return hr; + } public int GetCurrentAppDomain(ulong* pRetVal) { diff --git a/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs b/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs index 2bb87b3288c8e2..048f71881839ad 100644 --- a/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs +++ b/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs @@ -86,7 +86,11 @@ private static (TestPlaceholderTarget Target, IXCLRDataTask Task) CreateTargetWi Frame: TargetPointer.Null, FirstNestedException: firstNestedException, TEB: TargetPointer.Null, + ExposedObjectHandle: TargetPointer.Null, LastThrownObjectHandle: lastThrownObjectHandle, + CurrentCustomDebuggerNotificationHandle: TargetPointer.Null, + LastThrownObjectIsUnhandled: false, + HasUnhandledException: false, NextThread: TargetPointer.Null)); var target = new TestPlaceholderTarget.Builder(arch) @@ -465,7 +469,11 @@ private static (IXCLRDataTask Task, string ExpectedMessage) CreateTargetWithLast Frame: TargetPointer.Null, FirstNestedException: firstNestedException, TEB: TargetPointer.Null, + ExposedObjectHandle: TargetPointer.Null, LastThrownObjectHandle: lastThrownObjectHandle, + CurrentCustomDebuggerNotificationHandle: TargetPointer.Null, + LastThrownObjectIsUnhandled: false, + HasUnhandledException: false, NextThread: TargetPointer.Null)); var mockException = new Mock(); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs index 5ec7625383e899..61996b8dfcdb2b 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs @@ -107,6 +107,87 @@ public unsafe void TryGetVolatileOSThreadID_MatchesContract(TestConfiguration co } } + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void GetThreadObject_MatchesContractAndThreadStateRules(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + + IThread threadContract = Target.Contracts.Thread; + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + TargetPointer current = storeData.FirstThread; + while (current != TargetPointer.Null) + { + ThreadData data = threadContract.GetThreadData(current); + + ulong threadObject; + int hr = dbi.GetThreadObject(current, &threadObject); + + bool shouldReturnBadThreadState = (data.State & (Contracts.ThreadState.Stopped | Contracts.ThreadState.Unstarted | Contracts.ThreadState.Detached)) != 0; + if (shouldReturnBadThreadState) + { + Assert.Equal(CorDbgHResults.CORDBG_E_BAD_THREAD_STATE, hr); + } + else + { + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(data.ExposedObjectHandle.Value, threadObject); + } + + current = data.NextThread; + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void HasUnhandledException_MatchesContract(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + + IThread threadContract = Target.Contracts.Thread; + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + TargetPointer current = storeData.FirstThread; + while (current != TargetPointer.Null) + { + Interop.BOOL hasUnhandled; + int hr = dbi.HasUnhandledException(current, &hasUnhandled); + Assert.Equal(System.HResults.S_OK, hr); + + ThreadData data = threadContract.GetThreadData(current); + Assert.Equal(data.HasUnhandledException, hasUnhandled == Interop.BOOL.TRUE); + + current = data.NextThread; + } + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void GetCurrentCustomDebuggerNotification_MatchesContract(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + + IThread threadContract = Target.Contracts.Thread; + ThreadStoreData storeData = threadContract.GetThreadStoreData(); + + TargetPointer current = storeData.FirstThread; + while (current != TargetPointer.Null) + { + ulong notificationHandle; + int hr = dbi.GetCurrentCustomDebuggerNotification(current, ¬ificationHandle); + Assert.Equal(System.HResults.S_OK, hr); + + ThreadData data = threadContract.GetThreadData(current); + Assert.Equal(data.CurrentCustomDebuggerNotificationHandle.Value, notificationHandle); + + current = data.NextThread; + } + } + [ConditionalTheory] [MemberData(nameof(TestConfigurations))] public unsafe void GetUniqueThreadID_MatchesContract(TestConfiguration config) @@ -136,7 +217,7 @@ public unsafe void GetUniqueThreadID_MatchesContract(TestConfiguration config) [ConditionalTheory] [MemberData(nameof(TestConfigurations))] - public unsafe void GetCurrentException_CrossValidateWithContract(TestConfiguration config) + public unsafe void GetCurrentException_NotNull(TestConfiguration config) { InitializeDumpTest(config); DacDbiImpl dbi = CreateDacDbi(); @@ -149,15 +230,8 @@ public unsafe void GetCurrentException_CrossValidateWithContract(TestConfigurati ulong exception; int hr = dbi.GetCurrentException(current, &exception); - - // GetCurrentException depends on Thread.GetThrowableObject which is not yet - // implemented in the Thread contract. Skip until the contract is available. - if (hr == unchecked((int)0x80004001)) // E_NOTIMPL — GetThrowableObject not yet in Thread contract - { - throw new SkipTestException("GetThrowableObject not yet implemented in Thread contract"); - } - Assert.Equal(System.HResults.S_OK, hr); + Assert.NotEqual(0ul, exception); } [UnmanagedCallersOnly] diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs index 939f0674ec8c76..b2bae50f3b223d 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs @@ -179,7 +179,10 @@ internal sealed class MockThread : TypedView private const string CachedStackBaseFieldName = "CachedStackBase"; private const string CachedStackLimitFieldName = "CachedStackLimit"; private const string TEBFieldName = "TEB"; + private const string ExposedObjectFieldName = "ExposedObject"; private const string LastThrownObjectFieldName = "LastThrownObject"; + private const string LastThrownObjectIsUnhandledFieldName = "LastThrownObjectIsUnhandled"; + private const string CurrentCustomDebuggerNotificationFieldName = "CurrentCustomDebuggerNotification"; private const string LinkNextFieldName = "LinkNext"; private const string ExceptionTrackerFieldName = "ExceptionTracker"; private const string ThreadLocalDataPtrFieldName = "ThreadLocalDataPtr"; @@ -199,7 +202,10 @@ public static Layout CreateLayout(MockTarget.Architecture architectu .AddPointerField(CachedStackBaseFieldName) .AddPointerField(CachedStackLimitFieldName) .AddPointerField(TEBFieldName) + .AddPointerField(ExposedObjectFieldName) .AddPointerField(LastThrownObjectFieldName) + .AddUInt32Field(LastThrownObjectIsUnhandledFieldName) + .AddPointerField(CurrentCustomDebuggerNotificationFieldName) .AddPointerField(LinkNextFieldName) .AddPointerField(ExceptionTrackerFieldName) .AddPointerField(ThreadLocalDataPtrFieldName) @@ -256,6 +262,24 @@ public ulong ExceptionTracker set => WritePointerField(ExceptionTrackerFieldName, value); } + public ulong ExposedObject + { + get => ReadPointerField(ExposedObjectFieldName); + set => WritePointerField(ExposedObjectFieldName, value); + } + + public uint LastThrownObjectIsUnhandled + { + get => ReadUInt32Field(LastThrownObjectIsUnhandledFieldName); + set => WriteUInt32Field(LastThrownObjectIsUnhandledFieldName, value); + } + + public ulong CurrentCustomDebuggerNotification + { + get => ReadPointerField(CurrentCustomDebuggerNotificationFieldName); + set => WritePointerField(CurrentCustomDebuggerNotificationFieldName, value); + } + public ulong FrameAddress => GetFieldAddress(FrameFieldName); public ulong LinkAddress => GetFieldAddress(LinkNextFieldName); From 49bc87080467ab823f47ded0425c6b1ac2963606 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 02:17:26 +0000 Subject: [PATCH 2/4] Fix GetCurrentException_NotNull to iterate all threads and assert at least one has an exception Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/b90daa71-7183-49bc-93df-385c2013d983 Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- .../DumpTests/DacDbi/DacDbiThreadDumpTests.cs | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs index 61996b8dfcdb2b..f4405ee2aea91a 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiThreadDumpTests.cs @@ -217,7 +217,7 @@ public unsafe void GetUniqueThreadID_MatchesContract(TestConfiguration config) [ConditionalTheory] [MemberData(nameof(TestConfigurations))] - public unsafe void GetCurrentException_NotNull(TestConfiguration config) + public unsafe void GetCurrentException_AtLeastOneThreadHasException(TestConfiguration config) { InitializeDumpTest(config); DacDbiImpl dbi = CreateDacDbi(); @@ -228,10 +228,20 @@ public unsafe void GetCurrentException_NotNull(TestConfiguration config) TargetPointer current = storeData.FirstThread; Assert.NotEqual(TargetPointer.Null, current); - ulong exception; - int hr = dbi.GetCurrentException(current, &exception); - Assert.Equal(System.HResults.S_OK, hr); - Assert.NotEqual(0ul, exception); + bool foundException = false; + while (current != TargetPointer.Null) + { + ulong exception; + int hr = dbi.GetCurrentException(current, &exception); + Assert.Equal(System.HResults.S_OK, hr); + if (exception != 0ul) + foundException = true; + + ThreadData data = threadContract.GetThreadData(current); + current = data.NextThread; + } + + Assert.True(foundException, "Expected at least one thread to have a current exception in the FailFast dump."); } [UnmanagedCallersOnly] From 7143e200ba6bf77787dd99775eb18f8cd2d6f544 Mon Sep 17 00:00:00 2001 From: Rachel Date: Wed, 15 Apr 2026 21:38:29 -0700 Subject: [PATCH 3/4] Update Thread.md --- docs/design/datacontracts/Thread.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index 8b6b2b4fe14d61..f2e07c8debe428 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -25,6 +25,7 @@ enum ThreadState Unstarted = 0x00000400, // Thread has never been started Stopped = 0x00010000, // Thread has started to shut down ThreadPoolWorker = 0x01000000, // is this a threadpool worker thread? + Detached = unchecked((int)0x80000000), // Thread was detached } record struct ThreadData ( From 6625777aa99cb3250c2c3763324413bfbbeafa3a Mon Sep 17 00:00:00 2001 From: rcj1 Date: Sat, 18 Apr 2026 18:46:43 -0700 Subject: [PATCH 4/4] making field types consistent --- src/coreclr/vm/datadescriptor/datadescriptor.inc | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index aa344625666fd6..a13b9b275ce31c 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -48,12 +48,12 @@ CDAC_TYPE_FIELD(Thread, T_POINTER, DebuggerFilterContext, cdac_data::Deb #ifdef PROFILING_SUPPORTED CDAC_TYPE_FIELD(Thread, T_POINTER, ProfilerFilterContext, cdac_data::ProfilerFilterContext) #endif // PROFILING_SUPPORTED -CDAC_TYPE_FIELD(Thread, TYPE(GCHandle), ExposedObject, cdac_data::ExposedObject) -CDAC_TYPE_FIELD(Thread, TYPE(GCHandle), LastThrownObject, cdac_data::LastThrownObject) +CDAC_TYPE_FIELD(Thread, T_POINTER, ExposedObject, cdac_data::ExposedObject) +CDAC_TYPE_FIELD(Thread, T_POINTER, LastThrownObject, cdac_data::LastThrownObject) CDAC_TYPE_FIELD(Thread, T_UINT32, LastThrownObjectIsUnhandled, cdac_data::LastThrownObjectIsUnhandled) CDAC_TYPE_FIELD(Thread, T_POINTER, LinkNext, cdac_data::Link) CDAC_TYPE_FIELD(Thread, T_POINTER, ThreadLocalDataPtr, cdac_data::ThreadLocalDataPtr) -CDAC_TYPE_FIELD(Thread, TYPE(GCHandle), CurrentCustomDebuggerNotification, cdac_data::CurrentCustomDebuggerNotification) +CDAC_TYPE_FIELD(Thread, T_POINTER, CurrentCustomDebuggerNotification, cdac_data::CurrentCustomDebuggerNotification) #ifndef TARGET_UNIX CDAC_TYPE_FIELD(Thread, T_POINTER, UEWatsonBucketTrackerBuckets, cdac_data::UEWatsonBucketTrackerBuckets) #endif