From 729548ee914d95186f520f924572220535a0793c Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Mon, 14 Sep 2026 14:44:08 -0700 Subject: [PATCH 01/24] Add external memory handle concept --- src/coreclr/vm/CMakeLists.txt | 2 ++ src/coreclr/vm/appdomain.cpp | 15 ++++++++++ src/coreclr/vm/appdomain.hpp | 8 ++++++ src/coreclr/vm/externalmemoryhandle.cpp | 37 +++++++++++++++++++++++++ src/coreclr/vm/externalmemoryhandle.h | 32 +++++++++++++++++++++ src/coreclr/vm/gcenv.ee.cpp | 5 ++++ 6 files changed, 99 insertions(+) create mode 100644 src/coreclr/vm/externalmemoryhandle.cpp create mode 100644 src/coreclr/vm/externalmemoryhandle.h diff --git a/src/coreclr/vm/CMakeLists.txt b/src/coreclr/vm/CMakeLists.txt index ce4b849c84750d..ad6b44d9dff0f3 100644 --- a/src/coreclr/vm/CMakeLists.txt +++ b/src/coreclr/vm/CMakeLists.txt @@ -84,6 +84,7 @@ set(VM_SOURCES_DAC_AND_WKS_COMMON excep.cpp exinfo.cpp exstate.cpp + externalmemoryhandle.cpp field.cpp formattype.cpp fptrstubs.cpp @@ -145,6 +146,7 @@ set(VM_SOURCES_DAC_AND_WKS_COMMON ) set(VM_HEADERS_DAC_AND_WKS_COMMON + externalmemoryhandle.h ../inc/corjit.h ../inc/corjitflags.h ../inc/corjithost.h diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index 5f717668958e3e..c7a6c6549b3e45 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -3786,6 +3786,21 @@ void AppDomain::EnumStaticGCRefs(promote_func* fn, ScanContext* sc) } } +void AppDomain::GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + } + CONTRACTL_END; + + for (SListElem* item = m_externalMemoryHandles.GetHead(); item != NULL; item = SListTail>::GetNext(item)) + { + item->GetValue().GCScanRoot(fn, sc); + } +} + #endif // !DACCESS_COMPILE //------------------------------------------------------------------------ diff --git a/src/coreclr/vm/appdomain.hpp b/src/coreclr/vm/appdomain.hpp index 1a014b6d66814c..d0a89d11f5055e 100644 --- a/src/coreclr/vm/appdomain.hpp +++ b/src/coreclr/vm/appdomain.hpp @@ -35,6 +35,8 @@ #include "codeversion.h" +#include "externalmemoryhandle.h" + class SystemDomain; class AppDomain; class GlobalStringLiteralMap; @@ -1534,6 +1536,12 @@ class AppDomain final TieredCompilationManager m_tieredCompilationManager; friend struct cdac_data; + +public: + void GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc); + +private: + SListTail> m_externalMemoryHandles; }; // class AppDomain template<> diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp new file mode 100644 index 00000000000000..56bed93eecae69 --- /dev/null +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "externalmemoryhandle.h" +#include "siginfo.hpp" + +void ExternalMemoryHandle::GCScanRoot(promote_func *fn, ScanContext *sc) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + } + CONTRACTL_END; + + auto fromAddress = m_pMemory; + if (m_pMT->IsValueType()) + { + ReportPointersFromValueType(fn, sc, m_pMT, m_pMemory); + } + else + { + if (m_gcFlags != 0) + { + _ASSERTE(m_gcFlags & GC_CALL_INTERIOR); + PromoteCarefully(fn, (PTR_PTR_Object)m_pMemory, sc, m_gcFlags | CHECK_APP_DOMAIN); + } + else + { + (*fn)((PTR_PTR_Object)m_pMemory, sc, 0); + } + + auto toAddress = m_pMemory; + LOG((LF_GC, INFO3, "External Memory Handle promoted" FMT_ADDR "to" FMT_ADDR "\n", + DBG_ADDR(fromAddress), DBG_ADDR(toAddress))); + } +} diff --git a/src/coreclr/vm/externalmemoryhandle.h b/src/coreclr/vm/externalmemoryhandle.h new file mode 100644 index 00000000000000..9607d1e3d369fc --- /dev/null +++ b/src/coreclr/vm/externalmemoryhandle.h @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#ifndef EXTERNALMEMORYHANDLE_HPP +#define EXTERNALMEMORYHANDLE_HPP + +#include "common.h" +#include "gcinterface.h" + +// Represents a handle to external memory that can be scanned by the garbage collector. +class ExternalMemoryHandle final +{ +public: + // Create a reference to external memory. The memory should point to a value that represents PTR_MethodTable. + // So for example, if pMT is a struct, pMemory is the value of the struct. + // If pMT is a class type, pMemory is the PTR_Object pointing to the instance of the class on the GC heap. + ExternalMemoryHandle(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags) + : m_pMT(pMT), m_pMemory(pMemory), m_gcFlags(gcFlags) + { + } + + void GCScanRoot(promote_func *fn, ScanContext *sc); + +private: + PTR_MethodTable m_pMT; + PTR_VOID m_pMemory; + UINT m_gcFlags; +}; + +typedef DPTR(ExternalMemoryHandle) PTR_ExternalMemoryHandle; + +#endif // EXTERNALMEMORYHANDLE_HPP diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index 541b478acb1173..592d1c8fa65df2 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -334,6 +334,11 @@ void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, SystemDomain::EnumAllStaticGCRefs(fn, sc); } } + + if (::GetAppDomain()) + { + ::GetAppDomain()->GCScanExternalMemoryHandles(fn, sc); + } } void GCToEEInterface::GcStartWork (int condemned, int max_gen) From 2d88679a98fdad0a28f9e8bcfc360b42bf539548 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 10:45:48 -0700 Subject: [PATCH 02/24] Add diagnostic support --- docs/design/datacontracts/Loader.md | 92 +++++ .../design/datacontracts/RuntimeTypeSystem.md | 8 +- .../data-descriptor-meanings.json | 7 +- src/coreclr/debug/daccess/dacdbiimpl.cpp | 85 ++++- src/coreclr/debug/daccess/dacdbiimpl.h | 20 ++ src/coreclr/inc/CrstTypes.def | 4 + src/coreclr/inc/crsttypes_generated.h | 155 +++++---- src/coreclr/vm/appdomain.cpp | 69 +++- src/coreclr/vm/appdomain.hpp | 8 +- .../vm/datadescriptor/datadescriptor.inc | 11 +- src/coreclr/vm/externalmemoryhandle.cpp | 19 +- src/coreclr/vm/externalmemoryhandle.h | 23 +- src/coreclr/vm/gcenv.ee.cpp | 4 +- .../Contracts/ILoader.cs | 8 + .../Contracts/IRuntimeTypeSystem.cs | 4 + .../Contracts/Loader_1.cs | 2 + .../Contracts/Loader_2.cs | 222 ++++++++++++ .../Contracts/RuntimeTypeSystem_1.cs | 1 + .../StackWalk/GC/GCInteriorPointerResolver.cs | 104 ++++++ .../Contracts/StackWalk/GC/GcScanContext.cs | 87 +---- .../CoreCLRContracts.cs | 1 + .../Data/AppDomain.cs | 2 + .../Data/EEClass.cs | 5 + .../Data/ExternalMemoryHandle.cs | 13 + .../DataType.cs | 1 + .../Dbi/Helpers/RefWalk.cs | 33 +- .../DacDbi/DacDbiRefWalkDumpTests.cs | 21 ++ .../TestPlaceholderTarget.cs | 8 +- .../LoaderExternalMemoryHandleTests.cs | 142 ++++++++ .../cdac/tests/UnitTests/MethodTableTests.cs | 51 +++ .../RefWalkExternalMemoryHandleTests.cs | 328 ++++++++++++++++++ .../RefWalkLoaderContractVersionTests.cs | 145 ++++++++ .../UsageTests/ContractUsageGraphTests.cs | 8 +- 33 files changed, 1512 insertions(+), 179 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_2.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExternalMemoryHandle.cs create mode 100644 src/native/managed/cdac/tests/UnitTests/LoaderExternalMemoryHandleTests.cs create mode 100644 src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs create mode 100644 src/native/managed/cdac/tests/UnitTests/RefWalkLoaderContractVersionTests.cs diff --git a/docs/design/datacontracts/Loader.md b/docs/design/datacontracts/Loader.md index 53f5210f854147..00e535a7b905c4 100644 --- a/docs/design/datacontracts/Loader.md +++ b/docs/design/datacontracts/Loader.md @@ -57,6 +57,13 @@ enum ModuleLookupMapKind readonly record struct LoaderHeapBlock(TargetPointer Address, TargetNUInt Size); +sealed class ExternalMemoryHandleRootData +{ + bool IsInteriorPointer { get; init; } + TargetPointer Address { get; init; } + TargetPointer Object { get; init; } +} + enum LoaderAllocatorHeapType { Unknown, @@ -79,6 +86,7 @@ IEnumerable GetModuleHandles(TargetPointer appDomain, AssemblyIter TargetPointer GetRootAssembly(); string GetAppDomainFriendlyName(); TargetPointer GetAppDomain(); +IReadOnlyList GetExternalMemoryHandleRoots(bool resolveInteriorPointers); TargetPointer GetModule(ModuleHandle handle); TargetPointer GetAssembly(ModuleHandle handle); TargetPointer GetPEAssembly(ModuleHandle handle); @@ -251,6 +259,36 @@ enum ClrModifiableAssemblies : uint | `SHash` | +## Version 2 dependency changes from Version 1 + + +### Data descriptor changes from `c1` + +| Change | Data Descriptor | Field | Type | Meaning | +| --- | --- | --- | --- | --- | +| Added | `AppDomain` | `ExternalMemoryHandles` | `pointer` | Pointer to the head of the AppDomain's external memory handle list (SListTail) | +| Added | `Array` | `m_NumComponents` | `uint32` | Number of items in the array | +| Added | `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 | +| Added | `ExternalMemoryHandle` | `Memory` | `pointer` | Pointer to the external memory tracked by this handle | +| Added | `ExternalMemoryHandle` | `MethodTable` | `pointer` | Pointer to the MethodTable describing the type of the tracked memory | +| Added | `ExternalMemoryHandle` | `Next` | `pointer` | Pointer to the next ExternalMemoryHandle in the owning AppDomain's list | +| Added | `Object` | `m_pMethTab` | `pointer` | Method table for the object | +| Added | `String` | `m_StringLength` | `uint32` | Length of the string in UTF-16 characters | + +### Global variable changes from `c1` + +| Change | Global | Type | Meaning | +| --- | --- | --- | --- | +| Added | `ObjectToMethodTableUnmask` | `uint8` | Bits to clear when converting an object header value to a method table address | + +### Contract dependency changes from `c1` + +| Change | Contract Name | +| --- | --- | +| Added | `GC` | +| Added | `RuntimeTypeSystem` | + + ### Contract Constants: | Name | Type | Purpose | Value | | --- | --- | --- | --- | @@ -1050,3 +1088,57 @@ IEnumerable ILoader.EnumerateLoaderHeapBlocks(TargetPointer loa } } ``` + +## Version 2 + +Version 2 adds support for scanning an AppDomain's ExternalMemoryHandle list and returning the +resulting GC roots. GetExternalMemoryHandleRoots is part of the shared ILoader API surface (available +since Version 1), but Version 1's implementation always returns an empty list; only Version 2 walks +the real list. All other APIs behave identically to Version 1. + +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 ILoader.GetExternalMemoryHandleRoots(bool resolveInteriorPointers) +{ + TargetPointer appDomain = GetAppDomain(); + if (appDomain == TargetPointer.Null) + return []; + + AppDomain domain = // read AppDomain object starting at appDomain + HashSet visited = []; + List roots = []; + TargetPointer current = domain.ExternalMemoryHandles; + while (current != TargetPointer.Null) + { + if (!visited.Add(current)) + throw new InvalidOperationException(); // defend against a corrupted/cyclic list + + 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; +} +``` diff --git a/docs/design/datacontracts/RuntimeTypeSystem.md b/docs/design/datacontracts/RuntimeTypeSystem.md index b6bbb7aee425b8..152994ff08e385 100644 --- a/docs/design/datacontracts/RuntimeTypeSystem.md +++ b/docs/design/datacontracts/RuntimeTypeSystem.md @@ -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, ReadOnlySpan, 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 @@ -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) | @@ -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): diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 221478b81fe25c..2471dc135b3ac9 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -1,6 +1,7 @@ { "_fields": { "AppDomain.AssemblyList": "Pointer to the list of assemblies in the application domain", + "AppDomain.ExternalMemoryHandles": "Pointer to the head of the AppDomain's external memory handle list (SListTail)", "AppDomain.FriendlyName": "Pointer to the application domain's friendly name", "AppDomain.RootAssembly": "Pointer to the root assembly", "Array.m_NumComponents": "Number of items in the array", @@ -125,7 +126,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)", @@ -182,6 +183,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 owning AppDomain's list", "ExternalMethodFrame.Indirection": "Import slot pointer for GCRefMap resolution via FindReadyToRunModule", "FaultingExceptionFrame.TargetContext": "Frame's Target Context", "FCallMethodDesc.Size": "Base size for mcFCall classification", diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index dab05de698687e..8730e6dba8d84a 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -7792,7 +7792,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) { } @@ -7817,6 +7818,11 @@ HRESULT DacRefWalker::Init() hr = NextThread(); } + if ((mHandleMask & CorHandleStrong) && SUCCEEDED(hr)) + { + hr = WalkExternalMemoryHandles(); + } + return hr; } @@ -7863,6 +7869,78 @@ UINT32 DacRefWalker::GetHandleWalkerMask() return result; } +HRESULT DacRefWalker::WalkExternalMemoryHandles() +{ + AppDomain* appDomain = AppDomain::GetCurrentDomain(); + if (appDomain == NULL) + return S_OK; + + ExternalMemoryScanContext context(this); + appDomain->GCScanExternalMemoryHandles(ExternalMemoryHandleCallback, &context); + return context.Result; +} + +void DacRefWalker::ExternalMemoryHandleCallback(PTR_PTR_Object ppObj, ScanContext *sc, uint32_t flags) +{ + ExternalMemoryScanContext* context = static_cast(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) @@ -7887,6 +7965,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; diff --git a/src/coreclr/debug/daccess/dacdbiimpl.h b/src/coreclr/debug/daccess/dacdbiimpl.h index e9416d137f8a2d..fbe342d4424efa 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.h +++ b/src/coreclr/debug/daccess/dacdbiimpl.h @@ -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(); @@ -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; @@ -967,6 +981,12 @@ class DacRefWalker // Handles DacHandleWalker *mHandleWalker; + + // External memory handles + DacReferenceList mExternalMemoryHandles; + unsigned int mExternalMemoryHandleIndex; + DacHeapWalker mExternalMemoryHeap; + bool mExternalMemoryHeapInitialized; }; #endif // _DACDBI_IMPL_H_ diff --git a/src/coreclr/inc/CrstTypes.def b/src/coreclr/inc/CrstTypes.def index 1f5932d2ef8372..139fec19980fba 100644 --- a/src/coreclr/inc/CrstTypes.def +++ b/src/coreclr/inc/CrstTypes.def @@ -188,6 +188,10 @@ Crst ExecutableAllocatorLock AcquiredAfter LoaderHeap UMEntryThunkFreeListLock COMCallWrapper End +Crst ExternalMemoryHandle + Unordered +End + Crst FCall AcquiredBefore LoaderHeap End diff --git a/src/coreclr/inc/crsttypes_generated.h b/src/coreclr/inc/crsttypes_generated.h index 150c3b25d865f1..f7312a0ce6429c 100644 --- a/src/coreclr/inc/crsttypes_generated.h +++ b/src/coreclr/inc/crsttypes_generated.h @@ -45,82 +45,83 @@ enum CrstType CrstEventStore = 27, CrstException = 28, CrstExecutableAllocatorLock = 29, - CrstFCall = 30, - CrstFrozenObjectHeap = 31, - CrstFuncPtrStubs = 32, - CrstFusionAppCtx = 33, - CrstGCCover = 34, - CrstGenericDictionaryExpansion = 35, - CrstGlobalStrLiteralMap = 36, - CrstHandleTable = 37, - CrstIJWFixupData = 38, - CrstIJWHash = 39, - CrstILStubGen = 40, - CrstInlineTrackingMap = 41, - CrstInstMethodHashTable = 42, - CrstInterfaceDispatchGlobalLists = 43, - CrstInterop = 44, - CrstInteropData = 45, - CrstIsJMCMethod = 46, - CrstISymUnmanagedReader = 47, - CrstJit = 48, - CrstJitInlineTrackingMap = 49, - CrstJitPatchpoint = 50, - CrstJumpStubCache = 51, - CrstLeafLock = 52, - CrstListLock = 53, - CrstLoaderAllocator = 54, - CrstLoaderAllocatorReferences = 55, - CrstLoaderHeap = 56, - CrstManagedObjectWrapperMap = 57, - CrstMethodDescBackpatchInfoTracker = 58, - CrstMethodTableExposedObject = 59, - CrstModule = 60, - CrstModuleLookupTable = 61, - CrstMulticoreJitHash = 62, - CrstMulticoreJitManager = 63, - CrstNativeImageEagerFixups = 64, - CrstNativeImageLoad = 65, - CrstNotifyGdb = 66, - CrstPEImage = 67, - CrstPendingTypeLoadEntry = 68, - CrstPerfMap = 69, - CrstPgoData = 70, - CrstPinnedByrefValidation = 71, - CrstPinnedHeapHandleTable = 72, - CrstPregeneratedStringThunks = 73, - CrstProfilerGCRefDataFreeList = 74, - CrstProfilingAPIStatus = 75, - CrstRCWCache = 76, - CrstRCWCleanupList = 77, - CrstReadyToRunEntryPointToMethodDescMap = 78, - CrstReflection = 79, - CrstReJITGlobalRequest = 80, - CrstSigConvert = 81, - CrstSingleUseLock = 82, - CrstStressLog = 83, - CrstStubCache = 84, - CrstStubDispatchCache = 85, - CrstSyncBlockCache = 86, - CrstSyncHashLock = 87, - CrstSystemDomain = 88, - CrstSystemDomainDelayedUnloadList = 89, - CrstThreadIdDispenser = 90, - CrstThreadLocalStorageLock = 91, - CrstThreadStore = 92, - CrstTieredCompilation = 93, - CrstTypeEquivalenceMap = 94, - CrstTypeIDMap = 95, - CrstUMEntryThunkCache = 96, - CrstUMEntryThunkFreeListLock = 97, - CrstUniqueStack = 98, - CrstUnresolvedClassLock = 99, - CrstUnwindInfoTablePendingLock = 100, - CrstUnwindInfoTablePublishLock = 101, - CrstVSDIndirectionCellLock = 102, - CrstWebcilImageRelocation = 103, - CrstWrapperTemplate = 104, - kNumberOfCrstTypes = 105 + CrstExternalMemoryHandle = 30, + CrstFCall = 31, + CrstFrozenObjectHeap = 32, + CrstFuncPtrStubs = 33, + CrstFusionAppCtx = 34, + CrstGCCover = 35, + CrstGenericDictionaryExpansion = 36, + CrstGlobalStrLiteralMap = 37, + CrstHandleTable = 38, + CrstIJWFixupData = 39, + CrstIJWHash = 40, + CrstILStubGen = 41, + CrstInlineTrackingMap = 42, + CrstInstMethodHashTable = 43, + CrstInterfaceDispatchGlobalLists = 44, + CrstInterop = 45, + CrstInteropData = 46, + CrstIsJMCMethod = 47, + CrstISymUnmanagedReader = 48, + CrstJit = 49, + CrstJitInlineTrackingMap = 50, + CrstJitPatchpoint = 51, + CrstJumpStubCache = 52, + CrstLeafLock = 53, + CrstListLock = 54, + CrstLoaderAllocator = 55, + CrstLoaderAllocatorReferences = 56, + CrstLoaderHeap = 57, + CrstManagedObjectWrapperMap = 58, + CrstMethodDescBackpatchInfoTracker = 59, + CrstMethodTableExposedObject = 60, + CrstModule = 61, + CrstModuleLookupTable = 62, + CrstMulticoreJitHash = 63, + CrstMulticoreJitManager = 64, + CrstNativeImageEagerFixups = 65, + CrstNativeImageLoad = 66, + CrstNotifyGdb = 67, + CrstPEImage = 68, + CrstPendingTypeLoadEntry = 69, + CrstPerfMap = 70, + CrstPgoData = 71, + CrstPinnedByrefValidation = 72, + CrstPinnedHeapHandleTable = 73, + CrstPregeneratedStringThunks = 74, + CrstProfilerGCRefDataFreeList = 75, + CrstProfilingAPIStatus = 76, + CrstRCWCache = 77, + CrstRCWCleanupList = 78, + CrstReadyToRunEntryPointToMethodDescMap = 79, + CrstReflection = 80, + CrstReJITGlobalRequest = 81, + CrstSigConvert = 82, + CrstSingleUseLock = 83, + CrstStressLog = 84, + CrstStubCache = 85, + CrstStubDispatchCache = 86, + CrstSyncBlockCache = 87, + CrstSyncHashLock = 88, + CrstSystemDomain = 89, + CrstSystemDomainDelayedUnloadList = 90, + CrstThreadIdDispenser = 91, + CrstThreadLocalStorageLock = 92, + CrstThreadStore = 93, + CrstTieredCompilation = 94, + CrstTypeEquivalenceMap = 95, + CrstTypeIDMap = 96, + CrstUMEntryThunkCache = 97, + CrstUMEntryThunkFreeListLock = 98, + CrstUniqueStack = 99, + CrstUnresolvedClassLock = 100, + CrstUnwindInfoTablePendingLock = 101, + CrstUnwindInfoTablePublishLock = 102, + CrstVSDIndirectionCellLock = 103, + CrstWebcilImageRelocation = 104, + CrstWrapperTemplate = 105, + kNumberOfCrstTypes = 106 }; #endif // __CRST_TYPES_INCLUDED @@ -161,6 +162,7 @@ int g_rgCrstLevelMap[] = 0, // CrstEventStore 0, // CrstException 0, // CrstExecutableAllocatorLock + -1, // CrstExternalMemoryHandle 3, // CrstFCall -1, // CrstFrozenObjectHeap 6, // CrstFuncPtrStubs @@ -271,6 +273,7 @@ LPCSTR g_rgCrstNameMap[] = "CrstEventStore", "CrstException", "CrstExecutableAllocatorLock", + "CrstExternalMemoryHandle", "CrstFCall", "CrstFrozenObjectHeap", "CrstFuncPtrStubs", diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index c7a6c6549b3e45..2129c3d94a60e9 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -1652,6 +1652,11 @@ AppDomain::~AppDomain() CONTRACTL_END; m_AssemblyCache.Clear(); + + while (!m_externalMemoryHandles.IsEmpty()) + { + delete m_externalMemoryHandles.RemoveHead(); + } } //***************************************************************************** @@ -1676,6 +1681,7 @@ void AppDomain::Init() m_crstGenericDictionaryExpansionLock.Init(CrstGenericDictionaryExpansion); m_FileLoadLock.Init(CrstAssemblyLoader, CrstFlags(CRST_DEFAULT)); m_DomainCacheCrst.Init(CrstAppDomainCache); + m_externalMemoryHandlesCrst.Init(CrstExternalMemoryHandle, CrstFlags(CRST_UNSAFE_COOPGC | CRST_TAKEN_DURING_SHUTDOWN)); // Has to switch thread to GC_NOTRIGGER while being held m_crstAssemblyList.Init(CrstAssemblyList, CrstFlags( @@ -3786,23 +3792,74 @@ void AppDomain::EnumStaticGCRefs(promote_func* fn, ScanContext* sc) } } -void AppDomain::GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc) +ExternalMemoryHandle* AppDomain::AddExternalMemoryHandle(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_COOPERATIVE; + CAN_TAKE_LOCK; + } + CONTRACTL_END; + + _ASSERTE(pMT != nullptr); + _ASSERTE(pMemory != nullptr); + + ExternalMemoryHandle* handle = new ExternalMemoryHandle(pMT, pMemory, gcFlags); + + { + CrstHolder lock(&m_externalMemoryHandlesCrst); + m_externalMemoryHandles.InsertTail(handle); + } + + return handle; +} + +void AppDomain::RemoveExternalMemoryHandle(ExternalMemoryHandle* handle) { CONTRACTL { NOTHROW; GC_NOTRIGGER; + MODE_COOPERATIVE; + CAN_TAKE_LOCK; } CONTRACTL_END; - for (SListElem* item = m_externalMemoryHandles.GetHead(); item != NULL; item = SListTail>::GetNext(item)) + _ASSERTE(handle != nullptr); + + bool removed; { - item->GetValue().GCScanRoot(fn, sc); + CrstHolder lock(&m_externalMemoryHandlesCrst); + removed = m_externalMemoryHandles.RemoveFirst(handle); } + + _ASSERTE(removed); + if (!removed) + return; + + delete handle; } #endif // !DACCESS_COMPILE +void AppDomain::GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + } + CONTRACTL_END; + + // Mutations happen in cooperative mode, so the suspended EE makes the list stable during a GC. + for (ExternalMemoryHandle* handle = m_externalMemoryHandles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) + { + handle->GCScanRoot(fn, sc); + } +} + //------------------------------------------------------------------------ PTR_LoaderAllocator AppDomain::GetLoaderAllocator() { @@ -4217,6 +4274,12 @@ AppDomain::EnumMemoryRegions(CLRDataEnumMemoryFlags flags, bool enumThis) { pAssembly->EnumMemoryRegions(flags); } + + for (ExternalMemoryHandle* handle = m_externalMemoryHandles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) + { + DacEnumMemoryRegion(dac_cast(handle), sizeof(ExternalMemoryHandle)); + handle->EnumMemoryRegions(flags); + } } void diff --git a/src/coreclr/vm/appdomain.hpp b/src/coreclr/vm/appdomain.hpp index d0a89d11f5055e..a3577ae98cd59c 100644 --- a/src/coreclr/vm/appdomain.hpp +++ b/src/coreclr/vm/appdomain.hpp @@ -1538,10 +1538,15 @@ class AppDomain final friend struct cdac_data; public: +#ifndef DACCESS_COMPILE + ExternalMemoryHandle* AddExternalMemoryHandle(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags); + void RemoveExternalMemoryHandle(ExternalMemoryHandle* handle); +#endif void GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc); private: - SListTail> m_externalMemoryHandles; + CrstExplicitInit m_externalMemoryHandlesCrst; + SListTail m_externalMemoryHandles; }; // class AppDomain template<> @@ -1550,6 +1555,7 @@ struct cdac_data static constexpr size_t RootAssembly = offsetof(AppDomain, m_pRootAssembly); static constexpr size_t AssemblyList = offsetof(AppDomain, m_Assemblies) + offsetof(AppDomain::AssemblyList, m_array); static constexpr size_t FriendlyName = offsetof(AppDomain, m_friendlyName); + static constexpr size_t ExternalMemoryHandles = offsetof(AppDomain, m_externalMemoryHandles) + offsetof(SListTail, m_pHead); }; typedef DPTR(class SystemDomain) PTR_SystemDomain; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 25c22dc3a928fc..3030fb4aa8bd53 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -481,8 +481,17 @@ CDAC_TYPE_INDETERMINATE(AppDomain) CDAC_TYPE_FIELD(AppDomain, T_POINTER, RootAssembly, cdac_data::RootAssembly) CDAC_TYPE_FIELD(AppDomain, TYPE(ArrayListBase), AssemblyList, cdac_data::AssemblyList) CDAC_TYPE_FIELD(AppDomain, T_POINTER, FriendlyName, cdac_data::FriendlyName) +CDAC_TYPE_FIELD(AppDomain, T_POINTER, ExternalMemoryHandles, cdac_data::ExternalMemoryHandles) CDAC_TYPE_END(AppDomain) +CDAC_TYPE_BEGIN(ExternalMemoryHandle) +CDAC_TYPE_INDETERMINATE(ExternalMemoryHandle) +CDAC_TYPE_FIELD(ExternalMemoryHandle, T_POINTER, Next, cdac_data::Next) +CDAC_TYPE_FIELD(ExternalMemoryHandle, T_POINTER, MethodTable, cdac_data::MethodTable) +CDAC_TYPE_FIELD(ExternalMemoryHandle, T_POINTER, Memory, cdac_data::Memory) +CDAC_TYPE_FIELD(ExternalMemoryHandle, T_UINT32, GCFlags, cdac_data::GCFlags) +CDAC_TYPE_END(ExternalMemoryHandle) + CDAC_TYPE_BEGIN(SystemDomain) CDAC_TYPE_INDETERMINATE(SystemDomain) CDAC_TYPE_FIELD(SystemDomain, TYPE(LoaderAllocator), GlobalLoaderAllocator, cdac_data::GlobalLoaderAllocator) @@ -1888,7 +1897,7 @@ CDAC_GLOBAL_CONTRACT(RuntimeMutableTypeSystem, c1) CDAC_GLOBAL_CONTRACT(Exception, c1) CDAC_GLOBAL_CONTRACT(ExecutionManager, c1) CDAC_GLOBAL_CONTRACT(GCInfo, c1) -CDAC_GLOBAL_CONTRACT(Loader, c1) +CDAC_GLOBAL_CONTRACT(Loader, c2) CDAC_GLOBAL_CONTRACT(ManagedTypeSource, c1) CDAC_GLOBAL_CONTRACT(Notifications, c1) #ifdef FEATURE_OBJCMARSHAL diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp index 56bed93eecae69..0e5a9ac8b968dd 100644 --- a/src/coreclr/vm/externalmemoryhandle.cpp +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -13,7 +13,7 @@ void ExternalMemoryHandle::GCScanRoot(promote_func *fn, ScanContext *sc) } CONTRACTL_END; - auto fromAddress = m_pMemory; + PTR_VOID fromAddress = m_pMemory; if (m_pMT->IsValueType()) { ReportPointersFromValueType(fn, sc, m_pMT, m_pMemory); @@ -30,8 +30,23 @@ void ExternalMemoryHandle::GCScanRoot(promote_func *fn, ScanContext *sc) (*fn)((PTR_PTR_Object)m_pMemory, sc, 0); } - auto toAddress = m_pMemory; + PTR_VOID toAddress = m_pMemory; LOG((LF_GC, INFO3, "External Memory Handle promoted" FMT_ADDR "to" FMT_ADDR "\n", DBG_ADDR(fromAddress), DBG_ADDR(toAddress))); } } + +#ifdef DACCESS_COMPILE + +void ExternalMemoryHandle::EnumMemoryRegions(CLRDataEnumMemoryFlags flags) +{ + SUPPORTS_DAC; + + m_pMT.EnumMem(); + m_pMT->EnumMemoryRegions(flags); + + TSIZE_T size = m_pMT->IsValueType() ? m_pMT->GetNumInstanceFieldBytes() : TARGET_POINTER_SIZE; + DacEnumMemoryRegion(m_pMemory.GetAddr(), size); +} + +#endif // DACCESS_COMPILE diff --git a/src/coreclr/vm/externalmemoryhandle.h b/src/coreclr/vm/externalmemoryhandle.h index 9607d1e3d369fc..d48d44de55c8cf 100644 --- a/src/coreclr/vm/externalmemoryhandle.h +++ b/src/coreclr/vm/externalmemoryhandle.h @@ -7,26 +7,45 @@ #include "common.h" #include "gcinterface.h" +class ExternalMemoryHandle; +typedef DPTR(ExternalMemoryHandle) PTR_ExternalMemoryHandle; + // Represents a handle to external memory that can be scanned by the garbage collector. class ExternalMemoryHandle final { + friend struct cdac_data; + public: // Create a reference to external memory. The memory should point to a value that represents PTR_MethodTable. // So for example, if pMT is a struct, pMemory is the value of the struct. // If pMT is a class type, pMemory is the PTR_Object pointing to the instance of the class on the GC heap. ExternalMemoryHandle(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags) - : m_pMT(pMT), m_pMemory(pMemory), m_gcFlags(gcFlags) + : m_pNext(nullptr), m_pMT(pMT), m_pMemory(pMemory), m_gcFlags(gcFlags) { } void GCScanRoot(promote_func *fn, ScanContext *sc); +#ifdef DACCESS_COMPILE + void EnumMemoryRegions(CLRDataEnumMemoryFlags flags); +#endif + + // Next pointer for SList linkage. + PTR_ExternalMemoryHandle m_pNext; + private: PTR_MethodTable m_pMT; PTR_VOID m_pMemory; UINT m_gcFlags; }; -typedef DPTR(ExternalMemoryHandle) PTR_ExternalMemoryHandle; +template<> +struct cdac_data +{ + static constexpr size_t Next = offsetof(ExternalMemoryHandle, m_pNext); + static constexpr size_t MethodTable = offsetof(ExternalMemoryHandle, m_pMT); + static constexpr size_t Memory = offsetof(ExternalMemoryHandle, m_pMemory); + static constexpr size_t GCFlags = offsetof(ExternalMemoryHandle, m_gcFlags); +}; #endif // EXTERNALMEMORYHANDLE_HPP diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index 592d1c8fa65df2..8e7976c4e54500 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -335,7 +335,9 @@ void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, } } - if (::GetAppDomain()) + // External memory handles are a single global list. Only one server GC thread may update its + // root slots during relocation. + if (sc->thread_number == 0 && ::GetAppDomain()) { ::GetAppDomain()->GCScanExternalMemoryHandles(fn, sc); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs index 42da5b04d49d59..a7b9d6da13a613 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs @@ -84,6 +84,13 @@ public enum ModuleLookupMapKind public readonly record struct LoaderHeapBlock(TargetPointer Address, TargetNUInt Size); +public sealed class ExternalMemoryHandleRootData +{ + public bool IsInteriorPointer { get; init; } + public TargetPointer Address { get; init; } + public TargetPointer Object { get; init; } +} + public interface ILoader : IContract { static string IContract.Name => nameof(Loader); @@ -94,6 +101,7 @@ public interface ILoader : IContract TargetPointer GetRootAssembly() => throw new NotImplementedException(); string GetAppDomainFriendlyName() => throw new NotImplementedException(); TargetPointer GetAppDomain() => throw new NotImplementedException(); + IReadOnlyList GetExternalMemoryHandleRoots(bool resolveInteriorPointers) => throw new NotImplementedException(); TargetPointer GetModule(ModuleHandle handle) => throw new NotImplementedException(); TargetPointer GetAssembly(ModuleHandle handle) => throw new NotImplementedException(); TargetPointer GetPEAssembly(ModuleHandle handle) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs index 069f7c266f0aa8..b18be11e20b354 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs @@ -183,6 +183,10 @@ public interface IRuntimeTypeSystem : IContract bool ContainsGCPointers(ITypeHandle typeHandle) => throw new NotImplementedException(); // True if MethodTable represents a byreflike value (Span, ReadOnlySpan, etc.). bool IsByRefLike(ITypeHandle typeHandle) => throw new NotImplementedException(); + // 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. + bool IsInlineArray(ITypeHandle typeHandle) => throw new NotImplementedException(); // 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 diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs index ee8939779b4301..837d50c0f4bca8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs @@ -163,6 +163,8 @@ TargetPointer ILoader.GetAppDomain() return appDomain; } + IReadOnlyList ILoader.GetExternalMemoryHandleRoots(bool resolveInteriorPointers) => []; + TargetPointer ILoader.GetModule(ModuleHandle handle) { return handle.Address; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_2.cs new file mode 100644 index 00000000000000..619c91e23b17c8 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_2.cs @@ -0,0 +1,222 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal readonly struct Loader_2 : ILoader +{ + private readonly Target _target; + private readonly Loader_1 _v1; + + internal Loader_2(Target target) + { + _target = target; + _v1 = new Loader_1(target); + } + + IReadOnlyList ILoader.GetExternalMemoryHandleRoots(bool resolveInteriorPointers) + { + List roots = []; + TargetPointer appDomain = ((ILoader)_v1).GetAppDomain(); + if (appDomain == TargetPointer.Null) + return roots; + + Data.AppDomain domain = _target.ProcessedData.GetOrAdd(appDomain); + IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; + GCInteriorPointerResolver? interiorPointerResolver = null; + + HashSet visited = new(); + TargetPointer current = domain.ExternalMemoryHandles; + while (current != TargetPointer.Null) + { + if (!visited.Add(current)) + { + // Defend against a corrupted/cyclic list rather than looping forever. + throw new InvalidOperationException("ExternalMemoryHandle list is cyclic."); + } + + Data.ExternalMemoryHandle handle = _target.ProcessedData.GetOrAdd(current); + ITypeHandle typeHandle = rts.GetTypeHandle(handle.MethodTable); + + if (rts.IsValueType(typeHandle)) + { + AddValueTypeRoots(roots, rts, typeHandle, handle.Memory, resolveInteriorPointers, ref interiorPointerResolver); + } + else if (handle.GCFlags != 0) + { + AddInteriorRoot(roots, handle.Memory, resolveInteriorPointers, ref interiorPointerResolver); + } + else + { + roots.Add(new ExternalMemoryHandleRootData { Address = handle.Memory }); + } + + current = handle.Next; + } + + return roots; + } + + private void AddValueTypeRoots( + List roots, + IRuntimeTypeSystem rts, + ITypeHandle typeHandle, + TargetPointer memory, + bool resolveInteriorPointers, + ref GCInteriorPointerResolver? interiorPointerResolver) + { + ulong pointerSize = (ulong)_target.PointerSize; + + if (rts.IsByRefLike(typeHandle)) + { + foreach (ulong slotAddress in EnumerateByRefLikeInteriorSlots(rts, typeHandle, memory.Value, (uint)_target.PointerSize)) + AddInteriorRoot(roots, new TargetPointer(slotAddress), resolveInteriorPointers, ref interiorPointerResolver); + } + + if (rts.ContainsGCPointers(typeHandle)) + { + foreach ((uint seriesOffset, uint seriesSize) in rts.GetGCDescSeries(typeHandle)) + { + // GCDesc series offsets include the boxed object's MethodTable pointer. + ulong fieldStart = memory.Value + seriesOffset - pointerSize; + for (ulong suboffset = 0; suboffset < seriesSize; suboffset += pointerSize) + roots.Add(new ExternalMemoryHandleRootData { Address = new TargetPointer(fieldStart + suboffset) }); + } + } + } + + private void AddInteriorRoot( + List roots, + TargetPointer slotAddress, + bool resolveInteriorPointers, + ref GCInteriorPointerResolver? interiorPointerResolver) + { + TargetPointer obj = _target.ReadPointer(slotAddress.Value); + if (obj == TargetPointer.Null || obj.Value == ulong.MaxValue) + return; + + if (resolveInteriorPointers) + { + interiorPointerResolver ??= new GCInteriorPointerResolver(_target); + obj = interiorPointerResolver.Resolve(obj); + if (obj == TargetPointer.Null) + return; + } + + roots.Add(new ExternalMemoryHandleRootData + { + IsInteriorPointer = true, + Address = slotAddress, + Object = obj, + }); + } + + private static IEnumerable EnumerateByRefLikeInteriorSlots( + IRuntimeTypeSystem rts, + ITypeHandle typeHandle, + ulong baseAddress, + uint pointerSize) + { + bool isInlineArray = rts.IsInlineArray(typeHandle); + + foreach (TargetPointer fieldDesc in rts.GetFieldDescList(typeHandle)) + { + if (rts.IsFieldDescStatic(fieldDesc)) + continue; + + CorElementType fieldType = rts.GetFieldDescType(fieldDesc); + ITypeHandle? fieldTypeHandle = fieldType == CorElementType.ValueType + ? rts.GetFieldDescApproxTypeHandle(fieldDesc) + : null; + + if (isInlineArray) + { + uint elementSize = fieldType == CorElementType.Byref + ? pointerSize + : fieldTypeHandle is not null ? rts.GetNumInstanceFieldBytes(fieldTypeHandle) : 0; + if (elementSize == 0) + continue; + + uint totalSize = rts.GetNumInstanceFieldBytes(typeHandle); + for (uint elementOffset = 0; elementOffset < totalSize; elementOffset += elementSize) + { + foreach (ulong slot in EnumerateByRefLikeFieldSlots(rts, fieldType, fieldTypeHandle, baseAddress + elementOffset, pointerSize)) + yield return slot; + } + } + else + { + uint fieldOffset = rts.GetFieldDescOffset(fieldDesc, fieldDef: null); + foreach (ulong slot in EnumerateByRefLikeFieldSlots(rts, fieldType, fieldTypeHandle, baseAddress + fieldOffset, pointerSize)) + yield return slot; + } + } + } + + private static IEnumerable EnumerateByRefLikeFieldSlots( + IRuntimeTypeSystem rts, + CorElementType fieldType, + ITypeHandle? fieldTypeHandle, + ulong fieldAddress, + uint pointerSize) + { + if (fieldType == CorElementType.Byref) + { + yield return fieldAddress; + } + else if (fieldType == CorElementType.ValueType && fieldTypeHandle is not null && rts.IsByRefLike(fieldTypeHandle)) + { + foreach (ulong slot in EnumerateByRefLikeInteriorSlots(rts, fieldTypeHandle, fieldAddress, pointerSize)) + yield return slot; + } + } + + ModuleHandle ILoader.GetModuleHandleFromModulePtr(TargetPointer modulePointer) => ((ILoader)_v1).GetModuleHandleFromModulePtr(modulePointer); + ModuleHandle ILoader.GetModuleHandleFromAssemblyPtr(TargetPointer assemblyPointer) => ((ILoader)_v1).GetModuleHandleFromAssemblyPtr(assemblyPointer); + IEnumerable ILoader.GetModuleHandles(TargetPointer appDomain, AssemblyIterationFlags iterationFlags) => ((ILoader)_v1).GetModuleHandles(appDomain, iterationFlags); + TargetPointer ILoader.GetRootAssembly() => ((ILoader)_v1).GetRootAssembly(); + string ILoader.GetAppDomainFriendlyName() => ((ILoader)_v1).GetAppDomainFriendlyName(); + TargetPointer ILoader.GetAppDomain() => ((ILoader)_v1).GetAppDomain(); + TargetPointer ILoader.GetModule(ModuleHandle handle) => ((ILoader)_v1).GetModule(handle); + TargetPointer ILoader.GetAssembly(ModuleHandle handle) => ((ILoader)_v1).GetAssembly(handle); + TargetPointer ILoader.GetPEAssembly(ModuleHandle handle) => ((ILoader)_v1).GetPEAssembly(handle); + bool ILoader.TryGetLoadedImageContents(ModuleHandle handle, out TargetPointer baseAddress, out uint size, out uint imageFlags) => ((ILoader)_v1).TryGetLoadedImageContents(handle, out baseAddress, out size, out imageFlags); + TargetPointer ILoader.GetILAddr(TargetPointer peAssemblyPtr, int rva) => ((ILoader)_v1).GetILAddr(peAssemblyPtr, rva); + TargetPointer ILoader.GetFieldAddressFromRva(TargetPointer peAssemblyPtr, int rva) => ((ILoader)_v1).GetFieldAddressFromRva(peAssemblyPtr, rva); + bool ILoader.TryGetSymbolStream(ModuleHandle handle, out TargetPointer buffer, out uint size) => ((ILoader)_v1).TryGetSymbolStream(handle, out buffer, out size); + IEnumerable ILoader.GetAvailableTypeParams(ModuleHandle handle) => ((ILoader)_v1).GetAvailableTypeParams(handle); + IEnumerable ILoader.GetInstantiatedMethods(ModuleHandle handle) => ((ILoader)_v1).GetInstantiatedMethods(handle); + bool ILoader.IsProbeExtensionResultValid(ModuleHandle handle) => ((ILoader)_v1).IsProbeExtensionResultValid(handle); + ModuleFlags ILoader.GetFlags(ModuleHandle handle) => ((ILoader)_v1).GetFlags(handle); + bool ILoader.IsReadyToRun(ModuleHandle handle) => ((ILoader)_v1).IsReadyToRun(handle); + string ILoader.GetSimpleName(ModuleHandle handle) => ((ILoader)_v1).GetSimpleName(handle); + string ILoader.GetPath(ModuleHandle handle) => ((ILoader)_v1).GetPath(handle); + string ILoader.GetFileName(ModuleHandle handle) => ((ILoader)_v1).GetFileName(handle); + bool ILoader.GetFileHeadersInfo(ModuleHandle handle, out uint timeStamp, out uint imageSize) => ((ILoader)_v1).GetFileHeadersInfo(handle, out timeStamp, out imageSize); + TargetPointer ILoader.GetLoaderAllocator(ModuleHandle handle) => ((ILoader)_v1).GetLoaderAllocator(handle); + TargetPointer ILoader.GetILBase(ModuleHandle handle) => ((ILoader)_v1).GetILBase(handle); + TargetPointer ILoader.GetAssemblyLoadContext(ModuleHandle handle) => ((ILoader)_v1).GetAssemblyLoadContext(handle); + TargetPointer ILoader.GetModuleLookupMapBase(ModuleHandle module, ModuleLookupMapKind kind) => ((ILoader)_v1).GetModuleLookupMapBase(module, kind); + TargetPointer ILoader.GetModuleLookupMapElement(ModuleHandle module, ModuleLookupMapKind kind, uint token, out TargetNUInt flags) => ((ILoader)_v1).GetModuleLookupMapElement(module, kind, token, out flags); + TargetPointer ILoader.LookupMemberRefAsMethod(ModuleHandle handle, uint token) => ((ILoader)_v1).LookupMemberRefAsMethod(handle, token); + IEnumerable<(TargetPointer Value, uint Token)> ILoader.EnumerateModuleLookupMap(ModuleHandle module, ModuleLookupMapKind kind) => ((ILoader)_v1).EnumerateModuleLookupMap(module, kind); + bool ILoader.IsCollectible(ModuleHandle handle) => ((ILoader)_v1).IsCollectible(handle); + bool ILoader.IsDynamic(ModuleHandle handle) => ((ILoader)_v1).IsDynamic(handle); + bool ILoader.IsModuleMapped(ModuleHandle handle) => ((ILoader)_v1).IsModuleMapped(handle); + bool ILoader.IsAssemblyLoaded(ModuleHandle handle) => ((ILoader)_v1).IsAssemblyLoaded(handle); + TargetPointer ILoader.GetGlobalLoaderAllocator() => ((ILoader)_v1).GetGlobalLoaderAllocator(); + TargetPointer ILoader.GetSystemAssembly() => ((ILoader)_v1).GetSystemAssembly(); + TargetPointer ILoader.GetHighFrequencyHeap(TargetPointer loaderAllocatorPointer) => ((ILoader)_v1).GetHighFrequencyHeap(loaderAllocatorPointer); + TargetPointer ILoader.GetLowFrequencyHeap(TargetPointer loaderAllocatorPointer) => ((ILoader)_v1).GetLowFrequencyHeap(loaderAllocatorPointer); + TargetPointer ILoader.GetILHeader(ModuleHandle handle, uint token) => ((ILoader)_v1).GetILHeader(handle, token); + TargetPointer ILoader.GetObjectHandle(TargetPointer loaderAllocatorPointer) => ((ILoader)_v1).GetObjectHandle(loaderAllocatorPointer); + TargetPointer ILoader.GetDynamicIL(ModuleHandle handle, uint token) => ((ILoader)_v1).GetDynamicIL(handle, token); + IEnumerable ILoader.EnumerateLoaderHeapBlocks(TargetPointer loaderHeap) => ((ILoader)_v1).EnumerateLoaderHeapBlocks(loaderHeap); + IReadOnlyDictionary ILoader.GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer) => ((ILoader)_v1).GetLoaderAllocatorHeaps(loaderAllocatorPointer); + DebuggerAssemblyControlFlags ILoader.GetDebuggerInfoBits(ModuleHandle handle) => ((ILoader)_v1).GetDebuggerInfoBits(handle); + void ILoader.SetDebuggerInfoBits(ModuleHandle handle, DebuggerAssemblyControlFlags newBits) => ((ILoader)_v1).SetDebuggerInfoBits(handle, newBits); +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs index c2f37e674b193e..56f40019db79f8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs @@ -665,6 +665,7 @@ public TargetPointer GetWellKnownMethodTable(WellKnownMethodTable kind) public bool ContainsGCPointers(ITypeHandle typeHandle) => !typeHandle.IsMethodTable() ? false : _methodTables[typeHandle.Address].Flags.ContainsGCPointers; public bool IsByRefLike(ITypeHandle typeHandle) => typeHandle.IsMethodTable() && _methodTables[typeHandle.Address].Flags.IsByRefLike; + public bool IsInlineArray(ITypeHandle typeHandle) => typeHandle.IsMethodTable() && GetClassData(typeHandle).IsInlineArray; private bool IsFeatureHfaTarget(out RuntimeInfoArchitecture arch) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs new file mode 100644 index 00000000000000..1a0df494d8967a --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs @@ -0,0 +1,104 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; + +// Resolves an address that may point into the interior of a managed object to the address of the +// containing object by linearly walking the GC heap segment objects. This is used by GcScanContext +// to resolve interior stack roots and by RefWalk to resolve external memory handle roots. +public sealed class GCInteriorPointerResolver +{ + private readonly IGC _gc; + private readonly IRuntimeTypeSystem _rts; + + private readonly LinearReadCache _cache; + private readonly uint _numComponentsOffsetArray; + private readonly uint _numComponentsOffsetString; + private readonly ulong _methodTableOffset; + private readonly byte _objectToMethodTableUnmask; + + public GCInteriorPointerResolver(Target target) + { + _gc = target.Contracts.GC; + _rts = target.Contracts.RuntimeTypeSystem; + _cache = new LinearReadCache(target); + _numComponentsOffsetArray = (uint)Data.Array.GetNumComponentsOffset(target); + _numComponentsOffsetString = (uint)Data.String.GetStringLengthOffset(target); + _methodTableOffset = (ulong)Data.Object.GetMethodTableOffset(target); + _objectToMethodTableUnmask = target.ReadGlobal(Constants.Globals.ObjectToMethodTableUnmask); + } + + // Resolves obj (an address that may point anywhere within a managed object) to the address of + // the object that contains it, or TargetPointer.Null if obj does not fall within a live object + // on any GC heap segment (including if obj is itself null/invalid, or the heap data is corrupt). + public TargetPointer Resolve(TargetPointer obj) + { + TargetPointer outerObj = TargetPointer.Null; + foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) + { + if (obj.Value < seg.Start.Value || obj.Value >= seg.End.Value) + continue; + + TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); + ulong size = 0; + while (currentObj.Value <= obj.Value) + { + // Replicate IObject.GetMethodTableAddress in fast path with linear read cache + if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) + { + return TargetPointer.Null; + } + mt = mt.Value & (ulong)~_objectToMethodTableUnmask; + + // Replicate IObject.GetSize in fast path with linear read cache + if (!TryGetObjectSize(currentObj, mt, out size) || size == 0) + { + return TargetPointer.Null; + } + + size = _gc.AlignObjectSize(size, seg.Generation); + if (currentObj.Value + size > seg.End.Value || size == 0) + { + return TargetPointer.Null; + } + outerObj = currentObj; + currentObj = _gc.GetPotentialNextObjectAddress(currentObj, size, seg); + } + return outerObj + size > obj ? outerObj : TargetPointer.Null; + } + return outerObj; + } + + private bool TryGetObjectSize(TargetPointer objAddr, TargetPointer mt, out ulong size) + { + size = 0; + try + { + ITypeHandle handle = _rts.GetTypeHandle(mt); + ulong baseSize = _rts.GetBaseSize(handle); + uint componentSize = _rts.GetComponentSize(handle); + uint numComponentsOffset = 0; + if (componentSize != 0) + { + if (_rts.IsArray(handle, out _) || _rts.IsFreeObjectMethodTable(handle)) + numComponentsOffset = _numComponentsOffsetArray; + else if (_rts.IsString(handle)) + numComponentsOffset = _numComponentsOffsetString; + else + return false; // unrecognized component type + if (!_cache.TryReadUInt32(objAddr.Value + numComponentsOffset, out uint numComponents)) + return false; + baseSize += (ulong)componentSize * numComponents; + } + size = baseSize; + return true; + } + catch + { + // The MT may be corrupt — surface as a read failure. + return false; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index ead93db69edf11..5d1eacad18e8c9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -11,14 +11,8 @@ internal class GcScanContext { private readonly Target _target; - private readonly IGC _gc; - private readonly IRuntimeTypeSystem _rts; - - private readonly LinearReadCache _cache; - private readonly uint _numComponentsOffsetArray; - private readonly uint _numComponentsOffsetString; - private readonly ulong _methodTableOffset; - private readonly byte _objectToMethodTableUnmask; + private readonly GCInteriorPointerResolver _interiorPointerResolver; + public bool ResolveInteriorPointers { get; } public List StackRefs { get; } = []; public TargetPointer StackPointer { get; private set; } @@ -34,13 +28,7 @@ public GcScanContext(Target target, bool resolveInteriorPointers) { _target = target; ResolveInteriorPointers = resolveInteriorPointers; - _gc = target.Contracts.GC; - _rts = target.Contracts.RuntimeTypeSystem; - _cache = new LinearReadCache(target); - _numComponentsOffsetArray = (uint)Data.Array.GetNumComponentsOffset(target); - _numComponentsOffsetString = (uint)Data.String.GetStringLengthOffset(target); - _methodTableOffset = (ulong)Data.Object.GetMethodTableOffset(target); - _objectToMethodTableUnmask = target.ReadGlobal(Constants.Globals.ObjectToMethodTableUnmask); + _interiorPointerResolver = new GCInteriorPointerResolver(target); } public void UpdateScanContext(TargetPointer sp, TargetCodePointer ip, TargetPointer frame, StackRefData.SourceTypes? sourceTypeOverride = null) @@ -128,74 +116,7 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL StackRefs.Add(data); } - private TargetPointer GetInteriorPointer(TargetPointer obj) - { - TargetPointer outerObj = TargetPointer.Null; - foreach ((GCHeapSegmentInfo seg, GCHeapData _) in _gc.EnumerateAllSegments()) - { - if (obj.Value < seg.Start.Value || obj.Value >= seg.End.Value) - continue; - - TargetPointer currentObj = _gc.GetPotentialNextObjectAddress(seg.Start, 0, seg); - ulong size = 0; - while (currentObj.Value <= obj.Value) - { - // Replicate IObject.GetMethodTableAddress in fast path with linear read cache - if (!_cache.TryReadPointer(currentObj.Value + _methodTableOffset, out TargetPointer mt)) - { - return TargetPointer.Null; - } - mt = mt.Value & (ulong)~_objectToMethodTableUnmask; - - // Replicate IObject.GetSize in fast path with linear read cache - if (!TryGetObjectSize(currentObj, mt, out size) || size == 0) - { - return TargetPointer.Null; - } - - size = _gc.AlignObjectSize(size, seg.Generation); - if (currentObj.Value + size > seg.End.Value || size == 0) - { - return TargetPointer.Null; - } - outerObj = currentObj; - currentObj = _gc.GetPotentialNextObjectAddress(currentObj, size, seg); - } - return outerObj + size > obj ? outerObj : TargetPointer.Null; - } - return outerObj; - } - - private bool TryGetObjectSize(TargetPointer objAddr, TargetPointer mt, out ulong size) - { - size = 0; - try - { - ITypeHandle handle = _rts.GetTypeHandle(mt); - ulong baseSize = _rts.GetBaseSize(handle); - uint componentSize = _rts.GetComponentSize(handle); - uint numComponentsOffset = 0; - if (componentSize != 0) - { - if (_rts.IsArray(handle, out _) || _rts.IsFreeObjectMethodTable(handle)) - numComponentsOffset = _numComponentsOffsetArray; - else if (_rts.IsString(handle)) - numComponentsOffset = _numComponentsOffsetString; - else - return false; // unrecognized component type - if (!_cache.TryReadUInt32(objAddr.Value + numComponentsOffset, out uint numComponents)) - return false; - baseSize += (ulong)componentSize * numComponents; - } - size = baseSize; - return true; - } - catch - { - // The MT may be corrupt — surface as a read failure. - return false; - } - } + private TargetPointer GetInteriorPointer(TargetPointer obj) => _interiorPointerResolver.Resolve(obj); public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index e9b8312f726eab..4674cd0268ade6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -17,6 +17,7 @@ public static void Register(ContractRegistry registry) { registry.Register("c1", static t => new Exception_1(t)); registry.Register("c1", static t => new Loader_1(t)); + registry.Register("c2", static t => new Loader_2(t)); registry.Register("c1", static t => new EcmaMetadata_1(t)); registry.Register("c1", static t => new DacStreams_1(t)); registry.Register("c1", static t => new CodeVersions_1(t)); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AppDomain.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AppDomain.cs index acf55e4fce52e9..15d3a926defbb8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AppDomain.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AppDomain.cs @@ -12,4 +12,6 @@ internal sealed partial class AppDomain : IData public partial TargetPointer AssemblyList { get; } [Field] public partial TargetPointer FriendlyName { get; } + + [Field] public partial TargetPointer ExternalMemoryHandles { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs index 34d9581d9d728b..cf5d112b50a6f5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/EEClass.cs @@ -28,8 +28,13 @@ internal sealed partial class EEClass : IData [Field] public partial TargetPointer OptionalFields { get; } private const uint HasLayoutFlag = 0x00000040; + // EEClass::VMFLAG_INLINE_ARRAY (class.h): set for the compiler-generated buffer types backing + // C# inline arrays (InlineArrayAttribute). Their single instance field is repeated across the + // whole array rather than declared once per element. + private const uint InlineArrayFlag = 0x00010000; [Field] public partial uint? VMFlags { get; } public bool HasLayout => VMFlags.HasValue && (VMFlags.Value & HasLayoutFlag) != 0; + public bool IsInlineArray => VMFlags.HasValue && (VMFlags.Value & InlineArrayFlag) != 0; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExternalMemoryHandle.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExternalMemoryHandle.cs new file mode 100644 index 00000000000000..53bcf7e6f395f0 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExternalMemoryHandle.cs @@ -0,0 +1,13 @@ +// 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; + +[CdacType(nameof(DataType.ExternalMemoryHandle))] +internal sealed partial class ExternalMemoryHandle : IData +{ + [Field] public partial TargetPointer Next { get; } + [Field] public partial TargetPointer MethodTable { get; } + [Field] public partial TargetPointer Memory { get; } + [Field] public partial uint GCFlags { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index ab30d4293917d9..cb5995fae3f5db 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -43,6 +43,7 @@ public enum DataType Module, ModuleLookupMap, AppDomain, + ExternalMemoryHandle, Debugger, DebuggerRCThread, DebuggerPatchTable, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs index dfdf3ff5a47bb8..f12d8c432228a0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs @@ -39,6 +39,13 @@ private IEnumerable Walk() yield return reference; } + // External memory handles are strong roots, but are stored independently of the GC handle table. + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrong)) + { + foreach (DacGcReference reference in WalkExternalMemoryHandles()) + yield return reference; + } + if (_walkStacks) { foreach (DacGcReference reference in WalkStacks()) @@ -68,8 +75,7 @@ private IEnumerable WalkHandles() private HandleType[] GetRequestedHandleTypes() { - // Mirror native DacRefWalker::GetHandleWalkerMask: translate the CorGCReferenceType bits - // in the mask into the handle types consumed by IGC.GetHandles. + // Translate the CorGCReferenceType bits into the handle types consumed by IGC.GetHandles. List types = new(); if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrong)) types.Add(HandleType.Strong); @@ -126,6 +132,29 @@ private bool TryMapHandle(HandleData handle, out CorGCReferenceType dwType, out } } + // Walks the external memory handles registered with the current AppDomain. + private IEnumerable WalkExternalMemoryHandles() + { + ILoader loader = _target.Contracts.Loader; + + foreach (ExternalMemoryHandleRootData root in loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)) + { + DacGcReference reference = new() + { + vmDomain = _appDomain.Value, + dwType = CorGCReferenceType.CorHandleStrong, + i64ExtraData = 0, + }; + + if (root.IsInteriorPointer || root.Address == TargetPointer.Null) + reference.pObject = root.Object.Value | 1; + else + reference.objHnd = root.Address.Value; + + yield return reference; + } + } + private IEnumerable WalkStacks() { IThread threadContract = _target.Contracts.Thread; diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs index d013c487b01704..441736b8253400 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -139,4 +139,25 @@ public unsafe void WalkRefs_StacksOnly_MatchStackReferenceWalk(TestConfiguration Assert.Equal(expected, refs.Count); } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void WalkRefs_ExternalMemoryHandles_ContributeNothing_WhenAppDomainHasNone(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + ILoader loader = Target.Contracts.Loader; + IGC gc = Target.Contracts.GC; + + // The GCRoots debuggee does not create any ExternalMemoryHandle instances, so requesting + // the strong handle mask (which also drives external-memory-handle scanning, matching + // native DacRefWalker::Init) must report exactly the strong GC handles and nothing more. + Assert.Empty(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + + List refs = WalkAllRefs(dbi, walkStacks: false, handleWalkMask: CorGCReferenceType.CorHandleStrong); + + HashSet expectedHandles = gc.GetHandles([HandleType.Strong]).Select(h => h.Handle.Value).ToHashSet(); + HashSet walkedHandles = refs.Select(r => r.pObject).ToHashSet(); + Assert.Equal(expectedHandles, walkedHandles); + } } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index f016f625003d72..cce51961e16a7c 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -531,7 +531,13 @@ protected TargetCodePointer DefaultReadCodePointer(ulong address) } #endregion subclass reader helpers - public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) => throw new NotImplementedException(); + public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) + { + ulong value = PointerSize == sizeof(uint) + ? ReadFromSpan(bytes.Slice(0, sizeof(uint)), IsLittleEndian) + : ReadFromSpan(bytes.Slice(0, sizeof(ulong)), IsLittleEndian); + return new TargetPointer(value); + } public override Target.TypeInfo GetTypeInfo(string typeName) { diff --git a/src/native/managed/cdac/tests/UnitTests/LoaderExternalMemoryHandleTests.cs b/src/native/managed/cdac/tests/UnitTests/LoaderExternalMemoryHandleTests.cs new file mode 100644 index 00000000000000..714d128446ac2a --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/LoaderExternalMemoryHandleTests.cs @@ -0,0 +1,142 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class LoaderExternalMemoryHandleTests +{ + private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; + + private const ulong AppDomainStaticSlotAddr = 0x0500; + private const ulong AppDomainAddr = 0x1000; + private const ulong Handle1Addr = 0x2000; + private const ulong Handle2Addr = 0x2100; + private const ulong MethodTable1Addr = 0x9000; + private const ulong MethodTable2Addr = 0x9100; + private const ulong Memory1Addr = 0x9500; + private const ulong Memory2Addr = 0x9600; + + private static TestPlaceholderTarget CreateTarget(string version, bool hasAppDomain = true) + { + TargetTestHelpers helpers = new(Arch); + int ptrSize = helpers.PointerSize; + var rts = new Mock(MockBehavior.Strict); + ITypeHandle typeHandle1 = new TargetTypeHandle(new TargetPointer(MethodTable1Addr)); + ITypeHandle typeHandle2 = new TargetTypeHandle(new TargetPointer(MethodTable2Addr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTable1Addr))).Returns(typeHandle1); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTable2Addr))).Returns(typeHandle2); + rts.Setup(r => r.IsValueType(typeHandle1)).Returns(false); + rts.Setup(r => r.IsValueType(typeHandle2)).Returns(false); + + var targetBuilder = new TestPlaceholderTarget.Builder(Arch) + .AddGlobals(("AppDomain", AppDomainStaticSlotAddr)) + .AddTypes(new Dictionary + { + [DataType.AppDomain] = new() + { + Fields = new Dictionary + { + { nameof(Data.AppDomain.ExternalMemoryHandles), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, + } + }, + [DataType.ExternalMemoryHandle] = new() + { + Fields = new Dictionary + { + { nameof(Data.ExternalMemoryHandle.Next), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.MethodTable), new() { Offset = ptrSize, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.Memory), new() { Offset = 2 * ptrSize, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.GCFlags), new() { Offset = 3 * ptrSize, TypeName = DataType.uint32.ToString() } }, + } + }, + }) + .AddContract(version: version) + .AddMockContract(rts); + + // AppDomain* static slot -> the AppDomain instance + targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainStaticSlotAddr, hasAppDomain ? AppDomainAddr : 0)); + + // AppDomain.ExternalMemoryHandles -> Handle1 + targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainAddr, Handle1Addr)); + + // Handle1: Next -> Handle2, MethodTable1, Memory1, GCFlags=0 + targetBuilder.MemoryBuilder.AddHeapFragment(ExternalMemoryHandleFragment(helpers, Handle1Addr, Handle2Addr, MethodTable1Addr, Memory1Addr, 0)); + + // Handle2: Next -> null, MethodTable2, Memory2, GCFlags=1 + targetBuilder.MemoryBuilder.AddHeapFragment(ExternalMemoryHandleFragment(helpers, Handle2Addr, 0, MethodTable2Addr, Memory2Addr, 1)); + targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, Memory2Addr, 0x9700)); + + return targetBuilder.Build(); + } + + private static MockMemorySpace.HeapFragment PointerFragment(TargetTestHelpers helpers, ulong address, ulong value) + { + byte[] data = new byte[helpers.PointerSize]; + helpers.WritePointer(data, value); + return new MockMemorySpace.HeapFragment { Address = address, Data = data, Name = "Pointer" }; + } + + private static MockMemorySpace.HeapFragment ExternalMemoryHandleFragment(TargetTestHelpers helpers, ulong address, ulong next, ulong methodTable, ulong memory, uint gcFlags) + { + int ptrSize = helpers.PointerSize; + byte[] data = new byte[3 * ptrSize + sizeof(uint)]; + helpers.WritePointer(data.AsSpan(0, ptrSize), next); + helpers.WritePointer(data.AsSpan(ptrSize, ptrSize), methodTable); + helpers.WritePointer(data.AsSpan(2 * ptrSize, ptrSize), memory); + helpers.Write(data.AsSpan(3 * ptrSize, sizeof(uint)), gcFlags); + return new MockMemorySpace.HeapFragment { Address = address, Data = data, Name = "ExternalMemoryHandle" }; + } + + [Fact] + public void GetExternalMemoryHandleRoots_WalksChain() + { + TestPlaceholderTarget target = CreateTarget("c2"); + ILoader loader = target.Contracts.Loader; + + IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: false); + + Assert.Equal(2, roots.Count); + Assert.False(roots[0].IsInteriorPointer); + Assert.Equal(new TargetPointer(Memory1Addr), roots[0].Address); + Assert.True(roots[1].IsInteriorPointer); + Assert.Equal(new TargetPointer(Memory2Addr), roots[1].Address); + Assert.Equal(new TargetPointer(0x9700), roots[1].Object); + } + + [Fact] + public void GetExternalMemoryHandleRoots_Version1_AlwaysEmpty() + { + // Loader_1 must ignore the native ExternalMemoryHandle list entirely and report an empty + // sequence, even though the exact same backing memory (with a populated, valid chain) is + // present as it would be for Loader_2. Only contract version c2 enumerates real handles. + TestPlaceholderTarget target = CreateTarget("c1"); + ILoader loader = target.Contracts.Loader; + + IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true); + + Assert.Empty(roots); + } + + [Theory] + [InlineData("c1")] + [InlineData("c2")] + public void GetExternalMemoryHandleRoots_NullAppDomain_ReturnsEmpty(string version) + { + // GetExternalMemoryHandleRoots must return an empty sequence rather than throw, for both + // Loader_1 (always empty) and Loader_2 (nothing to enumerate from a null AppDomain). + TestPlaceholderTarget target = CreateTarget(version, hasAppDomain: false); + ILoader loader = target.Contracts.Loader; + + IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true); + + Assert.Empty(roots); + } + +} diff --git a/src/native/managed/cdac/tests/UnitTests/MethodTableTests.cs b/src/native/managed/cdac/tests/UnitTests/MethodTableTests.cs index 0d8a4c4d674102..973c5e11794e6b 100644 --- a/src/native/managed/cdac/tests/UnitTests/MethodTableTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/MethodTableTests.cs @@ -887,6 +887,57 @@ public void GetGCDescSeriesReturnsEmptyForNonMethodTable(MockTarget.Architecture Assert.Empty(contract.GetGCDescSeries(typeDescHandle)); } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsInlineArrayReturnsFalseWhenVMFlagNotSet(MockTarget.Architecture arch) + { + TargetPointer mtPtr = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + MockEEClass eeClass = rtsBuilder.AddEEClass("NotInlineArray"); + MockMethodTable mt = rtsBuilder.AddMethodTable("NotInlineArray"); + mt.BaseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + // EEClass.VMFlags does NOT have VMFLAG_INLINE_ARRAY (0x00010000) set + mtPtr = mt.Address; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + ITypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.False(contract.IsInlineArray(typeHandle)); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsInlineArrayReturnsTrueWhenVMFlagSet(MockTarget.Architecture arch) + { + const uint InlineArrayVMFlag = 0x00010000; + TargetPointer mtPtr = default; + TestPlaceholderTarget target = CreateTarget( + arch, + rtsBuilder => + { + MockEEClass eeClass = rtsBuilder.AddEEClass("InlineArray"); + eeClass.VMFlags = InlineArrayVMFlag; + MockMethodTable mt = rtsBuilder.AddMethodTable("InlineArray"); + mt.BaseSize = rtsBuilder.Builder.TargetTestHelpers.ObjectBaseSize; + mt.ParentMethodTable = rtsBuilder.SystemObjectMethodTable.Address; + mt.NumVirtuals = 3; + eeClass.MethodTable = mt.Address; + mt.EEClassOrCanonMT = eeClass.Address; + mtPtr = mt.Address; + }); + + IRuntimeTypeSystem contract = target.Contracts.RuntimeTypeSystem; + ITypeHandle typeHandle = contract.GetTypeHandle(mtPtr); + Assert.True(contract.IsInlineArray(typeHandle)); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetGCDescSeriesReturnsEmptyWhenNoGCPointers(MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs new file mode 100644 index 00000000000000..30aeeb4f5ab508 --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs @@ -0,0 +1,328 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +public class ExternalMemoryHandleRootTests +{ + private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; + private static readonly TargetPointer AppDomainAddr = new(0x1000); + private const ulong AppDomainStaticSlotAddr = 0x0500; + private const ulong HandleAddr = 0x1800; + private const ulong MethodTableAddr = 0x2000; + private const ulong ObjectSize = 0x100; + private const ulong ResolvedMethodTableAddr = 0x9000; + + private static TestPlaceholderTarget CreateTarget( + TargetPointer memory, + uint gcFlags, + Mock rts, + IEnumerable? fragments = null, + Mock? gc = null, + bool hasHandle = true) + { + TargetTestHelpers helpers = new(Arch); + int pointerSize = helpers.PointerSize; + Mock mockGC = gc ?? new Mock(); + if (gc is null) + mockGC.Setup(g => g.GetGCIdentifiers()).Returns([]); + + var builder = new TestPlaceholderTarget.Builder(Arch) + .AddGlobals(("AppDomain", AppDomainStaticSlotAddr)) + .AddTypes(new Dictionary + { + [DataType.AppDomain] = new() + { + Fields = new Dictionary + { + { nameof(Data.AppDomain.ExternalMemoryHandles), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, + } + }, + [DataType.ExternalMemoryHandle] = new() + { + Fields = new Dictionary + { + { nameof(Data.ExternalMemoryHandle.Next), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.MethodTable), new() { Offset = pointerSize, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.Memory), new() { Offset = 2 * pointerSize, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.GCFlags), new() { Offset = 3 * pointerSize, TypeName = DataType.uint32.ToString() } }, + } + }, + [DataType.Object] = TargetTestHelpers.CreateTypeInfo(MockObjectData.CreateLayout(Arch)), + [DataType.Array] = TargetTestHelpers.CreateTypeInfo(MockArrayObjectData.CreateLayout(Arch)), + [DataType.String] = TargetTestHelpers.CreateTypeInfo(MockStringObjectData.CreateLayout(Arch)), + }) + .AddGlobals((nameof(Constants.Globals.ObjectToMethodTableUnmask), 0ul)) + .AddContract(version: "c2") + .AddMockContract(mockGC) + .AddMockContract(rts); + + builder.MemoryBuilder.AddHeapFragment(PointerFragment(AppDomainStaticSlotAddr, AppDomainAddr.Value)); + builder.MemoryBuilder.AddHeapFragment(PointerFragment(AppDomainAddr.Value, hasHandle ? HandleAddr : 0)); + if (hasHandle) + builder.MemoryBuilder.AddHeapFragment(ExternalMemoryHandleFragment(memory.Value, gcFlags)); + + if (fragments is not null) + { + foreach (MockMemorySpace.HeapFragment fragment in fragments) + builder.MemoryBuilder.AddHeapFragment(fragment); + } + + return builder.Build(); + } + + private static void SetupResolvableObjects( + Mock gc, + Mock rts, + List fragments, + params ulong[] objectAddresses) + { + ulong segmentStart = objectAddresses[0] - ObjectSize; + ulong segmentEnd = objectAddresses[^1] + ObjectSize; + var segment = new GCHeapSegmentInfo(new TargetPointer(segmentStart), new TargetPointer(segmentEnd), GCSegmentClassification.Gen0); + + gc.Setup(g => g.GetGCIdentifiers()).Returns([GCIdentifiers.Workstation]); + gc.Setup(g => g.GetHeapData()).Returns(default(GCHeapData)); + gc.Setup(g => g.EnumerateHeapSegments(It.IsAny())).Returns([segment]); + gc.Setup(g => g.AlignObjectSize(ObjectSize, GCSegmentClassification.Gen0)).Returns(ObjectSize); + + ulong previous = segmentStart; + foreach (ulong objectAddress in objectAddresses) + { + gc.Setup(g => g.GetPotentialNextObjectAddress(new TargetPointer(previous), previous == segmentStart ? 0ul : ObjectSize, segment)) + .Returns(new TargetPointer(objectAddress)); + fragments.Add(PointerFragment(objectAddress, ResolvedMethodTableAddr)); + previous = objectAddress; + } + gc.Setup(g => g.GetPotentialNextObjectAddress(new TargetPointer(previous), ObjectSize, segment)) + .Returns(new TargetPointer(segmentEnd)); + + ITypeHandle resolvedTypeHandle = new TargetTypeHandle(new TargetPointer(ResolvedMethodTableAddr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(ResolvedMethodTableAddr))).Returns(resolvedTypeHandle); + rts.Setup(r => r.GetBaseSize(resolvedTypeHandle)).Returns((uint)ObjectSize); + rts.Setup(r => r.GetComponentSize(resolvedTypeHandle)).Returns(0u); + } + + private static MockMemorySpace.HeapFragment PointerFragment(ulong address, ulong value) + { + TargetTestHelpers helpers = new(Arch); + byte[] data = new byte[helpers.PointerSize]; + helpers.WritePointer(data, value); + return new MockMemorySpace.HeapFragment { Address = address, Data = data, Name = "Pointer" }; + } + + private static MockMemorySpace.HeapFragment ExternalMemoryHandleFragment(ulong memory, uint gcFlags) + { + TargetTestHelpers helpers = new(Arch); + int pointerSize = helpers.PointerSize; + byte[] data = new byte[3 * pointerSize + sizeof(uint)]; + helpers.WritePointer(data.AsSpan(pointerSize, pointerSize), MethodTableAddr); + helpers.WritePointer(data.AsSpan(2 * pointerSize, pointerSize), memory); + helpers.Write(data.AsSpan(3 * pointerSize, sizeof(uint)), gcFlags); + return new MockMemorySpace.HeapFragment { Address = HandleAddr, Data = data, Name = "ExternalMemoryHandle" }; + } + + [Fact] + public void ReferenceType_OrdinarySlot_ReportsAddress() + { + const ulong MemoryAddr = 0x3000; + var rts = new Mock(MockBehavior.Strict); + ITypeHandle typeHandle = new TargetTypeHandle(new TargetPointer(MethodTableAddr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); + rts.Setup(r => r.IsValueType(typeHandle)).Returns(false); + + ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts).Contracts.Loader; + + ExternalMemoryHandleRootData root = Assert.Single(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + Assert.False(root.IsInteriorPointer); + Assert.Equal(new TargetPointer(MemoryAddr), root.Address); + Assert.Equal(TargetPointer.Null, root.Object); + } + + [Fact] + public void ReferenceType_InteriorSlot_ReportsResolvedObject() + { + const ulong MemoryAddr = 0x3000; + const ulong ObjectAddr = 0x4000; + var rts = new Mock(MockBehavior.Strict); + ITypeHandle typeHandle = new TargetTypeHandle(new TargetPointer(MethodTableAddr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); + rts.Setup(r => r.IsValueType(typeHandle)).Returns(false); + + var gc = new Mock(); + List fragments = [PointerFragment(MemoryAddr, ObjectAddr)]; + SetupResolvableObjects(gc, rts, fragments, ObjectAddr); + + ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 1, rts, fragments, gc).Contracts.Loader; + + ExternalMemoryHandleRootData root = Assert.Single(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + Assert.True(root.IsInteriorPointer); + Assert.Equal(new TargetPointer(MemoryAddr), root.Address); + Assert.Equal(new TargetPointer(ObjectAddr), root.Object); + } + + [Fact] + public void ReferenceType_InteriorSlot_UnresolvableInteriorPointer_IsDropped() + { + const ulong MemoryAddr = 0x3000; + const ulong ObjectAddr = 0x4000; + var rts = new Mock(MockBehavior.Strict); + ITypeHandle typeHandle = new TargetTypeHandle(new TargetPointer(MethodTableAddr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); + rts.Setup(r => r.IsValueType(typeHandle)).Returns(false); + + ILoader loader = CreateTarget( + new TargetPointer(MemoryAddr), + gcFlags: 1, + rts, + [PointerFragment(MemoryAddr, ObjectAddr)]).Contracts.Loader; + + Assert.Empty(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + } + + [Fact] + public void ValueType_ContainsGCPointers_ReportsSeriesSlots() + { + const ulong MemoryAddr = 0x5000; + var rts = new Mock(MockBehavior.Strict); + ITypeHandle typeHandle = new TargetTypeHandle(new TargetPointer(MethodTableAddr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); + rts.Setup(r => r.IsValueType(typeHandle)).Returns(true); + rts.Setup(r => r.IsByRefLike(typeHandle)).Returns(false); + rts.Setup(r => r.ContainsGCPointers(typeHandle)).Returns(true); + rts.Setup(r => r.GetGCDescSeries(typeHandle, 0u)).Returns([(16u, 16u)]); + + ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts).Contracts.Loader; + + IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true); + Assert.Equal([MemoryAddr + 8, MemoryAddr + 16], roots.Select(r => r.Address.Value).ToArray()); + Assert.All(roots, r => Assert.False(r.IsInteriorPointer)); + } + + [Fact] + public void ValueType_ByRefLike_ReportsResolvedObject() + { + const ulong MemoryAddr = 0x6000; + const ulong ObjectAddr = 0x7000; + const ulong FieldDescAddr = 0x8000; + var rts = new Mock(MockBehavior.Strict); + ITypeHandle typeHandle = new TargetTypeHandle(new TargetPointer(MethodTableAddr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); + rts.Setup(r => r.IsValueType(typeHandle)).Returns(true); + rts.Setup(r => r.IsByRefLike(typeHandle)).Returns(true); + rts.Setup(r => r.ContainsGCPointers(typeHandle)).Returns(false); + rts.Setup(r => r.IsInlineArray(typeHandle)).Returns(false); + rts.Setup(r => r.GetFieldDescList(typeHandle)).Returns([new TargetPointer(FieldDescAddr)]); + rts.Setup(r => r.IsFieldDescStatic(new TargetPointer(FieldDescAddr))).Returns(false); + rts.Setup(r => r.GetFieldDescType(new TargetPointer(FieldDescAddr))).Returns(CorElementType.Byref); + rts.Setup(r => r.GetFieldDescOffset(new TargetPointer(FieldDescAddr), null)).Returns(0u); + + var gc = new Mock(); + List fragments = [PointerFragment(MemoryAddr, ObjectAddr)]; + SetupResolvableObjects(gc, rts, fragments, ObjectAddr); + + ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts, fragments, gc).Contracts.Loader; + + ExternalMemoryHandleRootData root = Assert.Single(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + Assert.True(root.IsInteriorPointer); + Assert.Equal(new TargetPointer(ObjectAddr), root.Object); + } + + [Fact] + public void ValueType_ByRefLikeInlineArray_ReportsEveryElement() + { + const ulong MemoryAddr = 0x6100; + const ulong FieldDescAddr = 0x8100; + const uint ElementSize = 8; + ulong[] objectAddresses = [0x7100, 0x7200, 0x7300]; + var rts = new Mock(MockBehavior.Strict); + ITypeHandle typeHandle = new TargetTypeHandle(new TargetPointer(MethodTableAddr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); + rts.Setup(r => r.IsValueType(typeHandle)).Returns(true); + rts.Setup(r => r.IsByRefLike(typeHandle)).Returns(true); + rts.Setup(r => r.ContainsGCPointers(typeHandle)).Returns(false); + rts.Setup(r => r.IsInlineArray(typeHandle)).Returns(true); + rts.Setup(r => r.GetNumInstanceFieldBytes(typeHandle)).Returns(ElementSize * (uint)objectAddresses.Length); + rts.Setup(r => r.GetFieldDescList(typeHandle)).Returns([new TargetPointer(FieldDescAddr)]); + rts.Setup(r => r.IsFieldDescStatic(new TargetPointer(FieldDescAddr))).Returns(false); + rts.Setup(r => r.GetFieldDescType(new TargetPointer(FieldDescAddr))).Returns(CorElementType.Byref); + + List fragments = []; + for (int i = 0; i < objectAddresses.Length; i++) + fragments.Add(PointerFragment(MemoryAddr + (ulong)i * ElementSize, objectAddresses[i])); + + var gc = new Mock(); + SetupResolvableObjects(gc, rts, fragments, objectAddresses); + + ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts, fragments, gc).Contracts.Loader; + + IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true); + Assert.Equal(objectAddresses, roots.Select(r => r.Object.Value).ToArray()); + Assert.All(roots, r => Assert.True(r.IsInteriorPointer)); + } + + [Fact] + public void NoExternalMemoryHandles_YieldsNoRoots() + { + var rts = new Mock(MockBehavior.Strict); + ILoader loader = CreateTarget(TargetPointer.Null, gcFlags: 0, rts, hasHandle: false).Contracts.Loader; + + Assert.Empty(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + } +} + +public class RefWalkExternalMemoryHandleTests +{ + private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; + + [Fact] + public void WalkExternalMemoryHandles_MapsContractRoots() + { + TargetPointer appDomain = new(0x1000); + TargetPointer ordinarySlot = new(0x2000); + TargetPointer interiorObject = new(0x3000); + var loader = new Mock(); + loader.Setup(l => l.GetAppDomain()).Returns(appDomain); + loader.Setup(l => l.GetExternalMemoryHandleRoots(true)).Returns( + [ + new ExternalMemoryHandleRootData { Address = ordinarySlot }, + new ExternalMemoryHandleRootData { IsInteriorPointer = true, Object = interiorObject }, + ]); + + var gc = new Mock(); + gc.Setup(g => g.GetSupportedHandleTypes()).Returns([]); + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(Arch) + .AddMockContract(loader) + .AddMockContract(gc) + .Build(); + + RefWalk walk = new(target, walkStacks: false, CorGCReferenceType.CorHandleStrong); + List references = []; + while (walk.Enumerator.MoveNext()) + references.Add(walk.Enumerator.Current); + + Assert.Collection( + references, + reference => + { + Assert.Equal(CorGCReferenceType.CorHandleStrong, reference.dwType); + Assert.Equal(appDomain.Value, reference.vmDomain); + Assert.Equal(ordinarySlot.Value, reference.objHnd); + }, + reference => + { + Assert.Equal(CorGCReferenceType.CorHandleStrong, reference.dwType); + Assert.Equal(appDomain.Value, reference.vmDomain); + Assert.Equal(interiorObject.Value | 1, reference.pObject); + }); + } +} diff --git a/src/native/managed/cdac/tests/UnitTests/RefWalkLoaderContractVersionTests.cs b/src/native/managed/cdac/tests/UnitTests/RefWalkLoaderContractVersionTests.cs new file mode 100644 index 00000000000000..ae0cfbfb073811 --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkLoaderContractVersionTests.cs @@ -0,0 +1,145 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; +using Moq; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.Tests; + +/// +/// End-to-end tests confirming that RefWalk's external-memory-handle scanning is driven entirely +/// by which Loader contract version is registered: Loader contract version c1 (Loader_1) always +/// contributes zero external-memory-handle roots, while version c2 (Loader_2) walks the real +/// AppDomain::m_externalMemoryHandles chain, using the actual (non-mocked) Loader contract +/// implementations rather than a mocked ILoader. +/// +public class RefWalkLoaderContractVersionTests +{ + private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; + + private const ulong AppDomainStaticSlotAddr = 0x0500; + private const ulong AppDomainAddr = 0x1000; + private const ulong HandleAddr = 0x2000; + private const ulong MethodTableAddr = 0x9000; + private const ulong MemoryAddr = 0x9500; + + private static TestPlaceholderTarget CreateTarget(string loaderVersion, Mock rts) + { + TargetTestHelpers helpers = new(Arch); + int ptrSize = helpers.PointerSize; + + var mockGC = new Mock(); + mockGC.Setup(g => g.GetSupportedHandleTypes()).Returns([]); + + var targetBuilder = new TestPlaceholderTarget.Builder(Arch) + .AddGlobals(("AppDomain", AppDomainStaticSlotAddr)) + .AddTypes(new Dictionary + { + [DataType.AppDomain] = new() + { + Fields = new Dictionary + { + { nameof(Data.AppDomain.ExternalMemoryHandles), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, + } + }, + [DataType.ExternalMemoryHandle] = new() + { + Fields = new Dictionary + { + { nameof(Data.ExternalMemoryHandle.Next), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.MethodTable), new() { Offset = ptrSize, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.Memory), new() { Offset = 2 * ptrSize, TypeName = DataType.pointer.ToString() } }, + { nameof(Data.ExternalMemoryHandle.GCFlags), new() { Offset = 3 * ptrSize, TypeName = DataType.uint32.ToString() } }, + } + }, + // GCInteriorPointerResolver (constructed unconditionally by RefWalk) reads these + // minimal Object/Array/String descriptors regardless of whether a test exercises + // interior-pointer resolution. + [DataType.Object] = TargetTestHelpers.CreateTypeInfo(MockObjectData.CreateLayout(Arch)), + [DataType.Array] = TargetTestHelpers.CreateTypeInfo(MockArrayObjectData.CreateLayout(Arch)), + [DataType.String] = TargetTestHelpers.CreateTypeInfo(MockStringObjectData.CreateLayout(Arch)), + }) + .AddGlobals((nameof(Constants.Globals.ObjectToMethodTableUnmask), 0ul)) + .AddContract(version: loaderVersion) + .AddMockContract(mockGC) + .AddMockContract(rts); + + // AppDomain* static slot -> the AppDomain instance + targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainStaticSlotAddr, AppDomainAddr)); + + // AppDomain.ExternalMemoryHandles -> a single handle + targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainAddr, HandleAddr)); + + // Handle: Next -> null, MethodTable, Memory, GCFlags=0 (ordinary reference-type root) + targetBuilder.MemoryBuilder.AddHeapFragment(ExternalMemoryHandleFragment(helpers, HandleAddr, 0, MethodTableAddr, MemoryAddr, 0)); + + return targetBuilder.Build(); + } + + private static MockMemorySpace.HeapFragment PointerFragment(TargetTestHelpers helpers, ulong address, ulong value) + { + byte[] data = new byte[helpers.PointerSize]; + helpers.WritePointer(data, value); + return new MockMemorySpace.HeapFragment { Address = address, Data = data, Name = "Pointer" }; + } + + private static MockMemorySpace.HeapFragment ExternalMemoryHandleFragment(TargetTestHelpers helpers, ulong address, ulong next, ulong methodTable, ulong memory, uint gcFlags) + { + int ptrSize = helpers.PointerSize; + byte[] data = new byte[3 * ptrSize + sizeof(uint)]; + helpers.WritePointer(data.AsSpan(0, ptrSize), next); + helpers.WritePointer(data.AsSpan(ptrSize, ptrSize), methodTable); + helpers.WritePointer(data.AsSpan(2 * ptrSize, ptrSize), memory); + helpers.Write(data.AsSpan(3 * ptrSize, sizeof(uint)), gcFlags); + return new MockMemorySpace.HeapFragment { Address = address, Data = data, Name = "ExternalMemoryHandle" }; + } + + private static List Walk(TestPlaceholderTarget target) + { + RefWalk walk = new(target, walkStacks: false, CorGCReferenceType.CorHandleStrong); + List results = new(); + while (walk.Enumerator.MoveNext()) + results.Add(walk.Enumerator.Current); + return results; + } + + [Fact] + public void LoaderVersion1_ContributesNoExternalMemoryHandleRoots() + { + // Loader_1 (contract version c1) has no external-memory-handle support: RefWalk must not + // surface any roots for the handle even though it is present in the target's memory. + var rts = new Mock(MockBehavior.Strict); + TestPlaceholderTarget target = CreateTarget("c1", rts); + + List refs = Walk(target); + + Assert.Empty(refs); + } + + [Fact] + public void LoaderVersion2_ContributesExternalMemoryHandleRoot() + { + // Loader_2 (contract version c2) walks the real AppDomain::m_externalMemoryHandles chain; + // RefWalk must report the resulting handle as a CorHandleStrong root. + var rts = new Mock(MockBehavior.Strict); + ITypeHandle typeHandle = new TargetTypeHandle(new TargetPointer(MethodTableAddr)); + rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); + rts.Setup(r => r.IsValueType(typeHandle)).Returns(false); + + TestPlaceholderTarget target = CreateTarget("c2", rts); + + List refs = Walk(target); + + DacGcReference reference = Assert.Single(refs); + Assert.Equal(CorGCReferenceType.CorHandleStrong, reference.dwType); + Assert.Equal(AppDomainAddr, reference.vmDomain); + Assert.Equal(MemoryAddr, reference.objHnd); + Assert.Equal(0ul, reference.i64ExtraData); + } +} diff --git a/src/native/managed/cdac/tests/UsageTests/ContractUsageGraphTests.cs b/src/native/managed/cdac/tests/UsageTests/ContractUsageGraphTests.cs index 65367aefd79326..c3447712244773 100644 --- a/src/native/managed/cdac/tests/UsageTests/ContractUsageGraphTests.cs +++ b/src/native/managed/cdac/tests/UsageTests/ContractUsageGraphTests.cs @@ -23,14 +23,14 @@ public sealed class ContractUsageGraphTests [ [ nameof(ICodeVersions), + nameof(IEcmaMetadata), nameof(IExecutionManager), + nameof(IGC), + nameof(ILoader), + nameof(IObject), nameof(IRuntimeTypeSystem), nameof(ISignature), ], - [ - nameof(IEcmaMetadata), - nameof(ILoader), - ], ]; public ContractUsageGraphTests(UsageGraphFixture fixture) => _usage = fixture.Usage; From 78589496c04892dc7ba3f13a8ad04a2621974f34 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 10:59:21 -0700 Subject: [PATCH 03/24] Move to separate contract to break contract cycles --- .../datacontracts/ExternalMemoryHandles.md | 98 +++++++++++++++++++ docs/design/datacontracts/Loader.md | 92 ----------------- .../vm/datadescriptor/datadescriptor.inc | 3 +- .../ContractRegistry.cs | 4 + .../Contracts/IExternalMemoryHandles.cs | 26 +++++ .../Contracts/ILoader.cs | 8 -- ...Loader_2.cs => ExternalMemoryHandles_1.cs} | 61 ++---------- .../Contracts/Loader_1.cs | 2 - .../CoreCLRContracts.cs | 3 +- .../Dbi/Helpers/RefWalk.cs | 4 +- .../DacDbi/DacDbiRefWalkDumpTests.cs | 4 +- .../ContractDescriptor/TargetTests.cs | 1 + ...Tests.cs => ExternalMemoryHandlesTests.cs} | 40 +++----- .../RefWalkExternalMemoryHandleTests.cs | 36 +++---- ...WalkExternalMemoryHandlesContractTests.cs} | 33 ++----- .../UsageTests/ContractUsageGraphTests.cs | 8 +- 16 files changed, 183 insertions(+), 240 deletions(-) create mode 100644 docs/design/datacontracts/ExternalMemoryHandles.md create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExternalMemoryHandles.cs rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/{Loader_2.cs => ExternalMemoryHandles_1.cs} (50%) rename src/native/managed/cdac/tests/UnitTests/{LoaderExternalMemoryHandleTests.cs => ExternalMemoryHandlesTests.cs} (75%) rename src/native/managed/cdac/tests/UnitTests/{RefWalkLoaderContractVersionTests.cs => RefWalkExternalMemoryHandlesContractTests.cs} (79%) diff --git a/docs/design/datacontracts/ExternalMemoryHandles.md b/docs/design/datacontracts/ExternalMemoryHandles.md new file mode 100644 index 00000000000000..2abe5a14a48da0 --- /dev/null +++ b/docs/design/datacontracts/ExternalMemoryHandles.md @@ -0,0 +1,98 @@ +# 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 GetRoots(bool resolveInteriorPointers); +``` + +## Version 1 + + +### Data descriptors used + +| Data Descriptor | Field | Type | Meaning | +| --- | --- | --- | --- | +| `AppDomain` | `ExternalMemoryHandles` | `pointer` | Pointer to the head of the AppDomain's external memory handle list (SListTail) | +| `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 owning AppDomain's list | +| `Object` | `m_pMethTab` | `pointer` | Method table for the object | +| `String` | `m_StringLength` | `uint32` | Length of the string in UTF-16 characters | + +### Global variables used + +| Global | Type | Meaning | +| --- | --- | --- | +| `AppDomain` | `pointer` | Pointer to the global application domain | +| `ObjectToMethodTableUnmask` | `uint8` | Bits to clear when converting an object header value to a method table address | + +### Contracts used + +| Contract Name | +| --- | +| `GC` | +| `RuntimeTypeSystem` | + + +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 IExternalMemoryHandles.GetRoots(bool resolveInteriorPointers) +{ + TargetPointer appDomain = // read the AppDomain global + if (appDomain == TargetPointer.Null) + return []; + + AppDomain domain = // read AppDomain object starting at appDomain + HashSet visited = []; + List roots = []; + TargetPointer current = domain.ExternalMemoryHandles; + 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; +} +``` diff --git a/docs/design/datacontracts/Loader.md b/docs/design/datacontracts/Loader.md index 00e535a7b905c4..53f5210f854147 100644 --- a/docs/design/datacontracts/Loader.md +++ b/docs/design/datacontracts/Loader.md @@ -57,13 +57,6 @@ enum ModuleLookupMapKind readonly record struct LoaderHeapBlock(TargetPointer Address, TargetNUInt Size); -sealed class ExternalMemoryHandleRootData -{ - bool IsInteriorPointer { get; init; } - TargetPointer Address { get; init; } - TargetPointer Object { get; init; } -} - enum LoaderAllocatorHeapType { Unknown, @@ -86,7 +79,6 @@ IEnumerable GetModuleHandles(TargetPointer appDomain, AssemblyIter TargetPointer GetRootAssembly(); string GetAppDomainFriendlyName(); TargetPointer GetAppDomain(); -IReadOnlyList GetExternalMemoryHandleRoots(bool resolveInteriorPointers); TargetPointer GetModule(ModuleHandle handle); TargetPointer GetAssembly(ModuleHandle handle); TargetPointer GetPEAssembly(ModuleHandle handle); @@ -259,36 +251,6 @@ enum ClrModifiableAssemblies : uint | `SHash` | -## Version 2 dependency changes from Version 1 - - -### Data descriptor changes from `c1` - -| Change | Data Descriptor | Field | Type | Meaning | -| --- | --- | --- | --- | --- | -| Added | `AppDomain` | `ExternalMemoryHandles` | `pointer` | Pointer to the head of the AppDomain's external memory handle list (SListTail) | -| Added | `Array` | `m_NumComponents` | `uint32` | Number of items in the array | -| Added | `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 | -| Added | `ExternalMemoryHandle` | `Memory` | `pointer` | Pointer to the external memory tracked by this handle | -| Added | `ExternalMemoryHandle` | `MethodTable` | `pointer` | Pointer to the MethodTable describing the type of the tracked memory | -| Added | `ExternalMemoryHandle` | `Next` | `pointer` | Pointer to the next ExternalMemoryHandle in the owning AppDomain's list | -| Added | `Object` | `m_pMethTab` | `pointer` | Method table for the object | -| Added | `String` | `m_StringLength` | `uint32` | Length of the string in UTF-16 characters | - -### Global variable changes from `c1` - -| Change | Global | Type | Meaning | -| --- | --- | --- | --- | -| Added | `ObjectToMethodTableUnmask` | `uint8` | Bits to clear when converting an object header value to a method table address | - -### Contract dependency changes from `c1` - -| Change | Contract Name | -| --- | --- | -| Added | `GC` | -| Added | `RuntimeTypeSystem` | - - ### Contract Constants: | Name | Type | Purpose | Value | | --- | --- | --- | --- | @@ -1088,57 +1050,3 @@ IEnumerable ILoader.EnumerateLoaderHeapBlocks(TargetPointer loa } } ``` - -## Version 2 - -Version 2 adds support for scanning an AppDomain's ExternalMemoryHandle list and returning the -resulting GC roots. GetExternalMemoryHandleRoots is part of the shared ILoader API surface (available -since Version 1), but Version 1's implementation always returns an empty list; only Version 2 walks -the real list. All other APIs behave identically to Version 1. - -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 ILoader.GetExternalMemoryHandleRoots(bool resolveInteriorPointers) -{ - TargetPointer appDomain = GetAppDomain(); - if (appDomain == TargetPointer.Null) - return []; - - AppDomain domain = // read AppDomain object starting at appDomain - HashSet visited = []; - List roots = []; - TargetPointer current = domain.ExternalMemoryHandles; - while (current != TargetPointer.Null) - { - if (!visited.Add(current)) - throw new InvalidOperationException(); // defend against a corrupted/cyclic list - - 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; -} -``` diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 3030fb4aa8bd53..89242a340ec5c2 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1896,8 +1896,9 @@ CDAC_GLOBAL_CONTRACT(RuntimeMutableTypeSystem, c1) #endif // FEATURE_METADATA_UPDATER CDAC_GLOBAL_CONTRACT(Exception, c1) CDAC_GLOBAL_CONTRACT(ExecutionManager, c1) +CDAC_GLOBAL_CONTRACT(ExternalMemoryHandles, c1) CDAC_GLOBAL_CONTRACT(GCInfo, c1) -CDAC_GLOBAL_CONTRACT(Loader, c2) +CDAC_GLOBAL_CONTRACT(Loader, c1) CDAC_GLOBAL_CONTRACT(ManagedTypeSource, c1) CDAC_GLOBAL_CONTRACT(Notifications, c1) #ifdef FEATURE_OBJCMARSHAL diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs index cccea8cb6724cd..d6d89c5c15d473 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs @@ -18,6 +18,10 @@ public abstract class ContractRegistry /// public virtual IException Exception => GetContract(); /// + /// Gets an instance of the ExternalMemoryHandles contract for the target. + /// + public virtual IExternalMemoryHandles ExternalMemoryHandles => GetContract(); + /// /// Gets an instance of the Loader contract for the target. /// public virtual ILoader Loader => GetContract(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExternalMemoryHandles.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExternalMemoryHandles.cs new file mode 100644 index 00000000000000..591f0a96559ffe --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IExternalMemoryHandles.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +public sealed class ExternalMemoryHandleRootData +{ + public bool IsInteriorPointer { get; init; } + public TargetPointer Address { get; init; } + public TargetPointer Object { get; init; } +} + +public interface IExternalMemoryHandles : IContract +{ + static string IContract.Name => nameof(ExternalMemoryHandles); + + IReadOnlyList GetRoots(bool resolveInteriorPointers) => throw new NotImplementedException(); +} + +public readonly struct ExternalMemoryHandles : IExternalMemoryHandles +{ + // Everything throws NotImplementedException +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs index a7b9d6da13a613..42da5b04d49d59 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ILoader.cs @@ -84,13 +84,6 @@ public enum ModuleLookupMapKind public readonly record struct LoaderHeapBlock(TargetPointer Address, TargetNUInt Size); -public sealed class ExternalMemoryHandleRootData -{ - public bool IsInteriorPointer { get; init; } - public TargetPointer Address { get; init; } - public TargetPointer Object { get; init; } -} - public interface ILoader : IContract { static string IContract.Name => nameof(Loader); @@ -101,7 +94,6 @@ public interface ILoader : IContract TargetPointer GetRootAssembly() => throw new NotImplementedException(); string GetAppDomainFriendlyName() => throw new NotImplementedException(); TargetPointer GetAppDomain() => throw new NotImplementedException(); - IReadOnlyList GetExternalMemoryHandleRoots(bool resolveInteriorPointers) => throw new NotImplementedException(); TargetPointer GetModule(ModuleHandle handle) => throw new NotImplementedException(); TargetPointer GetAssembly(ModuleHandle handle) => throw new NotImplementedException(); TargetPointer GetPEAssembly(ModuleHandle handle) => throw new NotImplementedException(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_2.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs similarity index 50% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_2.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs index 619c91e23b17c8..ea877946a0d91d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_2.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs @@ -7,21 +7,20 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; -internal readonly struct Loader_2 : ILoader +internal readonly struct ExternalMemoryHandles_1 : IExternalMemoryHandles { private readonly Target _target; - private readonly Loader_1 _v1; - internal Loader_2(Target target) + internal ExternalMemoryHandles_1(Target target) { _target = target; - _v1 = new Loader_1(target); } - IReadOnlyList ILoader.GetExternalMemoryHandleRoots(bool resolveInteriorPointers) + IReadOnlyList IExternalMemoryHandles.GetRoots(bool resolveInteriorPointers) { List roots = []; - TargetPointer appDomain = ((ILoader)_v1).GetAppDomain(); + TargetPointer appDomainPointer = _target.ReadGlobalPointer(Constants.Globals.AppDomain); + TargetPointer appDomain = _target.ReadPointer(appDomainPointer); if (appDomain == TargetPointer.Null) return roots; @@ -29,15 +28,12 @@ IReadOnlyList ILoader.GetExternalMemoryHandleRoots IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; GCInteriorPointerResolver? interiorPointerResolver = null; - HashSet visited = new(); + HashSet visited = []; TargetPointer current = domain.ExternalMemoryHandles; while (current != TargetPointer.Null) { if (!visited.Add(current)) - { - // Defend against a corrupted/cyclic list rather than looping forever. throw new InvalidOperationException("ExternalMemoryHandle list is cyclic."); - } Data.ExternalMemoryHandle handle = _target.ProcessedData.GetOrAdd(current); ITypeHandle typeHandle = rts.GetTypeHandle(handle.MethodTable); @@ -174,49 +170,4 @@ private static IEnumerable EnumerateByRefLikeFieldSlots( yield return slot; } } - - ModuleHandle ILoader.GetModuleHandleFromModulePtr(TargetPointer modulePointer) => ((ILoader)_v1).GetModuleHandleFromModulePtr(modulePointer); - ModuleHandle ILoader.GetModuleHandleFromAssemblyPtr(TargetPointer assemblyPointer) => ((ILoader)_v1).GetModuleHandleFromAssemblyPtr(assemblyPointer); - IEnumerable ILoader.GetModuleHandles(TargetPointer appDomain, AssemblyIterationFlags iterationFlags) => ((ILoader)_v1).GetModuleHandles(appDomain, iterationFlags); - TargetPointer ILoader.GetRootAssembly() => ((ILoader)_v1).GetRootAssembly(); - string ILoader.GetAppDomainFriendlyName() => ((ILoader)_v1).GetAppDomainFriendlyName(); - TargetPointer ILoader.GetAppDomain() => ((ILoader)_v1).GetAppDomain(); - TargetPointer ILoader.GetModule(ModuleHandle handle) => ((ILoader)_v1).GetModule(handle); - TargetPointer ILoader.GetAssembly(ModuleHandle handle) => ((ILoader)_v1).GetAssembly(handle); - TargetPointer ILoader.GetPEAssembly(ModuleHandle handle) => ((ILoader)_v1).GetPEAssembly(handle); - bool ILoader.TryGetLoadedImageContents(ModuleHandle handle, out TargetPointer baseAddress, out uint size, out uint imageFlags) => ((ILoader)_v1).TryGetLoadedImageContents(handle, out baseAddress, out size, out imageFlags); - TargetPointer ILoader.GetILAddr(TargetPointer peAssemblyPtr, int rva) => ((ILoader)_v1).GetILAddr(peAssemblyPtr, rva); - TargetPointer ILoader.GetFieldAddressFromRva(TargetPointer peAssemblyPtr, int rva) => ((ILoader)_v1).GetFieldAddressFromRva(peAssemblyPtr, rva); - bool ILoader.TryGetSymbolStream(ModuleHandle handle, out TargetPointer buffer, out uint size) => ((ILoader)_v1).TryGetSymbolStream(handle, out buffer, out size); - IEnumerable ILoader.GetAvailableTypeParams(ModuleHandle handle) => ((ILoader)_v1).GetAvailableTypeParams(handle); - IEnumerable ILoader.GetInstantiatedMethods(ModuleHandle handle) => ((ILoader)_v1).GetInstantiatedMethods(handle); - bool ILoader.IsProbeExtensionResultValid(ModuleHandle handle) => ((ILoader)_v1).IsProbeExtensionResultValid(handle); - ModuleFlags ILoader.GetFlags(ModuleHandle handle) => ((ILoader)_v1).GetFlags(handle); - bool ILoader.IsReadyToRun(ModuleHandle handle) => ((ILoader)_v1).IsReadyToRun(handle); - string ILoader.GetSimpleName(ModuleHandle handle) => ((ILoader)_v1).GetSimpleName(handle); - string ILoader.GetPath(ModuleHandle handle) => ((ILoader)_v1).GetPath(handle); - string ILoader.GetFileName(ModuleHandle handle) => ((ILoader)_v1).GetFileName(handle); - bool ILoader.GetFileHeadersInfo(ModuleHandle handle, out uint timeStamp, out uint imageSize) => ((ILoader)_v1).GetFileHeadersInfo(handle, out timeStamp, out imageSize); - TargetPointer ILoader.GetLoaderAllocator(ModuleHandle handle) => ((ILoader)_v1).GetLoaderAllocator(handle); - TargetPointer ILoader.GetILBase(ModuleHandle handle) => ((ILoader)_v1).GetILBase(handle); - TargetPointer ILoader.GetAssemblyLoadContext(ModuleHandle handle) => ((ILoader)_v1).GetAssemblyLoadContext(handle); - TargetPointer ILoader.GetModuleLookupMapBase(ModuleHandle module, ModuleLookupMapKind kind) => ((ILoader)_v1).GetModuleLookupMapBase(module, kind); - TargetPointer ILoader.GetModuleLookupMapElement(ModuleHandle module, ModuleLookupMapKind kind, uint token, out TargetNUInt flags) => ((ILoader)_v1).GetModuleLookupMapElement(module, kind, token, out flags); - TargetPointer ILoader.LookupMemberRefAsMethod(ModuleHandle handle, uint token) => ((ILoader)_v1).LookupMemberRefAsMethod(handle, token); - IEnumerable<(TargetPointer Value, uint Token)> ILoader.EnumerateModuleLookupMap(ModuleHandle module, ModuleLookupMapKind kind) => ((ILoader)_v1).EnumerateModuleLookupMap(module, kind); - bool ILoader.IsCollectible(ModuleHandle handle) => ((ILoader)_v1).IsCollectible(handle); - bool ILoader.IsDynamic(ModuleHandle handle) => ((ILoader)_v1).IsDynamic(handle); - bool ILoader.IsModuleMapped(ModuleHandle handle) => ((ILoader)_v1).IsModuleMapped(handle); - bool ILoader.IsAssemblyLoaded(ModuleHandle handle) => ((ILoader)_v1).IsAssemblyLoaded(handle); - TargetPointer ILoader.GetGlobalLoaderAllocator() => ((ILoader)_v1).GetGlobalLoaderAllocator(); - TargetPointer ILoader.GetSystemAssembly() => ((ILoader)_v1).GetSystemAssembly(); - TargetPointer ILoader.GetHighFrequencyHeap(TargetPointer loaderAllocatorPointer) => ((ILoader)_v1).GetHighFrequencyHeap(loaderAllocatorPointer); - TargetPointer ILoader.GetLowFrequencyHeap(TargetPointer loaderAllocatorPointer) => ((ILoader)_v1).GetLowFrequencyHeap(loaderAllocatorPointer); - TargetPointer ILoader.GetILHeader(ModuleHandle handle, uint token) => ((ILoader)_v1).GetILHeader(handle, token); - TargetPointer ILoader.GetObjectHandle(TargetPointer loaderAllocatorPointer) => ((ILoader)_v1).GetObjectHandle(loaderAllocatorPointer); - TargetPointer ILoader.GetDynamicIL(ModuleHandle handle, uint token) => ((ILoader)_v1).GetDynamicIL(handle, token); - IEnumerable ILoader.EnumerateLoaderHeapBlocks(TargetPointer loaderHeap) => ((ILoader)_v1).EnumerateLoaderHeapBlocks(loaderHeap); - IReadOnlyDictionary ILoader.GetLoaderAllocatorHeaps(TargetPointer loaderAllocatorPointer) => ((ILoader)_v1).GetLoaderAllocatorHeaps(loaderAllocatorPointer); - DebuggerAssemblyControlFlags ILoader.GetDebuggerInfoBits(ModuleHandle handle) => ((ILoader)_v1).GetDebuggerInfoBits(handle); - void ILoader.SetDebuggerInfoBits(ModuleHandle handle, DebuggerAssemblyControlFlags newBits) => ((ILoader)_v1).SetDebuggerInfoBits(handle, newBits); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs index 837d50c0f4bca8..ee8939779b4301 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs @@ -163,8 +163,6 @@ TargetPointer ILoader.GetAppDomain() return appDomain; } - IReadOnlyList ILoader.GetExternalMemoryHandleRoots(bool resolveInteriorPointers) => []; - TargetPointer ILoader.GetModule(ModuleHandle handle) { return handle.Address; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index 4674cd0268ade6..9165bc94674da6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -16,8 +16,8 @@ public static class CoreCLRContracts public static void Register(ContractRegistry registry) { registry.Register("c1", static t => new Exception_1(t)); + registry.Register("c1", static t => new ExternalMemoryHandles_1(t)); registry.Register("c1", static t => new Loader_1(t)); - registry.Register("c2", static t => new Loader_2(t)); registry.Register("c1", static t => new EcmaMetadata_1(t)); registry.Register("c1", static t => new DacStreams_1(t)); registry.Register("c1", static t => new CodeVersions_1(t)); @@ -118,6 +118,7 @@ public static void ValidateForDataAccess(Target target, Lock? apiLock = null) Validate(registry); Validate(registry); Validate(registry); + Validate(registry); Validate(registry); Validate(registry); Validate(registry); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs index f12d8c432228a0..1be0b4d0887405 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs @@ -135,9 +135,9 @@ private bool TryMapHandle(HandleData handle, out CorGCReferenceType dwType, out // Walks the external memory handles registered with the current AppDomain. private IEnumerable WalkExternalMemoryHandles() { - ILoader loader = _target.Contracts.Loader; + IExternalMemoryHandles externalMemoryHandles = _target.Contracts.ExternalMemoryHandles; - foreach (ExternalMemoryHandleRootData root in loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)) + foreach (ExternalMemoryHandleRootData root in externalMemoryHandles.GetRoots(resolveInteriorPointers: true)) { DacGcReference reference = new() { diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs index 441736b8253400..63c5c09d25774b 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -146,13 +146,13 @@ public unsafe void WalkRefs_ExternalMemoryHandles_ContributeNothing_WhenAppDomai { InitializeDumpTest(config); DacDbiImpl dbi = CreateDacDbi(); - ILoader loader = Target.Contracts.Loader; + IExternalMemoryHandles externalMemoryHandles = Target.Contracts.ExternalMemoryHandles; IGC gc = Target.Contracts.GC; // The GCRoots debuggee does not create any ExternalMemoryHandle instances, so requesting // the strong handle mask (which also drives external-memory-handle scanning, matching // native DacRefWalker::Init) must report exactly the strong GC handles and nothing more. - Assert.Empty(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + Assert.Empty(externalMemoryHandles.GetRoots(resolveInteriorPointers: true)); List refs = WalkAllRefs(dbi, walkStacks: false, handleWalkMask: CorGCReferenceType.CorHandleStrong); diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs index 2035a8076134e4..10cb970aedc9b3 100644 --- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs @@ -401,6 +401,7 @@ public void TryGetContract_UnrecognizedVersion_ReturnsContractUnrecognizedExcept ["EcmaMetadata"] = "c1", ["Exception"] = "c1", ["ExecutionManager"] = "c1", + ["ExternalMemoryHandles"] = "c1", ["FeatureFlags"] = "c1", ["GC"] = "c1", ["GCInfo"] = "c1", diff --git a/src/native/managed/cdac/tests/UnitTests/LoaderExternalMemoryHandleTests.cs b/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs similarity index 75% rename from src/native/managed/cdac/tests/UnitTests/LoaderExternalMemoryHandleTests.cs rename to src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs index 714d128446ac2a..906264cff2daf0 100644 --- a/src/native/managed/cdac/tests/UnitTests/LoaderExternalMemoryHandleTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs @@ -10,7 +10,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -public class LoaderExternalMemoryHandleTests +public class ExternalMemoryHandlesTests { private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; @@ -23,7 +23,7 @@ public class LoaderExternalMemoryHandleTests private const ulong Memory1Addr = 0x9500; private const ulong Memory2Addr = 0x9600; - private static TestPlaceholderTarget CreateTarget(string version, bool hasAppDomain = true) + private static TestPlaceholderTarget CreateTarget(bool hasAppDomain = true) { TargetTestHelpers helpers = new(Arch); int ptrSize = helpers.PointerSize; @@ -57,7 +57,7 @@ private static TestPlaceholderTarget CreateTarget(string version, bool hasAppDom } }, }) - .AddContract(version: version) + .AddContract(version: "c1") .AddMockContract(rts); // AppDomain* static slot -> the AppDomain instance @@ -95,12 +95,12 @@ private static MockMemorySpace.HeapFragment ExternalMemoryHandleFragment(TargetT } [Fact] - public void GetExternalMemoryHandleRoots_WalksChain() + public void GetRoots_WalksChain() { - TestPlaceholderTarget target = CreateTarget("c2"); - ILoader loader = target.Contracts.Loader; + TestPlaceholderTarget target = CreateTarget(); + IExternalMemoryHandles externalMemoryHandles = target.Contracts.ExternalMemoryHandles; - IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: false); + IReadOnlyList roots = externalMemoryHandles.GetRoots(resolveInteriorPointers: false); Assert.Equal(2, roots.Count); Assert.False(roots[0].IsInteriorPointer); @@ -111,30 +111,12 @@ public void GetExternalMemoryHandleRoots_WalksChain() } [Fact] - public void GetExternalMemoryHandleRoots_Version1_AlwaysEmpty() + public void GetRoots_NullAppDomain_ReturnsEmpty() { - // Loader_1 must ignore the native ExternalMemoryHandle list entirely and report an empty - // sequence, even though the exact same backing memory (with a populated, valid chain) is - // present as it would be for Loader_2. Only contract version c2 enumerates real handles. - TestPlaceholderTarget target = CreateTarget("c1"); - ILoader loader = target.Contracts.Loader; + TestPlaceholderTarget target = CreateTarget(hasAppDomain: false); + IExternalMemoryHandles externalMemoryHandles = target.Contracts.ExternalMemoryHandles; - IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true); - - Assert.Empty(roots); - } - - [Theory] - [InlineData("c1")] - [InlineData("c2")] - public void GetExternalMemoryHandleRoots_NullAppDomain_ReturnsEmpty(string version) - { - // GetExternalMemoryHandleRoots must return an empty sequence rather than throw, for both - // Loader_1 (always empty) and Loader_2 (nothing to enumerate from a null AppDomain). - TestPlaceholderTarget target = CreateTarget(version, hasAppDomain: false); - ILoader loader = target.Contracts.Loader; - - IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true); + IReadOnlyList roots = externalMemoryHandles.GetRoots(resolveInteriorPointers: true); Assert.Empty(roots); } diff --git a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs index 30aeeb4f5ab508..cad0572b12e240 100644 --- a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs @@ -62,7 +62,7 @@ private static TestPlaceholderTarget CreateTarget( [DataType.String] = TargetTestHelpers.CreateTypeInfo(MockStringObjectData.CreateLayout(Arch)), }) .AddGlobals((nameof(Constants.Globals.ObjectToMethodTableUnmask), 0ul)) - .AddContract(version: "c2") + .AddContract(version: "c1") .AddMockContract(mockGC) .AddMockContract(rts); @@ -140,9 +140,9 @@ public void ReferenceType_OrdinarySlot_ReportsAddress() rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); rts.Setup(r => r.IsValueType(typeHandle)).Returns(false); - ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts).Contracts.Loader; + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts).Contracts.ExternalMemoryHandles; - ExternalMemoryHandleRootData root = Assert.Single(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + ExternalMemoryHandleRootData root = Assert.Single(externalMemoryHandles.GetRoots(resolveInteriorPointers: true)); Assert.False(root.IsInteriorPointer); Assert.Equal(new TargetPointer(MemoryAddr), root.Address); Assert.Equal(TargetPointer.Null, root.Object); @@ -162,9 +162,9 @@ public void ReferenceType_InteriorSlot_ReportsResolvedObject() List fragments = [PointerFragment(MemoryAddr, ObjectAddr)]; SetupResolvableObjects(gc, rts, fragments, ObjectAddr); - ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 1, rts, fragments, gc).Contracts.Loader; + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 1, rts, fragments, gc).Contracts.ExternalMemoryHandles; - ExternalMemoryHandleRootData root = Assert.Single(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + ExternalMemoryHandleRootData root = Assert.Single(externalMemoryHandles.GetRoots(resolveInteriorPointers: true)); Assert.True(root.IsInteriorPointer); Assert.Equal(new TargetPointer(MemoryAddr), root.Address); Assert.Equal(new TargetPointer(ObjectAddr), root.Object); @@ -180,13 +180,13 @@ public void ReferenceType_InteriorSlot_UnresolvableInteriorPointer_IsDropped() rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); rts.Setup(r => r.IsValueType(typeHandle)).Returns(false); - ILoader loader = CreateTarget( + IExternalMemoryHandles externalMemoryHandles = CreateTarget( new TargetPointer(MemoryAddr), gcFlags: 1, rts, - [PointerFragment(MemoryAddr, ObjectAddr)]).Contracts.Loader; + [PointerFragment(MemoryAddr, ObjectAddr)]).Contracts.ExternalMemoryHandles; - Assert.Empty(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + Assert.Empty(externalMemoryHandles.GetRoots(resolveInteriorPointers: true)); } [Fact] @@ -201,9 +201,9 @@ public void ValueType_ContainsGCPointers_ReportsSeriesSlots() rts.Setup(r => r.ContainsGCPointers(typeHandle)).Returns(true); rts.Setup(r => r.GetGCDescSeries(typeHandle, 0u)).Returns([(16u, 16u)]); - ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts).Contracts.Loader; + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts).Contracts.ExternalMemoryHandles; - IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true); + IReadOnlyList roots = externalMemoryHandles.GetRoots(resolveInteriorPointers: true); Assert.Equal([MemoryAddr + 8, MemoryAddr + 16], roots.Select(r => r.Address.Value).ToArray()); Assert.All(roots, r => Assert.False(r.IsInteriorPointer)); } @@ -230,9 +230,9 @@ public void ValueType_ByRefLike_ReportsResolvedObject() List fragments = [PointerFragment(MemoryAddr, ObjectAddr)]; SetupResolvableObjects(gc, rts, fragments, ObjectAddr); - ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts, fragments, gc).Contracts.Loader; + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts, fragments, gc).Contracts.ExternalMemoryHandles; - ExternalMemoryHandleRootData root = Assert.Single(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + ExternalMemoryHandleRootData root = Assert.Single(externalMemoryHandles.GetRoots(resolveInteriorPointers: true)); Assert.True(root.IsInteriorPointer); Assert.Equal(new TargetPointer(ObjectAddr), root.Object); } @@ -263,9 +263,9 @@ public void ValueType_ByRefLikeInlineArray_ReportsEveryElement() var gc = new Mock(); SetupResolvableObjects(gc, rts, fragments, objectAddresses); - ILoader loader = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts, fragments, gc).Contracts.Loader; + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts, fragments, gc).Contracts.ExternalMemoryHandles; - IReadOnlyList roots = loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true); + IReadOnlyList roots = externalMemoryHandles.GetRoots(resolveInteriorPointers: true); Assert.Equal(objectAddresses, roots.Select(r => r.Object.Value).ToArray()); Assert.All(roots, r => Assert.True(r.IsInteriorPointer)); } @@ -274,9 +274,9 @@ public void ValueType_ByRefLikeInlineArray_ReportsEveryElement() public void NoExternalMemoryHandles_YieldsNoRoots() { var rts = new Mock(MockBehavior.Strict); - ILoader loader = CreateTarget(TargetPointer.Null, gcFlags: 0, rts, hasHandle: false).Contracts.Loader; + IExternalMemoryHandles externalMemoryHandles = CreateTarget(TargetPointer.Null, gcFlags: 0, rts, hasHandle: false).Contracts.ExternalMemoryHandles; - Assert.Empty(loader.GetExternalMemoryHandleRoots(resolveInteriorPointers: true)); + Assert.Empty(externalMemoryHandles.GetRoots(resolveInteriorPointers: true)); } } @@ -292,7 +292,8 @@ public void WalkExternalMemoryHandles_MapsContractRoots() TargetPointer interiorObject = new(0x3000); var loader = new Mock(); loader.Setup(l => l.GetAppDomain()).Returns(appDomain); - loader.Setup(l => l.GetExternalMemoryHandleRoots(true)).Returns( + var externalMemoryHandles = new Mock(); + externalMemoryHandles.Setup(c => c.GetRoots(true)).Returns( [ new ExternalMemoryHandleRootData { Address = ordinarySlot }, new ExternalMemoryHandleRootData { IsInteriorPointer = true, Object = interiorObject }, @@ -302,6 +303,7 @@ public void WalkExternalMemoryHandles_MapsContractRoots() gc.Setup(g => g.GetSupportedHandleTypes()).Returns([]); TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(Arch) .AddMockContract(loader) + .AddMockContract(externalMemoryHandles) .AddMockContract(gc) .Build(); diff --git a/src/native/managed/cdac/tests/UnitTests/RefWalkLoaderContractVersionTests.cs b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs similarity index 79% rename from src/native/managed/cdac/tests/UnitTests/RefWalkLoaderContractVersionTests.cs rename to src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs index ae0cfbfb073811..e2af4a0476870b 100644 --- a/src/native/managed/cdac/tests/UnitTests/RefWalkLoaderContractVersionTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs @@ -12,14 +12,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; -/// -/// End-to-end tests confirming that RefWalk's external-memory-handle scanning is driven entirely -/// by which Loader contract version is registered: Loader contract version c1 (Loader_1) always -/// contributes zero external-memory-handle roots, while version c2 (Loader_2) walks the real -/// AppDomain::m_externalMemoryHandles chain, using the actual (non-mocked) Loader contract -/// implementations rather than a mocked ILoader. -/// -public class RefWalkLoaderContractVersionTests +public class RefWalkExternalMemoryHandlesContractTests { private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; @@ -29,7 +22,7 @@ public class RefWalkLoaderContractVersionTests private const ulong MethodTableAddr = 0x9000; private const ulong MemoryAddr = 0x9500; - private static TestPlaceholderTarget CreateTarget(string loaderVersion, Mock rts) + private static TestPlaceholderTarget CreateTarget(Mock rts) { TargetTestHelpers helpers = new(Arch); int ptrSize = helpers.PointerSize; @@ -66,7 +59,8 @@ private static TestPlaceholderTarget CreateTarget(string loaderVersion, Mock(version: loaderVersion) + .AddContract(version: "c1") + .AddContract(version: "c1") .AddMockContract(mockGC) .AddMockContract(rts); @@ -110,29 +104,14 @@ private static List Walk(TestPlaceholderTarget target) } [Fact] - public void LoaderVersion1_ContributesNoExternalMemoryHandleRoots() + public void ExternalMemoryHandlesContract_ContributesRoot() { - // Loader_1 (contract version c1) has no external-memory-handle support: RefWalk must not - // surface any roots for the handle even though it is present in the target's memory. - var rts = new Mock(MockBehavior.Strict); - TestPlaceholderTarget target = CreateTarget("c1", rts); - - List refs = Walk(target); - - Assert.Empty(refs); - } - - [Fact] - public void LoaderVersion2_ContributesExternalMemoryHandleRoot() - { - // Loader_2 (contract version c2) walks the real AppDomain::m_externalMemoryHandles chain; - // RefWalk must report the resulting handle as a CorHandleStrong root. var rts = new Mock(MockBehavior.Strict); ITypeHandle typeHandle = new TargetTypeHandle(new TargetPointer(MethodTableAddr)); rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTableAddr))).Returns(typeHandle); rts.Setup(r => r.IsValueType(typeHandle)).Returns(false); - TestPlaceholderTarget target = CreateTarget("c2", rts); + TestPlaceholderTarget target = CreateTarget(rts); List refs = Walk(target); diff --git a/src/native/managed/cdac/tests/UsageTests/ContractUsageGraphTests.cs b/src/native/managed/cdac/tests/UsageTests/ContractUsageGraphTests.cs index c3447712244773..65367aefd79326 100644 --- a/src/native/managed/cdac/tests/UsageTests/ContractUsageGraphTests.cs +++ b/src/native/managed/cdac/tests/UsageTests/ContractUsageGraphTests.cs @@ -23,14 +23,14 @@ public sealed class ContractUsageGraphTests [ [ nameof(ICodeVersions), - nameof(IEcmaMetadata), nameof(IExecutionManager), - nameof(IGC), - nameof(ILoader), - nameof(IObject), nameof(IRuntimeTypeSystem), nameof(ISignature), ], + [ + nameof(IEcmaMetadata), + nameof(ILoader), + ], ]; public ContractUsageGraphTests(UsageGraphFixture fixture) => _usage = fixture.Usage; From 6567d365e4575d20d2ac27ddcbc4476770a49509 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 11:05:56 -0700 Subject: [PATCH 04/24] Additional cleanup --- .../Contracts/ExternalMemoryHandles_1.cs | 38 +++++++++---------- .../StackWalk/GC/GCInteriorPointerResolver.cs | 9 ++++- .../UnitTests/ExternalMemoryHandlesTests.cs | 6 +++ 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs index ea877946a0d91d..6a7c4008acbeb5 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs @@ -7,13 +7,19 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts; -internal readonly struct ExternalMemoryHandles_1 : IExternalMemoryHandles +internal sealed class ExternalMemoryHandles_1 : IExternalMemoryHandles { private readonly Target _target; + private readonly IGC _gc; + private readonly IRuntimeTypeSystem _rts; + private readonly GCInteriorPointerResolver _interiorPointerResolver; internal ExternalMemoryHandles_1(Target target) { _target = target; + _gc = target.Contracts.GC; + _rts = target.Contracts.RuntimeTypeSystem; + _interiorPointerResolver = new GCInteriorPointerResolver(target, _gc, _rts); } IReadOnlyList IExternalMemoryHandles.GetRoots(bool resolveInteriorPointers) @@ -25,8 +31,6 @@ IReadOnlyList IExternalMemoryHandles.GetRoots(bool return roots; Data.AppDomain domain = _target.ProcessedData.GetOrAdd(appDomain); - IRuntimeTypeSystem rts = _target.Contracts.RuntimeTypeSystem; - GCInteriorPointerResolver? interiorPointerResolver = null; HashSet visited = []; TargetPointer current = domain.ExternalMemoryHandles; @@ -36,15 +40,15 @@ IReadOnlyList IExternalMemoryHandles.GetRoots(bool throw new InvalidOperationException("ExternalMemoryHandle list is cyclic."); Data.ExternalMemoryHandle handle = _target.ProcessedData.GetOrAdd(current); - ITypeHandle typeHandle = rts.GetTypeHandle(handle.MethodTable); + ITypeHandle typeHandle = _rts.GetTypeHandle(handle.MethodTable); - if (rts.IsValueType(typeHandle)) + if (_rts.IsValueType(typeHandle)) { - AddValueTypeRoots(roots, rts, typeHandle, handle.Memory, resolveInteriorPointers, ref interiorPointerResolver); + AddValueTypeRoots(roots, typeHandle, handle.Memory, resolveInteriorPointers); } else if (handle.GCFlags != 0) { - AddInteriorRoot(roots, handle.Memory, resolveInteriorPointers, ref interiorPointerResolver); + AddInteriorRoot(roots, handle.Memory, resolveInteriorPointers); } else { @@ -59,23 +63,21 @@ IReadOnlyList IExternalMemoryHandles.GetRoots(bool private void AddValueTypeRoots( List roots, - IRuntimeTypeSystem rts, ITypeHandle typeHandle, TargetPointer memory, - bool resolveInteriorPointers, - ref GCInteriorPointerResolver? interiorPointerResolver) + bool resolveInteriorPointers) { ulong pointerSize = (ulong)_target.PointerSize; - if (rts.IsByRefLike(typeHandle)) + if (_rts.IsByRefLike(typeHandle)) { - foreach (ulong slotAddress in EnumerateByRefLikeInteriorSlots(rts, typeHandle, memory.Value, (uint)_target.PointerSize)) - AddInteriorRoot(roots, new TargetPointer(slotAddress), resolveInteriorPointers, ref interiorPointerResolver); + foreach (ulong slotAddress in EnumerateByRefLikeInteriorSlots(_rts, typeHandle, memory.Value, (uint)_target.PointerSize)) + AddInteriorRoot(roots, new TargetPointer(slotAddress), resolveInteriorPointers); } - if (rts.ContainsGCPointers(typeHandle)) + if (_rts.ContainsGCPointers(typeHandle)) { - foreach ((uint seriesOffset, uint seriesSize) in rts.GetGCDescSeries(typeHandle)) + foreach ((uint seriesOffset, uint seriesSize) in _rts.GetGCDescSeries(typeHandle)) { // GCDesc series offsets include the boxed object's MethodTable pointer. ulong fieldStart = memory.Value + seriesOffset - pointerSize; @@ -88,8 +90,7 @@ private void AddValueTypeRoots( private void AddInteriorRoot( List roots, TargetPointer slotAddress, - bool resolveInteriorPointers, - ref GCInteriorPointerResolver? interiorPointerResolver) + bool resolveInteriorPointers) { TargetPointer obj = _target.ReadPointer(slotAddress.Value); if (obj == TargetPointer.Null || obj.Value == ulong.MaxValue) @@ -97,8 +98,7 @@ private void AddInteriorRoot( if (resolveInteriorPointers) { - interiorPointerResolver ??= new GCInteriorPointerResolver(_target); - obj = interiorPointerResolver.Resolve(obj); + obj = _interiorPointerResolver.Resolve(obj); if (obj == TargetPointer.Null) return; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs index 1a0df494d8967a..1ac7b4bf650bf8 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs @@ -20,9 +20,14 @@ public sealed class GCInteriorPointerResolver private readonly byte _objectToMethodTableUnmask; public GCInteriorPointerResolver(Target target) + : this(target, target.Contracts.GC, target.Contracts.RuntimeTypeSystem) { - _gc = target.Contracts.GC; - _rts = target.Contracts.RuntimeTypeSystem; + } + + internal GCInteriorPointerResolver(Target target, IGC gc, IRuntimeTypeSystem rts) + { + _gc = gc; + _rts = rts; _cache = new LinearReadCache(target); _numComponentsOffsetArray = (uint)Data.Array.GetNumComponentsOffset(target); _numComponentsOffsetString = (uint)Data.String.GetStringLengthOffset(target); diff --git a/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs b/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs index 906264cff2daf0..3744b30f8d422d 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs @@ -34,9 +34,11 @@ private static TestPlaceholderTarget CreateTarget(bool hasAppDomain = true) rts.Setup(r => r.GetTypeHandle(new TargetPointer(MethodTable2Addr))).Returns(typeHandle2); rts.Setup(r => r.IsValueType(typeHandle1)).Returns(false); rts.Setup(r => r.IsValueType(typeHandle2)).Returns(false); + var gc = new Mock(); var targetBuilder = new TestPlaceholderTarget.Builder(Arch) .AddGlobals(("AppDomain", AppDomainStaticSlotAddr)) + .AddGlobals((nameof(Constants.Globals.ObjectToMethodTableUnmask), 0ul)) .AddTypes(new Dictionary { [DataType.AppDomain] = new() @@ -56,8 +58,12 @@ private static TestPlaceholderTarget CreateTarget(bool hasAppDomain = true) { nameof(Data.ExternalMemoryHandle.GCFlags), new() { Offset = 3 * ptrSize, TypeName = DataType.uint32.ToString() } }, } }, + [DataType.Object] = TargetTestHelpers.CreateTypeInfo(MockObjectData.CreateLayout(Arch)), + [DataType.Array] = TargetTestHelpers.CreateTypeInfo(MockArrayObjectData.CreateLayout(Arch)), + [DataType.String] = TargetTestHelpers.CreateTypeInfo(MockStringObjectData.CreateLayout(Arch)), }) .AddContract(version: "c1") + .AddMockContract(gc) .AddMockContract(rts); // AppDomain* static slot -> the AppDomain instance From 5b17d8eb1084ce60947348627aad986154cda2c3 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 12:01:46 -0700 Subject: [PATCH 05/24] Protect byref-like func-eval results Transfer external memory handle ownership to the returned debugger value so byref-like result storage remains GC-protected for its lifetime. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/di/divalue.cpp | 46 ++++++++++++-- src/coreclr/debug/di/process.cpp | 1 + src/coreclr/debug/di/rspriv.h | 13 +++- src/coreclr/debug/di/rsthread.cpp | 40 +++++++++++- src/coreclr/debug/ee/debugger.cpp | 80 +++++++++++++++++++++--- src/coreclr/debug/ee/debugger.h | 28 ++++++++- src/coreclr/debug/ee/funceval.cpp | 43 ++++++++----- src/coreclr/debug/inc/dbgipcevents.h | 7 +++ src/coreclr/debug/inc/dbgipceventtypes.h | 3 +- src/coreclr/vm/appdomain.cpp | 8 ++- src/coreclr/vm/appdomain.hpp | 2 +- 11 files changed, 235 insertions(+), 36 deletions(-) diff --git a/src/coreclr/debug/di/divalue.cpp b/src/coreclr/debug/di/divalue.cpp index 4a0abfa6bacd43..d77448faea9dcd 100644 --- a/src/coreclr/debug/di/divalue.cpp +++ b/src/coreclr/debug/di/divalue.cpp @@ -145,7 +145,8 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - ICorDebugValue** ppValue) + ICorDebugValue** ppValue, + VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle) { HRESULT hr = S_OK; @@ -166,6 +167,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain ppRemoteRegAddr)); IfFailThrow(pVCValue->Init(localValue)); + pVCValue->SetExternalMemoryHandle(vmExternalMemoryHandle); pVCValue->AddRef(); *ppValue = (ICorDebugValue*)(ICorDebugObjectValue*)pVCValue; @@ -206,7 +208,8 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - ICorDebugValue** ppValue) + ICorDebugValue** ppValue, + VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle) { INTERNAL_SYNC_API_ENTRY(pAppdomain->GetProcess()); // @@ -249,11 +252,12 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain case ELEMENT_TYPE_PTR: case ELEMENT_TYPE_BYREF: case ELEMENT_TYPE_TYPEDBYREF: + case ELEMENT_TYPE_VALUETYPE: case ELEMENT_TYPE_ARRAY: case ELEMENT_TYPE_SZARRAY: case ELEMENT_TYPE_FNPTR: { - CreateVCObjOrRefValue(pAppdomain, pType, boxed, remoteValue, localValue, ppRemoteRegAddr, ppValue); // throws + CreateVCObjOrRefValue(pAppdomain, pType, boxed, remoteValue, localValue, ppRemoteRegAddr, ppValue, vmExternalMemoryHandle); // throws break; } @@ -2922,7 +2926,8 @@ CordbVCObjectValue::CordbVCObjectValue(CordbAppDomain * pAppdomain false, pAppdomain->GetSweepableExitNeuterList()), m_pObjectCopy(NULL), - m_pValueHome(NULL) + m_pValueHome(NULL), + m_vmExternalMemoryHandle(VMPTR_DebuggerExternalMemoryHandle::NullPtr()) { // instantiate the value home NewHolder pHome(NULL); @@ -2961,6 +2966,39 @@ CordbVCObjectValue::~CordbVCObjectValue() } } // CordbVCObjectValue::~CordbVCObjectValue +void CordbVCObjectValue::SetExternalMemoryHandle(VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle) +{ + _ASSERTE(m_vmExternalMemoryHandle.IsNull()); + m_vmExternalMemoryHandle = vmExternalMemoryHandle; +} + +void CordbVCObjectValue::NeuterLeftSideResources() +{ + if (!m_vmExternalMemoryHandle.IsNull() && GetProcess()->IsSafeToSendEvents()) + { + DebuggerIPCEvent event; + GetProcess()->InitIPCEvent( + &event, + DB_IPCE_DISPOSE_EXTERNAL_MEMORY_HANDLE, + false, + m_appdomain->GetADToken()); + event.DisposeExternalMemoryHandle.vmExternalMemoryHandle = m_vmExternalMemoryHandle; + GetProcess()->SendIPCEvent(&event, sizeof(DebuggerIPCEvent)); + m_vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr(); + } + + RSLockHolder lockHolder(GetProcess()->GetProcessLock()); + Neuter(); +} + +void CordbVCObjectValue::Neuter() +{ + BOOL fTargetIsDead = !GetProcess()->IsSafeToSendEvents() || GetProcess()->m_exiting; + _ASSERTE(fTargetIsDead || m_vmExternalMemoryHandle.IsNull()); + + CordbValue::Neuter(); +} + HRESULT CordbVCObjectValue::QueryInterface(REFIID id, void **pInterface) { if (id == IID_ICorDebugValue) diff --git a/src/coreclr/debug/di/process.cpp b/src/coreclr/debug/di/process.cpp index 21ddf8b5f37383..643a3d3cd84fc9 100644 --- a/src/coreclr/debug/di/process.cpp +++ b/src/coreclr/debug/di/process.cpp @@ -5112,6 +5112,7 @@ void CordbProcess::RawDispatchEvent( pEval->m_aborted = !!pEvent->FuncEvalComplete.aborted; pEval->m_resultAddr = pEvent->FuncEvalComplete.resultAddr; pEval->m_vmObjectHandle = pEvent->FuncEvalComplete.vmObjectHandle; + pEval->m_vmExternalMemoryHandle = pEvent->FuncEvalComplete.vmExternalMemoryHandle; pEval->m_resultType = pEvent->FuncEvalComplete.resultType; pEval->m_resultAppDomainToken = pEvent->FuncEvalComplete.vmAppDomain; diff --git a/src/coreclr/debug/di/rspriv.h b/src/coreclr/debug/di/rspriv.h index f8011e14d564a8..17cd9897908480 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -8688,7 +8688,8 @@ class CordbValue : public CordbBase TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - ICorDebugValue** ppValue); + ICorDebugValue** ppValue, + VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr()); // Create the proper ICDValue instance based on the given element type. static void CreateValueByType(CordbAppDomain * appdomain, @@ -8697,7 +8698,8 @@ class CordbValue : public CordbBase TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - ICorDebugValue** ppValue); + ICorDebugValue** ppValue, + VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr()); // Create the proper ICDValue instance based on the given remote heap object static ICorDebugValue* CreateHeapValue(CordbAppDomain* pAppDomain, @@ -9317,6 +9319,8 @@ class CordbVCObjectValue : public CordbValue, TargetBuffer remoteValue, EnregisteredValueHomeHolder * ppRemoteRegAddr); virtual ~CordbVCObjectValue(); + virtual void Neuter(); + virtual void NeuterLeftSideResources(); #ifdef _DEBUG virtual const char * DbgGetName() { return "CordbVCObjectValue"; } @@ -9414,6 +9418,7 @@ class CordbVCObjectValue : public CordbValue, // Initializes the Right-Side's representation of a Value Class object. HRESULT Init(MemoryRange localValue); + void SetExternalMemoryHandle(VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle); //HRESULT ResolveValueClass(); CordbClass *GetClass(); @@ -9432,6 +9437,8 @@ class CordbVCObjectValue : public CordbValue, // location information ValueHome * m_pValueHome; + + VMPTR_DebuggerExternalMemoryHandle m_vmExternalMemoryHandle; }; @@ -10006,8 +10013,10 @@ class CordbEval : public CordbBase, public ICorDebugEval, public ICorDebugEval2 // This is an External reference, which keeps the Value from being neutered // on a NeuterAtWill sweep. RSExtSmartPtr m_pHandleValue; + RSExtSmartPtr m_pValueClassResult; DebuggerIPCE_ExpandedTypeData m_resultType; + VMPTR_DebuggerExternalMemoryHandle m_vmExternalMemoryHandle; VMPTR_AppDomain m_resultAppDomainToken; // Left-side memory that needs to be freed. diff --git a/src/coreclr/debug/di/rsthread.cpp b/src/coreclr/debug/di/rsthread.cpp index 345b59adaa86ea..e85c6718740a6e 100644 --- a/src/coreclr/debug/di/rsthread.cpp +++ b/src/coreclr/debug/di/rsthread.cpp @@ -9031,6 +9031,7 @@ CordbEval::CordbEval(CordbThread *pThread) m_evalDuringException(false) { m_vmObjectHandle = VMPTR_OBJECTHANDLE::NullPtr(); + m_vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr(); m_debuggerEvalKey = LSPTR_DEBUGGEREVAL::NullPtr(); m_resultType.elementType = ELEMENT_TYPE_VOID; @@ -9133,6 +9134,20 @@ HRESULT CordbEval::SendCleanup() // Release the cached HandleValue for the result. This may cleanup resources, // like our object handle to the func-eval result. m_pHandleValue.Clear(); + m_pValueClassResult.Clear(); + + if (!m_vmExternalMemoryHandle.IsNull() && GetProcess()->IsSafeToSendEvents()) + { + DebuggerIPCEvent event; + GetProcess()->InitIPCEvent( + &event, + DB_IPCE_DISPOSE_EXTERNAL_MEMORY_HANDLE, + false, + m_thread->GetAppDomain()->GetADToken()); + event.DisposeExternalMemoryHandle.vmExternalMemoryHandle = m_vmExternalMemoryHandle; + hr = WORST_HR(hr, GetProcess()->SendIPCEvent(&event, sizeof(DebuggerIPCEvent))); + m_vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr(); + } return hr; @@ -10460,16 +10475,35 @@ HRESULT CordbEval::GetResult(ICorDebugValue **ppResult) } else { - TargetBuffer remoteValue(m_resultAddr, CordbValue::GetSizeForType(pType, kBoxed)); + bool boxed = m_resultType.elementType != ELEMENT_TYPE_VALUETYPE; + if (!boxed && m_pValueClassResult != NULL) + { + CordbVCObjectValue *pValueClassResult = m_pValueClassResult; + *ppResult = static_cast( + static_cast(pValueClassResult)); + m_pValueClassResult->ExternalAddRef(); + return S_OK; + } + + TargetBuffer remoteValue(m_resultAddr, CordbValue::GetSizeForType(pType, boxed ? kBoxed : kUnboxed)); // Now that we have the module, go ahead and create the result. CordbValue::CreateValueByType(pAppDomain, pType, - true, + boxed, remoteValue, MemoryRange(NULL, 0), NULL, - ppResult); // throws + ppResult, + boxed ? VMPTR_DebuggerExternalMemoryHandle::NullPtr() : m_vmExternalMemoryHandle); // throws + + if (!boxed) + { + CordbVCObjectValue *pValueClassResult = static_cast( + static_cast(*ppResult)); + m_pValueClassResult.Assign(pValueClassResult); + m_vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr(); + } } } diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index 30cf2c29604744..b8e128a67f2f3a 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -1289,6 +1289,7 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval memset(m_result, 0, sizeof(m_result)); m_md = NULL; m_resultType = TypeHandle(); + m_externalMemoryHandle = NULL; m_aborting = FE_ABORT_NONE; m_aborted = false; m_completed = false; @@ -1310,6 +1311,58 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval } } +DebuggerExternalMemoryHandle::DebuggerExternalMemoryHandle(AppDomain *pAppDomain, MethodTable *pMT, BYTE *pMemory) + : m_pAppDomain(pAppDomain), + m_pHandle(NULL), + m_pMemory(pMemory) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_COOPERATIVE; + } + CONTRACTL_END; + + m_pHandle = m_pAppDomain->AddExternalMemoryHandle(pMT, m_pMemory, 0); +} + +DebuggerExternalMemoryHandle::~DebuggerExternalMemoryHandle() +{ + WRAPPER_NO_CONTRACT; + + m_pAppDomain->RemoveExternalMemoryHandle(m_pHandle DEBUG_ARG(g_pDebugger->IsStopped())); + DeleteInteropSafe(m_pMemory); +} + +BYTE *DebuggerEval::CreateExternalMemoryHandle(MethodTable *pMT, SIZE_T size) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_COOPERATIVE; + } + CONTRACTL_END; + + _ASSERTE(m_externalMemoryHandle == NULL); + + BYTE *pMemory = new (interopsafe) BYTE[size]; + EX_TRY + { + m_externalMemoryHandle = new (interopsafe) DebuggerExternalMemoryHandle( + AppDomain::GetCurrentDomain(), pMT, pMemory); + } + EX_CATCH + { + DeleteInteropSafe(pMemory); + EX_RETHROW; + } + EX_END_CATCH + + return pMemory; +} + #ifdef _DEBUG // Thread proc for interop stress coverage. Have an unmanaged thread // that just loops throwing native exceptions. This can test corner cases @@ -9821,9 +9874,13 @@ void Debugger::FuncEvalComplete(Thread* pThread, DebuggerEval *pDE) ipce->FuncEvalComplete.funcEvalKey = pDE->m_funcEvalKey; ipce->FuncEvalComplete.successful = pDE->m_successful; ipce->FuncEvalComplete.aborted = pDE->m_aborted; - ipce->FuncEvalComplete.resultAddr = (CORDB_ADDRESS)(pDE->m_result); + void *pResult = pDE->m_externalMemoryHandle != NULL + ? static_cast(pDE->m_externalMemoryHandle->GetMemory()) + : static_cast(pDE->m_result); + ipce->FuncEvalComplete.resultAddr = (CORDB_ADDRESS)pResult; ipce->FuncEvalComplete.vmAppDomain.SetRawPtr(pDomain); ipce->FuncEvalComplete.vmObjectHandle = pDE->m_vmObjectHandle; + ipce->FuncEvalComplete.vmExternalMemoryHandle.SetRawPtr(pDE->m_externalMemoryHandle); LOG((LF_CORDB, LL_INFO1000, "D::FEC: TypeHandle is %p\n", pDE->m_resultType.AsPtr())); @@ -9832,17 +9889,19 @@ void Debugger::FuncEvalComplete(Thread* pThread, DebuggerEval *pDE) pDE->m_resultType, &ipce->FuncEvalComplete.resultType); - _ASSERTE(ipce->FuncEvalComplete.resultType.elementType != ELEMENT_TYPE_VALUETYPE); - - // We must adjust the result address to point to the right place - ipce->FuncEvalComplete.resultAddr = (CORDB_ADDRESS)(ArgSlotEndiannessFixup((ARG_SLOT*)(CORDB_ADDRESS_TO_PTR(ipce->FuncEvalComplete.resultAddr)), - GetSizeForCorElementType(ipce->FuncEvalComplete.resultType.elementType))); + if (ipce->FuncEvalComplete.resultType.elementType != ELEMENT_TYPE_VALUETYPE) + { + // We must adjust the result address to point to the right place + ipce->FuncEvalComplete.resultAddr = (CORDB_ADDRESS)(ArgSlotEndiannessFixup((ARG_SLOT*)(CORDB_ADDRESS_TO_PTR(ipce->FuncEvalComplete.resultAddr)), + GetSizeForCorElementType(ipce->FuncEvalComplete.resultType.elementType))); + } LOG((LF_CORDB, LL_INFO1000, "D::FEC: returned el %04x resultAddr %p\n", static_cast(ipce->FuncEvalComplete.resultType.elementType), (CORDB_ADDRESS_TO_PTR(ipce->FuncEvalComplete.resultAddr)))); - m_pRCThread->SendIPCEvent(); + IfFailThrow(m_pRCThread->SendIPCEvent()); + pDE->m_externalMemoryHandle = NULL; #endif } @@ -10879,6 +10938,13 @@ bool Debugger::HandleIPCEvent(DebuggerIPCEvent * pEvent) break; } + case DB_IPCE_DISPOSE_EXTERNAL_MEMORY_HANDLE: + { + DebuggerExternalMemoryHandle *pHandle = pEvent->DisposeExternalMemoryHandle.vmExternalMemoryHandle.GetRawPtr(); + DeleteInteropSafe(pHandle); + break; + } + #ifndef DACCESS_COMPILE case DB_IPCE_FUNC_EVAL_ABORT: diff --git a/src/coreclr/debug/ee/debugger.h b/src/coreclr/debug/ee/debugger.h index 48959f4815c735..754a27cea586b9 100644 --- a/src/coreclr/debug/ee/debugger.h +++ b/src/coreclr/debug/ee/debugger.h @@ -3361,6 +3361,23 @@ class DebuggerEvalBreakpointInfoSegment * type arguments > you get string followed by List followed by int. * ------------------------------------------------------------------------ */ +class DebuggerExternalMemoryHandle +{ +public: + DebuggerExternalMemoryHandle(AppDomain *pAppDomain, MethodTable *pMT, BYTE *pMemory); + ~DebuggerExternalMemoryHandle(); + + BYTE *GetMemory() const + { + return m_pMemory; + } + +private: + AppDomain *m_pAppDomain; + ExternalMemoryHandle *m_pHandle; + BYTE *m_pMemory; +}; + class DebuggerEval { public: @@ -3394,6 +3411,7 @@ class DebuggerEval PCODE m_targetCodeAddr; ARG_SLOT m_result[NUMBER_RETURNVALUE_SLOTS]; TypeHandle m_resultType; + DebuggerExternalMemoryHandle *m_externalMemoryHandle; SIZE_T m_arrayRank; FUNC_EVAL_ABORT_TYPE m_aborting; // Has an abort been requested, and what type. bool m_aborted; // Was this eval aborted @@ -3405,6 +3423,8 @@ class DebuggerEval DebuggerEval(T_CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX); + BYTE *CreateExternalMemoryHandle(MethodTable *pMT, SIZE_T size); + bool Init() { if (m_bpInfoSegment == NULL) @@ -3440,8 +3460,13 @@ class DebuggerEval { WRAPPER_NO_CONTRACT; + if (m_externalMemoryHandle != NULL) + { + DeleteInteropSafe(m_externalMemoryHandle); + } + // Clean up any temporary buffers used to send the argument type information. These were allocated - // in respnse to a GET_BUFFER message + // in response to a GET_BUFFER message. DebuggerIPCE_FuncEvalArgData *argData = GetArgData(); for (unsigned int i = 0; i < m_argCount; i++) { @@ -3463,6 +3488,7 @@ class DebuggerEval m_completed = false; #endif } + }; /* ------------------------------------------------------------------------ * diff --git a/src/coreclr/debug/ee/funceval.cpp b/src/coreclr/debug/ee/funceval.cpp index d1fe3751457ba0..a9c007475c18b6 100644 --- a/src/coreclr/debug/ee/funceval.cpp +++ b/src/coreclr/debug/ee/funceval.cpp @@ -2813,18 +2813,27 @@ void PackArgumentArray(DebuggerEval *pDE, size = max(size, (unsigned)ENREGISTERED_RETURNTYPE_MAXSIZE); #endif - BYTE * pTemp = new (interopsafe) BYTE[ALIGN_UP(sizeof(ValueClassInfo), 8) + size]; + LPVOID pData; + if (RetValueType.IsByRefLike()) + { + pData = pDE->CreateExternalMemoryHandle(RetValueType.GetMethodTable(), size); + memset(pData, 0, size); + } + else + { + BYTE * pTemp = new (interopsafe) BYTE[ALIGN_UP(sizeof(ValueClassInfo), 8) + size]; - ValueClassInfo * pValueClassInfo = (ValueClassInfo *)pTemp; - LPVOID pData = pTemp + ALIGN_UP(sizeof(ValueClassInfo), 8); + ValueClassInfo * pValueClassInfo = (ValueClassInfo *)pTemp; + pData = pTemp + ALIGN_UP(sizeof(ValueClassInfo), 8); - memset(pData, 0, size); + memset(pData, 0, size); - pValueClassInfo->pData = pData; - pValueClassInfo->pMT = RetValueType.GetMethodTable(); + pValueClassInfo->pData = pData; + pValueClassInfo->pMT = RetValueType.GetMethodTable(); - pValueClassInfo->pNext = *ppProtectedValueClasses; - *ppProtectedValueClasses = pValueClassInfo; + pValueClassInfo->pNext = *ppProtectedValueClasses; + *ppProtectedValueClasses = pValueClassInfo; + } pArguments[currArgSlot++] = PtrToArgSlot(pData); *ppRetValue = pData; @@ -2906,7 +2915,7 @@ void UnpackFuncEvalResult(DebuggerEval *pDE, pDE->m_result[0] = ObjToArgSlot(newObj); pDE->m_retValueBoxing = Debugger::AllBoxed; } - else if (!RetValueType.IsNull()) + else if (!RetValueType.IsNull() && !RetValueType.IsByRefLike()) { LOG((LF_CORDB, LL_EVERYTHING, "FuncEval call is saving a boxed VC return value.\n")); @@ -2939,10 +2948,16 @@ void UnpackFuncEvalResult(DebuggerEval *pDE, } else { - // - // Other FuncEvals return primitives as unboxed. - // - pDE->m_retValueBoxing = Debugger::OnlyPrimitivesUnboxed; + pDE->m_retValueBoxing = pDE->m_resultType.IsByRefLike() + ? Debugger::NoValueTypeBoxing + : Debugger::OnlyPrimitivesUnboxed; + + if (pDE->m_resultType.IsByRefLike() && pDE->m_externalMemoryHandle == NULL) + { + SIZE_T size = pDE->m_resultType.GetMethodTable()->GetNumInstanceFieldBytes(); + BYTE *pResult = pDE->CreateExternalMemoryHandle(pDE->m_resultType.GetMethodTable(), size); + memcpy(pResult, pDE->m_result, size); + } } LOG((LF_CORDB, LL_INFO10000, "FuncEval call has saved the return value.\n")); @@ -3338,7 +3353,7 @@ static void DoNormalFuncEval( DebuggerEval *pDE, OBJECTREF retObject = NULL; GCPROTECT_BEGIN(retObject); - if ((pDE->m_evalType != DB_IPCE_FET_NEW_OBJECT) && !RetValueType.IsNull()) + if ((pDE->m_evalType != DB_IPCE_FET_NEW_OBJECT) && !RetValueType.IsNull() && !RetValueType.IsByRefLike()) { ValidateFuncEvalReturnType(pDE->m_evalType, RetValueType.GetMethodTable()); RetValueType.GetMethodTable()->EnsureInstanceActive(); diff --git a/src/coreclr/debug/inc/dbgipcevents.h b/src/coreclr/debug/inc/dbgipcevents.h index 8a087492ca3d17..fcfe7d6482d424 100644 --- a/src/coreclr/debug/inc/dbgipcevents.h +++ b/src/coreclr/debug/inc/dbgipcevents.h @@ -779,6 +779,7 @@ DEFINE_VMPTR(class FieldDesc, PTR_FieldDesc, VMPTR_FieldDesc); // ObjectHandle is a safe way to represent an object into the GC heap. It gets updated // when a GC occurs. DEFINE_VMPTR(struct OBJECTHANDLE__, TADDR, VMPTR_OBJECTHANDLE); +DEFINE_VMPTR(class DebuggerExternalMemoryHandle, TADDR, VMPTR_DebuggerExternalMemoryHandle); DEFINE_VMPTR(class TypeHandle, PTR_TypeHandle, VMPTR_TypeHandle); @@ -1741,6 +1742,7 @@ struct MSLAYOUT DebuggerIPCEvent Portable vmAppDomain; Portable vmObjectHandle; + Portable vmExternalMemoryHandle; DebuggerIPCE_ExpandedTypeData resultType; } FuncEvalComplete; @@ -1840,6 +1842,11 @@ struct MSLAYOUT DebuggerIPCEvent Portable handleType; } DisposeHandle; + struct MSLAYOUT + { + Portable vmExternalMemoryHandle; + } DisposeExternalMemoryHandle; + struct MSLAYOUT { Portable framePointer; diff --git a/src/coreclr/debug/inc/dbgipceventtypes.h b/src/coreclr/debug/inc/dbgipceventtypes.h index 43411e02cd2bf8..1279c821e77a1f 100644 --- a/src/coreclr/debug/inc/dbgipceventtypes.h +++ b/src/coreclr/debug/inc/dbgipceventtypes.h @@ -130,4 +130,5 @@ IPC_EVENT_TYPE2(DB_IPCE_RESOLVE_UPDATE_METADATA_2 ,0x0257) IPC_EVENT_TYPE2(DB_IPCE_DISABLE_OPTS ,0x0258) IPC_EVENT_TYPE2(DB_IPCE_FORCE_CATCH_HANDLER_FOUND ,0x025A) IPC_EVENT_TYPE2(DB_IPCE_SET_ENABLE_CUSTOM_NOTIFICATION, 0x025B) -IPC_EVENT_TYPE0(DB_IPCE_DEBUGGER_LAST ,0x025C) // The last event from the debugger +IPC_EVENT_TYPE2(DB_IPCE_DISPOSE_EXTERNAL_MEMORY_HANDLE, 0x025C) +IPC_EVENT_TYPE0(DB_IPCE_DEBUGGER_LAST ,0x025D) // The last event from the debugger diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index 2129c3d94a60e9..e0133e4f647a91 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -3816,18 +3816,19 @@ ExternalMemoryHandle* AppDomain::AddExternalMemoryHandle(PTR_MethodTable pMT, PT return handle; } -void AppDomain::RemoveExternalMemoryHandle(ExternalMemoryHandle* handle) +void AppDomain::RemoveExternalMemoryHandle(ExternalMemoryHandle* handle DEBUG_ARG(bool isEESuspended)) { CONTRACTL { NOTHROW; GC_NOTRIGGER; - MODE_COOPERATIVE; + MODE_ANY; CAN_TAKE_LOCK; } CONTRACTL_END; _ASSERTE(handle != nullptr); + _ASSERTE(isEESuspended || (GetThreadNULLOk() != nullptr && GetThread()->PreemptiveGCDisabled())); bool removed; { @@ -3853,7 +3854,8 @@ void AppDomain::GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc) } CONTRACTL_END; - // Mutations happen in cooperative mode, so the suspended EE makes the list stable during a GC. + // Mutations happen in cooperative mode or while the debugger has suspended the EE, so the list + // is stable during a GC. for (ExternalMemoryHandle* handle = m_externalMemoryHandles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) { handle->GCScanRoot(fn, sc); diff --git a/src/coreclr/vm/appdomain.hpp b/src/coreclr/vm/appdomain.hpp index a3577ae98cd59c..9ffa5432888eed 100644 --- a/src/coreclr/vm/appdomain.hpp +++ b/src/coreclr/vm/appdomain.hpp @@ -1540,7 +1540,7 @@ class AppDomain final public: #ifndef DACCESS_COMPILE ExternalMemoryHandle* AddExternalMemoryHandle(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags); - void RemoveExternalMemoryHandle(ExternalMemoryHandle* handle); + void RemoveExternalMemoryHandle(ExternalMemoryHandle* handle DEBUG_ARG(bool isEESuspended = false)); #endif void GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc); From 78578027072f5ab213e09a75f2652625397f9730 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 14:17:12 -0700 Subject: [PATCH 06/24] Exclude byref-like return values from strong-handle creation CCR flagged that the strong-handle path treated any non-null RetValueType as a boxed object, including byref-like results whose slot holds value data copied into the external memory handle rather than an OBJECTREF. Exclude byref-like RetValueType from that condition; the external memory handle remains the sole GC root for byref-like func-eval results. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/ee/funceval.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/debug/ee/funceval.cpp b/src/coreclr/debug/ee/funceval.cpp index a9c007475c18b6..9913731dc572d5 100644 --- a/src/coreclr/debug/ee/funceval.cpp +++ b/src/coreclr/debug/ee/funceval.cpp @@ -2970,7 +2970,7 @@ void UnpackFuncEvalResult(DebuggerEval *pDE, CorElementType retClassET = pDE->m_resultType.GetSignatureCorElementType(); if ((pDE->m_retValueBoxing == Debugger::AllBoxed) || - !RetValueType.IsNull() || + (!RetValueType.IsNull() && !RetValueType.IsByRefLike()) || IsElementTypeSpecial(retClassET)) { LOG((LF_CORDB, LL_EVERYTHING, "Creating strong handle for boxed DoNormalFuncEval result.\n")); From dda7b25599a02dca19f1d26386aba413e1d484ac Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 14:35:58 -0700 Subject: [PATCH 07/24] Remove Unordered flag from CrstExternalMemoryHandle The new lock has no dependencies with other Crst types, so it can participate in ranking instead of being explicitly unordered. Regenerated crsttypes_generated.h via CrstTypeTool. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/CrstTypes.def | 1 - src/coreclr/inc/crsttypes_generated.h | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/inc/CrstTypes.def b/src/coreclr/inc/CrstTypes.def index 139fec19980fba..aece1015e30135 100644 --- a/src/coreclr/inc/CrstTypes.def +++ b/src/coreclr/inc/CrstTypes.def @@ -189,7 +189,6 @@ Crst ExecutableAllocatorLock End Crst ExternalMemoryHandle - Unordered End Crst FCall diff --git a/src/coreclr/inc/crsttypes_generated.h b/src/coreclr/inc/crsttypes_generated.h index f7312a0ce6429c..559dc30f574a83 100644 --- a/src/coreclr/inc/crsttypes_generated.h +++ b/src/coreclr/inc/crsttypes_generated.h @@ -162,7 +162,7 @@ int g_rgCrstLevelMap[] = 0, // CrstEventStore 0, // CrstException 0, // CrstExecutableAllocatorLock - -1, // CrstExternalMemoryHandle + 0, // CrstExternalMemoryHandle 3, // CrstFCall -1, // CrstFrozenObjectHeap 6, // CrstFuncPtrStubs From 47e9afcfc5ea0079a3513337205e4f1575d931d4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 15:13:47 -0700 Subject: [PATCH 08/24] Move ExternalMemoryHandle list to a process-wide static The external memory handle list was previously a member of AppDomain, even though there is only ever a single AppDomain per process. Move the list (and its lock) to a static on ExternalMemoryHandle itself so the type owns its own lifetime and stops hanging off AppDomain's grab-bag of unrelated state. - AppDomain::AddExternalMemoryHandle/RemoveExternalMemoryHandle/ GCScanExternalMemoryHandles become ExternalMemoryHandle::Add/Remove/ GCScanRoots (plus a new Init/Cleanup pair called from AppDomain::Init/~AppDomain). - DebuggerExternalMemoryHandle and DacRefWalker::WalkExternalMemoryHandles no longer need an AppDomain to reach the list. - The cDAC ExternalMemoryHandles contract now reads a new ExternalMemoryHandles global pointer instead of walking through the AppDomain data descriptor. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../datacontracts/ExternalMemoryHandles.md | 12 +- .../data-descriptor-meanings.json | 4 +- src/coreclr/debug/daccess/dacdbiimpl.cpp | 6 +- src/coreclr/debug/ee/debugger.cpp | 12 +- src/coreclr/debug/ee/debugger.h | 3 +- src/coreclr/vm/appdomain.cpp | 77 +------------ src/coreclr/vm/appdomain.hpp | 12 -- .../vm/datadescriptor/datadescriptor.inc | 2 +- src/coreclr/vm/externalmemoryhandle.cpp | 105 ++++++++++++++++++ src/coreclr/vm/externalmemoryhandle.h | 15 +++ src/coreclr/vm/gcenv.ee.cpp | 4 +- .../Constants.cs | 1 + .../Contracts/ExternalMemoryHandles_1.cs | 9 +- .../Data/AppDomain.cs | 2 - .../UnitTests/ExternalMemoryHandlesTests.cs | 25 ++--- .../RefWalkExternalMemoryHandleTests.cs | 15 +-- ...fWalkExternalMemoryHandlesContractTests.cs | 16 +-- 17 files changed, 158 insertions(+), 162 deletions(-) diff --git a/docs/design/datacontracts/ExternalMemoryHandles.md b/docs/design/datacontracts/ExternalMemoryHandles.md index 2abe5a14a48da0..729eab854bfbcd 100644 --- a/docs/design/datacontracts/ExternalMemoryHandles.md +++ b/docs/design/datacontracts/ExternalMemoryHandles.md @@ -25,12 +25,11 @@ IReadOnlyList GetRoots(bool resolveInteriorPointer | Data Descriptor | Field | Type | Meaning | | --- | --- | --- | --- | -| `AppDomain` | `ExternalMemoryHandles` | `pointer` | Pointer to the head of the AppDomain's external memory handle list (SListTail) | | `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 owning AppDomain's list | +| `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 | @@ -38,7 +37,7 @@ IReadOnlyList GetRoots(bool resolveInteriorPointer | Global | Type | Meaning | | --- | --- | --- | -| `AppDomain` | `pointer` | Pointer to the global application domain | +| `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 @@ -64,14 +63,11 @@ layout. ``` csharp IReadOnlyList IExternalMemoryHandles.GetRoots(bool resolveInteriorPointers) { - TargetPointer appDomain = // read the AppDomain global - if (appDomain == TargetPointer.Null) - return []; + TargetPointer headPointer = // read the ExternalMemoryHandles global + TargetPointer current = // read a pointer from headPointer - AppDomain domain = // read AppDomain object starting at appDomain HashSet visited = []; List roots = []; - TargetPointer current = domain.ExternalMemoryHandles; while (current != TargetPointer.Null) { if (!visited.Add(current)) diff --git a/docs/design/datacontracts/data-descriptor-meanings.json b/docs/design/datacontracts/data-descriptor-meanings.json index 2471dc135b3ac9..bd5efa01c8b7e4 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -1,7 +1,6 @@ { "_fields": { "AppDomain.AssemblyList": "Pointer to the list of assemblies in the application domain", - "AppDomain.ExternalMemoryHandles": "Pointer to the head of the AppDomain's external memory handle list (SListTail)", "AppDomain.FriendlyName": "Pointer to the application domain's friendly name", "AppDomain.RootAssembly": "Pointer to the root assembly", "Array.m_NumComponents": "Number of items in the array", @@ -186,7 +185,7 @@ "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 owning AppDomain's list", + "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", @@ -749,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", diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 8730e6dba8d84a..0dedfe453d7045 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -7871,12 +7871,8 @@ UINT32 DacRefWalker::GetHandleWalkerMask() HRESULT DacRefWalker::WalkExternalMemoryHandles() { - AppDomain* appDomain = AppDomain::GetCurrentDomain(); - if (appDomain == NULL) - return S_OK; - ExternalMemoryScanContext context(this); - appDomain->GCScanExternalMemoryHandles(ExternalMemoryHandleCallback, &context); + ExternalMemoryHandle::GCScanRoots(ExternalMemoryHandleCallback, &context); return context.Result; } diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index b8e128a67f2f3a..0d9af3cfbf3d81 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -1311,9 +1311,8 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval } } -DebuggerExternalMemoryHandle::DebuggerExternalMemoryHandle(AppDomain *pAppDomain, MethodTable *pMT, BYTE *pMemory) - : m_pAppDomain(pAppDomain), - m_pHandle(NULL), +DebuggerExternalMemoryHandle::DebuggerExternalMemoryHandle(MethodTable *pMT, BYTE *pMemory) + : m_pHandle(NULL), m_pMemory(pMemory) { CONTRACTL @@ -1324,14 +1323,14 @@ DebuggerExternalMemoryHandle::DebuggerExternalMemoryHandle(AppDomain *pAppDomain } CONTRACTL_END; - m_pHandle = m_pAppDomain->AddExternalMemoryHandle(pMT, m_pMemory, 0); + m_pHandle = ExternalMemoryHandle::Add(pMT, m_pMemory, 0); } DebuggerExternalMemoryHandle::~DebuggerExternalMemoryHandle() { WRAPPER_NO_CONTRACT; - m_pAppDomain->RemoveExternalMemoryHandle(m_pHandle DEBUG_ARG(g_pDebugger->IsStopped())); + ExternalMemoryHandle::Remove(m_pHandle DEBUG_ARG(g_pDebugger->IsStopped())); DeleteInteropSafe(m_pMemory); } @@ -1350,8 +1349,7 @@ BYTE *DebuggerEval::CreateExternalMemoryHandle(MethodTable *pMT, SIZE_T size) BYTE *pMemory = new (interopsafe) BYTE[size]; EX_TRY { - m_externalMemoryHandle = new (interopsafe) DebuggerExternalMemoryHandle( - AppDomain::GetCurrentDomain(), pMT, pMemory); + m_externalMemoryHandle = new (interopsafe) DebuggerExternalMemoryHandle(pMT, pMemory); } EX_CATCH { diff --git a/src/coreclr/debug/ee/debugger.h b/src/coreclr/debug/ee/debugger.h index 754a27cea586b9..42a837b2aa3d48 100644 --- a/src/coreclr/debug/ee/debugger.h +++ b/src/coreclr/debug/ee/debugger.h @@ -3364,7 +3364,7 @@ class DebuggerEvalBreakpointInfoSegment class DebuggerExternalMemoryHandle { public: - DebuggerExternalMemoryHandle(AppDomain *pAppDomain, MethodTable *pMT, BYTE *pMemory); + DebuggerExternalMemoryHandle(MethodTable *pMT, BYTE *pMemory); ~DebuggerExternalMemoryHandle(); BYTE *GetMemory() const @@ -3373,7 +3373,6 @@ class DebuggerExternalMemoryHandle } private: - AppDomain *m_pAppDomain; ExternalMemoryHandle *m_pHandle; BYTE *m_pMemory; }; diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index e0133e4f647a91..8aeedb65eca055 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -1653,10 +1653,7 @@ AppDomain::~AppDomain() m_AssemblyCache.Clear(); - while (!m_externalMemoryHandles.IsEmpty()) - { - delete m_externalMemoryHandles.RemoveHead(); - } + ExternalMemoryHandle::Cleanup(); } //***************************************************************************** @@ -1681,7 +1678,7 @@ void AppDomain::Init() m_crstGenericDictionaryExpansionLock.Init(CrstGenericDictionaryExpansion); m_FileLoadLock.Init(CrstAssemblyLoader, CrstFlags(CRST_DEFAULT)); m_DomainCacheCrst.Init(CrstAppDomainCache); - m_externalMemoryHandlesCrst.Init(CrstExternalMemoryHandle, CrstFlags(CRST_UNSAFE_COOPGC | CRST_TAKEN_DURING_SHUTDOWN)); + ExternalMemoryHandle::Init(); // Has to switch thread to GC_NOTRIGGER while being held m_crstAssemblyList.Init(CrstAssemblyList, CrstFlags( @@ -3792,76 +3789,8 @@ void AppDomain::EnumStaticGCRefs(promote_func* fn, ScanContext* sc) } } -ExternalMemoryHandle* AppDomain::AddExternalMemoryHandle(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags) -{ - CONTRACTL - { - THROWS; - GC_NOTRIGGER; - MODE_COOPERATIVE; - CAN_TAKE_LOCK; - } - CONTRACTL_END; - - _ASSERTE(pMT != nullptr); - _ASSERTE(pMemory != nullptr); - - ExternalMemoryHandle* handle = new ExternalMemoryHandle(pMT, pMemory, gcFlags); - - { - CrstHolder lock(&m_externalMemoryHandlesCrst); - m_externalMemoryHandles.InsertTail(handle); - } - - return handle; -} - -void AppDomain::RemoveExternalMemoryHandle(ExternalMemoryHandle* handle DEBUG_ARG(bool isEESuspended)) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - MODE_ANY; - CAN_TAKE_LOCK; - } - CONTRACTL_END; - - _ASSERTE(handle != nullptr); - _ASSERTE(isEESuspended || (GetThreadNULLOk() != nullptr && GetThread()->PreemptiveGCDisabled())); - - bool removed; - { - CrstHolder lock(&m_externalMemoryHandlesCrst); - removed = m_externalMemoryHandles.RemoveFirst(handle); - } - - _ASSERTE(removed); - if (!removed) - return; - - delete handle; -} - #endif // !DACCESS_COMPILE -void AppDomain::GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc) -{ - CONTRACTL - { - NOTHROW; - GC_NOTRIGGER; - } - CONTRACTL_END; - - // Mutations happen in cooperative mode or while the debugger has suspended the EE, so the list - // is stable during a GC. - for (ExternalMemoryHandle* handle = m_externalMemoryHandles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) - { - handle->GCScanRoot(fn, sc); - } -} - //------------------------------------------------------------------------ PTR_LoaderAllocator AppDomain::GetLoaderAllocator() { @@ -4277,7 +4206,7 @@ AppDomain::EnumMemoryRegions(CLRDataEnumMemoryFlags flags, bool enumThis) pAssembly->EnumMemoryRegions(flags); } - for (ExternalMemoryHandle* handle = m_externalMemoryHandles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) + for (ExternalMemoryHandle* handle = ExternalMemoryHandle::GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) { DacEnumMemoryRegion(dac_cast(handle), sizeof(ExternalMemoryHandle)); handle->EnumMemoryRegions(flags); diff --git a/src/coreclr/vm/appdomain.hpp b/src/coreclr/vm/appdomain.hpp index 9ffa5432888eed..45aa7f5c7fb605 100644 --- a/src/coreclr/vm/appdomain.hpp +++ b/src/coreclr/vm/appdomain.hpp @@ -1536,17 +1536,6 @@ class AppDomain final TieredCompilationManager m_tieredCompilationManager; friend struct cdac_data; - -public: -#ifndef DACCESS_COMPILE - ExternalMemoryHandle* AddExternalMemoryHandle(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags); - void RemoveExternalMemoryHandle(ExternalMemoryHandle* handle DEBUG_ARG(bool isEESuspended = false)); -#endif - void GCScanExternalMemoryHandles(promote_func *fn, ScanContext *sc); - -private: - CrstExplicitInit m_externalMemoryHandlesCrst; - SListTail m_externalMemoryHandles; }; // class AppDomain template<> @@ -1555,7 +1544,6 @@ struct cdac_data static constexpr size_t RootAssembly = offsetof(AppDomain, m_pRootAssembly); static constexpr size_t AssemblyList = offsetof(AppDomain, m_Assemblies) + offsetof(AppDomain::AssemblyList, m_array); static constexpr size_t FriendlyName = offsetof(AppDomain, m_friendlyName); - static constexpr size_t ExternalMemoryHandles = offsetof(AppDomain, m_externalMemoryHandles) + offsetof(SListTail, m_pHead); }; typedef DPTR(class SystemDomain) PTR_SystemDomain; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 89242a340ec5c2..b7a8497c26ea7f 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -481,7 +481,6 @@ CDAC_TYPE_INDETERMINATE(AppDomain) CDAC_TYPE_FIELD(AppDomain, T_POINTER, RootAssembly, cdac_data::RootAssembly) CDAC_TYPE_FIELD(AppDomain, TYPE(ArrayListBase), AssemblyList, cdac_data::AssemblyList) CDAC_TYPE_FIELD(AppDomain, T_POINTER, FriendlyName, cdac_data::FriendlyName) -CDAC_TYPE_FIELD(AppDomain, T_POINTER, ExternalMemoryHandles, cdac_data::ExternalMemoryHandles) CDAC_TYPE_END(AppDomain) CDAC_TYPE_BEGIN(ExternalMemoryHandle) @@ -1799,6 +1798,7 @@ CDAC_GLOBAL_POINTER(CanonMethodTable, &::g_pCanonMethodTableClass) CDAC_GLOBAL_POINTER(ContinuationMethodTable, &::g_pContinuationClassIfSubTypeCreated) CDAC_GLOBAL_POINTER(ContinuationSingletonEEClass, &::g_singletonContinuationEEClass) CDAC_GLOBAL_POINTER(ExceptionMethodTable, &::g_pExceptionClass) +CDAC_GLOBAL_POINTER(ExternalMemoryHandles, cdac_data::HandlesHead) CDAC_GLOBAL_POINTER(FreeObjectMethodTable, &::g_pFreeObjectMethodTable) CDAC_GLOBAL_POINTER(MulticastDelegateMethodTable, &::g_pMulticastDelegateClass) CDAC_GLOBAL_POINTER(ObjectMethodTable, &::g_pObjectClass) diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp index 0e5a9ac8b968dd..23137e9e5715fc 100644 --- a/src/coreclr/vm/externalmemoryhandle.cpp +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -4,6 +4,111 @@ #include "externalmemoryhandle.h" #include "siginfo.hpp" +CrstExplicitInit ExternalMemoryHandle::s_crst; +SListTail ExternalMemoryHandle::s_handles; + +#ifndef DACCESS_COMPILE + +void ExternalMemoryHandle::Init() +{ + STANDARD_VM_CONTRACT; + + s_crst.Init(CrstExternalMemoryHandle, CrstFlags(CRST_UNSAFE_COOPGC | CRST_TAKEN_DURING_SHUTDOWN)); +} + +ExternalMemoryHandle* ExternalMemoryHandle::Add(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_COOPERATIVE; + CAN_TAKE_LOCK; + } + CONTRACTL_END; + + _ASSERTE(pMT != nullptr); + _ASSERTE(pMemory != nullptr); + + ExternalMemoryHandle* handle = new ExternalMemoryHandle(pMT, pMemory, gcFlags); + + { + CrstHolder lock(&s_crst); + s_handles.InsertTail(handle); + } + + return handle; +} + +void ExternalMemoryHandle::Remove(ExternalMemoryHandle* handle DEBUG_ARG(bool isEESuspended)) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + MODE_ANY; + CAN_TAKE_LOCK; + } + CONTRACTL_END; + + _ASSERTE(handle != nullptr); + _ASSERTE(isEESuspended || (GetThreadNULLOk() != nullptr && GetThread()->PreemptiveGCDisabled())); + + bool removed; + { + CrstHolder lock(&s_crst); + removed = s_handles.RemoveFirst(handle); + } + + _ASSERTE(removed); + if (!removed) + return; + + delete handle; +} + +void ExternalMemoryHandle::Cleanup() +{ + CONTRACTL + { + NOTHROW; + GC_TRIGGERS; + MODE_ANY; + } + CONTRACTL_END; + + while (!s_handles.IsEmpty()) + { + delete s_handles.RemoveHead(); + } +} + +#endif // !DACCESS_COMPILE + +void ExternalMemoryHandle::GCScanRoots(promote_func *fn, ScanContext *sc) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + } + CONTRACTL_END; + + // Mutations happen in cooperative mode or while the debugger has suspended the EE, so the list + // is stable during a GC. + for (ExternalMemoryHandle* handle = s_handles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) + { + handle->GCScanRoot(fn, sc); + } +} + +PTR_ExternalMemoryHandle ExternalMemoryHandle::GetHead() +{ + LIMITED_METHOD_DAC_CONTRACT; + + return s_handles.GetHead(); +} + void ExternalMemoryHandle::GCScanRoot(promote_func *fn, ScanContext *sc) { CONTRACTL diff --git a/src/coreclr/vm/externalmemoryhandle.h b/src/coreclr/vm/externalmemoryhandle.h index d48d44de55c8cf..cad74d431e4c5c 100644 --- a/src/coreclr/vm/externalmemoryhandle.h +++ b/src/coreclr/vm/externalmemoryhandle.h @@ -33,10 +33,24 @@ class ExternalMemoryHandle final // Next pointer for SList linkage. PTR_ExternalMemoryHandle m_pNext; + // The list of external memory handles is process-wide (there is only ever one AppDomain), so it + // is kept as a static on ExternalMemoryHandle rather than hanging off AppDomain. +#ifndef DACCESS_COMPILE + static void Init(); + static ExternalMemoryHandle* Add(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags); + static void Remove(ExternalMemoryHandle* handle DEBUG_ARG(bool isEESuspended = false)); + static void Cleanup(); +#endif + static void GCScanRoots(promote_func *fn, ScanContext *sc); + static PTR_ExternalMemoryHandle GetHead(); + private: PTR_MethodTable m_pMT; PTR_VOID m_pMemory; UINT m_gcFlags; + + static CrstExplicitInit s_crst; + static SListTail s_handles; }; template<> @@ -46,6 +60,7 @@ struct cdac_data static constexpr size_t MethodTable = offsetof(ExternalMemoryHandle, m_pMT); static constexpr size_t Memory = offsetof(ExternalMemoryHandle, m_pMemory); static constexpr size_t GCFlags = offsetof(ExternalMemoryHandle, m_gcFlags); + static constexpr PTR_ExternalMemoryHandle* HandlesHead = &ExternalMemoryHandle::s_handles.m_pHead; }; #endif // EXTERNALMEMORYHANDLE_HPP diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index 8e7976c4e54500..0c7368f1fe4b4b 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -337,9 +337,9 @@ void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, // External memory handles are a single global list. Only one server GC thread may update its // root slots during relocation. - if (sc->thread_number == 0 && ::GetAppDomain()) + if (sc->thread_number == 0) { - ::GetAppDomain()->GCScanExternalMemoryHandles(fn, sc); + ExternalMemoryHandle::GCScanRoots(fn, sc); } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs index 8c257267fa5072..11c229f47b896e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -40,6 +40,7 @@ public static class Globals public const string CanonMethodTable = nameof(CanonMethodTable); public const string ContinuationSingletonEEClass = nameof(ContinuationSingletonEEClass); public const string ExceptionMethodTable = nameof(ExceptionMethodTable); + public const string ExternalMemoryHandles = nameof(ExternalMemoryHandles); public const string FreeObjectMethodTable = nameof(FreeObjectMethodTable); public const string ObjectMethodTable = nameof(ObjectMethodTable); public const string ObjectArrayMethodTable = nameof(ObjectArrayMethodTable); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs index 6a7c4008acbeb5..e7c6a86e470a2c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs @@ -25,15 +25,10 @@ internal ExternalMemoryHandles_1(Target target) IReadOnlyList IExternalMemoryHandles.GetRoots(bool resolveInteriorPointers) { List roots = []; - TargetPointer appDomainPointer = _target.ReadGlobalPointer(Constants.Globals.AppDomain); - TargetPointer appDomain = _target.ReadPointer(appDomainPointer); - if (appDomain == TargetPointer.Null) - return roots; - - Data.AppDomain domain = _target.ProcessedData.GetOrAdd(appDomain); + TargetPointer headPointer = _target.ReadGlobalPointer(Constants.Globals.ExternalMemoryHandles); + TargetPointer current = _target.ReadPointer(headPointer); HashSet visited = []; - TargetPointer current = domain.ExternalMemoryHandles; while (current != TargetPointer.Null) { if (!visited.Add(current)) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AppDomain.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AppDomain.cs index 15d3a926defbb8..acf55e4fce52e9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AppDomain.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/AppDomain.cs @@ -12,6 +12,4 @@ internal sealed partial class AppDomain : IData public partial TargetPointer AssemblyList { get; } [Field] public partial TargetPointer FriendlyName { get; } - - [Field] public partial TargetPointer ExternalMemoryHandles { get; } } diff --git a/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs b/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs index 3744b30f8d422d..5e3d822e0bc296 100644 --- a/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs @@ -14,8 +14,7 @@ public class ExternalMemoryHandlesTests { private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; - private const ulong AppDomainStaticSlotAddr = 0x0500; - private const ulong AppDomainAddr = 0x1000; + private const ulong ExternalMemoryHandlesHeadSlotAddr = 0x0500; private const ulong Handle1Addr = 0x2000; private const ulong Handle2Addr = 0x2100; private const ulong MethodTable1Addr = 0x9000; @@ -23,7 +22,7 @@ public class ExternalMemoryHandlesTests private const ulong Memory1Addr = 0x9500; private const ulong Memory2Addr = 0x9600; - private static TestPlaceholderTarget CreateTarget(bool hasAppDomain = true) + private static TestPlaceholderTarget CreateTarget(bool hasHandles = true) { TargetTestHelpers helpers = new(Arch); int ptrSize = helpers.PointerSize; @@ -37,17 +36,10 @@ private static TestPlaceholderTarget CreateTarget(bool hasAppDomain = true) var gc = new Mock(); var targetBuilder = new TestPlaceholderTarget.Builder(Arch) - .AddGlobals(("AppDomain", AppDomainStaticSlotAddr)) + .AddGlobals((Constants.Globals.ExternalMemoryHandles, ExternalMemoryHandlesHeadSlotAddr)) .AddGlobals((nameof(Constants.Globals.ObjectToMethodTableUnmask), 0ul)) .AddTypes(new Dictionary { - [DataType.AppDomain] = new() - { - Fields = new Dictionary - { - { nameof(Data.AppDomain.ExternalMemoryHandles), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, - } - }, [DataType.ExternalMemoryHandle] = new() { Fields = new Dictionary @@ -66,11 +58,8 @@ private static TestPlaceholderTarget CreateTarget(bool hasAppDomain = true) .AddMockContract(gc) .AddMockContract(rts); - // AppDomain* static slot -> the AppDomain instance - targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainStaticSlotAddr, hasAppDomain ? AppDomainAddr : 0)); - - // AppDomain.ExternalMemoryHandles -> Handle1 - targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainAddr, Handle1Addr)); + // ExternalMemoryHandles global slot -> head of the list (or null when empty) + targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, ExternalMemoryHandlesHeadSlotAddr, hasHandles ? Handle1Addr : 0)); // Handle1: Next -> Handle2, MethodTable1, Memory1, GCFlags=0 targetBuilder.MemoryBuilder.AddHeapFragment(ExternalMemoryHandleFragment(helpers, Handle1Addr, Handle2Addr, MethodTable1Addr, Memory1Addr, 0)); @@ -117,9 +106,9 @@ public void GetRoots_WalksChain() } [Fact] - public void GetRoots_NullAppDomain_ReturnsEmpty() + public void GetRoots_NoHandles_ReturnsEmpty() { - TestPlaceholderTarget target = CreateTarget(hasAppDomain: false); + TestPlaceholderTarget target = CreateTarget(hasHandles: false); IExternalMemoryHandles externalMemoryHandles = target.Contracts.ExternalMemoryHandles; IReadOnlyList roots = externalMemoryHandles.GetRoots(resolveInteriorPointers: true); diff --git a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs index cad0572b12e240..edb42a17bed79f 100644 --- a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs @@ -15,8 +15,7 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; public class ExternalMemoryHandleRootTests { private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; - private static readonly TargetPointer AppDomainAddr = new(0x1000); - private const ulong AppDomainStaticSlotAddr = 0x0500; + private const ulong ExternalMemoryHandlesHeadSlotAddr = 0x0500; private const ulong HandleAddr = 0x1800; private const ulong MethodTableAddr = 0x2000; private const ulong ObjectSize = 0x100; @@ -37,16 +36,9 @@ private static TestPlaceholderTarget CreateTarget( mockGC.Setup(g => g.GetGCIdentifiers()).Returns([]); var builder = new TestPlaceholderTarget.Builder(Arch) - .AddGlobals(("AppDomain", AppDomainStaticSlotAddr)) + .AddGlobals((Constants.Globals.ExternalMemoryHandles, ExternalMemoryHandlesHeadSlotAddr)) .AddTypes(new Dictionary { - [DataType.AppDomain] = new() - { - Fields = new Dictionary - { - { nameof(Data.AppDomain.ExternalMemoryHandles), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, - } - }, [DataType.ExternalMemoryHandle] = new() { Fields = new Dictionary @@ -66,8 +58,7 @@ private static TestPlaceholderTarget CreateTarget( .AddMockContract(mockGC) .AddMockContract(rts); - builder.MemoryBuilder.AddHeapFragment(PointerFragment(AppDomainStaticSlotAddr, AppDomainAddr.Value)); - builder.MemoryBuilder.AddHeapFragment(PointerFragment(AppDomainAddr.Value, hasHandle ? HandleAddr : 0)); + builder.MemoryBuilder.AddHeapFragment(PointerFragment(ExternalMemoryHandlesHeadSlotAddr, hasHandle ? HandleAddr : 0)); if (hasHandle) builder.MemoryBuilder.AddHeapFragment(ExternalMemoryHandleFragment(memory.Value, gcFlags)); diff --git a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs index e2af4a0476870b..66284f84024a15 100644 --- a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs @@ -18,6 +18,7 @@ public class RefWalkExternalMemoryHandlesContractTests private const ulong AppDomainStaticSlotAddr = 0x0500; private const ulong AppDomainAddr = 0x1000; + private const ulong ExternalMemoryHandlesHeadSlotAddr = 0x0600; private const ulong HandleAddr = 0x2000; private const ulong MethodTableAddr = 0x9000; private const ulong MemoryAddr = 0x9500; @@ -32,15 +33,10 @@ private static TestPlaceholderTarget CreateTarget(Mock rts) var targetBuilder = new TestPlaceholderTarget.Builder(Arch) .AddGlobals(("AppDomain", AppDomainStaticSlotAddr)) + .AddGlobals((Constants.Globals.ExternalMemoryHandles, ExternalMemoryHandlesHeadSlotAddr)) .AddTypes(new Dictionary { - [DataType.AppDomain] = new() - { - Fields = new Dictionary - { - { nameof(Data.AppDomain.ExternalMemoryHandles), new() { Offset = 0, TypeName = DataType.pointer.ToString() } }, - } - }, + [DataType.AppDomain] = new(), [DataType.ExternalMemoryHandle] = new() { Fields = new Dictionary @@ -64,11 +60,11 @@ private static TestPlaceholderTarget CreateTarget(Mock rts) .AddMockContract(mockGC) .AddMockContract(rts); - // AppDomain* static slot -> the AppDomain instance + // AppDomain* static slot -> the AppDomain instance (used by ILoader.GetAppDomain for vmDomain) targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainStaticSlotAddr, AppDomainAddr)); - // AppDomain.ExternalMemoryHandles -> a single handle - targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainAddr, HandleAddr)); + // ExternalMemoryHandles global slot -> a single handle + targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, ExternalMemoryHandlesHeadSlotAddr, HandleAddr)); // Handle: Next -> null, MethodTable, Memory, GCFlags=0 (ordinary reference-type root) targetBuilder.MemoryBuilder.AddHeapFragment(ExternalMemoryHandleFragment(helpers, HandleAddr, 0, MethodTableAddr, MemoryAddr, 0)); From 767e3aea7719bc855988dd36aa5860233ace5999 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 15:45:01 -0700 Subject: [PATCH 09/24] Use CrstStatic for ExternalMemoryHandle::s_crst CrstStatic (rather than CrstExplicitInit) is the established pattern for a genuine process-wide static Crst that is explicitly Init()'d and never destructed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/externalmemoryhandle.cpp | 2 +- src/coreclr/vm/externalmemoryhandle.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp index 23137e9e5715fc..2e9b51c8c7df53 100644 --- a/src/coreclr/vm/externalmemoryhandle.cpp +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -4,7 +4,7 @@ #include "externalmemoryhandle.h" #include "siginfo.hpp" -CrstExplicitInit ExternalMemoryHandle::s_crst; +CrstStatic ExternalMemoryHandle::s_crst; SListTail ExternalMemoryHandle::s_handles; #ifndef DACCESS_COMPILE diff --git a/src/coreclr/vm/externalmemoryhandle.h b/src/coreclr/vm/externalmemoryhandle.h index cad74d431e4c5c..7279f1396bc064 100644 --- a/src/coreclr/vm/externalmemoryhandle.h +++ b/src/coreclr/vm/externalmemoryhandle.h @@ -49,7 +49,7 @@ class ExternalMemoryHandle final PTR_VOID m_pMemory; UINT m_gcFlags; - static CrstExplicitInit s_crst; + static CrstStatic s_crst; static SListTail s_handles; }; From a04e25e20ce0759c84e1e7a24f7002124ddcb96f Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 15:46:53 -0700 Subject: [PATCH 10/24] Remove comment on ExternalMemoryHandle static list Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/externalmemoryhandle.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/coreclr/vm/externalmemoryhandle.h b/src/coreclr/vm/externalmemoryhandle.h index 7279f1396bc064..da6115547e3c7b 100644 --- a/src/coreclr/vm/externalmemoryhandle.h +++ b/src/coreclr/vm/externalmemoryhandle.h @@ -33,8 +33,6 @@ class ExternalMemoryHandle final // Next pointer for SList linkage. PTR_ExternalMemoryHandle m_pNext; - // The list of external memory handles is process-wide (there is only ever one AppDomain), so it - // is kept as a static on ExternalMemoryHandle rather than hanging off AppDomain. #ifndef DACCESS_COMPILE static void Init(); static ExternalMemoryHandle* Add(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags); From c86127a68ebcf4a7224be560935123503e86d80b Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Tue, 15 Sep 2026 16:09:42 -0700 Subject: [PATCH 11/24] Skip external memory handle scan during concurrent BGC mark Add/Remove mutate the ExternalMemoryHandle list under s_crst, but GCScanRoots walked it without taking that lock. During the initial, concurrent mark phase of a background GC the EE keeps running, so a debugger func-eval could concurrently insert or delete a node while the GC thread iterates, risking a use-after-free (delete) or a missed root (insert). Follow the existing pattern used for interop ref-counted handle callbacks (Interop::OnBeforeGCScanRoots/OnAfterGCScanRoots) and for AppDomain statics (EnumAllStaticGCRefs), which skip work during the concurrent phase: only invoke ExternalMemoryHandle::GCScanRoots when sc->concurrent is false. The list can only be mutated while the EE is suspended for a GC or by a thread in cooperative mode, so it is safe to walk lock-free during the blocking mark/relocate phases and the final, EE-suspended mark phase of a background GC; the concurrent mark phase is simply skipped, and the subsequent final mark phase sees the up-to-date list. --- src/coreclr/vm/externalmemoryhandle.cpp | 5 +++-- src/coreclr/vm/gcenv.ee.cpp | 10 +++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp index 2e9b51c8c7df53..cc8a7c6cb188c3 100644 --- a/src/coreclr/vm/externalmemoryhandle.cpp +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -94,8 +94,9 @@ void ExternalMemoryHandle::GCScanRoots(promote_func *fn, ScanContext *sc) } CONTRACTL_END; - // Mutations happen in cooperative mode or while the debugger has suspended the EE, so the list - // is stable during a GC. + // The caller (GCToEEInterface::GcScanRoots) only invokes this outside the concurrent mark phase of + // a background GC, so the EE is always suspended for a GC (or, for the DAC, the target process is + // stopped) whenever this list is walked, and the list cannot be mutated concurrently with this scan. for (ExternalMemoryHandle* handle = s_handles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) { handle->GCScanRoot(fn, sc); diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index 0c7368f1fe4b4b..ea8c601b14a484 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -337,7 +337,15 @@ void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, // External memory handles are a single global list. Only one server GC thread may update its // root slots during relocation. - if (sc->thread_number == 0) + // + // The list can only be mutated while the EE is suspended for a GC or by a thread in cooperative + // mode, so it is safe to walk without taking the list's lock during any non-concurrent scan (the + // blocking mark/relocate phases, and the final, EE-suspended mark phase of a background GC). It is + // not safe to walk during the initial, concurrent mark phase of a background GC, since the EE is + // running then and a cooperative-mode thread could add or remove a handle while we are iterating. + // Skip the scan in that phase; the subsequent final mark phase, which runs with the EE suspended, + // will see the up-to-date list. + if (sc->thread_number == 0 && !sc->concurrent) { ExternalMemoryHandle::GCScanRoots(fn, sc); } From 6c084d98a2ee3beed5d6bb0021b73bda433d3c19 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 16 Sep 2026 11:14:08 -0700 Subject: [PATCH 12/24] Take s_crst during ExternalMemoryHandle::Cleanup Add and Remove serialize access to s_handles with s_crst, but shutdown cleanup walked and mutated the same list without taking that lock. If an external handle were disposed while AppDomain teardown was running, this could race with IsEmpty/RemoveHead and corrupt the list. Hold s_crst for the whole cleanup traversal; the lock is explicitly initialized with CRST_TAKEN_DURING_SHUTDOWN for this purpose. --- src/coreclr/vm/externalmemoryhandle.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp index cc8a7c6cb188c3..657c497ae786c4 100644 --- a/src/coreclr/vm/externalmemoryhandle.cpp +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -74,9 +74,12 @@ void ExternalMemoryHandle::Cleanup() NOTHROW; GC_TRIGGERS; MODE_ANY; + CAN_TAKE_LOCK; } CONTRACTL_END; + CrstHolder lock(&s_crst); + while (!s_handles.IsEmpty()) { delete s_handles.RemoveHead(); From 0c0117f31165fcc748e5a715aead8ec2dfe89070 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 16 Sep 2026 11:39:35 -0700 Subject: [PATCH 13/24] Cleanup condition and make the comment reasonable. --- src/coreclr/vm/gcenv.ee.cpp | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index ea8c601b14a484..1d0dc77b2188de 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -335,17 +335,11 @@ void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, } } - // External memory handles are a single global list. Only one server GC thread may update its - // root slots during relocation. - // - // The list can only be mutated while the EE is suspended for a GC or by a thread in cooperative - // mode, so it is safe to walk without taking the list's lock during any non-concurrent scan (the - // blocking mark/relocate phases, and the final, EE-suspended mark phase of a background GC). It is - // not safe to walk during the initial, concurrent mark phase of a background GC, since the EE is - // running then and a cooperative-mode thread could add or remove a handle while we are iterating. - // Skip the scan in that phase; the subsequent final mark phase, which runs with the EE suspended, - // will see the up-to-date list. - if (sc->thread_number == 0 && !sc->concurrent) + // Only mark external memory handles during non-concurrent scanning. + // During concurrent scanning, other EE threads may add or remove handles. + // To avoid taking a lock when we scan the external memory handles, + // we only scan during a non-concurrent phase when the EE threads are blocked. + if (!sc->concurrent) { ExternalMemoryHandle::GCScanRoots(fn, sc); } From ea46337437f0fae544995b06f4eb4ba945169b3d Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 16 Sep 2026 11:45:22 -0700 Subject: [PATCH 14/24] Remove early-return --- src/coreclr/vm/externalmemoryhandle.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp index 657c497ae786c4..ccb07b821ba200 100644 --- a/src/coreclr/vm/externalmemoryhandle.cpp +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -61,8 +61,6 @@ void ExternalMemoryHandle::Remove(ExternalMemoryHandle* handle DEBUG_ARG(bool is } _ASSERTE(removed); - if (!removed) - return; delete handle; } From 1c4f3535de80157f3766bed4a250d33154bb7667 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 16 Sep 2026 13:49:33 -0700 Subject: [PATCH 15/24] Move ExternalMemoryHandle init to EEStartupHelper; clean up includes - Move ExternalMemoryHandle::Init() call from AppDomain::Init() to EEStartupHelper in ceemain.cpp, alongside other process-wide GC-scanned static structures initialized before the GC heap. - Remove the transitive #include "externalmemoryhandle.h" from appdomain.hpp; add explicit includes to the .cpp/.h files that actually reference ExternalMemoryHandle (appdomain.cpp, ceemain.cpp, gcenv.ee.cpp, debugger.cpp, dacdbiimpl.cpp, datadescriptor.h), and a forward declaration in debugger.h where only a pointer member is needed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 1 + src/coreclr/debug/ee/debugger.cpp | 1 + src/coreclr/debug/ee/debugger.h | 1 + src/coreclr/vm/appdomain.cpp | 10 +---- src/coreclr/vm/appdomain.hpp | 2 - src/coreclr/vm/ceemain.cpp | 3 ++ .../vm/datadescriptor/datadescriptor.h | 1 + src/coreclr/vm/externalmemoryhandle.cpp | 37 ++++++------------- src/coreclr/vm/externalmemoryhandle.h | 3 +- src/coreclr/vm/gcenv.ee.cpp | 1 + .../Contracts/ExternalMemoryHandles_1.cs | 3 +- 11 files changed, 24 insertions(+), 39 deletions(-) diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 0dedfe453d7045..50736e614abb03 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -20,6 +20,7 @@ #include "generics.h" #include "stackwalk.h" #include "virtualcallstub.h" +#include "externalmemoryhandle.h" #include "dacdbiimpl.h" diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index 0d9af3cfbf3d81..0ba01f963744a8 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -23,6 +23,7 @@ #include "../../vm/dwreport.h" #include "../../vm/eepolicy.h" #include "../../vm/excep.h" +#include "../../vm/externalmemoryhandle.h" #if defined(FEATURE_DBGIPC_TRANSPORT_VM) #include "dbgtransportsession.h" diff --git a/src/coreclr/debug/ee/debugger.h b/src/coreclr/debug/ee/debugger.h index 42a837b2aa3d48..76da93ff226a0b 100644 --- a/src/coreclr/debug/ee/debugger.h +++ b/src/coreclr/debug/ee/debugger.h @@ -90,6 +90,7 @@ class DebuggerEval; class DebuggerControllerQueue; class DebuggerController; class Crst; +class ExternalMemoryHandle; typedef CUnorderedArray PATCH_UNORDERED_ARRAY; template void DeleteInteropSafe(T *p); diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index 8aeedb65eca055..afb7b7cc5c92d5 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -4,6 +4,7 @@ #include "common.h" #include "appdomain.hpp" +#include "externalmemoryhandle.h" #include "peimagelayout.inl" #include "field.h" #include "strongnameinternal.h" @@ -1652,8 +1653,6 @@ AppDomain::~AppDomain() CONTRACTL_END; m_AssemblyCache.Clear(); - - ExternalMemoryHandle::Cleanup(); } //***************************************************************************** @@ -1678,7 +1677,6 @@ void AppDomain::Init() m_crstGenericDictionaryExpansionLock.Init(CrstGenericDictionaryExpansion); m_FileLoadLock.Init(CrstAssemblyLoader, CrstFlags(CRST_DEFAULT)); m_DomainCacheCrst.Init(CrstAppDomainCache); - ExternalMemoryHandle::Init(); // Has to switch thread to GC_NOTRIGGER while being held m_crstAssemblyList.Init(CrstAssemblyList, CrstFlags( @@ -4206,11 +4204,7 @@ AppDomain::EnumMemoryRegions(CLRDataEnumMemoryFlags flags, bool enumThis) pAssembly->EnumMemoryRegions(flags); } - for (ExternalMemoryHandle* handle = ExternalMemoryHandle::GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) - { - DacEnumMemoryRegion(dac_cast(handle), sizeof(ExternalMemoryHandle)); - handle->EnumMemoryRegions(flags); - } + ExternalMemoryHandle::EnumMemoryRegionsForAllHandles(flags); } void diff --git a/src/coreclr/vm/appdomain.hpp b/src/coreclr/vm/appdomain.hpp index 45aa7f5c7fb605..1a014b6d66814c 100644 --- a/src/coreclr/vm/appdomain.hpp +++ b/src/coreclr/vm/appdomain.hpp @@ -35,8 +35,6 @@ #include "codeversion.h" -#include "externalmemoryhandle.h" - class SystemDomain; class AppDomain; class GlobalStringLiteralMap; diff --git a/src/coreclr/vm/ceemain.cpp b/src/coreclr/vm/ceemain.cpp index 188cdb9abd50b4..806ee604244bc7 100644 --- a/src/coreclr/vm/ceemain.cpp +++ b/src/coreclr/vm/ceemain.cpp @@ -144,6 +144,7 @@ #include "cordbpriv.h" #include "comdelegate.h" #include "appdomain.hpp" +#include "externalmemoryhandle.h" #include "eventtrace.h" #include "corhost.h" #include "binder.h" @@ -910,6 +911,8 @@ void EEStartupHelper() // Set up the sync block SyncBlockCache::Start(); + ExternalMemoryHandle::Init(); + // This isn't done as part of InitializeGarbageCollector() above because it // requires write barriers to have been set up on x86, which happens as part // of InitJITWriteBarrierHelpers. diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.h b/src/coreclr/vm/datadescriptor/datadescriptor.h index 02ab1064a8c76e..b154113686fef6 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.h +++ b/src/coreclr/vm/datadescriptor/datadescriptor.h @@ -22,6 +22,7 @@ #include "threads.h" #include "vars.hpp" #include "exinfo.h" +#include "externalmemoryhandle.h" #include "configure.h" diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp index ccb07b821ba200..8806c54b64068e 100644 --- a/src/coreclr/vm/externalmemoryhandle.cpp +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -65,25 +65,6 @@ void ExternalMemoryHandle::Remove(ExternalMemoryHandle* handle DEBUG_ARG(bool is delete handle; } -void ExternalMemoryHandle::Cleanup() -{ - CONTRACTL - { - NOTHROW; - GC_TRIGGERS; - MODE_ANY; - CAN_TAKE_LOCK; - } - CONTRACTL_END; - - CrstHolder lock(&s_crst); - - while (!s_handles.IsEmpty()) - { - delete s_handles.RemoveHead(); - } -} - #endif // !DACCESS_COMPILE void ExternalMemoryHandle::GCScanRoots(promote_func *fn, ScanContext *sc) @@ -104,13 +85,6 @@ void ExternalMemoryHandle::GCScanRoots(promote_func *fn, ScanContext *sc) } } -PTR_ExternalMemoryHandle ExternalMemoryHandle::GetHead() -{ - LIMITED_METHOD_DAC_CONTRACT; - - return s_handles.GetHead(); -} - void ExternalMemoryHandle::GCScanRoot(promote_func *fn, ScanContext *sc) { CONTRACTL @@ -156,4 +130,15 @@ void ExternalMemoryHandle::EnumMemoryRegions(CLRDataEnumMemoryFlags flags) DacEnumMemoryRegion(m_pMemory.GetAddr(), size); } +void ExternalMemoryHandle::EnumMemoryRegionsForAllHandles(CLRDataEnumMemoryFlags flags) +{ + SUPPORTS_DAC; + + for (ExternalMemoryHandle* handle = s_handles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) + { + DacEnumMemoryRegion(dac_cast(handle), sizeof(ExternalMemoryHandle)); + handle->EnumMemoryRegions(flags); + } +} + #endif // DACCESS_COMPILE diff --git a/src/coreclr/vm/externalmemoryhandle.h b/src/coreclr/vm/externalmemoryhandle.h index da6115547e3c7b..dc2c8b2507cf92 100644 --- a/src/coreclr/vm/externalmemoryhandle.h +++ b/src/coreclr/vm/externalmemoryhandle.h @@ -28,6 +28,7 @@ class ExternalMemoryHandle final #ifdef DACCESS_COMPILE void EnumMemoryRegions(CLRDataEnumMemoryFlags flags); + static void EnumMemoryRegionsForAllHandles(CLRDataEnumMemoryFlags flags); #endif // Next pointer for SList linkage. @@ -37,10 +38,8 @@ class ExternalMemoryHandle final static void Init(); static ExternalMemoryHandle* Add(PTR_MethodTable pMT, PTR_VOID pMemory, UINT gcFlags); static void Remove(ExternalMemoryHandle* handle DEBUG_ARG(bool isEESuspended = false)); - static void Cleanup(); #endif static void GCScanRoots(promote_func *fn, ScanContext *sc); - static PTR_ExternalMemoryHandle GetHead(); private: PTR_MethodTable m_pMT; diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index 1d0dc77b2188de..d5d8a0d732bfeb 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -14,6 +14,7 @@ #include "threadsuspend.h" #include "interoplibinterface.h" #include "exinfo.h" +#include "externalmemoryhandle.h" #ifdef FEATURE_COMINTEROP #include "runtimecallablewrapper.h" diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs index e7c6a86e470a2c..17c7f5c8246f0f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs @@ -88,7 +88,8 @@ private void AddInteriorRoot( bool resolveInteriorPointers) { TargetPointer obj = _target.ReadPointer(slotAddress.Value); - if (obj == TargetPointer.Null || obj.Value == ulong.MaxValue) + ulong invalidPointer = _target.PointerSize == 8 ? ulong.MaxValue : uint.MaxValue; + if (obj == TargetPointer.Null || obj.Value == invalidPointer) return; if (resolveInteriorPointers) From 1916159eb7ee8005aadc2ff610a7e5f4bd219960 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 16 Sep 2026 14:44:56 -0700 Subject: [PATCH 16/24] Fix condition and extract helper to gcheaputilities.h --- src/coreclr/vm/gcenv.ee.cpp | 10 +++++----- src/coreclr/vm/gcheaputilities.h | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index d5d8a0d732bfeb..6c4ca2c389d6bc 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -336,11 +336,11 @@ void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, } } - // Only mark external memory handles during non-concurrent scanning. - // During concurrent scanning, other EE threads may add or remove handles. - // To avoid taking a lock when we scan the external memory handles, - // we only scan during a non-concurrent phase when the EE threads are blocked. - if (!sc->concurrent) + // In server GC, we can be scanning GC roots from multiple GC threads concurrently. + // It's unsafe for us to scan unpinned roots from multiple threads concurrently + // as this could lead to invalid relocations during compaction. + // As a result, we will only scan these roots on one context to ensure they are scanned exactly once. + if (GCHeapUtilities::ShouldScanUnpinnedRoots(sc)) { ExternalMemoryHandle::GCScanRoots(fn, sc); } diff --git a/src/coreclr/vm/gcheaputilities.h b/src/coreclr/vm/gcheaputilities.h index f6fde55fc0f502..abc317780a9b76 100644 --- a/src/coreclr/vm/gcheaputilities.h +++ b/src/coreclr/vm/gcheaputilities.h @@ -229,6 +229,21 @@ class GCHeapUtilities { return IsServerHeap() && g_SystemInfo.dwNumberOfProcessors >= 2; } + // Returns true if only the single-pass roots should be scanned for the given scan context. + // When scanning over GC roots that are unpinned on the EE side, + // we need to ensure that we don't ever double-scan the same roots at the same addresses. + // When GC compaction occurs, we would end up moving the addresses of the target objects multiple times + // and possibly end up with them pointing at incorrect objects. + // Therefore, we restrict scanning these unpinned roots to a single pass. + inline static bool ShouldScanUnpinnedRoots(ScanContext* sc) + { + WRAPPER_NO_CONTRACT; + + // Only scan unpinned roots on the 0th thread's scan context. + // Every GC MUST always report exactly one context with thread number 0. + return (sc->thread_number == 0); + } + // Waits until a GC is complete, if the heap has been initialized. inline static void WaitForGCCompletion(bool bConsiderGCStart = false) { From 3738cac048761d20d838fe910c886bf3c7238ad0 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 16 Sep 2026 14:45:37 -0700 Subject: [PATCH 17/24] Fix byref scanning to not assume we're in the context of the last thread in the thread store when doing careful promotion --- src/coreclr/vm/gcenv.ee.cpp | 3 +++ src/coreclr/vm/siginfo.cpp | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/coreclr/vm/gcenv.ee.cpp b/src/coreclr/vm/gcenv.ee.cpp index 6c4ca2c389d6bc..9fc9021371730a 100644 --- a/src/coreclr/vm/gcenv.ee.cpp +++ b/src/coreclr/vm/gcenv.ee.cpp @@ -342,6 +342,9 @@ void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, // As a result, we will only scan these roots on one context to ensure they are scanned exactly once. if (GCHeapUtilities::ShouldScanUnpinnedRoots(sc)) { + // We are going to scan over possible byref values located not on any given thread's stack. + // Ensure that we don't try to check the stack limits of any particular thread while scanning these roots. + sc->thread_under_crawl = nullptr; ExternalMemoryHandle::GCScanRoots(fn, sc); } } diff --git a/src/coreclr/vm/siginfo.cpp b/src/coreclr/vm/siginfo.cpp index 05c245f78cfc8f..5966f2b126a0b7 100644 --- a/src/coreclr/vm/siginfo.cpp +++ b/src/coreclr/vm/siginfo.cpp @@ -5165,7 +5165,7 @@ void PromoteCarefully(promote_func fn, // shrink the current reserved stack space. That causes the real limit of the stack to move up and // the range can be reused for other purposes. But the sc->stack_limit is stable during the scan. // Even on Windows, we care just about the stack above the stack_limit. - if ((sc->thread_under_crawl->IsAddressInStack(*ppObj)) && (PTR_TO_TADDR(*ppObj) >= sc->stack_limit)) + if (sc->thread_under_crawl != NULL && sc->thread_under_crawl->IsAddressInStack(*ppObj) && (PTR_TO_TADDR(*ppObj) >= sc->stack_limit)) { return; } From 043f8ae502f959595a1f7226e60072d723ae8ff8 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 16 Sep 2026 15:10:41 -0700 Subject: [PATCH 18/24] Order external memory handle before output parameters Document the external memory handle input and pass explicit null handles from callers that do not transfer ownership. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/debug/di/divalue.cpp | 14 +++++++++----- src/coreclr/debug/di/rsclass.cpp | 1 + src/coreclr/debug/di/rspriv.h | 8 ++++---- src/coreclr/debug/di/rsthread.cpp | 13 +++++++++++-- src/coreclr/debug/di/valuehome.cpp | 3 ++- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/coreclr/debug/di/divalue.cpp b/src/coreclr/debug/di/divalue.cpp index d77448faea9dcd..43d47758bf4b97 100644 --- a/src/coreclr/debug/di/divalue.cpp +++ b/src/coreclr/debug/di/divalue.cpp @@ -96,6 +96,7 @@ void CordbValue::Neuter() // remoteValue - remote address and size of the value // localValue - local address and size of the value // ppRemoteRegAddr - register address of the value +// vmExternalMemoryHandle - external memory handle whose ownership is transferred to the value // output: ppValue - the newly created instance of an ICDValue // Notes: // - only one of the three locations will be non-NULL @@ -145,8 +146,8 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - ICorDebugValue** ppValue, - VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle) + VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle, + ICorDebugValue** ppValue) { HRESULT hr = S_OK; @@ -198,6 +199,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain // remoteValue - remote address and size of the value // localValue - local address and size of the value // ppRemoteRegAddr - register address of the value +// vmExternalMemoryHandle - external memory handle whose ownership is transferred to the value // output: ppValue - the newly created instance of an ICDValue // Notes: // - Only one of the three locations, remoteValue, localValue or ppRemoteRegAddr, will be non-NULL. @@ -208,8 +210,8 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - ICorDebugValue** ppValue, - VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle) + VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle, + ICorDebugValue** ppValue) { INTERNAL_SYNC_API_ENTRY(pAppdomain->GetProcess()); // @@ -257,7 +259,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain case ELEMENT_TYPE_SZARRAY: case ELEMENT_TYPE_FNPTR: { - CreateVCObjOrRefValue(pAppdomain, pType, boxed, remoteValue, localValue, ppRemoteRegAddr, ppValue, vmExternalMemoryHandle); // throws + CreateVCObjOrRefValue(pAppdomain, pType, boxed, remoteValue, localValue, ppRemoteRegAddr, vmExternalMemoryHandle, ppValue); // throws break; } @@ -1190,6 +1192,7 @@ HRESULT CordbReferenceValue::DereferenceCommon( remoteValue, MemoryRange(NULL, 0), // local value NULL, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -1216,6 +1219,7 @@ HRESULT CordbReferenceValue::DereferenceCommon( remoteValue, MemoryRange(NULL, 0), // local value NULL, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); diff --git a/src/coreclr/debug/di/rsclass.cpp b/src/coreclr/debug/di/rsclass.cpp index 4b1f5dc535c97d..9e311ac9e484d5 100644 --- a/src/coreclr/debug/di/rsclass.cpp +++ b/src/coreclr/debug/di/rsclass.cpp @@ -345,6 +345,7 @@ HRESULT CordbClass::GetStaticFieldValue2(CordbModule * pModule, remoteValue, MemoryRange(NULL, 0), NULL, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), &pValue); // throws } EX_CATCH_HRESULT(hr); diff --git a/src/coreclr/debug/di/rspriv.h b/src/coreclr/debug/di/rspriv.h index 17cd9897908480..2f6154bbee33e2 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -8688,8 +8688,8 @@ class CordbValue : public CordbBase TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - ICorDebugValue** ppValue, - VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr()); + VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle, + ICorDebugValue** ppValue); // Create the proper ICDValue instance based on the given element type. static void CreateValueByType(CordbAppDomain * appdomain, @@ -8698,8 +8698,8 @@ class CordbValue : public CordbBase TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - ICorDebugValue** ppValue, - VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr()); + VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle, + ICorDebugValue** ppValue); // Create the proper ICDValue instance based on the given remote heap object static ICorDebugValue* CreateHeapValue(CordbAppDomain* pAppDomain, diff --git a/src/coreclr/debug/di/rsthread.cpp b/src/coreclr/debug/di/rsthread.cpp index e85c6718740a6e..629d685a880e74 100644 --- a/src/coreclr/debug/di/rsthread.cpp +++ b/src/coreclr/debug/di/rsthread.cpp @@ -6740,6 +6740,7 @@ HRESULT CordbNativeFrame::GetLocalRegisterValue(CorDebugRegister reg, EMPTY_BUFFER, MemoryRange(pLocalValue, REG_SIZE), pRegHolder, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), &pValue); // throws *ppValue = pValue; @@ -6773,6 +6774,7 @@ HRESULT CordbNativeFrame::GetLocalDoubleRegisterValue( EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -6822,6 +6824,7 @@ CordbNativeFrame::GetLocalMemoryValue(CORDB_ADDRESS address, TargetBuffer(address, CordbValue::GetSizeForType(pType, kUnboxed)), MemoryRange(NULL, 0), NULL, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), &pValue); // throws } EX_CATCH_HRESULT(hr); @@ -6877,6 +6880,7 @@ CordbNativeFrame::GetLocalRegisterMemoryValue(CorDebugRegister highWordReg, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -6924,6 +6928,7 @@ CordbNativeFrame::GetLocalMemoryRegisterValue(CORDB_ADDRESS highWordAddress, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -7035,6 +7040,7 @@ HRESULT CordbNativeFrame::GetLocalFloatingPointValue(DWORD index, EMPTY_BUFFER, MemoryRange(&(pThread->m_floatValues[index]), sizeof(double)), pRegHolder, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), &pValue); // throws *ppValue = pValue; @@ -7141,6 +7147,7 @@ HRESULT CordbNativeFrame::GetLocalTwoRegisterValue(DWORD lowReg, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -10494,8 +10501,8 @@ HRESULT CordbEval::GetResult(ICorDebugValue **ppResult) remoteValue, MemoryRange(NULL, 0), NULL, - ppResult, - boxed ? VMPTR_DebuggerExternalMemoryHandle::NullPtr() : m_vmExternalMemoryHandle); // throws + boxed ? VMPTR_DebuggerExternalMemoryHandle::NullPtr() : m_vmExternalMemoryHandle, + ppResult); // throws if (!boxed) { @@ -11248,6 +11255,7 @@ HRESULT CordbAsyncFrame::GetArgument(DWORD dwIndex, ICorDebugValue ** ppValue) TargetBuffer(m_continuationAddress + m_asyncVars[i].offset, CordbValue::GetSizeForType(pType, kUnboxed)), MemoryRange(NULL, 0), NULL, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); foundArg = true; break; @@ -11427,6 +11435,7 @@ HRESULT CordbAsyncFrame::GetLocalVariableEx(ILCodeKind flags, DWORD dwIndex, ICo TargetBuffer(m_continuationAddress + m_asyncVars[i].offset, CordbValue::GetSizeForType(pType, kUnboxed)), MemoryRange(NULL, 0), NULL, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); foundLocal = true; break; diff --git a/src/coreclr/debug/di/valuehome.cpp b/src/coreclr/debug/di/valuehome.cpp index 24230bb6c85cd1..7b74963bf9c140 100644 --- a/src/coreclr/debug/di/valuehome.cpp +++ b/src/coreclr/debug/di/valuehome.cpp @@ -690,6 +690,7 @@ void RemoteValueHome::CreateInternalValue(CordbType * pType, TargetBuffer(m_remoteValue.pAddress + offset, size), MemoryRange(localAddress, localSize), NULL, // remote reg + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); // throws } // RemoteValueHome::CreateInternalValue @@ -814,6 +815,7 @@ void RegisterValueHome::CreateInternalValue(CordbType * pType, EMPTY_BUFFER, // remote address MemoryRange(localAddress, size), pRegHolder, + VMPTR_DebuggerExternalMemoryHandle::NullPtr(), ppValue); // throws } // RegisterValueHome::CreateInternalValue @@ -1145,4 +1147,3 @@ RefValueHome::RefValueHome(CordbProcess * pProcess, } // RefValueHome::RefValueHome - From 1c29ca468f29161d7cc8413c6b5979500b3945f4 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 16 Sep 2026 19:30:58 -0700 Subject: [PATCH 19/24] Share byreflike field scanning with CallingConvention contract and add stress test --- .../Contracts/ByRefPointerOffsetsReporter.cs | 93 +++++++++ .../CallingConvention/CallingConvention_1.cs | 128 +----------- .../Contracts/ExternalMemoryHandles_1.cs | 63 +----- .../cdac/tests/StressTests/CdacStressTests.cs | 1 + .../InlineArrayByRefLike.il | 183 ++++++++++++++++++ .../InlineArrayByRefLike.ilproj | 5 + .../managed/cdac/tests/StressTests/README.md | 6 +- .../cdac/tests/StressTests/RunStressTests.ps1 | 11 +- .../tests/StressTests/StressTests.targets | 3 +- 9 files changed, 307 insertions(+), 186 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il create mode 100644 src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.ilproj diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs new file mode 100644 index 00000000000000..81462f0dc78620 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +internal sealed class ByRefPointerOffsetsReporter +{ + private const int MaxByRefLikeRecursionDepth = 16; + + private readonly IRuntimeTypeSystem _rts; + private readonly ulong _source; + private readonly uint _pointerSize; + + public ByRefPointerOffsetsReporter( + IRuntimeTypeSystem rts, + TargetPointer source, + uint pointerSize) + { + _rts = rts; + _source = source.Value; + _pointerSize = pointerSize; + } + + private IEnumerable Find( + TargetPointer fieldDesc, + ulong baseOffset, + int depth, + bool isInlineArrayElement = false) + { + CorElementType fieldType = _rts.GetFieldDescType(fieldDesc); + uint fieldOffset = isInlineArrayElement + ? 0 + : _rts.GetFieldDescOffset(fieldDesc, fieldDef: null); + + if (fieldType == CorElementType.ValueType) + { + ITypeHandle? fieldTypeHandle = _rts.GetFieldDescApproxTypeHandle(fieldDesc); + if (fieldTypeHandle is not null && _rts.IsByRefLike(fieldTypeHandle)) + { + foreach (ulong slot in Find(fieldTypeHandle, baseOffset + fieldOffset, depth + 1)) + yield return slot; + } + } + else if (fieldType == CorElementType.Byref) + { + yield return _source + baseOffset + fieldOffset; + } + } + + public IEnumerable Find(ITypeHandle typeHandle, ulong baseOffset) + => Find(typeHandle, baseOffset, depth: 0); + + private IEnumerable Find(ITypeHandle typeHandle, ulong baseOffset, int depth) + { + if (depth > MaxByRefLikeRecursionDepth) + yield break; + + bool isInlineArray = _rts.IsInlineArray(typeHandle); + + foreach (TargetPointer fieldDesc in _rts.GetFieldDescList(typeHandle)) + { + if (_rts.IsFieldDescStatic(fieldDesc)) + continue; + + if (isInlineArray) + { + CorElementType fieldType = _rts.GetFieldDescType(fieldDesc); + ITypeHandle? fieldTypeHandle = fieldType == CorElementType.ValueType + ? _rts.GetFieldDescApproxTypeHandle(fieldDesc) + : null; + uint elementSize = fieldType == CorElementType.Byref + ? _pointerSize + : fieldTypeHandle is not null ? _rts.GetNumInstanceFieldBytes(fieldTypeHandle) : 0; + if (elementSize == 0) + continue; + + uint totalSize = _rts.GetNumInstanceFieldBytes(typeHandle); + for (uint offset = 0; offset < totalSize; offset += elementSize) + { + foreach (ulong slot in Find(fieldDesc, baseOffset + offset, depth, isInlineArrayElement: true)) + yield return slot; + } + } + else + { + foreach (ulong slot in Find(fieldDesc, baseOffset, depth)) + yield return slot; + } + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs index f4e144c3cc84a4..a5f931638d9ee7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -357,7 +357,6 @@ private CdacTypeHandle GetIntPtrTypeHandle(IRuntimeTypeSystem rts) // ===================================================================== private const int MaxGCRefMapBlobLength = 252; - private const int MaxByRefLikeRecursionDepth = 16; private byte[]? ComputeArgGCRefMapBlobCore(MethodDescHandle methodDesc) { @@ -456,27 +455,15 @@ private CdacTypeHandle GetIntPtrTypeHandle(IRuntimeTypeSystem rts) if (arg.IsByRefLikeStruct) { - // ByRefLike value type (Span, ReadOnlySpan, - // ByteRef, any ref struct). Mirrors the runtime's - // ByRefPointerOffsetsReporter (siginfo.cpp): walk - // the type's instance fields and emit INTERIOR - // for each ELEMENT_TYPE_BYREF field at its - // in-struct offset. ELEMENT_TYPE_PTR / IntPtr / - // void* fields are explicitly NOT reported - // (so QCallTypeHandle, ObjectHandleOnStack, - // StringHandleOnStack contribute nothing). - // - // For uncached generic instantiations (Span - // whose closed MT isn't loaded), the field - // layout lives on the open generic (Span). - // The byref/ptr distinction is preserved at the - // FieldDesc level regardless of which T closes - // the type. - EmitByRefLikeInterior( - rts, - arg.TypeInfo, - arg.Offset, - tokens); + ITypeHandle? layoutType = + arg.TypeInfo.ExactTypeHandle ?? arg.TypeInfo.GenericTypeDefinition; + if (layoutType is not null) + { + ByRefPointerOffsetsReporter reporter = + new(rts, TargetPointer.Null, (uint)pointerSize); + foreach (ulong offset in reporter.Find(layoutType, (ulong)arg.Offset)) + tokens[checked((int)offset)] = GCRefMapToken.Interior; + } emitted = true; } @@ -610,103 +597,6 @@ private static GenericContextLoc SafeGetGenericContextLoc(IRuntimeTypeSystem rts } } - // Mirror of runtime ByRefPointerOffsetsReporter (siginfo.cpp): walk the - // instance fields of a ByRefLike value type and emit one INTERIOR token - // per ELEMENT_TYPE_BYREF field at its offset within the unboxed struct - // (so absolute offset is baseOffset + fieldOffset). Recurses into nested - // ByRefLike value-type fields. ELEMENT_TYPE_PTR / IntPtr / void* fields - // are deliberately skipped to match runtime behavior for QCall-style - // handle wrappers. - private void EmitByRefLikeInterior( - IRuntimeTypeSystem rts, - SignatureTypeInfo byRefLikeType, - int baseOffset, - SortedDictionary tokens) - { - // Bound recursion just in case the data is corrupt / cycles in a dump. - EmitByRefLikeInteriorRecursive( - rts, - byRefLikeType, - baseOffset, - tokens, - depth: 0); - } - - private void EmitByRefLikeInteriorRecursive( - IRuntimeTypeSystem rts, - SignatureTypeInfo byRefLikeType, - int baseOffset, - SortedDictionary tokens, - int depth) - { - if (depth > MaxByRefLikeRecursionDepth) - return; - - ITypeHandle? layoutType = byRefLikeType.ExactTypeHandle ?? byRefLikeType.GenericTypeDefinition; - if (layoutType is null) - return; - - IEnumerable fieldDescs; - try - { - fieldDescs = rts.GetFieldDescList(layoutType); - } - catch - { - return; - } - - foreach (TargetPointer fdPtr in fieldDescs) - { - bool isStatic; - CorElementType fieldType; - uint fieldOffset; - try - { - isStatic = rts.IsFieldDescStatic(fdPtr); - if (isStatic) - continue; - fieldType = rts.GetFieldDescType(fdPtr); - fieldOffset = rts.GetFieldDescOffset(fdPtr, fieldDef: null); - } - catch - { - continue; - } - - int absOffset = baseOffset + (int)fieldOffset; - - if (fieldType == CorElementType.Byref) - { - tokens[absOffset] = GCRefMapToken.Interior; - } - else if (fieldType == CorElementType.ValueType) - { - SignatureTypeInfo nestedType; - try - { - nestedType = GetFieldTypeInfo(fdPtr, byRefLikeType); - } - catch - { - continue; - } - - ITypeHandle? nestedProbe = - nestedType.ExactTypeHandle ?? nestedType.GenericTypeDefinition; - if (nestedProbe is null || !rts.IsByRefLike(nestedProbe)) - continue; - - EmitByRefLikeInteriorRecursive( - rts, - nestedType, - absOffset, - tokens, - depth + 1); - } - } - } - private static byte[] EmptyGCRefMapBlob() { GCRefMapEncoder enc = default; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs index 17c7f5c8246f0f..f772c4509acd8d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs @@ -66,7 +66,8 @@ private void AddValueTypeRoots( if (_rts.IsByRefLike(typeHandle)) { - foreach (ulong slotAddress in EnumerateByRefLikeInteriorSlots(_rts, typeHandle, memory.Value, (uint)_target.PointerSize)) + ByRefPointerOffsetsReporter reporter = new(_rts, memory, (uint)_target.PointerSize); + foreach (ulong slotAddress in reporter.Find(typeHandle, 0)) AddInteriorRoot(roots, new TargetPointer(slotAddress), resolveInteriorPointers); } @@ -106,64 +107,4 @@ private void AddInteriorRoot( Object = obj, }); } - - private static IEnumerable EnumerateByRefLikeInteriorSlots( - IRuntimeTypeSystem rts, - ITypeHandle typeHandle, - ulong baseAddress, - uint pointerSize) - { - bool isInlineArray = rts.IsInlineArray(typeHandle); - - foreach (TargetPointer fieldDesc in rts.GetFieldDescList(typeHandle)) - { - if (rts.IsFieldDescStatic(fieldDesc)) - continue; - - CorElementType fieldType = rts.GetFieldDescType(fieldDesc); - ITypeHandle? fieldTypeHandle = fieldType == CorElementType.ValueType - ? rts.GetFieldDescApproxTypeHandle(fieldDesc) - : null; - - if (isInlineArray) - { - uint elementSize = fieldType == CorElementType.Byref - ? pointerSize - : fieldTypeHandle is not null ? rts.GetNumInstanceFieldBytes(fieldTypeHandle) : 0; - if (elementSize == 0) - continue; - - uint totalSize = rts.GetNumInstanceFieldBytes(typeHandle); - for (uint elementOffset = 0; elementOffset < totalSize; elementOffset += elementSize) - { - foreach (ulong slot in EnumerateByRefLikeFieldSlots(rts, fieldType, fieldTypeHandle, baseAddress + elementOffset, pointerSize)) - yield return slot; - } - } - else - { - uint fieldOffset = rts.GetFieldDescOffset(fieldDesc, fieldDef: null); - foreach (ulong slot in EnumerateByRefLikeFieldSlots(rts, fieldType, fieldTypeHandle, baseAddress + fieldOffset, pointerSize)) - yield return slot; - } - } - } - - private static IEnumerable EnumerateByRefLikeFieldSlots( - IRuntimeTypeSystem rts, - CorElementType fieldType, - ITypeHandle? fieldTypeHandle, - ulong fieldAddress, - uint pointerSize) - { - if (fieldType == CorElementType.Byref) - { - yield return fieldAddress; - } - else if (fieldType == CorElementType.ValueType && fieldTypeHandle is not null && rts.IsByRefLike(fieldTypeHandle)) - { - foreach (ulong slot in EnumerateByRefLikeInteriorSlots(rts, fieldTypeHandle, fieldAddress, pointerSize)) - yield return slot; - } - } } diff --git a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs index 6439915c156802..bf1b0f925aa184 100644 --- a/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs +++ b/src/native/managed/cdac/tests/StressTests/CdacStressTests.cs @@ -34,6 +34,7 @@ [new Debuggee("Comprehensive")], [new Debuggee("ExceptionHandling")], [new Debuggee("StructScenarios")], [new Debuggee("DynamicMethods")], + [new Debuggee("InlineArrayByRefLike")], [new Debuggee("CallSignatures")], [new Debuggee("CrossModule")], [new Debuggee("NotYetLoadedArgType")], diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il b/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il new file mode 100644 index 00000000000000..4b571281183ca2 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il @@ -0,0 +1,183 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +.assembly extern System.Runtime +{ + .publickeytoken = (B0 3F 5F 7F 11 D5 0A 3A) +} + +.assembly InlineArrayByRefLike { } + +.class private sequential ansi sealed beforefieldinit InlineArrayOfSpans + extends [System.Runtime]System.ValueType +{ + .custom instance void [System.Runtime]System.Runtime.CompilerServices.InlineArrayAttribute::.ctor(int32) = { + int32(2) + } + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsByRefLikeAttribute::.ctor() = ( + 01 00 00 00 + ) + + .field assembly valuetype [System.Runtime]System.Span`1 _element0 + + .method public hidebysig specialname rtspecialname instance void .ctor( + valuetype [System.Runtime]System.Span`1 first, + valuetype [System.Runtime]System.Span`1 second) cil managed + { + .maxstack 2 + + ldarg.0 + ldarg.1 + stfld valuetype [System.Runtime]System.Span`1 InlineArrayOfSpans::_element0 + + ldarg.0 + ldflda valuetype [System.Runtime]System.Span`1 InlineArrayOfSpans::_element0 + sizeof valuetype [System.Runtime]System.Span`1 + conv.i + add + ldarg.2 + stobj valuetype [System.Runtime]System.Span`1 + ret + } +} + +.class private auto ansi abstract sealed beforefieldinit Program + extends [System.Runtime]System.Object +{ + .field private static object s_sink + + .method private hidebysig static valuetype [System.Runtime]System.Span`1 CreateSpan( + int32 x, + int32 length) cil managed noinlining + { + .maxstack 3 + .locals init ( + uint8[] values + ) + + ldarg.1 + newarr [System.Runtime]System.Byte + stloc.0 + + ldloc.0 + ldarg.0 + conv.u1 + call void [System.Runtime]System.Array::Fill(!!0[], !!0) + + ldloc.0 + call valuetype [System.Runtime]System.Span`1 valuetype [System.Runtime]System.Span`1::op_Implicit(!0[]) + ret + } + + .method private hidebysig static int32 SumValues( + valuetype InlineArrayOfSpans spans) cil managed noinlining + { + .maxstack 3 + .locals init ( + valuetype [System.Runtime]System.Span`1 first, + valuetype [System.Runtime]System.Span`1 second, + int32 sum, + int32 index + ) + + newobj instance void [System.Runtime]System.Object::.ctor() + stsfld object Program::s_sink + + ldarga.s spans + ldfld valuetype [System.Runtime]System.Span`1 InlineArrayOfSpans::_element0 + stloc.0 + + ldarga.s spans + ldflda valuetype [System.Runtime]System.Span`1 InlineArrayOfSpans::_element0 + sizeof valuetype [System.Runtime]System.Span`1 + conv.i + add + ldobj valuetype [System.Runtime]System.Span`1 + stloc.1 + + ldc.i4.0 + stloc.2 + ldc.i4.0 + stloc.3 + br.s FIRST_CHECK + + FIRST_ADD: + ldloc.2 + ldloca.s first + ldloc.3 + call instance !0& valuetype [System.Runtime]System.Span`1::get_Item(int32) + ldind.u1 + add + stloc.2 + + ldloc.3 + ldc.i4.1 + add + stloc.3 + + FIRST_CHECK: + ldloc.3 + ldloca.s first + call instance int32 valuetype [System.Runtime]System.Span`1::get_Length() + blt.s FIRST_ADD + + ldc.i4.0 + stloc.3 + br.s SECOND_CHECK + + SECOND_ADD: + ldloc.2 + ldloca.s second + ldloc.3 + call instance !0& valuetype [System.Runtime]System.Span`1::get_Item(int32) + ldind.u1 + add + stloc.2 + + ldloc.3 + ldc.i4.1 + add + stloc.3 + + SECOND_CHECK: + ldloc.3 + ldloca.s second + call instance int32 valuetype [System.Runtime]System.Span`1::get_Length() + blt.s SECOND_ADD + + ldloc.2 + ret + } + + .method public hidebysig static int32 Main() cil managed + { + .entrypoint + .maxstack 2 + .locals init ( + valuetype [System.Runtime]System.Span`1 first, + valuetype [System.Runtime]System.Span`1 second, + valuetype InlineArrayOfSpans spans + ) + + ldc.i4.s 10 + ldc.i4.4 + call valuetype [System.Runtime]System.Span`1 Program::CreateSpan(int32, int32) + stloc.0 + + ldc.i4.s 20 + ldc.i4.3 + call valuetype [System.Runtime]System.Span`1 Program::CreateSpan(int32, int32) + stloc.1 + + ldloc.0 + ldloc.1 + newobj instance void InlineArrayOfSpans::.ctor( + valuetype [System.Runtime]System.Span`1, + valuetype [System.Runtime]System.Span`1) + stloc.2 + + ldloc.2 + call int32 Program::SumValues(valuetype InlineArrayOfSpans) + ret + } +} diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.ilproj b/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.ilproj new file mode 100644 index 00000000000000..84fd8fe3a6b1ef --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.ilproj @@ -0,0 +1,5 @@ + + + True + + diff --git a/src/native/managed/cdac/tests/StressTests/README.md b/src/native/managed/cdac/tests/StressTests/README.md index ae0b26907c2a57..03c7b075765b2c 100644 --- a/src/native/managed/cdac/tests/StressTests/README.md +++ b/src/native/managed/cdac/tests/StressTests/README.md @@ -135,8 +135,9 @@ $env:CORE_ROOT = "path\to\Core_Root" ## Adding a New Debuggee -1. Create a folder under `Debuggees/` with a `.csproj` and `Program.cs` -2. The `.csproj` just needs: `` +1. Create a folder under `Debuggees/` with a `.csproj` or `.ilproj` and its source +2. C# projects only need ``; IL projects use + `` (inherits OutputType=Exe and TFM from `Directory.Build.props`) 3. `Main()` must return `100` on success 4. Use `[MethodImpl(MethodImplOptions.NoInlining)]` on methods to prevent inlining @@ -156,6 +157,7 @@ $env:CORE_ROOT = "path\to\Core_Root" | **Comprehensive** | All-in-one: every scenario in a single run | | **StructScenarios** | Struct returns, by-ref params | | **DynamicMethods** | DynamicMethod / IL emit | +| **InlineArrayByRefLike** | IL-defined byref-like inline array containing two `Span` values, passed by value | | **CallSignatures** | Wide signature surface for the ARGITER sub-check (primitives, byref/ptr, structs, generics) | | **CrossModule** | Calls across multiple assemblies exercising cross-module type references | | **NotYetLoadedArgType** | Regression coverage for the cDAC dropping a GC-root whose argument type is not yet loaded when a `PrestubMethodFrame` is scanned. | diff --git a/src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 b/src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 index dbc80e7c1a46b5..55d130a598bb44 100644 --- a/src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 +++ b/src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 @@ -111,7 +111,10 @@ $cdacDll = if ($isWin) { "mscordaccore_universal.dll" } elseif ($IsMacOS) { "lib $debuggeesDir = Join-Path $scriptDir "Debuggees" # Discover available debuggees -$allDebuggees = Get-ChildItem $debuggeesDir -Directory | Where-Object { Test-Path (Join-Path $_.FullName "*.csproj") } | ForEach-Object { $_.Name } +$allDebuggees = Get-ChildItem $debuggeesDir -Directory | Where-Object { + (Test-Path (Join-Path $_.FullName "*.csproj")) -or + (Test-Path (Join-Path $_.FullName "*.ilproj")) +} | ForEach-Object { $_.Name } # Resolve which debuggees to run if ($Debuggee.Count -eq 0) { @@ -195,8 +198,10 @@ if (!(Test-Path (Join-Path $coreRoot $cdacDll))) { # --------------------------------------------------------------------------- Write-Host ">>> Step 2: Building debuggees..." -ForegroundColor Yellow foreach ($d in $selectedDebuggees) { - $csproj = Get-ChildItem (Join-Path $debuggeesDir $d) -Filter "*.csproj" | Select-Object -First 1 - & $dotnetExe build $csproj.FullName -c Release --nologo -v q + $project = Get-ChildItem (Join-Path $debuggeesDir $d) | + Where-Object { $_.Extension -in ".csproj", ".ilproj" } | + Select-Object -First 1 + & $dotnetExe build $project.FullName -c Release --nologo -v q if ($LASTEXITCODE -ne 0) { Write-Error "Failed to build debuggee '$d'"; exit 1 } Write-Host " Built $d" -ForegroundColor DarkGray } diff --git a/src/native/managed/cdac/tests/StressTests/StressTests.targets b/src/native/managed/cdac/tests/StressTests/StressTests.targets index 3bd07f3e9fc521..9f94dd155f0d52 100644 --- a/src/native/managed/cdac/tests/StressTests/StressTests.targets +++ b/src/native/managed/cdac/tests/StressTests/StressTests.targets @@ -9,9 +9,10 @@ Release - + + From d73a90042eecf8f5f005017aad667dab87023973 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 17 Sep 2026 11:51:09 -0700 Subject: [PATCH 20/24] Strengthen cDAC inline array stress coverage Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3900c560-18f8-4900-9840-ae7022248647 --- .../InlineArrayByRefLike.il | 195 ++++++++---------- 1 file changed, 88 insertions(+), 107 deletions(-) diff --git a/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il b/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il index 4b571281183ca2..2bf671d157b514 100644 --- a/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il @@ -8,7 +8,38 @@ .assembly InlineArrayByRefLike { } -.class private sequential ansi sealed beforefieldinit InlineArrayOfSpans +.class private sequential ansi sealed beforefieldinit ByRef + extends [System.Runtime]System.ValueType +{ + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsByRefLikeAttribute::.ctor() = ( + 01 00 00 00 + ) + + .field assembly int32& _value + + .method public hidebysig specialname rtspecialname instance void .ctor( + int32&) cil managed + { + .maxstack 2 + + ldarg.0 + ldarg.1 + stfld int32& ByRef::_value + ret + } + + .method public hidebysig instance int32 GetValue() cil managed + { + .maxstack 1 + + ldarg.0 + ldfld int32& ByRef::_value + ldind.i4 + ret + } +} + +.class private sequential ansi sealed beforefieldinit InlineArrayOfByRefs extends [System.Runtime]System.ValueType { .custom instance void [System.Runtime]System.Runtime.CompilerServices.InlineArrayAttribute::.ctor(int32) = { @@ -18,25 +49,25 @@ 01 00 00 00 ) - .field assembly valuetype [System.Runtime]System.Span`1 _element0 + .field assembly valuetype ByRef _element0 .method public hidebysig specialname rtspecialname instance void .ctor( - valuetype [System.Runtime]System.Span`1 first, - valuetype [System.Runtime]System.Span`1 second) cil managed + valuetype ByRef first, + valuetype ByRef second) cil managed { .maxstack 2 ldarg.0 ldarg.1 - stfld valuetype [System.Runtime]System.Span`1 InlineArrayOfSpans::_element0 + stfld valuetype ByRef InlineArrayOfByRefs::_element0 ldarg.0 - ldflda valuetype [System.Runtime]System.Span`1 InlineArrayOfSpans::_element0 - sizeof valuetype [System.Runtime]System.Span`1 + ldflda valuetype ByRef InlineArrayOfByRefs::_element0 + sizeof valuetype ByRef conv.i add ldarg.2 - stobj valuetype [System.Runtime]System.Span`1 + stobj valuetype ByRef ret } } @@ -46,138 +77,88 @@ { .field private static object s_sink - .method private hidebysig static valuetype [System.Runtime]System.Span`1 CreateSpan( - int32 x, - int32 length) cil managed noinlining - { - .maxstack 3 - .locals init ( - uint8[] values - ) - - ldarg.1 - newarr [System.Runtime]System.Byte - stloc.0 - - ldloc.0 - ldarg.0 - conv.u1 - call void [System.Runtime]System.Array::Fill(!!0[], !!0) - - ldloc.0 - call valuetype [System.Runtime]System.Span`1 valuetype [System.Runtime]System.Span`1::op_Implicit(!0[]) - ret - } - .method private hidebysig static int32 SumValues( - valuetype InlineArrayOfSpans spans) cil managed noinlining + valuetype InlineArrayOfByRefs values) cil managed noinlining { - .maxstack 3 + .maxstack 2 .locals init ( - valuetype [System.Runtime]System.Span`1 first, - valuetype [System.Runtime]System.Span`1 second, - int32 sum, - int32 index + valuetype ByRef first, + valuetype ByRef second ) - newobj instance void [System.Runtime]System.Object::.ctor() + ldc.i4 65536 + newarr [System.Runtime]System.Byte stsfld object Program::s_sink - ldarga.s spans - ldfld valuetype [System.Runtime]System.Span`1 InlineArrayOfSpans::_element0 + ldarga.s values + ldfld valuetype ByRef InlineArrayOfByRefs::_element0 stloc.0 - ldarga.s spans - ldflda valuetype [System.Runtime]System.Span`1 InlineArrayOfSpans::_element0 - sizeof valuetype [System.Runtime]System.Span`1 + ldarga.s values + ldflda valuetype ByRef InlineArrayOfByRefs::_element0 + sizeof valuetype ByRef conv.i add - ldobj valuetype [System.Runtime]System.Span`1 + ldobj valuetype ByRef stloc.1 - ldc.i4.0 - stloc.2 - ldc.i4.0 - stloc.3 - br.s FIRST_CHECK - - FIRST_ADD: - ldloc.2 ldloca.s first - ldloc.3 - call instance !0& valuetype [System.Runtime]System.Span`1::get_Item(int32) - ldind.u1 - add - stloc.2 - - ldloc.3 - ldc.i4.1 - add - stloc.3 - - FIRST_CHECK: - ldloc.3 - ldloca.s first - call instance int32 valuetype [System.Runtime]System.Span`1::get_Length() - blt.s FIRST_ADD - - ldc.i4.0 - stloc.3 - br.s SECOND_CHECK - - SECOND_ADD: - ldloc.2 + call instance int32 ByRef::GetValue() ldloca.s second - ldloc.3 - call instance !0& valuetype [System.Runtime]System.Span`1::get_Item(int32) - ldind.u1 - add - stloc.2 - - ldloc.3 - ldc.i4.1 + call instance int32 ByRef::GetValue() add - stloc.3 - - SECOND_CHECK: - ldloc.3 - ldloca.s second - call instance int32 valuetype [System.Runtime]System.Span`1::get_Length() - blt.s SECOND_ADD - - ldloc.2 ret } .method public hidebysig static int32 Main() cil managed { .entrypoint - .maxstack 2 + .maxstack 3 .locals init ( - valuetype [System.Runtime]System.Span`1 first, - valuetype [System.Runtime]System.Span`1 second, - valuetype InlineArrayOfSpans spans + int32[] firstValues, + int32[] secondValues, + valuetype ByRef first, + valuetype ByRef second, + valuetype InlineArrayOfByRefs values ) - ldc.i4.s 10 - ldc.i4.4 - call valuetype [System.Runtime]System.Span`1 Program::CreateSpan(int32, int32) + ldc.i4.1 + newarr [System.Runtime]System.Int32 stloc.0 + ldloc.0 + ldc.i4.0 + ldc.i4.s 40 + stelem.i4 - ldc.i4.s 20 - ldc.i4.3 - call valuetype [System.Runtime]System.Span`1 Program::CreateSpan(int32, int32) + ldc.i4.1 + newarr [System.Runtime]System.Int32 stloc.1 + ldloc.1 + ldc.i4.0 + ldc.i4.s 60 + stelem.i4 ldloc.0 - ldloc.1 - newobj instance void InlineArrayOfSpans::.ctor( - valuetype [System.Runtime]System.Span`1, - valuetype [System.Runtime]System.Span`1) + ldc.i4.0 + ldelema [System.Runtime]System.Int32 + newobj instance void ByRef::.ctor(int32&) stloc.2 + ldloc.1 + ldc.i4.0 + ldelema [System.Runtime]System.Int32 + newobj instance void ByRef::.ctor(int32&) + stloc.3 + ldloc.2 - call int32 Program::SumValues(valuetype InlineArrayOfSpans) + ldloc.3 + newobj instance void InlineArrayOfByRefs::.ctor( + valuetype ByRef, + valuetype ByRef) + stloc.s 4 + + ldloc.s 4 + call int32 Program::SumValues(valuetype InlineArrayOfByRefs) ret } } From 2525097155ba16abd402adb3c50ddd0d19946fcc Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 17 Sep 2026 12:11:00 -0700 Subject: [PATCH 21/24] Update InlineArrayByRefLike description in README Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/native/managed/cdac/tests/StressTests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/managed/cdac/tests/StressTests/README.md b/src/native/managed/cdac/tests/StressTests/README.md index 03c7b075765b2c..de4a531592cb87 100644 --- a/src/native/managed/cdac/tests/StressTests/README.md +++ b/src/native/managed/cdac/tests/StressTests/README.md @@ -157,7 +157,7 @@ $env:CORE_ROOT = "path\to\Core_Root" | **Comprehensive** | All-in-one: every scenario in a single run | | **StructScenarios** | Struct returns, by-ref params | | **DynamicMethods** | DynamicMethod / IL emit | -| **InlineArrayByRefLike** | IL-defined byref-like inline array containing two `Span` values, passed by value | +| **InlineArrayByRefLike** | IL-defined byref-like inline array containing two `int32&` values, passed by value | | **CallSignatures** | Wide signature surface for the ARGITER sub-check (primitives, byref/ptr, structs, generics) | | **CrossModule** | Calls across multiple assemblies exercising cross-module type references | | **NotYetLoadedArgType** | Regression coverage for the cDAC dropping a GC-root whose argument type is not yet loaded when a `PrestubMethodFrame` is scanned. | From 4f0af2a5b8f3394c0b68b19819358834316895ee Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 18 Sep 2026 12:55:27 -0700 Subject: [PATCH 22/24] Clarify debugger external memory ownership Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3900c560-18f8-4900-9840-ae7022248647 --- src/coreclr/debug/di/divalue.cpp | 34 ++++++++++++------------ src/coreclr/debug/di/process.cpp | 2 +- src/coreclr/debug/di/rsclass.cpp | 2 +- src/coreclr/debug/di/rspriv.h | 10 +++---- src/coreclr/debug/di/rsthread.cpp | 32 +++++++++++----------- src/coreclr/debug/di/valuehome.cpp | 4 +-- src/coreclr/debug/ee/debugger.cpp | 26 +++++++++--------- src/coreclr/debug/ee/debugger.h | 16 ++++++----- src/coreclr/debug/ee/funceval.cpp | 6 ++--- src/coreclr/debug/inc/dbgipcevents.h | 8 +++--- src/coreclr/debug/inc/dbgipceventtypes.h | 2 +- 11 files changed, 72 insertions(+), 70 deletions(-) diff --git a/src/coreclr/debug/di/divalue.cpp b/src/coreclr/debug/di/divalue.cpp index 43d47758bf4b97..46a549880899f3 100644 --- a/src/coreclr/debug/di/divalue.cpp +++ b/src/coreclr/debug/di/divalue.cpp @@ -96,7 +96,7 @@ void CordbValue::Neuter() // remoteValue - remote address and size of the value // localValue - local address and size of the value // ppRemoteRegAddr - register address of the value -// vmExternalMemoryHandle - external memory handle whose ownership is transferred to the value +// vmExternalMemoryOwner - owner of external memory whose ownership is transferred to the value // output: ppValue - the newly created instance of an ICDValue // Notes: // - only one of the three locations will be non-NULL @@ -146,7 +146,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle, + VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner, ICorDebugValue** ppValue) { @@ -168,7 +168,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain ppRemoteRegAddr)); IfFailThrow(pVCValue->Init(localValue)); - pVCValue->SetExternalMemoryHandle(vmExternalMemoryHandle); + pVCValue->SetExternalMemoryOwner(vmExternalMemoryOwner); pVCValue->AddRef(); *ppValue = (ICorDebugValue*)(ICorDebugObjectValue*)pVCValue; @@ -199,7 +199,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain // remoteValue - remote address and size of the value // localValue - local address and size of the value // ppRemoteRegAddr - register address of the value -// vmExternalMemoryHandle - external memory handle whose ownership is transferred to the value +// vmExternalMemoryOwner - owner of external memory whose ownership is transferred to the value // output: ppValue - the newly created instance of an ICDValue // Notes: // - Only one of the three locations, remoteValue, localValue or ppRemoteRegAddr, will be non-NULL. @@ -210,7 +210,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle, + VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner, ICorDebugValue** ppValue) { INTERNAL_SYNC_API_ENTRY(pAppdomain->GetProcess()); // @@ -259,7 +259,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain case ELEMENT_TYPE_SZARRAY: case ELEMENT_TYPE_FNPTR: { - CreateVCObjOrRefValue(pAppdomain, pType, boxed, remoteValue, localValue, ppRemoteRegAddr, vmExternalMemoryHandle, ppValue); // throws + CreateVCObjOrRefValue(pAppdomain, pType, boxed, remoteValue, localValue, ppRemoteRegAddr, vmExternalMemoryOwner, ppValue); // throws break; } @@ -1192,7 +1192,7 @@ HRESULT CordbReferenceValue::DereferenceCommon( remoteValue, MemoryRange(NULL, 0), // local value NULL, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -1219,7 +1219,7 @@ HRESULT CordbReferenceValue::DereferenceCommon( remoteValue, MemoryRange(NULL, 0), // local value NULL, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -2931,7 +2931,7 @@ CordbVCObjectValue::CordbVCObjectValue(CordbAppDomain * pAppdomain pAppdomain->GetSweepableExitNeuterList()), m_pObjectCopy(NULL), m_pValueHome(NULL), - m_vmExternalMemoryHandle(VMPTR_DebuggerExternalMemoryHandle::NullPtr()) + m_vmExternalMemoryOwner(VMPTR_DebuggerExternalMemoryOwner::NullPtr()) { // instantiate the value home NewHolder pHome(NULL); @@ -2970,25 +2970,25 @@ CordbVCObjectValue::~CordbVCObjectValue() } } // CordbVCObjectValue::~CordbVCObjectValue -void CordbVCObjectValue::SetExternalMemoryHandle(VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle) +void CordbVCObjectValue::SetExternalMemoryOwner(VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner) { - _ASSERTE(m_vmExternalMemoryHandle.IsNull()); - m_vmExternalMemoryHandle = vmExternalMemoryHandle; + _ASSERTE(m_vmExternalMemoryOwner.IsNull()); + m_vmExternalMemoryOwner = vmExternalMemoryOwner; } void CordbVCObjectValue::NeuterLeftSideResources() { - if (!m_vmExternalMemoryHandle.IsNull() && GetProcess()->IsSafeToSendEvents()) + if (!m_vmExternalMemoryOwner.IsNull() && GetProcess()->IsSafeToSendEvents()) { DebuggerIPCEvent event; GetProcess()->InitIPCEvent( &event, - DB_IPCE_DISPOSE_EXTERNAL_MEMORY_HANDLE, + DB_IPCE_DISPOSE_EXTERNAL_MEMORY_OWNER, false, m_appdomain->GetADToken()); - event.DisposeExternalMemoryHandle.vmExternalMemoryHandle = m_vmExternalMemoryHandle; + event.DisposeExternalMemoryOwner.vmExternalMemoryOwner = m_vmExternalMemoryOwner; GetProcess()->SendIPCEvent(&event, sizeof(DebuggerIPCEvent)); - m_vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr(); + m_vmExternalMemoryOwner = VMPTR_DebuggerExternalMemoryOwner::NullPtr(); } RSLockHolder lockHolder(GetProcess()->GetProcessLock()); @@ -2998,7 +2998,7 @@ void CordbVCObjectValue::NeuterLeftSideResources() void CordbVCObjectValue::Neuter() { BOOL fTargetIsDead = !GetProcess()->IsSafeToSendEvents() || GetProcess()->m_exiting; - _ASSERTE(fTargetIsDead || m_vmExternalMemoryHandle.IsNull()); + _ASSERTE(fTargetIsDead || m_vmExternalMemoryOwner.IsNull()); CordbValue::Neuter(); } diff --git a/src/coreclr/debug/di/process.cpp b/src/coreclr/debug/di/process.cpp index 643a3d3cd84fc9..8dd661297398ac 100644 --- a/src/coreclr/debug/di/process.cpp +++ b/src/coreclr/debug/di/process.cpp @@ -5112,7 +5112,7 @@ void CordbProcess::RawDispatchEvent( pEval->m_aborted = !!pEvent->FuncEvalComplete.aborted; pEval->m_resultAddr = pEvent->FuncEvalComplete.resultAddr; pEval->m_vmObjectHandle = pEvent->FuncEvalComplete.vmObjectHandle; - pEval->m_vmExternalMemoryHandle = pEvent->FuncEvalComplete.vmExternalMemoryHandle; + pEval->m_vmExternalMemoryOwner = pEvent->FuncEvalComplete.vmExternalMemoryOwner; pEval->m_resultType = pEvent->FuncEvalComplete.resultType; pEval->m_resultAppDomainToken = pEvent->FuncEvalComplete.vmAppDomain; diff --git a/src/coreclr/debug/di/rsclass.cpp b/src/coreclr/debug/di/rsclass.cpp index 9e311ac9e484d5..8e30f5f759982c 100644 --- a/src/coreclr/debug/di/rsclass.cpp +++ b/src/coreclr/debug/di/rsclass.cpp @@ -345,7 +345,7 @@ HRESULT CordbClass::GetStaticFieldValue2(CordbModule * pModule, remoteValue, MemoryRange(NULL, 0), NULL, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), &pValue); // throws } EX_CATCH_HRESULT(hr); diff --git a/src/coreclr/debug/di/rspriv.h b/src/coreclr/debug/di/rspriv.h index 2f6154bbee33e2..64ae475be8ec9f 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -8688,7 +8688,7 @@ class CordbValue : public CordbBase TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle, + VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner, ICorDebugValue** ppValue); // Create the proper ICDValue instance based on the given element type. @@ -8698,7 +8698,7 @@ class CordbValue : public CordbBase TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, - VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle, + VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner, ICorDebugValue** ppValue); // Create the proper ICDValue instance based on the given remote heap object @@ -9418,7 +9418,7 @@ class CordbVCObjectValue : public CordbValue, // Initializes the Right-Side's representation of a Value Class object. HRESULT Init(MemoryRange localValue); - void SetExternalMemoryHandle(VMPTR_DebuggerExternalMemoryHandle vmExternalMemoryHandle); + void SetExternalMemoryOwner(VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner); //HRESULT ResolveValueClass(); CordbClass *GetClass(); @@ -9438,7 +9438,7 @@ class CordbVCObjectValue : public CordbValue, // location information ValueHome * m_pValueHome; - VMPTR_DebuggerExternalMemoryHandle m_vmExternalMemoryHandle; + VMPTR_DebuggerExternalMemoryOwner m_vmExternalMemoryOwner; }; @@ -10016,7 +10016,7 @@ class CordbEval : public CordbBase, public ICorDebugEval, public ICorDebugEval2 RSExtSmartPtr m_pValueClassResult; DebuggerIPCE_ExpandedTypeData m_resultType; - VMPTR_DebuggerExternalMemoryHandle m_vmExternalMemoryHandle; + VMPTR_DebuggerExternalMemoryOwner m_vmExternalMemoryOwner; VMPTR_AppDomain m_resultAppDomainToken; // Left-side memory that needs to be freed. diff --git a/src/coreclr/debug/di/rsthread.cpp b/src/coreclr/debug/di/rsthread.cpp index 629d685a880e74..99ae8fe1ae0850 100644 --- a/src/coreclr/debug/di/rsthread.cpp +++ b/src/coreclr/debug/di/rsthread.cpp @@ -6740,7 +6740,7 @@ HRESULT CordbNativeFrame::GetLocalRegisterValue(CorDebugRegister reg, EMPTY_BUFFER, MemoryRange(pLocalValue, REG_SIZE), pRegHolder, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), &pValue); // throws *ppValue = pValue; @@ -6774,7 +6774,7 @@ HRESULT CordbNativeFrame::GetLocalDoubleRegisterValue( EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -6824,7 +6824,7 @@ CordbNativeFrame::GetLocalMemoryValue(CORDB_ADDRESS address, TargetBuffer(address, CordbValue::GetSizeForType(pType, kUnboxed)), MemoryRange(NULL, 0), NULL, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), &pValue); // throws } EX_CATCH_HRESULT(hr); @@ -6880,7 +6880,7 @@ CordbNativeFrame::GetLocalRegisterMemoryValue(CorDebugRegister highWordReg, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -6928,7 +6928,7 @@ CordbNativeFrame::GetLocalMemoryRegisterValue(CORDB_ADDRESS highWordAddress, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -7040,7 +7040,7 @@ HRESULT CordbNativeFrame::GetLocalFloatingPointValue(DWORD index, EMPTY_BUFFER, MemoryRange(&(pThread->m_floatValues[index]), sizeof(double)), pRegHolder, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), &pValue); // throws *ppValue = pValue; @@ -7147,7 +7147,7 @@ HRESULT CordbNativeFrame::GetLocalTwoRegisterValue(DWORD lowReg, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -9038,7 +9038,7 @@ CordbEval::CordbEval(CordbThread *pThread) m_evalDuringException(false) { m_vmObjectHandle = VMPTR_OBJECTHANDLE::NullPtr(); - m_vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr(); + m_vmExternalMemoryOwner = VMPTR_DebuggerExternalMemoryOwner::NullPtr(); m_debuggerEvalKey = LSPTR_DEBUGGEREVAL::NullPtr(); m_resultType.elementType = ELEMENT_TYPE_VOID; @@ -9143,17 +9143,17 @@ HRESULT CordbEval::SendCleanup() m_pHandleValue.Clear(); m_pValueClassResult.Clear(); - if (!m_vmExternalMemoryHandle.IsNull() && GetProcess()->IsSafeToSendEvents()) + if (!m_vmExternalMemoryOwner.IsNull() && GetProcess()->IsSafeToSendEvents()) { DebuggerIPCEvent event; GetProcess()->InitIPCEvent( &event, - DB_IPCE_DISPOSE_EXTERNAL_MEMORY_HANDLE, + DB_IPCE_DISPOSE_EXTERNAL_MEMORY_OWNER, false, m_thread->GetAppDomain()->GetADToken()); - event.DisposeExternalMemoryHandle.vmExternalMemoryHandle = m_vmExternalMemoryHandle; + event.DisposeExternalMemoryOwner.vmExternalMemoryOwner = m_vmExternalMemoryOwner; hr = WORST_HR(hr, GetProcess()->SendIPCEvent(&event, sizeof(DebuggerIPCEvent))); - m_vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr(); + m_vmExternalMemoryOwner = VMPTR_DebuggerExternalMemoryOwner::NullPtr(); } @@ -10501,7 +10501,7 @@ HRESULT CordbEval::GetResult(ICorDebugValue **ppResult) remoteValue, MemoryRange(NULL, 0), NULL, - boxed ? VMPTR_DebuggerExternalMemoryHandle::NullPtr() : m_vmExternalMemoryHandle, + boxed ? VMPTR_DebuggerExternalMemoryOwner::NullPtr() : m_vmExternalMemoryOwner, ppResult); // throws if (!boxed) @@ -10509,7 +10509,7 @@ HRESULT CordbEval::GetResult(ICorDebugValue **ppResult) CordbVCObjectValue *pValueClassResult = static_cast( static_cast(*ppResult)); m_pValueClassResult.Assign(pValueClassResult); - m_vmExternalMemoryHandle = VMPTR_DebuggerExternalMemoryHandle::NullPtr(); + m_vmExternalMemoryOwner = VMPTR_DebuggerExternalMemoryOwner::NullPtr(); } } @@ -11255,7 +11255,7 @@ HRESULT CordbAsyncFrame::GetArgument(DWORD dwIndex, ICorDebugValue ** ppValue) TargetBuffer(m_continuationAddress + m_asyncVars[i].offset, CordbValue::GetSizeForType(pType, kUnboxed)), MemoryRange(NULL, 0), NULL, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); foundArg = true; break; @@ -11435,7 +11435,7 @@ HRESULT CordbAsyncFrame::GetLocalVariableEx(ILCodeKind flags, DWORD dwIndex, ICo TargetBuffer(m_continuationAddress + m_asyncVars[i].offset, CordbValue::GetSizeForType(pType, kUnboxed)), MemoryRange(NULL, 0), NULL, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); foundLocal = true; break; diff --git a/src/coreclr/debug/di/valuehome.cpp b/src/coreclr/debug/di/valuehome.cpp index 7b74963bf9c140..82bc8188e55107 100644 --- a/src/coreclr/debug/di/valuehome.cpp +++ b/src/coreclr/debug/di/valuehome.cpp @@ -690,7 +690,7 @@ void RemoteValueHome::CreateInternalValue(CordbType * pType, TargetBuffer(m_remoteValue.pAddress + offset, size), MemoryRange(localAddress, localSize), NULL, // remote reg - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } // RemoteValueHome::CreateInternalValue @@ -815,7 +815,7 @@ void RegisterValueHome::CreateInternalValue(CordbType * pType, EMPTY_BUFFER, // remote address MemoryRange(localAddress, size), pRegHolder, - VMPTR_DebuggerExternalMemoryHandle::NullPtr(), + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } // RegisterValueHome::CreateInternalValue diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index 0ba01f963744a8..6e062d235dd3b8 100644 --- a/src/coreclr/debug/ee/debugger.cpp +++ b/src/coreclr/debug/ee/debugger.cpp @@ -1290,7 +1290,7 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval memset(m_result, 0, sizeof(m_result)); m_md = NULL; m_resultType = TypeHandle(); - m_externalMemoryHandle = NULL; + m_externalMemoryOwner = NULL; m_aborting = FE_ABORT_NONE; m_aborted = false; m_completed = false; @@ -1312,7 +1312,7 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval } } -DebuggerExternalMemoryHandle::DebuggerExternalMemoryHandle(MethodTable *pMT, BYTE *pMemory) +DebuggerExternalMemoryOwner::DebuggerExternalMemoryOwner(MethodTable *pMT, BYTE *pMemory) : m_pHandle(NULL), m_pMemory(pMemory) { @@ -1327,7 +1327,7 @@ DebuggerExternalMemoryHandle::DebuggerExternalMemoryHandle(MethodTable *pMT, BYT m_pHandle = ExternalMemoryHandle::Add(pMT, m_pMemory, 0); } -DebuggerExternalMemoryHandle::~DebuggerExternalMemoryHandle() +DebuggerExternalMemoryOwner::~DebuggerExternalMemoryOwner() { WRAPPER_NO_CONTRACT; @@ -1335,7 +1335,7 @@ DebuggerExternalMemoryHandle::~DebuggerExternalMemoryHandle() DeleteInteropSafe(m_pMemory); } -BYTE *DebuggerEval::CreateExternalMemoryHandle(MethodTable *pMT, SIZE_T size) +BYTE *DebuggerEval::CreateExternalMemory(MethodTable *pMT, SIZE_T size) { CONTRACTL { @@ -1345,12 +1345,12 @@ BYTE *DebuggerEval::CreateExternalMemoryHandle(MethodTable *pMT, SIZE_T size) } CONTRACTL_END; - _ASSERTE(m_externalMemoryHandle == NULL); + _ASSERTE(m_externalMemoryOwner == NULL); BYTE *pMemory = new (interopsafe) BYTE[size]; EX_TRY { - m_externalMemoryHandle = new (interopsafe) DebuggerExternalMemoryHandle(pMT, pMemory); + m_externalMemoryOwner = new (interopsafe) DebuggerExternalMemoryOwner(pMT, pMemory); } EX_CATCH { @@ -9873,13 +9873,13 @@ void Debugger::FuncEvalComplete(Thread* pThread, DebuggerEval *pDE) ipce->FuncEvalComplete.funcEvalKey = pDE->m_funcEvalKey; ipce->FuncEvalComplete.successful = pDE->m_successful; ipce->FuncEvalComplete.aborted = pDE->m_aborted; - void *pResult = pDE->m_externalMemoryHandle != NULL - ? static_cast(pDE->m_externalMemoryHandle->GetMemory()) + void *pResult = pDE->m_externalMemoryOwner != NULL + ? static_cast(pDE->m_externalMemoryOwner->GetMemory()) : static_cast(pDE->m_result); ipce->FuncEvalComplete.resultAddr = (CORDB_ADDRESS)pResult; ipce->FuncEvalComplete.vmAppDomain.SetRawPtr(pDomain); ipce->FuncEvalComplete.vmObjectHandle = pDE->m_vmObjectHandle; - ipce->FuncEvalComplete.vmExternalMemoryHandle.SetRawPtr(pDE->m_externalMemoryHandle); + ipce->FuncEvalComplete.vmExternalMemoryOwner.SetRawPtr(pDE->m_externalMemoryOwner); LOG((LF_CORDB, LL_INFO1000, "D::FEC: TypeHandle is %p\n", pDE->m_resultType.AsPtr())); @@ -9900,7 +9900,7 @@ void Debugger::FuncEvalComplete(Thread* pThread, DebuggerEval *pDE) (CORDB_ADDRESS_TO_PTR(ipce->FuncEvalComplete.resultAddr)))); IfFailThrow(m_pRCThread->SendIPCEvent()); - pDE->m_externalMemoryHandle = NULL; + pDE->m_externalMemoryOwner = NULL; #endif } @@ -10937,10 +10937,10 @@ bool Debugger::HandleIPCEvent(DebuggerIPCEvent * pEvent) break; } - case DB_IPCE_DISPOSE_EXTERNAL_MEMORY_HANDLE: + case DB_IPCE_DISPOSE_EXTERNAL_MEMORY_OWNER: { - DebuggerExternalMemoryHandle *pHandle = pEvent->DisposeExternalMemoryHandle.vmExternalMemoryHandle.GetRawPtr(); - DeleteInteropSafe(pHandle); + DebuggerExternalMemoryOwner *pOwner = pEvent->DisposeExternalMemoryOwner.vmExternalMemoryOwner.GetRawPtr(); + DeleteInteropSafe(pOwner); break; } diff --git a/src/coreclr/debug/ee/debugger.h b/src/coreclr/debug/ee/debugger.h index 76da93ff226a0b..e6abff340902c5 100644 --- a/src/coreclr/debug/ee/debugger.h +++ b/src/coreclr/debug/ee/debugger.h @@ -3362,11 +3362,13 @@ class DebuggerEvalBreakpointInfoSegment * type arguments > you get string followed by List followed by int. * ------------------------------------------------------------------------ */ -class DebuggerExternalMemoryHandle +// Owns an interop-safe buffer and the ExternalMemoryHandle registration that keeps references in +// the buffer visible to the GC. +class DebuggerExternalMemoryOwner { public: - DebuggerExternalMemoryHandle(MethodTable *pMT, BYTE *pMemory); - ~DebuggerExternalMemoryHandle(); + DebuggerExternalMemoryOwner(MethodTable *pMT, BYTE *pMemory); + ~DebuggerExternalMemoryOwner(); BYTE *GetMemory() const { @@ -3411,7 +3413,7 @@ class DebuggerEval PCODE m_targetCodeAddr; ARG_SLOT m_result[NUMBER_RETURNVALUE_SLOTS]; TypeHandle m_resultType; - DebuggerExternalMemoryHandle *m_externalMemoryHandle; + DebuggerExternalMemoryOwner *m_externalMemoryOwner; SIZE_T m_arrayRank; FUNC_EVAL_ABORT_TYPE m_aborting; // Has an abort been requested, and what type. bool m_aborted; // Was this eval aborted @@ -3423,7 +3425,7 @@ class DebuggerEval DebuggerEval(T_CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX); - BYTE *CreateExternalMemoryHandle(MethodTable *pMT, SIZE_T size); + BYTE *CreateExternalMemory(MethodTable *pMT, SIZE_T size); bool Init() { @@ -3460,9 +3462,9 @@ class DebuggerEval { WRAPPER_NO_CONTRACT; - if (m_externalMemoryHandle != NULL) + if (m_externalMemoryOwner != NULL) { - DeleteInteropSafe(m_externalMemoryHandle); + DeleteInteropSafe(m_externalMemoryOwner); } // Clean up any temporary buffers used to send the argument type information. These were allocated diff --git a/src/coreclr/debug/ee/funceval.cpp b/src/coreclr/debug/ee/funceval.cpp index 9913731dc572d5..4df7b8cda1d872 100644 --- a/src/coreclr/debug/ee/funceval.cpp +++ b/src/coreclr/debug/ee/funceval.cpp @@ -2816,7 +2816,7 @@ void PackArgumentArray(DebuggerEval *pDE, LPVOID pData; if (RetValueType.IsByRefLike()) { - pData = pDE->CreateExternalMemoryHandle(RetValueType.GetMethodTable(), size); + pData = pDE->CreateExternalMemory(RetValueType.GetMethodTable(), size); memset(pData, 0, size); } else @@ -2952,10 +2952,10 @@ void UnpackFuncEvalResult(DebuggerEval *pDE, ? Debugger::NoValueTypeBoxing : Debugger::OnlyPrimitivesUnboxed; - if (pDE->m_resultType.IsByRefLike() && pDE->m_externalMemoryHandle == NULL) + if (pDE->m_resultType.IsByRefLike() && pDE->m_externalMemoryOwner == NULL) { SIZE_T size = pDE->m_resultType.GetMethodTable()->GetNumInstanceFieldBytes(); - BYTE *pResult = pDE->CreateExternalMemoryHandle(pDE->m_resultType.GetMethodTable(), size); + BYTE *pResult = pDE->CreateExternalMemory(pDE->m_resultType.GetMethodTable(), size); memcpy(pResult, pDE->m_result, size); } } diff --git a/src/coreclr/debug/inc/dbgipcevents.h b/src/coreclr/debug/inc/dbgipcevents.h index fcfe7d6482d424..36eac9df480b73 100644 --- a/src/coreclr/debug/inc/dbgipcevents.h +++ b/src/coreclr/debug/inc/dbgipcevents.h @@ -779,7 +779,7 @@ DEFINE_VMPTR(class FieldDesc, PTR_FieldDesc, VMPTR_FieldDesc); // ObjectHandle is a safe way to represent an object into the GC heap. It gets updated // when a GC occurs. DEFINE_VMPTR(struct OBJECTHANDLE__, TADDR, VMPTR_OBJECTHANDLE); -DEFINE_VMPTR(class DebuggerExternalMemoryHandle, TADDR, VMPTR_DebuggerExternalMemoryHandle); +DEFINE_VMPTR(class DebuggerExternalMemoryOwner, TADDR, VMPTR_DebuggerExternalMemoryOwner); DEFINE_VMPTR(class TypeHandle, PTR_TypeHandle, VMPTR_TypeHandle); @@ -1742,7 +1742,7 @@ struct MSLAYOUT DebuggerIPCEvent Portable vmAppDomain; Portable vmObjectHandle; - Portable vmExternalMemoryHandle; + Portable vmExternalMemoryOwner; DebuggerIPCE_ExpandedTypeData resultType; } FuncEvalComplete; @@ -1844,8 +1844,8 @@ struct MSLAYOUT DebuggerIPCEvent struct MSLAYOUT { - Portable vmExternalMemoryHandle; - } DisposeExternalMemoryHandle; + Portable vmExternalMemoryOwner; + } DisposeExternalMemoryOwner; struct MSLAYOUT { diff --git a/src/coreclr/debug/inc/dbgipceventtypes.h b/src/coreclr/debug/inc/dbgipceventtypes.h index 1279c821e77a1f..e0b5cf45fd885e 100644 --- a/src/coreclr/debug/inc/dbgipceventtypes.h +++ b/src/coreclr/debug/inc/dbgipceventtypes.h @@ -130,5 +130,5 @@ IPC_EVENT_TYPE2(DB_IPCE_RESOLVE_UPDATE_METADATA_2 ,0x0257) IPC_EVENT_TYPE2(DB_IPCE_DISABLE_OPTS ,0x0258) IPC_EVENT_TYPE2(DB_IPCE_FORCE_CATCH_HANDLER_FOUND ,0x025A) IPC_EVENT_TYPE2(DB_IPCE_SET_ENABLE_CUSTOM_NOTIFICATION, 0x025B) -IPC_EVENT_TYPE2(DB_IPCE_DISPOSE_EXTERNAL_MEMORY_HANDLE, 0x025C) +IPC_EVENT_TYPE2(DB_IPCE_DISPOSE_EXTERNAL_MEMORY_OWNER, 0x025C) IPC_EVENT_TYPE0(DB_IPCE_DEBUGGER_LAST ,0x025D) // The last event from the debugger From f75d0cd0219a7b49d538aed677b550e580dba4f3 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 18 Sep 2026 12:59:01 -0700 Subject: [PATCH 23/24] Remove external handle dump enumeration Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3900c560-18f8-4900-9840-ae7022248647 --- src/coreclr/vm/appdomain.cpp | 2 -- src/coreclr/vm/externalmemoryhandle.cpp | 26 ------------------------- src/coreclr/vm/externalmemoryhandle.h | 5 ----- 3 files changed, 33 deletions(-) diff --git a/src/coreclr/vm/appdomain.cpp b/src/coreclr/vm/appdomain.cpp index afb7b7cc5c92d5..4568cc787c54af 100644 --- a/src/coreclr/vm/appdomain.cpp +++ b/src/coreclr/vm/appdomain.cpp @@ -4203,8 +4203,6 @@ AppDomain::EnumMemoryRegions(CLRDataEnumMemoryFlags flags, bool enumThis) { pAssembly->EnumMemoryRegions(flags); } - - ExternalMemoryHandle::EnumMemoryRegionsForAllHandles(flags); } void diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp index 8806c54b64068e..71a7be0882cb16 100644 --- a/src/coreclr/vm/externalmemoryhandle.cpp +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -116,29 +116,3 @@ void ExternalMemoryHandle::GCScanRoot(promote_func *fn, ScanContext *sc) DBG_ADDR(fromAddress), DBG_ADDR(toAddress))); } } - -#ifdef DACCESS_COMPILE - -void ExternalMemoryHandle::EnumMemoryRegions(CLRDataEnumMemoryFlags flags) -{ - SUPPORTS_DAC; - - m_pMT.EnumMem(); - m_pMT->EnumMemoryRegions(flags); - - TSIZE_T size = m_pMT->IsValueType() ? m_pMT->GetNumInstanceFieldBytes() : TARGET_POINTER_SIZE; - DacEnumMemoryRegion(m_pMemory.GetAddr(), size); -} - -void ExternalMemoryHandle::EnumMemoryRegionsForAllHandles(CLRDataEnumMemoryFlags flags) -{ - SUPPORTS_DAC; - - for (ExternalMemoryHandle* handle = s_handles.GetHead(); handle != nullptr; handle = SListTail::GetNext(handle)) - { - DacEnumMemoryRegion(dac_cast(handle), sizeof(ExternalMemoryHandle)); - handle->EnumMemoryRegions(flags); - } -} - -#endif // DACCESS_COMPILE diff --git a/src/coreclr/vm/externalmemoryhandle.h b/src/coreclr/vm/externalmemoryhandle.h index dc2c8b2507cf92..d26c1f5e3d4350 100644 --- a/src/coreclr/vm/externalmemoryhandle.h +++ b/src/coreclr/vm/externalmemoryhandle.h @@ -26,11 +26,6 @@ class ExternalMemoryHandle final void GCScanRoot(promote_func *fn, ScanContext *sc); -#ifdef DACCESS_COMPILE - void EnumMemoryRegions(CLRDataEnumMemoryFlags flags); - static void EnumMemoryRegionsForAllHandles(CLRDataEnumMemoryFlags flags); -#endif - // Next pointer for SList linkage. PTR_ExternalMemoryHandle m_pNext; From 0b043b599ba40b0e027eb5c6a135bb514198eee0 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 18 Sep 2026 13:24:08 -0700 Subject: [PATCH 24/24] Address cDAC contract compatibility feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3900c560-18f8-4900-9840-ae7022248647 --- .../Contracts/ByRefPointerOffsetsReporter.cs | 17 +++---- .../CallingConvention/CallingConvention_1.cs | 7 ++- .../Contracts/ExternalMemoryHandles_1.cs | 8 ++-- .../StackWalk/GC/GCInteriorPointerResolver.cs | 9 +--- .../CoreCLRContracts.cs | 21 ++++++++- .../Dbi/Helpers/RefWalk.cs | 3 +- .../ContractDescriptor/TargetTests.cs | 46 +++++++++++++++++++ .../RefWalkExternalMemoryHandleTests.cs | 19 ++++++++ 8 files changed, 102 insertions(+), 28 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs index 81462f0dc78620..7e15c1bd1366e9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs @@ -10,17 +10,12 @@ internal sealed class ByRefPointerOffsetsReporter private const int MaxByRefLikeRecursionDepth = 16; private readonly IRuntimeTypeSystem _rts; - private readonly ulong _source; private readonly uint _pointerSize; - public ByRefPointerOffsetsReporter( - IRuntimeTypeSystem rts, - TargetPointer source, - uint pointerSize) + public ByRefPointerOffsetsReporter(Target target) { - _rts = rts; - _source = source.Value; - _pointerSize = pointerSize; + _rts = target.Contracts.RuntimeTypeSystem; + _pointerSize = (uint)target.PointerSize; } private IEnumerable Find( @@ -45,12 +40,12 @@ private IEnumerable Find( } else if (fieldType == CorElementType.Byref) { - yield return _source + baseOffset + fieldOffset; + yield return baseOffset + fieldOffset; } } - public IEnumerable Find(ITypeHandle typeHandle, ulong baseOffset) - => Find(typeHandle, baseOffset, depth: 0); + public IEnumerable Find(ITypeHandle typeHandle) + => Find(typeHandle, baseOffset: 0, depth: 0); private IEnumerable Find(ITypeHandle typeHandle, ulong baseOffset, int depth) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs index a5f931638d9ee7..7f9d509ebc7318 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs @@ -459,10 +459,9 @@ private CdacTypeHandle GetIntPtrTypeHandle(IRuntimeTypeSystem rts) arg.TypeInfo.ExactTypeHandle ?? arg.TypeInfo.GenericTypeDefinition; if (layoutType is not null) { - ByRefPointerOffsetsReporter reporter = - new(rts, TargetPointer.Null, (uint)pointerSize); - foreach (ulong offset in reporter.Find(layoutType, (ulong)arg.Offset)) - tokens[checked((int)offset)] = GCRefMapToken.Interior; + ByRefPointerOffsetsReporter reporter = new(_target); + foreach (ulong offset in reporter.Find(layoutType)) + tokens[checked(arg.Offset + (int)offset)] = GCRefMapToken.Interior; } emitted = true; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs index f772c4509acd8d..28aa62863fb57f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs @@ -19,7 +19,7 @@ internal ExternalMemoryHandles_1(Target target) _target = target; _gc = target.Contracts.GC; _rts = target.Contracts.RuntimeTypeSystem; - _interiorPointerResolver = new GCInteriorPointerResolver(target, _gc, _rts); + _interiorPointerResolver = new GCInteriorPointerResolver(target); } IReadOnlyList IExternalMemoryHandles.GetRoots(bool resolveInteriorPointers) @@ -66,9 +66,9 @@ private void AddValueTypeRoots( if (_rts.IsByRefLike(typeHandle)) { - ByRefPointerOffsetsReporter reporter = new(_rts, memory, (uint)_target.PointerSize); - foreach (ulong slotAddress in reporter.Find(typeHandle, 0)) - AddInteriorRoot(roots, new TargetPointer(slotAddress), resolveInteriorPointers); + ByRefPointerOffsetsReporter reporter = new(_target); + foreach (ulong offset in reporter.Find(typeHandle)) + AddInteriorRoot(roots, memory + offset, resolveInteriorPointers); } if (_rts.ContainsGCPointers(typeHandle)) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs index 1ac7b4bf650bf8..1a0df494d8967a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GCInteriorPointerResolver.cs @@ -20,14 +20,9 @@ public sealed class GCInteriorPointerResolver private readonly byte _objectToMethodTableUnmask; public GCInteriorPointerResolver(Target target) - : this(target, target.Contracts.GC, target.Contracts.RuntimeTypeSystem) { - } - - internal GCInteriorPointerResolver(Target target, IGC gc, IRuntimeTypeSystem rts) - { - _gc = gc; - _rts = rts; + _gc = target.Contracts.GC; + _rts = target.Contracts.RuntimeTypeSystem; _cache = new LinearReadCache(target); _numComponentsOffsetArray = (uint)Data.Array.GetNumComponentsOffset(target); _numComponentsOffsetString = (uint)Data.String.GetStringLengthOffset(target); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index 9165bc94674da6..1e36b4ea04ed33 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -118,7 +118,6 @@ public static void ValidateForDataAccess(Target target, Lock? apiLock = null) Validate(registry); Validate(registry); Validate(registry); - Validate(registry); Validate(registry); Validate(registry); Validate(registry); @@ -135,6 +134,13 @@ public static void ValidateForDataAccess(Target target, Lock? apiLock = null) Validate(registry); Validate(registry); + // ExternalMemoryHandles was introduced in .NET 12. Readers built from this source may still + // inspect .NET 11 targets, which do not advertise the contract. + if (GetRuntimeMajorVersion(target) >= 12) + { + Validate(registry); + } + // Transitive contract accesses from the implementations above. Validate(registry); // IComWrappers: ComWrappers_1.cs Validate(registry); // IStackWalk: StackWalk_1.cs @@ -200,5 +206,18 @@ static void ValidateSubDescriptorContract(Target target) where TContr message: $"Contract '{TContract.Name}' validation failed but no reason was reported."); } } + + static int GetRuntimeMajorVersion(Target target) + { + if (!target.TryReadGlobalString(Constants.Globals.RuntimeProductVersionString, out string? productVersion)) + { + // Preserve required-contract validation when the target version is unavailable. + return int.MaxValue; + } + + int separator = productVersion.IndexOf('.'); + string majorVersionText = separator >= 0 ? productVersion[..separator] : productVersion; + return int.TryParse(majorVersionText, out int majorVersion) ? majorVersion : int.MaxValue; + } } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs index 1be0b4d0887405..b70fdcad2542bd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs @@ -135,7 +135,8 @@ private bool TryMapHandle(HandleData handle, out CorGCReferenceType dwType, out // Walks the external memory handles registered with the current AppDomain. private IEnumerable WalkExternalMemoryHandles() { - IExternalMemoryHandles externalMemoryHandles = _target.Contracts.ExternalMemoryHandles; + if (!_target.Contracts.TryGetContract(out IExternalMemoryHandles externalMemoryHandles)) + yield break; foreach (ExternalMemoryHandleRootData root in externalMemoryHandles.GetRoots(resolveInteriorPointers: true)) { diff --git a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs index 10cb970aedc9b3..66ded94c2f11f7 100644 --- a/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs @@ -492,6 +492,52 @@ public void ValidateForDataAccess_AllRequiredPresent_DoesNotThrow(MockTarget.Arc Contracts.CoreCLRContracts.ValidateForDataAccess(target); } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ValidateForDataAccess_Net11Target_DoesNotRequireExternalMemoryHandles(MockTarget.Architecture arch) + { + TargetTestHelpers targetTestHelpers = new(arch); + ContractDescriptorBuilder builder = new(targetTestHelpers); + ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder); + descriptorBuilder + .SetContracts( + s_requiredDataAccessContracts + .Where(static pair => pair.Key != "ExternalMemoryHandles") + .ToDictionary(static pair => pair.Key, static pair => pair.Value)) + .SetGlobals( + [ + (Constants.Globals.RuntimeProductVersionString, null, "11.0.0", "string"), + ]); + + Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target)); + + Contracts.CoreCLRContracts.ValidateForDataAccess(target); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void ValidateForDataAccess_Net12Target_RequiresExternalMemoryHandles(MockTarget.Architecture arch) + { + TargetTestHelpers targetTestHelpers = new(arch); + ContractDescriptorBuilder builder = new(targetTestHelpers); + ContractDescriptorBuilder.DescriptorBuilder descriptorBuilder = new(builder); + descriptorBuilder + .SetContracts( + s_requiredDataAccessContracts + .Where(static pair => pair.Key != "ExternalMemoryHandles") + .ToDictionary(static pair => pair.Key, static pair => pair.Value)) + .SetGlobals( + [ + (Constants.Globals.RuntimeProductVersionString, null, "12.0.0", "string"), + ]); + + Assert.True(builder.TryCreateTarget(descriptorBuilder, out ContractDescriptorTarget? target)); + + ContractMissingException ex = Assert.Throws( + () => Contracts.CoreCLRContracts.ValidateForDataAccess(target)); + Assert.Equal("ExternalMemoryHandles", ex.ContractName); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void ValidateForDataAccess_MissingRequiredContract_ThrowsNotAdvertised(MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs index edb42a17bed79f..85aafb741ccf2e 100644 --- a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs @@ -318,4 +318,23 @@ public void WalkExternalMemoryHandles_MapsContractRoots() Assert.Equal(interiorObject.Value | 1, reference.pObject); }); } + + [Fact] + public void WalkExternalMemoryHandles_MissingContract_YieldsNoRoots() + { + var loader = new Mock(); + loader.Setup(l => l.GetAppDomain()).Returns(new TargetPointer(0x1000)); + + var gc = new Mock(); + gc.Setup(g => g.GetSupportedHandleTypes()).Returns([]); + + TestPlaceholderTarget target = new TestPlaceholderTarget.Builder(Arch) + .AddMockContract(loader) + .AddMockContract(gc) + .Build(); + + RefWalk walk = new(target, walkStacks: false, CorGCReferenceType.CorHandleStrong); + + Assert.False(walk.Enumerator.MoveNext()); + } }