diff --git a/docs/design/datacontracts/ExternalMemoryHandles.md b/docs/design/datacontracts/ExternalMemoryHandles.md new file mode 100644 index 00000000000000..729eab854bfbcd --- /dev/null +++ b/docs/design/datacontracts/ExternalMemoryHandles.md @@ -0,0 +1,94 @@ +# ExternalMemoryHandles contract + +The ExternalMemoryHandles contract scans memory registered with the runtime as containing managed +references outside the GC heap and managed stacks. + +## APIs of contract + +``` csharp +sealed class ExternalMemoryHandleRootData +{ + bool IsInteriorPointer { get; init; } + TargetPointer Address { get; init; } + TargetPointer Object { get; init; } +} +``` + +``` csharp +IReadOnlyList GetRoots(bool resolveInteriorPointers); +``` + +## Version 1 + + +### Data descriptors used + +| Data Descriptor | Field | Type | Meaning | +| --- | --- | --- | --- | +| `Array` | `m_NumComponents` | `uint32` | Number of items in the array | +| `ExternalMemoryHandle` | `GCFlags` | `uint32` | Non-zero if the handle's memory holds a direct object pointer (interior/GC_CALL_INTERIOR root) rather than the address of an object reference slot | +| `ExternalMemoryHandle` | `Memory` | `pointer` | Pointer to the external memory tracked by this handle | +| `ExternalMemoryHandle` | `MethodTable` | `pointer` | Pointer to the MethodTable describing the type of the tracked memory | +| `ExternalMemoryHandle` | `Next` | `pointer` | Pointer to the next ExternalMemoryHandle in the process-wide list | +| `Object` | `m_pMethTab` | `pointer` | Method table for the object | +| `String` | `m_StringLength` | `uint32` | Length of the string in UTF-16 characters | + +### Global variables used + +| Global | Type | Meaning | +| --- | --- | --- | +| `ExternalMemoryHandles` | `pointer` | Address of the global pointer to the head of the process-wide external memory handle list (read a TargetPointer from this address to obtain the head ExternalMemoryHandle, or null if the list is empty) | +| `ObjectToMethodTableUnmask` | `uint8` | Bits to clear when converting an object header value to a method table address | + +### Contracts used + +| Contract Name | +| --- | +| `GC` | +| `RuntimeTypeSystem` | + + +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 headPointer = // read the ExternalMemoryHandles global + TargetPointer current = // read a pointer from headPointer + + HashSet visited = []; + List roots = []; + while (current != TargetPointer.Null) + { + if (!visited.Add(current)) + throw new InvalidOperationException(); + + ExternalMemoryHandle handle = // read ExternalMemoryHandle object starting at current + TypeHandle type = // get the RuntimeTypeSystem handle for handle.MethodTable + if (type.IsValueType) + { + // Add GCDesc object-reference slots and recursively discovered byref-like interior roots. + } + else if (handle.GCFlags != 0) + { + // Read the pointer from handle.Memory and optionally resolve it to its containing object. + } + else + { + roots.Add(new ExternalMemoryHandleRootData { Address = handle.Memory }); + } + current = handle.Next; + } + return roots; +} +``` 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..bd5efa01c8b7e4 100644 --- a/docs/design/datacontracts/data-descriptor-meanings.json +++ b/docs/design/datacontracts/data-descriptor-meanings.json @@ -125,7 +125,7 @@ "EEClass.NumStaticFields": "Count of static fields of the EEClass", "EEClass.NumThreadStaticFields": "Count of threadstatic fields of the EEClass", "EEClass.OptionalFields": "Pointer to the `EEClassOptionalFields` for this type, or null if it has none", - "EEClass.VMFlags": "Optional flags for the EEClass. Bit `0x40` (`VMFLAG_HASLAYOUT`) indicates the EEClass is a `LayoutEEClass` and its `LayoutInfo` may be read", + "EEClass.VMFlags": "Optional flags for the EEClass. Bit `0x40` (`VMFLAG_HASLAYOUT`) indicates the EEClass is a `LayoutEEClass` and its `LayoutInfo` may be read. Bit `0x10000` (`VMFLAG_INLINE_ARRAY`) indicates the type is a compiler-generated inline array buffer whose single declared instance field is repeated across the whole array", "EEClassLayoutInfo.AlignmentRequirement": "Largest alignment requirement of all members of the type", "EEClassLayoutInfo.Flags": "Layout flags. Bit `0x01` (`e_BLITTABLE`) indicates the type is blittable", "EEClassLayoutInfo.LayoutType": "Layout kind: `Auto` (0), `Sequential` (1), `Explicit` (2), `CStruct` (3), `CUnion` (4)", @@ -182,6 +182,10 @@ "ExceptionLookupTableEntry.ExceptionInfoRVA": "RVA of the exception clause data", "ExceptionLookupTableEntry.MethodStartRVA": "RVA of the method start", "ExceptionLookupTableEntry.Size": "Size of an exception lookup table entry in bytes", + "ExternalMemoryHandle.GCFlags": "Non-zero if the handle's memory holds a direct object pointer (interior/GC_CALL_INTERIOR root) rather than the address of an object reference slot", + "ExternalMemoryHandle.Memory": "Pointer to the external memory tracked by this handle", + "ExternalMemoryHandle.MethodTable": "Pointer to the MethodTable describing the type of the tracked memory", + "ExternalMemoryHandle.Next": "Pointer to the next ExternalMemoryHandle in the process-wide list", "ExternalMethodFrame.Indirection": "Import slot pointer for GCRefMap resolution via FindReadyToRunModule", "FaultingExceptionFrame.TargetContext": "Frame's Target Context", "FCallMethodDesc.Size": "Base size for mcFCall classification", @@ -744,6 +748,7 @@ "ExceptionMethodTable": "A pointer to the address of the System.Exception MethodTable (g_pExceptionClass)", "ExecutionManagerCodeRangeMapAddress": "Pointer to the global RangeSectionMap", "ExpandMechanismsLength": "The number of elements in the ExpandMechanisms array", + "ExternalMemoryHandles": "Address of the global pointer to the head of the process-wide external memory handle list (read a TargetPointer from this address to obtain the head ExternalMemoryHandle, or null if the list is empty)", "FeatureCOMInterop": "Present (nonzero) when COM interop is enabled", "FeatureComWrappers": "Present (nonzero) when ComWrappers is enabled", "FeatureEHFunclets": "1 if EH funclets are enabled, 0 otherwise", diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index dab05de698687e..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" @@ -7792,7 +7793,8 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetGenericArgTokenIndex(VMPTR_Met DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers) : mDac(dac), mWalkStacks(walkStacks), mHandleMask(handleMask), mStackWalker(NULL), - mResolvePointers(resolvePointers), mHandleWalker(NULL) + mResolvePointers(resolvePointers), mHandleWalker(NULL), mExternalMemoryHandleIndex(0), + mExternalMemoryHeapInitialized(false) { } @@ -7817,6 +7819,11 @@ HRESULT DacRefWalker::Init() hr = NextThread(); } + if ((mHandleMask & CorHandleStrong) && SUCCEEDED(hr)) + { + hr = WalkExternalMemoryHandles(); + } + return hr; } @@ -7863,6 +7870,74 @@ UINT32 DacRefWalker::GetHandleWalkerMask() return result; } +HRESULT DacRefWalker::WalkExternalMemoryHandles() +{ + ExternalMemoryScanContext context(this); + ExternalMemoryHandle::GCScanRoots(ExternalMemoryHandleCallback, &context); + return context.Result; +} + +void DacRefWalker::ExternalMemoryHandleCallback(PTR_PTR_Object ppObj, ScanContext *sc, uint32_t flags) +{ + ExternalMemoryScanContext* context = static_cast(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 +7962,11 @@ HRESULT DacRefWalker::Next(ULONG celt, DacGcReference roots[], ULONG *pceltFetch } } + while (total < celt && mExternalMemoryHandleIndex < mExternalMemoryHandles.GetCount()) + { + roots[total++] = mExternalMemoryHandles.Get(mExternalMemoryHandleIndex++); + } + while (total < celt && mStackWalker) { ULONG fetched = 0; 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/debug/di/divalue.cpp b/src/coreclr/debug/di/divalue.cpp index 4a0abfa6bacd43..46a549880899f3 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 +// 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 @@ -145,6 +146,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, + VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner, ICorDebugValue** ppValue) { @@ -166,6 +168,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain ppRemoteRegAddr)); IfFailThrow(pVCValue->Init(localValue)); + pVCValue->SetExternalMemoryOwner(vmExternalMemoryOwner); pVCValue->AddRef(); *ppValue = (ICorDebugValue*)(ICorDebugObjectValue*)pVCValue; @@ -196,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 +// 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. @@ -206,6 +210,7 @@ void CordbValue::CreateVCObjOrRefValue(CordbAppDomain * pAppdomain TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, + VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner, ICorDebugValue** ppValue) { INTERNAL_SYNC_API_ENTRY(pAppdomain->GetProcess()); // @@ -249,11 +254,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, vmExternalMemoryOwner, ppValue); // throws break; } @@ -1186,6 +1192,7 @@ HRESULT CordbReferenceValue::DereferenceCommon( remoteValue, MemoryRange(NULL, 0), // local value NULL, + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -1212,6 +1219,7 @@ HRESULT CordbReferenceValue::DereferenceCommon( remoteValue, MemoryRange(NULL, 0), // local value NULL, + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -2922,7 +2930,8 @@ CordbVCObjectValue::CordbVCObjectValue(CordbAppDomain * pAppdomain false, pAppdomain->GetSweepableExitNeuterList()), m_pObjectCopy(NULL), - m_pValueHome(NULL) + m_pValueHome(NULL), + m_vmExternalMemoryOwner(VMPTR_DebuggerExternalMemoryOwner::NullPtr()) { // instantiate the value home NewHolder pHome(NULL); @@ -2961,6 +2970,39 @@ CordbVCObjectValue::~CordbVCObjectValue() } } // CordbVCObjectValue::~CordbVCObjectValue +void CordbVCObjectValue::SetExternalMemoryOwner(VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner) +{ + _ASSERTE(m_vmExternalMemoryOwner.IsNull()); + m_vmExternalMemoryOwner = vmExternalMemoryOwner; +} + +void CordbVCObjectValue::NeuterLeftSideResources() +{ + if (!m_vmExternalMemoryOwner.IsNull() && GetProcess()->IsSafeToSendEvents()) + { + DebuggerIPCEvent event; + GetProcess()->InitIPCEvent( + &event, + DB_IPCE_DISPOSE_EXTERNAL_MEMORY_OWNER, + false, + m_appdomain->GetADToken()); + event.DisposeExternalMemoryOwner.vmExternalMemoryOwner = m_vmExternalMemoryOwner; + GetProcess()->SendIPCEvent(&event, sizeof(DebuggerIPCEvent)); + m_vmExternalMemoryOwner = VMPTR_DebuggerExternalMemoryOwner::NullPtr(); + } + + RSLockHolder lockHolder(GetProcess()->GetProcessLock()); + Neuter(); +} + +void CordbVCObjectValue::Neuter() +{ + BOOL fTargetIsDead = !GetProcess()->IsSafeToSendEvents() || GetProcess()->m_exiting; + _ASSERTE(fTargetIsDead || m_vmExternalMemoryOwner.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..8dd661297398ac 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_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 4b1f5dc535c97d..8e30f5f759982c 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_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 f8011e14d564a8..64ae475be8ec9f 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -8688,6 +8688,7 @@ class CordbValue : public CordbBase TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, + VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner, ICorDebugValue** ppValue); // Create the proper ICDValue instance based on the given element type. @@ -8697,6 +8698,7 @@ class CordbValue : public CordbBase TargetBuffer remoteValue, MemoryRange localValue, EnregisteredValueHomeHolder * ppRemoteRegAddr, + VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner, ICorDebugValue** ppValue); // Create the proper ICDValue instance based on the given remote heap object @@ -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 SetExternalMemoryOwner(VMPTR_DebuggerExternalMemoryOwner vmExternalMemoryOwner); //HRESULT ResolveValueClass(); CordbClass *GetClass(); @@ -9432,6 +9437,8 @@ class CordbVCObjectValue : public CordbValue, // location information ValueHome * m_pValueHome; + + VMPTR_DebuggerExternalMemoryOwner m_vmExternalMemoryOwner; }; @@ -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_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 345b59adaa86ea..99ae8fe1ae0850 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_DebuggerExternalMemoryOwner::NullPtr(), &pValue); // throws *ppValue = pValue; @@ -6773,6 +6774,7 @@ HRESULT CordbNativeFrame::GetLocalDoubleRegisterValue( EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, + VMPTR_DebuggerExternalMemoryOwner::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_DebuggerExternalMemoryOwner::NullPtr(), &pValue); // throws } EX_CATCH_HRESULT(hr); @@ -6877,6 +6880,7 @@ CordbNativeFrame::GetLocalRegisterMemoryValue(CorDebugRegister highWordReg, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -6924,6 +6928,7 @@ CordbNativeFrame::GetLocalMemoryRegisterValue(CORDB_ADDRESS highWordAddress, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, + VMPTR_DebuggerExternalMemoryOwner::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_DebuggerExternalMemoryOwner::NullPtr(), &pValue); // throws *ppValue = pValue; @@ -7141,6 +7147,7 @@ HRESULT CordbNativeFrame::GetLocalTwoRegisterValue(DWORD lowReg, EMPTY_BUFFER, MemoryRange(NULL, 0), pRegHolder, + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } EX_CATCH_HRESULT(hr); @@ -9031,6 +9038,7 @@ CordbEval::CordbEval(CordbThread *pThread) m_evalDuringException(false) { m_vmObjectHandle = VMPTR_OBJECTHANDLE::NullPtr(); + m_vmExternalMemoryOwner = VMPTR_DebuggerExternalMemoryOwner::NullPtr(); m_debuggerEvalKey = LSPTR_DEBUGGEREVAL::NullPtr(); m_resultType.elementType = ELEMENT_TYPE_VOID; @@ -9133,6 +9141,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_vmExternalMemoryOwner.IsNull() && GetProcess()->IsSafeToSendEvents()) + { + DebuggerIPCEvent event; + GetProcess()->InitIPCEvent( + &event, + DB_IPCE_DISPOSE_EXTERNAL_MEMORY_OWNER, + false, + m_thread->GetAppDomain()->GetADToken()); + event.DisposeExternalMemoryOwner.vmExternalMemoryOwner = m_vmExternalMemoryOwner; + hr = WORST_HR(hr, GetProcess()->SendIPCEvent(&event, sizeof(DebuggerIPCEvent))); + m_vmExternalMemoryOwner = VMPTR_DebuggerExternalMemoryOwner::NullPtr(); + } return hr; @@ -10460,16 +10482,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, + boxed ? VMPTR_DebuggerExternalMemoryOwner::NullPtr() : m_vmExternalMemoryOwner, ppResult); // throws + + if (!boxed) + { + CordbVCObjectValue *pValueClassResult = static_cast( + static_cast(*ppResult)); + m_pValueClassResult.Assign(pValueClassResult); + m_vmExternalMemoryOwner = VMPTR_DebuggerExternalMemoryOwner::NullPtr(); + } } } @@ -11214,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_DebuggerExternalMemoryOwner::NullPtr(), ppValue); foundArg = true; break; @@ -11393,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_DebuggerExternalMemoryOwner::NullPtr(), ppValue); foundLocal = true; break; diff --git a/src/coreclr/debug/di/valuehome.cpp b/src/coreclr/debug/di/valuehome.cpp index 24230bb6c85cd1..82bc8188e55107 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_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } // RemoteValueHome::CreateInternalValue @@ -814,6 +815,7 @@ void RegisterValueHome::CreateInternalValue(CordbType * pType, EMPTY_BUFFER, // remote address MemoryRange(localAddress, size), pRegHolder, + VMPTR_DebuggerExternalMemoryOwner::NullPtr(), ppValue); // throws } // RegisterValueHome::CreateInternalValue @@ -1145,4 +1147,3 @@ RefValueHome::RefValueHome(CordbProcess * pProcess, } // RefValueHome::RefValueHome - diff --git a/src/coreclr/debug/ee/debugger.cpp b/src/coreclr/debug/ee/debugger.cpp index 30cf2c29604744..6e062d235dd3b8 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" @@ -1289,6 +1290,7 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval memset(m_result, 0, sizeof(m_result)); m_md = NULL; m_resultType = TypeHandle(); + m_externalMemoryOwner = NULL; m_aborting = FE_ABORT_NONE; m_aborted = false; m_completed = false; @@ -1310,6 +1312,56 @@ DebuggerEval::DebuggerEval(CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEval } } +DebuggerExternalMemoryOwner::DebuggerExternalMemoryOwner(MethodTable *pMT, BYTE *pMemory) + : m_pHandle(NULL), + m_pMemory(pMemory) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_COOPERATIVE; + } + CONTRACTL_END; + + m_pHandle = ExternalMemoryHandle::Add(pMT, m_pMemory, 0); +} + +DebuggerExternalMemoryOwner::~DebuggerExternalMemoryOwner() +{ + WRAPPER_NO_CONTRACT; + + ExternalMemoryHandle::Remove(m_pHandle DEBUG_ARG(g_pDebugger->IsStopped())); + DeleteInteropSafe(m_pMemory); +} + +BYTE *DebuggerEval::CreateExternalMemory(MethodTable *pMT, SIZE_T size) +{ + CONTRACTL + { + THROWS; + GC_NOTRIGGER; + MODE_COOPERATIVE; + } + CONTRACTL_END; + + _ASSERTE(m_externalMemoryOwner == NULL); + + BYTE *pMemory = new (interopsafe) BYTE[size]; + EX_TRY + { + m_externalMemoryOwner = new (interopsafe) DebuggerExternalMemoryOwner(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 +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; - ipce->FuncEvalComplete.resultAddr = (CORDB_ADDRESS)(pDE->m_result); + 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.vmExternalMemoryOwner.SetRawPtr(pDE->m_externalMemoryOwner); LOG((LF_CORDB, LL_INFO1000, "D::FEC: TypeHandle is %p\n", pDE->m_resultType.AsPtr())); @@ -9832,17 +9888,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_externalMemoryOwner = NULL; #endif } @@ -10879,6 +10937,13 @@ bool Debugger::HandleIPCEvent(DebuggerIPCEvent * pEvent) break; } + case DB_IPCE_DISPOSE_EXTERNAL_MEMORY_OWNER: + { + DebuggerExternalMemoryOwner *pOwner = pEvent->DisposeExternalMemoryOwner.vmExternalMemoryOwner.GetRawPtr(); + DeleteInteropSafe(pOwner); + 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..e6abff340902c5 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); @@ -3361,6 +3362,24 @@ class DebuggerEvalBreakpointInfoSegment * type arguments > you get string followed by List followed by int. * ------------------------------------------------------------------------ */ +// Owns an interop-safe buffer and the ExternalMemoryHandle registration that keeps references in +// the buffer visible to the GC. +class DebuggerExternalMemoryOwner +{ +public: + DebuggerExternalMemoryOwner(MethodTable *pMT, BYTE *pMemory); + ~DebuggerExternalMemoryOwner(); + + BYTE *GetMemory() const + { + return m_pMemory; + } + +private: + ExternalMemoryHandle *m_pHandle; + BYTE *m_pMemory; +}; + class DebuggerEval { public: @@ -3394,6 +3413,7 @@ class DebuggerEval PCODE m_targetCodeAddr; ARG_SLOT m_result[NUMBER_RETURNVALUE_SLOTS]; TypeHandle m_resultType; + 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 @@ -3405,6 +3425,8 @@ class DebuggerEval DebuggerEval(T_CONTEXT * pContext, DebuggerIPCE_FuncEvalInfo * pEvalInfo, DebuggerEvalBreakpointInfoSegment* bpInfoSegmentRX); + BYTE *CreateExternalMemory(MethodTable *pMT, SIZE_T size); + bool Init() { if (m_bpInfoSegment == NULL) @@ -3440,8 +3462,13 @@ class DebuggerEval { WRAPPER_NO_CONTRACT; + if (m_externalMemoryOwner != NULL) + { + DeleteInteropSafe(m_externalMemoryOwner); + } + // 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 +3490,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..4df7b8cda1d872 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->CreateExternalMemory(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_externalMemoryOwner == NULL) + { + SIZE_T size = pDE->m_resultType.GetMethodTable()->GetNumInstanceFieldBytes(); + BYTE *pResult = pDE->CreateExternalMemory(pDE->m_resultType.GetMethodTable(), size); + memcpy(pResult, pDE->m_result, size); + } } LOG((LF_CORDB, LL_INFO10000, "FuncEval call has saved the return value.\n")); @@ -2955,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")); @@ -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..36eac9df480b73 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 DebuggerExternalMemoryOwner, TADDR, VMPTR_DebuggerExternalMemoryOwner); DEFINE_VMPTR(class TypeHandle, PTR_TypeHandle, VMPTR_TypeHandle); @@ -1741,6 +1742,7 @@ struct MSLAYOUT DebuggerIPCEvent Portable vmAppDomain; Portable vmObjectHandle; + Portable vmExternalMemoryOwner; DebuggerIPCE_ExpandedTypeData resultType; } FuncEvalComplete; @@ -1840,6 +1842,11 @@ struct MSLAYOUT DebuggerIPCEvent Portable handleType; } DisposeHandle; + struct MSLAYOUT + { + Portable vmExternalMemoryOwner; + } DisposeExternalMemoryOwner; + struct MSLAYOUT { Portable framePointer; diff --git a/src/coreclr/debug/inc/dbgipceventtypes.h b/src/coreclr/debug/inc/dbgipceventtypes.h index 43411e02cd2bf8..e0b5cf45fd885e 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_OWNER, 0x025C) +IPC_EVENT_TYPE0(DB_IPCE_DEBUGGER_LAST ,0x025D) // The last event from the debugger diff --git a/src/coreclr/inc/CrstTypes.def b/src/coreclr/inc/CrstTypes.def index 1f5932d2ef8372..aece1015e30135 100644 --- a/src/coreclr/inc/CrstTypes.def +++ b/src/coreclr/inc/CrstTypes.def @@ -188,6 +188,9 @@ Crst ExecutableAllocatorLock AcquiredAfter LoaderHeap UMEntryThunkFreeListLock COMCallWrapper End +Crst ExternalMemoryHandle +End + Crst FCall AcquiredBefore LoaderHeap End diff --git a/src/coreclr/inc/crsttypes_generated.h b/src/coreclr/inc/crsttypes_generated.h index 150c3b25d865f1..559dc30f574a83 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 + 0, // 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/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..4568cc787c54af 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" 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/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 25c22dc3a928fc..b7a8497c26ea7f 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -483,6 +483,14 @@ CDAC_TYPE_FIELD(AppDomain, TYPE(ArrayListBase), AssemblyList, cdac_data::FriendlyName) 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) @@ -1790,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) @@ -1887,6 +1896,7 @@ 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, c1) CDAC_GLOBAL_CONTRACT(ManagedTypeSource, c1) diff --git a/src/coreclr/vm/externalmemoryhandle.cpp b/src/coreclr/vm/externalmemoryhandle.cpp new file mode 100644 index 00000000000000..71a7be0882cb16 --- /dev/null +++ b/src/coreclr/vm/externalmemoryhandle.cpp @@ -0,0 +1,118 @@ +// 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" + +CrstStatic 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); + + delete handle; +} + +#endif // !DACCESS_COMPILE + +void ExternalMemoryHandle::GCScanRoots(promote_func *fn, ScanContext *sc) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + } + CONTRACTL_END; + + // 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); + } +} + +void ExternalMemoryHandle::GCScanRoot(promote_func *fn, ScanContext *sc) +{ + CONTRACTL + { + NOTHROW; + GC_NOTRIGGER; + } + CONTRACTL_END; + + PTR_VOID 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); + } + + 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))); + } +} diff --git a/src/coreclr/vm/externalmemoryhandle.h b/src/coreclr/vm/externalmemoryhandle.h new file mode 100644 index 00000000000000..d26c1f5e3d4350 --- /dev/null +++ b/src/coreclr/vm/externalmemoryhandle.h @@ -0,0 +1,58 @@ +// 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" + +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_pNext(nullptr), m_pMT(pMT), m_pMemory(pMemory), m_gcFlags(gcFlags) + { + } + + void GCScanRoot(promote_func *fn, ScanContext *sc); + + // Next pointer for SList linkage. + PTR_ExternalMemoryHandle m_pNext; + +#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)); +#endif + static void GCScanRoots(promote_func *fn, ScanContext *sc); + +private: + PTR_MethodTable m_pMT; + PTR_VOID m_pMemory; + UINT m_gcFlags; + + static CrstStatic s_crst; + static SListTail s_handles; +}; + +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); + 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 541b478acb1173..9fc9021371730a 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" @@ -334,6 +335,18 @@ void GCToEEInterface::GcScanRoots(promote_func* fn, int condemned, int max_gen, SystemDomain::EnumAllStaticGCRefs(fn, sc); } } + + // 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)) + { + // 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); + } } void GCToEEInterface::GcStartWork (int condemned, int max_gen) 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) { 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; } 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/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/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/ByRefPointerOffsetsReporter.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs new file mode 100644 index 00000000000000..7e15c1bd1366e9 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ByRefPointerOffsetsReporter.cs @@ -0,0 +1,88 @@ +// 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 uint _pointerSize; + + public ByRefPointerOffsetsReporter(Target target) + { + _rts = target.Contracts.RuntimeTypeSystem; + _pointerSize = (uint)target.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 baseOffset + fieldOffset; + } + } + + public IEnumerable Find(ITypeHandle typeHandle) + => Find(typeHandle, baseOffset: 0, 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..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 @@ -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,14 @@ 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(_target); + foreach (ulong offset in reporter.Find(layoutType)) + tokens[checked(arg.Offset + (int)offset)] = GCRefMapToken.Interior; + } emitted = true; } @@ -610,103 +596,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 new file mode 100644 index 00000000000000..28aa62863fb57f --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExternalMemoryHandles_1.cs @@ -0,0 +1,110 @@ +// 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 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); + } + + IReadOnlyList IExternalMemoryHandles.GetRoots(bool resolveInteriorPointers) + { + List roots = []; + TargetPointer headPointer = _target.ReadGlobalPointer(Constants.Globals.ExternalMemoryHandles); + TargetPointer current = _target.ReadPointer(headPointer); + + HashSet visited = []; + while (current != TargetPointer.Null) + { + if (!visited.Add(current)) + 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, typeHandle, handle.Memory, resolveInteriorPointers); + } + else if (handle.GCFlags != 0) + { + AddInteriorRoot(roots, handle.Memory, resolveInteriorPointers); + } + else + { + roots.Add(new ExternalMemoryHandleRootData { Address = handle.Memory }); + } + + current = handle.Next; + } + + return roots; + } + + private void AddValueTypeRoots( + List roots, + ITypeHandle typeHandle, + TargetPointer memory, + bool resolveInteriorPointers) + { + ulong pointerSize = (ulong)_target.PointerSize; + + if (_rts.IsByRefLike(typeHandle)) + { + ByRefPointerOffsetsReporter reporter = new(_target); + foreach (ulong offset in reporter.Find(typeHandle)) + AddInteriorRoot(roots, memory + offset, resolveInteriorPointers); + } + + 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) + { + TargetPointer obj = _target.ReadPointer(slotAddress.Value); + ulong invalidPointer = _target.PointerSize == 8 ? ulong.MaxValue : uint.MaxValue; + if (obj == TargetPointer.Null || obj.Value == invalidPointer) + return; + + if (resolveInteriorPointers) + { + obj = _interiorPointerResolver.Resolve(obj); + if (obj == TargetPointer.Null) + return; + } + + roots.Add(new ExternalMemoryHandleRootData + { + IsInteriorPointer = true, + Address = slotAddress, + Object = obj, + }); + } +} 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..1e36b4ea04ed33 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -16,6 +16,7 @@ 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("c1", static t => new EcmaMetadata_1(t)); registry.Register("c1", static t => new DacStreams_1(t)); @@ -133,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 @@ -198,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.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..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 @@ -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,30 @@ private bool TryMapHandle(HandleData handle, out CorGCReferenceType dwType, out } } + // Walks the external memory handles registered with the current AppDomain. + private IEnumerable WalkExternalMemoryHandles() + { + if (!_target.Contracts.TryGetContract(out IExternalMemoryHandles externalMemoryHandles)) + yield break; + + foreach (ExternalMemoryHandleRootData root in externalMemoryHandles.GetRoots(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..63c5c09d25774b 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(); + 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(externalMemoryHandles.GetRoots(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/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..2bf671d157b514 --- /dev/null +++ b/src/native/managed/cdac/tests/StressTests/Debuggees/InlineArrayByRefLike/InlineArrayByRefLike.il @@ -0,0 +1,164 @@ +// 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 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) = { + int32(2) + } + .custom instance void [System.Runtime]System.Runtime.CompilerServices.IsByRefLikeAttribute::.ctor() = ( + 01 00 00 00 + ) + + .field assembly valuetype ByRef _element0 + + .method public hidebysig specialname rtspecialname instance void .ctor( + valuetype ByRef first, + valuetype ByRef second) cil managed + { + .maxstack 2 + + ldarg.0 + ldarg.1 + stfld valuetype ByRef InlineArrayOfByRefs::_element0 + + ldarg.0 + ldflda valuetype ByRef InlineArrayOfByRefs::_element0 + sizeof valuetype ByRef + conv.i + add + ldarg.2 + stobj valuetype ByRef + ret + } +} + +.class private auto ansi abstract sealed beforefieldinit Program + extends [System.Runtime]System.Object +{ + .field private static object s_sink + + .method private hidebysig static int32 SumValues( + valuetype InlineArrayOfByRefs values) cil managed noinlining + { + .maxstack 2 + .locals init ( + valuetype ByRef first, + valuetype ByRef second + ) + + ldc.i4 65536 + newarr [System.Runtime]System.Byte + stsfld object Program::s_sink + + ldarga.s values + ldfld valuetype ByRef InlineArrayOfByRefs::_element0 + stloc.0 + + ldarga.s values + ldflda valuetype ByRef InlineArrayOfByRefs::_element0 + sizeof valuetype ByRef + conv.i + add + ldobj valuetype ByRef + stloc.1 + + ldloca.s first + call instance int32 ByRef::GetValue() + ldloca.s second + call instance int32 ByRef::GetValue() + add + ret + } + + .method public hidebysig static int32 Main() cil managed + { + .entrypoint + .maxstack 3 + .locals init ( + int32[] firstValues, + int32[] secondValues, + valuetype ByRef first, + valuetype ByRef second, + valuetype InlineArrayOfByRefs values + ) + + ldc.i4.1 + newarr [System.Runtime]System.Int32 + stloc.0 + ldloc.0 + ldc.i4.0 + ldc.i4.s 40 + stelem.i4 + + ldc.i4.1 + newarr [System.Runtime]System.Int32 + stloc.1 + ldloc.1 + ldc.i4.0 + ldc.i4.s 60 + stelem.i4 + + ldloc.0 + 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 + ldloc.3 + newobj instance void InlineArrayOfByRefs::.ctor( + valuetype ByRef, + valuetype ByRef) + stloc.s 4 + + ldloc.s 4 + call int32 Program::SumValues(valuetype InlineArrayOfByRefs) + 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..de4a531592cb87 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 `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. | 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 - + + 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/ContractDescriptor/TargetTests.cs b/src/native/managed/cdac/tests/UnitTests/ContractDescriptor/TargetTests.cs index 2035a8076134e4..66ded94c2f11f7 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", @@ -491,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/ExternalMemoryHandlesTests.cs b/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs new file mode 100644 index 00000000000000..5e3d822e0bc296 --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/ExternalMemoryHandlesTests.cs @@ -0,0 +1,119 @@ +// 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 ExternalMemoryHandlesTests +{ + private static readonly MockTarget.Architecture Arch = new() { IsLittleEndian = true, Is64Bit = true }; + + private const ulong ExternalMemoryHandlesHeadSlotAddr = 0x0500; + 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(bool hasHandles = 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 gc = new Mock(); + + var targetBuilder = new TestPlaceholderTarget.Builder(Arch) + .AddGlobals((Constants.Globals.ExternalMemoryHandles, ExternalMemoryHandlesHeadSlotAddr)) + .AddGlobals((nameof(Constants.Globals.ObjectToMethodTableUnmask), 0ul)) + .AddTypes(new Dictionary + { + [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() } }, + } + }, + [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); + + // 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)); + + // 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 GetRoots_WalksChain() + { + TestPlaceholderTarget target = CreateTarget(); + IExternalMemoryHandles externalMemoryHandles = target.Contracts.ExternalMemoryHandles; + + IReadOnlyList roots = externalMemoryHandles.GetRoots(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 GetRoots_NoHandles_ReturnsEmpty() + { + TestPlaceholderTarget target = CreateTarget(hasHandles: false); + IExternalMemoryHandles externalMemoryHandles = target.Contracts.ExternalMemoryHandles; + + IReadOnlyList roots = externalMemoryHandles.GetRoots(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..85aafb741ccf2e --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandleTests.cs @@ -0,0 +1,340 @@ +// 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 const ulong ExternalMemoryHandlesHeadSlotAddr = 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((Constants.Globals.ExternalMemoryHandles, ExternalMemoryHandlesHeadSlotAddr)) + .AddTypes(new Dictionary + { + [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: "c1") + .AddMockContract(mockGC) + .AddMockContract(rts); + + builder.MemoryBuilder.AddHeapFragment(PointerFragment(ExternalMemoryHandlesHeadSlotAddr, 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); + + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts).Contracts.ExternalMemoryHandles; + + 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); + } + + [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); + + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 1, rts, fragments, gc).Contracts.ExternalMemoryHandles; + + 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); + } + + [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); + + IExternalMemoryHandles externalMemoryHandles = CreateTarget( + new TargetPointer(MemoryAddr), + gcFlags: 1, + rts, + [PointerFragment(MemoryAddr, ObjectAddr)]).Contracts.ExternalMemoryHandles; + + Assert.Empty(externalMemoryHandles.GetRoots(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)]); + + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts).Contracts.ExternalMemoryHandles; + + 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)); + } + + [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); + + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts, fragments, gc).Contracts.ExternalMemoryHandles; + + ExternalMemoryHandleRootData root = Assert.Single(externalMemoryHandles.GetRoots(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); + + IExternalMemoryHandles externalMemoryHandles = CreateTarget(new TargetPointer(MemoryAddr), gcFlags: 0, rts, fragments, gc).Contracts.ExternalMemoryHandles; + + IReadOnlyList roots = externalMemoryHandles.GetRoots(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); + IExternalMemoryHandles externalMemoryHandles = CreateTarget(TargetPointer.Null, gcFlags: 0, rts, hasHandle: false).Contracts.ExternalMemoryHandles; + + Assert.Empty(externalMemoryHandles.GetRoots(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); + var externalMemoryHandles = new Mock(); + externalMemoryHandles.Setup(c => c.GetRoots(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(externalMemoryHandles) + .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); + }); + } + + [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()); + } +} diff --git a/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs new file mode 100644 index 00000000000000..66284f84024a15 --- /dev/null +++ b/src/native/managed/cdac/tests/UnitTests/RefWalkExternalMemoryHandlesContractTests.cs @@ -0,0 +1,120 @@ +// 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 RefWalkExternalMemoryHandlesContractTests +{ + 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 = 0x0600; + private const ulong HandleAddr = 0x2000; + private const ulong MethodTableAddr = 0x9000; + private const ulong MemoryAddr = 0x9500; + + private static TestPlaceholderTarget CreateTarget(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)) + .AddGlobals((Constants.Globals.ExternalMemoryHandles, ExternalMemoryHandlesHeadSlotAddr)) + .AddTypes(new Dictionary + { + [DataType.AppDomain] = new(), + [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: "c1") + .AddContract(version: "c1") + .AddMockContract(mockGC) + .AddMockContract(rts); + + // AppDomain* static slot -> the AppDomain instance (used by ILoader.GetAppDomain for vmDomain) + targetBuilder.MemoryBuilder.AddHeapFragment(PointerFragment(helpers, AppDomainStaticSlotAddr, AppDomainAddr)); + + // 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)); + + 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 ExternalMemoryHandlesContract_ContributesRoot() + { + 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(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); + } +}