diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 29e13c20a4be6c..1fecac7ab458c1 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -118,6 +118,11 @@ This contract depends on the following descriptors: | `ExceptionInfo` | `PassNumber` | Exception handling pass (1 or 2) | | `ExceptionInfo` | `ClauseForCatchHandlerStartPC` | Start PC offset of the catch handler clause, used for interruptible offset override | | `ExceptionInfo` | `ClauseForCatchHandlerEndPC` | End PC offset of the catch handler clause, used for interruptible offset override | +| `Thread` | `GCFrame` | Pointer to the head of the thread's GCFrame (GCPROTECT) chain, scanned by `WalkStackReferences` (optional) | +| `GCFrame` | `Next` | Pointer to the next `GCFrame` toward the top of the chain (terminated by `GCFRAME_TOP`) | +| `GCFrame` | `ObjRefs` | Pointer to the array of protected object reference slots | +| `GCFrame` | `NumObjRefs` | Count of protected object reference slots starting at `ObjRefs` | +| `GCFrame` | `GCFlags` | `GC_CALL_*` promotion flags applied when reporting the protected slots | Global variables used: | Global Name | Type | Purpose | @@ -137,6 +142,7 @@ Contracts used: | `Thread` | | `RuntimeTypeSystem` | | `GCInfo` | +| `Exception` | ### Stackwalk Algorithm @@ -555,7 +561,7 @@ The implementation uses the same stack walk algorithm as `CreateStackWalk`, but ### GC Stack Reference Scanning -`WalkStackReferences` scans the stack for GC references by walking through each frame and reporting live object references and interior pointers. The native equivalent is `DacStackReferenceWalker` which calls `GcStackCrawlCallBack` at each frame. +`WalkStackReferences` scans the stack for GC references by walking through each frame and reporting live object references and interior pointers, then reporting the thread's GCFrame (GCPROTECT) chain and in-flight exception (ExInfo) chain. This mirrors the GC's own root enumeration, `ScanStackRoots` (`src/coreclr/vm/gcenv.ee.cpp`); the legacy `DacStackReferenceWalker` reports only the per-frame subset and omits the GCFrame and ExInfo roots. #### Stack Walk Integration @@ -589,6 +595,15 @@ At each frame yielded by `Filter`, the walk determines whether to scan for GC re See [GCRefMap Format and Resolution](#gcrefmap-format-and-resolution) for the GCRefMap scanning path and [Signature-Based Scanning](#signature-based-scanning) for the signature decoding path. +#### GCFrame and Exception Tracker Roots + +After walking the thread's frames, `WalkStackReferences` reports two additional sets of roots that the GC keeps alive but that are not surfaced by per-frame GC info (matching native `gcenv.ee.cpp` `ScanStackRoots`): + +- **GCFrame (GCPROTECT) chain**: starting from `Thread.GCFrame`, each `GCFrame` is walked via its `Next` pointer until the `GCFRAME_TOP` terminator (`~0`, sized to the pointer width). For each node, the `NumObjRefs` slots starting at `ObjRefs` are reported, applying the node's `GCFlags` (`GC_CALL_INTERIOR` / `GC_CALL_PINNED`) as the promotion flags. This mirrors native `GCFrame::GcScanRoots`. +- **Exception tracker (ExInfo) chain**: starting from `Thread.ExceptionTracker`, each in-flight exception object (the current one and any superseded/nested ones reached via `PreviousNestedInfo`) is reported through its thrown-object slot. + +Both sets are reported as frame-sourced roots: `Source` is the GCFrame / ExInfo node address, and `StackPointer` is that same address (the node lives on the stack), so the roots carry a non-zero, stack-resident location consistent with the per-frame roots. Each helper is independently wrapped in a try/catch so a single unreadable node yields partial results rather than failing the whole walk. + ### Signature-Based Scanning When a transition frame's calling convention is not described by a precomputed GCRefMap (`PrestubMethodFrame`, `CallCountingHelperFrame`, and the fallback path for `StubDispatchFrame`/`ExternalMethodFrame`), the GC reference walk classifies caller-stack arguments by decoding the callee's method signature. This corresponds to native `TransitionFrame::PromoteCallerStack` (`src/coreclr/vm/frames.cpp`). diff --git a/docs/design/datacontracts/Thread.md b/docs/design/datacontracts/Thread.md index b781ebfc7ea77e..9ce690983e2c13 100644 --- a/docs/design/datacontracts/Thread.md +++ b/docs/design/datacontracts/Thread.md @@ -122,6 +122,7 @@ The contract additionally depends on these data descriptors | `Thread` | `DebuggerControlledThreadState` | Thread state flags controlled by the debugger | | `Thread` | `PreemptiveGCDisabled` | Flag indicating if preemptive GC is disabled | | `Thread` | `Frame` | Pointer to current frame | +| `Thread` | `GCFrame` | Pointer to the head of the thread's GCFrame (GCPROTECT) chain (optional; readers should expect `TargetPointer.Null` when the field is absent) | | `Thread` | `CachedStackBase` | Pointer to the base of the stack | | `Thread` | `CachedStackLimit` | Pointer to the limit of the stack | | `Thread` | `ExposedObject` | Handle to the managed `Thread` object exposed to the debugger | diff --git a/src/coreclr/vm/cdacstress.cpp b/src/coreclr/vm/cdacstress.cpp index b750c069891af0..1f7743a09aeb79 100644 --- a/src/coreclr/vm/cdacstress.cpp +++ b/src/coreclr/vm/cdacstress.cpp @@ -692,10 +692,24 @@ static bool CollectRuntimeStackRefs(Thread* pThread, PCONTEXT regs, StackRef* ou pThread->StackWalkFrames(dacLikeCallback, &diagCtx, flagsStackWalk); - // NOTE: ScanStackRoots also scans the separate GCFrame linked list - // (Thread::GetGCFrame), but the DAC's GetStackReferences / DacStackReferenceWalker - // does NOT include those. We intentionally omit GCFrame scanning here so our - // runtime-side collection matches what the cDAC is expected to produce. + // ScanStackRoots also scans two root sets that are not part of the frame walk: the + // GCFrame (GCPROTECT) chain and the in-flight ExInfo chain. GetStackReferences reports + // both, so mirror them here to keep the runtime-side collection in parity. See + // ScanStackRoots in gcenv.ee.cpp. + GCFrame* pGCFrame = pThread->GetGCFrame(); + while (pGCFrame != GCFRAME_TOP) + { + pGCFrame->GcScanRoots(gcctx.f, gcctx.sc); + pGCFrame = pGCFrame->PtrNextFrame(); + } + + PTR_ExInfo pExInfo = pThread->GetExceptionState()->GetCurrentExceptionTracker(); + while (pExInfo != NULL) + { + PTR_PTR_Object pRef = dac_cast(&pExInfo->m_exception); + gcctx.f(pRef, gcctx.sc, 0); + pExInfo = pExInfo->GetPreviousExceptionTracker(); + } // Copy results out *outCount = collectCtx.count; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 813180a13e6b14..b16cd621ddd9f3 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -41,6 +41,7 @@ CDAC_TYPE_FIELD(Thread, T_UINT32, State, cdac_data::State) CDAC_TYPE_FIELD(Thread, T_UINT32, PreemptiveGCDisabled, cdac_data::PreemptiveGCDisabled) CDAC_TYPE_FIELD(Thread, T_POINTER, RuntimeThreadLocals, cdac_data::RuntimeThreadLocals) CDAC_TYPE_FIELD(Thread, T_POINTER, Frame, cdac_data::Frame) +CDAC_TYPE_FIELD(Thread, T_POINTER, GCFrame, cdac_data::GCFrame) CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackBase, cdac_data::CachedStackBase) CDAC_TYPE_FIELD(Thread, T_POINTER, CachedStackLimit, cdac_data::CachedStackLimit) CDAC_TYPE_FIELD(Thread, T_POINTER, ExceptionTracker, cdac_data::ExceptionTracker) @@ -70,6 +71,14 @@ CDAC_TYPE_FIELD(ThreadStore, T_INT32, PendingCount, cdac_data::Pend CDAC_TYPE_FIELD(ThreadStore, T_INT32, DeadCount, cdac_data::DeadCount) CDAC_TYPE_END(ThreadStore) +CDAC_TYPE_BEGIN(GCFrame) +CDAC_TYPE_INDETERMINATE(GCFrame) +CDAC_TYPE_FIELD(GCFrame, T_POINTER, Next, cdac_data::Next) +CDAC_TYPE_FIELD(GCFrame, T_POINTER, ObjRefs, cdac_data::ObjRefs) +CDAC_TYPE_FIELD(GCFrame, T_UINT32, NumObjRefs, cdac_data::NumObjRefs) +CDAC_TYPE_FIELD(GCFrame, T_UINT32, GCFlags, cdac_data::GCFlags) +CDAC_TYPE_END(GCFrame) + CDAC_TYPE_BEGIN(RuntimeThreadLocals) CDAC_TYPE_INDETERMINATE(RuntimeThreadLocals) CDAC_TYPE_FIELD(RuntimeThreadLocals, TYPE(EEAllocContext), AllocContext, offsetof(RuntimeThreadLocals, alloc_context)) diff --git a/src/coreclr/vm/frames.h b/src/coreclr/vm/frames.h index 74a3c131e8e2e7..898717d16cfc1a 100644 --- a/src/coreclr/vm/frames.h +++ b/src/coreclr/vm/frames.h @@ -1764,6 +1764,17 @@ class GCFrame #ifdef FEATURE_INTERPRETER PTR_VOID m_osStackLocation; #endif + + friend struct ::cdac_data; +}; + +template<> +struct cdac_data +{ + static constexpr size_t Next = offsetof(GCFrame, m_Next); + static constexpr size_t ObjRefs = offsetof(GCFrame, m_pObjRefs); + static constexpr size_t NumObjRefs = offsetof(GCFrame, m_numObjRefs); + static constexpr size_t GCFlags = offsetof(GCFrame, m_gcFlags); }; //----------------------------------------------------------------------------- diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index ddf01bf860e0e0..c4e605646c4cc8 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -3766,6 +3766,7 @@ struct cdac_data static constexpr size_t PreemptiveGCDisabled = offsetof(Thread, m_fPreemptiveGCDisabled); static constexpr size_t RuntimeThreadLocals = offsetof(Thread, m_pRuntimeThreadLocals); static constexpr size_t Frame = offsetof(Thread, m_pFrame); + static constexpr size_t GCFrame = offsetof(Thread, m_pGCFrame); static constexpr size_t CachedStackBase = offsetof(Thread, m_CacheStackBase); static constexpr size_t CachedStackLimit = offsetof(Thread, m_CacheStackLimit); static constexpr size_t ExposedObject = offsetof(Thread, m_ExposedObject); 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 5de2562d36461f..dd45e816c58137 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 @@ -292,6 +292,14 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre } } + // Report the thread's GCFrame (GCPROTECT) chain: each GCFrame keeps a set of object + // references alive across a runtime operation, so report them as roots. + ReportGCFrameRoots(threadData, scanContext); + + // Report the thread's exception-tracking (ExInfo) chain: the current in-flight exception + // and any superseded/nested ones are kept alive by the runtime, so report them as roots. + ReportExceptionTrackerRoots(threadData, scanContext); + return scanContext.StackRefs.Select(r => new StackReferenceData { HasRegisterInformation = r.HasRegisterInformation, @@ -306,6 +314,80 @@ IReadOnlyList IStackWalk.WalkStackReferences(ThreadData thre }).ToList(); } + // Reports each in-flight exception object held on the thread's exception-tracking (ExInfo) + // chain: the current exception and any superseded/nested ones. The GC reports the same set in + // gcenv.ee.cpp ScanStackRoots. + private void ReportExceptionTrackerRoots(ThreadData threadData, GcScanContext scanContext) + { + try + { + Data.Thread thread = _target.ProcessedData.GetOrAdd(threadData.ThreadAddress); + TargetPointer pExInfo = _target.ReadPointer(thread.ExceptionTracker); + if (pExInfo == TargetPointer.Null) + return; + + IException exceptionContract = _target.Contracts.Exception; + HashSet seen = new(); + while (pExInfo != TargetPointer.Null && seen.Add(pExInfo)) + { + // GetNestedExceptionInfo yields the address of the thrown-object slot (ExInfo::m_exception) + // and the previous (nested) ExInfo; GCReportCallback reads the object through that slot. + // The ExInfo lives on the stack, so report its address as the StackPointer: native + // (DacStackReferenceWalker) reports frame-sourced roots with a non-zero SP, and consumers + // (SOSDacImpl.GetStackReferences) forward it. + exceptionContract.GetNestedExceptionInfo(pExInfo, out TargetPointer previous, out TargetPointer thrownObjectSlot); + scanContext.UpdateScanContext(pExInfo, TargetPointer.Null, pExInfo); + scanContext.GCReportCallback(thrownObjectSlot, GcScanFlags.None); + pExInfo = previous; + } + } + catch (System.Exception ex) + { + Debug.WriteLine($"Exception during {nameof(ReportExceptionTrackerRoots)}: {ex.GetType().Name}: {ex.Message}"); + } + } + + // Reports each object reference protected by the thread's GCFrame (GCPROTECT) chain. + // GCFrame::GcScanRoots reports m_pObjRefs[0..m_numObjRefs), using an interior promotion when + // m_gcFlags != 0; the GC reports the same set in gcenv.ee.cpp ScanStackRoots. + private void ReportGCFrameRoots(ThreadData threadData, GcScanContext scanContext) + { + try + { + Data.Thread thread = _target.ProcessedData.GetOrAdd(threadData.ThreadAddress); + + // The GCFrame field is optional in the data contract; nothing to scan when the target + // does not describe it. + if (thread.GCFrame is not TargetPointer head) + return; + + // The chain is terminated by GCFRAME_TOP (FRAME_TOP_VALUE == ~0), sized to the pointer width. + TargetPointer terminator = TargetPointer.PlatformMaxValue(_target); + ulong pointerSize = (ulong)_target.PointerSize; + HashSet seen = new(); + TargetPointer pGCFrame = head; + while (pGCFrame != TargetPointer.Null && pGCFrame != terminator && seen.Add(pGCFrame)) + { + Data.GCFrame gcFrame = _target.ProcessedData.GetOrAdd(pGCFrame); + // The GCFrame lives on the stack, so report its address as the StackPointer: native + // (DacStackReferenceWalker) reports frame-sourced roots with a non-zero SP, and consumers + // (SOSDacImpl.GetStackReferences) forward it. + scanContext.UpdateScanContext(pGCFrame, TargetPointer.Null, pGCFrame); + GcScanFlags flags = (GcScanFlags)gcFrame.GCFlags; + for (uint i = 0; i < gcFrame.NumObjRefs; i++) + { + TargetPointer slot = new(gcFrame.ObjRefs.Value + (ulong)i * pointerSize); + scanContext.GCReportCallback(slot, flags); + } + pGCFrame = gcFrame.Next; + } + } + catch (System.Exception ex) + { + Debug.WriteLine($"Exception during {nameof(ReportGCFrameRoots)}: {ex.GetType().Name}: {ex.Message}"); + } + } + private record GCFrameData { public GCFrameData(StackDataFrameHandle frame) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GCFrame.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GCFrame.cs new file mode 100644 index 00000000000000..ae436263ca1192 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/GCFrame.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Diagnostics.DataContractReader.Data; + +[CdacType(nameof(DataType.GCFrame))] +internal sealed partial class GCFrame : IData +{ + [Field] public TargetPointer Next { get; } + [Field] public TargetPointer ObjRefs { get; } + [Field] public uint NumObjRefs { get; } + [Field] public uint GCFlags { get; } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs index 98558beab9c3f1..a7d3bd996efc66 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Thread.cs @@ -12,6 +12,8 @@ internal sealed partial class Thread : IData [Field(Writable = true)] public uint DebuggerControlledThreadState { get; private set; } [Field] public uint PreemptiveGCDisabled { get; } [Field] public TargetPointer Frame { get; } + // Optional: only targets that describe the GCFrame chain carry this field. + [Field] public TargetPointer? GCFrame { get; } [Field] public TargetPointer CachedStackBase { get; } [Field] public TargetPointer CachedStackLimit { get; } [Field] public ObjectHandle ExposedObject { get; } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs index 145a2278801878..107fde1ebfd15a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -160,6 +160,7 @@ public enum DataType HijackArgs, Frame, + GCFrame, InlinedCallFrame, SoftwareExceptionFrame, FramedMethodFrame, diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/GCProtect/GCProtect.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCProtect/GCProtect.csproj new file mode 100644 index 00000000000000..bb776824769fe6 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCProtect/GCProtect.csproj @@ -0,0 +1,5 @@ + + + Full + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/GCProtect/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCProtect/Program.cs new file mode 100644 index 00000000000000..db0cc0bc8a2b9a --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/GCProtect/Program.cs @@ -0,0 +1,45 @@ +// 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.Reflection; +using System.Runtime.CompilerServices; + +/// +/// Debuggee for cDAC dump tests — exercises GCFrame (GCPROTECT) root reporting. +/// Triggers AppDomain.AssemblyResolve by loading a missing assembly. The native +/// AppDomain::RaiseAssemblyResolveEvent invokes the managed handler while holding a +/// GCPROTECT frame over the requesting Assembly reference. The handler FailFasts so the +/// dump captures the thread with that GCFrame still live. The GC reports the GCFrame's protected +/// objects via GCFrame::GcScanRoots; the test verifies WalkStackReferences reports them too. +/// +internal static class Program +{ + private static void Main() + { + AppDomain.CurrentDomain.AssemblyResolve += OnAssemblyResolve; + TriggerResolve(); + Environment.FailFast("cDAC dump test: GCProtect debuggee did not hit the resolve handler"); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void TriggerResolve() + { + try + { + Assembly.Load("cDAC_Missing_Assembly_GCFrameMarker, Version=9.9.9.9, Culture=neutral, PublicKeyToken=null"); + } + catch + { + // Resolution ultimately fails; the crash happens inside the handler below first. + } + } + + private static Assembly? OnAssemblyResolve(object? sender, ResolveEventArgs args) + { + // Runs inside AppDomain::RaiseAssemblyResolveEvent's GCPROTECT(gc) scope, so the + // requesting Assembly reference is held only by that native GCFrame at this point. + Environment.FailFast("cDAC dump test: GCProtect debuggee intentional crash"); + return null; + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/NestedException/NestedException.csproj b/src/native/managed/cdac/tests/DumpTests/Debuggees/NestedException/NestedException.csproj new file mode 100644 index 00000000000000..bb776824769fe6 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/NestedException/NestedException.csproj @@ -0,0 +1,5 @@ + + + Full + + diff --git a/src/native/managed/cdac/tests/DumpTests/Debuggees/NestedException/Program.cs b/src/native/managed/cdac/tests/DumpTests/Debuggees/NestedException/Program.cs new file mode 100644 index 00000000000000..177645d1538127 --- /dev/null +++ b/src/native/managed/cdac/tests/DumpTests/Debuggees/NestedException/Program.cs @@ -0,0 +1,47 @@ +// 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.IO; +using System.Runtime.CompilerServices; + +/// +/// Debuggee for cDAC dump tests — exercises in-flight exception root reporting. +/// Builds a nested exception chain: a superseded +/// (kept alive on the thread's ExInfo chain as the previous nested tracker) plus the +/// current , then crashes via FailFast while both +/// are still in flight. The GC reports these via gcenv.ee.cpp ScanStackRoots; the test verifies +/// WalkStackReferences reports them as stack references. +/// +internal static class Program +{ + private static void Main() + { + try + { + ThrowNested(); + } + catch (Exception current) + { + // 'current' is the InvalidOperationException; the superseded + // FileNotFoundException is still held on the thread's ExInfo chain. + GC.KeepAlive(current); + Environment.FailFast("cDAC dump test: NestedException debuggee intentional crash"); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowNested() + { + try + { + throw new FileNotFoundException("cDAC-NestedException-inner"); + } + catch (FileNotFoundException inner) + { + // Throwing while handling 'inner' supersedes its ExInfo (kept as the + // previous nested tracker) and starts a new tracker for the outer exception. + throw new InvalidOperationException("cDAC-NestedException-outer", inner); + } + } +} diff --git a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs index fe299495996aa4..755735d9c945c0 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackReferenceDumpTests.cs @@ -103,6 +103,110 @@ public void GCRoots_RefsPointToValidObjects(TestConfiguration config) $"Expected at least one stack ref pointing to a valid object (total refs: {refs.Count})"); } + // --- NestedException debuggee: in-flight exception objects reported as roots --- + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + [SkipOnArch("x86", "GCInfo decoder does not support x86")] + public void NestedException_InFlightExceptionsReportedAsRoots(TestConfiguration config) + { + InitializeDumpTest(config, "NestedException", "full"); + IStackWalk stackWalk = Target.Contracts.StackWalk; + IException exceptionContract = Target.Contracts.Exception; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + // FirstNestedException is the previous tracker on the thread's ExInfo chain + // (Thread_1.GetThreadData sets it from currentExInfo.PreviousNestedInfo), so it + // enumerates the superseded / nested in-flight exceptions. These are held only by the + // runtime's exception-tracking chain (the nested FileNotFoundException lives on the heap + // as InvalidOperationException.InnerException, not as a stack local), so the ExInfo scan in + // WalkStackReferences is what surfaces them as roots. + HashSet expected = new(); + HashSet seenTrackers = new(); + TargetPointer exInfo = crashingThread.FirstNestedException; + while (exInfo != TargetPointer.Null && seenTrackers.Add(exInfo)) + { + exceptionContract.GetNestedExceptionInfo(exInfo, out TargetPointer next, out TargetPointer thrownObjectSlot); + TargetPointer obj = Target.ReadPointer(thrownObjectSlot); + if (obj != TargetPointer.Null) + expected.Add(obj); + exInfo = next; + } + + Assert.True(expected.Count >= 1, + $"NestedException debuggee should hold at least one superseded exception on its ExInfo chain; found {expected.Count}"); + + // WalkStackReferences must surface every in-flight exception object as a stack reference. + HashSet reported = new(); + foreach (StackReferenceData r in stackWalk.WalkStackReferences(crashingThread)) + { + if (r.Object != TargetPointer.Null) + reported.Add(r.Object); + } + + foreach (ulong exc in expected) + Assert.True(reported.Contains(exc), + $"Expected in-flight exception object 0x{exc:x} to be reported as a stack reference"); + } + + // --- GCProtect debuggee: GCFrame (GCPROTECT) protected objects reported as roots --- + + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + [SkipOnArch("x86", "GCInfo decoder does not support x86")] + public void GCProtect_GCFrameRootsAreReported(TestConfiguration config) + { + InitializeDumpTest(config, "GCProtect", "full"); + IStackWalk stackWalk = Target.Contracts.StackWalk; + + ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + + // The debuggee crashes inside an AppDomain.AssemblyResolve handler the runtime invokes while + // holding a GCPROTECT frame over the requesting Assembly reference. WalkStackReferences reports + // each GCFrame-protected object with the GCFrame node address as its Source; the test walks the + // thread's GCFrame chain and asserts a reported root's Source matches a node in that chain. + IReadOnlyList refs = stackWalk.WalkStackReferences(crashingThread); + + // Enumerate the thread's GCFrame chain node addresses; each reported GCFrame root carries the + // GCFrame node address as its Source (UpdateScanContext(frame: pGCFrame)). + Target.TypeInfo threadType = Target.GetTypeInfo("Thread"); + Target.TypeInfo gcFrameType = Target.GetTypeInfo("GCFrame"); + TargetPointer terminator = TargetPointer.PlatformMaxValue(Target); + + HashSet gcFrameNodes = new(); + TargetPointer node = Target.ReadPointerFieldOrNull(crashingThread.ThreadAddress, threadType, "GCFrame"); + while (node != TargetPointer.Null && node != terminator && gcFrameNodes.Add(node)) + node = Target.ReadPointerField(node, gcFrameType, "Next"); + + Assert.True(gcFrameNodes.Count > 0, "GCProtect debuggee should have at least one live GCFrame"); + + int gcFrameRoots = 0; + foreach (StackReferenceData r in refs) + { + if (!gcFrameNodes.Contains(r.Source) || r.Object == TargetPointer.Null) + continue; + + // A real heap object held alive by a GCFrame: its MethodTable must be readable. A reported + // GCFrame root can be an interior pointer or otherwise unreadable, so guard the read. + try + { + TargetPointer methodTable = Target.ReadPointer(r.Object); + if (methodTable != TargetPointer.Null) + gcFrameRoots++; + } + catch + { + // Interior pointer or otherwise unreadable slot; not a countable heap object. + } + } + + Assert.True(gcFrameRoots > 0, + $"Expected at least one GCFrame (GCPROTECT) protected object to be reported as a stack reference (total refs: {refs.Count})"); + } + // --- StackRefs debuggee: known objects on stack with verifiable content --- // These tests require Frame-based GC root scanning (ScanFrameRoots) which is not yet implemented.