From dd9cfb1b9982138a8d2004b76d8da682e2f114bd Mon Sep 17 00:00:00 2001 From: Lee Culver Date: Thu, 11 Jun 2026 10:30:08 -0400 Subject: [PATCH 1/4] cDAC: dereference OBJECTHANDLE in collectible thread-static base resolution The cDAC IThread.GetThreadLocalStaticBase collectible-TLS branch returned the collectible TLS array slot contents directly as the static base. That slot holds an OBJECTHANDLE, not the object: native GetThreadLocalStaticBaseNoCreate (threadstatics.cpp) reads the handle from the slot and then calls ObjectFromHandleUnchecked to get the object. cDAC skipped the dereference, so collectible (unloadable-ALC) thread-static base resolution returned the handle cell address instead of the object base, silently mis-resolving every subsequent field read. Dereference the handle via the ObjectHandle data type (which reads the handle then the object, matching ObjectFromHandleUnchecked and the in-flight TLS path that already uses ObjectHandle.Object). The NonCollectible branch is unchanged: that array stores direct object refs. Flows through ISOSDacInterface14::GetThreadStaticBaseAddress, affecting cDAC-backed SOS thread-static field display and ClrMD ClrThreadStaticField.GetAddress. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/Thread_1.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 56471abe6389fb..9f50cddfaec145 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 @@ -216,7 +216,11 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ if (collectibleCount > indexOffset) { TargetPointer collectibleArray = threadLocalData.CollectibleTlsArrayData; - threadLocalStaticBase = _target.ReadPointer(collectibleArray + (ulong)(indexOffset * _target.PointerSize)); + // The collectible TLS array slot holds an OBJECTHANDLE, not the object directly. + // Dereference the handle to get the static base (matches the native + // GetThreadLocalStaticBaseNoCreate, which does ObjectFromHandleUnchecked). + TargetPointer handleAddress = collectibleArray + (ulong)(indexOffset * _target.PointerSize); + threadLocalStaticBase = _target.ProcessedData.GetOrAdd(handleAddress).Object; } break; case TLSIndexType.DirectOnThreadLocalData: From 9af36bacfea3abcf8ab508a47e7268f86bc9f89a Mon Sep 17 00:00:00 2001 From: Lee Culver Date: Thu, 11 Jun 2026 10:48:43 -0400 Subject: [PATCH 2/4] Address PR feedback: clarify handle-slot naming and comment Rename handleAddress -> handleSlotAddress and reword the comment to make explicit that the value is the address of the TLS array slot containing the OBJECTHANDLE (not the handle/object), and that ObjectHandle reads the handle from that slot and dereferences it. No behavior change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Contracts/Thread_1.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 9f50cddfaec145..e0fb8a3ea4efb8 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 @@ -216,11 +216,12 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ if (collectibleCount > indexOffset) { TargetPointer collectibleArray = threadLocalData.CollectibleTlsArrayData; - // The collectible TLS array slot holds an OBJECTHANDLE, not the object directly. - // Dereference the handle to get the static base (matches the native + // The collectible TLS array slot holds an OBJECTHANDLE. Constructing an + // ObjectHandle from the slot address reads the handle from the slot and + // dereferences it to the object (matching the native // GetThreadLocalStaticBaseNoCreate, which does ObjectFromHandleUnchecked). - TargetPointer handleAddress = collectibleArray + (ulong)(indexOffset * _target.PointerSize); - threadLocalStaticBase = _target.ProcessedData.GetOrAdd(handleAddress).Object; + TargetPointer handleSlotAddress = collectibleArray + (ulong)(indexOffset * _target.PointerSize); + threadLocalStaticBase = _target.ProcessedData.GetOrAdd(handleSlotAddress).Object; } break; case TLSIndexType.DirectOnThreadLocalData: From 67fea10f333b61801f0a12276acf137cc0236771 Mon Sep 17 00:00:00 2001 From: Lee Culver Date: Fri, 12 Jun 2026 13:35:48 -0400 Subject: [PATCH 3/4] Documentation update --- docs/design/datacontracts/Thread.md | 4 +++- .../Contracts/Thread_1.cs | 4 ---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index f2ad63e4e7ab65..de285e4ab77b72 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -302,7 +302,9 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ if (collectibleCount > indexOffset) { TargetPointer collectibleArray = target.ReadPointer(threadLocalDataPtr + /* ThreadLocalData::CollectibleTlsArrayData offset */); - threadLocalStaticBase = target.ReadPointer(collectibleArray + (ulong)(indexOffset * target.PointerSize)); + // The collectible TLS array slot holds an OBJECTHANDLE; dereference the handle to the object + TargetPointer handleSlotAddress = collectibleArray + (ulong)(indexOffset * target.PointerSize); + threadLocalStaticBase = target.ReadPointer(target.ReadPointer(handleSlotAddress)); } break; case TLSIndexType.DirectOnThreadLocalData: 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 e0fb8a3ea4efb8..3ab8573b78f074 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 @@ -216,10 +216,6 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ if (collectibleCount > indexOffset) { TargetPointer collectibleArray = threadLocalData.CollectibleTlsArrayData; - // The collectible TLS array slot holds an OBJECTHANDLE. Constructing an - // ObjectHandle from the slot address reads the handle from the slot and - // dereferences it to the object (matching the native - // GetThreadLocalStaticBaseNoCreate, which does ObjectFromHandleUnchecked). TargetPointer handleSlotAddress = collectibleArray + (ulong)(indexOffset * _target.PointerSize); threadLocalStaticBase = _target.ProcessedData.GetOrAdd(handleSlotAddress).Object; } From c072f3ff54ead64f42e9fdba216d4d4cb6125e0e Mon Sep 17 00:00:00 2001 From: Lee Culver Date: Fri, 12 Jun 2026 13:44:45 -0400 Subject: [PATCH 4/4] PR Feedback: Doc update --- docs/design/datacontracts/Thread.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index de285e4ab77b72..b23c40e7f93377 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -304,7 +304,9 @@ TargetPointer IThread.GetThreadLocalStaticBase(TargetPointer threadPointer, Targ TargetPointer collectibleArray = target.ReadPointer(threadLocalDataPtr + /* ThreadLocalData::CollectibleTlsArrayData offset */); // The collectible TLS array slot holds an OBJECTHANDLE; dereference the handle to the object TargetPointer handleSlotAddress = collectibleArray + (ulong)(indexOffset * target.PointerSize); - threadLocalStaticBase = target.ReadPointer(target.ReadPointer(handleSlotAddress)); + TargetPointer handle = target.ReadPointer(handleSlotAddress); + if (handle != TargetPointer.Null && target.TryReadPointer(handle, out TargetPointer obj)) + threadLocalStaticBase = obj; } break; case TLSIndexType.DirectOnThreadLocalData: