@@ -839,3 +839,78 @@ instance.**
839839 pass 2 of ` FindVirtualMethodDef ` . If ` MatchSignatureForVirtualDispatch `
840840 mishandles the VAR↔GENERICINST drain (specifically for multi-argument
841841 generics like ` KeyValuePair<,> ` ), this is the symptom.
842+
843+ ---
844+
845+ ## 16. ` Span<T> ` /` ReadOnlySpan<T> ` storage-pointer arrays and GC
846+
847+ Not generics-specific, but the same "storage array split/mis-relocated/
848+ swept" family as §9, so documented the same way: rationale here, source
849+ stays terse.
850+
851+ ** Background.** ` Span<T> ` 's native backing (` corlib_native_System_Span_1.cpp ` ,
852+ ` corlib_native_System_ReadOnlySpan_1.cpp ` ) doesn't reuse the wrapped
853+ ` T[] ` 's ` CLR_RT_HeapBlock_Array ` directly — it allocates a second, small
854+ "shell" ` CLR_RT_HeapBlock_Array ` via ` CreateInstanceWithStorage ` whose
855+ ` ReflectionData().kind == REFLECTION_STORAGE_PTR ` . The shell has no
856+ element storage of its own; ` GetFirstElement() ` returns a raw
857+ ` m_StoragePointer ` address that points into the * original* array's
858+ element data (or, for the ` Span(void*, int) ` ctor, into unmanaged
859+ memory). This lets slicing/wrapping avoid copying.
860+
861+ ** The bug.** Originally the shell held nothing but that raw address —
862+ no ` CLR_RT_HeapBlock ` reference back to the array that actually owns the
863+ memory. Two independent failures followed from that:
864+ 1 . ** Reachability.** ` ComputeReachabilityGraphForMultipleBlocks ` 's
865+ ` DATATYPE_SZARRAY ` case only marks an array's * elements* reachable,
866+ and only when ` m_fReference ` is set (never true for a shell, since
867+ ` Span<T> ` rejects reference-containing ` T ` ). Nothing marked the
868+ * owning* array reachable through the shell, so a temporary like
869+ ` new Span<int>(new int[n]) ` had no live reference to the backing
870+ ` int[] ` once the constructor returned — ` --forcegc ` would sweep it,
871+ filling it with ` SENTINEL_RECOVERED ` (` 0xDFDFDFDF ` ,
872+ ` CLR_RT_HeapCluster::RecoverFromGC ` in ` CLR_RT_HeapCluster.cpp ` ) while
873+ the shell's raw pointer kept pointing at that now-dead memory. This is
874+ the ` CopyTo_WithLargeArray_ShouldCopyAllElements ` failure signature:
875+ ` Actual:<-538976289> ` is ` 0xDFDFDFDF ` as ` int32 ` .
876+ 2 . ** Compaction.** Even had the owner survived sweep, ` m_StoragePointer `
877+ is a bare address with no type tag — ` CLR_RT_HeapBlock_Array::Relocate() `
878+ had no way to know it needed adjusting when the owner's element data
879+ physically moved, so it would go stale across ` --compactionaftergc `
880+ too.
881+
882+ Both require ` --forcegc --compactionaftergc ` together to reproduce
883+ reliably; either flag alone can miss it depending on allocation timing —
884+ about 1 run in 9–10 failed under both flags before the fix, which is why
885+ a single clean run proves nothing here (loop 15–20×, see §14).
886+
887+ ** The fix.** ` CreateInstanceWithStorage ` now takes an ` owner ` reference
888+ (the array actually backing the memory — ` nullptr ` for the unmanaged-
889+ memory ctor) and allocates one extra ` sizeof(CLR_RT_HeapBlock) ` of
890+ storage beyond the shell's header (` extraBytes ` threaded through
891+ ` CLR_RT_HeapBlock_Array::CreateInstance ` → `CLR_RT_ExecutionEngine::
892+ ExtractHeapBlocksForArray` ). That slot — ` StorageOwner()`, aliasing the
893+ same ` &this[1] ` address a normal array would use for element 0, which is
894+ otherwise unused on a storage-pointer shell — holds a real
895+ ` SetObjectReference ` to the owner. ` ComputeReachabilityGraphForMultipleBlocks `
896+ marks it explicitly for ` IsStoragePointer() ` arrays, so the owner is
897+ reachable transitively through the shell like any other object
898+ reference. ` CLR_RT_HeapBlock_Array::Relocate() ` relocates that reference
899+ via the normal ` Heap_Relocate(CLR_RT_HeapBlock*, 1) ` path, then relocates
900+ ` m_StoragePointer ` itself via the generic ` Heap_Relocate(void**) ` address-
901+ range lookup — safe because that call only rewrites the stored address
902+ value by table lookup, it never dereferences memory at the (possibly
903+ not-yet-moved) target, so ordering within the compaction pass doesn't
904+ matter. A shell built over a shell (e.g. ` Span.Slice ` of a ` Span ` ) chains
905+ correctly with no special-casing: each shell only tracks its immediate
906+ ` sourceArray ` , and marking/relocation recurse through the chain via the
907+ same generic per-object dispatch.
908+
909+ ** If you see ` SENTINEL_RECOVERED ` (` 0xDFDFDFDF ` ) or a stale value read
910+ through a ` Span<T> ` /` ReadOnlySpan<T> ` :**
911+ - Confirm it reproduces only with ` --forcegc ` (sweep) or needs
912+ ` --compactionaftergc ` too (relocation) — tells you which of the two
913+ mechanisms above is implicated.
914+ - Check that the shell's ` StorageOwner() ` was actually set (i.e. the
915+ constructor path went through the array-backed ` CreateInstanceWithStorage `
916+ call, not the raw-pointer one, which intentionally has no owner).
0 commit comments