Skip to content

Commit 38ffa46

Browse files
authored
Fix GC compaction for static fields (#3521)
***NO_CI***
1 parent 0ddc00a commit 38ffa46

5 files changed

Lines changed: 167 additions & 96 deletions

File tree

src/CLR/Core/CLAUDE.md

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,66 @@ slots:
352352
so a bare-VAR field like `static T DefaultValue` in
353353
`Foo<int>` is stamped `I4` and in `Foo<string>` is stamped OBJECT.
354354
Passing no instance here would leave every VAR field wrongly typed as
355-
the OBJECT block that `ExtractHeapBlocksForObjects` produced.
355+
the OBJECT block that `CLR_AllocateGenericStaticFieldStorage` produced.
356+
357+
### GC contract for the per-instantiation storage
358+
359+
The per-instantiation slots are the one place in the CLR where an *array*
360+
of heap blocks is addressed by pointer arithmetic
361+
(`GetGenericStaticField` returns `&ts.genericStaticFields[i]`) while
362+
living outside a containing object. That forces three rules, and getting
363+
any of them wrong produces the same symptom: a static field read returns
364+
the contents of an unrelated heap block. When the block it lands on is a
365+
generic instance, the value comes back as a `CLR_RT_TypeSpec_Index` bit
366+
pattern such as `0x01000001` (§13).
367+
368+
**1. The run must not be split.** `CLR_AllocateGenericStaticFieldStorage`
369+
allocates the slots as the payload of an unmovable
370+
`DATATYPE_BINARY_BLOB_HEAD` block (`ExtractHeapBlocksForEvents`, which
371+
adds `HB_Event`, hence `HB_Unmovable`). Compaction moves *maximal
372+
contiguous groups* of movable blocks into whatever free region is
373+
current, and ends a group when that region fills
374+
(`GarbageCollector_Compaction.cpp`, the `freeRegion_Size < len` break).
375+
So a bare run of N separately-movable size-1 blocks — what
376+
`ExtractHeapBlocksForObjects` gives you — can be broken in the middle,
377+
after which `m_fields[1]` points at an unrelated block while
378+
`m_fields[0]` is still correct. The blob header carries the whole run's
379+
`DataSize`, so the heap walk steps over the payload and compaction skips
380+
the run as a unit.
381+
382+
*Rejected: pinning the slots individually.* Compaction tests
383+
`HB_Unmovable` per block, so every slot would need the flag, and slot
384+
flags do not survive. `InitializeReference` ends with
385+
`SetDataId(RAW_ID(dt, HB_Alive, 1))`, and `stsfld` on a reference-typed
386+
field goes through `AssignAndPreserveType`, which copies the whole `m_id`
387+
(flags included) from the eval-stack value whenever the slot's datatype
388+
is above `DATATYPE_LAST_PRIMITIVE_TO_PRESERVE`. The first write to a
389+
`static T` field would silently unpin the run.
390+
391+
**2. Marking and relocation must walk the same array.** Both are driven
392+
from the global registry `g_CLR_RT_TypeSystem.m_genericStaticFields[]`
393+
marking in `Assembly_Mark`, relocation in `CLR_RT_TypeSystem::Relocate`.
394+
Marking used to iterate per-assembly `crossReferenceTypeSpec` rows while
395+
relocation iterated the global array; a record reachable only from the
396+
registry (`FindOrCreateGenericStaticFields` creates exactly that shape,
397+
though it currently has no callers) would then be relocated but never
398+
marked, and freed while live.
399+
400+
**3. Relocation runs once per pass, not once per assembly.**
401+
`Heap_Relocate(void**)` adds a region offset — it is *not* idempotent.
402+
`CLR_RT_Assembly::Relocate` is the `DATATYPE_ASSEMBLY` handler
403+
(`TypeSystemLookup.cpp`), so the heap walk calls it once per loaded
404+
assembly; driving the global registry from there shifted every pointer
405+
once per assembly. The hook is now `CLR_RT_ExecutionEngine::Relocate`,
406+
which `Heap_Relocate_Pass` calls exactly once after the walk.
407+
408+
Because the payload is inside a blob the heap walk steps over, the slots'
409+
own contents are relocated *only* by
410+
`RelocateGenericStaticField` — that is why it still calls
411+
`Heap_Relocate(m_fields, m_count)`. Conversely `m_fields` itself never
412+
moves, so it is not relocated, and neither are the `tsCross` caches that
413+
copy it. `m_fieldDefs` is `platform_malloc`'d and was never a heap
414+
pointer; relocating it was always meaningless.
356415

357416
### Nested generic construction
358417

@@ -744,6 +803,17 @@ When you see one of these symptoms, this is where to look first.
744803
priority-2 result is being picked up when it shouldn't (because the
745804
callee's `genericType` is open), the gate is missing or wrong.
746805

806+
**A generic static field reads back as `0x0100000N` (or any unrelated
807+
value) only when the GC compacts.**
808+
- Reproduce with **both** `--forcegc` and `--compactionaftergc`;
809+
`--forcegc` alone only marks and sweeps, and the bug will not show.
810+
- The storage array was split, mis-relocated, or swept. Check the three
811+
rules in §9 "GC contract for the per-instantiation storage" — the
812+
symptom is identical for all three.
813+
- A crash (host exit code 3) on the *next* generic-static test rather
814+
than a bad value is the same bug reaching a slot that holds an object
815+
reference instead of an `int`.
816+
747817
**Generic `.cctor` not firing or firing twice.**
748818
- `FindOrCreateGenericStaticFields` hash mismatch — two TypeSpec rows
749819
encode the same closed type but hash differently, or vice versa. See §10.

src/CLR/Core/Execution.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,8 @@ void CLR_RT_ExecutionEngine::Relocate()
456456
CLR_RT_GarbageCollector::Heap_Relocate((void **)&m_currentUICulture);
457457

458458
m_weakReferences.Relocate();
459+
460+
g_CLR_RT_TypeSystem.Relocate();
459461
}
460462

461463
//--//

src/CLR/Core/GarbageCollector.cpp

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -734,22 +734,23 @@ void CLR_RT_GarbageCollector::Assembly_Mark()
734734

735735
#if !defined(NANOCLR_APPDOMAINS)
736736
CheckMultipleBlocks(pASSM->staticFields, pASSM->staticFieldsCount);
737-
738-
// Mark generic static fields for each TypeSpec
739-
for (int i = 0; i < pASSM->tablesSize[TBL_TypeSpec]; i++)
740-
{
741-
CLR_RT_TypeSpec_CrossReference &ts = pASSM->crossReferenceTypeSpec[i];
742-
743-
if (ts.genericStaticFields != nullptr && ts.genericStaticFieldsCount > 0)
744-
{
745-
CheckMultipleBlocks(ts.genericStaticFields, ts.genericStaticFieldsCount);
746-
}
747-
}
748737
#endif
749738

750739
CheckSingleBlock(&pASSM->file);
751740
}
752741
NANOCLR_FOREACH_ASSEMBLY_END();
742+
743+
// Generic static fields are rooted in the global registry, not per assembly: marking has to walk
744+
// the same array relocation does. See CLAUDE.md "Static fields on generic types".
745+
for (CLR_UINT32 i = 0; i < g_CLR_RT_TypeSystem.m_genericStaticFieldsCount; i++)
746+
{
747+
CLR_RT_GenericStaticFieldRecord &record = g_CLR_RT_TypeSystem.m_genericStaticFields[i];
748+
749+
if (record.m_fields != nullptr && record.m_count > 0)
750+
{
751+
CheckMultipleBlocks(record.m_fields, record.m_count);
752+
}
753+
}
753754
}
754755

755756
//--//

src/CLR/Core/TypeSystem.cpp

Lines changed: 78 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -5860,6 +5860,39 @@ HRESULT CLR_RT_Assembly::ResolveAllocateStaticFields(CLR_RT_HeapBlock *pStaticFi
58605860
NANOCLR_NOCLEANUP();
58615861
}
58625862

5863+
// The slots are indexed as an array, so the run has to stay contiguous and unmoved: see CLAUDE.md
5864+
// "Static fields on generic types".
5865+
static CLR_RT_HeapBlock *CLR_AllocateGenericStaticFieldStorage(CLR_UINT32 count)
5866+
{
5867+
NATIVE_PROFILE_CLR_CORE();
5868+
5869+
const CLR_UINT32 headerBlocks = CONVERTFROMSIZETOHEAPBLOCKS(sizeof(CLR_RT_HeapBlock_BinaryBlob));
5870+
5871+
auto *blob = (CLR_RT_HeapBlock_BinaryBlob *)g_CLR_RT_ExecutionEngine.ExtractHeapBlocksForEvents(
5872+
DATATYPE_BINARY_BLOB_HEAD,
5873+
0,
5874+
headerBlocks + count);
5875+
5876+
if (blob == nullptr)
5877+
{
5878+
return nullptr;
5879+
}
5880+
5881+
// no handlers: the registry marks and relocates the payload, see CLR_RT_TypeSystem::Relocate
5882+
blob->SetBinaryBlobHandlers(nullptr, nullptr);
5883+
blob->m_assembly = nullptr;
5884+
5885+
CLR_RT_HeapBlock *fields = (CLR_RT_HeapBlock *)blob + headerBlocks;
5886+
5887+
// the record is registered before the slots are typed, so they have to be walkable right away
5888+
for (CLR_UINT32 i = 0; i < count; i++)
5889+
{
5890+
fields[i].SetObjectReference(nullptr);
5891+
}
5892+
5893+
return fields;
5894+
}
5895+
58635896
HRESULT CLR_RT_Assembly::ResolveAllocateGenericTypeStaticFields()
58645897
{
58655898
NATIVE_PROFILE_CLR_CORE();
@@ -5977,26 +6010,24 @@ HRESULT CLR_RT_Assembly::ResolveAllocateGenericTypeStaticFields()
59776010
g_CLR_RT_TypeSystem.m_genericStaticFieldsMaxCount = newMax;
59786011
}
59796012

5980-
// Allocate storage for the static fields
5981-
CLR_RT_HeapBlock *fields = g_CLR_RT_ExecutionEngine.ExtractHeapBlocksForObjects(
5982-
DATATYPE_OBJECT, // heapblock kind
5983-
0, // flags
5984-
count); // number of CLR_RT_HeapBlock entries
6013+
// Allocate mapping for field definitions first: a platform_malloc failure here is trivial to
6014+
// unwind, whereas the GC heap blob allocated below has no direct release path and would
6015+
// otherwise leak until the next GC cycle if allocated first and this failed.
6016+
CLR_RT_FieldDef_Index *fieldDefs =
6017+
(CLR_RT_FieldDef_Index *)platform_malloc(sizeof(CLR_RT_FieldDef_Index) * count);
59856018

5986-
if (fields == nullptr)
6019+
if (fieldDefs == nullptr)
59876020
{
59886021
NANOCLR_SET_AND_LEAVE(CLR_E_OUT_OF_MEMORY);
59896022
}
59906023

5991-
// Allocate mapping for field definitions
5992-
CLR_RT_FieldDef_Index *fieldDefs =
5993-
(CLR_RT_FieldDef_Index *)platform_malloc(sizeof(CLR_RT_FieldDef_Index) * count);
6024+
// Allocate storage for the static fields
6025+
CLR_RT_HeapBlock *fields = CLR_AllocateGenericStaticFieldStorage(count);
59946026

5995-
if (fieldDefs == nullptr)
6027+
if (fields == nullptr)
59966028
{
5997-
// Free already allocated fields
5998-
// Since we don't have a direct ReleaseHeapBlocksForObjects function,
5999-
// we'll need to have the GC clean it up later
6029+
// Field defs mapping isn't published anywhere yet, so it can be freed directly
6030+
platform_free(fieldDefs);
60006031
NANOCLR_SET_AND_LEAVE(CLR_E_OUT_OF_MEMORY);
60016032
}
60026033

@@ -6246,22 +6277,22 @@ HRESULT CLR_RT_Assembly::AllocateGenericStaticFieldsOnDemand(
62466277
g_CLR_RT_TypeSystem.m_genericStaticFieldsMaxCount = newMax;
62476278
}
62486279

6249-
// Allocate storage for the static fields
6250-
fields = g_CLR_RT_ExecutionEngine.ExtractHeapBlocksForObjects(
6251-
DATATYPE_OBJECT, // heapblock kind
6252-
0, // flags
6253-
count); // number of CLR_RT_HeapBlock entries
6280+
// Allocate mapping for field definitions first: a platform_malloc failure here is trivial to
6281+
// unwind, whereas the GC heap blob allocated below has no direct release path.
6282+
fieldDefs = (CLR_RT_FieldDef_Index *)platform_malloc(sizeof(CLR_RT_FieldDef_Index) * count);
62546283

6255-
if (fields == nullptr)
6284+
if (fieldDefs == nullptr)
62566285
{
62576286
NANOCLR_SET_AND_LEAVE(CLR_E_OUT_OF_MEMORY);
62586287
}
62596288

6260-
// Allocate mapping for field definitions
6261-
fieldDefs = (CLR_RT_FieldDef_Index *)platform_malloc(sizeof(CLR_RT_FieldDef_Index) * count);
6289+
// Allocate storage for the static fields
6290+
fields = CLR_AllocateGenericStaticFieldStorage(count);
62626291

6263-
if (fieldDefs == nullptr)
6292+
if (fields == nullptr)
62646293
{
6294+
// Field defs mapping isn't published anywhere yet, so it can be freed directly
6295+
platform_free(fieldDefs);
62656296
NANOCLR_SET_AND_LEAVE(CLR_E_OUT_OF_MEMORY);
62666297
}
62676298

@@ -7200,36 +7231,6 @@ void CLR_RT_Assembly::Relocate()
72007231
CLR_RT_GarbageCollector::Heap_Relocate(staticFields, staticFieldsCount);
72017232
#endif
72027233

7203-
// Relocate all generic static field entries
7204-
for (CLR_UINT32 i = 0; i < g_CLR_RT_TypeSystem.m_genericStaticFieldsCount; i++)
7205-
{
7206-
CLR_RT_GarbageCollector::RelocateGenericStaticField(&g_CLR_RT_TypeSystem.m_genericStaticFields[i]);
7207-
}
7208-
7209-
// Resync TypeSpec cross-ref caches into the relocated generic static field arrays
7210-
// (they are platform_malloc'd, so GC relocation above does not update them).
7211-
for (CLR_UINT32 i = 0; i < g_CLR_RT_TypeSystem.m_genericStaticFieldsCount; i++)
7212-
{
7213-
const CLR_RT_GenericStaticFieldRecord &record = g_CLR_RT_TypeSystem.m_genericStaticFields[i];
7214-
7215-
// Walk every assembly, every TypeSpec cross-reference, and update the cache if it was
7216-
// pointing at this record's old field block.
7217-
NANOCLR_FOREACH_ASSEMBLY(g_CLR_RT_TypeSystem)
7218-
{
7219-
for (int tsIdx = 0; tsIdx < pASSM->tablesSize[TBL_TypeSpec]; tsIdx++)
7220-
{
7221-
CLR_RT_TypeSpec_CrossReference &tsCross = pASSM->crossReferenceTypeSpec[tsIdx];
7222-
7223-
if (tsCross.genericStaticFields != nullptr && tsCross.genericStaticFieldsCount == record.m_count &&
7224-
tsCross.genericStaticFieldDefs == record.m_fieldDefs)
7225-
{
7226-
tsCross.genericStaticFields = record.m_fields;
7227-
}
7228-
}
7229-
}
7230-
NANOCLR_FOREACH_ASSEMBLY_END();
7231-
}
7232-
72337234
CLR_RT_GarbageCollector::Heap_Relocate((void **)&header);
72347235
CLR_RT_GarbageCollector::Heap_Relocate((void **)&name);
72357236
CLR_RT_GarbageCollector::Heap_Relocate((void **)&file);
@@ -7238,6 +7239,20 @@ void CLR_RT_Assembly::Relocate()
72387239

72397240
////////////////////////////////////////////////////////////////////////////////////////////////////
72407241

7242+
void CLR_RT_TypeSystem::Relocate()
7243+
{
7244+
NATIVE_PROFILE_CLR_CORE();
7245+
7246+
// The registry is global, so this runs once per relocation pass, never per assembly:
7247+
// see CLAUDE.md "Static fields on generic types".
7248+
for (CLR_UINT32 i = 0; i < m_genericStaticFieldsCount; i++)
7249+
{
7250+
CLR_RT_GarbageCollector::RelocateGenericStaticField(&m_genericStaticFields[i]);
7251+
}
7252+
}
7253+
7254+
////////////////////////////////////////////////////////////////////////////////////////////////////
7255+
72417256
void CLR_RT_TypeSystem::TypeSystem_Initialize()
72427257
{
72437258
NATIVE_PROFILE_CLR_CORE();
@@ -9325,34 +9340,24 @@ CLR_RT_GenericStaticFieldRecord *CLR_RT_TypeSystem::FindOrCreateGenericStaticFie
93259340
// Get the type definition record
93269341
const CLR_RECORD_TYPEDEF *ownerTd = ownerAssembly->GetTypeDef(typeDef.Type());
93279342

9328-
// Allocate storage for the static fields
9329-
CLR_RT_HeapBlock *pFields = g_CLR_RT_ExecutionEngine.ExtractHeapBlocksForObjects(
9330-
DATATYPE_OBJECT, // Use OBJECT type for proper GC management
9331-
0, // No special flags
9332-
staticFieldCount // Number of fields
9333-
);
9334-
9335-
if (pFields == nullptr)
9336-
{
9337-
return nullptr; // Out of memory
9338-
}
9339-
9340-
// Allocate mapping for field definitions using platform_malloc since we need to manage this memory separately
9343+
// Allocate mapping for field definitions first using platform_malloc: if this fails there's
9344+
// nothing to unwind. Doing this before the GC heap blob avoids leaking GC-managed memory
9345+
// that has no direct release path.
93419346
CLR_RT_FieldDef_Index *pFieldDefs =
93429347
(CLR_RT_FieldDef_Index *)platform_malloc(sizeof(CLR_RT_FieldDef_Index) * staticFieldCount);
93439348

93449349
if (pFieldDefs == nullptr)
93459350
{
9346-
// Unable to allocate field definitions, must clean up the fields we already allocated
9347-
// Since ExtractHeapBlocksForObjects allocates memory that's managed by the GC,
9348-
// we don't explicitly free it. The next GC cycle will reclaim it.
9351+
return nullptr; // Out of memory
9352+
}
93499353

9350-
// Reset the allocated fields to null to ensure no dangling references
9351-
for (CLR_UINT32 i = 0; i < staticFieldCount; i++)
9352-
{
9353-
pFields[i].SetObjectReference(nullptr);
9354-
}
9354+
// Allocate storage for the static fields
9355+
CLR_RT_HeapBlock *pFields = CLR_AllocateGenericStaticFieldStorage(staticFieldCount);
93559356

9357+
if (pFields == nullptr)
9358+
{
9359+
// Field defs mapping isn't published anywhere yet, so it can be freed directly
9360+
platform_free(pFieldDefs);
93569361
return nullptr; // Out of memory
93579362
}
93589363

src/CLR/Include/nanoCLR_Runtime.h

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2077,6 +2077,8 @@ struct CLR_RT_TypeSystem // EVENT HEAP - NO RELOCATION -
20772077
void TypeSystem_Initialize();
20782078
void TypeSystem_Cleanup();
20792079

2080+
void Relocate();
2081+
20802082
void Link(CLR_RT_Assembly *assm);
20812083
void PostLinkageProcessing(CLR_RT_Assembly *assm);
20822084

@@ -3228,18 +3230,9 @@ struct CLR_RT_GarbageCollector
32283230
{
32293231
if (field->m_fields)
32303232
{
3231-
// Relocate the internal pointers within each HeapBlock in the array
3232-
// (must be done before updating m_fields, while it still points to the old location)
3233+
// The slots sit inside an unmovable blob the heap walk steps over, so they are relocated
3234+
// here and nowhere else. See CLAUDE.md "Static fields on generic types".
32333235
CLR_RT_GarbageCollector::Heap_Relocate(field->m_fields, field->m_count);
3234-
3235-
// Update m_fields pointer itself to wherever the block array moved after compaction.
3236-
// Without this, m_fields becomes a dangling pointer after any GC compaction.
3237-
CLR_RT_GarbageCollector::Heap_Relocate((void **)&field->m_fields);
3238-
}
3239-
3240-
if (field->m_fieldDefs)
3241-
{
3242-
CLR_RT_GarbageCollector::Heap_Relocate((void **)&field->m_fieldDefs);
32433236
}
32443237
}
32453238

0 commit comments

Comments
 (0)