Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
729548e
Add external memory handle concept
jkoritzinsky Sep 14, 2026
2d88679
Add diagnostic support
jkoritzinsky Sep 15, 2026
7858949
Move to separate contract to break contract cycles
jkoritzinsky Sep 15, 2026
6567d36
Additional cleanup
jkoritzinsky Sep 15, 2026
5b17d8e
Protect byref-like func-eval results
jkoritzinsky Sep 15, 2026
7857802
Exclude byref-like return values from strong-handle creation
jkoritzinsky Sep 15, 2026
dda7b25
Remove Unordered flag from CrstExternalMemoryHandle
jkoritzinsky Sep 15, 2026
47e9afc
Move ExternalMemoryHandle list to a process-wide static
jkoritzinsky Sep 15, 2026
767e3ae
Use CrstStatic for ExternalMemoryHandle::s_crst
jkoritzinsky Sep 15, 2026
a04e25e
Remove comment on ExternalMemoryHandle static list
jkoritzinsky Sep 15, 2026
c86127a
Skip external memory handle scan during concurrent BGC mark
jkoritzinsky Sep 15, 2026
6c084d9
Take s_crst during ExternalMemoryHandle::Cleanup
jkoritzinsky Sep 16, 2026
0c0117f
Cleanup condition and make the comment reasonable.
jkoritzinsky Sep 16, 2026
ea46337
Remove early-return
jkoritzinsky Sep 16, 2026
1c4f353
Move ExternalMemoryHandle init to EEStartupHelper; clean up includes
jkoritzinsky Sep 16, 2026
1916159
Fix condition and extract helper to gcheaputilities.h
jkoritzinsky Sep 16, 2026
3738cac
Fix byref scanning to not assume we're in the context of the last thr…
jkoritzinsky Sep 16, 2026
043f8ae
Order external memory handle before output parameters
jkoritzinsky Sep 16, 2026
1c29ca4
Share byreflike field scanning with CallingConvention contract and ad…
jkoritzinsky Sep 17, 2026
d73a900
Strengthen cDAC inline array stress coverage
jkoritzinsky Sep 17, 2026
2525097
Update InlineArrayByRefLike description in README
jkoritzinsky Sep 17, 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
94 changes: 94 additions & 0 deletions docs/design/datacontracts/ExternalMemoryHandles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ExternalMemoryHandles contract

The ExternalMemoryHandles contract scans memory registered with the runtime as containing managed
references outside the GC heap and managed stacks.

## APIs of contract

``` csharp
sealed class ExternalMemoryHandleRootData
{
bool IsInteriorPointer { get; init; }
TargetPointer Address { get; init; }
TargetPointer Object { get; init; }
}
```

``` csharp
IReadOnlyList<ExternalMemoryHandleRootData> GetRoots(bool resolveInteriorPointers);
```

## Version 1

<!-- BEGIN GENERATED: usage contract=ExternalMemoryHandles version=c1 -->
### Data descriptors used

| Data Descriptor | Field | Type | Meaning |
| --- | --- | --- | --- |
| `Array` | `m_NumComponents` | `uint32` | Number of items in the array |
| `ExternalMemoryHandle` | `GCFlags` | `uint32` | Non-zero if the handle's memory holds a direct object pointer (interior/GC_CALL_INTERIOR root) rather than the address of an object reference slot |
| `ExternalMemoryHandle` | `Memory` | `pointer` | Pointer to the external memory tracked by this handle |
| `ExternalMemoryHandle` | `MethodTable` | `pointer` | Pointer to the MethodTable describing the type of the tracked memory |
| `ExternalMemoryHandle` | `Next` | `pointer` | Pointer to the next ExternalMemoryHandle in the process-wide list |
| `Object` | `m_pMethTab` | `pointer` | Method table for the object |
| `String` | `m_StringLength` | `uint32` | Length of the string in UTF-16 characters |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where does this use m_StringLength?


### Global variables used

| Global | Type | Meaning |
| --- | --- | --- |
| `ExternalMemoryHandles` | `pointer` | Address of the global pointer to the head of the process-wide external memory handle list (read a TargetPointer from this address to obtain the head ExternalMemoryHandle, or null if the list is empty) |
| `ObjectToMethodTableUnmask` | `uint8` | Bits to clear when converting an object header value to a method table address |

### Contracts used

| Contract Name |
| --- |
| `GC` |
| `RuntimeTypeSystem` |
<!-- END GENERATED: usage contract=ExternalMemoryHandles version=c1 -->

Each returned root identifies either an ordinary object-reference slot through `Address`, or an
interior root through `IsInteriorPointer` and `Object`. When `resolveInteriorPointers` is true,
`Object` is the containing managed object; null, invalid, or unresolvable interior pointers are
omitted. When it is false, `Object` is the raw pointer read from `Address`.

For reference-type handles, a zero `GCFlags` value produces an ordinary root at the handle's
`Memory` address and a non-zero value produces an interior root. For value-type handles, the
implementation reports ordinary object-reference fields described by the type's GCDesc and
recursively finds `ELEMENT_TYPE_BYREF` fields in byref-like value types, including every element of
an inline array. GCDesc offsets are adjusted from boxed-object layout to the unboxed external-memory
layout.

``` csharp
IReadOnlyList<ExternalMemoryHandleRootData> IExternalMemoryHandles.GetRoots(bool resolveInteriorPointers)
{
TargetPointer headPointer = // read the ExternalMemoryHandles global
TargetPointer current = // read a pointer from headPointer

HashSet<TargetPointer> visited = [];
List<ExternalMemoryHandleRootData> roots = [];
while (current != TargetPointer.Null)
{
if (!visited.Add(current))
throw new InvalidOperationException();

ExternalMemoryHandle handle = // read ExternalMemoryHandle object starting at current
TypeHandle type = // get the RuntimeTypeSystem handle for handle.MethodTable
if (type.IsValueType)
{
// Add GCDesc object-reference slots and recursively discovered byref-like interior roots.
}
else if (handle.GCFlags != 0)
{
// Read the pointer from handle.Memory and optionally resolve it to its containing object.
}
else
{
roots.Add(new ExternalMemoryHandleRootData { Address = handle.Memory });
}
current = handle.Next;
}
return roots;
}
```
8 changes: 7 additions & 1 deletion docs/design/datacontracts/RuntimeTypeSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ partial interface IRuntimeTypeSystem : IContract
public virtual bool ContainsGCPointers(ITypeHandle typeHandle);
// True if the MethodTable represents a byref-like value type (Span<T>, ReadOnlySpan<T>, any ref struct).
public virtual bool IsByRefLike(ITypeHandle typeHandle);
// True if the type is a compiler-generated inline array buffer type (EEClass::IsInlineArray):
// its single declared instance field is repeated across the whole GetNumInstanceFieldBytes
// span, one element per (field size) bytes, rather than declared once per element.
public virtual bool IsInlineArray(ITypeHandle typeHandle);
// If the type is an HFA (or HVA on ARM64), returns true and sets elementSize
// to 4, 8, or 16. Returns false otherwise (including on targets that don't
// define FEATURE_HFA). Mirrors MethodTable::GetHFAType in
Expand Down Expand Up @@ -561,7 +565,7 @@ static class RuntimeTypeSystem_1_Helpers
| `EEClass` | `NumStaticFields` | `uint16` | Count of static fields of the EEClass |
| `EEClass` | `NumThreadStaticFields` | `uint16` | Count of threadstatic fields of the EEClass |
| `EEClass` | `OptionalFields` | `pointer` | Pointer to the `EEClassOptionalFields` for this type, or null if it has none |
| `EEClass` | `VMFlags` | `uint32` | Optional flags for the EEClass. Bit `0x40` (`VMFLAG_HASLAYOUT`) indicates the EEClass is a `LayoutEEClass` and its `LayoutInfo` may be read |
| `EEClass` | `VMFlags` | `uint32` | Optional flags for the EEClass. Bit `0x40` (`VMFLAG_HASLAYOUT`) indicates the EEClass is a `LayoutEEClass` and its `LayoutInfo` may be read. Bit `0x10000` (`VMFLAG_INLINE_ARRAY`) indicates the type is a compiler-generated inline array buffer whose single declared instance field is repeated across the whole array |
| `EEClassLayoutInfo` | `AlignmentRequirement` | `uint8` | Largest alignment requirement of all members of the type |
| `EEClassLayoutInfo` | `Flags` | `uint8` | Layout flags. Bit `0x01` (`e_BLITTABLE`) indicates the type is blittable |
| `EEClassLayoutInfo` | `LayoutType` | `uint8` | Layout kind: `Auto` (0), `Sequential` (1), `Explicit` (2), `CStruct` (3), `CUnion` (4) |
Expand Down Expand Up @@ -826,6 +830,8 @@ static class RuntimeTypeSystem_1_Helpers

public bool IsByRefLike(ITypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike;

public bool IsInlineArray(ITypeHandle typeHandle) => typeHandle.IsMethodTable() && GetClassData(typeHandle).IsInlineArray;

// Mirrors MethodTable::GetHFAType in src/coreclr/vm/class.cpp. Pseudocode:
//
// TryGetHFAElementSize(th):
Expand Down
7 changes: 6 additions & 1 deletion docs/design/datacontracts/data-descriptor-meanings.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@
"EEClass.NumStaticFields": "Count of static fields of the EEClass",
"EEClass.NumThreadStaticFields": "Count of threadstatic fields of the EEClass",
"EEClass.OptionalFields": "Pointer to the `EEClassOptionalFields` for this type, or null if it has none",
"EEClass.VMFlags": "Optional flags for the EEClass. Bit `0x40` (`VMFLAG_HASLAYOUT`) indicates the EEClass is a `LayoutEEClass` and its `LayoutInfo` may be read",
"EEClass.VMFlags": "Optional flags for the EEClass. Bit `0x40` (`VMFLAG_HASLAYOUT`) indicates the EEClass is a `LayoutEEClass` and its `LayoutInfo` may be read. Bit `0x10000` (`VMFLAG_INLINE_ARRAY`) indicates the type is a compiler-generated inline array buffer whose single declared instance field is repeated across the whole array",
"EEClassLayoutInfo.AlignmentRequirement": "Largest alignment requirement of all members of the type",
"EEClassLayoutInfo.Flags": "Layout flags. Bit `0x01` (`e_BLITTABLE`) indicates the type is blittable",
"EEClassLayoutInfo.LayoutType": "Layout kind: `Auto` (0), `Sequential` (1), `Explicit` (2), `CStruct` (3), `CUnion` (4)",
Expand Down Expand Up @@ -182,6 +182,10 @@
"ExceptionLookupTableEntry.ExceptionInfoRVA": "RVA of the exception clause data",
"ExceptionLookupTableEntry.MethodStartRVA": "RVA of the method start",
"ExceptionLookupTableEntry.Size": "Size of an exception lookup table entry in bytes",
"ExternalMemoryHandle.GCFlags": "Non-zero if the handle's memory holds a direct object pointer (interior/GC_CALL_INTERIOR root) rather than the address of an object reference slot",
"ExternalMemoryHandle.Memory": "Pointer to the external memory tracked by this handle",
"ExternalMemoryHandle.MethodTable": "Pointer to the MethodTable describing the type of the tracked memory",
"ExternalMemoryHandle.Next": "Pointer to the next ExternalMemoryHandle in the process-wide list",
"ExternalMethodFrame.Indirection": "Import slot pointer for GCRefMap resolution via FindReadyToRunModule",
"FaultingExceptionFrame.TargetContext": "Frame's Target Context",
"FCallMethodDesc.Size": "Base size for mcFCall classification",
Expand Down Expand Up @@ -744,6 +748,7 @@
"ExceptionMethodTable": "A pointer to the address of the System.Exception MethodTable (g_pExceptionClass)",
"ExecutionManagerCodeRangeMapAddress": "Pointer to the global RangeSectionMap",
"ExpandMechanismsLength": "The number of elements in the ExpandMechanisms array",
"ExternalMemoryHandles": "Address of the global pointer to the head of the process-wide external memory handle list (read a TargetPointer from this address to obtain the head ExternalMemoryHandle, or null if the list is empty)",
"FeatureCOMInterop": "Present (nonzero) when COM interop is enabled",
"FeatureComWrappers": "Present (nonzero) when ComWrappers is enabled",
"FeatureEHFunclets": "1 if EH funclets are enabled, 0 otherwise",
Expand Down
82 changes: 81 additions & 1 deletion src/coreclr/debug/daccess/dacdbiimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include "generics.h"
#include "stackwalk.h"
#include "virtualcallstub.h"
#include "externalmemoryhandle.h"

#include "dacdbiimpl.h"

Expand Down Expand Up @@ -7792,7 +7793,8 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetGenericArgTokenIndex(VMPTR_Met

DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers)
: mDac(dac), mWalkStacks(walkStacks), mHandleMask(handleMask), mStackWalker(NULL),
mResolvePointers(resolvePointers), mHandleWalker(NULL)
mResolvePointers(resolvePointers), mHandleWalker(NULL), mExternalMemoryHandleIndex(0),
mExternalMemoryHeapInitialized(false)
{
}

Expand All @@ -7817,6 +7819,11 @@ HRESULT DacRefWalker::Init()
hr = NextThread();
}

if ((mHandleMask & CorHandleStrong) && SUCCEEDED(hr))
{
hr = WalkExternalMemoryHandles();
}

return hr;
}

Expand Down Expand Up @@ -7863,6 +7870,74 @@ UINT32 DacRefWalker::GetHandleWalkerMask()
return result;
}

HRESULT DacRefWalker::WalkExternalMemoryHandles()
{
ExternalMemoryScanContext context(this);
ExternalMemoryHandle::GCScanRoots(ExternalMemoryHandleCallback, &context);
return context.Result;
}

void DacRefWalker::ExternalMemoryHandleCallback(PTR_PTR_Object ppObj, ScanContext *sc, uint32_t flags)
{
ExternalMemoryScanContext* context = static_cast<ExternalMemoryScanContext*>(sc);
DacRefWalker* walker = context->Walker;

DacGcReference data = {};
data.vmDomain.SetDacTargetPtr(AppDomain::GetCurrentDomain().GetAddr());
data.dwType = CorHandleStrong;
data.i64ExtraData = 0;

if (flags & GC_CALL_INTERIOR)
{
CLRDATA_ADDRESS object = walker->ReadPointer(ppObj.GetAddr());
if (object == 0 || object == (CLRDATA_ADDRESS)~0)
return;

if (walker->mResolvePointers)
{
if (!walker->mExternalMemoryHeapInitialized)
{
HRESULT hr = walker->mExternalMemoryHeap.Init();
if (FAILED(hr))
{
context->Result = hr;
return;
}

walker->mExternalMemoryHeapInitialized = true;
}

CORDB_ADDRESS resolvedObject = 0;
HRESULT hr = walker->mExternalMemoryHeap.ListNearObjects((CORDB_ADDRESS)object, NULL, &resolvedObject, NULL);
if (FAILED(hr))
return;

object = TO_CDADDR(resolvedObject);
}

data.pObject = CLRDATA_ADDRESS_TO_TADDR(object) | 1;
}
else
{
data.objHnd.SetDacTargetPtr(ppObj.GetAddr());
}

if (!walker->mExternalMemoryHandles.Add(data))
context->Result = E_OUTOFMEMORY;
}

CLRDATA_ADDRESS DacRefWalker::ReadPointer(TADDR address)
{
ULONG32 bytesRead = 0;
TADDR result = 0;
HRESULT hr = mDac->m_pTarget->ReadVirtual(address, (BYTE*)&result, sizeof(TADDR), &bytesRead);

if (FAILED(hr) || bytesRead != sizeof(TADDR))
return (CLRDATA_ADDRESS)~0;

return TO_CDADDR(result);
}



HRESULT DacRefWalker::Next(ULONG celt, DacGcReference roots[], ULONG *pceltFetched)
Expand All @@ -7887,6 +7962,11 @@ HRESULT DacRefWalker::Next(ULONG celt, DacGcReference roots[], ULONG *pceltFetch
}
}

while (total < celt && mExternalMemoryHandleIndex < mExternalMemoryHandles.GetCount())
{
roots[total++] = mExternalMemoryHandles.Get(mExternalMemoryHandleIndex++);
}

while (total < celt && mStackWalker)
{
ULONG fetched = 0;
Expand Down
20 changes: 20 additions & 0 deletions src/coreclr/debug/daccess/dacdbiimpl.h
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,17 @@ class DDHolder

class DacRefWalker
{
struct ExternalMemoryScanContext : public ScanContext
{
DacRefWalker *Walker;
HRESULT Result;

ExternalMemoryScanContext(DacRefWalker *walker)
: Walker(walker), Result(S_OK)
{
}
};

public:
DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers);
~DacRefWalker();
Expand All @@ -955,6 +966,9 @@ class DacRefWalker
UINT32 GetHandleWalkerMask();
void Clear();
HRESULT NextThread();
HRESULT WalkExternalMemoryHandles();
static void ExternalMemoryHandleCallback(PTR_PTR_Object ppObj, ScanContext *sc, uint32_t flags);
CLRDATA_ADDRESS ReadPointer(TADDR address);

private:
ClrDataAccess *mDac;
Expand All @@ -967,6 +981,12 @@ class DacRefWalker

// Handles
DacHandleWalker *mHandleWalker;

// External memory handles
DacReferenceList<DacGcReference> mExternalMemoryHandles;
unsigned int mExternalMemoryHandleIndex;
DacHeapWalker mExternalMemoryHeap;
bool mExternalMemoryHeapInitialized;
};

#endif // _DACDBI_IMPL_H_
Loading
Loading