Skip to content

Fix Static/ThreadStatic walking#1477

Open
leculver wants to merge 4 commits into
microsoft:mainfrom
leculver:issue_1474
Open

Fix Static/ThreadStatic walking#1477
leculver wants to merge 4 commits into
microsoft:mainfrom
leculver:issue_1474

Conversation

@leculver

Copy link
Copy Markdown
Contributor

StaticVariables: The DATAS feature broke HandleTable walking. When we downscale the number of GC heaps, this affected the dac's view of the HandleTable, skipping the difference between the number of processors (which is how many subtables were created) and the current GC count. These used to always be in alignment but now are not.

ThreadStaticVariables: ThreadStatics are now handled completely separately from the rest of the code. We need to update that in the cDac in the runtime, but for now we are able to find the right values and enumerate them.

However, full handle table walking is broken, and likely can't be fixed until .Net 11 releases.

Fixes #1474.

leculver added 4 commits June 8, 2026 11:57
…oft#1474)

On .NET 9+ Core the runtime stores non-collectible thread statics in a
per-thread Object[] referenced by the native ThreadLocalData
(pNonCollectibleTlsArrayData), scanned directly by the GC as part of the
owning thread's roots -- not a GC handle and not on the stack. ClrHeap
.EnumerateRoots only enumerated handles, finalizer roots, and stack roots,
so an object rooted only by a [ThreadStatic] field had no enumerable root
and GCRoot.EnumerateRootPaths returned nothing for it. Pre-.NET 9 these
were folded into the handle table, so this is a .NET 10 regression.

Add ClrRootKind.ThreadStaticVar and
IAbstractTypeHelpers.EnumerateThreadStaticRoots(moduleAddress, threadAddress),
gated to flavor==Core && CLR major>=9 (empty otherwise). The DAC
implementation walks the module's constructed types via the type-def map
(no ClrType/ClrField hydration), filters with metadata (skip types with no
static FieldDef), and yields each type's non-zero GC thread-static base from
ISOSDacInterface14.GetThreadStaticBaseAddress. Per-module candidate
MethodTable sets are cached in a ConcurrentDictionary (thread-independent and
safe for concurrent enumeration) so the walk is O(modules), not
O(threads*modules); per-type DAC faults are contained so one corrupt type
cannot abort root enumeration. ClrHeap exposes the new public
EnumerateThreadStaticRoots() (also part of IClrHeap and included in
EnumerateRoots), yielding each base as a ThreadStaticVar root; on non-Core
or pre-9 targets it returns empty.

Adds a [ThreadStatic]-only rooted object to the GCRoot test target and a
ThreadStaticRootsAreEnumerated regression test.
…onalRoots (microsoft#1474)

On .NET 9/10 a type's GC statics live in a shared pinned Object[] (the
runtime's PinnedHeapHandleTable bucket) kept alive by a single strong handle.
On Server GC with DATAS the DAC's handle walker can fail to enumerate that
handle (DacHandleWalker::WalkHandles walks only GCHeapCount() per-proc handle
slots, fewer than the actual slot count once the dynamic heap count is
reduced), so an object reachable only through a regular static has no
enumerable root and GCRoot.EnumerateRootPaths returns nothing. Verified on the
issue's customer Server-GC dump: GCRoot went from 0 to 1 path via a StaticVar
root through the exact bucket whose strong handle the DAC skipped.

Replace the separate EnumerateThreadStaticRoots/EnumerateStaticRoots (neither
shipped) with a single ClrHeap.EnumerateAdditionalRoots() backed by one tagged
DAC walk, IAbstractTypeHelpers.EnumerateAdditionalRoots(module, threads) ->
AdditionalRootInfo{Address, IsThreadStatic}. One module pass covers both static
kinds; the cached per-module candidate MethodTable set is reused. Adds
ClrRootKind.StaticVar; regular-static bases are interior to the pinned bucket
and resolved to (and deduped by) their containing object, reported pinned.
Scoped to Core major 9-10 (pre-9 folded statics into the handle table; .NET 11+
enumerates the handle table and statics correctly). Per-type DAC faults are
contained so one corrupt type cannot abort root enumeration.

Adds a regular-static-rooted object to the GCRoot test target and a
StaticRootsAreEnumerated regression test; updates ThreadStaticRootsAreEnumerated
to the unified API. Full GCRoot suite (38) green.
…with NotImplemented default

Replace AdditionalRootInfo.IsThreadStatic with an extensible AdditionalRootKind
enum (StaticVariable, ThreadStaticVariable) plus a separate IsInteriorPointer
flag so the address semantics are decoupled from the root kind and future kinds
can be added cleanly.  ClrHeap.EnumerateAdditionalRoots now decides whether to
resolve a containing object from IsInteriorPointer (honored for any kind, not
just statics) and maps Kind -> ClrRootKind via a switch whose default throws
NotImplementedException, so an unhandled future kind fails loudly.

Add a last-resolved-object fast path for interior bases to avoid repeated
GetContainingObject calls.  A single last-object cache is insufficient on its own
because the DAC interleaves bases across the (few) live static buckets rather than
grouping them -- caching alone re-yielded buckets (186 vs 3 roots, 86 vs 1 path on
the customer dump) -- so the fast path is backed by the small resolved-bucket set
that still guarantees each bucket is reported exactly once.  Verified on the
customer dumps (faulty: 3 StaticVar roots, 1 GCRoot path) and full GCRoot suite (38) green.
Within EnumerateAdditionalRoots a heap object is either a static bucket or a
thread-static storage object, never both, so there is no value in reporting the
same object twice -- a single HashSet<ulong> of already-reported object addresses
is sufficient (no need for per-kind sets).  The multi-kind reporting of one object
(e.g. a bucket seen as StrongHandle + Stack + StaticVar) happens across the
separate enumerators composed by EnumerateRoots, not within this method.

Dedup now lives entirely at the ClrHeap layer (robust to any IAbstractTypeHelpers
output); the last-object cache and resolved-bucket range list are demoted to a
pure perf cache that only avoids repeated GetContainingObject calls.  Verified on
the customer dumps (faulty: 3 StaticVar roots, 1 GCRoot path) and full GCRoot
suite (38) green.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GCRoot.EnumerateRootPaths does not return any elements

1 participant