From 5575ffc243bc81a55f33a825962452e1834f7107 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 19 May 2026 05:10:51 +0000 Subject: [PATCH 1/4] Implement cDAC DacDBI GetThreadHandle and GetThreadAllocInfo Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- docs/design/datacontracts/Thread.md | 2 + .../vm/datadescriptor/datadescriptor.inc | 1 + src/coreclr/vm/threads.h | 1 + .../Contracts/IThread.cs | 3 +- .../Contracts/Thread_1.cs | 3 +- .../Data/Thread.cs | 2 + .../Dbi/DacDbiImpl.cs | 59 ++++++++++++++++++- .../Dbi/IDacDbiInterface.cs | 2 +- .../cdac/tests/ClrDataExceptionStateTests.cs | 6 +- .../MockDescriptors/MockDescriptors.Thread.cs | 8 +++ src/native/managed/cdac/tests/ThreadTests.cs | 20 +++++++ 11 files changed, 99 insertions(+), 8 deletions(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index 52173840f3c9f2..dbea69e239838a 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -51,6 +51,7 @@ record struct ThreadData ( bool LastThrownObjectIsUnhandled; bool HasUnhandledException; TargetPointer NextThread; + TargetPointer ThreadHandle; ); ``` @@ -131,6 +132,7 @@ The contract additionally depends on these data descriptors | `Thread` | `DebuggerFilterContext` | Pointer to the debugger filter context for the thread | | `Thread` | `RuntimeThreadLocals` | Pointer to some thread-local storage | | `Thread` | `ThreadLocalDataPtr` | Pointer to thread local data structure | +| `Thread` | `ThreadHandle` | OS thread handle | | `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data (optional, Windows only) | | `ThreadLocalData` | `NonCollectibleTlsData` | Count of non-collectible TLS data entries | | `ThreadLocalData` | `NonCollectibleTlsArrayData` | Pointer to non-collectible TLS array data | diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index fb94ca4218e96f..f50af0d8b8ab18 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -46,6 +46,7 @@ CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackLimit, cdac_data::CachedSt CDAC_TYPE_FIELD(Thread, T_POINTER, ExceptionTracker, cdac_data::ExceptionTracker) CDAC_TYPE_FIELD(Thread, T_UINT32, DebuggerControlledThreadState, cdac_data::DebuggerControlledThreadState) CDAC_TYPE_FIELD(Thread, T_POINTER, DebuggerFilterContext, cdac_data::DebuggerFilterContext) +CDAC_TYPE_FIELD(Thread, T_POINTER, ThreadHandle, cdac_data::ThreadHandle) CDAC_TYPE_FIELD(Thread, TYPE(ObjectHandle), ExposedObject, cdac_data::ExposedObject) CDAC_TYPE_FIELD(Thread, TYPE(ObjectHandle), LastThrownObject, cdac_data::LastThrownObject) CDAC_TYPE_FIELD(Thread, T_UINT32, LastThrownObjectIsUnhandled, cdac_data::LastThrownObjectIsUnhandled) diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index a28139db66d73f..1bc3071e3bc8a9 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -3779,6 +3779,7 @@ struct cdac_data "Thread::m_ExceptionState is of type ThreadExceptionState"); static constexpr size_t ExceptionTracker = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_pCurrentTracker); static constexpr size_t DebuggerFilterContext = offsetof(Thread, m_debuggerFilterContext); + static constexpr size_t ThreadHandle = offsetof(Thread, m_ThreadHandle); #ifndef TARGET_UNIX static constexpr size_t UEWatsonBucketTrackerBuckets = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_UEWatsonBucketTracker) + offsetof(EHWatsonBucketTracker, m_WatsonUnhandledInfo.m_pUnhandledBuckets); 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 d9389392edc94b..5bf33501369753 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 @@ -59,7 +59,8 @@ public record struct ThreadData( TargetPointer CurrentCustomDebuggerNotificationHandle, bool LastThrownObjectIsUnhandled, bool HasUnhandledException, - TargetPointer NextThread); + TargetPointer NextThread, + TargetPointer ThreadHandle); public interface IThread : IContract { 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 2215f20bf4a472..f1dbe19e0db53d 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 @@ -147,7 +147,8 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) thread.CurrentCustomDebuggerNotification.Handle, thread.LastThrownObjectIsUnhandled != 0, hasUnhandledException, - thread.LinkNext); + thread.LinkNext, + thread.ThreadHandle); } void IThread.GetThreadAllocContext(TargetPointer threadPointer, out long allocBytes, out long allocBytesLoh) 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 1883e0f17d21f1..3e826023395941 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 @@ -34,6 +34,7 @@ public Thread(Target target, TargetPointer address) UEWatsonBucketTrackerBuckets = target.ReadPointerFieldOrNull(address, type, nameof(UEWatsonBucketTrackerBuckets)); ThreadLocalDataPtr = target.ReadPointerField(address, type, nameof(ThreadLocalDataPtr)); DebuggerFilterContext = target.ReadPointerField(address, type, nameof(DebuggerFilterContext)); + ThreadHandle = target.ReadPointerField(address, type, nameof(ThreadHandle)); CurrentCustomDebuggerNotification = target.ReadDataField(address, type, nameof(CurrentCustomDebuggerNotification)); } @@ -54,5 +55,6 @@ public Thread(Target target, TargetPointer address) public TargetPointer UEWatsonBucketTrackerBuckets { get; init; } public TargetPointer ThreadLocalDataPtr { get; init; } public TargetPointer DebuggerFilterContext { get; init; } + public TargetPointer ThreadHandle { get; init; } public ObjectHandle 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 6cafa6afe3c976..1090e26648aefa 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 @@ -725,8 +725,30 @@ public int IsThreadMarkedDead(ulong vmThread, Interop.BOOL* pResult) return hr; } - public int GetThreadHandle(ulong vmThread, nint pRetVal) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetThreadHandle(vmThread, pRetVal) : HResults.E_NOTIMPL; + public int GetThreadHandle(ulong vmThread, void** pRetVal) + { + int hr = HResults.S_OK; + try + { + Contracts.ThreadData threadData = _target.Contracts.Thread.GetThreadData(new TargetPointer(vmThread)); + *pRetVal = (void*)threadData.ThreadHandle.Value; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + void* retValLocal = null; + int hrLocal = _legacy.GetThreadHandle(vmThread, &retValLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pRetVal == retValLocal, $"cDAC: {(nuint)(*pRetVal):x}, DAC: {(nuint)retValLocal:x}"); + } +#endif + return hr; + } public int GetThreadObject(ulong vmThread, ulong* pRetVal) { @@ -758,7 +780,38 @@ public int GetThreadObject(ulong vmThread, ulong* pRetVal) } public int GetThreadAllocInfo(ulong vmThread, DacDbiThreadAllocInfo* pThreadAllocInfo) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetThreadAllocInfo(vmThread, pThreadAllocInfo) : HResults.E_NOTIMPL; + { + int hr = HResults.S_OK; + try + { + TargetPointer threadPtr = new TargetPointer(vmThread); + Contracts.ThreadData threadData = _target.Contracts.Thread.GetThreadData(threadPtr); + _target.Contracts.Thread.GetThreadAllocContext(threadPtr, out long allocBytes, out long allocBytesLoh); + + ulong limit = threadData.AllocContextLimit.Value; + ulong pointer = threadData.AllocContextPointer.Value; + pThreadAllocInfo->allocBytesSOH = (ulong)allocBytes - (limit - pointer); + pThreadAllocInfo->allocBytesUOH = (ulong)allocBytesLoh; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + DacDbiThreadAllocInfo allocInfoLocal = default; + int hrLocal = _legacy.GetThreadAllocInfo(vmThread, &allocInfoLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert(pThreadAllocInfo->allocBytesSOH == allocInfoLocal.allocBytesSOH, $"cDAC: {pThreadAllocInfo->allocBytesSOH}, DAC: {allocInfoLocal.allocBytesSOH}"); + Debug.Assert(pThreadAllocInfo->allocBytesUOH == allocInfoLocal.allocBytesUOH, $"cDAC: {pThreadAllocInfo->allocBytesUOH}, DAC: {allocInfoLocal.allocBytesUOH}"); + } + } +#endif + return hr; + } public int SetDebugState(ulong vmThread, int debugState) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index 48f7bc11cc6862..96fcbf6bfd5abe 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -323,7 +323,7 @@ public unsafe partial interface IDacDbiInterface int IsThreadMarkedDead(ulong vmThread, Interop.BOOL* pResult); [PreserveSig] - int GetThreadHandle(ulong vmThread, nint pRetVal); + int GetThreadHandle(ulong vmThread, void** pRetVal); [PreserveSig] int GetThreadObject(ulong vmThread, ulong* pRetVal); diff --git a/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs b/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs index d3d83ed0b99ef7..53ffc57b7142be 100644 --- a/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs +++ b/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs @@ -89,7 +89,8 @@ private static (TestPlaceholderTarget Target, IXCLRDataTask Task) CreateTargetWi CurrentCustomDebuggerNotificationHandle: TargetPointer.Null, LastThrownObjectIsUnhandled: false, HasUnhandledException: false, - NextThread: TargetPointer.Null)); + NextThread: TargetPointer.Null, + ThreadHandle: TargetPointer.Null)); var target = new TestPlaceholderTarget.Builder(arch) .UseReader((ulong _, Span _) => -1) @@ -470,7 +471,8 @@ private static (IXCLRDataTask Task, string ExpectedMessage) CreateTargetWithLast CurrentCustomDebuggerNotificationHandle: TargetPointer.Null, LastThrownObjectIsUnhandled: false, HasUnhandledException: false, - NextThread: TargetPointer.Null)); + NextThread: TargetPointer.Null, + ThreadHandle: TargetPointer.Null)); var mockException = new Mock(); mockException.Setup(e => e.GetExceptionData(exceptionObjectAddr)).Returns(new ExceptionData( diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs index 1a3c857975a7de..e1d106962d5b8c 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs @@ -190,6 +190,7 @@ internal sealed class MockThread : TypedView private const string LinkNextFieldName = "LinkNext"; private const string ExceptionTrackerFieldName = "ExceptionTracker"; private const string ThreadLocalDataPtrFieldName = "ThreadLocalDataPtr"; + private const string ThreadHandleFieldName = "ThreadHandle"; private const string UEWatsonBucketTrackerBucketsFieldName = "UEWatsonBucketTrackerBuckets"; private const string DebuggerFilterContextFieldName = "DebuggerFilterContext"; @@ -212,6 +213,7 @@ public static Layout CreateLayout(MockTarget.Architecture architectu .AddPointerField(LinkNextFieldName) .AddPointerField(ExceptionTrackerFieldName) .AddPointerField(ThreadLocalDataPtrFieldName) + .AddPointerField(ThreadHandleFieldName) .AddPointerField(UEWatsonBucketTrackerBucketsFieldName) .AddPointerField(DebuggerFilterContextFieldName); @@ -297,6 +299,12 @@ public ulong LastThrownObject get => ReadPointerField(LastThrownObjectFieldName); set => WritePointerField(LastThrownObjectFieldName, value); } + + public ulong ThreadHandle + { + get => ReadPointerField(ThreadHandleFieldName); + set => WritePointerField(ThreadHandleFieldName, value); + } } internal sealed class MockThreadBuilder diff --git a/src/native/managed/cdac/tests/ThreadTests.cs b/src/native/managed/cdac/tests/ThreadTests.cs index 1f0400fbb70b2c..921096a8196ec0 100644 --- a/src/native/managed/cdac/tests/ThreadTests.cs +++ b/src/native/managed/cdac/tests/ThreadTests.cs @@ -351,6 +351,26 @@ public void GetThreadData_LastThrownObjectHandle_NoActiveException(MockTarget.Ar Assert.Equal(lastThrownHandle, data.LastThrownObjectHandle); } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetThreadData_ThreadHandle(MockTarget.Architecture arch) + { + const ulong threadHandle = 0xABCD_1234; + MockThread? thread = null; + + TestPlaceholderTarget target = CreateTarget( + arch, + threadBuilder => + { + thread = threadBuilder.AddThread(1, 1234); + thread.ThreadHandle = threadHandle; + }); + + IThread contract = target.Contracts.Thread; + ThreadData data = contract.GetThreadData(new TargetPointer(thread!.Address)); + Assert.Equal(new TargetPointer(threadHandle), data.ThreadHandle); + } + public static IEnumerable SetDebuggerControlledThreadStateData() { foreach (var arch in new MockTarget.StdArch()) From 87bdc7f8f4ccc50e8cfc34d23c7536df5dfcc98e Mon Sep 17 00:00:00 2001 From: rcj1 Date: Wed, 20 May 2026 14:47:43 -0700 Subject: [PATCH 2/4] code review --- docs/design/datacontracts/Thread.md | 1 + src/coreclr/vm/datadescriptor/datadescriptor.inc | 2 ++ src/coreclr/vm/threads.h | 2 ++ .../Data/Thread.cs | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index dbea69e239838a..7e8e1eee0d8d95 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -37,6 +37,7 @@ enum ThreadState } record struct ThreadData ( + TargetPointer ThreadAddress, uint Id; TargetNUInt OSId; ThreadState State; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index f50af0d8b8ab18..b994987d05ccf3 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -46,7 +46,9 @@ CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackLimit, cdac_data::CachedSt CDAC_TYPE_FIELD(Thread, T_POINTER, ExceptionTracker, cdac_data::ExceptionTracker) CDAC_TYPE_FIELD(Thread, T_UINT32, DebuggerControlledThreadState, cdac_data::DebuggerControlledThreadState) CDAC_TYPE_FIELD(Thread, T_POINTER, DebuggerFilterContext, cdac_data::DebuggerFilterContext) +#ifdef TARGET_WINDOWS CDAC_TYPE_FIELD(Thread, T_POINTER, ThreadHandle, cdac_data::ThreadHandle) +#endif // TARGET_WINDOWS CDAC_TYPE_FIELD(Thread, TYPE(ObjectHandle), ExposedObject, cdac_data::ExposedObject) CDAC_TYPE_FIELD(Thread, TYPE(ObjectHandle), LastThrownObject, cdac_data::LastThrownObject) CDAC_TYPE_FIELD(Thread, T_UINT32, LastThrownObjectIsUnhandled, cdac_data::LastThrownObjectIsUnhandled) diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index 1bc3071e3bc8a9..ddf01bf860e0e0 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -3779,7 +3779,9 @@ struct cdac_data "Thread::m_ExceptionState is of type ThreadExceptionState"); static constexpr size_t ExceptionTracker = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_pCurrentTracker); static constexpr size_t DebuggerFilterContext = offsetof(Thread, m_debuggerFilterContext); +#ifdef TARGET_WINDOWS static constexpr size_t ThreadHandle = offsetof(Thread, m_ThreadHandle); +#endif #ifndef TARGET_UNIX static constexpr size_t UEWatsonBucketTrackerBuckets = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_UEWatsonBucketTracker) + offsetof(EHWatsonBucketTracker, m_WatsonUnhandledInfo.m_pUnhandledBuckets); 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 3e826023395941..17e3e0dfdb8733 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 @@ -34,7 +34,7 @@ public Thread(Target target, TargetPointer address) UEWatsonBucketTrackerBuckets = target.ReadPointerFieldOrNull(address, type, nameof(UEWatsonBucketTrackerBuckets)); ThreadLocalDataPtr = target.ReadPointerField(address, type, nameof(ThreadLocalDataPtr)); DebuggerFilterContext = target.ReadPointerField(address, type, nameof(DebuggerFilterContext)); - ThreadHandle = target.ReadPointerField(address, type, nameof(ThreadHandle)); + ThreadHandle = target.ReadPointerFieldOrNull(address, type, nameof(ThreadHandle)); CurrentCustomDebuggerNotification = target.ReadDataField(address, type, nameof(CurrentCustomDebuggerNotification)); } From 4afe729b0151ff0c8b6a944432e001c911d96f67 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Thu, 21 May 2026 13:26:08 -0700 Subject: [PATCH 3/4] Update dacdbiimpl.cpp --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 9ca1993ca49259..adbe2f429efb2d 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -4499,9 +4499,12 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetThreadHandle(VMPTR_Thread vmTh HRESULT hr = S_OK; EX_TRY { - +#ifdef TARGET_WINDOWS Thread * pThread = vmThread.GetDacPtr(); *pRetVal = pThread->GetThreadHandle(); +#else + *pRetVal = NULL; +#endif // TARGET_WINDOWS } EX_CATCH_HRESULT(hr); return hr; From 260a881e76d33724296989e6bda252e53a74ebfa Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Fri, 22 May 2026 11:02:34 -0700 Subject: [PATCH 4/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/design/datacontracts/Thread.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index 7e8e1eee0d8d95..b00c13924b19f9 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -133,7 +133,7 @@ The contract additionally depends on these data descriptors | `Thread` | `DebuggerFilterContext` | Pointer to the debugger filter context for the thread | | `Thread` | `RuntimeThreadLocals` | Pointer to some thread-local storage | | `Thread` | `ThreadLocalDataPtr` | Pointer to thread local data structure | -| `Thread` | `ThreadHandle` | OS thread handle | +| `Thread` | `ThreadHandle` | OS thread handle (optional, Windows only; readers should expect `TargetPointer.Null` on non-Windows targets) | | `Thread` | `UEWatsonBucketTrackerBuckets` | Pointer to thread Watson buckets data (optional, Windows only) | | `ThreadLocalData` | `NonCollectibleTlsData` | Count of non-collectible TLS data entries | | `ThreadLocalData` | `NonCollectibleTlsArrayData` | Pointer to non-collectible TLS array data |