Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
04535d2
Initial plan
Copilot Mar 3, 2026
f5cfd10
Implement cDAC TraverseLoaderHeap API using GC contract
Copilot Mar 3, 2026
ae89c94
Address code review: add kind validation, debug validation block, and…
Copilot Mar 3, 2026
80a6655
Move LoaderHeap APIs from GC contract to Loader contract
Copilot Mar 3, 2026
6c76e30
Use offsetof() directly in datadescriptor.inc instead of cdac_data st…
Copilot Mar 3, 2026
c6ebd5a
Revert stray template<typename T> cdac_data forward declaration from …
Copilot Mar 3, 2026
2073ae1
Add LoaderHeapKind enum and separate ExplicitControlLoaderHeap cDAC t…
Copilot Mar 3, 2026
a04f5c8
Fix build break: add cdac_data specialization and friend declaration …
Copilot Mar 3, 2026
668fc1c
fix
rcj1 Mar 6, 2026
0b34106
simplify loader heap
rcj1 Mar 6, 2026
54a628c
Update docs/design/datacontracts/GC.md
rcj1 Mar 6, 2026
b15b9ce
moving debug helpers
rcj1 Mar 6, 2026
221ef85
comments
rcj1 Mar 6, 2026
f21396f
Revert "simplify loader heap"
rcj1 Mar 7, 2026
60e0be1
code review
rcj1 Mar 7, 2026
d8c4a3f
fix callconv
rcj1 Mar 9, 2026
805c47f
Merge branch 'main' into copilot/implement-cdac-api-traverse-loader-heap
rcj1 Mar 17, 2026
36f229e
incorporating refactor
rcj1 Mar 24, 2026
4f54031
nits
rcj1 Mar 24, 2026
5c557d9
validation
rcj1 Mar 24, 2026
dbaf075
Merge branch 'main' into copilot/implement-cdac-api-traverse-loader-heap
rcj1 Mar 24, 2026
3889a45
Merge branch 'main' into copilot/implement-cdac-api-traverse-loader-heap
rcj1 Apr 17, 2026
8c8e760
updating datadescriptor types
rcj1 Apr 17, 2026
5b0d234
Merge branch 'main' into copilot/implement-cdac-api-traverse-loader-heap
rcj1 Apr 18, 2026
dde9736
Update docs/design/datacontracts/Loader.md
rcj1 Apr 18, 2026
c1181d6
Update src/coreclr/inc/loaderheap.h
rcj1 Apr 18, 2026
eccd8b1
Update LoaderHeapTests to use new TypedView/Layout test infrastructure
Copilot Apr 18, 2026
52d60d2
Remove duplicate GetGlobalAllocationContext section from GC.md
Copilot Apr 18, 2026
e3e63ec
Reorder pseudocode blocks in GC.md to match declarations order
Copilot Apr 18, 2026
8195d2b
Update GC.md
rcj1 Apr 18, 2026
e75df12
Update GC.md
rcj1 Apr 18, 2026
3a81183
Update GC.md
rcj1 Apr 18, 2026
281aad6
Merge branch 'main' into copilot/implement-cdac-api-traverse-loader-heap
rcj1 Apr 20, 2026
56eee51
Update Loader.md to remove Webcil section details
rcj1 Apr 20, 2026
ecbab65
code review feedback
rcj1 Apr 20, 2026
2486f0d
fix test
rcj1 Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/design/datacontracts/GC.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public readonly struct GCOomData
IEnumerable<TargetPointer> GetGCHeaps();

// The following APIs have both a workstation and serer variant.
Comment thread
rcj1 marked this conversation as resolved.
Outdated
// The workstation variant implitly operates on the global heap.
// The workstation variant implicitly operates on the global heap.
// The server variants allow passing in a heap pointer.

// Gets data about a GC heap
Expand Down Expand Up @@ -740,3 +740,4 @@ void IGC.GetGlobalAllocationContext(out TargetPointer allocPtr, out TargetPointe
allocLimit = target.ReadPointer(globalAllocContextAddress + /* EEAllocContext::GCAllocationContext offset */ + /* GCAllocContext::Limit offset */);
}
```

43 changes: 43 additions & 0 deletions docs/design/datacontracts/Loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,20 @@ TargetPointer GetStubHeap(TargetPointer loaderAllocatorPointer);
TargetPointer GetObjectHandle(TargetPointer loaderAllocatorPointer);
TargetPointer GetILHeader(ModuleHandle handle, uint token);
TargetPointer GetDynamicIL(ModuleHandle handle, uint token);

// Loader heap traversal
readonly struct LoaderHeapBlockData
{
public TargetPointer VirtualAddress { get; init; }
public TargetNUInt VirtualSize { get; init; }
}

// Returns the first block of the loader heap linked list, or TargetPointer.Null if the heap has no blocks
TargetPointer GetFirstLoaderHeapBlock(TargetPointer loaderHeap);
// Returns the address and size of virtual memory for the given loader heap block
LoaderHeapBlockData GetLoaderHeapBlockData(TargetPointer block);
// Returns the next block in the loader heap linked list, or TargetPointer.Null if there are no more blocks
TargetPointer GetNextLoaderHeapBlock(TargetPointer block);
```

## Version 1
Expand Down Expand Up @@ -159,6 +173,10 @@ TargetPointer GetDynamicIL(ModuleHandle handle, uint token);
| `DynamicILBlobTable` | `EntrySize` | Size of each table entry |
| `DynamicILBlobTable` | `EntryMethodToken` | Offset of each entry method token from entry address |
| `DynamicILBlobTable` | `EntryIL` | Offset of each entry IL from entry address |
| `LoaderHeap` | `FirstBlock` | Pointer to the first `LoaderHeapBlock` in the linked list |
| `LoaderHeapBlock` | `Next` | Pointer to the next `LoaderHeapBlock` in the linked list |
| `LoaderHeapBlock` | `VirtualAddress` | Pointer to the start of the reserved virtual memory |
| `LoaderHeapBlock` | `VirtualSize` | Size in bytes of the reserved virtual memory region |



Expand Down Expand Up @@ -761,3 +779,28 @@ class InstMethodHashTable
}
}
```

#### GetFirstLoaderHeapBlock, GetLoaderHeapBlockData, GetNextLoaderHeapBlock

`LoaderHeap` instances use `UnlockedLoaderHeapBaseTraversable` as their primary base class. Traversal follows the `m_pFirstBlock` linked list via `LoaderHeapBlock::pNext`.

```csharp
TargetPointer ILoader.GetFirstLoaderHeapBlock(TargetPointer loaderHeap)
{
return target.ReadPointer(loaderHeap + /* LoaderHeap::FirstBlock offset */);
}

LoaderHeapBlockData ILoader.GetLoaderHeapBlockData(TargetPointer block)
{
return new LoaderHeapBlockData
{
VirtualAddress = target.ReadPointer(block + /* LoaderHeapBlock::VirtualAddress offset */),
VirtualSize = target.ReadNUInt(block + /* LoaderHeapBlock::VirtualSize offset */),
};
}

TargetPointer ILoader.GetNextLoaderHeapBlock(TargetPointer block)
{
return target.ReadPointer(block + /* LoaderHeapBlock::Next offset */);
}
```
3 changes: 2 additions & 1 deletion src/coreclr/inc/loaderheap.h
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ struct TaggedMemAllocPtr

#define VIRTUAL_ALLOC_RESERVE_GRANULARITY (64*1024) // 0x10000 (64 KB)

template<typename T> struct cdac_data;
Comment thread
rcj1 marked this conversation as resolved.
Outdated

typedef DPTR(struct LoaderHeapBlock) PTR_LoaderHeapBlock;

struct LoaderHeapBlock
Expand Down Expand Up @@ -143,7 +145,6 @@ struct LoaderHeapBlock
#endif
};


struct LoaderHeapFreeBlock;

// Collection of methods for helping in debugging heap corruptions
Expand Down
11 changes: 11 additions & 0 deletions src/coreclr/vm/datadescriptor/datadescriptor.inc
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,17 @@ CDAC_TYPE_FIELD(LoaderAllocator, /*pointer*/, StubHeap, cdac_data<LoaderAllocato
CDAC_TYPE_FIELD(LoaderAllocator, /*pointer*/, ObjectHandle, cdac_data<LoaderAllocator>::ObjectHandle)
CDAC_TYPE_END(LoaderAllocator)

CDAC_TYPE_BEGIN(LoaderHeap)
CDAC_TYPE_INDETERMINATE(LoaderHeap)
CDAC_TYPE_FIELD(LoaderHeap, /*pointer*/, FirstBlock, offsetof(UnlockedLoaderHeapBaseTraversable, m_pFirstBlock))
CDAC_TYPE_END(LoaderHeap)

CDAC_TYPE_BEGIN(LoaderHeapBlock)
CDAC_TYPE_FIELD(LoaderHeapBlock, /*pointer*/, Next, offsetof(LoaderHeapBlock, pNext))
CDAC_TYPE_FIELD(LoaderHeapBlock, /*pointer*/, VirtualAddress, offsetof(LoaderHeapBlock, pVirtualAddress))
CDAC_TYPE_FIELD(LoaderHeapBlock, /*nuint*/, VirtualSize, offsetof(LoaderHeapBlock, dwVirtualSize))
CDAC_TYPE_END(LoaderHeapBlock)

CDAC_TYPE_BEGIN(PEAssembly)
CDAC_TYPE_INDETERMINATE(PEAssembly)
CDAC_TYPE_FIELD(PEAssembly, /*pointer*/, PEImage, cdac_data<PEAssembly>::PEImage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ public ModuleHandle(TargetPointer address)
public TargetPointer Address { get; }
}

public readonly struct LoaderHeapBlockData
{
public TargetPointer VirtualAddress { get; init; }
public TargetNUInt VirtualSize { get; init; }
}

[Flags]
public enum ModuleFlags
{
Expand Down Expand Up @@ -94,6 +100,13 @@ public interface ILoader : IContract
TargetPointer GetILHeader(ModuleHandle handle, uint token) => throw new NotImplementedException();
TargetPointer GetObjectHandle(TargetPointer loaderAllocatorPointer) => throw new NotImplementedException();
TargetPointer GetDynamicIL(ModuleHandle handle, uint token) => throw new NotImplementedException();

// Returns the first block of the loader heap linked list, or TargetPointer.Null if the heap has no blocks
TargetPointer GetFirstLoaderHeapBlock(TargetPointer loaderHeap) => throw new NotImplementedException();
// Returns the address and size of virtual memory for the given loader heap block
LoaderHeapBlockData GetLoaderHeapBlockData(TargetPointer block) => throw new NotImplementedException();
// Returns the next block in the loader heap linked list, or TargetPointer.Null if there are no more blocks
TargetPointer GetNextLoaderHeapBlock(TargetPointer block) => throw new NotImplementedException();
}

public readonly struct Loader : ILoader
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public enum DataType
SystemDomain,
Assembly,
LoaderAllocator,
LoaderHeap,
LoaderHeapBlock,
PEAssembly,
AssemblyBinder,
PEImage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,4 +552,26 @@ TargetPointer ILoader.GetDynamicIL(ModuleHandle handle, uint token)
ISHash shashContract = _target.Contracts.SHash;
return shashContract.LookupSHash(dynamicILBlobTable.HashTable, token).EntryIL;
}

TargetPointer ILoader.GetFirstLoaderHeapBlock(TargetPointer loaderHeap)
{
Data.LoaderHeap heap = _target.ProcessedData.GetOrAdd<Data.LoaderHeap>(loaderHeap);
return heap.FirstBlock;
}

LoaderHeapBlockData ILoader.GetLoaderHeapBlockData(TargetPointer block)
{
Data.LoaderHeapBlock blockData = _target.ProcessedData.GetOrAdd<Data.LoaderHeapBlock>(block);
return new LoaderHeapBlockData
{
VirtualAddress = blockData.VirtualAddress,
VirtualSize = blockData.VirtualSize,
};
}

TargetPointer ILoader.GetNextLoaderHeapBlock(TargetPointer block)
{
Data.LoaderHeapBlock blockData = _target.ProcessedData.GetOrAdd<Data.LoaderHeapBlock>(block);
return blockData.Next;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Diagnostics.DataContractReader.Data;

internal sealed class LoaderHeap : IData<LoaderHeap>
{
static LoaderHeap IData<LoaderHeap>.Create(Target target, TargetPointer address)
=> new LoaderHeap(target, address);

public LoaderHeap(Target target, TargetPointer address)
{
Target.TypeInfo type = target.GetTypeInfo(DataType.LoaderHeap);

FirstBlock = target.ReadPointer(address + (ulong)type.Fields[nameof(FirstBlock)].Offset);
}

public TargetPointer FirstBlock { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.Diagnostics.DataContractReader.Data;

internal sealed class LoaderHeapBlock : IData<LoaderHeapBlock>
{
static LoaderHeapBlock IData<LoaderHeapBlock>.Create(Target target, TargetPointer address)
=> new LoaderHeapBlock(target, address);

public LoaderHeapBlock(Target target, TargetPointer address)
{
Target.TypeInfo type = target.GetTypeInfo(DataType.LoaderHeapBlock);

Next = target.ReadPointer(address + (ulong)type.Fields[nameof(Next)].Offset);
VirtualAddress = target.ReadPointer(address + (ulong)type.Fields[nameof(VirtualAddress)].Offset);
VirtualSize = target.ReadNUInt(address + (ulong)type.Fields[nameof(VirtualSize)].Offset);
}

public TargetPointer Next { get; init; }
public TargetPointer VirtualAddress { get; init; }
public TargetNUInt VirtualSize { get; init; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -4881,8 +4881,87 @@ int ISOSDacInterface12.GetGlobalAllocationContext(ClrDataAddress* allocPtr, ClrD
#endregion ISOSDacInterface12

#region ISOSDacInterface13
#if DEBUG
[ThreadStatic]
private static List<(ulong VirtualAddress, nuint VirtualSize)>? _debugTraverseLoaderHeapBlocks;

[UnmanagedCallersOnly]
private static Interop.BOOL TraverseLoaderHeapDebugCallback(ulong virtualAddress, nuint virtualSize)
{
List<(ulong VirtualAddress, nuint VirtualSize)>? expected = _debugTraverseLoaderHeapBlocks;
if (expected is not null)
{
bool found = expected.Remove((virtualAddress, virtualSize));
Debug.Assert(found, $"Unexpected loader heap block: address={virtualAddress:x}, size={virtualSize:x}");
}
return Interop.BOOL.TRUE;
}
#endif

int ISOSDacInterface13.TraverseLoaderHeap(ClrDataAddress loaderHeapAddr, /*LoaderHeapKind*/ int kind, /*VISITHEAP*/ delegate* unmanaged<ulong, nuint, Interop.BOOL> pCallback)
=> _legacyImpl13 is not null ? _legacyImpl13.TraverseLoaderHeap(loaderHeapAddr, kind, pCallback) : HResults.E_NOTIMPL;
{
Comment thread
rcj1 marked this conversation as resolved.
// Both LoaderHeapKindNormal (0) and LoaderHeapKindExplicitControl (1) use
// the same FirstBlock offset via UnlockedLoaderHeapBaseTraversable.
// Unknown kind values return E_NOTIMPL to match the native DAC behavior.
const int LoaderHeapKindNormal = 0;
const int LoaderHeapKindExplicitControl = 1;
if (kind != LoaderHeapKindNormal && kind != LoaderHeapKindExplicitControl)
return HResults.E_NOTIMPL;

int hr = HResults.S_OK;
try
{
if (loaderHeapAddr == 0)
throw new ArgumentException();
if (pCallback is null)
throw new ArgumentException();

Contracts.ILoader loader = _target.Contracts.Loader;
TargetPointer heapAddr = loaderHeapAddr.ToTargetPointer(_target);
TargetPointer block = loader.GetFirstLoaderHeapBlock(heapAddr);
while (block != TargetPointer.Null)
{
Contracts.LoaderHeapBlockData blockData = loader.GetLoaderHeapBlockData(block);
Interop.BOOL cont = pCallback(blockData.VirtualAddress.Value, (nuint)blockData.VirtualSize.Value);
if (cont == Interop.BOOL.FALSE)
break;
block = loader.GetNextLoaderHeapBlock(block);
}
}
catch (System.Exception ex)
{
hr = ex.HResult;
}
#if DEBUG
if (_legacyImpl13 is not null)
{
// Collect expected blocks via a second cDAC traversal (not re-invoking the original callback).
List<(ulong VirtualAddress, nuint VirtualSize)> cdacBlocks = [];
try
{
Contracts.ILoader loader = _target.Contracts.Loader;
TargetPointer heapAddr = loaderHeapAddr.ToTargetPointer(_target);
TargetPointer block = loader.GetFirstLoaderHeapBlock(heapAddr);
while (block != TargetPointer.Null)
{
Contracts.LoaderHeapBlockData blockData = loader.GetLoaderHeapBlockData(block);
cdacBlocks.Add((blockData.VirtualAddress.Value, (nuint)blockData.VirtualSize.Value));
block = loader.GetNextLoaderHeapBlock(block);
}
}
catch { }

_debugTraverseLoaderHeapBlocks = cdacBlocks;
delegate* unmanaged<ulong, nuint, Interop.BOOL> debugCallbackPtr = &TraverseLoaderHeapDebugCallback;
int hrLocal = _legacyImpl13.TraverseLoaderHeap(loaderHeapAddr, kind, debugCallbackPtr);
Debug.Assert(hrLocal == hr, $"cDAC: {hr:x}, DAC: {hrLocal:x}");
Comment thread
rcj1 marked this conversation as resolved.
Outdated
Debug.Assert(_debugTraverseLoaderHeapBlocks!.Count == 0,
$"cDAC found {cdacBlocks.Count} blocks but DAC found {cdacBlocks.Count - _debugTraverseLoaderHeapBlocks.Count} matching blocks");
_debugTraverseLoaderHeapBlocks = null;
}
#endif
return hr;
}
int ISOSDacInterface13.GetDomainLoaderAllocator(ClrDataAddress domainAddress, ClrDataAddress* pLoaderAllocator)
{
int hr = HResults.S_OK;
Expand Down
Loading
Loading