From 10891697a658dae220a124a808a72f3961ca6cf7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 03:55:20 +0000 Subject: [PATCH 1/9] Initial plan From a4cb8e46e0fb4552e3cfbcae97c78118ec04bbfd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 04:12:42 +0000 Subject: [PATCH 2/9] cDAC: Implement SosDacImpl GetRCWData Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- src/coreclr/vm/comcache.h | 1 + .../vm/datadescriptor/datadescriptor.inc | 7 +++ src/coreclr/vm/runtimecallablewrapper.h | 6 ++ .../Contracts/IBuiltInCOM.cs | 14 +++++ .../Contracts/BuiltInCOM_1.cs | 55 +++++++++++++++++-- .../Data/CtxEntry.cs | 2 + .../Data/RCW.cs | 12 ++++ 7 files changed, 92 insertions(+), 5 deletions(-) diff --git a/src/coreclr/vm/comcache.h b/src/coreclr/vm/comcache.h index 2f02827f3879a2..f3840aa7acda50 100644 --- a/src/coreclr/vm/comcache.h +++ b/src/coreclr/vm/comcache.h @@ -98,6 +98,7 @@ template<> struct cdac_data { static constexpr size_t STAThread = offsetof(CtxEntry, m_pSTAThread); + static constexpr size_t CtxCookie = offsetof(CtxEntry, m_pCtxCookie); }; //============================================================== diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 61b49340a39c2d..dc809eee13851e 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1116,11 +1116,18 @@ CDAC_TYPE_FIELD(RCW, /*uint32*/, Flags, cdac_data::Flags) CDAC_TYPE_FIELD(RCW, /*pointer*/, CtxCookie, cdac_data::CtxCookie) CDAC_TYPE_FIELD(RCW, /*pointer*/, CtxEntry, cdac_data::CtxEntry) CDAC_TYPE_FIELD(RCW, /*inline array*/, InterfaceEntries, cdac_data::InterfaceEntries) +CDAC_TYPE_FIELD(RCW, /*pointer*/, IdentityPointer, cdac_data::IdentityPointer) +CDAC_TYPE_FIELD(RCW, /*uint32*/, SyncBlockIndex, cdac_data::SyncBlockIndex) +CDAC_TYPE_FIELD(RCW, /*pointer*/, VTablePtr, cdac_data::VTablePtr) +CDAC_TYPE_FIELD(RCW, /*pointer*/, CreatorThread, cdac_data::CreatorThread) +CDAC_TYPE_FIELD(RCW, /*uint32*/, RefCount, cdac_data::RefCount) +CDAC_TYPE_FIELD(RCW, /*pointer*/, UnknownPointer, cdac_data::UnknownPointer) CDAC_TYPE_END(RCW) CDAC_TYPE_BEGIN(CtxEntry) CDAC_TYPE_INDETERMINATE(CtxEntry) CDAC_TYPE_FIELD(CtxEntry, /*pointer*/, STAThread, cdac_data::STAThread) +CDAC_TYPE_FIELD(CtxEntry, /*pointer*/, CtxCookie, cdac_data::CtxCookie) CDAC_TYPE_END(CtxEntry) CDAC_TYPE_BEGIN(InterfaceEntry) diff --git a/src/coreclr/vm/runtimecallablewrapper.h b/src/coreclr/vm/runtimecallablewrapper.h index fee5c74177ee78..fe113b46e3b9dc 100644 --- a/src/coreclr/vm/runtimecallablewrapper.h +++ b/src/coreclr/vm/runtimecallablewrapper.h @@ -591,6 +591,12 @@ struct cdac_data static constexpr size_t CtxCookie = offsetof(RCW, m_UnkEntry) + offsetof(IUnkEntry, m_pCtxCookie); static constexpr size_t CtxEntry = offsetof(RCW, m_UnkEntry) + offsetof(IUnkEntry, m_pCtxEntry); static constexpr size_t InterfaceEntries = offsetof(RCW, m_aInterfaceEntries); + static constexpr size_t IdentityPointer = offsetof(RCW, m_pIdentity); + static constexpr size_t SyncBlockIndex = offsetof(RCW, m_SyncBlockIndex); + static constexpr size_t VTablePtr = offsetof(RCW, m_vtablePtr); + static constexpr size_t CreatorThread = offsetof(RCW, m_pCreatorThread); + static constexpr size_t RefCount = offsetof(RCW, m_cbRefCount); + static constexpr size_t UnknownPointer = offsetof(RCW, m_UnkEntry) + offsetof(IUnkEntry, m_pUnknown); }; inline RCW::CreationFlags operator|(RCW::CreationFlags lhs, RCW::CreationFlags rhs) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IBuiltInCOM.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IBuiltInCOM.cs index a355c5601946f9..86884cc32df22d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IBuiltInCOM.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IBuiltInCOM.cs @@ -18,6 +18,19 @@ public record struct RCWCleanupInfo( TargetPointer STAThread, bool IsFreeThreaded); +public record struct RCWData( + TargetPointer IdentityPointer, + TargetPointer UnknownPointer, + TargetPointer ManagedObject, + TargetPointer VTablePtr, + TargetPointer CreatorThread, + TargetPointer CtxCookie, + uint RefCount, + bool IsAggregated, + bool IsContained, + bool IsFreeThreaded, + bool IsDisconnected); + public interface IBuiltInCOM : IContract { static string IContract.Name { get; } = nameof(BuiltInCOM); @@ -32,6 +45,7 @@ public interface IBuiltInCOM : IContract IEnumerable GetRCWCleanupList(TargetPointer cleanupListPtr) => throw new NotImplementedException(); IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw) => throw new NotImplementedException(); TargetPointer GetRCWContext(TargetPointer rcw) => throw new NotImplementedException(); + RCWData GetRCWData(TargetPointer rcw) => throw new NotImplementedException(); } public readonly struct BuiltInCOM : IBuiltInCOM diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs index 521a7e21421376..6a3458d7424f25 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs @@ -23,10 +23,16 @@ private enum ComWrapperFlags : uint { Slot_Basic = 0, } - // Matches the bit position of m_MarshalingType within RCW::RCWFlags::m_dwFlags. - private const int MarshalingTypeShift = 7; - private const uint MarshalingTypeMask = 0x3u << MarshalingTypeShift; - private const uint MarshalingTypeFreeThreaded = 2u; + // Mirrors RCW::RCWFlags bits in src/coreclr/vm/runtimecallablewrapper.h. + // [cDAC] [BuiltInCOM]: Contract depends on these bit positions within m_dwFlags. + private const uint URTAggregatedMask = 0x10u; // bit 4: m_fURTAggregated + private const uint URTContainedMask = 0x20u; // bit 5: m_fURTContained + private const uint MarshalingTypeMask = 0x180u; // bits 7-8: m_MarshalingType + private const uint MarshalingTypeFreeThreadedValue = 0x100u; // MarshalingType_FreeThreaded (2) in bits 7-8 + + // Sentinel value written to IUnkEntry::m_pUnknown when an RCW is disconnected from its COM object. + // Mirrors the value 0xBADF00D used in IUnkEntry::IsDisconnected in src/coreclr/vm/comcache.h. + private const ulong DisconnectedSentinel = 0xBADF00Du; internal BuiltInCOM_1(Target target) { @@ -173,7 +179,7 @@ public IEnumerable GetRCWCleanupList(TargetPointer cleanupListPt while (bucketPtr != TargetPointer.Null) { Data.RCW bucket = _target.ProcessedData.GetOrAdd(bucketPtr); - bool isFreeThreaded = (bucket.Flags & MarshalingTypeMask) == MarshalingTypeFreeThreaded << MarshalingTypeShift; + bool isFreeThreaded = (bucket.Flags & MarshalingTypeMask) == MarshalingTypeFreeThreadedValue; TargetPointer ctxCookie = bucket.CtxCookie; TargetPointer staThread = GetSTAThread(bucket); @@ -217,4 +223,43 @@ public TargetPointer GetRCWContext(TargetPointer rcw) return rcwData.CtxCookie; } + + public RCWData GetRCWData(TargetPointer rcw) + { + Data.RCW rcwData = _target.ProcessedData.GetOrAdd(rcw); + + TargetPointer managedObject = TargetPointer.Null; + if (rcwData.SyncBlockIndex != 0) + { + ISyncBlock syncBlock = _target.Contracts.SyncBlock; + managedObject = syncBlock.GetSyncBlockObject(rcwData.SyncBlockIndex); + } + + return new RCWData( + IdentityPointer: rcwData.IdentityPointer, + UnknownPointer: rcwData.UnknownPointer, + ManagedObject: managedObject, + VTablePtr: rcwData.VTablePtr, + CreatorThread: rcwData.CreatorThread, + CtxCookie: rcwData.CtxCookie, + RefCount: rcwData.RefCount, + IsAggregated: (rcwData.Flags & URTAggregatedMask) != 0, + IsContained: (rcwData.Flags & URTContainedMask) != 0, + IsFreeThreaded: (rcwData.Flags & MarshalingTypeMask) == MarshalingTypeFreeThreadedValue, + IsDisconnected: IsRCWDisconnected(rcwData)); + } + + // Mirrors IUnkEntry::IsDisconnected in src/coreclr/vm/comcache.h. + private bool IsRCWDisconnected(Data.RCW rcw) + { + if (rcw.UnknownPointer == DisconnectedSentinel) + return true; + + TargetPointer ctxEntryPtr = rcw.CtxEntry & ~(ulong)1; + if (ctxEntryPtr == TargetPointer.Null) + return false; + + Data.CtxEntry ctxEntry = _target.ProcessedData.GetOrAdd(ctxEntryPtr); + return rcw.CtxCookie != ctxEntry.CtxCookie; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CtxEntry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CtxEntry.cs index 44ad40ddd875f7..f67fa847424544 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CtxEntry.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/CtxEntry.cs @@ -11,7 +11,9 @@ public CtxEntry(Target target, TargetPointer address) Target.TypeInfo type = target.GetTypeInfo(DataType.CtxEntry); STAThread = target.ReadPointer(address + (ulong)type.Fields[nameof(STAThread)].Offset); + CtxCookie = target.ReadPointer(address + (ulong)type.Fields[nameof(CtxCookie)].Offset); } public TargetPointer STAThread { get; init; } + public TargetPointer CtxCookie { get; init; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RCW.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RCW.cs index 422a899090c23b..2022ef69a101ea 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RCW.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/RCW.cs @@ -17,6 +17,12 @@ public RCW(Target target, TargetPointer address) Flags = target.Read(address + (ulong)type.Fields[nameof(Flags)].Offset); CtxCookie = target.ReadPointer(address + (ulong)type.Fields[nameof(CtxCookie)].Offset); CtxEntry = target.ReadPointer(address + (ulong)type.Fields[nameof(CtxEntry)].Offset); + IdentityPointer = target.ReadPointer(address + (ulong)type.Fields[nameof(IdentityPointer)].Offset); + SyncBlockIndex = target.Read(address + (ulong)type.Fields[nameof(SyncBlockIndex)].Offset); + VTablePtr = target.ReadPointer(address + (ulong)type.Fields[nameof(VTablePtr)].Offset); + CreatorThread = target.ReadPointer(address + (ulong)type.Fields[nameof(CreatorThread)].Offset); + RefCount = target.Read(address + (ulong)type.Fields[nameof(RefCount)].Offset); + UnknownPointer = target.ReadPointer(address + (ulong)type.Fields[nameof(UnknownPointer)].Offset); TargetPointer interfaceEntriesAddr = address + (ulong)type.Fields[nameof(InterfaceEntries)].Offset; uint cacheSize = target.ReadGlobal(Constants.Globals.RCWInterfaceCacheSize); @@ -35,5 +41,11 @@ public RCW(Target target, TargetPointer address) public uint Flags { get; init; } public TargetPointer CtxCookie { get; init; } public TargetPointer CtxEntry { get; init; } + public TargetPointer IdentityPointer { get; init; } + public uint SyncBlockIndex { get; init; } + public TargetPointer VTablePtr { get; init; } + public TargetPointer CreatorThread { get; init; } + public uint RefCount { get; init; } + public TargetPointer UnknownPointer { get; init; } public List InterfaceEntries { get; } = new List(); } From c5f8cda358757b8f5b3297ab9598c2af0f8dbfde Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 04:24:18 +0000 Subject: [PATCH 3/9] cDAC: Implement ISOSDacInterface::GetRCWData Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- .../ISOSDacInterface.cs | 23 ++++++- .../SOSDacImpl.cs | 62 ++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs index b19a0c900d175c..3c475c10b79928 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -394,6 +394,27 @@ public struct DacpCOMInterfacePointerData public ClrDataAddress comContext; } +// Mirrors struct DacpRCWData in src/coreclr/inc/dacprivate.h. +// Size must remain 0x58 bytes (enforced by static_assert in the native code). +public struct DacpRCWData +{ + public ClrDataAddress identityPointer; + public ClrDataAddress unknownPointer; + public ClrDataAddress managedObject; + public ClrDataAddress jupiterObject; + public ClrDataAddress vtablePtr; + public ClrDataAddress creatorThread; + public ClrDataAddress ctxCookie; + public int refCount; + public int interfaceCount; + public int isJupiterObject; // BOOL + public int supportsIInspectable; // BOOL + public int isAggregated; // BOOL + public int isContained; // BOOL + public int isFreeThreaded; // BOOL + public int isDisconnected; // BOOL +} + [GeneratedComInterface] [Guid("286CA186-E763-4F61-9760-487D43AE4341")] public unsafe partial interface ISOSEnum @@ -758,7 +779,7 @@ public unsafe partial interface ISOSDacInterface // COM [PreserveSig] - int GetRCWData(ClrDataAddress addr, /*struct DacpRCWData */ void* data); + int GetRCWData(ClrDataAddress addr, DacpRCWData* data); [PreserveSig] int GetRCWInterfaces(ClrDataAddress rcw, uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] DacpCOMInterfacePointerData[]? interfaces, uint* pNeeded); [PreserveSig] diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 5b3648a94e496a..6bedd9d8ebd0dd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -3388,8 +3388,66 @@ int ISOSDacInterface.GetPrivateBinPaths(ClrDataAddress appDomain, int count, cha return hr; } - int ISOSDacInterface.GetRCWData(ClrDataAddress addr, void* data) - => _legacyImpl is not null ? _legacyImpl.GetRCWData(addr, data) : HResults.E_NOTIMPL; + int ISOSDacInterface.GetRCWData(ClrDataAddress addr, DacpRCWData* data) + { + int hr = HResults.S_OK; + try + { + if (addr == 0) + throw new ArgumentException(); + if (data is null) + throw new ArgumentException(); + + *data = default; + + TargetPointer rcwPtr = addr.ToTargetPointer(_target); + IBuiltInCOM builtInCom = _target.Contracts.BuiltInCOM; + Contracts.RCWData rcwData = builtInCom.GetRCWData(rcwPtr); + + data->identityPointer = rcwData.IdentityPointer.ToClrDataAddress(_target); + data->unknownPointer = rcwData.UnknownPointer.ToClrDataAddress(_target); + data->managedObject = rcwData.ManagedObject.ToClrDataAddress(_target); + data->vtablePtr = rcwData.VTablePtr.ToClrDataAddress(_target); + data->creatorThread = rcwData.CreatorThread.ToClrDataAddress(_target); + data->ctxCookie = rcwData.CtxCookie.ToClrDataAddress(_target); + data->refCount = (int)rcwData.RefCount; + data->interfaceCount = builtInCom.GetRCWInterfaces(rcwPtr).Count(); + data->isAggregated = rcwData.IsAggregated ? 1 : 0; + data->isContained = rcwData.IsContained ? 1 : 0; + data->isFreeThreaded = rcwData.IsFreeThreaded ? 1 : 0; + data->isDisconnected = rcwData.IsDisconnected ? 1 : 0; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl is not null) + { + DacpRCWData dataLocal; + int hrLocal = _legacyImpl.GetRCWData(addr, &dataLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert(data->identityPointer == dataLocal.identityPointer, $"cDAC: {data->identityPointer:x}, DAC: {dataLocal.identityPointer:x}"); + Debug.Assert(data->unknownPointer == dataLocal.unknownPointer, $"cDAC: {data->unknownPointer:x}, DAC: {dataLocal.unknownPointer:x}"); + Debug.Assert(data->managedObject == dataLocal.managedObject, $"cDAC: {data->managedObject:x}, DAC: {dataLocal.managedObject:x}"); + Debug.Assert(data->vtablePtr == dataLocal.vtablePtr, $"cDAC: {data->vtablePtr:x}, DAC: {dataLocal.vtablePtr:x}"); + Debug.Assert(data->creatorThread == dataLocal.creatorThread, $"cDAC: {data->creatorThread:x}, DAC: {dataLocal.creatorThread:x}"); + Debug.Assert(data->ctxCookie == dataLocal.ctxCookie, $"cDAC: {data->ctxCookie:x}, DAC: {dataLocal.ctxCookie:x}"); + Debug.Assert(data->refCount == dataLocal.refCount, $"cDAC: {data->refCount}, DAC: {dataLocal.refCount}"); + Debug.Assert(data->interfaceCount == dataLocal.interfaceCount, $"cDAC: {data->interfaceCount}, DAC: {dataLocal.interfaceCount}"); + Debug.Assert(data->isAggregated == dataLocal.isAggregated, $"cDAC: {data->isAggregated}, DAC: {dataLocal.isAggregated}"); + Debug.Assert(data->isContained == dataLocal.isContained, $"cDAC: {data->isContained}, DAC: {dataLocal.isContained}"); + Debug.Assert(data->isFreeThreaded == dataLocal.isFreeThreaded, $"cDAC: {data->isFreeThreaded}, DAC: {dataLocal.isFreeThreaded}"); + Debug.Assert(data->isDisconnected == dataLocal.isDisconnected, $"cDAC: {data->isDisconnected}, DAC: {dataLocal.isDisconnected}"); + } + } +#endif + + return hr; + } int ISOSDacInterface.GetRCWInterfaces(ClrDataAddress rcw, uint count, [In, MarshalUsing(CountElementName = nameof(count)), Out] DacpCOMInterfacePointerData[]? interfaces, uint* pNeeded) { int hr = HResults.S_OK; From 6d48b7be2fd6319f9abf6a2793edd1e5f90e537d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 04:35:48 +0000 Subject: [PATCH 4/9] cDAC: Add DacpRCWData, implement SOSDacImpl.GetRCWData, add BuiltInCOM GetRCWData tests Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- .../managed/cdac/tests/BuiltInCOMTests.cs | 238 +++++++++++++++++- 1 file changed, 235 insertions(+), 3 deletions(-) diff --git a/src/native/managed/cdac/tests/BuiltInCOMTests.cs b/src/native/managed/cdac/tests/BuiltInCOMTests.cs index f2b9e86c81e5ef..bd051a2308a4e5 100644 --- a/src/native/managed/cdac/tests/BuiltInCOMTests.cs +++ b/src/native/managed/cdac/tests/BuiltInCOMTests.cs @@ -28,6 +28,12 @@ public class BuiltInCOMTests new(nameof(Data.RCW.CtxCookie), DataType.pointer), new(nameof(Data.RCW.CtxEntry), DataType.pointer), new(nameof(Data.RCW.InterfaceEntries), DataType.pointer), + new(nameof(Data.RCW.IdentityPointer), DataType.pointer), + new(nameof(Data.RCW.SyncBlockIndex), DataType.uint32), + new(nameof(Data.RCW.VTablePtr), DataType.pointer), + new(nameof(Data.RCW.CreatorThread), DataType.pointer), + new(nameof(Data.RCW.RefCount), DataType.uint32), + new(nameof(Data.RCW.UnknownPointer), DataType.pointer), ] }; @@ -41,17 +47,28 @@ public class BuiltInCOMTests ] }; + private static readonly MockDescriptors.TypeFields CtxEntryFields = new MockDescriptors.TypeFields() + { + DataType = DataType.CtxEntry, + Fields = + [ + new(nameof(Data.CtxEntry.STAThread), DataType.pointer), + new(nameof(Data.CtxEntry.CtxCookie), DataType.pointer), + ] + }; + private static void BuiltInCOMContractHelper( MockTarget.Architecture arch, Action> configure, - Action testCase) + Action testCase, + ISyncBlock? syncBlock = null) { TargetTestHelpers targetTestHelpers = new(arch); MockMemorySpace.Builder builder = new(targetTestHelpers); Dictionary types = MockDescriptors.GetTypesForTypeFields( targetTestHelpers, - [RCWFields, InterfaceEntryFields]); + [RCWFields, InterfaceEntryFields, CtxEntryFields]); configure(builder, targetTestHelpers, types); @@ -61,8 +78,10 @@ private static void BuiltInCOMContractHelper( ]; var target = new TestPlaceholderTarget(arch, builder.GetMemoryContext().ReadFromTarget, types, globals); + ISyncBlock syncBlockContract = syncBlock ?? Mock.Of(); target.SetContracts(Mock.Of( - c => c.BuiltInCOM == ((IContractFactory)new BuiltInCOMFactory()).CreateContract(target, 1))); + c => c.BuiltInCOM == ((IContractFactory)new BuiltInCOMFactory()).CreateContract(target, 1) + && c.SyncBlock == syncBlockContract)); testCase(target); } @@ -755,6 +774,219 @@ public void GetRCWInterfaces_EmptyCache_ReturnsEmpty(MockTarget.Architecture arc }); } + // Bit-flag constants mirroring BuiltInCOM_1 internal constants, used to construct Flags for GetRCWData tests. + private const uint RCWFlagAggregated = 0x10u; // URTAggregatedMask + private const uint RCWFlagContained = 0x20u; // URTContainedMask + private const uint RCWFlagFreeThreaded = 0x100u; // MarshalingTypeFreeThreadedValue + + /// + /// Allocates a full RCW mock with all fields needed for . + /// + private static TargetPointer AddFullRCW( + MockMemorySpace.Builder builder, + TargetTestHelpers helpers, + Dictionary types, + MockMemorySpace.BumpAllocator allocator, + TargetPointer identityPointer = default, + TargetPointer unknownPointer = default, + TargetPointer vtablePtr = default, + TargetPointer creatorThread = default, + TargetPointer ctxCookie = default, + TargetPointer ctxEntry = default, + uint syncBlockIndex = 0, + uint refCount = 0, + uint flags = 0) + { + Target.TypeInfo rcwTypeInfo = types[DataType.RCW]; + Target.TypeInfo entryTypeInfo = types[DataType.InterfaceEntry]; + uint entrySize = entryTypeInfo.Size!.Value; + uint entriesOffset = (uint)rcwTypeInfo.Fields[nameof(Data.RCW.InterfaceEntries)].Offset; + uint totalSize = entriesOffset + entrySize * TestRCWInterfaceCacheSize; + + MockMemorySpace.HeapFragment fragment = allocator.Allocate(totalSize, "Full RCW"); + Span data = fragment.Data; + + helpers.WritePointer(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.IdentityPointer)].Offset), identityPointer); + helpers.WritePointer(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.UnknownPointer)].Offset), unknownPointer); + helpers.WritePointer(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.VTablePtr)].Offset), vtablePtr); + helpers.WritePointer(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.CreatorThread)].Offset), creatorThread); + helpers.WritePointer(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.CtxCookie)].Offset), ctxCookie); + helpers.WritePointer(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.CtxEntry)].Offset), ctxEntry); + helpers.Write(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.SyncBlockIndex)].Offset), syncBlockIndex); + helpers.Write(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.RefCount)].Offset), refCount); + helpers.Write(data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.Flags)].Offset), flags); + + builder.AddHeapFragment(fragment); + return fragment.Address; + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWData_ReturnsScalarFields(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + TargetPointer expectedIdentity = new TargetPointer(0x1000_0000); + TargetPointer expectedVTable = new TargetPointer(0x2000_0000); + TargetPointer expectedThread = new TargetPointer(0x3000_0000); + TargetPointer expectedCookie = new TargetPointer(0x4000_0000); + uint expectedRefCount = 42; + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddFullRCW(builder, targetTestHelpers, types, allocator, + identityPointer: expectedIdentity, + vtablePtr: expectedVTable, + creatorThread: expectedThread, + ctxCookie: expectedCookie, + refCount: expectedRefCount); + }, + (target) => + { + RCWData result = target.Contracts.BuiltInCOM.GetRCWData(rcwAddress); + + Assert.Equal(expectedIdentity, result.IdentityPointer); + Assert.Equal(expectedVTable, result.VTablePtr); + Assert.Equal(expectedThread, result.CreatorThread); + Assert.Equal(expectedCookie, result.CtxCookie); + Assert.Equal(expectedRefCount, result.RefCount); + Assert.Equal(TargetPointer.Null, result.ManagedObject); + Assert.False(result.IsAggregated); + Assert.False(result.IsContained); + Assert.False(result.IsFreeThreaded); + Assert.False(result.IsDisconnected); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWData_FlagsAggregatedAndContained(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddFullRCW(builder, targetTestHelpers, types, allocator, + flags: RCWFlagAggregated | RCWFlagContained); + }, + (target) => + { + RCWData result = target.Contracts.BuiltInCOM.GetRCWData(rcwAddress); + + Assert.True(result.IsAggregated); + Assert.True(result.IsContained); + Assert.False(result.IsFreeThreaded); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWData_FlagsFreeThreaded(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddFullRCW(builder, targetTestHelpers, types, allocator, + flags: RCWFlagFreeThreaded); + }, + (target) => + { + RCWData result = target.Contracts.BuiltInCOM.GetRCWData(rcwAddress); + + Assert.True(result.IsFreeThreaded); + Assert.False(result.IsAggregated); + Assert.False(result.IsContained); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWData_IsDisconnected_Sentinel(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + const ulong DisconnectedSentinel = 0xBADF00D; + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddFullRCW(builder, targetTestHelpers, types, allocator, + unknownPointer: new TargetPointer(DisconnectedSentinel)); + }, + (target) => + { + RCWData result = target.Contracts.BuiltInCOM.GetRCWData(rcwAddress); + + Assert.True(result.IsDisconnected); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWData_IsDisconnected_CtxCookieMismatch(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + + // Allocate a CtxEntry whose CtxCookie differs from the RCW's CtxCookie. + Target.TypeInfo ctxTypeInfo = types[DataType.CtxEntry]; + MockMemorySpace.HeapFragment ctxFragment = allocator.Allocate(ctxTypeInfo.Size!.Value, "CtxEntry"); + TargetPointer ctxCookieInEntry = new TargetPointer(0xAAAA_0000); + builder.TargetTestHelpers.WritePointer( + ctxFragment.Data.AsSpan().Slice(ctxTypeInfo.Fields[nameof(Data.CtxEntry.CtxCookie)].Offset), + ctxCookieInEntry); + builder.AddHeapFragment(ctxFragment); + + TargetPointer ctxCookieInRcw = new TargetPointer(0xBBBB_0000); // different from entry + rcwAddress = AddFullRCW(builder, targetTestHelpers, types, allocator, + ctxCookie: ctxCookieInRcw, + ctxEntry: ctxFragment.Address); // bit 0 clear → not null, not adjusted + }, + (target) => + { + RCWData result = target.Contracts.BuiltInCOM.GetRCWData(rcwAddress); + + Assert.True(result.IsDisconnected); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWData_ManagedObject_ResolvedViaSyncBlockIndex(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + TargetPointer expectedManagedObject = new TargetPointer(0xDEAD_BEEF_0000UL); + const uint syncBlockIndex = 3; + + var mockSyncBlock = new Mock(); + mockSyncBlock.Setup(s => s.GetSyncBlockObject(syncBlockIndex)).Returns(expectedManagedObject); + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddFullRCW(builder, targetTestHelpers, types, allocator, + syncBlockIndex: syncBlockIndex); + }, + (target) => + { + RCWData result = target.Contracts.BuiltInCOM.GetRCWData(rcwAddress); + + Assert.Equal(expectedManagedObject, result.ManagedObject); + }, + syncBlock: mockSyncBlock.Object); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetRCWContext_ReturnsCtxCookie(MockTarget.Architecture arch) From a680e614085c9379db1fa341eea6e57ae0541096 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 04:59:38 +0000 Subject: [PATCH 5/9] cDAC: Add GetRCWData dump test to RCWInterfacesDumpTests Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- .../tests/DumpTests/RCWInterfacesDumpTests.cs | 73 ++++++++++++++----- .../managed/cdac/tests/DumpTests/README.md | 2 + 2 files changed, 56 insertions(+), 19 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs index 52fb2520eec033..24cec63ddd62bb 100644 --- a/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs @@ -9,7 +9,7 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// -/// Dump-based integration tests for the BuiltInCOM contract's GetRCWInterfaces API. +/// Dump-based integration tests for the BuiltInCOM contract's RCW APIs. /// Uses the RCWInterfaces debuggee which creates a COM RCW and populates the /// inline interface entry cache before crashing. /// @@ -18,25 +18,16 @@ public class RCWInterfacesDumpTests : DumpTestBase protected override string DebuggeeName => "RCWInterfaces"; protected override string DumpType => "full"; - [ConditionalTheory] - [MemberData(nameof(TestConfigurations))] - [SkipOnOS(IncludeOnly = "windows", Reason = "COM interop (RCW) is only supported on Windows")] - public void GetRCWInterfaces_FindsRCWAndEnumeratesInterfaces(TestConfiguration config) + /// + /// Walks all strong GC handles and returns the first RCW pointer found. + /// Returns if no RCW-bearing object is found. + /// + private TargetPointer FindFirstRCW() { - InitializeDumpTest(config); - IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; - IObject objectContract = Target.Contracts.Object; IGC gcContract = Target.Contracts.GC; + IObject objectContract = Target.Contracts.Object; - Assert.NotNull(builtInCOM); - Assert.NotNull(objectContract); - Assert.NotNull(gcContract); - - // Walk all strong GC handles to find objects with COM data (RCWs) - List strongHandles = gcContract.GetHandles([HandleType.Strong]); - TargetPointer rcwPtr = TargetPointer.Null; - - foreach (HandleData handleData in strongHandles) + foreach (HandleData handleData in gcContract.GetHandles([HandleType.Strong])) { TargetPointer objectAddress = Target.ReadPointer(handleData.Handle); if (objectAddress == TargetPointer.Null) @@ -45,11 +36,22 @@ public void GetRCWInterfaces_FindsRCWAndEnumeratesInterfaces(TestConfiguration c if (objectContract.GetBuiltInComData(objectAddress, out TargetPointer rcw, out _, out _) && rcw != TargetPointer.Null) { - rcwPtr = rcw; - break; + return rcw; } } + return TargetPointer.Null; + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "COM interop (RCW) is only supported on Windows")] + public void GetRCWInterfaces_FindsRCWAndEnumeratesInterfaces(TestConfiguration config) + { + InitializeDumpTest(config); + IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; + + TargetPointer rcwPtr = FindFirstRCW(); Assert.NotEqual(TargetPointer.Null, rcwPtr); // Assert that the cookie is not null @@ -72,4 +74,37 @@ public void GetRCWInterfaces_FindsRCWAndEnumeratesInterfaces(TestConfiguration c Assert.NotEqual(TargetPointer.Null, unk); } } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "COM interop (RCW) is only supported on Windows")] + public void GetRCWData_ReturnsExpectedData(TestConfiguration config) + { + InitializeDumpTest(config); + IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; + + TargetPointer rcwPtr = FindFirstRCW(); + Assert.NotEqual(TargetPointer.Null, rcwPtr); + + RCWData data = builtInCOM.GetRCWData(rcwPtr); + + // The RCW wraps a live COM object, so its identity and vtable pointers must be valid. + Assert.NotEqual(TargetPointer.Null, data.IdentityPointer); + Assert.NotEqual(TargetPointer.Null, data.VTablePtr); + + // The debuggee pins the managed wrapper in a strong GC handle, so the managed + // object should be resolvable via the sync block index. + Assert.NotEqual(TargetPointer.Null, data.ManagedObject); + + // The RCW is alive at the time of the crash — it must not appear disconnected. + Assert.False(data.IsDisconnected); + + // The StdGlobalInterfaceTable RCW is a plain wrapper, not an aggregation or containment. + Assert.False(data.IsAggregated); + Assert.False(data.IsContained); + + // RefCount must be positive — the debuggee held a live reference. + Assert.True(data.RefCount > 0, + $"Expected positive RefCount, got {data.RefCount}"); + } } diff --git a/src/native/managed/cdac/tests/DumpTests/README.md b/src/native/managed/cdac/tests/DumpTests/README.md index 5c1d3a8503f6f7..20b500dd9fd324 100644 --- a/src/native/managed/cdac/tests/DumpTests/README.md +++ b/src/native/managed/cdac/tests/DumpTests/README.md @@ -37,6 +37,7 @@ features and then calls `Environment.FailFast()` to produce a crash dump. | SyncBlock | Sync block locks | Full | | CCWInterfaces | COM callable wrappers (CCW) on Windows | Full | | RCWCleanupList | STA-context RCW entries in g_pRCWCleanupList on Windows | Full | +| RCWInterfaces | COM RCW with populated interface entry cache on Windows | Full | The dump type is configured per-debuggee via the `DumpTypes` property in each debuggee's `.csproj` (default: `Heap`, set in `Debuggees/Directory.Build.props`). Debuggees that @@ -62,6 +63,7 @@ use. Tests are `[ConditionalTheory]` methods parameterized by `TestConfiguration | SyncBlockDumpTests | SyncBlock | SyncBlock | | BuiltInCOMDumpTests | BuiltInCOM | CCWInterfaces | | RCWCleanupListDumpTests | BuiltInCOM | RCWCleanupList | +| RCWInterfacesDumpTests | BuiltInCOM | RCWInterfaces | ### Runtime Versions From bc6648b08331e51e0f511f9f723899460c1940d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Mar 2026 05:14:34 +0000 Subject: [PATCH 6/9] cDAC: Replace raw RCW flag constants with RCWFlags [Flags] enum in BuiltInCOM_1 Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- .../Contracts/BuiltInCOM_1.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs index 6a3458d7424f25..b5f271afa84603 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs @@ -25,10 +25,14 @@ private enum ComWrapperFlags : uint } // Mirrors RCW::RCWFlags bits in src/coreclr/vm/runtimecallablewrapper.h. // [cDAC] [BuiltInCOM]: Contract depends on these bit positions within m_dwFlags. - private const uint URTAggregatedMask = 0x10u; // bit 4: m_fURTAggregated - private const uint URTContainedMask = 0x20u; // bit 5: m_fURTContained - private const uint MarshalingTypeMask = 0x180u; // bits 7-8: m_MarshalingType - private const uint MarshalingTypeFreeThreadedValue = 0x100u; // MarshalingType_FreeThreaded (2) in bits 7-8 + [System.Flags] + private enum RCWFlags : uint + { + URTAggregated = 0x010u, // bit 4: m_fURTAggregated + URTContained = 0x020u, // bit 5: m_fURTContained + MarshalingTypeMask = 0x180u, // bits 7-8: m_MarshalingType + MarshalingTypeFreeThreaded = 0x100u, // MarshalingType_FreeThreaded (2) in bits 7-8 + } // Sentinel value written to IUnkEntry::m_pUnknown when an RCW is disconnected from its COM object. // Mirrors the value 0xBADF00D used in IUnkEntry::IsDisconnected in src/coreclr/vm/comcache.h. @@ -179,7 +183,7 @@ public IEnumerable GetRCWCleanupList(TargetPointer cleanupListPt while (bucketPtr != TargetPointer.Null) { Data.RCW bucket = _target.ProcessedData.GetOrAdd(bucketPtr); - bool isFreeThreaded = (bucket.Flags & MarshalingTypeMask) == MarshalingTypeFreeThreadedValue; + bool isFreeThreaded = ((RCWFlags)bucket.Flags & RCWFlags.MarshalingTypeMask) == RCWFlags.MarshalingTypeFreeThreaded; TargetPointer ctxCookie = bucket.CtxCookie; TargetPointer staThread = GetSTAThread(bucket); @@ -243,9 +247,9 @@ public RCWData GetRCWData(TargetPointer rcw) CreatorThread: rcwData.CreatorThread, CtxCookie: rcwData.CtxCookie, RefCount: rcwData.RefCount, - IsAggregated: (rcwData.Flags & URTAggregatedMask) != 0, - IsContained: (rcwData.Flags & URTContainedMask) != 0, - IsFreeThreaded: (rcwData.Flags & MarshalingTypeMask) == MarshalingTypeFreeThreadedValue, + IsAggregated: ((RCWFlags)rcwData.Flags & RCWFlags.URTAggregated) != 0, + IsContained: ((RCWFlags)rcwData.Flags & RCWFlags.URTContained) != 0, + IsFreeThreaded: ((RCWFlags)rcwData.Flags & RCWFlags.MarshalingTypeMask) == RCWFlags.MarshalingTypeFreeThreaded, IsDisconnected: IsRCWDisconnected(rcwData)); } From 100956845766abdcaf1b1a0f9a6abc33a2d7be9f Mon Sep 17 00:00:00 2001 From: rcj1 Date: Wed, 11 Mar 2026 17:23:25 -0700 Subject: [PATCH 7/9] minor reorg etc --- docs/design/datacontracts/BuiltInCOM.md | 89 ++++++++++++++++--- src/coreclr/vm/runtimecallablewrapper.h | 1 + .../Contracts/BuiltInCOM_1.cs | 2 - .../ISOSDacInterface.cs | 40 ++++----- .../SOSDacImpl.cs | 17 ++-- .../{RCWInterfaces => RCW}/Program.cs | 42 +++++++-- .../RCWInterfaces.csproj => RCW/RCW.csproj} | 0 ...InterfacesDumpTests.cs => RCWDumpTests.cs} | 72 ++++++++++----- .../managed/cdac/tests/DumpTests/README.md | 4 +- 9 files changed, 195 insertions(+), 72 deletions(-) rename src/native/managed/cdac/tests/DumpTests/Debuggees/{RCWInterfaces => RCW}/Program.cs (68%) rename src/native/managed/cdac/tests/DumpTests/Debuggees/{RCWInterfaces/RCWInterfaces.csproj => RCW/RCW.csproj} (100%) rename src/native/managed/cdac/tests/DumpTests/{RCWInterfacesDumpTests.cs => RCWDumpTests.cs} (58%) diff --git a/docs/design/datacontracts/BuiltInCOM.md b/docs/design/datacontracts/BuiltInCOM.md index c9e28915501d52..3f1f054ea7e8ef 100644 --- a/docs/design/datacontracts/BuiltInCOM.md +++ b/docs/design/datacontracts/BuiltInCOM.md @@ -19,6 +19,19 @@ public record struct RCWCleanupInfo( TargetPointer STAThread, bool IsFreeThreaded); +public record struct RCWData( + TargetPointer IdentityPointer, + TargetPointer UnknownPointer, + TargetPointer ManagedObject, + TargetPointer VTablePtr, + TargetPointer CreatorThread, + TargetPointer CtxCookie, + uint RefCount, + bool IsAggregated, + bool IsContained, + bool IsFreeThreaded, + bool IsDisconnected); + public ulong GetRefCount(TargetPointer ccw); // Check whether the COM wrappers handle is weak. public bool IsHandleWeak(TargetPointer ccw); @@ -35,6 +48,8 @@ public IEnumerable GetRCWCleanupList(TargetPointer cleanupListPt public IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw); // Get the COM context cookie for an RCW. public TargetPointer GetRCWContext(TargetPointer rcw); +// Get detailed data about an RCW, including flags and the managed object reference. +RCWData GetRCWData(TargetPointer rcw); ``` ## Version 1 @@ -54,11 +69,18 @@ Data descriptors used: | `RCWCleanupList` | `FirstBucket` | Head of the bucket linked list | | `RCW` | `NextCleanupBucket` | Next bucket in the cleanup list | | `RCW` | `NextRCW` | Next RCW in the same bucket | -| `RCW` | `Flags` | Combined flags DWORD (contains `MarshalingType` bits) | +| `RCW` | `Flags` | Combined flags DWORD (contains marshaling type, aggregation, and containment bits) | | `RCW` | `CtxCookie` | COM context cookie for the RCW | | `RCW` | `CtxEntry` | Pointer to `CtxEntry` (bit 0 is a synchronization flag; must be masked off before use) | -| `CtxEntry` | `STAThread` | STA thread pointer for the context entry | +| `RCW` | `IdentityPointer` | Identity `IUnknown*` used to identify the underlying COM object | +| `RCW` | `SyncBlockIndex` | Index into the sync block table; used to resolve the managed object (0 = no managed object) | +| `RCW` | `VTablePtr` | Vtable pointer of the COM object | +| `RCW` | `CreatorThread` | Pointer to the thread that created this RCW | +| `RCW` | `RefCount` | Reference count of the RCW wrapper | +| `RCW` | `UnknownPointer` | Primary `IUnknown*` pointer for the RCW; a sentinel value indicates disconnection | | `RCW` | `InterfaceEntries` | Offset of the inline interface entry cache array within the RCW struct | +| `CtxEntry` | `STAThread` | STA thread pointer for the context entry | +| `CtxEntry` | `CtxCookie` | Context cookie stored in the context entry; compared against the RCW's cookie to detect disconnection | | `InterfaceEntry` | `MethodTable` | MethodTable pointer for the cached COM interface | | `InterfaceEntry` | `Unknown` | `IUnknown*` pointer for the cached COM interface | @@ -77,13 +99,12 @@ Global variables used: ### Contract Constants: | Name | Type | Purpose | Value | | --- | --- | --- | --- | -| `MarshalingTypeShift` | `int` | Bit position of `m_MarshalingType` within `RCW::RCWFlags::m_dwFlags` | `7` | -| `MarshalingTypeFreeThreaded` | `int` | Enum value for marshaling type within `RCW::RCWFlags::m_dwFlags` | `2` | +| `DisconnectedSentinel` | `ulong` | Sentinel value written to the unknown pointer when an RCW is disconnected | `0xBADF00D` | Contracts used: | Contract Name | | --- | -`None` +| `SyncBlock` | ``` csharp @@ -92,15 +113,19 @@ private enum CCWFlags IsHandleWeak = 0x4, } -// Mirrors enum Masks in src/coreclr/vm/comcallablewrapper.h private enum ComMethodTableFlags : ulong { LayoutComplete = 0x10, } -// MarshalingTypeShift = 7 matches the bit position of m_MarshalingType in RCW::RCWFlags::m_dwFlags -private const int MarshalingTypeShift = 7; -private const uint MarshalingTypeMask = 0x3u << MarshalingTypeShift; -private const uint MarshalingTypeFreeThreaded = 2u; // matches RCW::MarshalingType_FreeThreaded + +[Flags] +private enum RCWFlags : uint +{ + URTAggregated = 0x010u, + URTContained = 0x020u, + MarshalingTypeMask = 0x180u, + MarshalingTypeFreeThreaded = 0x100u, +} public ulong GetRefCount(TargetPointer address) { @@ -148,7 +173,7 @@ public IEnumerable GetRCWCleanupList(TargetPointer cleanupListPt while (bucketPtr != TargetPointer.Null) { uint flags = _target.Read(bucketPtr + /* RCW::Flags offset */); - bool isFreeThreaded = (flags & MarshalingTypeMask) == MarshalingTypeFreeThreaded << MarshalingTypeShift; + bool isFreeThreaded = ((RCWFlags)flags & RCWFlags.MarshalingTypeMask) == RCWFlags.MarshalingTypeFreeThreaded; TargetPointer ctxCookie = _target.ReadPointer(bucketPtr + /* RCW::CtxCookie offset */); // CtxEntry uses bit 0 for synchronization; strip it before dereferencing. @@ -190,5 +215,47 @@ public TargetPointer GetRCWContext(TargetPointer rcw) { return _target.ReadPointer(rcw + /* RCW::CtxCookie offset */); } + +public RCWData GetRCWData(TargetPointer rcw) +{ + TargetPointer managedObject = TargetPointer.Null; + uint syncBlockIndex = _target.Read(rcw + /* RCW::SyncBlockIndex offset */); + if (syncBlockIndex != 0) + { + managedObject = _target.Contracts.SyncBlock.GetSyncBlockObject(syncBlockIndex); + } + + uint flags = _target.Read(rcw + /* RCW::Flags offset */); + + return new RCWData( + IdentityPointer: _target.ReadPointer(rcw + /* RCW::IdentityPointer offset */), + UnknownPointer: _target.ReadPointer(rcw + /* RCW::UnknownPointer offset */), + ManagedObject: managedObject, + VTablePtr: _target.ReadPointer(rcw + /* RCW::VTablePtr offset */), + CreatorThread: _target.ReadPointer(rcw + /* RCW::CreatorThread offset */), + CtxCookie: _target.ReadPointer(rcw + /* RCW::CtxCookie offset */), + RefCount: _target.Read(rcw + /* RCW::RefCount offset */), + IsAggregated: ((RCWFlags)flags & RCWFlags.URTAggregated) != 0, + IsContained: ((RCWFlags)flags & RCWFlags.URTContained) != 0, + IsFreeThreaded: ((RCWFlags)flags & RCWFlags.MarshalingTypeMask) == RCWFlags.MarshalingTypeFreeThreaded, + IsDisconnected: IsRCWDisconnected(rcw)); +} + +// An RCW is disconnected if its unknown pointer holds the sentinel value, +// or if its context cookie no longer matches the cookie stored in its context entry. +private bool IsRCWDisconnected(TargetPointer rcw) +{ + TargetPointer unknownPointer = _target.ReadPointer(rcw + /* RCW::UnknownPointer offset */); + if (unknownPointer == DisconnectedSentinel) + return true; + + TargetPointer ctxEntryPtr = _target.ReadPointer(rcw + /* RCW::CtxEntry offset */) & ~(ulong)1; + if (ctxEntryPtr == TargetPointer.Null) + return false; + + TargetPointer rcwCookie = _target.ReadPointer(rcw + /* RCW::CtxCookie offset */); + TargetPointer entryCookie = _target.ReadPointer(ctxEntryPtr + /* CtxEntry::CtxCookie offset */); + return rcwCookie != entryCookie; +} ``` diff --git a/src/coreclr/vm/runtimecallablewrapper.h b/src/coreclr/vm/runtimecallablewrapper.h index fe113b46e3b9dc..9a0f4bcf8e5f34 100644 --- a/src/coreclr/vm/runtimecallablewrapper.h +++ b/src/coreclr/vm/runtimecallablewrapper.h @@ -510,6 +510,7 @@ struct RCW struct { + // [cDAC] [BuiltInCOM] : Contract depends on the encoding of m_fURTAggregated, m_fURTContained, and m_MarshalingType. static_assert((1 << 4) > INTERFACE_ENTRY_CACHE_SIZE, "m_iEntryToRelease needs a bigger data type"); DWORD m_iEntryToRelease:4; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs index b5f271afa84603..8014c9081dd18a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs @@ -24,8 +24,6 @@ private enum ComWrapperFlags : uint Slot_Basic = 0, } // Mirrors RCW::RCWFlags bits in src/coreclr/vm/runtimecallablewrapper.h. - // [cDAC] [BuiltInCOM]: Contract depends on these bit positions within m_dwFlags. - [System.Flags] private enum RCWFlags : uint { URTAggregated = 0x010u, // bit 4: m_fURTAggregated diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs index 3c475c10b79928..7ac1b8a283faad 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -394,27 +394,6 @@ public struct DacpCOMInterfacePointerData public ClrDataAddress comContext; } -// Mirrors struct DacpRCWData in src/coreclr/inc/dacprivate.h. -// Size must remain 0x58 bytes (enforced by static_assert in the native code). -public struct DacpRCWData -{ - public ClrDataAddress identityPointer; - public ClrDataAddress unknownPointer; - public ClrDataAddress managedObject; - public ClrDataAddress jupiterObject; - public ClrDataAddress vtablePtr; - public ClrDataAddress creatorThread; - public ClrDataAddress ctxCookie; - public int refCount; - public int interfaceCount; - public int isJupiterObject; // BOOL - public int supportsIInspectable; // BOOL - public int isAggregated; // BOOL - public int isContained; // BOOL - public int isFreeThreaded; // BOOL - public int isDisconnected; // BOOL -} - [GeneratedComInterface] [Guid("286CA186-E763-4F61-9760-487D43AE4341")] public unsafe partial interface ISOSEnum @@ -582,6 +561,25 @@ public unsafe partial interface ISOSMemoryEnum : ISOSEnum int Next(uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] SOSMemoryRegion[] memRegions, uint* pNeeded); } +public struct DacpRCWData +{ + public ClrDataAddress identityPointer; + public ClrDataAddress unknownPointer; + public ClrDataAddress managedObject; + public ClrDataAddress jupiterObject; + public ClrDataAddress vtablePtr; + public ClrDataAddress creatorThread; + public ClrDataAddress ctxCookie; + public int refCount; + public int interfaceCount; + public Interop.BOOL isJupiterObject; + public Interop.BOOL supportsIInspectable; + public Interop.BOOL isAggregated; + public Interop.BOOL isContained; + public Interop.BOOL isFreeThreaded; + public Interop.BOOL isDisconnected; +} + [GeneratedComInterface] [Guid("436f00f2-b42a-4b9f-870c-e73db66ae930")] public unsafe partial interface ISOSDacInterface diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs index 6bedd9d8ebd0dd..eb1ce958cff324 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -604,7 +604,7 @@ int ISOSDacInterface.GetCCWInterfaces(ClrDataAddress ccw, uint count, [In, Marsh if (ccw == 0 || (interfaces == null && pNeeded == null)) throw new ArgumentException(); - Contracts.IBuiltInCOM builtInCOMContract = _target.Contracts.BuiltInCOM; + Contracts.IBuiltInCOM builtInCOMContract = _target.Contracts.BuiltInCOM; // E_NOTIMPL if contract is not present // Try to resolve as a COM interface pointer; if not recognised, treat as a direct CCW pointer. // GetCCWInterfaces navigates to the start of the chain in both cases. TargetPointer startCCW = builtInCOMContract.GetCCWFromInterfacePointer(ccw.ToTargetPointer(_target)); @@ -3393,15 +3393,12 @@ int ISOSDacInterface.GetRCWData(ClrDataAddress addr, DacpRCWData* data) int hr = HResults.S_OK; try { - if (addr == 0) - throw new ArgumentException(); - if (data is null) + if (addr == 0 || data is null) throw new ArgumentException(); + IBuiltInCOM builtInCom = _target.Contracts.BuiltInCOM; // E_NOTIMPL if not defined (non-Windows) *data = default; - TargetPointer rcwPtr = addr.ToTargetPointer(_target); - IBuiltInCOM builtInCom = _target.Contracts.BuiltInCOM; Contracts.RCWData rcwData = builtInCom.GetRCWData(rcwPtr); data->identityPointer = rcwData.IdentityPointer.ToClrDataAddress(_target); @@ -3412,10 +3409,10 @@ int ISOSDacInterface.GetRCWData(ClrDataAddress addr, DacpRCWData* data) data->ctxCookie = rcwData.CtxCookie.ToClrDataAddress(_target); data->refCount = (int)rcwData.RefCount; data->interfaceCount = builtInCom.GetRCWInterfaces(rcwPtr).Count(); - data->isAggregated = rcwData.IsAggregated ? 1 : 0; - data->isContained = rcwData.IsContained ? 1 : 0; - data->isFreeThreaded = rcwData.IsFreeThreaded ? 1 : 0; - data->isDisconnected = rcwData.IsDisconnected ? 1 : 0; + data->isAggregated = rcwData.IsAggregated ? Interop.BOOL.TRUE : Interop.BOOL.FALSE; + data->isContained = rcwData.IsContained ? Interop.BOOL.TRUE : Interop.BOOL.FALSE; + data->isFreeThreaded = rcwData.IsFreeThreaded ? Interop.BOOL.TRUE : Interop.BOOL.FALSE; + data->isDisconnected = rcwData.IsDisconnected ? Interop.BOOL.TRUE : Interop.BOOL.FALSE; } catch (System.Exception ex) { diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/Program.cs similarity index 68% rename from src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/Program.cs rename to src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/Program.cs index de77918bc6e304..794040479d28f8 100644 --- a/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/Program.cs +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/Program.cs @@ -6,9 +6,13 @@ using System.Runtime.Versioning; /// -/// Debuggee for cDAC dump tests — exercises the BuiltInCOM contract's GetRCWInterfaces API. -/// Creates a real RCW for an unmanaged COM object with populated interface cache, -/// then crashes to produce a dump for analysis. +/// Debuggee for cDAC dump tests — exercises the BuiltInCOM contract's RCW APIs. +/// Creates two kinds of RCW: +/// 1. A plain RCW for the StdGlobalInterfaceTable COM object (not aggregated). +/// 2. A contained RCW via a managed class extending a [ComImport] base class. +/// The GIT singleton doesn't support aggregation, so the runtime falls back +/// to containment. +/// Both are pinned in GC handles so dump tests can find them. /// This debuggee is Windows-only, as RCW support requires FEATURE_COMINTEROP. /// internal static partial class Program @@ -36,6 +40,21 @@ private interface IGlobalInterfaceTable int GetInterfaceFromGlobal(int dwCookie, ref Guid riid, out IntPtr ppv); } + // A [ComImport] base class for the StdGlobalInterfaceTable CLSID. + // Managed classes that extend this produce an extensible (aggregated) RCW. + [ComImport] + [Guid("00000323-0000-0000-C000-000000000046")] + private class StdGlobalInterfaceTableClass + { + } + + // Extending the [ComImport] class makes this an "extensible RCW". + // The GIT is a singleton that doesn't support aggregation, so the runtime + // falls back to containment (RCW::MarkURTContained is called). + private class ContainedGlobalInterfaceTable : StdGlobalInterfaceTableClass + { + } + [LibraryImport("ole32.dll")] private static partial int CoInitializeEx(IntPtr pvReserved, uint dwCoInit); @@ -49,7 +68,7 @@ private static void Main() CreateRcwOnWindows(); } - Environment.FailFast("cDAC dump test: RCWInterfaces debuggee intentional crash"); + Environment.FailFast("cDAC dump test: RCW debuggee intentional crash"); } [SupportedOSPlatform("windows")] @@ -69,9 +88,11 @@ private static void CreateRcwOnWindows() IntPtr rcwIUnknown = IntPtr.Zero; IntPtr fetchedIUnknown = IntPtr.Zero; GCHandle rcwHandle = default; + GCHandle containedHandle = default; try { + // --- Plain RCW (not aggregated) --- Type comType = Type.GetTypeFromCLSID(CLSID_StdGlobalInterfaceTable, throwOnError: true)!; object rcwObject = Activator.CreateInstance(comType)!; @@ -89,12 +110,20 @@ private static void CreateRcwOnWindows() int revokeResult = globalInterfaceTable.RevokeInterfaceFromGlobal(cookie); Marshal.ThrowExceptionForHR(revokeResult); - // Pin the RCW object in a strong GC handle so the dump test can find it - // by walking the strong handle table (matching how GCRoots debuggee works). rcwHandle = GCHandle.Alloc(rcwObject, GCHandleType.Normal); + + // --- Contained RCW --- + // ContainedGlobalInterfaceTable extends StdGlobalInterfaceTableClass ([ComImport]), + // but the GIT singleton doesn't support aggregation, so the runtime + // falls back to containment (RCW::MarkURTContained). + object containedObject = new ContainedGlobalInterfaceTable(); + containedHandle = GCHandle.Alloc(containedObject, GCHandleType.Normal); + GC.KeepAlive(globalInterfaceTable); GC.KeepAlive(rcwHandle); GC.KeepAlive(rcwObject); + GC.KeepAlive(containedHandle); + GC.KeepAlive(containedObject); } finally { @@ -109,6 +138,7 @@ private static void CreateRcwOnWindows() } GC.KeepAlive(rcwHandle); + GC.KeepAlive(containedHandle); if (callCoUninitialize) { diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/RCWInterfaces.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/RCW.csproj similarity index 100% rename from src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/RCWInterfaces.csproj rename to src/native/managed/cdac/tests/DumpTests/Debuggees/RCW/RCW.csproj diff --git a/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/RCWDumpTests.cs similarity index 58% rename from src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs rename to src/native/managed/cdac/tests/DumpTests/RCWDumpTests.cs index 24cec63ddd62bb..c2d2e7a26d9592 100644 --- a/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/RCWDumpTests.cs @@ -10,22 +10,24 @@ namespace Microsoft.Diagnostics.DataContractReader.DumpTests; /// /// Dump-based integration tests for the BuiltInCOM contract's RCW APIs. -/// Uses the RCWInterfaces debuggee which creates a COM RCW and populates the -/// inline interface entry cache before crashing. +/// Uses the RCW debuggee which creates both a plain and a contained +/// COM RCW with populated interface entry caches before crashing. /// -public class RCWInterfacesDumpTests : DumpTestBase +public class RCWDumpTests : DumpTestBase { - protected override string DebuggeeName => "RCWInterfaces"; + protected override string DebuggeeName => "RCW"; protected override string DumpType => "full"; /// - /// Walks all strong GC handles and returns the first RCW pointer found. - /// Returns if no RCW-bearing object is found. + /// Walks all strong GC handles and returns all RCW pointers found, + /// paired with their . /// - private TargetPointer FindFirstRCW() + private List<(TargetPointer Rcw, RCWData Data)> FindAllRCWs() { IGC gcContract = Target.Contracts.GC; IObject objectContract = Target.Contracts.Object; + IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; + var results = new List<(TargetPointer, RCWData)>(); foreach (HandleData handleData in gcContract.GetHandles([HandleType.Strong])) { @@ -36,11 +38,12 @@ private TargetPointer FindFirstRCW() if (objectContract.GetBuiltInComData(objectAddress, out TargetPointer rcw, out _, out _) && rcw != TargetPointer.Null) { - return rcw; + RCWData data = builtInCOM.GetRCWData(rcw); + results.Add((rcw, data)); } } - return TargetPointer.Null; + return results; } [ConditionalTheory] @@ -51,8 +54,9 @@ public void GetRCWInterfaces_FindsRCWAndEnumeratesInterfaces(TestConfiguration c InitializeDumpTest(config); IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; - TargetPointer rcwPtr = FindFirstRCW(); - Assert.NotEqual(TargetPointer.Null, rcwPtr); + var allRcws = FindAllRCWs(); + // Find the plain (non-contained) RCW + (TargetPointer rcwPtr, _) = allRcws.First(r => !r.Data.IsContained); // Assert that the cookie is not null TargetPointer cookie = builtInCOM.GetRCWContext(rcwPtr); @@ -78,15 +82,13 @@ public void GetRCWInterfaces_FindsRCWAndEnumeratesInterfaces(TestConfiguration c [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnOS(IncludeOnly = "windows", Reason = "COM interop (RCW) is only supported on Windows")] - public void GetRCWData_ReturnsExpectedData(TestConfiguration config) + public void GetRCWData_PlainRCW_ReturnsExpectedData(TestConfiguration config) { InitializeDumpTest(config); - IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; - - TargetPointer rcwPtr = FindFirstRCW(); - Assert.NotEqual(TargetPointer.Null, rcwPtr); - RCWData data = builtInCOM.GetRCWData(rcwPtr); + var allRcws = FindAllRCWs(); + // Find the plain (non-contained) RCW + (_, RCWData data) = allRcws.First(r => !r.Data.IsContained); // The RCW wraps a live COM object, so its identity and vtable pointers must be valid. Assert.NotEqual(TargetPointer.Null, data.IdentityPointer); @@ -103,8 +105,38 @@ public void GetRCWData_ReturnsExpectedData(TestConfiguration config) Assert.False(data.IsAggregated); Assert.False(data.IsContained); - // RefCount must be positive — the debuggee held a live reference. - Assert.True(data.RefCount > 0, - $"Expected positive RefCount, got {data.RefCount}"); + // RefCount must be 1 — the debuggee held a live reference. + Assert.True(data.RefCount == 1, + $"Expected RefCount of 1, got {data.RefCount}"); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "COM interop (RCW) is only supported on Windows")] + public void GetRCWData_ContainedRCW_IsMarkedContained(TestConfiguration config) + { + InitializeDumpTest(config); + + var allRcws = FindAllRCWs(); + // Find the contained RCW (created from ContainedGlobalInterfaceTable). + // The GIT singleton doesn't support aggregation, so the runtime falls back + // to containment when an extensible RCW is created for it. + (_, RCWData data) = allRcws.First(r => r.Data.IsContained); + + Assert.True(data.IsContained); + Assert.False(data.IsAggregated); + + // The contained RCW wraps a live COM object. + Assert.NotEqual(TargetPointer.Null, data.IdentityPointer); + + // The managed object should be resolvable via the sync block index. + Assert.NotEqual(TargetPointer.Null, data.ManagedObject); + + // The RCW is alive at the time of the crash. + Assert.False(data.IsDisconnected); + + // RefCount must be 1 — the debuggee held a live reference. + Assert.True(data.RefCount == 1, + $"Expected RefCount of 1, got {data.RefCount}"); } } diff --git a/src/native/managed/cdac/tests/DumpTests/README.md b/src/native/managed/cdac/tests/DumpTests/README.md index 20b500dd9fd324..6e8b6bb2a16b7c 100644 --- a/src/native/managed/cdac/tests/DumpTests/README.md +++ b/src/native/managed/cdac/tests/DumpTests/README.md @@ -37,7 +37,7 @@ features and then calls `Environment.FailFast()` to produce a crash dump. | SyncBlock | Sync block locks | Full | | CCWInterfaces | COM callable wrappers (CCW) on Windows | Full | | RCWCleanupList | STA-context RCW entries in g_pRCWCleanupList on Windows | Full | -| RCWInterfaces | COM RCW with populated interface entry cache on Windows | Full | +| RCW | COM RCW with populated interface entry cache on Windows | Full | The dump type is configured per-debuggee via the `DumpTypes` property in each debuggee's `.csproj` (default: `Heap`, set in `Debuggees/Directory.Build.props`). Debuggees that @@ -63,7 +63,7 @@ use. Tests are `[ConditionalTheory]` methods parameterized by `TestConfiguration | SyncBlockDumpTests | SyncBlock | SyncBlock | | BuiltInCOMDumpTests | BuiltInCOM | CCWInterfaces | | RCWCleanupListDumpTests | BuiltInCOM | RCWCleanupList | -| RCWInterfacesDumpTests | BuiltInCOM | RCWInterfaces | +| RCWInterfacesDumpTests | BuiltInCOM | RCW | ### Runtime Versions From 1978f956bcfa221569509626711185ed4db557e3 Mon Sep 17 00:00:00 2001 From: Rachel Date: Thu, 12 Mar 2026 11:14:03 -0700 Subject: [PATCH 8/9] Update src/native/managed/cdac/tests/DumpTests/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/native/managed/cdac/tests/DumpTests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/native/managed/cdac/tests/DumpTests/README.md b/src/native/managed/cdac/tests/DumpTests/README.md index 6e8b6bb2a16b7c..88f349fb0e7d13 100644 --- a/src/native/managed/cdac/tests/DumpTests/README.md +++ b/src/native/managed/cdac/tests/DumpTests/README.md @@ -63,7 +63,7 @@ use. Tests are `[ConditionalTheory]` methods parameterized by `TestConfiguration | SyncBlockDumpTests | SyncBlock | SyncBlock | | BuiltInCOMDumpTests | BuiltInCOM | CCWInterfaces | | RCWCleanupListDumpTests | BuiltInCOM | RCWCleanupList | -| RCWInterfacesDumpTests | BuiltInCOM | RCW | +| RCWDumpTests | BuiltInCOM | RCW | ### Runtime Versions From e86393b8e308d751e8dba3388f60ef596c774559 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 13 Mar 2026 15:29:38 -0700 Subject: [PATCH 9/9] code review --- docs/design/datacontracts/BuiltInCOM.md | 6 +++--- .../Contracts/BuiltInCOM_1.cs | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/design/datacontracts/BuiltInCOM.md b/docs/design/datacontracts/BuiltInCOM.md index 3f1f054ea7e8ef..3125ef9744f398 100644 --- a/docs/design/datacontracts/BuiltInCOM.md +++ b/docs/design/datacontracts/BuiltInCOM.md @@ -49,7 +49,7 @@ public IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInt // Get the COM context cookie for an RCW. public TargetPointer GetRCWContext(TargetPointer rcw); // Get detailed data about an RCW, including flags and the managed object reference. -RCWData GetRCWData(TargetPointer rcw); +public RCWData GetRCWData(TargetPointer rcw); ``` ## Version 1 @@ -235,8 +235,8 @@ public RCWData GetRCWData(TargetPointer rcw) CreatorThread: _target.ReadPointer(rcw + /* RCW::CreatorThread offset */), CtxCookie: _target.ReadPointer(rcw + /* RCW::CtxCookie offset */), RefCount: _target.Read(rcw + /* RCW::RefCount offset */), - IsAggregated: ((RCWFlags)flags & RCWFlags.URTAggregated) != 0, - IsContained: ((RCWFlags)flags & RCWFlags.URTContained) != 0, + IsAggregated: ((RCWFlags)flags).HasFlag(RCWFlags.URTAggregated), + IsContained: ((RCWFlags)flags).HasFlag(RCWFlags.URTContained), IsFreeThreaded: ((RCWFlags)flags & RCWFlags.MarshalingTypeMask) == RCWFlags.MarshalingTypeFreeThreaded, IsDisconnected: IsRCWDisconnected(rcw)); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs index 8014c9081dd18a..7dfc12596f1e99 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/BuiltInCOM_1.cs @@ -1,5 +1,6 @@ // 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; @@ -24,6 +25,7 @@ private enum ComWrapperFlags : uint Slot_Basic = 0, } // Mirrors RCW::RCWFlags bits in src/coreclr/vm/runtimecallablewrapper.h. + [Flags] private enum RCWFlags : uint { URTAggregated = 0x010u, // bit 4: m_fURTAggregated @@ -245,8 +247,8 @@ public RCWData GetRCWData(TargetPointer rcw) CreatorThread: rcwData.CreatorThread, CtxCookie: rcwData.CtxCookie, RefCount: rcwData.RefCount, - IsAggregated: ((RCWFlags)rcwData.Flags & RCWFlags.URTAggregated) != 0, - IsContained: ((RCWFlags)rcwData.Flags & RCWFlags.URTContained) != 0, + IsAggregated: ((RCWFlags)rcwData.Flags).HasFlag(RCWFlags.URTAggregated), + IsContained: ((RCWFlags)rcwData.Flags).HasFlag(RCWFlags.URTContained), IsFreeThreaded: ((RCWFlags)rcwData.Flags & RCWFlags.MarshalingTypeMask) == RCWFlags.MarshalingTypeFreeThreaded, IsDisconnected: IsRCWDisconnected(rcwData)); }