From f893c6fbbea2449bc73888819ac471c187d32f6f Mon Sep 17 00:00:00 2001 From: rcj1 Date: Sat, 6 Jun 2026 20:03:38 -0700 Subject: [PATCH 1/9] stackwalk stuff --- docs/design/datacontracts/Debugger.md | 46 ++ docs/design/datacontracts/StackWalk.md | 57 ++- src/coreclr/debug/di/rsstackwalk.cpp | 4 +- src/coreclr/debug/ee/debugger.h | 4 +- src/coreclr/inc/memoryrange.h | 9 + .../vm/datadescriptor/datadescriptor.inc | 8 + .../Contracts/IDebugger.cs | 1 + .../Contracts/IStackWalk.cs | 4 +- .../Target.cs | 4 +- .../Constants.cs | 1 + .../Contracts/Debugger_1.cs | 35 ++ .../Context/IPlatformAgnosticContext.cs | 4 + .../Contracts/StackWalk/StackWalk_1.cs | 196 +++++++- .../Data/Debugger.cs | 1 + .../Data/MemoryRange.cs | 11 + .../DataType.cs | 1 + ...ostics.DataContractReader.Contracts.csproj | 1 + .../Dbi/DacDbiImpl.cs | 432 ++++++++++++++++-- .../Dbi/Helpers/StackWalkHandleData.cs | 66 +++ .../Dbi/IDacDbiInterface.cs | 14 +- .../ContractDescriptorTarget.cs | 4 +- .../cdac/tests/DataGenerator/TestTarget.cs | 2 +- .../DacDbi/DacDbiStackWalkDumpTests.cs | 51 ++- .../tests/DumpTests/StackWalkDumpTests.cs | 26 +- .../TestPlaceholderTarget.cs | 2 +- .../cdac/tests/UnitTests/DacDbiImplTests.cs | 4 +- .../cdac/tests/UnitTests/DebuggerTests.cs | 122 +++++ 27 files changed, 1024 insertions(+), 86 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/MemoryRange.cs create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/StackWalkHandleData.cs diff --git a/docs/design/datacontracts/Debugger.md b/docs/design/datacontracts/Debugger.md index 3823f14b31a9fb..c0fc6b0ed83f36 100644 --- a/docs/design/datacontracts/Debugger.md +++ b/docs/design/datacontracts/Debugger.md @@ -18,6 +18,7 @@ void RequestSyncAtEvent(); void SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC); TargetPointer GetDebuggerControlBlockAddress(); void EnableGCNotificationEvents(bool fEnable); +bool IsRuntimeUnwindableStub(TargetPointer controlPC, out bool isUnhandledException); ``` ## Version 1 @@ -30,6 +31,7 @@ The contract depends on the following globals | `CLRJitAttachState` | TargetPointer | Pointer to the CLR JIT attach state flags | | `CORDebuggerControlFlags` | TargetPointer | Pointer to `g_CORDebuggerControlFlags` | | `MetadataUpdatesApplied` | TargetPointer | Pointer to the g_metadataUpdatesApplied flag | +| `MaxHijackFunctions` | uint32 | Number of entries in the hijack function array. Zero on platforms/configurations where the hijack-function table is not present. | The contract additionally depends on these data descriptors @@ -42,7 +44,15 @@ The contract additionally depends on these data descriptors | `Debugger` | `RSRequestedSync` | Sync-at-event request flag | | `Debugger` | `SendExceptionsOutsideOfJMC` | Exception delivery policy flag | | `Debugger` | `GCNotificationEventsEnabled` | Whether GC notification events are enabled | +| `Debugger` | `RgHijackFunction` | Pointer to the runtime's array of hijack-stub address ranges. | | `DebuggerRCThread` | `DCB` | Pointer to `DebuggerIPCControlBlock` | +| `MemoryRange` | `StartAddress` | Inclusive start address of the range | +| `MemoryRange` | `Size` | Size of the range in bytes; the range covers `[StartAddress, StartAddress + Size)` | + +### Contract Constants: +| Name | Type | Purpose | Value | +| --- | --- | --- | --- | +| `UnhandledExceptionHijackIndex` | uint | Index of unhandled exception hijack memory range. | `0` | ```csharp @@ -134,4 +144,40 @@ void EnableGCNotificationEvents(bool fEnable) debuggerAddress + /* Debugger::GCNotificationEventsEnabled offset */, fEnable ? 1 : 0); } + +bool IsRuntimeUnwindableStub(TargetPointer controlPC, out bool isUnhandledException) +{ + isUnhandledException = false; + + if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) + return false; + + TargetPointer rgHijack = target.ReadPointer( + debuggerAddress + /* Debugger::RgHijackFunction offset */); + if (rgHijack == TargetPointer.Null) + return false; + + uint maxHijackFunctions = target.ReadGlobal("MaxHijackFunctions"); + if (maxHijackFunctions == 0) + return false; + + uint stride = // Size of one MemoryRange entry + + for (uint i = 0; i < maxHijackFunctions; i++) + { + TargetPointer entryAddress = rgHijack + (ulong)(i * stride); + TargetPointer start = target.ReadPointer( + entryAddress + /* MemoryRange::StartAddress offset */); + TargetNUInt size = target.Read( + entryAddress + /* MemoryRange::Size offset */); + + ulong end = start.Value + size.Value; + if (controlPC.Value >= start.Value && controlPC.Value < end) + { + isUnhandledException = (i == UnhandledExceptionHijackIndex); + return true; + } + } + return false; +} ``` diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 923023a6026036..50dd687c43fbca 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -36,8 +36,24 @@ public enum StackWalkState // Creates a stack walk and returns a handle IEnumerable CreateStackWalk(ThreadData threadData); +// Creates a stack walk and returns a handle, using a caller-provided seed CONTEXT. +// `contextBuffer` must be at least `IPlatformAgnosticContext.Size` bytes. +// `isFirst` indicates whether the seed frame should be treated as the active leaf. +// `skipFrames` controls whether explicit Frames below the seed SP are yielded as +// `SkippedFrame` entries before the containing managed frame. +IEnumerable CreateStackWalk( + ThreadData threadData, + byte[] contextBuffer, + bool isFirst = true, + bool skipFrames = false); + // Gets the thread context at the given stack dataframe. byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle); + +// Recovers the pre-hijack thread CONTEXT saved by a runtime redirection stub when +// the walker is currently stopped inside one of those stubs. +byte[] RetrieveHijackedContext(IStackDataFrameHandle stackDataFrameHandle, bool isUnhandledException); + // Gets the Frame address at the given stack dataframe. Returns TargetPointer.Null if the current dataframe does not have a valid Frame. TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle); @@ -69,8 +85,10 @@ DebuggerEvalData GetDebuggerEvalData(TargetPointer funcEvalFrameAddress); IReadOnlyList WalkStackReferences(ThreadData threadData); // Returns a context for the thread, trying (in order): the debugger filter context, -// the OS thread context, or a context derived from the explicit Frame chain. -byte[] GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags); +// the OS thread context, or a context derived from the explicit Frame chain. The caller +// owns the buffer and must ensure it is at least sizeof(CONTEXT) bytes and aligned to +// IPlatformAgnosticContext.ContextAlignment (16). +int GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags, Span contextBuffer); // Returns the saved TargetContext pointer carried by the head Frame, if applicable. TargetPointer GetRedirectedContextPointer(ThreadData threadData); @@ -160,6 +178,7 @@ Constants used: | --- | --- | --- | --- | | `ExceptionFlags` (`exstatecommon.h`) | `Ex_UnwindHasStarted` | `0x00000004` | Bit flag in `ExceptionInfo.ExceptionFlags` indicating exception unwinding (2nd pass) has started. Used by `IsInStackRegionUnwoundBySpecifiedException` to skip ExInfo trackers still in the 1st pass. | | `InlinedCallFrameMarker` (`exceptionhandling.h`) | `ExceptionHandlingHelper` | `2 (64-bit), 1(32-bit)` | Used to determine whether an active call on an InlinedCallFrame is an EH helper. | +| `Debugger::s_hijackFunction` (`debugger.h`) | `UnhandledExceptionHijackIndex` | `0` | Index of the unhandled-exception hijack entry in the runtime's hijack-function table. Used by `RetrieveHijackedContext` to choose between the SP-based and FP/SP-offset-based CONTEXT recovery paths, and by `IDebugger.IsRuntimeUnwindableStub` to set its `isUnhandledException` out parameter. | Contracts used: | Contract Name | @@ -584,14 +603,42 @@ IReadOnlyList WalkStackReferences(ThreadData threadData) The implementation uses the same stack walk algorithm as `CreateStackWalk`, but integrates the GC-aware `Filter` directly (rather than consuming pre-generated frames) and performs GC reference enumeration at each frame. See [GC Stack Reference Scanning](#gc-stack-reference-scanning) for details. -`GetContext` returns a thread context by trying three sources in order: (1) the debugger filter context from `ThreadData.DebuggerFilterContext` (when `ThreadContextSource.Debugger` is requested), (2) the OS thread context via `TryGetThreadContext`, or (3) a context derived from walking the explicit Frame chain (`Thread::Frame` linked list), returning the first frame that yields a usable context: -* If the current Frame is an `InterpreterFrame`, clear the context and update it from the Frame. Return the resulting bytes. +`GetContext` fills the caller-provided buffer with a thread context by trying three sources in order: (1) the debugger filter context from `ThreadData.DebuggerFilterContext` (when `ThreadContextSource.Debugger` is requested), (2) the OS thread context via `TryGetThreadContext`, or (3) a context derived from walking the explicit Frame chain (`Thread::Frame` linked list), returning the first frame that yields a usable context: +* If the current Frame is an `InterpreterFrame`, clear the context and update it from the Frame. Write the resulting bytes to the destination. * Otherwise, clear the context and update it from the current Frame; accept the context when both the stack pointer and instruction pointer are non-zero (e.g. `RedirectedThreadFrame`, `InlinedCallFrame`, `DynamicHelperFrame`). Mark `RawContextFlags = FullContextFlags` so callers know SP/PC/FP are valid. -If no Frame in the chain produces a usable context (thread is not running managed code), a zeroed context of the target architecture's size is returned. +If no Frame in the chain produces a usable context (thread is not running managed code), the destination is left zero-initialized (it is cleared by `GetContext` before any source writes into it). + +The caller owns the buffer and must ensure it is at least `IPlatformAgnosticContext.Size` bytes and aligned to `IPlatformAgnosticContext.ContextAlignment`. `GetRedirectedContextPointer` returns the saved `TargetContext` pointer carried by the head Frame when that Frame is a `RedirectedThreadFrame` (a `ResumableFrame`). Otherwise it returns `TargetPointer.Null`. +#### CreateStackWalk with a caller-provided CONTEXT + +`CreateStackWalk(ThreadData, byte[], bool isFirst, bool skipFrames)` seeds the walker from `contextBuffer` rather than from the thread's saved CONTEXT, and exposes two extra knobs: + +* `isFirst` (default `true`) is propagated to the first yielded `StackWalkData` as its `IsFirst` value. +* `skipFrames` (default `false`) controls whether the initial `CheckForSkippedFrames` step runs. +When the seed sits at a managed-to-unmanaged (M2U) boundary, the overload runs a pre-loop that mirrors native `StackFrameIterator::ResetRegDisp` (`stackwalk.cpp:1261-1308`): + +1. Compute the caller SP by cloning the seed context and unwinding the clone. +2. Iterate the explicit Frame chain; stop at the first Frame `>= callerSP` (on non-x86) or after the additional ReturnAddress/FP cross-check (on x86, where an external OS unwind can produce a slightly wrong SP — see `stackwalk.cpp:1240-1241`). +3. For every Frame whose `GetCurrentReturnAddress() == seedIP`, rewrite the seed context via `UpdateContextFromCurrentFrame` and record the matched Frame type. +4. After the loop, if a match was found, override the first yielded frame's `IsFirst` (true for `ResumableFrame`/`RedirectedThreadFrame`, and for `HijackFrame` on non-x86) and `IsInterrupted` (true for `FaultingExceptionFrame`/`SoftwareExceptionFrame`). + +The frame iterator is left positioned at the first Frame `>= callerSP`, so the rest of the walk picks up from the same logical position as the native iterator. + +#### RetrieveHijackedContext + +`RetrieveHijackedContext(handle, isUnhandledException)` returned byte buffer is the pre-hijack thread CONTEXT that the stub stashed on the stack at entry, and is used to re-seed the walker. + +The location of the saved `PT_CONTEXT*` differs by stub kind and by architecture: + +* `isUnhandledException == true` (the `ExceptionHijack` stub): the pointer is at `*SP` — the stub pushes it directly. The implementation reads `*context.StackPointer`. +* `isUnhandledException == false` (the `RedirectedHandledJITCaseFor*` stubs): the pointer is at a fixed offset from SP or FP, matching the `REDIRECTSTUB_{SP,EBP,RBP}_OFFSET_CONTEXT` constants in `src/coreclr/vm/{arch}/asmconstants.h`: + +The implementation reads the pointer from that slot via `Target.ReadPointer`, then materializes a fresh `IPlatformAgnosticContext` from the target memory at the resulting address and returns its raw bytes. + ### 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. diff --git a/src/coreclr/debug/di/rsstackwalk.cpp b/src/coreclr/debug/di/rsstackwalk.cpp index 1540ae24ae5ddf..d6ebb4b93450ee 100644 --- a/src/coreclr/debug/di/rsstackwalk.cpp +++ b/src/coreclr/debug/di/rsstackwalk.cpp @@ -427,8 +427,8 @@ BOOL CordbStackWalk::UnwindStackFrame() IfFailThrow(pDAC->UnwindStackWalkFrame(m_pSFIHandle, &retVal)); // Now that we have unwound, make sure we update the CONTEXT buffer to reflect the current stack frame. - // This call is safe regardless of whether the unwind is successful or not. - IfFailThrow(pDAC->GetStackWalkCurrentContext(m_pSFIHandle, &m_context)); + if (retVal) + IfFailThrow(pDAC->GetStackWalkCurrentContext(m_pSFIHandle, &m_context)); return retVal; } // CordbStackWalk::UnwindStackWalkFrame diff --git a/src/coreclr/debug/ee/debugger.h b/src/coreclr/debug/ee/debugger.h index 32df8dbca015f4..d4c093d0012333 100644 --- a/src/coreclr/debug/ee/debugger.h +++ b/src/coreclr/debug/ee/debugger.h @@ -2874,7 +2874,7 @@ class Debugger : public DebugInterface // represents different thead redirection functions recognized by the debugger enum HijackFunction { - kUnhandledException = 0, + kUnhandledException = 0, // [cDAC] [Debugger]: Contract depends on this value. kRedirectedForGCThreadControl, kRedirectedForDbgThreadControl, kRedirectedForUserSuspend, @@ -3866,6 +3866,8 @@ struct cdac_data static constexpr size_t RSRequestedSync = offsetof(Debugger, m_RSRequestedSync); static constexpr size_t SendExceptionsOutsideOfJMC = offsetof(Debugger, m_sendExceptionsOutsideOfJMC); static constexpr size_t GCNotificationEventsEnabled = offsetof(Debugger, m_isGarbageCollectionEventsEnabled); + static constexpr size_t RgHijackFunction = offsetof(Debugger, m_rgHijackFunction); + static constexpr size_t MaxHijackFunctions = Debugger::kMaxHijackFunctions; }; template<> diff --git a/src/coreclr/inc/memoryrange.h b/src/coreclr/inc/memoryrange.h index 1142d4a3fd9ecf..32d8430e8c59b3 100644 --- a/src/coreclr/inc/memoryrange.h +++ b/src/coreclr/inc/memoryrange.h @@ -11,6 +11,7 @@ #include "daccess.h" #include "contract.h" +#include "cdacdata.h" // MemoryRange is a descriptor of a memory range. This groups (pointer + size). // @@ -88,6 +89,14 @@ class MemoryRange // This is s SIZE_T so that it can describe any memory range in the process (for example, larger than 4gb on 64-bit machines) const SIZE_T m_cbBytes; + friend struct ::cdac_data; +}; + +template<> +struct cdac_data +{ + static constexpr size_t StartAddress = offsetof(MemoryRange, m_pStartAddress); + static constexpr size_t Size = offsetof(MemoryRange, m_cbBytes); }; typedef ArrayDPTR(MemoryRange) ARRAY_PTR_MemoryRange; diff --git a/src/coreclr/vm/datadescriptor/datadescriptor.inc b/src/coreclr/vm/datadescriptor/datadescriptor.inc index 70dc6b27ebff2d..55f4e17777f72c 100644 --- a/src/coreclr/vm/datadescriptor/datadescriptor.inc +++ b/src/coreclr/vm/datadescriptor/datadescriptor.inc @@ -446,12 +446,19 @@ CDAC_TYPE_FIELD(Debugger, T_POINTER, RCThread, cdac_data::RCThread) CDAC_TYPE_FIELD(Debugger, T_INT32, RSRequestedSync, cdac_data::RSRequestedSync) CDAC_TYPE_FIELD(Debugger, T_INT32, SendExceptionsOutsideOfJMC, cdac_data::SendExceptionsOutsideOfJMC) CDAC_TYPE_FIELD(Debugger, T_INT32, GCNotificationEventsEnabled, cdac_data::GCNotificationEventsEnabled) +CDAC_TYPE_FIELD(Debugger, T_POINTER, RgHijackFunction, cdac_data::RgHijackFunction) CDAC_TYPE_END(Debugger) CDAC_TYPE_BEGIN(DebuggerRCThread) CDAC_TYPE_INDETERMINATE(DebuggerRCThread) CDAC_TYPE_FIELD(DebuggerRCThread, T_POINTER, DCB, cdac_data::DCB) CDAC_TYPE_END(DebuggerRCThread) + +CDAC_TYPE_BEGIN(MemoryRange) +CDAC_TYPE_SIZE(sizeof(MemoryRange)) +CDAC_TYPE_FIELD(MemoryRange, T_POINTER, StartAddress, cdac_data::StartAddress) +CDAC_TYPE_FIELD(MemoryRange, T_NUINT, Size, cdac_data::Size) +CDAC_TYPE_END(MemoryRange) #endif // DEBUGGING_SUPPORTED && !TARGET_WASM CDAC_TYPE_BEGIN(ArrayListBase) @@ -1517,6 +1524,7 @@ CDAC_GLOBAL_POINTER(EEConfig, &::g_pConfig) CDAC_GLOBAL_POINTER(Debugger, &::g_pDebugger) CDAC_GLOBAL_POINTER(CLRJitAttachState, &::CLRJitAttachState) CDAC_GLOBAL_POINTER(CORDebuggerControlFlags, &::g_CORDebuggerControlFlags) +CDAC_GLOBAL(MaxHijackFunctions, T_UINT32, cdac_data::MaxHijackFunctions) #endif // DEBUGGING_SUPPORTED && !TARGET_WASM #ifdef FEATURE_METADATA_UPDATER CDAC_GLOBAL_POINTER(MetadataUpdatesApplied, &::g_metadataUpdatesApplied) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs index 1762e12dfc2ef4..09b8e6bb828faf 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IDebugger.cs @@ -20,6 +20,7 @@ public interface IDebugger : IContract void SetSendExceptionsOutsideOfJMC(bool sendExceptionsOutsideOfJMC) => throw new NotImplementedException(); TargetPointer GetDebuggerControlBlockAddress() => throw new NotImplementedException(); void EnableGCNotificationEvents(bool fEnable) => throw new NotImplementedException(); + bool IsRuntimeUnwindableStub(TargetPointer controlPC, out bool isUnhandledException) => throw new NotImplementedException(); } public readonly struct Debugger : IDebugger 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..a087d6d1ec752d 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 @@ -73,6 +73,7 @@ public interface IStackWalk : IContract static string IContract.Name => nameof(StackWalk); public virtual IEnumerable CreateStackWalk(ThreadData threadData) => throw new NotImplementedException(); + public virtual IEnumerable CreateStackWalk(ThreadData threadData, byte[] contextBuffer, bool isFirst = true, bool skipFrames = true) => throw new NotImplementedException(); IReadOnlyList WalkStackReferences(ThreadData threadData) => throw new NotImplementedException(); byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); @@ -84,7 +85,8 @@ public interface IStackWalk : IContract bool IsExceptionHandlingHelperInlinedCallFrame(TargetPointer frameAddress) => throw new NotImplementedException(); DebuggerEvalData GetDebuggerEvalData(TargetPointer funcEvalFrameAddress) => throw new NotImplementedException(); TargetPointer GetRedirectedContextPointer(ThreadData threadData) => throw new NotImplementedException(); - byte[] GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags) => throw new NotImplementedException(); + int GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags, Span contextBuffer) => throw new NotImplementedException(); + byte[] RetrieveHijackedContext(IStackDataFrameHandle stackDataFrameHandle, bool isUnhandledException) => throw new NotImplementedException(); } public struct StackWalk : IStackWalk diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs index b091152bcc7a66..a3552e913e0ad4 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Target.cs @@ -33,8 +33,8 @@ public abstract class Target /// The identifier of the thread whose context is to be retrieved. The identifier is defined by the operating system. /// A bitwise combination of platform-dependent flags that indicate which portions of the context should be read. /// Buffer to be filled with thread context. - /// true if successful, false otherwise - public abstract bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer); + /// HRESULT indicating success or failure + public abstract int TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer); /// /// Reads a well-known global pointer value from the target process 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 37f49930fce10c..92440bb0e1ec3b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs @@ -14,6 +14,7 @@ public static class Globals public const string FinalizerThread = nameof(FinalizerThread); public const string GCThread = nameof(GCThread); public const string Debugger = nameof(Debugger); + public const string MaxHijackFunctions = nameof(MaxHijackFunctions); public const string CLRJitAttachState = nameof(CLRJitAttachState); public const string CORDebuggerControlFlags = nameof(CORDebuggerControlFlags); public const string MetadataUpdatesApplied = nameof(MetadataUpdatesApplied); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs index 83d568d117439d..52983c1aa881a7 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Debugger_1.cs @@ -10,6 +10,7 @@ private enum DebuggerControlFlag_1 : uint PendingAttach = 0x0100, Attached = 0x0200, } + private const uint UnhandledExceptionHijackIndex = 0; private readonly Target _target; @@ -117,4 +118,38 @@ void IDebugger.EnableGCNotificationEvents(bool fEnable) Data.Debugger debugger = _target.ProcessedData.GetOrAdd(debuggerAddress); debugger.WriteGCNotificationEventsEnabled(fEnable ? 1 : 0); } + + bool IDebugger.IsRuntimeUnwindableStub(TargetPointer controlPC, out bool isUnhandledException) + { + isUnhandledException = false; + + if (!TryGetDebuggerAddress(out TargetPointer debuggerAddress)) + return false; + + Data.Debugger debugger = _target.ProcessedData.GetOrAdd(debuggerAddress); + if (debugger.RgHijackFunction == TargetPointer.Null) + return false; + + uint maxHijackFunctions = _target.ReadGlobal(Constants.Globals.MaxHijackFunctions); + if (maxHijackFunctions == 0) + return false; + + Target.TypeInfo memoryRangeTypeInfo = _target.GetTypeInfo(DataType.MemoryRange); + uint stride = memoryRangeTypeInfo.Size!.Value; + + for (uint i = 0; i < maxHijackFunctions; i++) + { + TargetPointer entryAddress = debugger.RgHijackFunction + (ulong)(i * stride); + Data.MemoryRange entry = _target.ProcessedData.GetOrAdd(entryAddress); + + ulong start = entry.StartAddress.Value; + ulong end = start + entry.Size.Value; + if (controlPC.Value >= start && controlPC.Value < end) + { + isUnhandledException = i == UnhandledExceptionHijackIndex; + return true; + } + } + return false; + } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs index f7d210a6de2df2..554c8578e07077 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs @@ -7,6 +7,10 @@ namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; public interface IPlatformAgnosticContext { + // Required alignment, in bytes, of any memory buffer passed to Win32 GetThreadContext + // (or its cross-platform equivalents) as a CONTEXT*. + public const uint ContextAlignment = 16; + public abstract uint Size { get; } public abstract uint ContextControlFlags { get; } public abstract uint FullContextFlags { get; } 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..082f0bb071acf8 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 @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Reflection.Metadata; using System.Reflection.Metadata.Ecma335; +using System.Runtime.InteropServices; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.Data; using System.Linq; @@ -130,6 +131,98 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.Frameless : StackWalkState.InitialNativeContext; FrameIterator frameIterator = new(_target, threadData); + return RunStackWalk(context, state, frameIterator, threadData); + } + + IEnumerable IStackWalk.CreateStackWalk(ThreadData threadData, byte[] contextBuffer, bool isFirst, bool skipFrames) + { + IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); + context.FillFromBuffer(contextBuffer); + + FrameIterator frameIterator = new(_target, threadData); + + TargetPointer curSP = context.StackPointer; + TargetPointer curPc = context.InstructionPointer; + TargetPointer curFP = context.FramePointer; + StackWalkState state = IsManaged(context.InstructionPointer, out _) ? StackWalkState.Frameless : StackWalkState.InitialNativeContext; + if (state == StackWalkState.Frameless) + { + IPlatformAgnosticContext tmpContext = context.Clone(); + tmpContext.Unwind(_target); + curSP = tmpContext.StackPointer; + } + bool isX86 = _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86; + bool matched = false; + FrameType matchedType = FrameType.Unknown; + while (frameIterator.IsValid()) + { + if (frameIterator.CurrentFrameAddress.Value >= curSP.Value) + { + if (!isX86) + { + break; + } + + // On x86, the seed may be the result of an OS unwind + // performed by an outside debugger, which produces an incorrect SP at an M2U boundary. + // for the managed stack frame at the M2U boundary. The context is obtained by unwinding the + // marshaling stub, and it contains an SP which is actually higher (closer to the leaf) than the + // address of the transition frame. It is as if the explicit frame is not contained in the stack + // frame of any method. Here's an example: + // + // ChildEBP + // 0012e884 ntdll32!DbgBreakPoint + // 0012e89c CLRStub[StubLinkStub]@1f0ac1e + // 0012e8a4 invalid ESP of Foo() according to the context specified by the debuggers + // 0012e8b4 address of transition frame (PInvokeMethodFrameStandalone) + // 0012e8c8 real ESP of Foo() according to the transition frame + // 0012e8d8 managed!Dummy.Foo()+0x20 + // Re-validate that this Frame is still the M2U match by comparing + // both its return address and its view of EBP against the seed, + // mirroring stackwalk.cpp:1266-1280. If both match, fall through + // to the rewrite below; otherwise stop walking. + if (frameIterator.GetCurrentReturnAddress() != curPc) + break; + + IPlatformAgnosticContext tmpContext = context.Clone(); + frameIterator.UpdateContextFromCurrentFrame(tmpContext); + if (tmpContext.FramePointer != curFP) + break; + } + + if (frameIterator.GetCurrentReturnAddress() == curPc) + { + matched = true; + matchedType = frameIterator.GetCurrentFrameType(); + frameIterator.UpdateContextFromCurrentFrame(context); + } + + frameIterator.Next(); + } + + bool? matchedIsFirst = null; + bool? matchedIsInterrupted = null; + if (matched) + { + matchedIsFirst = matchedType is FrameType.ResumableFrame + or FrameType.RedirectedThreadFrame + || (matchedType is FrameType.HijackFrame && !isX86); + matchedIsInterrupted = matchedType is FrameType.FaultingExceptionFrame + or FrameType.SoftwareExceptionFrame; + } + + return RunStackWalk(context, state, frameIterator, threadData, isFirst, matchedIsFirst, matchedIsInterrupted, skipFrames); + } + private IEnumerable RunStackWalk( + IPlatformAgnosticContext context, + StackWalkState state, + FrameIterator frameIterator, + ThreadData threadData, + bool initialIsFirst = true, + bool? isFirstOverride = null, + bool? isInterruptedOverride = null, + bool skipFrames = true) + { // Skip the head InterpreterFrame when entering with a context already // inside an interpreter execution (e.g. a managed-debugger breakpoint // synthesized callback context). Without this, Frame would later @@ -145,11 +238,15 @@ IEnumerable IStackWalk.CreateStackWalk(ThreadData threadD StackWalkData stackWalkData = new(context, state, frameIterator, threadData); + stackWalkData.IsFirst = isFirstOverride ?? initialIsFirst; + if (isInterruptedOverride is bool overrideInterrupted) + stackWalkData.IsInterrupted = overrideInterrupted; + // Mirror native Init() -> ProcessCurrentFrame() -> CheckForSkippedFrames(): // When the initial frame is managed (Frameless), check if there are explicit // Frames below the caller SP that should be reported first. The native walker // yields skipped frames BEFORE the containing managed frame. - if (state == StackWalkState.Frameless && CheckForSkippedFrames(stackWalkData)) + if (skipFrames && stackWalkData.State == StackWalkState.Frameless && CheckForSkippedFrames(stackWalkData)) { stackWalkData.State = StackWalkState.SkippedFrame; } @@ -774,7 +871,7 @@ private void UpdateState(StackWalkData handle) } else { - handle.State = validFrame ? StackWalkState.NativeMarker : StackWalkState.Complete; + handle.State = (validFrame || handle.State == StackWalkState.Frameless) ? StackWalkState.NativeMarker : StackWalkState.Complete; } return; } @@ -817,6 +914,42 @@ byte[] IStackWalk.GetRawContext(IStackDataFrameHandle stackDataFrameHandle) return handle.Context.GetBytes(); } + byte[] IStackWalk.RetrieveHijackedContext(IStackDataFrameHandle stackDataFrameHandle, bool isUnhandledException) + { + StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); + TargetPointer slotAddress = isUnhandledException + ? handle.Context.StackPointer + : ComputeRedirectStubSlot(handle.Context); + + TargetPointer contextAddress = _target.ReadPointer(slotAddress.Value); + + IPlatformAgnosticContext recovered = IPlatformAgnosticContext.GetContextForPlatform(_target); + recovered.ReadFromAddress(_target, contextAddress); + return recovered.GetBytes(); + } + + private TargetPointer ComputeRedirectStubSlot(IPlatformAgnosticContext context) + { + // Per-architecture offset of the saved PTR_CONTEXT in the redirect-stub stack frame. + // Mirrors REDIRECTSTUB_{SP,EBP,RBP}_OFFSET_CONTEXT in + // src/coreclr/vm/{i386,amd64,arm,arm64,riscv64,loongarch64}/asmconstants.h + (bool useFramePointer, long offset) = _target.Contracts.RuntimeInfo.GetTargetArchitecture() switch + { + RuntimeInfoArchitecture.X86 => (true, -4L), + RuntimeInfoArchitecture.X64 => (true, 0x20L), + RuntimeInfoArchitecture.Arm => (false, 0L), + RuntimeInfoArchitecture.Arm64 => (false, 0L), + RuntimeInfoArchitecture.RiscV64 => (false, 0L), + RuntimeInfoArchitecture.LoongArch64 => (false, 0L), + var arch => throw new InvalidOperationException( + $"Hijack-stub CONTEXT recovery is not supported on {arch}"), + }; + + ulong baseValue = (useFramePointer ? context.FramePointer : context.StackPointer).Value; + // offset is signed (negative on x86). Two's-complement add via unchecked. + return new TargetPointer(unchecked(baseValue + (ulong)offset)); + } + TargetPointer IStackWalk.GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) { StackDataFrameHandle handle = AssertCorrectHandle(stackDataFrameHandle); @@ -918,11 +1051,14 @@ DebuggerEvalData IStackWalk.GetDebuggerEvalData(TargetPointer funcEvalFrameAddre return new DebuggerEvalData(debuggerEval.MethodToken, debuggerEval.AssemblyPtr); } - byte[] IStackWalk.GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags) + int IStackWalk.GetContext(ThreadData threadData, ThreadContextSource contextSource, uint contextFlags, Span contextBuffer) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); - byte[] bytes = new byte[context.Size]; - Span buffer = new Span(bytes); + if (contextBuffer.Length < context.Size) + return HResults.E_INVALIDARG; + Span buffer = contextBuffer.Slice(0, (int)context.Size); + + buffer.Clear(); TargetPointer filterContext = TargetPointer.Null; @@ -932,19 +1068,25 @@ byte[] IStackWalk.GetContext(ThreadData threadData, ThreadContextSource contextS if (filterContext != TargetPointer.Null) { _target.ReadBuffer(filterContext.Value, buffer); - return bytes; + return 0; } - if (_target.TryGetThreadContext(threadData.OSId.Value, contextFlags, buffer)) + int hr = _target.TryGetThreadContext(threadData.OSId.Value, contextFlags, buffer); + if (hr == 0) { - return bytes; + return 0; } // Fall back to deriving a context from the explicit Frame chain stored in the Thread object. - return GetContextFromFrames(threadData); + else if (hr == HResults.E_NOTIMPL) + { + WriteContextFromFrames(threadData, buffer); + return 0; + } + return hr; } - private byte[] GetContextFromFrames(ThreadData threadData) + private void WriteContextFromFrames(ThreadData threadData, Span destination) { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); @@ -957,7 +1099,8 @@ private byte[] GetContextFromFrames(ThreadData threadData) { context.Clear(); _frameHelpers.UpdateContextFromFrame(iterator.CurrentFrame, context); - return context.GetBytes(); + context.GetBytes().CopyTo(destination); + return; } // For other frames, look for the first (deepest) frame that yields a context @@ -968,15 +1111,15 @@ private byte[] GetContextFromFrames(ThreadData threadData) if (context.StackPointer.Value != 0 && context.InstructionPointer.Value != 0) { context.RawContextFlags = context.FullContextFlags; - return context.GetBytes(); + context.GetBytes().CopyTo(destination); + return; } iterator.Next(); } - // The thread is not running managed code: return a zeroed context. - context.Clear(); - return context.GetBytes(); + // The thread is not running managed code: leave destination zero-initialized + // (already cleared by the GetContext caller). } TargetPointer IStackWalk.GetRedirectedContextPointer(ThreadData threadData) @@ -1003,13 +1146,24 @@ private bool IsManaged(TargetPointer ip, [NotNullWhen(true)] out CodeBlockHandle return false; } - private void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData, uint flags) + private unsafe void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData, uint flags) { - byte[] bytes = ((IStackWalk)this).GetContext( - threadData, - ThreadContextSource.Debugger, - flags); - context.FillFromBuffer(bytes); + // CONTEXT requires 16-byte alignment for the OS GetThreadContext path + void* scratch = NativeMemory.AlignedAlloc(context.Size, IPlatformAgnosticContext.ContextAlignment); + try + { + Span buffer = new(scratch, (int)context.Size); + _ = ((IStackWalk)this).GetContext( + threadData, + ThreadContextSource.Debugger, + flags, + buffer); + context.FillFromBuffer(buffer); + } + finally + { + NativeMemory.AlignedFree(scratch); + } } private static StackDataFrameHandle AssertCorrectHandle(IStackDataFrameHandle stackDataFrameHandle) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Debugger.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Debugger.cs index 2a40a1489768bc..86bb807c1bfd26 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Debugger.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Debugger.cs @@ -14,4 +14,5 @@ internal sealed partial class Debugger : IData [Field(Writable = true)] public int RSRequestedSync { get; private set; } [Field(Writable = true)] public int SendExceptionsOutsideOfJMC { get; private set; } [Field(Writable = true)] public int GCNotificationEventsEnabled { get; private set; } + [Field] public TargetPointer RgHijackFunction { get; } } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/MemoryRange.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/MemoryRange.cs new file mode 100644 index 00000000000000..2caa48e70f12ed --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/MemoryRange.cs @@ -0,0 +1,11 @@ +// 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.MemoryRange))] +internal sealed partial class MemoryRange : IData +{ + [Field] public TargetPointer StartAddress { get; } + [Field] public TargetNUInt Size { 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 8267818e6ba13d..cb15dfd49c1a03 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs @@ -45,6 +45,7 @@ public enum DataType AppDomain, Debugger, DebuggerRCThread, + MemoryRange, SystemDomain, Assembly, LoaderAllocator, diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj index bf80c13bbf6212..639ef481e6a96c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj @@ -30,5 +30,6 @@ + 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 0bcbc3fededf64..06900cb664bd2f 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 @@ -1240,28 +1240,359 @@ public int GetManagedStoppedContext(ulong vmThread, ulong* pRetVal) return hr; } - public int CreateStackWalk(ulong vmThread, nint pInternalContextBuffer, nuint* ppSFIHandle) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.CreateStackWalk(vmThread, pInternalContextBuffer, ppSFIHandle) : HResults.E_NOTIMPL; + private void SeedHandleFromNativeContext(StackWalkHandleData handleData, byte* pContext, bool isFirst) + { + uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(_target).Size; + byte[] contextBuf = new Span(pContext, (int)contextSize).ToArray(); + handleData.Reset(contextBuf, isFirst); + } + +#if DEBUG + private string DescribeContextDiff(ReadOnlySpan cdac, ReadOnlySpan legacy) + { + System.Text.StringBuilder sb = new(); + sb.Append("CONTEXT byte mismatch (cdac vs legacy):"); + int diffs = 0; + for (int i = 0; i < cdac.Length && diffs < 16; i++) + { + if (cdac[i] != legacy[i]) + { + sb.Append($" @0x{i:X3} cdac=0x{cdac[i]:X2} dac=0x{legacy[i]:X2};"); + diffs++; + } + } + + try + { + IPlatformAgnosticContext cdacCtx = IPlatformAgnosticContext.GetContextForPlatform(_target); + IPlatformAgnosticContext dacCtx = IPlatformAgnosticContext.GetContextForPlatform(_target); + cdacCtx.FillFromBuffer(new Span(cdac.ToArray())); + dacCtx.FillFromBuffer(new Span(legacy.ToArray())); + sb.Append($" | flags cdac=0x{cdacCtx.RawContextFlags:X8} dac=0x{dacCtx.RawContextFlags:X8}"); + sb.Append($" | IP cdac=0x{cdacCtx.InstructionPointer.Value:X16} dac=0x{dacCtx.InstructionPointer.Value:X16}"); + sb.Append($" | SP cdac=0x{cdacCtx.StackPointer.Value:X16} dac=0x{dacCtx.StackPointer.Value:X16}"); + sb.Append($" | FP cdac=0x{cdacCtx.FramePointer.Value:X16} dac=0x{dacCtx.FramePointer.Value:X16}"); + } + catch (System.Exception ex) + { + sb.Append($" | (decode failed: {ex.GetType().Name}: {ex.Message})"); + } + + return sb.ToString(); + } +#endif + + public int CreateStackWalk(ulong vmThread, byte* pInternalContextBuffer, nuint* ppSFIHandle) + { + if (ppSFIHandle is null) + return HResults.E_POINTER; + *ppSFIHandle = 0; + + int hr = HResults.S_OK; + StackWalkHandleData? handleData = null; + try + { + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); + uint allFlags = ctx.AllContextFlags; + ThreadData threadData = _target.Contracts.Thread.GetThreadData(new TargetPointer(vmThread)); + Span seedSpan = new(pInternalContextBuffer, (int)ctx.Size); + hr = _target.Contracts.StackWalk.GetContext(threadData, ThreadContextSource.Debugger, allFlags, seedSpan); + if (hr < 0) + throw Marshal.GetExceptionForHR(hr)!; + + handleData = new StackWalkHandleData(_target.Contracts.StackWalk, threadData); + SeedHandleFromNativeContext(handleData, pInternalContextBuffer, isFirst: true); + *ppSFIHandle = handleData.GetHandle(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + // Mirror the create onto the legacy DBI + if (_legacy is not null && LegacyFallbackHelper.CanFallback()) + { + uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(_target).Size; + byte* pLocal = (byte*)NativeMemory.AlignedAlloc(contextSize, IPlatformAgnosticContext.ContextAlignment); + nuint legacyHandle = 0; + try + { + new Span(pLocal, (int)contextSize).Clear(); + + int hrLocal = _legacy.CreateStackWalk(vmThread, pLocal, &legacyHandle); + Debug.ValidateHResult(hr, hrLocal); + + if (hr == HResults.S_OK && hrLocal == HResults.S_OK) + { +#if DEBUG + ReadOnlySpan cdacBytes = new(pInternalContextBuffer, (int)contextSize); + ReadOnlySpan legacyBytes = new(pLocal, (int)contextSize); + if (!cdacBytes.SequenceEqual(legacyBytes)) + Debug.Fail(DescribeContextDiff(cdacBytes, legacyBytes)); +#endif + if (handleData is not null) + handleData.LegacyHandle = legacyHandle; + } + else if (hrLocal == HResults.S_OK) + { + _legacy.DeleteStackWalk(legacyHandle); + } + } + finally + { + NativeMemory.AlignedFree(pLocal); + } + } + return hr; + } public int DeleteStackWalk(nuint ppSFIHandle) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.DeleteStackWalk(ppSFIHandle) : HResults.E_NOTIMPL; + { + if (ppSFIHandle == 0) + return HResults.S_OK; + + int hr = HResults.S_OK; + nuint legacyHandle = 0; + try + { + GCHandle gcHandle = GCHandle.FromIntPtr((nint)ppSFIHandle); + if (gcHandle.Target is not StackWalkHandleData handleData) + throw new ArgumentException("Invalid stack walk handle", nameof(ppSFIHandle)); + legacyHandle = handleData.LegacyHandle; + handleData.Dispose(); + gcHandle.Free(); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + if (_legacy is not null && LegacyFallbackHelper.CanFallback() && legacyHandle != 0) + { + int hrLocal = _legacy.DeleteStackWalk(legacyHandle); + Debug.ValidateHResult(hr, hrLocal); + } + return hr; + } + + public int GetStackWalkCurrentContext(nuint pSFIHandle, byte* pContext) + { + if (pSFIHandle == 0) + return HResults.E_INVALIDARG; + if (pContext == null) + return HResults.E_POINTER; - public int GetStackWalkCurrentContext(nuint pSFIHandle, nint pContext) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetStackWalkCurrentContext(pSFIHandle, pContext) : HResults.E_NOTIMPL; + int hr = HResults.S_OK; + nuint legacyHandle = 0; + uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(_target).Size; + try + { + GCHandle gcHandle = GCHandle.FromIntPtr((nint)pSFIHandle); + if (gcHandle.Target is not StackWalkHandleData handleData) + throw new ArgumentException("Invalid stack walk handle", nameof(pSFIHandle)); + legacyHandle = handleData.LegacyHandle; - public int SetStackWalkCurrentContext(ulong vmThread, nuint pSFIHandle, int flag, nint pContext) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.SetStackWalkCurrentContext(vmThread, pSFIHandle, flag, pContext) : HResults.E_NOTIMPL; + if (handleData.IsValid) + { + IStackWalk sw = _target.Contracts.StackWalk; + byte[] context = sw.GetRawContext(handleData.Current!); + + // Apply any pending SP adjustment (x86 M2U adjustment after UnwindStackWalkFrame). + if (handleData.PendingStackPointerDelta != 0) + { + IPlatformAgnosticContext adjusted = IPlatformAgnosticContext.GetContextForPlatform(_target); + adjusted.FillFromBuffer(context); + adjusted.StackPointer = new TargetPointer(unchecked(adjusted.StackPointer.Value + (ulong)handleData.PendingStackPointerDelta)); + context = adjusted.GetBytes(); + } + + // Mirror dacdbiimplstackwalk.cpp:184-186: strip CONTEXT_XSTATE on x86/amd64 + RuntimeInfoArchitecture arch = _target.Contracts.RuntimeInfo.GetTargetArchitecture(); + if (arch is RuntimeInfoArchitecture.X86 or RuntimeInfoArchitecture.X64) + { + IPlatformAgnosticContext stripped = IPlatformAgnosticContext.GetContextForPlatform(_target); + stripped.FillFromBuffer(context); + stripped.RawContextFlags &= ~0x40u; + context = stripped.GetBytes(); + } + + context.AsSpan().CopyTo(new Span(pContext, context.Length)); + } + else + { + hr = HResults.S_FALSE; + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } +#if DEBUG + if (_legacy is not null && legacyHandle != 0) + { + byte* pLocal = (byte*)NativeMemory.AlignedAlloc(contextSize, IPlatformAgnosticContext.ContextAlignment); + try + { + new Span(pLocal, (int)contextSize).Clear(); + int hrLocal = _legacy.GetStackWalkCurrentContext(legacyHandle, pLocal); + Debug.ValidateHResult(hr, hrLocal); + if (hr == HResults.S_OK) + { + ReadOnlySpan cdacBytes = new(pContext, (int)contextSize); + ReadOnlySpan legacyBytes = new(pLocal, (int)contextSize); + if (!cdacBytes.SequenceEqual(legacyBytes)) + Debug.Fail(DescribeContextDiff(cdacBytes, legacyBytes)); + } + } + finally + { + NativeMemory.AlignedFree(pLocal); + } + } +#endif + return hr; + } + + public int SetStackWalkCurrentContext(ulong vmThread, nuint pSFIHandle, int flag, byte* pContext) + { + if (pSFIHandle == 0) + return HResults.E_INVALIDARG; + if (pContext == null) + return HResults.E_POINTER; + + int hr = HResults.S_OK; + nuint legacyHandle = 0; + try + { + GCHandle gcHandle = GCHandle.FromIntPtr((nint)pSFIHandle); + if (gcHandle.Target is not StackWalkHandleData handleData) + throw new ArgumentException("Invalid stack walk handle", nameof(pSFIHandle)); + legacyHandle = handleData.LegacyHandle; + + SeedHandleFromNativeContext(handleData, pContext, isFirst: flag == (int)CorDebugSetContextFlags.SET_CONTEXT_FLAG_ACTIVE_FRAME); + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + // Mirror to legacy DBI in all builds so the legacy walker tracks the cDAC walker + if (_legacy is not null && LegacyFallbackHelper.CanFallback() && legacyHandle != 0) + { + int hrLocal = _legacy.SetStackWalkCurrentContext(vmThread, legacyHandle, flag, pContext); + Debug.ValidateHResult(hr, hrLocal); + } + return hr; + } public int UnwindStackWalkFrame(nuint pSFIHandle, Interop.BOOL* pResult) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.UnwindStackWalkFrame(pSFIHandle, pResult) : HResults.E_NOTIMPL; + { + if (pSFIHandle == 0) + return HResults.E_INVALIDARG; + if (pResult == null) + return HResults.E_POINTER; + + *pResult = Interop.BOOL.FALSE; + + int hr = HResults.S_OK; + nuint legacyHandle = 0; + try + { + GCHandle gcHandle = GCHandle.FromIntPtr((nint)pSFIHandle); + if (gcHandle.Target is not StackWalkHandleData handleData) + throw new ArgumentException("Invalid stack walk handle", nameof(pSFIHandle)); + legacyHandle = handleData.LegacyHandle; + + // If the walker is already invalid, treat as end-of-stack. + if (!handleData.IsValid) + { + *pResult = Interop.BOOL.FALSE; + } + else + { + IStackWalk sw = _target.Contracts.StackWalk; + IExecutionManager eman = _target.Contracts.ExecutionManager; + + // Step 1: if we're sitting on the initial native context + // or a U2M native marker AND the control PC is inside a runtime hijack stub, + // recover the saved CONTEXT and re-seed the walker. + StackWalkState state = handleData.Current!.State; + bool handled = false; + if (state is StackWalkState.InitialNativeContext or StackWalkState.NativeMarker) + { + TargetPointer controlPC = sw.GetInstructionPointer(handleData.Current); + if (_target.Contracts.Debugger.IsRuntimeUnwindableStub(controlPC, out bool isUnhandled)) + { + byte[] recoveredContext = sw.RetrieveHijackedContext(handleData.Current, isUnhandled); + handleData.Reset(recoveredContext, isFirst: true); + if (!handleData.IsValid) + { + throw Marshal.GetExceptionForHR(HResults.E_FAIL)!; + } + *pResult = Interop.BOOL.TRUE; + handled = true; + } + } + + if (!handled) + { + // Step 2: on x86 we need to remember the + // callee's stack-parameter size before unwinding, so we can adjust SP after + // the unwind lands on an M2U NativeMarker. + uint cbStackParameterSize = 0; + if (state == StackWalkState.Frameless) + { + TargetPointer controlPC = sw.GetInstructionPointer(handleData.Current); + if (eman.GetCodeBlockHandle(new TargetCodePointer(controlPC)) is CodeBlockHandle cbh) + { + cbStackParameterSize = eman.GetStackParameterSize(cbh); + } + } + + // Step 3: advance past Frame/SkippedFrame entries. + handleData.AdvanceForUnwind(); - public int CheckContext(ulong vmThread, nint pContext) + bool atEndOfStack = !handleData.IsValid; + + // Step 4: if the unwind landed on a NativeMarker (M2U transition), + // apply the saved stack-parameter-size adjustment on x86. + if (!atEndOfStack + && handleData.Current!.State == StackWalkState.NativeMarker + && _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86 + && cbStackParameterSize != 0) + { + handleData.PendingStackPointerDelta = -cbStackParameterSize; + } + + *pResult = atEndOfStack ? Interop.BOOL.FALSE : Interop.BOOL.TRUE; + } + } + } + catch (System.Exception ex) + { + hr = ex.HResult; + } + + // Mirror to legacy DBI in all builds so the legacy walker tracks the cDAC walker + if (_legacy is not null && LegacyFallbackHelper.CanFallback() && legacyHandle != 0) + { + Interop.BOOL localResult; + int hrLocal = _legacy.UnwindStackWalkFrame(legacyHandle, &localResult); + Debug.ValidateHResult(hr, hrLocal); +#if DEBUG + if (hr == HResults.S_OK) + { + Debug.Assert(*pResult == localResult, $"cDAC: {*pResult}, DAC: {localResult}"); + } +#endif + } + return hr; + } + + public int CheckContext(ulong vmThread, byte* pContext) { int hr = HResults.S_OK; try { IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); - ctx.FillFromBuffer(new Span((void*)pContext, (int)ctx.Size)); + ctx.FillFromBuffer(new Span(pContext, (int)ctx.Size)); if ((ctx.RawContextFlags & ctx.ContextControlFlags) != 0) { @@ -1288,7 +1619,15 @@ public int CheckContext(ulong vmThread, nint pContext) } public int GetStackWalkCurrentFrameInfo(nuint pSFIHandle, nint pFrameData, int* pRetVal) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetStackWalkCurrentFrameInfo(pSFIHandle, pFrameData, pRetVal) : HResults.E_NOTIMPL; + { + if (pSFIHandle == 0) + return HResults.E_INVALIDARG; + + nuint legacyHandle = TryGetLegacyHandle(pSFIHandle); + return _legacy is not null && LegacyFallbackHelper.CanFallback() && legacyHandle != 0 + ? _legacy.GetStackWalkCurrentFrameInfo(legacyHandle, pFrameData, pRetVal) + : HResults.E_NOTIMPL; + } // Filter used by GetCountOfInternalFrames and EnumerateInternalFrames to decide // which entries from IStackWalk.GetFrames should be surfaced to the DBI as internal frames. @@ -1496,7 +1835,28 @@ public int GetStackParameterSize(ulong controlPC, uint* pRetVal) } public int GetFramePointer(nuint pSFIHandle, ulong* pRetVal) - => LegacyFallbackHelper.CanFallback() && _legacy is not null ? _legacy.GetFramePointer(pSFIHandle, pRetVal) : HResults.E_NOTIMPL; + { + if (pSFIHandle == 0) + return HResults.E_INVALIDARG; + + nuint legacyHandle = TryGetLegacyHandle(pSFIHandle); + return _legacy is not null && LegacyFallbackHelper.CanFallback() && legacyHandle != 0 + ? _legacy.GetFramePointer(legacyHandle, pRetVal) + : HResults.E_NOTIMPL; + } + + private static nuint TryGetLegacyHandle(nuint pSFIHandle) + { + try + { + GCHandle gcHandle = GCHandle.FromIntPtr((nint)pSFIHandle); + return gcHandle.Target is StackWalkHandleData handleData ? handleData.LegacyHandle : 0; + } + catch + { + return 0; + } + } public int IsLeafFrame(ulong vmThread, byte* pContext, Interop.BOOL* pResult) { @@ -1507,12 +1867,24 @@ public int IsLeafFrame(ulong vmThread, byte* pContext, Interop.BOOL* pResult) IPlatformAgnosticContext leafCtx = IPlatformAgnosticContext.GetContextForPlatform(_target); uint allFlags = leafCtx.AllContextFlags; ThreadData threadData = _target.Contracts.Thread.GetThreadData(new TargetPointer(vmThread)); - byte[] leafContext = _target.Contracts.StackWalk.GetContext(threadData, ThreadContextSource.None, allFlags); - leafCtx.FillFromBuffer(leafContext); + + byte* pLeaf = (byte*)NativeMemory.AlignedAlloc(leafCtx.Size, IPlatformAgnosticContext.ContextAlignment); + try + { + Span leafBuffer = new(pLeaf, (int)leafCtx.Size); + int hrLeaf = _target.Contracts.StackWalk.GetContext(threadData, ThreadContextSource.None, allFlags, leafBuffer); + if (hrLeaf < 0) + throw Marshal.GetExceptionForHR(hrLeaf)!; + leafCtx.FillFromBuffer(leafBuffer); + } + finally + { + NativeMemory.AlignedFree(pLeaf); + } // Read the given context from the native buffer. IPlatformAgnosticContext givenCtx = IPlatformAgnosticContext.GetContextForPlatform(_target); - givenCtx.FillFromBuffer(new Span(pContext, leafContext.Length)); + givenCtx.FillFromBuffer(new Span(pContext, (int)leafCtx.Size)); *pResult = givenCtx.StackPointer == leafCtx.StackPointer && givenCtx.InstructionPointer == leafCtx.InstructionPointer @@ -1540,11 +1912,13 @@ public int GetContext(ulong vmThread, byte* pContextBuffer) int hr = HResults.S_OK; try { - uint allFlags = IPlatformAgnosticContext.GetContextForPlatform(_target).AllContextFlags; + IPlatformAgnosticContext ctx = IPlatformAgnosticContext.GetContextForPlatform(_target); + uint allFlags = ctx.AllContextFlags; ThreadData threadData = _target.Contracts.Thread.GetThreadData(new TargetPointer(vmThread)); - byte[] context = _target.Contracts.StackWalk.GetContext(threadData, ThreadContextSource.Debugger, allFlags); - - context.AsSpan().CopyTo(new Span(pContextBuffer, context.Length)); + Span destination = new(pContextBuffer, (int)ctx.Size); + hr = _target.Contracts.StackWalk.GetContext(threadData, ThreadContextSource.Debugger, allFlags, destination); + if (hr < 0) + throw Marshal.GetExceptionForHR(hr)!; } catch (System.Exception ex) { @@ -1554,22 +1928,26 @@ public int GetContext(ulong vmThread, byte* pContextBuffer) if (_legacy is not null) { uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(_target).Size; - byte[] localContextBuf = new byte[contextSize]; - fixed (byte* pLocal = localContextBuf) + byte* pLocal = (byte*)NativeMemory.AlignedAlloc(contextSize, IPlatformAgnosticContext.ContextAlignment); + try { + new Span(pLocal, (int)contextSize).Clear(); + int hrLocal = _legacy.GetContext(vmThread, pLocal); Debug.ValidateHResult(hr, hrLocal); if (hr == HResults.S_OK) { - IPlatformAgnosticContext contextStruct = IPlatformAgnosticContext.GetContextForPlatform(_target); - IPlatformAgnosticContext localContextStruct = IPlatformAgnosticContext.GetContextForPlatform(_target); - contextStruct.FillFromBuffer(new Span(pContextBuffer, (int)contextSize)); - localContextStruct.FillFromBuffer(localContextBuf); - - Debug.Assert(contextStruct.Equals(localContextStruct)); + ReadOnlySpan cdacBytes = new(pContextBuffer, (int)contextSize); + ReadOnlySpan legacyBytes = new(pLocal, (int)contextSize); + if (!cdacBytes.SequenceEqual(legacyBytes)) + Debug.Fail(DescribeContextDiff(cdacBytes, legacyBytes)); } } + finally + { + NativeMemory.AlignedFree(pLocal); + } } #endif return hr; diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/StackWalkHandleData.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/StackWalkHandleData.cs new file mode 100644 index 00000000000000..9dafd4577cf217 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/Dbi/Helpers/StackWalkHandleData.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using Microsoft.Diagnostics.DataContractReader.Contracts; + +namespace Microsoft.Diagnostics.DataContractReader.Legacy; + +internal sealed class StackWalkHandleData +{ + private readonly IStackWalk _stackWalk; + private readonly ThreadData _threadData; + private IEnumerator? _enumerator; + + public StackWalkHandleData(IStackWalk stackWalk, ThreadData threadData) + { + _stackWalk = stackWalk; + _threadData = threadData; + } + + public IStackDataFrameHandle? Current { get; private set; } + public bool IsValid => Current is not null; + public nuint LegacyHandle { get; set; } + public long PendingStackPointerDelta { get; set; } + public void Reset(byte[] contextBuffer, bool isFirst) + { + _enumerator?.Dispose(); + _enumerator = _stackWalk.CreateStackWalk(_threadData, contextBuffer, isFirst, false).GetEnumerator(); + PendingStackPointerDelta = 0; + Advance(); + if (Current is null) + throw Marshal.GetExceptionForHR(HResults.E_FAIL)!; + } + + public void Advance() + { + PendingStackPointerDelta = 0; + Current = _enumerator is not null && _enumerator.MoveNext() ? _enumerator.Current : null; + } + + /// + /// Advance to the next non-Frame, non-SkippedFrame stop. + /// + public void AdvanceForUnwind() + { + while (true) + { + Advance(); + if (Current is null) + return; + if (Current.State is StackWalkState.Frame or StackWalkState.SkippedFrame) + continue; + return; + } + } + + public nuint GetHandle() + { + GCHandle gcHandle = GCHandle.Alloc(this); + return (nuint)GCHandle.ToIntPtr(gcHandle).ToInt64(); + } + + public void Dispose() => _enumerator?.Dispose(); +} 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..09bcfa888f55e8 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 @@ -351,6 +351,12 @@ public enum IlNum : int TYPECTXT_ILNUM = -3, } +public enum CorDebugSetContextFlags +{ + SET_CONTEXT_FLAG_ACTIVE_FRAME = 0x1, + SET_CONTEXT_FLAG_UNWIND_FRAME = 0x2, +} + // 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] @@ -487,22 +493,22 @@ public unsafe partial interface IDacDbiInterface int GetManagedStoppedContext(ulong vmThread, ulong* pRetVal); [PreserveSig] - int CreateStackWalk(ulong vmThread, nint pInternalContextBuffer, nuint* ppSFIHandle); + int CreateStackWalk(ulong vmThread, byte* pInternalContextBuffer, nuint* ppSFIHandle); [PreserveSig] int DeleteStackWalk(nuint ppSFIHandle); [PreserveSig] - int GetStackWalkCurrentContext(nuint pSFIHandle, nint pContext); + int GetStackWalkCurrentContext(nuint pSFIHandle, byte* pContext); [PreserveSig] - int SetStackWalkCurrentContext(ulong vmThread, nuint pSFIHandle, int flag, nint pContext); + int SetStackWalkCurrentContext(ulong vmThread, nuint pSFIHandle, int flag, byte* pContext); [PreserveSig] int UnwindStackWalkFrame(nuint pSFIHandle, Interop.BOOL* pResult); [PreserveSig] - int CheckContext(ulong vmThread, nint pContext); + int CheckContext(ulong vmThread, byte* pContext); [PreserveSig] int GetStackWalkCurrentFrameInfo(nuint pSFIHandle, nint pFrameData, int* pRetVal); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs index 633484f17e2578..e67dba7cfd67a2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader/ContractDescriptorTarget.cs @@ -408,11 +408,11 @@ private static bool TryReadContractDescriptor( public override int PointerSize => _config.PointerSize; public override bool IsLittleEndian => _config.IsLittleEndian; - public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) + public override int TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) { // Underlying API only supports 32-bit thread IDs, mask off top 32 bits int hr = _dataTargetDelegates.GetThreadContext((uint)(threadId & uint.MaxValue), contextFlags, buffer); - return hr == 0; + return hr; } /// diff --git a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs index e1e9a7e8bd6392..35628e571c79bd 100644 --- a/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs +++ b/src/native/managed/cdac/tests/DataGenerator/TestTarget.cs @@ -252,7 +252,7 @@ public override bool TryReadGlobalPointer(string name, [NotNullWhen(true)] out T public override bool TryRead(ulong address, out T value) => throw new NotImplementedException(); public override TargetPointer ReadPointerFromSpan(ReadOnlySpan bytes) => throw new NotImplementedException(); public override bool IsAlignedToPointerSize(TargetPointer pointer) => throw new NotImplementedException(); - public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) => throw new NotImplementedException(); + public override int TryGetThreadContext(ulong threadId, uint contextFlags, Span buffer) => throw new NotImplementedException(); // --- Stub ContractRegistry ------------------------------------- diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs index bc6661cde0f49e..51dd3f5c72927e 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs @@ -1,7 +1,9 @@ // 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.Linq; +using System.Runtime.InteropServices; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers; using Microsoft.Diagnostics.DataContractReader.Legacy; @@ -63,12 +65,23 @@ public unsafe void GetContext_MatchesContractGetContext(TestConfiguration config } uint allFlags = IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; - byte[] contractContext = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.Debugger, allFlags); IPlatformAgnosticContext dbiCtx = IPlatformAgnosticContext.GetContextForPlatform(Target); IPlatformAgnosticContext contractCtx = IPlatformAgnosticContext.GetContextForPlatform(Target); dbiCtx.FillFromBuffer(dbiContextBuffer); - contractCtx.FillFromBuffer(contractContext); + + // CONTEXT requires 16-byte alignment for the OS GetThreadContext path. + byte* pContract = (byte*)NativeMemory.AlignedAlloc(contextSize, IPlatformAgnosticContext.ContextAlignment); + try + { + Span contractContext = new(pContract, (int)contextSize); + _ = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.Debugger, allFlags, contractContext); + contractCtx.FillFromBuffer(contractContext); + } + finally + { + NativeMemory.AlignedFree(pContract); + } Assert.Equal(contractCtx.InstructionPointer, dbiCtx.InstructionPointer); Assert.Equal(contractCtx.StackPointer, dbiCtx.StackPointer); @@ -84,17 +97,25 @@ public unsafe void IsLeafFrame_TrueForLeafContext(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(Target).Size; uint allFlags = IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; - byte[] leafContext = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags); - Interop.BOOL result; - fixed (byte* pContext = leafContext) + // CONTEXT requires 16-byte alignment for the OS GetThreadContext path. + byte* pContext = (byte*)NativeMemory.AlignedAlloc(contextSize, IPlatformAgnosticContext.ContextAlignment); + try { + Span leafContext = new(pContext, (int)contextSize); + _ = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags, leafContext); + + Interop.BOOL result; int hr = dbi.IsLeafFrame(crashingThread.ThreadAddress, pContext, &result); Assert.Equal(System.HResults.S_OK, hr); + Assert.Equal(Interop.BOOL.TRUE, result); + } + finally + { + NativeMemory.AlignedFree(pContext); } - - Assert.Equal(Interop.BOOL.TRUE, result); } [ConditionalTheory] @@ -107,10 +128,22 @@ public unsafe void IsLeafFrame_FalseForNonLeafContext(TestConfiguration config) ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); + uint contextSize = IPlatformAgnosticContext.GetContextForPlatform(Target).Size; uint allFlags = IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; - byte[] leafContext = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags); IPlatformAgnosticContext leafCtx = IPlatformAgnosticContext.GetContextForPlatform(Target); - leafCtx.FillFromBuffer(leafContext); + + // CONTEXT requires 16-byte alignment for the OS GetThreadContext path. + byte* pLeaf = (byte*)NativeMemory.AlignedAlloc(contextSize, IPlatformAgnosticContext.ContextAlignment); + try + { + Span leafContext = new(pLeaf, (int)contextSize); + _ = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags, leafContext); + leafCtx.FillFromBuffer(leafContext); + } + finally + { + NativeMemory.AlignedFree(pLeaf); + } IStackWalk sw = Target.Contracts.StackWalk; diff --git a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs index e0832f67547037..d8d96faccf67fb 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.InteropServices; using Microsoft.Diagnostics.DataContractReader.Contracts; using Microsoft.Diagnostics.DataContractReader.Legacy; using Microsoft.Diagnostics.DataContractReader.TestInfrastructure; @@ -338,19 +339,28 @@ public unsafe void VarargPInvoke_GetCodeHeaderDataWithInvalidPrecodeAddress(Test [ConditionalTheory] [MemberData(nameof(TestConfigurations))] [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] - public void GetContext_ReturnsNonEmptyContext(TestConfiguration config) + public unsafe void GetContext_ReturnsNonEmptyContext(TestConfiguration config) { InitializeDumpTest(config); ThreadData crashingThread = DumpTestHelpers.FindFailFastThread(Target); - uint allFlags = Contracts.StackWalkHelpers.IPlatformAgnosticContext.GetContextForPlatform(Target).AllContextFlags; - byte[] context = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags); + var ctx = Contracts.StackWalkHelpers.IPlatformAgnosticContext.GetContextForPlatform(Target); + uint allFlags = ctx.AllContextFlags; - Assert.NotNull(context); - Assert.True(context.Length > 0, "Expected non-empty context"); + // CONTEXT requires 16-byte alignment for the OS GetThreadContext path. + byte* pContext = (byte*)NativeMemory.AlignedAlloc(ctx.Size, Contracts.StackWalkHelpers.IPlatformAgnosticContext.ContextAlignment); + try + { + Span context = new(pContext, (int)ctx.Size); + int hr = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags, context); - var ctx = Contracts.StackWalkHelpers.IPlatformAgnosticContext.GetContextForPlatform(Target); - ctx.FillFromBuffer(context); - Assert.NotEqual(TargetPointer.Null, ctx.InstructionPointer); + Assert.Equal(System.HResults.S_OK, hr); + ctx.FillFromBuffer(context); + Assert.NotEqual(TargetPointer.Null, ctx.InstructionPointer); + } + finally + { + NativeMemory.AlignedFree(pContext); + } } } diff --git a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs index 12561ffd8676a7..f82bbf9ee8986c 100644 --- a/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs +++ b/src/native/managed/cdac/tests/TestInfrastructure/TestPlaceholderTarget.cs @@ -546,7 +546,7 @@ public override Target.TypeInfo GetTypeInfo(string typeName) public override bool TryGetTypeInfo(string typeName, out Target.TypeInfo info) => _typeInfoCache.TryGetValue(typeName, out info); - public override bool TryGetThreadContext(ulong threadId, uint contextFlags, Span bufferToFill) => throw new NotImplementedException(); + public override int TryGetThreadContext(ulong threadId, uint contextFlags, Span bufferToFill) => throw new NotImplementedException(); public override Target.IDataCache ProcessedData => _dataCache; public override ContractRegistry Contracts => _contractRegistry; diff --git a/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs b/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs index ea5a0c6f70a7af..7ea4d1f9fab42e 100644 --- a/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/DacDbiImplTests.cs @@ -579,7 +579,7 @@ public void CheckContext_WithControlFlag_ValidatesSpRange(MockTarget.Architectur fixed (byte* pCtx = bytes) { - int hr = dacDbi.CheckContext(ThreadAddr, (nint)pCtx); + int hr = dacDbi.CheckContext(ThreadAddr, pCtx); Assert.Equal(expectedHr, hr); } } @@ -605,7 +605,7 @@ public void CheckContext_NoControlFlag_SkipsSpCheck(MockTarget.Architecture arch fixed (byte* pCtx = bytes) { - int hr = dacDbi.CheckContext(ThreadAddr, (nint)pCtx); + int hr = dacDbi.CheckContext(ThreadAddr, pCtx); Assert.Equal(System.HResults.S_OK, hr); } diff --git a/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs b/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs index bf962fa730e9f6..4a075491af0efd 100644 --- a/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs +++ b/src/native/managed/cdac/tests/UnitTests/DebuggerTests.cs @@ -38,6 +38,7 @@ private static TargetTestHelpers.LayoutResult GetDebuggerLayout(TargetTestHelper new(nameof(Data.Debugger.RSRequestedSync), DataType.int32), new(nameof(Data.Debugger.SendExceptionsOutsideOfJMC), DataType.int32), new(nameof(Data.Debugger.GCNotificationEventsEnabled), DataType.int32), + new(nameof(Data.Debugger.RgHijackFunction), DataType.pointer), ]); } @@ -402,4 +403,125 @@ public void EnableGCNotificationEvents_WritesFlag(MockTarget.Architecture arch, Assert.Equal(value ? 1 : 0, target.Read(debuggerAddress.Value + (ulong)fieldOffset)); } + + // ----------------------------------------------------------------------- + // IsRuntimeUnwindableStub + // ----------------------------------------------------------------------- + + private static TargetTestHelpers.LayoutResult GetMemoryRangeLayout(TargetTestHelpers helpers) + { + return helpers.LayoutFields( + [ + new(nameof(Data.MemoryRange.StartAddress), DataType.pointer), + new(nameof(Data.MemoryRange.Size), DataType.nuint), + ]); + } + + /// + /// Builds a target whose Debugger has an RgHijackFunction array of + /// MemoryRange entries. Index 0 in the array is the unhandled-exception hijack, matching + /// Debugger::kUnhandledException == 0 in native debugger.h. + /// + private static TestPlaceholderTarget BuildTargetWithHijackTable( + MockTarget.Architecture arch, + (ulong Start, ulong Size)[] ranges) + { + TargetTestHelpers helpers = new(arch); + var builder = new TestPlaceholderTarget.Builder(arch); + MockMemorySpace.BumpAllocator allocator = builder.MemoryBuilder.CreateAllocator(0x1_0000, 0x10_0000); + + TargetTestHelpers.LayoutResult debuggerLayout = GetDebuggerLayout(helpers); + TargetTestHelpers.LayoutResult memoryRangeLayout = GetMemoryRangeLayout(helpers); + builder.AddTypes(new Dictionary + { + [DataType.Debugger] = new() { Fields = debuggerLayout.Fields, Size = debuggerLayout.Stride }, + [DataType.MemoryRange] = new() { Fields = memoryRangeLayout.Fields, Size = memoryRangeLayout.Stride }, + }); + + // Allocate the RgHijackFunction array as a contiguous block of MemoryRange entries. + ulong rgHijackAddress = 0; + if (ranges.Length > 0) + { + MockMemorySpace.HeapFragment rgFrag = allocator.Allocate( + (ulong)ranges.Length * memoryRangeLayout.Stride, + "RgHijackFunction"); + int startOff = memoryRangeLayout.Fields[nameof(Data.MemoryRange.StartAddress)].Offset; + int sizeOff = memoryRangeLayout.Fields[nameof(Data.MemoryRange.Size)].Offset; + for (int i = 0; i < ranges.Length; i++) + { + int entryBase = i * (int)memoryRangeLayout.Stride; + helpers.WritePointer(rgFrag.Data.AsSpan(entryBase + startOff, helpers.PointerSize), ranges[i].Start); + helpers.WriteNUInt(rgFrag.Data.AsSpan(entryBase + sizeOff, helpers.PointerSize), new TargetNUInt(ranges[i].Size)); + } + rgHijackAddress = rgFrag.Address; + } + + // Allocate and populate the Debugger struct. + MockMemorySpace.HeapFragment debuggerFrag = allocator.Allocate(debuggerLayout.Stride, "Debugger"); + helpers.WritePointer( + debuggerFrag.Data.AsSpan(debuggerLayout.Fields[nameof(Data.Debugger.RgHijackFunction)].Offset, helpers.PointerSize), + rgHijackAddress); + + // g_pDebugger -> Debugger + MockMemorySpace.HeapFragment debuggerPtrFrag = allocator.Allocate((ulong)helpers.PointerSize, "g_pDebugger"); + helpers.WritePointer(debuggerPtrFrag.Data, debuggerFrag.Address); + builder.AddGlobals( + (Constants.Globals.Debugger, debuggerPtrFrag.Address), + (Constants.Globals.MaxHijackFunctions, (ulong)ranges.Length)); + + builder.AddContract(version: "c1"); + return builder.Build(); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsRuntimeUnwindableStub_DetectsUnhandledExceptionHijack(MockTarget.Architecture arch) + { + // Index 0 is the unhandled-exception hijack; index 1 is any other redirect stub. + (ulong Start, ulong Size)[] ranges = + [ + (0x10_0000, 0x100), + (0x20_0000, 0x100), + ]; + Target target = BuildTargetWithHijackTable(arch, ranges); + IDebugger debugger = target.Contracts.Debugger; + + Assert.True(debugger.IsRuntimeUnwindableStub(new TargetPointer(0x10_0080), out bool isUnhandled)); + Assert.True(isUnhandled); + + Assert.True(debugger.IsRuntimeUnwindableStub(new TargetPointer(0x20_0010), out isUnhandled)); + Assert.False(isUnhandled); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsRuntimeUnwindableStub_ReturnsFalseForUnmatchedPC(MockTarget.Architecture arch) + { + (ulong Start, ulong Size)[] ranges = + [ + (0x10_0000, 0x100), + ]; + Target target = BuildTargetWithHijackTable(arch, ranges); + IDebugger debugger = target.Contracts.Debugger; + + // Just outside the range (end is exclusive). + Assert.False(debugger.IsRuntimeUnwindableStub(new TargetPointer(0x10_0100), out bool isUnhandled)); + Assert.False(isUnhandled); + + // Well outside any range. + Assert.False(debugger.IsRuntimeUnwindableStub(new TargetPointer(0xDEAD_BEEF), out isUnhandled)); + Assert.False(isUnhandled); + } + + [Theory] + [ClassData(typeof(MockTarget.StdArch))] + public void IsRuntimeUnwindableStub_ReturnsFalseWhenTableEmpty(MockTarget.Architecture arch) + { + // FEATURE_HIJACK off / uninitialized: MaxHijackFunctions == 0. + Target target = BuildTargetWithHijackTable(arch, []); + IDebugger debugger = target.Contracts.Debugger; + + Assert.False(debugger.IsRuntimeUnwindableStub(new TargetPointer(0x10_0080), out bool isUnhandled)); + Assert.False(isUnhandled); + } } From 42e6e397df7e3135fa1de013c0a0d575e01b6d43 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Sat, 6 Jun 2026 22:36:32 -0700 Subject: [PATCH 2/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Contracts/IStackWalk.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 a087d6d1ec752d..3021307682816c 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 @@ -71,10 +71,8 @@ public record struct DebuggerEvalData( public interface IStackWalk : IContract { static string IContract.Name => nameof(StackWalk); - public virtual IEnumerable CreateStackWalk(ThreadData threadData) => throw new NotImplementedException(); - public virtual IEnumerable CreateStackWalk(ThreadData threadData, byte[] contextBuffer, bool isFirst = true, bool skipFrames = true) => throw new NotImplementedException(); - IReadOnlyList WalkStackReferences(ThreadData threadData) => throw new NotImplementedException(); + public virtual IEnumerable CreateStackWalk(ThreadData threadData, byte[] contextBuffer, bool isFirst = true, bool skipFrames = false) => throw new NotImplementedException(); byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); string GetFrameName(TargetPointer frameIdentifier) => throw new NotImplementedException(); From 69380ab807f5a3205dc5ba01be5ae2f880ff6e56 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Sat, 6 Jun 2026 22:54:46 -0700 Subject: [PATCH 3/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/StackWalk_1.cs | 4 +++- .../Dbi/DacDbiImpl.cs | 2 ++ .../tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs | 10 ++++++---- 3 files changed, 11 insertions(+), 5 deletions(-) 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 082f0bb071acf8..39046bb06f52d3 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 @@ -1153,11 +1153,13 @@ private unsafe void FillContextFromThread(IPlatformAgnosticContext context, Thre try { Span buffer = new(scratch, (int)context.Size); - _ = ((IStackWalk)this).GetContext( + int hr = ((IStackWalk)this).GetContext( threadData, ThreadContextSource.Debugger, flags, buffer); + if (hr < 0) + throw Marshal.GetExceptionForHR(hr)!; context.FillFromBuffer(buffer); } finally 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 06900cb664bd2f..498b01b1a01b3b 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 @@ -1286,6 +1286,8 @@ public int CreateStackWalk(ulong vmThread, byte* pInternalContextBuffer, nuint* { if (ppSFIHandle is null) return HResults.E_POINTER; + if (pInternalContextBuffer == null) + return HResults.E_POINTER; *ppSFIHandle = 0; int hr = HResults.S_OK; diff --git a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs index 51dd3f5c72927e..48ea2a15f3cef7 100644 --- a/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/DacDbi/DacDbiStackWalkDumpTests.cs @@ -75,7 +75,8 @@ public unsafe void GetContext_MatchesContractGetContext(TestConfiguration config try { Span contractContext = new(pContract, (int)contextSize); - _ = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.Debugger, allFlags, contractContext); + int hr = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.Debugger, allFlags, contractContext); + Assert.Equal(System.HResults.S_OK, hr); contractCtx.FillFromBuffer(contractContext); } finally @@ -105,8 +106,8 @@ public unsafe void IsLeafFrame_TrueForLeafContext(TestConfiguration config) try { Span leafContext = new(pContext, (int)contextSize); - _ = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags, leafContext); - + int hrContext = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags, leafContext); + Assert.Equal(System.HResults.S_OK, hrContext); Interop.BOOL result; int hr = dbi.IsLeafFrame(crashingThread.ThreadAddress, pContext, &result); Assert.Equal(System.HResults.S_OK, hr); @@ -137,7 +138,8 @@ public unsafe void IsLeafFrame_FalseForNonLeafContext(TestConfiguration config) try { Span leafContext = new(pLeaf, (int)contextSize); - _ = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags, leafContext); + int hrContext = Target.Contracts.StackWalk.GetContext(crashingThread, ThreadContextSource.None, allFlags, leafContext); + Assert.Equal(System.HResults.S_OK, hrContext); leafCtx.FillFromBuffer(leafContext); } finally From c32770092cff98477f73980528e5f62c1a8f4118 Mon Sep 17 00:00:00 2001 From: rcj1 Date: Sat, 6 Jun 2026 22:56:12 -0700 Subject: [PATCH 4/9] fix copilot suggestion --- .../Contracts/IStackWalk.cs | 1 + 1 file changed, 1 insertion(+) 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 3021307682816c..c0e9fa1205b0f9 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 @@ -73,6 +73,7 @@ public interface IStackWalk : IContract static string IContract.Name => nameof(StackWalk); public virtual IEnumerable CreateStackWalk(ThreadData threadData) => throw new NotImplementedException(); public virtual IEnumerable CreateStackWalk(ThreadData threadData, byte[] contextBuffer, bool isFirst = true, bool skipFrames = false) => throw new NotImplementedException(); + IReadOnlyList WalkStackReferences(ThreadData threadData) => throw new NotImplementedException(); byte[] GetRawContext(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); TargetPointer GetFrameAddress(IStackDataFrameHandle stackDataFrameHandle) => throw new NotImplementedException(); string GetFrameName(TargetPointer frameIdentifier) => throw new NotImplementedException(); From 0a4904df58638f7805bbc486da293e3f57cd356c Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Sat, 6 Jun 2026 23:07:44 -0700 Subject: [PATCH 5/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Contracts/StackWalk/StackWalk_1.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 39046bb06f52d3..dd7b62ed722a30 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 @@ -1149,8 +1149,9 @@ private bool IsManaged(TargetPointer ip, [NotNullWhen(true)] out CodeBlockHandle private unsafe void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData, uint flags) { // CONTEXT requires 16-byte alignment for the OS GetThreadContext path - void* scratch = NativeMemory.AlignedAlloc(context.Size, IPlatformAgnosticContext.ContextAlignment); - try + nuint alignment = IPlatformAgnosticContext.ContextAlignment; + nuint scratchSize = ((nuint)context.Size + (alignment - 1)) & ~(alignment - 1); + void* scratch = NativeMemory.AlignedAlloc(scratchSize, alignment); { Span buffer = new(scratch, (int)context.Size); int hr = ((IStackWalk)this).GetContext( From 034471c0affa3047e2e829dfab29a673167a1ae0 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Sat, 6 Jun 2026 23:24:02 -0700 Subject: [PATCH 6/9] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Dbi/DacDbiImpl.cs | 5 +---- 1 file changed, 1 insertion(+), 4 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 498b01b1a01b3b..d7c5f06e767164 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 @@ -1558,10 +1558,7 @@ public int UnwindStackWalkFrame(nuint pSFIHandle, Interop.BOOL* pResult) if (!atEndOfStack && handleData.Current!.State == StackWalkState.NativeMarker && _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86 - && cbStackParameterSize != 0) - { - handleData.PendingStackPointerDelta = -cbStackParameterSize; - } + handleData.PendingStackPointerDelta = -(long)cbStackParameterSize; *pResult = atEndOfStack ? Interop.BOOL.FALSE : Interop.BOOL.TRUE; } From 9b523ff4eaf5eb0e8062a43f24b8e98c912e9e54 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Sat, 6 Jun 2026 23:25:22 -0700 Subject: [PATCH 7/9] Update StackWalk_1.cs --- .../Contracts/StackWalk/StackWalk_1.cs | 1 + 1 file changed, 1 insertion(+) 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 dd7b62ed722a30..1cefeb95bd3080 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 @@ -1152,6 +1152,7 @@ private unsafe void FillContextFromThread(IPlatformAgnosticContext context, Thre nuint alignment = IPlatformAgnosticContext.ContextAlignment; nuint scratchSize = ((nuint)context.Size + (alignment - 1)) & ~(alignment - 1); void* scratch = NativeMemory.AlignedAlloc(scratchSize, alignment); + try { Span buffer = new(scratch, (int)context.Size); int hr = ((IStackWalk)this).GetContext( From 6e1d362bfa5d38ceb2cdc23d9e9eade454a1e0f0 Mon Sep 17 00:00:00 2001 From: Rachel Jarvi Date: Sun, 7 Jun 2026 00:08:14 -0700 Subject: [PATCH 8/9] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../Dbi/DacDbiImpl.cs | 5 +++-- 1 file changed, 3 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 d7c5f06e767164..10fdba8f48a7e8 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 @@ -1557,9 +1557,10 @@ public int UnwindStackWalkFrame(nuint pSFIHandle, Interop.BOOL* pResult) // apply the saved stack-parameter-size adjustment on x86. if (!atEndOfStack && handleData.Current!.State == StackWalkState.NativeMarker - && _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86 + && _target.Contracts.RuntimeInfo.GetTargetArchitecture() == RuntimeInfoArchitecture.X86) + { handleData.PendingStackPointerDelta = -(long)cbStackParameterSize; - + } *pResult = atEndOfStack ? Interop.BOOL.FALSE : Interop.BOOL.TRUE; } } From 533bafcba441b9eca33bdb757a955b1d5ffdc44d Mon Sep 17 00:00:00 2001 From: rcj1 Date: Sun, 7 Jun 2026 07:54:14 -0700 Subject: [PATCH 9/9] fix build --- .../Contracts/StackWalk/StackWalk_1.cs | 8 +++----- ...rosoft.Diagnostics.DataContractReader.Contracts.csproj | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) 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 1cefeb95bd3080..2bd103cda39ec1 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 @@ -1055,7 +1055,7 @@ int IStackWalk.GetContext(ThreadData threadData, ThreadContextSource contextSour { IPlatformAgnosticContext context = IPlatformAgnosticContext.GetContextForPlatform(_target); if (contextBuffer.Length < context.Size) - return HResults.E_INVALIDARG; + return unchecked((int)0x80070057); // E_INVALIDARG Span buffer = contextBuffer.Slice(0, (int)context.Size); buffer.Clear(); @@ -1078,7 +1078,7 @@ int IStackWalk.GetContext(ThreadData threadData, ThreadContextSource contextSour } // Fall back to deriving a context from the explicit Frame chain stored in the Thread object. - else if (hr == HResults.E_NOTIMPL) + else if ((uint)hr == 0x80004001 /* E_NOTIMPL */) { WriteContextFromFrames(threadData, buffer); return 0; @@ -1149,9 +1149,7 @@ private bool IsManaged(TargetPointer ip, [NotNullWhen(true)] out CodeBlockHandle private unsafe void FillContextFromThread(IPlatformAgnosticContext context, ThreadData threadData, uint flags) { // CONTEXT requires 16-byte alignment for the OS GetThreadContext path - nuint alignment = IPlatformAgnosticContext.ContextAlignment; - nuint scratchSize = ((nuint)context.Size + (alignment - 1)) & ~(alignment - 1); - void* scratch = NativeMemory.AlignedAlloc(scratchSize, alignment); + void* scratch = NativeMemory.AlignedAlloc(context.Size, IPlatformAgnosticContext.ContextAlignment); try { Span buffer = new(scratch, (int)context.Size); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj index 639ef481e6a96c..bf80c13bbf6212 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj @@ -30,6 +30,5 @@ -