diff --git a/docs/design/datacontracts/BuiltInCOM.md b/docs/design/datacontracts/BuiltInCOM.md index 4e7cae7200443b..c9e28915501d52 100644 --- a/docs/design/datacontracts/BuiltInCOM.md +++ b/docs/design/datacontracts/BuiltInCOM.md @@ -5,6 +5,20 @@ This contract is for getting information related to built-in COM. ## APIs of contract ``` csharp +public struct COMInterfacePointerData +{ + // Address of the slot in ComCallWrapper that holds the COM interface pointer. + public TargetPointer InterfacePointerAddress; + // MethodTable for this interface, or TargetPointer.Null for slot 0 (IUnknown/IDispatch). + public TargetPointer MethodTable; +} + +public record struct RCWCleanupInfo( + TargetPointer RCW, + TargetPointer Context, + TargetPointer STAThread, + bool IsFreeThreaded); + public ulong GetRefCount(TargetPointer ccw); // Check whether the COM wrappers handle is weak. public bool IsHandleWeak(TargetPointer ccw); @@ -14,20 +28,13 @@ public TargetPointer GetCCWFromInterfacePointer(TargetPointer interfacePointer); // Enumerate the COM interfaces exposed by the ComCallWrapper chain. // ccw may be any ComCallWrapper in the chain; the implementation navigates to the start. public IEnumerable GetCCWInterfaces(TargetPointer ccw); -``` - -where `COMInterfacePointerData` is: -``` csharp -public struct COMInterfacePointerData -{ - // Address of the slot in ComCallWrapper that holds the COM interface pointer. - public TargetPointer InterfacePointerAddress; - // MethodTable for this interface, or TargetPointer.Null for slot 0 (IUnknown/IDispatch). - public TargetPointer MethodTable; -} // Enumerate entries in the RCW cleanup list. // If cleanupListPtr is Null, the global g_pRCWCleanupList is used. public IEnumerable GetRCWCleanupList(TargetPointer cleanupListPtr); +// Enumerate the interface entries cached in an RCW. +public IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw); +// Get the COM context cookie for an RCW. +public TargetPointer GetRCWContext(TargetPointer rcw); ``` ## Version 1 @@ -51,6 +58,9 @@ Data descriptors used: | `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` | `InterfaceEntries` | Offset of the inline interface entry cache array within the RCW struct | +| `InterfaceEntry` | `MethodTable` | MethodTable pointer for the cached COM interface | +| `InterfaceEntry` | `Unknown` | `IUnknown*` pointer for the cached COM interface | Global variables used: | Global Name | Type | Purpose | @@ -62,6 +72,7 @@ Global variables used: | `TearOffAddRefSimple` | pointer | Address of `Unknown_AddRefSpecial`; identifies `SimpleComCallWrapper` interface pointers | | `TearOffAddRefSimpleInner` | pointer | Address of `Unknown_AddRefInner`; identifies inner `SimpleComCallWrapper` interface pointers | | `RCWCleanupList` | `pointer` | Pointer to the global `g_pRCWCleanupList` instance | +| `RCWInterfaceCacheSize` | `uint32` | Number of entries in the inline interface entry cache (`INTERFACE_ENTRY_CACHE_SIZE`) | ### Contract Constants: | Name | Type | Purpose | Value | @@ -156,5 +167,28 @@ public IEnumerable GetRCWCleanupList(TargetPointer cleanupListPt bucketPtr = _target.ReadPointer(bucketPtr + /* RCW::NextCleanupBucket offset */); } } + +public IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw) +{ + // InterfaceEntries is an inline array — the offset gives the address of the first element. + TargetPointer interfaceEntriesAddr = rcw + /* RCW::InterfaceEntries offset */; + uint cacheSize = _target.ReadGlobal("RCWInterfaceCacheSize"); + uint entrySize = /* size of InterfaceEntry */; + + for (uint i = 0; i < cacheSize; i++) + { + TargetPointer entryAddress = interfaceEntriesAddr + i * entrySize; + TargetPointer methodTable = _target.ReadPointer(entryAddress + /* InterfaceEntry::MethodTable offset */); + TargetPointer unknown = _target.ReadPointer(entryAddress + /* InterfaceEntry::Unknown offset */); + // An entry is free if Unknown == null (matches InterfaceEntry::IsFree()) + if (unknown != TargetPointer.Null) + yield return (methodTable, unknown); + } +} + +public TargetPointer GetRCWContext(TargetPointer rcw) +{ + return _target.ReadPointer(rcw + /* RCW::CtxCookie offset */); +} ``` diff --git a/src/coreclr/vm/comcache.h b/src/coreclr/vm/comcache.h index 61059b9c9cec4b..2f02827f3879a2 100644 --- a/src/coreclr/vm/comcache.h +++ b/src/coreclr/vm/comcache.h @@ -262,6 +262,15 @@ struct InterfaceEntry // will not try and optimize reads and writes to them. Volatile m_pMT; // Interface asked for Volatile m_pUnknown; // Result of query + + friend struct ::cdac_data; +}; + +template<> +struct cdac_data +{ + static constexpr size_t MethodTable = offsetof(InterfaceEntry, m_pMT); + static constexpr size_t Unknown = offsetof(InterfaceEntry, m_pUnknown); }; class CtxEntryCacheTraits : public DefaultSHashTraits diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 48a72a4040c40f..61b49340a39c2d 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -1115,12 +1115,19 @@ CDAC_TYPE_FIELD(RCW, /*pointer*/, NextRCW, cdac_data::NextRCW) 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_END(RCW) CDAC_TYPE_BEGIN(CtxEntry) CDAC_TYPE_INDETERMINATE(CtxEntry) CDAC_TYPE_FIELD(CtxEntry, /*pointer*/, STAThread, cdac_data::STAThread) CDAC_TYPE_END(CtxEntry) + +CDAC_TYPE_BEGIN(InterfaceEntry) +CDAC_TYPE_SIZE(sizeof(InterfaceEntry)) +CDAC_TYPE_FIELD(InterfaceEntry, /*pointer*/, MethodTable, cdac_data::MethodTable) +CDAC_TYPE_FIELD(InterfaceEntry, /*pointer*/, Unknown, cdac_data::Unknown) +CDAC_TYPE_END(InterfaceEntry) #endif // FEATURE_COMINTEROP #ifdef FEATURE_COMWRAPPERS @@ -1306,6 +1313,7 @@ CDAC_GLOBAL_POINTER(TearOffAddRef, &g_cdacTearOffAddRef) CDAC_GLOBAL_POINTER(TearOffAddRefSimple, &g_cdacTearOffAddRefSimple) CDAC_GLOBAL_POINTER(TearOffAddRefSimpleInner, &g_cdacTearOffAddRefSimpleInner) CDAC_GLOBAL_POINTER(RCWCleanupList, &g_pRCWCleanupList) +CDAC_GLOBAL(RCWInterfaceCacheSize, uint32, INTERFACE_ENTRY_CACHE_SIZE) #endif // FEATURE_COMINTEROP // It is important for the subdescriptor pointers to be the last pointers in the global structure. diff --git a/src/coreclr/vm/runtimecallablewrapper.h b/src/coreclr/vm/runtimecallablewrapper.h index 54ad924248ffe8..fee5c74177ee78 100644 --- a/src/coreclr/vm/runtimecallablewrapper.h +++ b/src/coreclr/vm/runtimecallablewrapper.h @@ -542,7 +542,6 @@ private : // IUnkEntry needs to access m_UnkEntry field friend IUnkEntry; - // cdac_data needs access to m_UnkEntry friend struct ::cdac_data; private : @@ -591,6 +590,7 @@ struct cdac_data static constexpr size_t Flags = offsetof(RCW, m_Flags); 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); }; 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 98391986d0fda9..a355c5601946f9 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 @@ -30,6 +30,8 @@ public interface IBuiltInCOM : IContract // ccw may be any ComCallWrapper in the chain; the implementation navigates to the start. IEnumerable GetCCWInterfaces(TargetPointer ccw) => throw new NotImplementedException(); 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(); } public readonly struct BuiltInCOM : IBuiltInCOM diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs index 767effc40b595e..179e3bc5b05a44 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/DataType.cs @@ -148,6 +148,7 @@ public enum DataType RCWCleanupList, RCW, CtxEntry, + InterfaceEntry, /* GC Data Types */ 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 eaecb87a838900..b06840d4c35bd2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -94,6 +94,7 @@ public static class Globals public const string TearOffAddRefSimple = nameof(TearOffAddRefSimple); public const string TearOffAddRefSimpleInner = nameof(TearOffAddRefSimpleInner); public const string RCWCleanupList = nameof(RCWCleanupList); + public const string RCWInterfaceCacheSize = nameof(RCWInterfaceCacheSize); public const string HashMapSlotsPerBucket = nameof(HashMapSlotsPerBucket); public const string HashMapValueMask = nameof(HashMapValueMask); 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 efecac106ae097..521a7e21421376 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 @@ -198,4 +198,23 @@ private TargetPointer GetSTAThread(Data.RCW rcw) Data.CtxEntry ctxEntry = _target.ProcessedData.GetOrAdd(ctxEntryPtr); return ctxEntry.STAThread; } + + public IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> GetRCWInterfaces(TargetPointer rcw) + { + Data.RCW rcwData = _target.ProcessedData.GetOrAdd(rcw); + foreach (Data.InterfaceEntry entry in rcwData.InterfaceEntries) + { + if (entry.Unknown != TargetPointer.Null) + { + yield return (entry.MethodTable, entry.Unknown); + } + } + } + + public TargetPointer GetRCWContext(TargetPointer rcw) + { + Data.RCW rcwData = _target.ProcessedData.GetOrAdd(rcw); + + return rcwData.CtxCookie; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterfaceEntry.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterfaceEntry.cs new file mode 100644 index 00000000000000..2beb7fdc8d903d --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/InterfaceEntry.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +internal sealed class InterfaceEntry : IData +{ + static InterfaceEntry IData.Create(Target target, TargetPointer address) => new InterfaceEntry(target, address); + public InterfaceEntry(Target target, TargetPointer address) + { + Target.TypeInfo type = target.GetTypeInfo(DataType.InterfaceEntry); + + MethodTable = target.ReadPointer(address + (ulong)type.Fields[nameof(MethodTable)].Offset); + Unknown = target.ReadPointer(address + (ulong)type.Fields[nameof(Unknown)].Offset); + } + + public TargetPointer MethodTable { get; init; } + public TargetPointer Unknown { 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 d325653df89633..422a899090c23b 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 @@ -1,6 +1,8 @@ // 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.Data; internal sealed class RCW : IData @@ -15,6 +17,17 @@ 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); + TargetPointer interfaceEntriesAddr = address + (ulong)type.Fields[nameof(InterfaceEntries)].Offset; + + uint cacheSize = target.ReadGlobal(Constants.Globals.RCWInterfaceCacheSize); + Target.TypeInfo entryTypeInfo = target.GetTypeInfo(DataType.InterfaceEntry); + uint entrySize = entryTypeInfo.Size!.Value; + + for (uint i = 0; i < cacheSize; i++) + { + TargetPointer entryAddress = interfaceEntriesAddr + i * entrySize; + InterfaceEntries.Add(target.ProcessedData.GetOrAdd(entryAddress)); + } } public TargetPointer NextCleanupBucket { get; init; } @@ -22,4 +35,5 @@ public RCW(Target target, TargetPointer address) public uint Flags { get; init; } public TargetPointer CtxCookie { get; init; } public TargetPointer CtxEntry { get; init; } + public List InterfaceEntries { get; } = new List(); } 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 b645240cd13525..b19a0c900d175c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/ISOSDacInterface.cs @@ -387,6 +387,13 @@ public struct DacpGcHeapAnalyzeData public int heap_analyze_success; // BOOL } +public struct DacpCOMInterfacePointerData +{ + public ClrDataAddress methodTable; + public ClrDataAddress interfacePtr; + public ClrDataAddress comContext; +} + [GeneratedComInterface] [Guid("286CA186-E763-4F61-9760-487D43AE4341")] public unsafe partial interface ISOSEnum @@ -753,7 +760,7 @@ public unsafe partial interface ISOSDacInterface [PreserveSig] int GetRCWData(ClrDataAddress addr, /*struct DacpRCWData */ void* data); [PreserveSig] - int GetRCWInterfaces(ClrDataAddress rcw, uint count, /*struct DacpCOMInterfacePointerData*/ void* interfaces, uint* pNeeded); + int GetRCWInterfaces(ClrDataAddress rcw, uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] DacpCOMInterfacePointerData[]? interfaces, uint* pNeeded); [PreserveSig] int GetCCWData(ClrDataAddress ccw, /*struct DacpCCWData */ void* data); [PreserveSig] @@ -795,13 +802,6 @@ public unsafe partial interface ISOSDacInterface int GetFailedAssemblyDisplayName(ClrDataAddress assembly, uint count, char* name, uint* pNeeded); }; -public struct DacpCOMInterfacePointerData -{ - public ClrDataAddress methodTable; - public ClrDataAddress interfacePtr; - public ClrDataAddress comContext; -} - #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value public struct DacpExceptionObjectData { 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 9753e6caae5a2a..5b3648a94e496a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.cs @@ -3390,8 +3390,93 @@ int ISOSDacInterface.GetPrivateBinPaths(ClrDataAddress appDomain, int count, cha } int ISOSDacInterface.GetRCWData(ClrDataAddress addr, void* data) => _legacyImpl is not null ? _legacyImpl.GetRCWData(addr, data) : HResults.E_NOTIMPL; - int ISOSDacInterface.GetRCWInterfaces(ClrDataAddress rcw, uint count, void* interfaces, uint* pNeeded) - => _legacyImpl is not null ? _legacyImpl.GetRCWInterfaces(rcw, count, interfaces, pNeeded) : HResults.E_NOTIMPL; + int ISOSDacInterface.GetRCWInterfaces(ClrDataAddress rcw, uint count, [In, MarshalUsing(CountElementName = nameof(count)), Out] DacpCOMInterfacePointerData[]? interfaces, uint* pNeeded) + { + int hr = HResults.S_OK; +#if DEBUG + int numWritten = 0; +#endif + try + { + if (rcw == 0) + throw new ArgumentException(); + + TargetPointer rcwPtr = rcw.ToTargetPointer(_target); + IBuiltInCOM builtInCom = _target.Contracts.BuiltInCOM; // E_NOTIMPL if not defined (non-Windows) + IEnumerable<(TargetPointer MethodTable, TargetPointer Unknown)> entries = builtInCom.GetRCWInterfaces(rcwPtr); + + if (interfaces == null) + { + if (pNeeded == null) + { + throw new ArgumentException(); + } + else + { + *pNeeded = (uint)entries.Count(); +#if DEBUG + numWritten = (int)*pNeeded; +#endif + } + } + else + { + TargetPointer ctxCookie = builtInCom.GetRCWContext(rcwPtr); + uint itemIndex = 0; + foreach (var (methodTable, unknown) in entries) + { + if (itemIndex >= count) + { +#if DEBUG + numWritten = (int)itemIndex; +#endif + throw new ArgumentException(); + } + + interfaces[itemIndex].methodTable = methodTable.ToClrDataAddress(_target); + interfaces[itemIndex].interfacePtr = unknown.ToClrDataAddress(_target); + interfaces[itemIndex].comContext = ctxCookie.ToClrDataAddress(_target); + itemIndex++; + } + + if (pNeeded != null) + *pNeeded = itemIndex; +#if DEBUG + numWritten = (int)itemIndex; +#endif + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + +#if DEBUG + if (_legacyImpl is not null) + { + uint pNeededLocal = 0; + int hrLocal; + DacpCOMInterfacePointerData[]? interfacesLocal = interfaces != null ? new DacpCOMInterfacePointerData[count] : null; + hrLocal = _legacyImpl.GetRCWInterfaces(rcw, count, interfacesLocal, pNeeded == null && interfacesLocal == null ? null : &pNeededLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + Debug.Assert(numWritten == pNeededLocal, $"cDAC: {numWritten}, DAC: {pNeededLocal}"); + if (interfacesLocal is not null && interfaces is not null) + { + for (int i = 0; i < (int)pNeededLocal; i++) + { + Debug.Assert(interfaces[i].methodTable == interfacesLocal![i].methodTable, $"cDAC: {interfaces[i].methodTable:x}, DAC: {interfacesLocal[i].methodTable:x}"); + Debug.Assert(interfaces[i].interfacePtr == interfacesLocal![i].interfacePtr, $"cDAC: {interfaces[i].interfacePtr:x}, DAC: {interfacesLocal[i].interfacePtr:x}"); + Debug.Assert(interfaces[i].comContext == interfacesLocal![i].comContext, $"cDAC: {interfaces[i].comContext:x}, DAC: {interfacesLocal[i].comContext:x}"); + } + } + } + } +#endif + + return hr; + } int ISOSDacInterface.GetRegisterName(int regName, uint count, char* buffer, uint* pNeeded) { int hr = HResults.S_OK; @@ -4119,7 +4204,7 @@ int ISOSDacInterface.TraverseRCWCleanupList(ClrDataAddress cleanupListPtr, deleg if (pCallback is null) throw new ArgumentException(); - Contracts.IBuiltInCOM contract = _target.Contracts.BuiltInCOM; + Contracts.IBuiltInCOM contract = _target.Contracts.BuiltInCOM; // E_NOTIMPL if not defined (non-Windows) TargetPointer listPtr = cleanupListPtr.ToTargetPointer(_target); cleanupInfos = contract.GetRCWCleanupList(listPtr); diff --git a/src/native/managed/cdac/tests/BuiltInCOMTests.cs b/src/native/managed/cdac/tests/BuiltInCOMTests.cs index 3da3ec04d531de..f2b9e86c81e5ef 100644 --- a/src/native/managed/cdac/tests/BuiltInCOMTests.cs +++ b/src/native/managed/cdac/tests/BuiltInCOMTests.cs @@ -12,6 +12,105 @@ namespace Microsoft.Diagnostics.DataContractReader.Tests; public class BuiltInCOMTests { + private const ulong AllocationRangeStart = 0x00000000_20000000; + private const ulong AllocationRangeEnd = 0x00000000_30000000; + + private const uint TestRCWInterfaceCacheSize = 8; + + private static readonly MockDescriptors.TypeFields RCWFields = new MockDescriptors.TypeFields() + { + DataType = DataType.RCW, + Fields = + [ + new(nameof(Data.RCW.NextCleanupBucket), DataType.pointer), + new(nameof(Data.RCW.NextRCW), DataType.pointer), + new(nameof(Data.RCW.Flags), DataType.uint32), + new(nameof(Data.RCW.CtxCookie), DataType.pointer), + new(nameof(Data.RCW.CtxEntry), DataType.pointer), + new(nameof(Data.RCW.InterfaceEntries), DataType.pointer), + ] + }; + + private static readonly MockDescriptors.TypeFields InterfaceEntryFields = new MockDescriptors.TypeFields() + { + DataType = DataType.InterfaceEntry, + Fields = + [ + new(nameof(Data.InterfaceEntry.MethodTable), DataType.pointer), + new(nameof(Data.InterfaceEntry.Unknown), DataType.pointer), + ] + }; + + private static void BuiltInCOMContractHelper( + MockTarget.Architecture arch, + Action> configure, + Action testCase) + { + TargetTestHelpers targetTestHelpers = new(arch); + MockMemorySpace.Builder builder = new(targetTestHelpers); + + Dictionary types = MockDescriptors.GetTypesForTypeFields( + targetTestHelpers, + [RCWFields, InterfaceEntryFields]); + + configure(builder, targetTestHelpers, types); + + (string Name, ulong Value)[] globals = + [ + (nameof(Constants.Globals.RCWInterfaceCacheSize), TestRCWInterfaceCacheSize), + ]; + + var target = new TestPlaceholderTarget(arch, builder.GetMemoryContext().ReadFromTarget, types, globals); + target.SetContracts(Mock.Of( + c => c.BuiltInCOM == ((IContractFactory)new BuiltInCOMFactory()).CreateContract(target, 1))); + + testCase(target); + } + + /// + /// Allocates an RCW mock with the interface entries embedded inline (matching the real C++ layout + /// where m_aInterfaceEntries is an inline array within the RCW struct). + /// Returns the address of the RCW. + /// + private static TargetPointer AddRCWWithInlineEntries( + MockMemorySpace.Builder builder, + TargetTestHelpers targetTestHelpers, + Dictionary types, + MockMemorySpace.BumpAllocator allocator, + (TargetPointer MethodTable, TargetPointer Unknown)[] entries, + TargetPointer ctxCookie = default) + { + 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; + + // The RCW block must be large enough to hold the RCW header plus all inline entries + uint totalSize = entriesOffset + entrySize * TestRCWInterfaceCacheSize; + MockMemorySpace.HeapFragment fragment = allocator.Allocate(totalSize, "RCW with inline entries"); + Span data = fragment.Data; + + // Write RCW header fields + targetTestHelpers.WritePointer( + data.Slice(rcwTypeInfo.Fields[nameof(Data.RCW.CtxCookie)].Offset), + ctxCookie); + + // Write the inline interface entries starting at entriesOffset + for (int i = 0; i < entries.Length && i < TestRCWInterfaceCacheSize; i++) + { + Span entryData = data.Slice((int)(entriesOffset + i * entrySize)); + targetTestHelpers.WritePointer( + entryData.Slice(entryTypeInfo.Fields[nameof(Data.InterfaceEntry.MethodTable)].Offset), + entries[i].MethodTable); + targetTestHelpers.WritePointer( + entryData.Slice(entryTypeInfo.Fields[nameof(Data.InterfaceEntry.Unknown)].Offset), + entries[i].Unknown); + } + + builder.AddHeapFragment(fragment); + return fragment.Address; + } + // Flag values matching the C++ runtime private const ulong IsLayoutCompleteFlag = 0x10; @@ -473,6 +572,40 @@ public void GetCCWInterfaces_LinkedWrapper_WalksFullChainFromAnyWrapper(MockTarg } } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWInterfaces_ReturnsFilledEntries(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + (TargetPointer MethodTable, TargetPointer Unknown)[] expectedEntries = + [ + (new TargetPointer(0x1000), new TargetPointer(0x2000)), + (new TargetPointer(0x3000), new TargetPointer(0x4000)), + ]; + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddRCWWithInlineEntries(builder, targetTestHelpers, types, allocator, expectedEntries); + }, + (target) => + { + IBuiltInCOM contract = target.Contracts.BuiltInCOM; + Assert.NotNull(contract); + + List<(TargetPointer MethodTable, TargetPointer Unknown)> results = + contract.GetRCWInterfaces(rcwAddress).ToList(); + + Assert.Equal(expectedEntries.Length, results.Count); + for (int i = 0; i < expectedEntries.Length; i++) + { + Assert.Equal(expectedEntries[i].MethodTable, results[i].MethodTable); + Assert.Equal(expectedEntries[i].Unknown, results[i].Unknown); + } + }); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetCCWInterfaces_ComIpAddress_ResolvesToCCW(MockTarget.Architecture arch) @@ -566,6 +699,84 @@ public void GetCCWInterfaces_ComIpAddress_ResolvesToCCW(MockTarget.Architecture } } + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWInterfaces_SkipsEntriesWithNullUnknown(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + // The IsFree() check uses only Unknown == null; entries with Unknown == null are skipped. + (TargetPointer MethodTable, TargetPointer Unknown)[] entries = + [ + (new TargetPointer(0x1000), new TargetPointer(0x2000)), + (TargetPointer.Null, TargetPointer.Null), // free entry (Unknown == null) + (new TargetPointer(0x5000), new TargetPointer(0x6000)), + ]; + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddRCWWithInlineEntries(builder, targetTestHelpers, types, allocator, entries); + }, + (target) => + { + IBuiltInCOM contract = target.Contracts.BuiltInCOM; + List<(TargetPointer MethodTable, TargetPointer Unknown)> results = + contract.GetRCWInterfaces(rcwAddress).ToList(); + + // Only the 2 entries with non-null Unknown are returned + Assert.Equal(2, results.Count); + Assert.Equal(new TargetPointer(0x1000), results[0].MethodTable); + Assert.Equal(new TargetPointer(0x2000), results[0].Unknown); + Assert.Equal(new TargetPointer(0x5000), results[1].MethodTable); + Assert.Equal(new TargetPointer(0x6000), results[1].Unknown); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWInterfaces_EmptyCache_ReturnsEmpty(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddRCWWithInlineEntries(builder, targetTestHelpers, types, allocator, []); + }, + (target) => + { + IBuiltInCOM contract = target.Contracts.BuiltInCOM; + List<(TargetPointer MethodTable, TargetPointer Unknown)> results = + contract.GetRCWInterfaces(rcwAddress).ToList(); + + Assert.Empty(results); + }); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void GetRCWContext_ReturnsCtxCookie(MockTarget.Architecture arch) + { + TargetPointer rcwAddress = default; + TargetPointer expectedCookie = new TargetPointer(0xC00C_1E00); + + BuiltInCOMContractHelper(arch, + (builder, targetTestHelpers, types) => + { + MockMemorySpace.BumpAllocator allocator = builder.CreateAllocator(AllocationRangeStart, AllocationRangeEnd); + rcwAddress = AddRCWWithInlineEntries(builder, targetTestHelpers, types, allocator, [], expectedCookie); + }, + (target) => + { + IBuiltInCOM contract = target.Contracts.BuiltInCOM; + TargetPointer result = contract.GetRCWContext(rcwAddress); + + Assert.Equal(expectedCookie, result); + }); + } + [Theory] [ClassData(typeof(MockTarget.StdArch))] public void GetCCWFromInterfacePointer_SCCWIp_ResolvesToStartCCW(MockTarget.Architecture arch) diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/Program.cs new file mode 100644 index 00000000000000..de77918bc6e304 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/Program.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.Runtime.InteropServices; +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. +/// This debuggee is Windows-only, as RCW support requires FEATURE_COMINTEROP. +/// +internal static partial class Program +{ + private const int S_OK = 0; + private const int S_FALSE = 1; + private const int RpcEChangedMode = unchecked((int)0x80010106); + private const uint CoInitMultithreaded = 0; + + private static readonly Guid CLSID_StdGlobalInterfaceTable = new("00000323-0000-0000-C000-000000000046"); + private static readonly Guid IID_IUnknown = new("00000000-0000-0000-C000-000000000046"); + + [ComImport] + [Guid("00000146-0000-0000-C000-000000000046")] + [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + private interface IGlobalInterfaceTable + { + [PreserveSig] + int RegisterInterfaceInGlobal(IntPtr pUnk, ref Guid riid, out int pdwCookie); + + [PreserveSig] + int RevokeInterfaceFromGlobal(int dwCookie); + + [PreserveSig] + int GetInterfaceFromGlobal(int dwCookie, ref Guid riid, out IntPtr ppv); + } + + [LibraryImport("ole32.dll")] + private static partial int CoInitializeEx(IntPtr pvReserved, uint dwCoInit); + + [LibraryImport("ole32.dll")] + private static partial void CoUninitialize(); + + private static void Main() + { + if (OperatingSystem.IsWindows()) + { + CreateRcwOnWindows(); + } + + Environment.FailFast("cDAC dump test: RCWInterfaces debuggee intentional crash"); + } + + [SupportedOSPlatform("windows")] + private static void CreateRcwOnWindows() + { + bool callCoUninitialize = false; + int coInitializeResult = CoInitializeEx(IntPtr.Zero, CoInitMultithreaded); + if (coInitializeResult == S_OK || coInitializeResult == S_FALSE) + { + callCoUninitialize = true; + } + else if (coInitializeResult != RpcEChangedMode) + { + Marshal.ThrowExceptionForHR(coInitializeResult); + } + + IntPtr rcwIUnknown = IntPtr.Zero; + IntPtr fetchedIUnknown = IntPtr.Zero; + GCHandle rcwHandle = default; + + try + { + Type comType = Type.GetTypeFromCLSID(CLSID_StdGlobalInterfaceTable, throwOnError: true)!; + object rcwObject = Activator.CreateInstance(comType)!; + + IGlobalInterfaceTable globalInterfaceTable = (IGlobalInterfaceTable)rcwObject; + + rcwIUnknown = Marshal.GetIUnknownForObject(rcwObject); + + Guid iidIUnknown = IID_IUnknown; + int registerResult = globalInterfaceTable.RegisterInterfaceInGlobal(rcwIUnknown, ref iidIUnknown, out int cookie); + Marshal.ThrowExceptionForHR(registerResult); + + int getResult = globalInterfaceTable.GetInterfaceFromGlobal(cookie, ref iidIUnknown, out fetchedIUnknown); + Marshal.ThrowExceptionForHR(getResult); + + 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); + GC.KeepAlive(globalInterfaceTable); + GC.KeepAlive(rcwHandle); + GC.KeepAlive(rcwObject); + } + finally + { + if (fetchedIUnknown != IntPtr.Zero) + { + Marshal.Release(fetchedIUnknown); + } + + if (rcwIUnknown != IntPtr.Zero) + { + Marshal.Release(rcwIUnknown); + } + + GC.KeepAlive(rcwHandle); + + if (callCoUninitialize) + { + CoUninitialize(); + } + } + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/RCWInterfaces.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/RCWInterfaces.csproj new file mode 100644 index 00000000000000..69a9ea88447b47 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/RCWInterfaces/RCWInterfaces.csproj @@ -0,0 +1,7 @@ + + + + $(NoWarn);CA1416 + Full + + diff --git a/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs new file mode 100644 index 00000000000000..52fb2520eec033 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/RCWInterfacesDumpTests.cs @@ -0,0 +1,75 @@ +// 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; +using System.Linq; +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for the BuiltInCOM contract's GetRCWInterfaces API. +/// Uses the RCWInterfaces debuggee which creates a COM RCW and populates the +/// inline interface entry cache before crashing. +/// +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) + { + InitializeDumpTest(config); + IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; + IObject objectContract = Target.Contracts.Object; + IGC gcContract = Target.Contracts.GC; + + 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) + { + TargetPointer objectAddress = Target.ReadPointer(handleData.Handle); + if (objectAddress == TargetPointer.Null) + continue; + + if (objectContract.GetBuiltInComData(objectAddress, out TargetPointer rcw, out _, out _) + && rcw != TargetPointer.Null) + { + rcwPtr = rcw; + break; + } + } + + Assert.NotEqual(TargetPointer.Null, rcwPtr); + + // Assert that the cookie is not null + TargetPointer cookie = builtInCOM.GetRCWContext(rcwPtr); + Assert.NotEqual(TargetPointer.Null, cookie); + + // Call GetRCWInterfaces on the found RCW — must not throw + List<(TargetPointer MethodTable, TargetPointer Unknown)> interfaces = + builtInCOM.GetRCWInterfaces(rcwPtr).ToList(); + + // The debuggee interacts with the RCW via IGlobalInterfaceTable / IUnknown, + // so the entry cache should have at least one cached interface entry + Assert.True(interfaces.Count >= 1, + $"Expected at least one cached interface entry in the RCW, got {interfaces.Count}"); + + // Every returned entry must have non-null MethodTable and Unknown pointers + foreach ((TargetPointer mt, TargetPointer unk) in interfaces) + { + Assert.NotEqual(TargetPointer.Null, mt); + Assert.NotEqual(TargetPointer.Null, unk); + } + } +}