diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ComWrappers_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ComWrappers_1.cs index 6d12808352319a..e7cdfc0400c0f7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ComWrappers_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ComWrappers_1.cs @@ -56,8 +56,9 @@ private bool IsComWrappersCCW(TargetPointer ccw) if (!GetComWrappersCCWVTableQIAddress(ccw, out _, out TargetPointer qiAddress)) return false; - TargetPointer comWrappersVtablePtrs = _target.ReadGlobalPointer(Constants.Globals.ComWrappersVtablePtrs); - Data.ComWrappersVtablePtrs comWrappersVtableStruct = _target.ProcessedData.GetOrAdd(comWrappersVtablePtrs); + if (!_target.TryReadGlobalPointer(Constants.Globals.ComWrappersVtablePtrs, out TargetPointer? comWrappersVtablePtrs)) + return false; + Data.ComWrappersVtablePtrs comWrappersVtableStruct = _target.ProcessedData.GetOrAdd(comWrappersVtablePtrs.Value); return comWrappersVtableStruct.ComWrappersInterfacePointers.Contains(CodePointerUtils.CodePointerFromAddress(qiAddress, _target)); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs index c1fe7be3d8c101..3103c306e4a3d2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/DacDbiImpl.cs @@ -652,7 +652,53 @@ public int GetCurrentException(ulong vmThread, ulong* pRetVal) } public int GetObjectForCCW(ulong ccwPtr, ulong* pRetVal) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetObjectForCCW(ccwPtr, pRetVal) : HResults.E_NOTIMPL; + { + *pRetVal = 0; + int hr = HResults.S_OK; + try + { + TargetPointer objectHandle = TargetPointer.Null; + TargetPointer ccwAddress = new(ccwPtr); + bool comWrappersSuccess = false; + + if (_target.Contracts.TryGetContract(out IComWrappers? comWrappers)) + { + TargetPointer managedObjectWrapper = comWrappers.GetManagedObjectWrapperFromCCW(ccwAddress); + if (managedObjectWrapper != TargetPointer.Null) + { + comWrappersSuccess = _target.TryReadPointer(managedObjectWrapper, out objectHandle); + } + } + + if (!comWrappersSuccess && _target.Contracts.TryGetContract(out IBuiltInCOM? builtInCOM)) + { + TargetPointer ccw = builtInCOM.GetCCWFromInterfacePointer(ccwAddress); + if (ccw == TargetPointer.Null) + { + ccw = ccwAddress; + } + ccw = builtInCOM.GetStartWrapper(ccw); + objectHandle = builtInCOM.GetObjectHandle(ccw); + } + + *pRetVal = objectHandle.Value; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + ulong retValLocal; + int hrLocal = _legacy.GetObjectForCCW(ccwPtr, &retValLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pRetVal == retValLocal, $"cDAC: {*pRetVal:x}, DAC: {retValLocal:x}"); + } +#endif + return hr; + } public int GetCurrentCustomDebuggerNotification(ulong vmThread, ulong* pRetVal) { @@ -1011,7 +1057,31 @@ public int GetStackFramesFromException(ulong vmObject, nint pDacStackFrames) => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetStackFramesFromException(vmObject, pDacStackFrames) : HResults.E_NOTIMPL; public int IsRcw(ulong vmObject, Interop.BOOL* pResult) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.IsRcw(vmObject, pResult) : HResults.E_NOTIMPL; + { + *pResult = Interop.BOOL.FALSE; + int hr = HResults.S_OK; + try + { + IObject obj = _target.Contracts.Object; + _ = obj.GetBuiltInComData(new TargetPointer(vmObject), out TargetPointer rcw, out _, out _); + *pResult = rcw != TargetPointer.Null ? Interop.BOOL.TRUE : Interop.BOOL.FALSE; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + Interop.BOOL resultLocal; + int hrLocal = _legacy.IsRcw(vmObject, &resultLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + Debug.Assert(*pResult == resultLocal, $"cDAC: {*pResult}, DAC: {resultLocal}"); + } +#endif + return hr; + } public int GetRcwCachedInterfacePointers(ulong vmObject, Interop.BOOL bIInspectableOnly, nint pDacItfPtrs) => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetRcwCachedInterfacePointers(vmObject, bIInspectableOnly, pDacItfPtrs) : HResults.E_NOTIMPL; diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiCCWDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiCCWDumpTests.cs new file mode 100644 index 00000000000000..a08cfe17300e1c --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiCCWDumpTests.cs @@ -0,0 +1,74 @@ +// 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 Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for DacDbiImpl.GetObjectForCCW. +/// Uses the CCW debuggee (full dump). +/// +public class DacDbiCCWDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "CCW"; + protected override string DumpType => "full"; + + private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); + + private (TargetPointer Ccw, TargetPointer InterfacePointer) FindBuiltInComCcwWithInterface() + { + IGC gc = Target.Contracts.GC; + IObject obj = Target.Contracts.Object; + IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; + + foreach (HandleData handleData in gc.GetHandles([HandleType.Strong])) + { + TargetPointer objectAddress = Target.ReadPointer(handleData.Handle); + if (objectAddress == TargetPointer.Null) + continue; + + if (!obj.GetBuiltInComData(objectAddress, out _, out TargetPointer ccw, out _) + || ccw == TargetPointer.Null) + { + continue; + } + + // Normalize to the start wrapper, matching what DacDbiImpl.GetObjectForCCW does + // before calling GetObjectHandle, so the expected handle in the test is consistent. + TargetPointer startCcw = builtInCOM.GetStartWrapper(ccw); + + List interfaces = builtInCOM.GetCCWInterfaces(startCcw).ToList(); + if (interfaces.Count == 0) + continue; + + return (startCcw, interfaces[0].InterfacePointerAddress); + } + + throw new SkipTestException("No BuiltInCOM CCW interface pointer found in dump."); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "COM callable wrappers require Windows")] + public unsafe void GetObjectForCCW_ReturnsBuiltInComObjectHandle(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IBuiltInCOM builtInCOM = Target.Contracts.BuiltInCOM; + + (TargetPointer ccw, TargetPointer interfacePointer) = FindBuiltInComCcwWithInterface(); + + ulong resultHandle; + int hr = dbi.GetObjectForCCW(interfacePointer.Value, &resultHandle); + + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(builtInCOM.GetObjectHandle(ccw).Value, resultHandle); + Assert.NotEqual(TargetPointer.Null, Target.ReadPointer(new TargetPointer(resultHandle))); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiComWrappersDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiComWrappersDumpTests.cs new file mode 100644 index 00000000000000..0bcb2521ee5f92 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiComWrappersDumpTests.cs @@ -0,0 +1,64 @@ +// 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 Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for DacDbiImpl.GetObjectForCCW via the ComWrappers path. +/// Uses the ComWrappers debuggee (full dump). +/// +public class DacDbiComWrappersDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "ComWrappers"; + protected override string DumpType => "full"; + + private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "ComWrappers cDAC support not available in .NET 10")] + public unsafe void GetObjectForCCW_ComWrappersIdentityPointer_ReturnsManagedObjectWrapperHandle(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IGC gc = Target.Contracts.GC; + IComWrappers comWrappers = Target.Contracts.ComWrappers; + + foreach (HandleData handleData in gc.GetHandles([HandleType.Strong])) + { + TargetPointer objectAddress = Target.ReadPointer(handleData.Handle); + if (objectAddress == TargetPointer.Null) + continue; + + List mows = comWrappers.GetMOWs(objectAddress, out bool hasMowTable); + if (!hasMowTable || mows.Count == 0) + continue; + + TargetPointer mow = mows[0]; + TargetPointer identity = comWrappers.GetIdentityForMOW(mow); + if (identity == TargetPointer.Null) + continue; + + ulong expectedHandle = Target.ReadPointer(mow).Value; + ulong actualHandle; + int hr = dbi.GetObjectForCCW(identity.Value, &actualHandle); + + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(expectedHandle, actualHandle); + + // Verify the returned handle dereferences to a live object in the dump. + TargetPointer actualHandleTarget = new(actualHandle); + TargetPointer actualObjectAddress = Target.ReadPointer(actualHandleTarget); + Assert.NotEqual(TargetPointer.Null, actualObjectAddress); + return; + } + + throw new SkipTestException("No ComWrappers MOW/CCW identity found in dump."); + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRCWDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRCWDumpTests.cs new file mode 100644 index 00000000000000..47f6ed89748625 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRCWDumpTests.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Diagnostics.DataContractReader.Contracts; +using Microsoft.Diagnostics.DataContractReader.Legacy; +using Microsoft.DotNet.XUnitExtensions; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for DacDbiImpl.IsRcw. +/// Uses the RCW debuggee (full dump). +/// +public class DacDbiRCWDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "RCW"; + protected override string DumpType => "full"; + + private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "COM interop (RCW) is only supported on Windows")] + public unsafe void IsRcw_ReturnsTrueForBuiltInRcwObject(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IGC gc = Target.Contracts.GC; + IObject obj = Target.Contracts.Object; + + foreach (HandleData handleData in gc.GetHandles([HandleType.Strong])) + { + TargetPointer objectAddress = Target.ReadPointer(handleData.Handle); + if (objectAddress == TargetPointer.Null) + continue; + + if (!obj.GetBuiltInComData(objectAddress, out TargetPointer rcw, out _, out _) || rcw == TargetPointer.Null) + continue; + + Interop.BOOL result; + int hr = dbi.IsRcw(objectAddress.Value, &result); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(Interop.BOOL.TRUE, result); + return; + } + + throw new SkipTestException("No built-in RCW object found in dump."); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnOS(IncludeOnly = "windows", Reason = "COM interop (RCW) is only supported on Windows")] + public unsafe void IsRcw_ReturnsFalseForNonRcwObject(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IGC gc = Target.Contracts.GC; + IObject obj = Target.Contracts.Object; + + foreach (HandleData handleData in gc.GetHandles([HandleType.Strong])) + { + TargetPointer objectAddress = Target.ReadPointer(handleData.Handle); + if (objectAddress == TargetPointer.Null) + continue; + + _ = obj.GetBuiltInComData(objectAddress, out TargetPointer rcw, out _, out _); + if (rcw != TargetPointer.Null) + continue; + + Interop.BOOL result; + int hr = dbi.IsRcw(objectAddress.Value, &result); + Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(Interop.BOOL.FALSE, result); + return; + } + + throw new SkipTestException("No non-RCW object found in dump."); + } +}