From dbec1eb06c4dab8b44b55b43036cae498ddd0721 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Mon, 15 Jun 2026 23:00:56 -0400 Subject: [PATCH 1/6] [cdac] Implement IGCInfo for x86; consolidate decoder under GCInfo contract Fixes the cDAC `GetCodeHeaderData` failure that surfaced as `Unable to get codeHeader information` when SOS ran `!clru` against an IL stub MethodDesc on Windows x86, .NET 11, with cDAC enabled (the new default behavior introduced by dotnet/diagnostics#5874). x86 uses a fundamentally different GC info encoding from every other architecture: the legacy bit-packed `InfoHdr` byte-stream format from `src/coreclr/vm/gc_unwind_x86.inl` and `src/coreclr/inc/gcdecoder.cpp` (`USE_GC_INFO_DECODER` is defined for every target except x86, see `eetwain.h:34`). The GCInfo contract had no x86 implementation, so it fell through to `default(GCInfo)` and threw `NotImplementedException` from the interface's default `DecodePlatformSpecificGCInfo`. Any SOS path that needed method size on x86 (`!clru`, `GetCodeHeaderData`, `GetMethodRegionInfo`) failed. The cDAC already had a substantial x86 `InfoHdr` decoder under `Contracts/StackWalk/Context/X86/GCInfoDecoding/`, used by the x86 stack walker. This change relocates that decoder under the `GCInfo` contract so there is a single canonical x86 GC info implementation shared between SOS callers and the stack walker. Changes: * Move `Contracts/StackWalk/Context/X86/GCInfoDecoding/*` -> `Contracts/GCInfo/X86/*` (6 files, tracked as renames). Rename namespace `StackWalkHelpers.X86` -> `GCInfoHelpers.X86`. * Rename the moved class `GCInfo` -> `X86GCInfo` to avoid collision with the empty `Contracts.GCInfo` IGCInfo fallback struct. * Make `relativeOffset` ctor arg optional. Implement `IGCInfoDecoder` directly on `X86GCInfo`: `GetCodeLength` / `GetStackBaseRegister` / `GetSizeOfStackParameterArea` are wired up. `GetInterruptibleRanges` and `EnumerateLiveSlots` throw `NotSupportedException` (future work, needed for `!gcroot` / `!clrstack -l` etc.). * Add `GCInfoX86_1` IGCInfo for x86; register it in `CoreCLRContracts.cs` for `RuntimeInfoArchitecture.X86`. * Update `ExecutionManagerCore.GetStackParameterSize` to delegate to `IGCInfo.GetSizeOfStackParameterArea` (one source of truth). * `X86Unwinder` continues to construct `X86GCInfo` directly because it needs offset-bound state (`IsInProlog`/`IsInEpilog`/`PushedArgSize`) not exposed through `IGCInfoDecoder`. Tests: * Add `VarargPInvoke_GetCodeHeaderDataForILStub_ReturnsMethodSize` regression test that asserts the IL stub path returns S_OK with non-zero MethodSize. Runs against the existing cdac-dump-helix `windows_x86` matrix on every PR. Docs: * Update `docs/design/datacontracts/GCInfo.md` to reflect partial x86 support and document `GetSizeOfStackParameterArea`. * Update `docs/design/datacontracts/StackWalk.md` x86 section to point at the consolidated decoder location. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GCInfo.md | 11 +++- docs/design/datacontracts/StackWalk.md | 2 + .../ExecutionManager/ExecutionManagerCore.cs | 6 +-- .../Contracts/GCInfo/GCInfoX86_1.cs | 53 +++++++++++++++++++ .../X86}/CallPattern.cs | 2 +- .../X86}/GCArgTable.cs | 2 +- .../GCInfoDecoding => GCInfo/X86}/GCInfo.cs | 30 +++++++++-- .../X86}/GCInfoTargetExtensions.cs | 2 +- .../X86}/GCTransition.cs | 2 +- .../GCInfoDecoding => GCInfo/X86}/InfoHdr.cs | 2 +- .../StackWalk/Context/X86/X86Unwinder.cs | 19 +++---- .../CoreCLRContracts.cs | 1 + .../tests/DumpTests/StackWalkDumpTests.cs | 53 +++++++++++++++++++ 13 files changed, 164 insertions(+), 21 deletions(-) create mode 100644 src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoX86_1.cs rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/{StackWalk/Context/X86/GCInfoDecoding => GCInfo/X86}/CallPattern.cs (97%) rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/{StackWalk/Context/X86/GCInfoDecoding => GCInfo/X86}/GCArgTable.cs (99%) rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/{StackWalk/Context/X86/GCInfoDecoding => GCInfo/X86}/GCInfo.cs (84%) rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/{StackWalk/Context/X86/GCInfoDecoding => GCInfo/X86}/GCInfoTargetExtensions.cs (96%) rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/{StackWalk/Context/X86/GCInfoDecoding => GCInfo/X86}/GCTransition.cs (98%) rename src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/{StackWalk/Context/X86/GCInfoDecoding => GCInfo/X86}/InfoHdr.cs (99%) diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index 963d7eb21514bc..d809bc4cdbbd23 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -1,6 +1,6 @@ # Contract GCInfo -This contract is for fetching information related to GCInfo associated with native code. Currently, this contract does not support x86 architecture. +This contract is for fetching information related to GCInfo associated with native code. The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is not currently supported by this contract. @@ -25,10 +25,19 @@ uint GetCodeLength(IGCInfoHandle handle); // Returns the stack base register number decoded from GCInfo uint GetStackBaseRegister(IGCInfoHandle handle); +// Returns the size in bytes of stack-passed parameters at the call to the method. +// Always returns 0 except on x86 (mirrors EECodeManager::GetStackParameterSize); on x86 it +// returns 0 for varargs methods (caller-popped) and otherwise returns the argument size +// encoded in the GC info header. +uint GetSizeOfStackParameterArea(IGCInfoHandle handle); + // Returns the list of interruptible code offset ranges from the GCInfo +// (not implemented for x86 — x86 encodes per-offset transitions rather than explicit ranges). IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle); // Returns all live GC slots at the given instruction offset +// (not implemented for x86 — see X86GCInfo for the underlying transition data; the cDAC +// adapter is future work). IReadOnlyList EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, GcSlotEnumerationOptions options); ``` diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index 5ca5484bc71a19..e56575be5d5200 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -663,6 +663,8 @@ The x86 platform has some major differences to other platforms. In general this #### GCInfo Parsing The GCInfo structure is encoded using a variety of formats to optimize for speed of decoding and size on disk. For information on decoding and parsing refer to [GC Information Encoding for x86](../coreclr/jit/jit-gc-info-x86.md). +The x86 GCInfo decoder lives under the [GCInfo contract](GCInfo.md) at `src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/` (`X86GCInfo` and the supporting `InfoHdr`, `GCArgTable`, `GCTransition`, `CallPattern`, `GCInfoTargetExtensions` types). It is shared between the GCInfo contract (which exposes the offset-independent queries `GetCodeLength` / `GetStackBaseRegister` / `GetSizeOfStackParameterArea` for SOS callers) and the x86 stack walker (which constructs `X86GCInfo` directly with a `relativeOffset` to access offset-bound state — `IsInProlog`, `IsInEpilog`, `PrologOffset`, `EpilogOffset`, `PushedArgSize` — that is not exposed through `IGCInfoDecoder`). + #### Unwinding Algorithm The x86 architecture uses a custom unwinding algorithm defined in `gc_unwind_x86.inl`. The cDAC uses a copy of this algorithm ported to managed code in `X86Unwinder.cs`. diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index 7c8465b2baf26e..60e7887e26d3f2 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -419,9 +419,9 @@ uint IExecutionManager.GetStackParameterSize(CodeBlockHandle codeInfoHandle) if (gcInfoAddress == TargetPointer.Null) throw new InvalidOperationException($"GC info not available for {codeInfoHandle.Address}"); - uint relOffset = (uint)eman.GetRelativeOffset(codeInfoHandle).Value; - StackWalkHelpers.X86.GCInfo gcInfo = new(_target, gcInfoAddress, gcInfoVersion, relOffset); - return gcInfo.Header.VarArgs ? 0u : gcInfo.Header.ArgCount * (uint)_target.PointerSize; + IGCInfo gcInfoContract = _target.Contracts.GCInfo; + IGCInfoHandle handle = gcInfoContract.DecodePlatformSpecificGCInfo(gcInfoAddress, gcInfoVersion); + return gcInfoContract.GetSizeOfStackParameterArea(handle); } TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoX86_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoX86_1.cs new file mode 100644 index 00000000000000..27315911207be2 --- /dev/null +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoX86_1.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers; + +namespace Microsoft.Diagnostics.DataContractReader.Contracts; + +/// +/// IGCInfo implementation for x86. x86 uses the legacy bit-packed InfoHdr GC info format, +/// which is fundamentally different from the GcInfoDecoder format used by every other +/// architecture, so it cannot share . +/// The decoder lives at and is shared with the +/// x86 stack walker. +/// +internal sealed class GCInfoX86_1 : IGCInfo +{ + private readonly Target _target; + + internal GCInfoX86_1(Target target) + { + _target = target; + } + + IGCInfoHandle IGCInfo.DecodePlatformSpecificGCInfo(TargetPointer gcInfoAddress, uint gcVersion) + => new GCInfoHelpers.X86.X86GCInfo(_target, gcInfoAddress, gcVersion); + + IGCInfoHandle IGCInfo.DecodeInterpreterGCInfo(TargetPointer gcInfoAddress, uint gcVersion) + => new GcInfoDecoder(_target, gcInfoAddress, gcVersion); + + uint IGCInfo.GetCodeLength(IGCInfoHandle gcInfoHandle) + => AssertCorrectHandle(gcInfoHandle).GetCodeLength(); + + uint IGCInfo.GetStackBaseRegister(IGCInfoHandle gcInfoHandle) + => AssertCorrectHandle(gcInfoHandle).GetStackBaseRegister(); + + uint IGCInfo.GetSizeOfStackParameterArea(IGCInfoHandle gcInfoHandle) + => AssertCorrectHandle(gcInfoHandle).GetSizeOfStackParameterArea(); + + IReadOnlyList IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle) + => AssertCorrectHandle(gcInfoHandle).GetInterruptibleRanges(); + + IReadOnlyList IGCInfo.EnumerateLiveSlots(IGCInfoHandle gcInfoHandle, uint instructionOffset, GcSlotEnumerationOptions options) + => AssertCorrectHandle(gcInfoHandle).EnumerateLiveSlots(instructionOffset, options); + + private static IGCInfoDecoder AssertCorrectHandle(IGCInfoHandle gcInfoHandle) + { + if (gcInfoHandle is not IGCInfoDecoder handle) + throw new System.ArgumentException("Invalid GC info handle", nameof(gcInfoHandle)); + + return handle; + } +} diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/CallPattern.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/CallPattern.cs similarity index 97% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/CallPattern.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/CallPattern.cs index da2024c826262a..e660eea2452c3c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/CallPattern.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/CallPattern.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86; +namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86; internal static class CallPattern { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCArgTable.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs similarity index 99% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCArgTable.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs index 3d2ec4afdaf071..d0284332d230d6 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCArgTable.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCArgTable.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86; +namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86; public class GCArgTable { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs similarity index 84% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCInfo.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs index 513e3401acab8e..d18ebe45e9ccf0 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs @@ -7,7 +7,7 @@ using System.Diagnostics; using System.Linq; -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86; +namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86; [Flags] public enum RegMask @@ -27,7 +27,7 @@ public enum RegMask RM_CALLEE_TRASHED = RM_ALL & ~RM_CALLEE_SAVED, } -public record GCInfo +public record X86GCInfo : IGCInfoDecoder { private const uint MINIMUM_SUPPORTED_GCINFO_VERSION = 4; private const uint MAXIMUM_SUPPORTED_GCINFO_VERSION = 5; @@ -69,7 +69,7 @@ public record GCInfo public uint PushedArgSize => _pushedArgSize.Value; private readonly Lazy _pushedArgSize; - public GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, uint relativeOffset) + public X86GCInfo(Target target, TargetPointer gcInfoAddress, uint gcInfoVersion, uint relativeOffset = 0) { if (gcInfoVersion < MINIMUM_SUPPORTED_GCINFO_VERSION) { @@ -242,4 +242,28 @@ private uint CalculatePushedArgSize() return (uint)(depth * _target.PointerSize); } + + uint IGCInfoDecoder.GetCodeLength() => MethodSize; + + uint IGCInfoDecoder.GetStackBaseRegister() + { + // x86 ModRM register encoding: ESP = 4, EBP = 5. EBP is the stack base for + // EBP-frames and double-aligned frames; otherwise stack base is ESP. + const uint REG_ESP = 4; + const uint REG_EBP = 5; + return (Header.EbpFrame || Header.DoubleAlign) ? REG_EBP : REG_ESP; + } + + uint IGCInfoDecoder.GetSizeOfStackParameterArea() + { + // VarArgs methods report 0 (caller pops, count not statically known). + // Mirrors the logic in EECodeManager::GetStackParameterSize on x86. + return Header.VarArgs ? 0u : Header.ArgCount * (uint)_target.PointerSize; + } + + IReadOnlyList IGCInfoDecoder.GetInterruptibleRanges() + => throw new NotSupportedException("x86 GC info does not encode explicit interruptible ranges; per-offset transitions are used instead. Decoding for the cDAC IGCInfoDecoder consumers is not yet implemented."); + + IReadOnlyList IGCInfoDecoder.EnumerateLiveSlots(uint instructionOffset, GcSlotEnumerationOptions options) + => throw new NotSupportedException("x86 GC info live-slot enumeration through IGCInfoDecoder is not yet implemented; the underlying InfoHdr/Transitions data is decoded but the IGCInfoDecoder.EnumerateLiveSlots adapter is future work."); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCInfoTargetExtensions.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfoTargetExtensions.cs similarity index 96% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCInfoTargetExtensions.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfoTargetExtensions.cs index 8634e0cde7afbe..b92cfe3664baeb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCInfoTargetExtensions.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfoTargetExtensions.cs @@ -3,7 +3,7 @@ using System; -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86; +namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86; public static class GCInfoTargetExtensions { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCTransition.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs similarity index 98% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCTransition.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs index 71e9003e997e6a..d693702051410b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/GCTransition.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCTransition.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Text; -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86; +namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86; public enum Action { diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/InfoHdr.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/InfoHdr.cs similarity index 99% rename from src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/InfoHdr.cs rename to src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/InfoHdr.cs index 42a204084f2d8c..1e2745ba16798f 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/GCInfoDecoding/InfoHdr.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/InfoHdr.cs @@ -6,7 +6,7 @@ using System.Collections.Immutable; using System.Diagnostics; -namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86; +namespace Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86; /// /// based on src\inc\gcinfotypes.h InfoHdrSmall diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/X86Unwinder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/X86Unwinder.cs index e7a4c4505ca95d..f28dc4a9667fd3 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/X86Unwinder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/X86/X86Unwinder.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using Microsoft.Diagnostics.DataContractReader.Contracts.Extensions; +using Microsoft.Diagnostics.DataContractReader.Contracts.GCInfoHelpers.X86; using static Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86Context; namespace Microsoft.Diagnostics.DataContractReader.Contracts.StackWalkHelpers.X86; @@ -62,7 +63,7 @@ public bool Unwind(ref X86Context context) TargetPointer funcletStart = eman.GetFuncletStartAddress(cbh); bool isFunclet = eman.IsFunclet(cbh); - GCInfo gcInfo = new(_target, gcInfoAddress, gcInfoVersion, relOffset); + X86GCInfo gcInfo = new(_target, gcInfoAddress, gcInfoVersion, relOffset); if (gcInfo.IsInEpilog) { @@ -96,7 +97,7 @@ public bool Unwind(ref X86Context context) #endregion #region Unwind Logic - private void UnwindEpilog(ref X86Context context, GCInfo gcInfo, TargetPointer epilogBase) + private void UnwindEpilog(ref X86Context context, X86GCInfo gcInfo, TargetPointer epilogBase) { Debug.Assert(gcInfo.IsInEpilog); Debug.Assert(gcInfo.EpilogOffset > 0); @@ -114,7 +115,7 @@ private void UnwindEpilog(ref X86Context context, GCInfo gcInfo, TargetPointer e context.Esp += ESPIncrementOnReturn(gcInfo); } - private void UnwindEbpDoubleAlignFrameEpilog(ref X86Context context, GCInfo gcInfo, TargetPointer epilogBase) + private void UnwindEbpDoubleAlignFrameEpilog(ref X86Context context, X86GCInfo gcInfo, TargetPointer epilogBase) { /* See how many instructions we have executed in the epilog to determine which callee-saved registers @@ -241,7 +242,7 @@ have already been popped */ context.Esp = esp; } - private void UnwindEspFrameEpilog(ref X86Context context, GCInfo gcInfo, TargetPointer epilogBase) + private void UnwindEspFrameEpilog(ref X86Context context, X86GCInfo gcInfo, TargetPointer epilogBase) { Debug.Assert(gcInfo.IsInEpilog); Debug.Assert(!gcInfo.Header.EbpFrame && !gcInfo.Header.DoubleAlign); @@ -307,7 +308,7 @@ regsMask as well to exclude registers which have already been popped. */ context.Esp = esp; } - private void UnwindEspFrame(ref X86Context context, GCInfo gcInfo, TargetPointer methodStart) + private void UnwindEspFrame(ref X86Context context, X86GCInfo gcInfo, TargetPointer methodStart) { Debug.Assert(!gcInfo.Header.EbpFrame && !gcInfo.Header.DoubleAlign); Debug.Assert(!gcInfo.IsInEpilog); @@ -347,7 +348,7 @@ private void UnwindEspFrame(ref X86Context context, GCInfo gcInfo, TargetPointer context.Esp = esp + ESPIncrementOnReturn(gcInfo); } - private void UnwindEspFrameProlog(ref X86Context context, GCInfo gcInfo, TargetPointer methodStart) + private void UnwindEspFrameProlog(ref X86Context context, X86GCInfo gcInfo, TargetPointer methodStart) { Debug.Assert(gcInfo.IsInProlog); Debug.Assert(!gcInfo.Header.EbpFrame && !gcInfo.Header.DoubleAlign); @@ -426,7 +427,7 @@ private void UnwindEspFrameProlog(ref X86Context context, GCInfo gcInfo, TargetP private bool UnwindEbpDoubleAlignFrame( ref X86Context context, - GCInfo gcInfo, + X86GCInfo gcInfo, TargetPointer methodStart, TargetPointer funcletStart, bool isFunclet) @@ -510,7 +511,7 @@ private bool UnwindEbpDoubleAlignFrame( return true; } - private void UnwindEbpDoubleAlignFrameProlog(ref X86Context context, GCInfo gcInfo, TargetPointer methodStart) + private void UnwindEbpDoubleAlignFrameProlog(ref X86Context context, X86GCInfo gcInfo, TargetPointer methodStart) { Debug.Assert(gcInfo.IsInProlog); Debug.Assert(gcInfo.Header.EbpFrame || gcInfo.Header.DoubleAlign); @@ -610,7 +611,7 @@ private static bool InstructionAlreadyExecuted(uint walkOffset, uint actualHaltO return walkOffset < actualHaltOffset; } - private uint ESPIncrementOnReturn(GCInfo gcInfo) + private uint ESPIncrementOnReturn(X86GCInfo gcInfo) { uint stackParameterSize = gcInfo.Header.VarArgs ? 0 // varargs are caller-popped diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs index fe3fabf7f8d8f9..1343a6028056bb 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs @@ -60,6 +60,7 @@ public static void Register(ContractRegistry registry) return arch switch { RuntimeInfoArchitecture.X64 => new GCInfo_1(t), + RuntimeInfoArchitecture.X86 => new GCInfoX86_1(t), RuntimeInfoArchitecture.Arm64 => new GCInfo_1(t), RuntimeInfoArchitecture.Arm => new GCInfo_1(t), RuntimeInfoArchitecture.LoongArch64 => new GCInfo_1(t), diff --git a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs index e0832f67547037..e6043d99e7f71a 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs @@ -333,6 +333,59 @@ public unsafe void VarargPInvoke_GetCodeHeaderDataWithInvalidPrecodeAddress(Test Assert.Fail("Expected to find a frame with a valid entry point"); } + /// + /// Exercises for the IL stub MethodDesc + /// that ships a vararg P/Invoke. This is the path SOS !clru uses after !IP2MD: + /// it takes the JIT-emitted native code address of the IL stub and expects to get back + /// a populated with non-zero MethodSize. Regression + /// coverage for the x86 cDAC GetCodeHeaderData failure where the IGCInfo contract had no + /// x86 implementation and threw NotImplementedException, breaking !clru on .NET 11. + /// + [ConditionalTheory] + [MemberData(nameof(TestConfigurations))] + [SkipOnVersion("net10.0", "InlinedCallFrame.Datum was added after net10.0")] + [SkipOnOS(IncludeOnly = "windows", Reason = "VarargPInvoke debuggee uses msvcrt.dll (Windows only)")] + public unsafe void VarargPInvoke_GetCodeHeaderDataForILStub_ReturnsMethodSize(TestConfiguration config) + { + InitializeDumpTest(config, "VarargPInvoke", "full"); + IStackWalk stackWalk = Target.Contracts.StackWalk; + IRuntimeTypeSystem rts = Target.Contracts.RuntimeTypeSystem; + ISOSDacInterface sosDac = new SOSDacImpl(Target, legacyObj: null); + + ThreadData crashingThread = DumpTestHelpers.FindThreadWithMethod(Target, "Main"); + IEnumerable frames = DumpTestStackWalker.LegacyVisibleFrames(stackWalk, crashingThread); + + foreach (IStackDataFrameHandle frame in frames) + { + TargetPointer methodDescPtr = stackWalk.GetMethodDescPtr(frame); + if (methodDescPtr == TargetPointer.Null) + continue; + + MethodDescHandle mdHandle = rts.GetMethodDescHandle(methodDescPtr); + if (!rts.IsILStub(mdHandle)) + continue; + + TargetCodePointer entryPoint = rts.GetMethodEntryPointIfExists(mdHandle); + Assert.NotEqual(TargetCodePointer.Null, entryPoint); + + DacpCodeHeaderData codeHeaderData; + int hr = sosDac.GetCodeHeaderData(new ClrDataAddress(entryPoint.Value), &codeHeaderData); + AssertHResult(HResults.S_OK, hr); + + Assert.Equal(JitTypes.TYPE_JIT, codeHeaderData.JITType); + Assert.Equal(methodDescPtr.ToClrDataAddress(Target), codeHeaderData.MethodDescPtr); + Assert.True(codeHeaderData.MethodSize > 0, + $"Expected non-zero MethodSize for IL stub (was {codeHeaderData.MethodSize}). " + + "On x86 this asserts that the GCInfo contract returns a valid X86GCInfo decoder " + + "rather than the default IGCInfo whose GetCodeLength throws NotImplementedException."); + Assert.NotEqual(default, codeHeaderData.MethodStart); + + return; + } + + Assert.Fail("Expected to find an IL stub MethodDesc on the crashing thread stack"); + } + // ========== GetContext API tests ========== [ConditionalTheory] From 1488f42a441137c40818191b53450a8041590de1 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 16 Jun 2026 09:19:46 -0400 Subject: [PATCH 2/6] Address PR feedback: x86 GCInfo is partially supported, not unsupported Tweaks the second intro paragraph in GCInfo.md to say x86 is "partially supported" rather than "not currently supported", reflecting the IGCInfo implementation added in the first commit. Addresses https://github.com/dotnet/runtime/pull/129456#discussion_r3417874573 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GCInfo.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index d809bc4cdbbd23..7c3e2c0b1969c3 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -2,7 +2,7 @@ This contract is for fetching information related to GCInfo associated with native code. -The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is not currently supported by this contract. +The GCInfo contract has platform specific implementations as GCInfo differs per architecture. With the exception of x86, all platforms have a common encoding scheme with different encoding lengths and normalization functions for data. x86 uses an entirely different scheme which is partially supported by this contract. ## APIs of contract From 2389991ed176106583c32390aa0da2f7cb6d95fa Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 16 Jun 2026 11:05:53 -0400 Subject: [PATCH 3/6] remove false assertion --- src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs index e6043d99e7f71a..b0b957956a9078 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs @@ -372,7 +372,6 @@ public unsafe void VarargPInvoke_GetCodeHeaderDataForILStub_ReturnsMethodSize(Te int hr = sosDac.GetCodeHeaderData(new ClrDataAddress(entryPoint.Value), &codeHeaderData); AssertHResult(HResults.S_OK, hr); - Assert.Equal(JitTypes.TYPE_JIT, codeHeaderData.JITType); Assert.Equal(methodDescPtr.ToClrDataAddress(Target), codeHeaderData.MethodDescPtr); Assert.True(codeHeaderData.MethodSize > 0, $"Expected non-zero MethodSize for IL stub (was {codeHeaderData.MethodSize}). " + From 2b9765283ef569328b891d005a72fd8443811340 Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Tue, 16 Jun 2026 14:38:29 -0400 Subject: [PATCH 4/6] [cdac] Split EECodeManager pop-size from GcInfoDecoder scratch area Address PR feedback: the IGCInfo contract was conflating two distinct concepts under one API name. GcInfoDecoder::GetSizeOfStackParameterArea returns the outgoing-argument scratch area (_fixedStackParameterScratchArea, platform-specific, used by the GC scanner). EECodeManager::GetStackParameterSize returns the x86 __stdcall callee-popped argument size (used by the debugger to adjust SP after return). Keep IGCInfo.GetSizeOfStackParameterArea as the GcInfoDecoder scratch-area accessor (consumed by GcScanner). On x86 it now throws NotSupportedException since x86 has no separate scratch-area concept. Add IGCInfo.GetCalleePoppedArgumentsSize mirroring EECodeManager::GetStackParameterSize: 0 by default, x86 returns varargs ? 0 : argCount * ptrSize. ExecutionManagerCore.GetStackParameterSize now routes through this new method. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GCInfo.md | 16 ++++++++++++---- .../Contracts/IGCInfo.cs | 1 + .../ExecutionManager/ExecutionManagerCore.cs | 2 +- .../Contracts/GCInfo/GCInfoX86_1.cs | 3 +++ .../Contracts/GCInfo/GCInfo_1.cs | 6 ++++++ .../Contracts/GCInfo/IGCInfoDecoder.cs | 5 +++++ .../Contracts/GCInfo/X86/GCInfo.cs | 11 +++++++++-- 7 files changed, 37 insertions(+), 7 deletions(-) diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index 7c3e2c0b1969c3..70964f409a93d1 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -25,12 +25,20 @@ uint GetCodeLength(IGCInfoHandle handle); // Returns the stack base register number decoded from GCInfo uint GetStackBaseRegister(IGCInfoHandle handle); -// Returns the size in bytes of stack-passed parameters at the call to the method. -// Always returns 0 except on x86 (mirrors EECodeManager::GetStackParameterSize); on x86 it -// returns 0 for varargs methods (caller-popped) and otherwise returns the argument size -// encoded in the GC info header. +// Returns the size in bytes of the outgoing-argument scratch area in the current +// frame, decoded from `_fixedStackParameterScratchArea` (`SizeOfStackOutgoingAndScratchArea`, +// encoded via `SIZE_OF_STACK_AREA_ENCBASE`). Platform-specific and can be non-zero on +// AMD64/ARM64/ARM/RISCV64/LoongArch64. Not supported on x86 (the x86 GC info scheme has +// no separate scratch-area concept; the x86 GC walker uses per-offset transitions). uint GetSizeOfStackParameterArea(IGCInfoHandle handle); +// Returns the size in bytes of stack-passed arguments that the callee pops on return, +// mirroring native `EECodeManager::GetStackParameterSize` (eetwain.cpp). Non-zero only +// on x86 (where managed code uses `__stdcall` calling convention with callee-popped args): +// returns 0 for varargs (caller-popped) and otherwise the argument size from the GC info +// header. Returns 0 on every other architecture. +uint GetCalleePoppedArgumentsSize(IGCInfoHandle handle); + // Returns the list of interruptible code offset ranges from the GCInfo // (not implemented for x86 — x86 encodes per-offset transitions rather than explicit ranges). IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs index 990af49d265f64..74e085dc62508c 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IGCInfo.cs @@ -52,6 +52,7 @@ public interface IGCInfo : IContract uint GetCodeLength(IGCInfoHandle handle) => throw new NotImplementedException(); uint GetStackBaseRegister(IGCInfoHandle handle) => throw new NotImplementedException(); uint GetSizeOfStackParameterArea(IGCInfoHandle handle) => throw new NotImplementedException(); + uint GetCalleePoppedArgumentsSize(IGCInfoHandle handle) => throw new NotImplementedException(); IReadOnlyList GetInterruptibleRanges(IGCInfoHandle handle) => throw new NotImplementedException(); IReadOnlyList EnumerateLiveSlots(IGCInfoHandle handle, uint instructionOffset, GcSlotEnumerationOptions options) => throw new NotImplementedException(); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs index 60e7887e26d3f2..1e9aff1540ba9a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/ExecutionManager/ExecutionManagerCore.cs @@ -421,7 +421,7 @@ uint IExecutionManager.GetStackParameterSize(CodeBlockHandle codeInfoHandle) IGCInfo gcInfoContract = _target.Contracts.GCInfo; IGCInfoHandle handle = gcInfoContract.DecodePlatformSpecificGCInfo(gcInfoAddress, gcInfoVersion); - return gcInfoContract.GetSizeOfStackParameterArea(handle); + return gcInfoContract.GetCalleePoppedArgumentsSize(handle); } TargetPointer IExecutionManager.FindReadyToRunModule(TargetPointer address) diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoX86_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoX86_1.cs index 27315911207be2..fa2d810c765fdd 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoX86_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoX86_1.cs @@ -37,6 +37,9 @@ uint IGCInfo.GetStackBaseRegister(IGCInfoHandle gcInfoHandle) uint IGCInfo.GetSizeOfStackParameterArea(IGCInfoHandle gcInfoHandle) => AssertCorrectHandle(gcInfoHandle).GetSizeOfStackParameterArea(); + uint IGCInfo.GetCalleePoppedArgumentsSize(IGCInfoHandle gcInfoHandle) + => AssertCorrectHandle(gcInfoHandle).GetCalleePoppedArgumentsSize(); + IReadOnlyList IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle) => AssertCorrectHandle(gcInfoHandle).GetInterruptibleRanges(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs index ad261440db826b..d13e0f6051e98d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfo_1.cs @@ -40,6 +40,12 @@ uint IGCInfo.GetSizeOfStackParameterArea(IGCInfoHandle gcInfoHandle) return handle.GetSizeOfStackParameterArea(); } + uint IGCInfo.GetCalleePoppedArgumentsSize(IGCInfoHandle gcInfoHandle) + { + IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); + return handle.GetCalleePoppedArgumentsSize(); + } + IReadOnlyList IGCInfo.GetInterruptibleRanges(IGCInfoHandle gcInfoHandle) { IGCInfoDecoder handle = AssertCorrectHandle(gcInfoHandle); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs index e6b146e4396284..279992fb4cd0dc 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/IGCInfoDecoder.cs @@ -12,6 +12,11 @@ internal interface IGCInfoDecoder : IGCInfoHandle uint GetCodeLength(); uint GetStackBaseRegister(); uint GetSizeOfStackParameterArea(); + + // Default 0; mirrors native EECodeManager::GetStackParameterSize, which only + // returns non-zero on x86 (where managed code uses __stdcall, callee-popped args). + uint GetCalleePoppedArgumentsSize() => 0; + IReadOnlyList GetInterruptibleRanges(); IReadOnlyList EnumerateLiveSlots(uint instructionOffset, GcSlotEnumerationOptions options); } diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs index d18ebe45e9ccf0..9a8c44f7234f38 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/GCInfo.cs @@ -255,9 +255,16 @@ uint IGCInfoDecoder.GetStackBaseRegister() } uint IGCInfoDecoder.GetSizeOfStackParameterArea() + => throw new NotSupportedException( + "x86 GC info does not encode a separate outgoing-argument scratch area; the cDAC " + + "GC scanner does not consume scratch-area sizing on x86 (the legacy x86 GC walker " + + "reasons over per-offset transitions instead)."); + + uint IGCInfoDecoder.GetCalleePoppedArgumentsSize() { - // VarArgs methods report 0 (caller pops, count not statically known). - // Mirrors the logic in EECodeManager::GetStackParameterSize on x86. + // Mirrors native ::GetStackParameterSize(hdrInfo) in gc_unwind_x86.inl: varargs are + // caller-popped (return 0); other methods report the argument size from the GC info + // header. Used by EECodeManager::GetStackParameterSize on x86. return Header.VarArgs ? 0u : Header.ArgCount * (uint)_target.PointerSize; } From d46fdf5a7a28f1f0f95404db5fd231013b84122e Mon Sep 17 00:00:00 2001 From: Max Charlamb Date: Wed, 17 Jun 2026 10:35:50 -0400 Subject: [PATCH 5/6] [cdac] Fix VarargPInvoke IL stub regression test to actually exercise x86 GCInfo The previous test passed the IL stub's stable entry point (GetMethodEntryPointIfExists) to GetCodeHeaderData. On x86 r2r that resolves to a precode, so SOSDacImpl.GetCodeHeaderData takes the NonVirtualEntry2MethodDesc fallback (MethodSize=0, TYPE_UNKNOWN) and never calls IGCInfo.GetCodeLength - the exact path the regression was about. Pass the live instruction pointer of a Frameless IL stub frame instead. The IP is guaranteed to be inside the JIT-emitted code body, so GetCodeHeaderData takes the full path through IGCInfo.GetCodeLength and the test actually exercises the x86 X86GCInfo decoder it claims to cover. Validated locally against the x86 dumps from the original failing CI run: all 452 dump tests pass (previously 1 failure). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/DumpTests/StackWalkDumpTests.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs index b0b957956a9078..63180dd9c47d3e 100644 --- a/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs +++ b/src/native/managed/cdac/tests/DumpTests/StackWalkDumpTests.cs @@ -335,9 +335,10 @@ public unsafe void VarargPInvoke_GetCodeHeaderDataWithInvalidPrecodeAddress(Test /// /// Exercises for the IL stub MethodDesc - /// that ships a vararg P/Invoke. This is the path SOS !clru uses after !IP2MD: - /// it takes the JIT-emitted native code address of the IL stub and expects to get back - /// a populated with non-zero MethodSize. Regression + /// that ships a vararg P/Invoke, passing the live instruction pointer of an executing + /// IL stub frame. This is the path SOS !clru uses after !IP2MD: the IP is + /// inside the JIT-emitted code body, so GetCodeHeaderData takes the full path + /// through IGCInfo.GetCodeLength rather than the precode/stub fallback. Regression /// coverage for the x86 cDAC GetCodeHeaderData failure where the IGCInfo contract had no /// x86 implementation and threw NotImplementedException, breaking !clru on .NET 11. /// @@ -357,6 +358,9 @@ public unsafe void VarargPInvoke_GetCodeHeaderDataForILStub_ReturnsMethodSize(Te foreach (IStackDataFrameHandle frame in frames) { + if (frame.State != StackWalkState.Frameless) + continue; + TargetPointer methodDescPtr = stackWalk.GetMethodDescPtr(frame); if (methodDescPtr == TargetPointer.Null) continue; @@ -365,13 +369,14 @@ public unsafe void VarargPInvoke_GetCodeHeaderDataForILStub_ReturnsMethodSize(Te if (!rts.IsILStub(mdHandle)) continue; - TargetCodePointer entryPoint = rts.GetMethodEntryPointIfExists(mdHandle); - Assert.NotEqual(TargetCodePointer.Null, entryPoint); + TargetPointer ip = stackWalk.GetInstructionPointer(frame); + Assert.NotEqual(TargetPointer.Null, ip); DacpCodeHeaderData codeHeaderData; - int hr = sosDac.GetCodeHeaderData(new ClrDataAddress(entryPoint.Value), &codeHeaderData); + int hr = sosDac.GetCodeHeaderData(new ClrDataAddress(ip.Value), &codeHeaderData); AssertHResult(HResults.S_OK, hr); + Assert.Equal(JitTypes.TYPE_JIT, codeHeaderData.JITType); Assert.Equal(methodDescPtr.ToClrDataAddress(Target), codeHeaderData.MethodDescPtr); Assert.True(codeHeaderData.MethodSize > 0, $"Expected non-zero MethodSize for IL stub (was {codeHeaderData.MethodSize}). " + @@ -382,7 +387,7 @@ public unsafe void VarargPInvoke_GetCodeHeaderDataForILStub_ReturnsMethodSize(Te return; } - Assert.Fail("Expected to find an IL stub MethodDesc on the crashing thread stack"); + Assert.Fail("Expected to find an IL stub Frameless frame on the crashing thread stack"); } // ========== GetContext API tests ========== From 28919cdd78abb58866f9b459378035f0aaf99e02 Mon Sep 17 00:00:00 2001 From: Max Charlamb <44248479+max-charlamb@users.noreply.github.com> Date: Wed, 17 Jun 2026 10:44:14 -0400 Subject: [PATCH 6/6] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/design/datacontracts/StackWalk.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design/datacontracts/StackWalk.md b/docs/design/datacontracts/StackWalk.md index e56575be5d5200..5064eda133f6f5 100644 --- a/docs/design/datacontracts/StackWalk.md +++ b/docs/design/datacontracts/StackWalk.md @@ -663,7 +663,7 @@ The x86 platform has some major differences to other platforms. In general this #### GCInfo Parsing The GCInfo structure is encoded using a variety of formats to optimize for speed of decoding and size on disk. For information on decoding and parsing refer to [GC Information Encoding for x86](../coreclr/jit/jit-gc-info-x86.md). -The x86 GCInfo decoder lives under the [GCInfo contract](GCInfo.md) at `src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/` (`X86GCInfo` and the supporting `InfoHdr`, `GCArgTable`, `GCTransition`, `CallPattern`, `GCInfoTargetExtensions` types). It is shared between the GCInfo contract (which exposes the offset-independent queries `GetCodeLength` / `GetStackBaseRegister` / `GetSizeOfStackParameterArea` for SOS callers) and the x86 stack walker (which constructs `X86GCInfo` directly with a `relativeOffset` to access offset-bound state — `IsInProlog`, `IsInEpilog`, `PrologOffset`, `EpilogOffset`, `PushedArgSize` — that is not exposed through `IGCInfoDecoder`). +The x86 GCInfo decoder lives under the [GCInfo contract](GCInfo.md) at `src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/X86/` (`X86GCInfo` and the supporting `InfoHdr`, `GCArgTable`, `GCTransition`, `CallPattern`, `GCInfoTargetExtensions` types). It is shared between the GCInfo contract (which exposes the offset-independent queries `GetCodeLength` / `GetStackBaseRegister` / `GetCalleePoppedArgumentsSize` for SOS callers) and the x86 stack walker (which constructs `X86GCInfo` directly with a `relativeOffset` to access offset-bound state — `IsInProlog`, `IsInEpilog`, `PrologOffset`, `EpilogOffset`, `PushedArgSize` — that is not exposed through `IGCInfoDecoder`). #### Unwinding Algorithm