From b5b6c84d303c5db2e7ebcac92a534035afc48477 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:07:13 -0400 Subject: [PATCH 1/2] Remove TEB field from Thread data contract and DAC The TEB (Thread Environment Block) pointer was stored in Thread::m_pTEB and exposed through the DAC via DacpThreadData.teb and through the cDAC via the Thread data contract. Consumers (SOS, ClrMD) can instead look up the TEB from the OS thread ID via the debugger's native API, which does not depend on the runtime carrying this value. Changes: - request.cpp: Always set threadData->teb to NULL (field retained for binary layout compatibility of DacpThreadData) - datadescriptor.inc: Remove TEB field from Thread type descriptor - threads.h: Remove TEB from cdac_data - Data/Thread.cs: Remove TEB property and reading logic - Thread_1.cs: Remove TEB from ThreadData construction - IThread.cs: Remove TEB from ThreadData record - SOSDacImpl.cs: Set data->teb = 0, remove debug assertion - Thread.md: Remove TEB from data contract documentation - Test mocks: Remove TEB from mock thread descriptors Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/Thread.md | 3 --- src/coreclr/debug/daccess/request.cpp | 6 ++---- src/coreclr/vm/datadescriptor/datadescriptor.inc | 1 - .../Contracts/IThread.cs | 1 - .../Contracts/Thread_1.cs | 1 - .../Data/Thread.cs | 3 --- .../SOSDacImpl.cs | 3 +-- src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs | 2 -- .../cdac/tests/MockDescriptors/MockDescriptors.Thread.cs | 2 -- 9 files changed, 3 insertions(+), 19 deletions(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index f1345105acb6aa..c245c25e4e8eeb 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -36,7 +36,6 @@ record struct ThreadData ( TargetPointer AllocContextLimit; TargetPointer Frame; TargetPointer FirstNestedException; - TargetPointer TEB; TargetPointer LastThrownObjectHandle; TargetPointer NextThread; ); @@ -97,7 +96,6 @@ The contract additionally depends on these data descriptors | `Thread` | `Frame` | Pointer to current frame | | `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` | `LastThrownObject` | Handle to last thrown exception object | | `Thread` | `LinkNext` | Pointer to get next thread | | `Thread` | `ExceptionTracker` | Pointer to exception tracking information | @@ -184,7 +182,6 @@ ThreadData GetThreadData(TargetPointer address) AllocContextPointer: allocContextPointer, AllocContextLimit: allocContextLimit, Frame: target.ReadPointer(address + /* Thread::Frame offset */), - TEB : /* Has Thread::TEB offset */ ? target.ReadPointer(address + /* Thread::TEB offset */) : TargetPointer.Null, LastThrownObjectHandle : target.ReadPointer(address + /* Thread::LastThrownObject offset */), FirstNestedException : firstNestedException, NextThread: target.ReadPointer(address + /* Thread::LinkNext offset */) - threadLinkOffset; diff --git a/src/coreclr/debug/daccess/request.cpp b/src/coreclr/debug/daccess/request.cpp index 6f322bf5413e5f..cf7110ea284094 100644 --- a/src/coreclr/debug/daccess/request.cpp +++ b/src/coreclr/debug/daccess/request.cpp @@ -885,11 +885,9 @@ HRESULT ClrDataAccess::GetThreadData(CLRDATA_ADDRESS threadAddr, struct DacpThre threadData->context = PTR_CDADDR(AppDomain::GetCurrentDomain()); threadData->domain = PTR_CDADDR(AppDomain::GetCurrentDomain()); threadData->lockCount = (DWORD)-1; -#ifndef TARGET_UNIX - threadData->teb = TO_CDADDR(thread->m_pTEB); -#else + // TEB is no longer provided by the runtime. Consumers should look up the TEB + // from the OS thread ID via the debugger's native API (e.g., IDebuggerServices::GetThreadTeb). threadData->teb = (CLRDATA_ADDRESS)NULL; -#endif threadData->lastThrownObjectHandle = TO_CDADDR(thread->m_LastThrownObjectHandle); threadData->nextThread = diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 0ee65734c32da2..427b7c8341a775 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -53,7 +53,6 @@ CDAC_TYPE_FIELD(Thread, TYPE(GCHandle), LastThrownObject, cdac_data::Las CDAC_TYPE_FIELD(Thread, T_POINTER, LinkNext, cdac_data::Link) CDAC_TYPE_FIELD(Thread, T_POINTER, ThreadLocalDataPtr, cdac_data::ThreadLocalDataPtr) #ifndef TARGET_UNIX -CDAC_TYPE_FIELD(Thread, T_POINTER, TEB, cdac_data::TEB) CDAC_TYPE_FIELD(Thread, T_POINTER, UEWatsonBucketTrackerBuckets, cdac_data::UEWatsonBucketTrackerBuckets) #endif CDAC_TYPE_END(Thread) 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..f699c639cdc86c 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 @@ -38,7 +38,6 @@ public record struct ThreadData( TargetPointer AllocContextLimit, TargetPointer Frame, TargetPointer FirstNestedException, - TargetPointer TEB, TargetPointer LastThrownObjectHandle, TargetPointer NextThread); 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..c0b5cfc619e441 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 @@ -98,7 +98,6 @@ ThreadData IThread.GetThreadData(TargetPointer threadPointer) thread.RuntimeThreadLocals?.AllocContext.GCAllocationContext.Limit ?? TargetPointer.Null, thread.Frame, firstNestedException, - thread.TEB, thread.LastThrownObject.Handle, 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..83579602848ab9 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 @@ -23,8 +23,6 @@ public Thread(Target target, TargetPointer address) CachedStackBase = target.ReadPointerField(address, type, nameof(CachedStackBase)); CachedStackLimit = target.ReadPointerField(address, type, nameof(CachedStackLimit)); - // TEB does not exist on certain platforms - TEB = target.ReadPointerFieldOrNull(address, type, nameof(TEB)); LastThrownObject = target.ProcessedData.GetOrAdd( target.ReadPointerField(address, type, nameof(LastThrownObject))); LinkNext = target.ReadPointerField(address, type, nameof(LinkNext)); @@ -46,7 +44,6 @@ public Thread(Target target, TargetPointer address) public TargetPointer Frame { get; init; } public TargetPointer CachedStackBase { get; init; } public TargetPointer CachedStackLimit { get; init; } - public TargetPointer TEB { get; init; } public ObjectHandle LastThrownObject { get; init; } public TargetPointer LinkNext { get; init; } public TargetPointer ExceptionTracker { get; init; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index b7379397373f7f..f69ca8d475f590 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -4266,7 +4266,7 @@ int ISOSDacInterface.GetThreadData(ClrDataAddress thread, DacpThreadData* data) data->lockCount = -1; // Always set to -1 - lock count was .NET Framework and no longer needed data->pFrame = threadData.Frame.ToClrDataAddress(_target); data->firstNestedException = threadData.FirstNestedException.ToClrDataAddress(_target); - data->teb = threadData.TEB.ToClrDataAddress(_target); + data->teb = 0; // TEB is no longer provided by the DAC - consumers should look it up from the OS thread ID data->lastThrownObjectHandle = threadData.LastThrownObjectHandle.ToClrDataAddress(_target); data->nextThread = threadData.NextThread.ToClrDataAddress(_target); } @@ -4295,7 +4295,6 @@ int ISOSDacInterface.GetThreadData(ClrDataAddress thread, DacpThreadData* data) Debug.Assert(data->lockCount == dataLocal.lockCount, $"cDAC: {data->lockCount}, DAC: {dataLocal.lockCount}"); Debug.Assert(data->pFrame == dataLocal.pFrame, $"cDAC: {data->pFrame:x}, DAC: {dataLocal.pFrame:x}"); Debug.Assert(data->firstNestedException == dataLocal.firstNestedException, $"cDAC: {data->firstNestedException:x}, DAC: {dataLocal.firstNestedException:x}"); - Debug.Assert(data->teb == dataLocal.teb, $"cDAC: {data->teb:x}, DAC: {dataLocal.teb:x}"); Debug.Assert(data->lastThrownObjectHandle == dataLocal.lastThrownObjectHandle, $"cDAC: {data->lastThrownObjectHandle:x}, DAC: {dataLocal.lastThrownObjectHandle:x}"); Debug.Assert(data->nextThread == dataLocal.nextThread, $"cDAC: {data->nextThread:x}, DAC: {dataLocal.nextThread:x}"); } diff --git a/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs b/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs index 2bb87b3288c8e2..363604cc885609 100644 --- a/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs +++ b/src/native/managed/cdac/tests/ClrDataExceptionStateTests.cs @@ -85,7 +85,6 @@ private static (TestPlaceholderTarget Target, IXCLRDataTask Task) CreateTargetWi AllocContextLimit: TargetPointer.Null, Frame: TargetPointer.Null, FirstNestedException: firstNestedException, - TEB: TargetPointer.Null, LastThrownObjectHandle: lastThrownObjectHandle, NextThread: TargetPointer.Null)); @@ -464,7 +463,6 @@ private static (IXCLRDataTask Task, string ExpectedMessage) CreateTargetWithLast AllocContextLimit: TargetPointer.Null, Frame: TargetPointer.Null, FirstNestedException: firstNestedException, - TEB: TargetPointer.Null, LastThrownObjectHandle: lastThrownObjectHandle, NextThread: TargetPointer.Null)); diff --git a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs index 939f0674ec8c76..e152354c22577c 100644 --- a/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs +++ b/src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs @@ -178,7 +178,6 @@ internal sealed class MockThread : TypedView private const string FrameFieldName = "Frame"; private const string CachedStackBaseFieldName = "CachedStackBase"; private const string CachedStackLimitFieldName = "CachedStackLimit"; - private const string TEBFieldName = "TEB"; private const string LastThrownObjectFieldName = "LastThrownObject"; private const string LinkNextFieldName = "LinkNext"; private const string ExceptionTrackerFieldName = "ExceptionTracker"; @@ -198,7 +197,6 @@ public static Layout CreateLayout(MockTarget.Architecture architectu .AddPointerField(FrameFieldName) .AddPointerField(CachedStackBaseFieldName) .AddPointerField(CachedStackLimitFieldName) - .AddPointerField(TEBFieldName) .AddPointerField(LastThrownObjectFieldName) .AddPointerField(LinkNextFieldName) .AddPointerField(ExceptionTrackerFieldName) From 7a7bfc287565cdc6cb770ca4a0005387d8e4c845 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:08:24 -0400 Subject: [PATCH 2/2] Remove m_pTEB field from Thread class The m_pTEB field cached a pointer to the thread's TEB (_NT_TIB) that was only used in three places: - GetTEB() accessor - no longer called by any consumer - GetExceptionListPtr() - dead code, never called - Debug-only stack bounds check in RedirectThreadAtHandledJITCase The debug check now uses GetCachedStackBase/Limit which are already cached on the Thread object. The cached stack limit (from VirtualQuery) is slightly more permissive than the TEB's committed stack limit, which is acceptable for a diagnostic assert. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/daccess/enummem.cpp | 4 ---- src/coreclr/vm/threads.cpp | 2 -- src/coreclr/vm/threads.h | 15 --------------- src/coreclr/vm/threadsuspend.cpp | 6 +----- 4 files changed, 1 insertion(+), 26 deletions(-) diff --git a/src/coreclr/debug/daccess/enummem.cpp b/src/coreclr/debug/daccess/enummem.cpp index f39f576993f027..a329019ae051dc 100644 --- a/src/coreclr/debug/daccess/enummem.cpp +++ b/src/coreclr/debug/daccess/enummem.cpp @@ -1227,10 +1227,6 @@ HRESULT ClrDataAccess::EnumMemDumpAllThreadsStack(CLRDataEnumMemoryFlags flags) // Write out the Thread instance DacEnumHostDPtrMem(pThread); - // @TODO - // write TEB pointed by the thread - // DacEnumHostDPtrMem(pThread->GetTEB()); - // @TODO // If CLR is hosted, we want to write out fiber data diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index 8d554cbca625b7..ac6744c92ecb4d 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -1513,8 +1513,6 @@ void Thread::InitThread() #ifndef TARGET_UNIX (void) _controlfp_s( NULL, _RC_NEAR, _RC_CHOP|_RC_UP|_RC_DOWN|_RC_NEAR ); - m_pTEB = (struct _NT_TIB*)NtCurrentTeb(); - #endif // !TARGET_UNIX if (m_CacheStackBase == 0) diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index e120e0b3bdddc2..7e400bbc3a6ba3 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -912,20 +912,6 @@ class Thread InterpThreadContext* GetOrCreateInterpThreadContext(); #endif // FEATURE_INTERPRETER -#ifndef TARGET_UNIX -private: - _NT_TIB *m_pTEB; -public: - _NT_TIB *GetTEB() { - LIMITED_METHOD_CONTRACT; - return m_pTEB; - } - PEXCEPTION_REGISTRATION_RECORD *GetExceptionListPtr() { - WRAPPER_NO_CONTRACT; - return &GetTEB()->ExceptionList; - } -#endif // !TARGET_UNIX - inline void SetTHAllocContextObj(TypeHandle th) {LIMITED_METHOD_CONTRACT; m_thAllocContextObj = th; } inline TypeHandle GetTHAllocContextObj() {LIMITED_METHOD_CONTRACT; return m_thAllocContextObj; } @@ -3786,7 +3772,6 @@ struct cdac_data static constexpr size_t ProfilerFilterContext = offsetof(Thread, m_pProfilerFilterContext); #endif // PROFILING_SUPPORTED #ifndef TARGET_UNIX - static constexpr size_t TEB = offsetof(Thread, m_pTEB); static constexpr size_t UEWatsonBucketTrackerBuckets = offsetof(Thread, m_ExceptionState) + offsetof(ThreadExceptionState, m_UEWatsonBucketTracker) + offsetof(EHWatsonBucketTracker, m_WatsonUnhandledInfo.m_pUnhandledBuckets); #endif diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index 3e235ddcafcdd5..f265094ce9876a 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -2893,12 +2893,8 @@ BOOL Thread::RedirectThreadAtHandledJITCase(PFN_REDIRECTTARGET pTgt) #ifdef _DEBUG // In some rare cases the stack pointer may be outside the stack limits. // SetThreadContext would fail assuming that we are trying to bypass CFG. - // - // NB: the check here is slightly more strict than what OS requires, - // but it is simple and uses only documented parts of TEB - auto pTeb = this->GetTEB(); void* stackPointer = (void*)GetSP(pCtx); - if ((stackPointer < pTeb->StackLimit) || (stackPointer > pTeb->StackBase)) + if ((stackPointer < this->GetCachedStackLimit()) || (stackPointer > this->GetCachedStackBase())) { return (FALSE); }