From 891c68708d76c6fbeeaa184a7b7e5c9b5c45e2cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:33:28 +0000 Subject: [PATCH 1/4] Implement DacDbi cDAC APIs CreateRefWalk, DeleteRefWalk, WalkRefs Co-authored-by: rcj1 <77995559+rcj1@users.noreply.github.com> --- src/coreclr/debug/daccess/dacdbiimpl.cpp | 8 +- src/coreclr/debug/daccess/dacdbiimpl.h | 6 +- src/coreclr/debug/di/process.cpp | 6 +- src/coreclr/debug/di/rspriv.h | 2 +- src/coreclr/debug/inc/dacdbiinterface.h | 3 +- src/coreclr/inc/dacdbi.idl | 2 +- .../Contracts/IStackWalk.cs | 1 + .../Contracts/StackWalk/GC/GcScanContext.cs | 3 + .../Contracts/StackWalk/GC/StackRefData.cs | 1 + .../Contracts/StackWalk/StackWalk_1.cs | 1 + .../Dbi/DacDbiImpl.cs | 148 +++++++++++++++- .../Dbi/Helpers/RefWalk.cs | 167 ++++++++++++++++++ .../Dbi/IDacDbiInterface.cs | 27 ++- .../DacDbi/DacDbiRefWalkDumpTests.cs | 123 +++++++++++++ 14 files changed, 477 insertions(+), 21 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs create mode 100644 src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs diff --git a/src/coreclr/debug/daccess/dacdbiimpl.cpp b/src/coreclr/debug/daccess/dacdbiimpl.cpp index 54d31a3cdeb424..0a166b36b4870f 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.cpp +++ b/src/coreclr/debug/daccess/dacdbiimpl.cpp @@ -6861,11 +6861,11 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::IsValidObject(CORDB_ADDRESS obj, return hr; } -HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask) +HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask) { DD_ENTER_MAY_THROW; - DacRefWalker *walker = new (nothrow) DacRefWalker(this, walkStacks, walkFQ, handleWalkMask, TRUE); + DacRefWalker *walker = new (nothrow) DacRefWalker(this, walkStacks, handleWalkMask, TRUE); if (walker == NULL) return E_OUTOFMEMORY; @@ -7506,8 +7506,8 @@ HRESULT STDMETHODCALLTYPE DacDbiInterfaceImpl::GetGenericArgTokenIndex(VMPTR_Met return S_OK; } -DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, BOOL walkFQ, UINT32 handleMask, BOOL resolvePointers) - : mDac(dac), mWalkStacks(walkStacks), mWalkFQ(walkFQ), mHandleMask(handleMask), mStackWalker(NULL), +DacRefWalker::DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers) + : mDac(dac), mWalkStacks(walkStacks), mHandleMask(handleMask), mStackWalker(NULL), mResolvePointers(resolvePointers), mHandleWalker(NULL), mFQStart(PTR_NULL), mFQEnd(PTR_NULL), mFQCurr(PTR_NULL) { } diff --git a/src/coreclr/debug/daccess/dacdbiimpl.h b/src/coreclr/debug/daccess/dacdbiimpl.h index a59bedda6e60c3..0065010c1290d8 100644 --- a/src/coreclr/debug/daccess/dacdbiimpl.h +++ b/src/coreclr/debug/daccess/dacdbiimpl.h @@ -123,7 +123,7 @@ class DacDbiInterfaceImpl : HRESULT STDMETHODCALLTYPE IsValidObject(CORDB_ADDRESS obj, OUT BOOL * pResult); - HRESULT STDMETHODCALLTYPE CreateRefWalk(RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask); + HRESULT STDMETHODCALLTYPE CreateRefWalk(RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask); HRESULT STDMETHODCALLTYPE DeleteRefWalk(RefWalkHandle handle); HRESULT STDMETHODCALLTYPE WalkRefs(RefWalkHandle handle, ULONG count, OUT DacGcReference * objects, OUT ULONG *pFetched); @@ -964,7 +964,7 @@ class DDHolder class DacRefWalker { public: - DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, BOOL walkFQ, UINT32 handleMask, BOOL resolvePointers); + DacRefWalker(ClrDataAccess *dac, BOOL walkStacks, UINT32 handleMask, BOOL resolvePointers); ~DacRefWalker(); HRESULT Init(); @@ -977,7 +977,7 @@ class DacRefWalker private: ClrDataAccess *mDac; - BOOL mWalkStacks, mWalkFQ; + BOOL mWalkStacks; UINT32 mHandleMask; // Stacks diff --git a/src/coreclr/debug/di/process.cpp b/src/coreclr/debug/di/process.cpp index dc654fe21480d0..09abc68a736ac7 100644 --- a/src/coreclr/debug/di/process.cpp +++ b/src/coreclr/debug/di/process.cpp @@ -2552,13 +2552,13 @@ HRESULT CordbProcess::GetTypeForObject(CORDB_ADDRESS addr, CordbType **ppType, C // CordbRefEnum // ****************************************** CordbRefEnum::CordbRefEnum(CordbProcess *proc, BOOL walkWeakRefs) - : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacksFQ(TRUE), + : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacks(TRUE), mHandleMask((UINT32)(walkWeakRefs ? CorHandleAll : CorHandleStrongOnly)) { } CordbRefEnum::CordbRefEnum(CordbProcess *proc, CorGCReferenceType types) - : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacksFQ(FALSE), + : CordbBase(proc, 0, enumCordbHeap), mRefHandle(0), mEnumStacks(FALSE), mHandleMask((UINT32)types) { } @@ -2657,7 +2657,7 @@ HRESULT CordbRefEnum::Next(ULONG celt, COR_GC_REFERENCE refs[], ULONG *pceltFetc EX_TRY { if (!mRefHandle) - hr = process->GetDAC()->CreateRefWalk(&mRefHandle, mEnumStacksFQ, mEnumStacksFQ, mHandleMask); + hr = process->GetDAC()->CreateRefWalk(&mRefHandle, mEnumStacks, mHandleMask); if (SUCCEEDED(hr)) { diff --git a/src/coreclr/debug/di/rspriv.h b/src/coreclr/debug/di/rspriv.h index c61d532dde6680..91e6b24b055859 100644 --- a/src/coreclr/debug/di/rspriv.h +++ b/src/coreclr/debug/di/rspriv.h @@ -10618,7 +10618,7 @@ class CordbRefEnum : public CordbBase, public ICorDebugGCReferenceEnum private: RefWalkHandle mRefHandle; - BOOL mEnumStacksFQ; + BOOL mEnumStacks; UINT32 mHandleMask; }; diff --git a/src/coreclr/debug/inc/dacdbiinterface.h b/src/coreclr/debug/inc/dacdbiinterface.h index 5665c617629706..ff9f25c38fdd8b 100644 --- a/src/coreclr/debug/inc/dacdbiinterface.h +++ b/src/coreclr/debug/inc/dacdbiinterface.h @@ -2012,13 +2012,12 @@ IDacDbiInterface : public IUnknown // Parameters: // pHandle - out - the reference walk handle to create // walkStacks - in - whether or not to report stack references - // walkFQ - in - whether or not to report references from the finalizer queue // handleWalkMask - in - the types of handles report (see CorGCReferenceType, cordebug.idl) // Returns: // An HRESULT indicating whether it succeeded or failed. // Exceptions: // Returns an HRESULT indicating success or failure. - virtual HRESULT STDMETHODCALLTYPE CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, BOOL walkFQ, UINT32 handleWalkMask) = 0; + virtual HRESULT STDMETHODCALLTYPE CreateRefWalk(OUT RefWalkHandle * pHandle, BOOL walkStacks, UINT32 handleWalkMask) = 0; // Deletes a reference walk. // Parameters: diff --git a/src/coreclr/inc/dacdbi.idl b/src/coreclr/inc/dacdbi.idl index 700bd4c85e2618..53eeb324371e3e 100644 --- a/src/coreclr/inc/dacdbi.idl +++ b/src/coreclr/inc/dacdbi.idl @@ -384,7 +384,7 @@ interface IDacDbiInterface : IUnknown HRESULT IsValidObject([in] CORDB_ADDRESS obj, [out] BOOL * pResult); // Reference Walking - HRESULT CreateRefWalk([out] RefWalkHandle * pHandle, [in] BOOL walkStacks, [in] BOOL walkFQ, [in] UINT32 handleWalkMask); + HRESULT CreateRefWalk([out] RefWalkHandle * pHandle, [in] BOOL walkStacks, [in] UINT32 handleWalkMask); HRESULT DeleteRefWalk([in] RefWalkHandle handle); HRESULT WalkRefs([in] RefWalkHandle handle, [in] ULONG count, [out] struct DacGcReference * refs, [out] ULONG * pFetched); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs index c3b73db751a660..f3221d7dc1843e 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IStackWalk.cs @@ -37,6 +37,7 @@ public enum StackWalkState public class StackReferenceData { public bool HasRegisterInformation { get; init; } + public bool IsInteriorPointer { get; init; } public int Register { get; init; } public int Offset { get; init; } public TargetPointer Address { get; init; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs index 1a2b8b5b2c226e..a1a983532faaf9 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanContext.cs @@ -34,6 +34,7 @@ public void RecordDeferredFrame(TargetPointer frameAddress) StackRefs.Add(new StackRefData { HasRegisterInformation = false, + IsInteriorPointer = false, Register = 0, Offset = 0, Address = 0, @@ -71,6 +72,7 @@ public void GCEnumCallback(TargetPointer pObject, GcScanFlags flags, GcScanSlotL StackRefData data = new() { HasRegisterInformation = true, + IsInteriorPointer = flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR), Register = loc.Reg, Offset = loc.RegOffset, Address = addr, @@ -109,6 +111,7 @@ public void GCReportCallback(TargetPointer ppObj, GcScanFlags flags) StackRefData data = new() { HasRegisterInformation = false, + IsInteriorPointer = flags.HasFlag(GcScanFlags.GC_CALL_INTERIOR), Register = 0, Offset = 0, Address = ppObj, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs index 46e5bac46f6431..d09499a93c0dae 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/StackRefData.cs @@ -12,6 +12,7 @@ public enum SourceTypes } public bool HasRegisterInformation { get; set; } + public bool IsInteriorPointer { get; set; } public int Register { get; set; } public int Offset { get; set; } public TargetPointer Address { get; set; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs index af08b278df0f8a..9341642d71bf30 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs @@ -271,6 +271,7 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre return scanContext.StackRefs.Select(r => new StackReferenceData { HasRegisterInformation = r.HasRegisterInformation, + IsInteriorPointer = r.IsInteriorPointer, Register = r.Register, Offset = r.Offset, Address = r.Address, 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 5748401c6b3b70..b11219b0b4381d 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 @@ -3531,14 +3531,152 @@ public int IsValidObject(ulong obj, Interop.BOOL* pResult) return hr; } - public int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, Interop.BOOL walkFQ, uint handleWalkMask) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.CreateRefWalk(pHandle, walkStacks, walkFQ, handleWalkMask) : HResults.E_NOTIMPL; + public int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, CorGCReferenceType handleWalkMask) + { + int hr = HResults.S_OK; + RefWalk? walk = null; + try + { + if (pHandle is null) + throw new NullReferenceException(nameof(pHandle)); + walk = new RefWalk(_target, walkStacks != Interop.BOOL.FALSE, handleWalkMask); + *pHandle = (nuint)((IEnum)walk).GetHandle(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null) + { + nuint legacyHandle = 0; + int hrLocal = _legacy.CreateRefWalk(&legacyHandle, walkStacks, handleWalkMask); + Debug.ValidateHResult(hr, hrLocal); + if (hrLocal == HResults.S_OK && walk is not null) + walk.LegacyHandle = legacyHandle; + else if (hrLocal == HResults.S_OK) + _legacy.DeleteRefWalk(legacyHandle); + } +#endif + return hr; + } public int DeleteRefWalk(nuint handle) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.DeleteRefWalk(handle) : HResults.E_NOTIMPL; + { + if (handle == 0) + return HResults.S_OK; + + int hr = HResults.S_OK; + nuint legacyHandle = 0; + try + { + GCHandle gcHandle = GCHandle.FromIntPtr((nint)handle); + if (gcHandle.Target is not RefWalk walk) + throw new ArgumentException("Handle does not reference a valid RefWalk instance.", nameof(handle)); + legacyHandle = walk.LegacyHandle; + ((IEnum)walk).Dispose(); + gcHandle.Free(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null && legacyHandle != 0) + { + int hrLocal = _legacy.DeleteRefWalk(legacyHandle); + Debug.ValidateHResult(hr, hrLocal); + } +#endif + return hr; + } - public int WalkRefs(nuint handle, uint count, nint refs, uint* pFetched) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.WalkRefs(handle, count, refs, pFetched) : HResults.E_NOTIMPL; + // Should be called repeatedly until it returns S_FALSE. + public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetched) + { + RefWalk walk; + try + { + if (pFetched is null) + throw new NullReferenceException(nameof(pFetched)); + if (refs is null && count > 0) + throw new NullReferenceException(nameof(refs)); + if (handle == 0) + throw new ArgumentException("Handle is invalid.", nameof(handle)); + GCHandle gcHandle = GCHandle.FromIntPtr((nint)handle); + if (gcHandle.Target is not RefWalk rw) + throw new ArgumentException("Handle does not reference a valid RefWalk instance.", nameof(handle)); + walk = rw; + *pFetched = 0; + } + catch (System.Exception ex) + { + return ex.HResult; + } + + int hr = HResults.S_OK; + uint i = 0; + try + { + while (i < count && walk.Enumerator.MoveNext()) + refs[i++] = walk.Enumerator.Current; + + // A clean batch reports S_FALSE iff we couldn't fill the caller's request. + if (i < count) + hr = HResults.S_FALSE; + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + *pFetched = i; + +#if DEBUG + if (_legacy is not null && walk.LegacyHandle != 0 && count > 0) + { + // Parity check covers the handle prefix only. + DacGcReference* legacyRefs = stackalloc DacGcReference[(int)count]; + uint legacyFetched = 0; + int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); + Debug.ValidateHResult(hr, hrLocal); + + uint cdacHandlePrefix = CountHandlePrefix(refs, i); + uint legacyHandlePrefix = CountHandlePrefix(legacyRefs, legacyFetched); + Debug.Assert( + cdacHandlePrefix == legacyHandlePrefix, + $"cDAC handle-prefix count {cdacHandlePrefix}, legacy {legacyHandlePrefix}"); + + uint compare = Math.Min(cdacHandlePrefix, legacyHandlePrefix); + for (uint j = 0; j < compare; j++) + { + Debug.Assert(refs[j].dwType == legacyRefs[j].dwType, + $"refs[{j}].dwType cDAC={refs[j].dwType:X}, legacy={legacyRefs[j].dwType:X}"); + Debug.Assert(refs[j].vmDomain == legacyRefs[j].vmDomain, + $"refs[{j}].vmDomain cDAC=0x{refs[j].vmDomain:X}, legacy=0x{legacyRefs[j].vmDomain:X}"); + Debug.Assert(refs[j].objHnd == legacyRefs[j].objHnd, + $"refs[{j}].objHnd cDAC=0x{refs[j].objHnd:X}, legacy=0x{legacyRefs[j].objHnd:X}"); + Debug.Assert(refs[j].i64ExtraData == legacyRefs[j].i64ExtraData, + $"refs[{j}].i64ExtraData cDAC=0x{refs[j].i64ExtraData:X}, legacy=0x{legacyRefs[j].i64ExtraData:X}"); + } + } + + static uint CountHandlePrefix(DacGcReference* buffer, uint length) + { + for (uint j = 0; j < length; j++) + { + CorGCReferenceType dwType = buffer[j].dwType; + if (dwType == CorGCReferenceType.CorReferenceStack) + { + return j; + } + } + return length; + } +#endif + + return hr; + } public int GetTypeID(ulong obj, COR_TYPEID* pType) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs new file mode 100644 index 00000000000000..6d9813b77254c5 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/RefWalk.cs @@ -0,0 +1,167 @@ +// 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; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +/// +/// cDAC port of the native DacRefWalker. +/// +internal sealed class RefWalk : IEnum +{ + private const uint CDAC_DEFERRED_FRAME = 0x40000000; + private readonly Target _target; + private readonly IGC _gc; + private readonly bool _walkStacks; + private readonly CorGCReferenceType _handleWalkMask; + + public IEnumerator Enumerator { get; } + public nuint LegacyHandle { get; set; } = 0; + + public RefWalk(Target target, bool walkStacks, CorGCReferenceType handleWalkMask) + { + _target = target; + _gc = target.Contracts.GC; + _walkStacks = walkStacks; + _handleWalkMask = handleWalkMask; + Enumerator = Walk().GetEnumerator(); + } + + private IEnumerable Walk() + { + // The single AppDomain pointer; used to fill vmDomain for both handle and stack references. + TargetPointer appDomain = _target.ReadPointer(_target.ReadGlobalPointer(Constants.Globals.AppDomain)); + + if (_handleWalkMask != 0) + { + foreach (DacGcReference reference in WalkHandles(appDomain)) + yield return reference; + } + + if (_walkStacks) + { + foreach (DacGcReference reference in WalkStacks(appDomain)) + yield return reference; + } + } + + private IEnumerable WalkHandles(TargetPointer appDomain) + { + HandleType[] requestedTypes = GetRequestedHandleTypes(); + if (requestedTypes.Length == 0) + yield break; + + foreach (HandleData handle in _gc.GetHandles(requestedTypes)) + { + if (!TryMapHandle(handle, out CorGCReferenceType dwType, out ulong extraData)) + continue; + yield return new DacGcReference + { + vmDomain = appDomain.Value, + objHnd = handle.Handle.Value, + dwType = dwType, + i64ExtraData = extraData, + }; + } + } + + private HandleType[] GetRequestedHandleTypes() + { + // Mirror native DacRefWalker::GetHandleWalkerMask: translate the CorGCReferenceType bits + // in the mask into the handle types consumed by IGC.GetHandles. + List types = new(); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrong)) + types.Add(HandleType.Strong); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongPinning)) + types.Add(HandleType.Pinned); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakShort)) + types.Add(HandleType.WeakShort); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakLong)) + types.Add(HandleType.WeakLong); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleWeakRefCount) || _handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongRefCount)) + types.Add(HandleType.RefCounted); + if (_handleWalkMask.HasFlag(CorGCReferenceType.CorHandleStrongDependent)) + types.Add(HandleType.Dependent); + + if (types.Count == 0) + return []; + + // Only request types the target actually supports + HashSet supported = new(_gc.GetSupportedHandleTypes()); + types.RemoveAll(t => !supported.Contains(t)); + return types.ToArray(); + } + + private bool TryMapHandle(HandleData handle, out CorGCReferenceType dwType, out ulong extraData) + { + extraData = 0; + switch (_gc.GetHandleTypes([handle.Type])[0]) + { + case HandleType.Strong: + dwType = CorGCReferenceType.CorHandleStrong; + return true; + case HandleType.Pinned: + dwType = CorGCReferenceType.CorHandleStrongPinning; + return true; + case HandleType.WeakShort: + dwType = CorGCReferenceType.CorHandleWeakShort; + return true; + case HandleType.WeakLong: + dwType = CorGCReferenceType.CorHandleWeakLong; + return true; + case HandleType.RefCounted: + extraData = handle.RefCount; + dwType = handle.RefCount != 0 + ? CorGCReferenceType.CorHandleStrongRefCount + : CorGCReferenceType.CorHandleWeakRefCount; + return true; + case HandleType.Dependent: + dwType = CorGCReferenceType.CorHandleStrongDependent; + extraData = handle.Secondary.Value; + return true; + default: + dwType = 0; + return false; + } + } + + private IEnumerable WalkStacks(TargetPointer appDomain) + { + IThread threadContract = _target.Contracts.Thread; + IStackWalk stackWalkContract = _target.Contracts.StackWalk; + + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData threadData = threadContract.GetThreadData(threadAddr); + + foreach (StackReferenceData stackRef in stackWalkContract.WalkStackReferences(threadData)) + { + // Skip cDAC-private deferred-frame markers; they are not real GC references. + if ((stackRef.Flags & CDAC_DEFERRED_FRAME) != 0) + continue; + + DacGcReference reference = new() + { + vmDomain = appDomain.Value, + dwType = CorGCReferenceType.CorReferenceStack, + i64ExtraData = 0, + }; + + // Interior pointers, Frame refs, and enregistered vars are reported as a direct object pointer with the low bit set; + // everything else is reported by the address of the stack slot holding the object. + if (stackRef.IsInteriorPointer || stackRef.Address == TargetPointer.Null) + reference.pObject = stackRef.Object.Value | 1; + else + reference.objHnd = stackRef.Address.Value; + + yield return reference; + } + + threadAddr = threadData.NextThread; + } + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index 48f9c08fe109b8..e79582288b63ea 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -130,6 +130,16 @@ public struct COR_HEAPOBJECT public COR_TYPEID type; } +[StructLayout(LayoutKind.Explicit)] +public struct DacGcReference +{ + [FieldOffset(0)] public ulong vmDomain; + [FieldOffset(8)] public ulong pObject; + [FieldOffset(8)] public ulong objHnd; + [FieldOffset(16)] public CorGCReferenceType dwType; + [FieldOffset(24)] public ulong i64ExtraData; +} + [StructLayout(LayoutKind.Sequential)] public struct COR_SEGMENT { @@ -351,6 +361,19 @@ public enum IlNum : int TYPECTXT_ILNUM = -3, } +[Flags] +public enum CorGCReferenceType : uint +{ + CorHandleStrong = 1 << 0, + CorHandleStrongPinning = 1 << 1, + CorHandleWeakShort = 1 << 2, + CorHandleWeakLong = 1 << 3, + CorHandleWeakRefCount = 1 << 4, + CorHandleStrongRefCount = 1 << 5, + CorHandleStrongDependent = 1 << 6, + CorReferenceStack = 0x80000001, +} + // Name-surface projection of IDacDbiInterface in native method order for COM binding validation. // Parameter shapes are intentionally coarse placeholders and will be refined with method implementation work. [GeneratedComInterface] @@ -669,13 +692,13 @@ int EnumerateTypeHandleParams(ulong vmTypeHandle, int IsValidObject(ulong obj, Interop.BOOL* pResult); [PreserveSig] - int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, Interop.BOOL walkFQ, uint handleWalkMask); + int CreateRefWalk(nuint* pHandle, Interop.BOOL walkStacks, CorGCReferenceType handleWalkMask); [PreserveSig] int DeleteRefWalk(nuint handle); [PreserveSig] - int WalkRefs(nuint handle, uint count, nint refs, uint* pFetched); + int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetched); [PreserveSig] int GetTypeID(ulong obj, COR_TYPEID* pType); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs new file mode 100644 index 00000000000000..fe4cdaaa6ee255 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -0,0 +1,123 @@ +// 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.Diagnostics.DataContractReader.TestInfrastructure; +using Xunit; + +namespace Microsoft.Diagnostics.DataContractReader.DumpTests; + +/// +/// Dump-based integration tests for the ref-walking APIs +/// ( / / +/// ), cross-validated against the GC handle table +/// and the stack-reference walk in the GCRoots debuggee. +/// +public class DacDbiRefWalkDumpTests : DumpTestBase +{ + protected override string DebuggeeName => "GCRoots"; + + private DacDbiImpl CreateDacDbi() => new DacDbiImpl(Target, legacyObj: null); + + /// + /// Drives to completion and returns every reference reported. + /// + private static unsafe List WalkAllRefs(DacDbiImpl dbi, bool walkStacks, CorGCReferenceType handleWalkMask, uint batchSize = 32) + { + List refs = new(); + + nuint handle = 0; + int hr = dbi.CreateRefWalk(&handle, walkStacks ? Interop.BOOL.TRUE : Interop.BOOL.FALSE, handleWalkMask); + Assert.Equal(System.HResults.S_OK, hr); + Assert.True(handle != 0, "CreateRefWalk produced a null handle"); + + try + { + DacGcReference[] buffer = new DacGcReference[batchSize]; + while (true) + { + uint fetched = 0; + int walkHr; + fixed (DacGcReference* bufPtr = buffer) + { + walkHr = dbi.WalkRefs(handle, batchSize, bufPtr, &fetched); + } + + Assert.True( + walkHr == System.HResults.S_OK || walkHr == System.HResults.S_FALSE, + $"WalkRefs returned 0x{walkHr:x}"); + + for (uint i = 0; i < fetched; i++) + refs.Add(buffer[i]); + + if (walkHr == System.HResults.S_FALSE) + break; + } + } + finally + { + int delHr = dbi.DeleteRefWalk(handle); + Assert.Equal(System.HResults.S_OK, delHr); + } + + return refs; + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void WalkRefs_StrongHandles_MatchHandleTable(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IGC gc = Target.Contracts.GC; + + List refs = WalkAllRefs(dbi, walkStacks: false, handleWalkMask: CorGCReferenceType.CorHandleStrong); + + // Every reference must be a strong handle reported by its (low-bit-clear) handle address. + HashSet walkedHandles = new(); + foreach (DacGcReference r in refs) + { + Assert.Equal(CorGCReferenceType.CorHandleStrong, r.dwType); + Assert.Equal(0ul, r.pObject & 1); + walkedHandles.Add(r.pObject); + } + + HashSet expectedHandles = gc.GetHandles([HandleType.Strong]).Select(h => h.Handle.Value).ToHashSet(); + Assert.True(expectedHandles.Count > 0, "Expected at least one strong handle in GCRoots."); + Assert.Equal(expectedHandles, walkedHandles); + } + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + public unsafe void WalkRefs_StacksOnly_MatchStackReferenceWalk(TestConfiguration config) + { + InitializeDumpTest(config); + DacDbiImpl dbi = CreateDacDbi(); + IThread threadContract = Target.Contracts.Thread; + IStackWalk stackWalk = Target.Contracts.StackWalk; + + List refs = WalkAllRefs(dbi, walkStacks: true, handleWalkMask: CorGCReferenceType.CorReferenceStack); + + // Compute the expected count of stack references directly (excluding cDAC-private + // deferred-frame markers, which WalkRefs filters out). + // Mirrors StackWalkHelpers.GcScanFlags.CDAC_DEFERRED_FRAME (internal to the Contracts assembly). + const uint CdacDeferredFrame = 0x40000000; + int expected = 0; + ThreadStoreData threadStore = threadContract.GetThreadStoreData(); + TargetPointer threadAddr = threadStore.FirstThread; + while (threadAddr != TargetPointer.Null) + { + ThreadData td = threadContract.GetThreadData(threadAddr); + expected += stackWalk.WalkStackReferences(td).Count(r => (r.Flags & CdacDeferredFrame) == 0); + threadAddr = td.NextThread; + } + + foreach (DacGcReference r in refs) + Assert.Equal(CorGCReferenceType.CorReferenceStack, r.dwType); + + Assert.Equal(expected, refs.Count); + } +} From 71d7a3baa3d776604f866df4d5308bc5dd2ac2d5 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 12 Jun 2026 14:54:53 -0700 Subject: [PATCH 2/4] code review --- .../Dbi/DacDbiImpl.cs | 2 +- .../cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 b11219b0b4381d..35932c8489f18b 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 @@ -3636,7 +3636,7 @@ public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetch if (_legacy is not null && walk.LegacyHandle != 0 && count > 0) { // Parity check covers the handle prefix only. - DacGcReference* legacyRefs = stackalloc DacGcReference[(int)count]; + DacGcReference* legacyRefs = new DacGcReference[(int)count]; uint legacyFetched = 0; int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); Debug.ValidateHResult(hr, hrLocal); diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs index fe4cdaaa6ee255..f70c03764a5345 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -99,7 +99,7 @@ public unsafe void WalkRefs_StacksOnly_MatchStackReferenceWalk(TestConfiguration IThread threadContract = Target.Contracts.Thread; IStackWalk stackWalk = Target.Contracts.StackWalk; - List refs = WalkAllRefs(dbi, walkStacks: true, handleWalkMask: CorGCReferenceType.CorReferenceStack); + List refs = WalkAllRefs(dbi, walkStacks: true, handleWalkMask: (CorGCReferenceType)0); // Compute the expected count of stack references directly (excluding cDAC-private // deferred-frame markers, which WalkRefs filters out). From 89e8153569b884f70cdf8281744d79ab4173f28b Mon Sep 17 00:00:00 2001 From: rcj1 Date: Fri, 12 Jun 2026 17:06:35 -0700 Subject: [PATCH 3/4] use marshal count --- .../Dbi/DacDbiImpl.cs | 8 +++----- .../Dbi/IDacDbiInterface.cs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) 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 35932c8489f18b..61ef4cac59c432 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 @@ -3592,15 +3592,13 @@ public int DeleteRefWalk(nuint handle) } // Should be called repeatedly until it returns S_FALSE. - public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetched) + public int WalkRefs(nuint handle, uint count, [In, MarshalUsing(CountElementName = "count"), Out] DacGcReference[] refs, uint* pFetched) { RefWalk walk; try { if (pFetched is null) throw new NullReferenceException(nameof(pFetched)); - if (refs is null && count > 0) - throw new NullReferenceException(nameof(refs)); if (handle == 0) throw new ArgumentException("Handle is invalid.", nameof(handle)); GCHandle gcHandle = GCHandle.FromIntPtr((nint)handle); @@ -3636,7 +3634,7 @@ public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetch if (_legacy is not null && walk.LegacyHandle != 0 && count > 0) { // Parity check covers the handle prefix only. - DacGcReference* legacyRefs = new DacGcReference[(int)count]; + DacGcReference[] legacyRefs = new DacGcReference[(int)count]; uint legacyFetched = 0; int hrLocal = _legacy.WalkRefs(walk.LegacyHandle, count, legacyRefs, &legacyFetched); Debug.ValidateHResult(hr, hrLocal); @@ -3661,7 +3659,7 @@ public int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetch } } - static uint CountHandlePrefix(DacGcReference* buffer, uint length) + static uint CountHandlePrefix(DacGcReference[] buffer, uint length) { for (uint j = 0; j < length; j++) { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs index e79582288b63ea..77ffc82120543c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/IDacDbiInterface.cs @@ -698,7 +698,7 @@ int EnumerateTypeHandleParams(ulong vmTypeHandle, int DeleteRefWalk(nuint handle); [PreserveSig] - int WalkRefs(nuint handle, uint count, DacGcReference* refs, uint* pFetched); + int WalkRefs(nuint handle, uint count, [In, Out, MarshalUsing(CountElementName = nameof(count))] DacGcReference[] refs, uint* pFetched); [PreserveSig] int GetTypeID(ulong obj, COR_TYPEID* pType); From d1ea1e24c2c7d490410eb933ce51be3a4256b072 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Fri, 12 Jun 2026 17:19:39 -0700 Subject: [PATCH 4/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs index f70c03764a5345..0aa63e18b0c527 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiRefWalkDumpTests.cs @@ -40,17 +40,13 @@ private static unsafe List WalkAllRefs(DacDbiImpl dbi, bool walk while (true) { uint fetched = 0; - int walkHr; - fixed (DacGcReference* bufPtr = buffer) - { - walkHr = dbi.WalkRefs(handle, batchSize, bufPtr, &fetched); - } + int walkHr = dbi.WalkRefs(handle, (uint)batchSize, buffer, &fetched); Assert.True( walkHr == System.HResults.S_OK || walkHr == System.HResults.S_FALSE, $"WalkRefs returned 0x{walkHr:x}"); - for (uint i = 0; i < fetched; i++) + for (int i = 0; i < (int)fetched; i++) refs.Add(buffer[i]); if (walkHr == System.HResults.S_FALSE)