Skip to content

[cDAC] ArgIterator stress validation: enable byte-identical GCRefMap blobs across x64/x86 and wire ARGITER sub-check into Helix#129769

Open
max-charlamb wants to merge 37 commits into
dotnet:mainfrom
max-charlamb:cdac-shared-argiterator
Open

[cDAC] ArgIterator stress validation: enable byte-identical GCRefMap blobs across x64/x86 and wire ARGITER sub-check into Helix#129769
max-charlamb wants to merge 37 commits into
dotnet:mainfrom
max-charlamb:cdac-shared-argiterator

Conversation

@max-charlamb

Copy link
Copy Markdown
Member

Builds out the cDAC CallingConvention.EnumerateArguments contract until its GCRefMap blob matches the runtime's ComputeCallRefMap byte-for-byte across the full argument-shape surface (x64 + x86), and wires an independent cdacstress ARGITER sub-check into Helix so future regressions surface as CI failures.

Why

The cDAC has been growing an in-process port of the runtime ArgIterator (PRs that landed earlier in this branch's history). To verify the port matches the runtime, this PR adds an end-to-end check that, on every managed allocation, builds a GCRefMap blob from the cDAC's EnumerateArguments output and compares it byte-for-byte against the runtime's ComputeCallRefMap for every MethodDesc on the stack. Any divergence fails the test and dumps a per-slot mismatch table.

Coverage / matrix

After this PR, all combinations below produce 0 fail / 0 skip / 0 error:

Debuggee x86 x64
Comprehensive 311 / 0 / 0 292 / 0 / 0
StructScenarios 275 / 0 / 0 276 / 0 / 0
CallSignatures (ARGITER only) 350 / 0 / 0 349 / 0 / 0

The new CallSignatures debuggee covers: argument-count / register-vs-stack spilling, ByRef / in / out, native + function pointers, single- and multi-dim arrays, by-value structs with embedded GC refs at varied offsets, nested structs (up to 3 levels deep), value-type this, Span<T> / ReadOnlySpan<T> / ref structs (including nested ref structs containing Span<T>), generics (ref / value / multi-arg / constrained / generic value-type instance methods), enums of every backing type, large-struct return (HasRetBuffArg), __arglist varargs, and a mutually-recursive deep stack with mixed signatures.

Contract changes

IRuntimeTypeSystem gains two small APIs that mirror existing runtime helpers:

  • IsByRefLike(TypeHandle) — wraps the existing enum_flag_IsByRefLike (0x1000) low bit. Required to detect Span<T> / ReadOnlySpan<T> / any ref struct arg where the runtime emits INTERIOR tokens per byref field via ByRefPointerOffsetsReporter.
  • GetFieldDescApproxTypeHandle(TargetPointer) — mirrors FieldDesc::GetApproxFieldTypeHandleThrowing. Resolves a field's type via GetMTOfEnclosingClassGetModuleEcmaMetadata.GetMetadataSignature.DecodeFieldSignature (same pipeline SOSDacImpl / DacDbiImpl already use). Returns default on any failure.

Encoder additions

CallingConventionGCRefMapBuilder now produces correct blobs for:

  • By-value structs with embedded GC pointers — walks GetGCDescSeries, translates from boxed offsets to in-frame offsets, emits one REF per pointer slot. Mirrors ReportPointersFromValueTypeArg.
  • ByRefLike structs — recurses through instance fields, emits INTERIOR per ELEMENT_TYPE_BYREF field, recurses into nested ByRefLike value-type fields via the new GetFieldDescApproxTypeHandle. Skips ELEMENT_TYPE_PTR / IntPtr / void* fields so QCall handles (QCallTypeHandle, ObjectHandleOnStack, StringHandleOnStack) correctly produce no token.
  • VarArgs (__arglist) — emits GCRefMapToken.VASigCookie at argit.GetVASigCookieOffset() and stops, mirroring the runtime's FakeGcScanRoots short-circuit.
  • x86-specific pathsWriteStackPop prefix, non-monotonic GCRefMapPosFromOffset for x86 (registers ECX/EDX live at highest offsets but lowest positions), IsTrivialPointerSizedStruct (recursive through single-field value-type wrappers), value-type normalization via GetInternalCorElementType so sub-pointer-size enums classify as register-passable.

Stress harness

src/coreclr/vm/cdacstress.cpp now emits one machine-readable summary line per enabled sub-check at shutdown:

  • [GC_STATS] verifications=N pass=N fail=N known_issue=N (iff GCREFS ran)
  • [ARG_STATS] pass=N fail=N skip=N error=N (iff ARGITER ran)

Presence of each marker is the authoritative signal that its sub-check ran. CdacStressResults parses both and exposes AnyGcRefsRecorded / AnyArgIterRecorded so the assertion helpers can distinguish "sub-check did not run" from "ran but recorded zero verifications".

The xunit harness gains two parallel theory families:

  • GCStress_AllVerificationsPass (DOTNET_CdacStress=0x101) — ConditionalTheory, skipped on Architecture.X86 (the GCREFS sub-check has not been validated on x86 yet).
  • ArgIterStress_AllVerificationsPass (DOTNET_CdacStress=0x201) — runs on every platform. Plus a separate ArgIterOnly variant for CallSignatures, which includes __arglist methods that hit a known cDAC GCREFS gap (the cDAC's GetStackReferences doesn't walk the VASigCookie signature blob to enumerate variadic-tail refs).

CI

runtime-diagnostics.yml cdacStressPlatforms gains windows_x86. The shared CdacBuild job already runs on x86 (it's in cdacDumpPlatforms), so the stress matrix only needs the platform entry.

Commit organization

This PR is a deep stack but each commit is self-contained and reviewable in isolation:

  • 11 foundational commits (the in-process ArgIterator port from earlier branch work)
  • 8 commits adding the cdacstress ARGITER sub-check + encoder support, organized by what they unlock (ByRefLike, nested structs, varargs, enum normalization, x86 enablement)
  • 2 commits adding the test harness + CI integration

A separately-mergeable RTS fix from this branch already landed as #129721 and was dropped in rebase.

Note

This change was authored with assistance from GitHub Copilot.

Max Charlamb and others added 24 commits June 23, 2026 17:03
Move ArgIterator.cs and TransitionBlock.cs from the ILCompiler.ReadyToRun
project directory into src/coreclr/tools/Common/CallingConvention/ to
enable future reuse by the cDAC.

This is a pure file relocation with no code changes. The namespace
remains ILCompiler.DependencyAnalysis.ReadyToRun. The csproj is
updated to use file-link includes from the new Common location.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Create ITypeHandle interface in CallingConvention/ with all type
  queries needed by ArgIterator and TransitionBlock
- Extract TypeHandle struct from ArgIterator.cs into TypeHandle.cs
- Move GetElemSize helper to static method on ITypeHandle
- Replace all GetRuntimeTypeHandle() escape hatches in ArgIterator (3)
  and TransitionBlock (3) with ITypeHandle method calls
- Port all TypeHandle references in ArgIterator/TransitionBlock to
  ITypeHandle (fields, parameters, locals, return types)
- TypeHandle retains GetRuntimeTypeHandle() for crossgen2-only code
  (WasmLowering) but it is not part of the interface

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
TypeHandle wraps TypeDesc which is crossgen2-specific, so it belongs
in the ReadyToRun project rather than in the shared Common directory.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…amespace and remove TypeSystemContext dependency

- Change namespace from ILCompiler.DependencyAnalysis.ReadyToRun to
  Internal.Runtime.CallingConvention for the shared files (ArgIterator,
  TransitionBlock, ITypeHandle)
- Remove TypeSystemContext from ArgIterator constructor; pass
  TransitionBlock directly along with isWindows, objectTypeHandle,
  and intPtrTypeHandle parameters
- Update all consumer files with new using and ArgIterator alias
  to resolve ambiguity with System.ArgIterator

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…peHandle adapter

- Change namespace to Internal.CallingConvention
- Replace TargetDetails with individual params in TransitionBlock.FromTarget
- Extract standalone SystemV/FpStruct types for cDAC use
- Refactor ReportPointersFromStructInRegisters to use ITypeHandle
- Add pragma suppressions for crossgen2 analyzer warnings
- Add CdacTypeHandle adapter wrapping IRuntimeTypeSystem + TypeHandle
- Add ICallingConvention contract with EnumerateCallerStackRefs
- Add CallingConvention_1 implementation using shared ArgIterator
- Wire shared CallingConvention files into cDAC project

TODO: VarArgs support in CallingConvention contract

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…umeration

- Implement EnumerateCallerStackRefs in CallingConvention_1 to walk caller
  stack frames and report GC references for object refs, byrefs, and structs
- Add EnumerateValueTypeGCRefs using GCDesc series to enumerate GC pointers
  within unboxed value types on the stack
- Scope to AMD64 Windows; throw NotImplementedException for SystemV struct-
  in-registers, HFA, FP struct, x86 trivial structs, and WASM field alignment
- Update CdacTypeHandle stubs with NotImplementedException for unimplemented
  platform-specific callbacks
- Add 10 unit tests for GCDesc-based value type GC reference enumeration

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace GC-specific CallerStackGCRef/EnumerateCallerStackRefs with
  general-purpose ArgumentLocation/EnumerateArguments on ICallingConvention
- Wire GcScanner.PromoteCallerStack to use ICallingConvention.EnumerateArguments
  instead of duplicate signature-decoding logic
- Add ReportPointersFromValueType for GCDesc-based struct GC walking
- Add IsUnboxingStub to IRuntimeTypeSystem for correct value-type this
  interior pointer detection (matches native: IsValueType && !IsUnboxingStub)
- Move CdacTypeHandle.cs to CallingConvention folder
- Remove CallingConventionTests.cs (was GCDesc-specific, needs rewrite)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ption frame skip

- Add CallingConventionDumpTests.cs comparing EnumerateArguments against
  R2R GCRefMap by walking thread stacks to collect MethodDescs (skips
  cleanly until CallingConvention contract is published).
- StackWalk_1: skip frames that throw NotImplementedException so DSO
  returns partial results instead of failing the whole walk.
- CallingConvention_1: throw NotImplementedException for the VarArgs and
  SystemV-struct-in-registers paths that aren't supported yet.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…cStress

Splits DOTNET_CdacStress into byte-wide regions:

  byte 0 (0x000000FF) -- WHERE: trigger points {0x01=ALLOC}

  byte 1 (0x0000FF00) -- WHAT:  sub-checks   {0x100=GCREFS, 0x200=ARGITER}

  byte 2 (0x00FF0000) -- MODIFIERS              {0x10000=VERBOSE}

A useful configuration combines at least one WHERE and one WHAT bit (e.g. 0x101 = ALLOC + GCREFS, the new default for the xUnit harness and RunStressTests.ps1).

The existing GC-refs comparison is now gated on the new CDACSTRESS_GCREFS bit (was implicit when CDACSTRESS_ALLOC was set). VERBOSE moves from 0x200 to 0x10000 so the WHAT byte is free for sub-check selectors.

Adds CDACSTRESS_ARGITER as the first new WHAT-byte sub-check. At every fired trigger it walks the active thread's frame chain and, for each frame with a non-null MethodDesc, sends the MD to the cDAC via a new private Request opcode (DACSTRESSPRIV_REQUEST_COMPUTE_ARG_GCREFMAP=0xf2000001) and bucketizes the result as [ARG_PASS/FAIL/SKIP/ERROR]. Per-MD dedup via SetSHash<MethodDesc*> keeps cost flat across long stress runs. Phase 1 wires the round-trip only -- the cDAC handler always returns E_NOTIMPL so every MD logs [ARG_SKIP].

Phase 2+ will wrap the runtime's ComputeCallRefMap as the oracle and have the cDAC handler walk CallingConvention.EnumerateArguments to produce a comparable GCRefMap blob.

Validated on Windows x64 Checked (BasicAlloc debuggee):

  0x001 ALLOC only           -> 0 verifications, 0 ARG_* (WHERE without WHAT is now a no-op, by design)

  0x101 ALLOC + GCREFS       -> 4928 PASS / 4 FAIL (matches pre-restructure baseline)

  0x201 ALLOC + ARGITER      -> 0 PASS, 3 ARG_SKIP   (only argiter runs)

  0x301 ALLOC + GCREFS + ARG -> 4922 PASS / 4 FAIL, 14 ARG_SKIP

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Calls ComputeCallRefMap (frames.cpp) wrapped in EX_TRY for every MD the ArgIterator sub-check encounters, captures the GCRefMap blob, asks the cDAC for the same blob via the existing private Request opcode, and compares byte-for-byte. Mismatches emit [ARG_FAIL] with both blobs hex-dumped.

Phase 2 keeps the cDAC handler stubbed to E_NOTIMPL, so the comparison path stays dormant for real MDs (all responses bucket as [ARG_SKIP]). The validation here is that the runtime oracle runs cleanly for every MD reachable from a stress point -- no fatal throws, no crashes.

Runtime-side failure modes are distinguished in [ARG_SKIP] reason codes:

  reason=runtime-threw           -- ComputeCallRefMap threw (signature unloadable)

  reason=runtime-blob-too-large  -- blob > DacStressArgGCRefMapResponse.Blob[252]

  reason=0x80004001              -- cDAC returned E_NOTIMPL (the Phase 1/2 default)

Validated on Windows x64 Checked (BasicAlloc):

  0x001  -> no-op (unchanged)

  0x101  -> 4926 PASS / 4 fail GCREFS, 0 ARG_* (unchanged)

  0x201  -> 0 PASS, 1 ARG_SKIP (runtime ran, cDAC E_NOTIMPL)

  0x301  -> 4922 PASS / 2 fail GCREFS, 13 ARG_SKIP (rtBlobSize logged proves the oracle executed)

  All exit 100.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ef/array/generic fixes

Phase 3 -- end-to-end ArgIterator comparison

  - DACSTRESSPRIV_REQUEST_COMPUTE_ARG_GCREFMAP handler in SOSDacImpl wraps

    the new CallingConventionGCRefMapBuilder, which produces a GCRefMap blob

    byte-for-byte identical to native GCRefMapBuilder (inc/gcrefmap.h).

  - ICallingConvention.TryComputeArgGCRefMapBlob exposes the encoder via

    the existing contract; CallingConvention_1 calls into the new builder.

  - Runtime-side flushes target-state caches before the ARGITER walk so

    ValidateMethodDescPointer doesn't fail when ARGITER runs without GCREFS.

  - CONTRACT_VIOLATION(ModeViolation|GCViolation) around ComputeCallRefMap

    so Checked builds don't assert when the alloc-point hook calls into

    metadata-loading code from cooperative GC mode.

  - Switched ARGITER walk from Frame chain to StackWalkFrames so JIT-compiled

    managed methods are verified too, not just capital-F frames.

  - datadescriptor.inc registers CDAC_GLOBAL_CONTRACT(CallingConvention, c1).

Phase 4 -- pretty-print mismatches

  Replaces the two-hex-string ARG_FAIL output with a per-slot comparison

  table: { pos, location (RCX/R9/[sp+32]/...), RT token, cDAC token,  diff }.

  Reads like a triage report instead of a puzzle.

Phase 5 -- preserve constructed-type wrappers through signature decode

  The standard SignatureTypeProvider collapses ELEMENT_TYPE_BYREF/_PTR/

  _SZARRAY/_ARRAY/_GENERICINST into the inner type (or a null TypeHandle

  when GetConstructedType isn't cached), causing the encoder to silently

  drop those args. Added ParamMetadataProvider, a wrapper provider that

  tracks IsByRef and the outermost ELEMENT_TYPE_* per parameter out-of-band.

  EnumerateArguments now uses that metadata to emit the right element type

  even when the constructed type isn't in the runtime's available-type-params.

Validated on Windows x64 Checked / Comprehensive debuggee:

  ARGITER-only (0x201): 277 PASS / 6 FAIL / 12 SKIP / 0 ERROR, exit 100, 3.4s

  BOTH (0x301):         4956 GCREFS PASS / 0 fail; ArgIter 273 PASS / 6 FAIL

  FAIL progression across the three fixes: 34 -> 13 -> 8 -> 6

The 6 remaining FAILs are all the same upstream cDAC bug:

GetGenericContextLoc returns InstArgMethodTable for exact-typed instantiations

(e.g. ArraySortHelper<int>.cctor()) where the runtime emits no inst arg.

Tracked separately; the harness deliberately doesn't mask it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The encoder previously bailed (returned null -> ARG_SKIP) whenever a parameter
was a by-value value type containing GC pointers, even though those args carry
real refs the runtime emits in the GCRefMap. Walk the type's GCDesc series
(IRuntimeTypeSystem.GetGCDescSeries) and emit one REF token per embedded
pointer slot, mirroring the runtime's ReportPointersFromValueTypeArg
(siginfo.cpp). The GCDesc series Offset is relative to a boxed object's start
(includes the leading MethodTable pointer); subtract pointerSize to translate
to the unboxed in-frame layout.

This covers struct args like RuntimeTypeHandle (1 inner ref), RuntimeMethodHandle,
CancellationToken, ReadOnlySpan<T>, and similar 8-byte single-pointer structs
passed by value in a register/stack slot on x64.

Validated on Windows x64 Checked across BasicAlloc, Comprehensive, Generics,
and StructScenarios debuggees (DOTNET_CdacStress=0x201 and 0x301):
  All 8 runs exit 100, 0 FAIL, 0 ERROR.
  ArgIter SKIP count drops from ~13 to ~8 (the remainder is method-level
  generic instantiations, which need an unrelated MethodDescHandle-context fix
  in DecodeMethodSignature -- tracked as Phase 6).

> [!NOTE]
> This commit was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… context

A method's signature can reference both ELEMENT_TYPE_VAR (the owning type's
type parameters) and ELEMENT_TYPE_MVAR (the method's own type parameters).
The existing SignatureTypeProvider<T> handles only one direction depending on
T: with T = TypeHandle, MVAR throws NotSupportedException; with T =
MethodDescHandle, VAR throws NotImplementedException. The cDAC calling-
convention path was using T = TypeHandle, so every signature containing an
MVAR (e.g. Activator.CreateInstance<T>(), Array.Sort<T>(), etc.) was dropped
on the floor and bucketed as ARG_SKIP.

Introduce `MethodSigContext(MethodDescHandle Method, TypeHandle OwningType)`
that carries both, plus a `MethodAndTypeContextProvider : SignatureTypeProvider<MethodSigContext>`
that overrides GetGenericMethodParameter and GetGenericTypeParameter to
resolve through the correct field. Mark the base provider's two methods
`virtual` so the overrides take effect (the old `public new` shadowing
trick doesn't reach calls made through the IRuntimeSignatureTypeProvider
interface).

DecodeMethodSignature and DecodeParamTypeInfo now both use this context and
provider; ParamMetadataProvider is re-parameterized on MethodSigContext.

Validated on Windows x64 Checked across BasicAlloc, Comprehensive, Generics,
and StructScenarios debuggees (DOTNET_CdacStress=0x201 and 0x301):
  All 8 runs exit 100, ARG: 0 FAIL / 0 SKIP / 0 ERROR.
  ArgIter SKIP count drops from ~8 to 0; PASS up by ~14 on Comprehensive.

> [!NOTE]
> This commit was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Removes the early-return for RuntimeInfoArchitecture.X86 in
CallingConventionGCRefMapBuilder. Adds the two missing pieces:

  1. WriteStackPop prefix: x86 blobs start with the callee-popped stack-byte
     count, encoded in pointer-size units using the same two-bit / extended-int
     encoding as WriteToken. New ICallingConvention.GetCbStackPop method on the
     contract surfaces this from the shared ArgIterator's CbStackPop() (which
     already implements the x86 logic and returns 0 for VarArgs and non-x86
     platforms).

  2. Position remapping: on x86 OffsetFromGCRefMapPos is non-monotonic --
     argument registers occupy the highest offsets but the lowest GCRefMap
     positions, with stack args at higher positions. The previous "find max
     offset, derive max pos" trick only works on x64/arm64/etc. Replace with
     an explicit inverse GCRefMapPosFromOffset that knows about the x86 vs
     uniform layouts.

Validated on Windows x86 Checked / Comprehensive debuggee (first run):
  exit 100, 229 ARG_PASS / 36 ARG_FAIL / 38 ARG_SKIP / 0 ARG_ERROR.

This proves the framework + encoder run end-to-end on x86 and produce real,
diagnosable signal. The remaining FAILs/SKIPs are encoder-side x86-specific
bugs to follow up on:

  - PTR / Void* / Char** args reported as INTERIOR by the runtime but dropped
    by the cDAC (TYPE_GC_NONE vs the runtime's interior-pointer treatment for
    raw pointers passed in argument registers).
  - Int64 args occupying two GCRefMap positions on x86: the encoder positions
    them by start slot only, mis-aligning subsequent stack args by one slot.
  - Non-trivial signatures used by Internal.Runtime.CompilerServices helpers
    that exit through DecodeMethodSignature paths returning null.

Validated on Windows x64 Checked / Comprehensive (regression check):
  exit 100, 296 ARG_PASS / 0 FAIL / 0 SKIP / 0 ERROR (unchanged from Phase 6).

> [!NOTE]
> This commit was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…rdening

Two small fixes uncovered while triaging x86 results from Phase 7:

1. The ARG_FAIL pretty-printer decoded the GCRefMap bitstream as if the
   x86 stack-pop prefix didn't exist, which made the first two bits of
   that prefix look like a spurious INTERIOR/REF token at position 0.
   Run x86 stress before this commit and the table claims pos 0 ECX
   INTERIOR <-- DIFF for every failing method even when the actual
   diff was a CbStackPop mismatch carrying no tokens.

   Thread the architecture through DecodeBlob; on x86 consume the
   WriteStackPop prefix into a new StackPop field and print it as a
   separate row above the per-pos table so a CbStackPop mismatch is
   immediately visible:

       stack_pop  RT=2  cDAC=0 <-- DIFF

2. Wrap CallingConvention_1.GetCbStackPop in try/catch returning 0 on
   any failure. Mirrors the encoder's overall behavior so an exception
   inside DecodeMethodSignature or ArgIterator construction surfaces as
   [ARG_FAIL] in the next-run table rather than killing the stress run.

These don't change x86 fail counts but make the remaining x86 failures
correctly attributable. The dominant x86 issue is CbStackPop=0 in the
cDAC due to PTR / Void* args losing their CorElementType through the
signature provider; that's a deeper fix tracked separately.

> [!NOTE]
> This commit was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…so x86 ArgIterator classifies PTR / BYREF / SZARRAY args correctly

The standard SignatureTypeProvider collapses ELEMENT_TYPE_PTR / _BYREF /
_SZARRAY / _ARRAY wrappers by calling IRuntimeTypeSystem.GetConstructedType,
which returns a null TypeHandle whenever the runtime hasn't cached that
exact instantiation. CdacTypeHandle.GetCorElementType then returned 0
(ELEMENT_TYPE_END) for the resulting handle, which on x86 made
ArgIterator.IsArgumentInRegister return false and StackElemSize return 0 --
SizeOfArgStack would total 0 and CbStackPop emitted the wrong x86 prefix
(generally 0 instead of the correct callee-popped byte count).

Phase 5 already computes the outermost ELEMENT_TYPE_* per parameter via
ParamMetadataProvider/ParamTypeInfo for the encoder side. Wire that same
metadata into a new CdacTypeHandle constructor overload and have both
EnumerateArguments and GetCbStackPop pass it in. CdacTypeHandle now reports:

  GetCorElementType()        returns the override when set (not END)
  IsPointerType()            true for Ptr override
  GetSize()                  returns PointerSize for the constructed-pointer
                             wrappers (Ptr/Byref/SzArray/Array), avoiding a
                             null-TypeHandle GetBaseSize fault
  IsNull()                   false when an override is present even if the
                             underlying TypeHandle is null

Validated on Windows x86 Checked / Comprehensive (DOTNET_CdacStress=0x201):
  ARG_FAIL: 33 -> 2
  ARG_PASS: 233 -> 267 (+34)
  exit 100, 0 ERROR

The 2 remaining x86 FAILs are both Span<T> (ByRefLike struct, ContainsGCPointers
== true) where the cDAC's GetGenericInstantiation returns a null TypeHandle and
we can't introspect the inner GCDesc series. Tracked separately for a follow-up.

x64 regression matrix (BasicAlloc, Comprehensive, Generics, StructScenarios) x
(0x201, 0x301): all 8 runs exit 100 with 0 FAIL / 0 SKIP / 0 ERROR -- unchanged.

> [!NOTE]
> This commit was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…register passing

CdacTypeHandle.IsTrivialPointerSizedStruct previously threw
NotImplementedException, which on x86 caused 39 of 41 ARG_SKIP cases on
the Comprehensive debuggee. Every method taking a value-type arg (Guid,
RuntimeMethodHandleInternal, QCallTypeHandle, MetadataToken, BindingFlags,
EventTask, ...) was dropped by the cDAC encoder because ArgIterator hit
the throw while classifying register vs stack placement.

Implement the algorithm from crossgen2's TypeHandle.IsTrivialPointerSizedStruct
(src/coreclr/tools/aot/ILCompiler.ReadyToRun): the value type must be exactly
pointer-size (4 on x86), have exactly one instance field, and that field must
be a pointer-sized primitive (I, U, I4, U4, Ptr, FnPtr) or recurse into a
nested trivial pointer-sized struct. The cDAC's IRuntimeTypeSystem already
exposes GetFieldDescList, IsFieldDescStatic, and GetFieldDescType so the
linear cases are straightforward; the nested-struct recursion is left as a
conservative false return for now (GetFieldDescType doesn't directly hand
us a child TypeHandle, and the relevant runtime types collapse to the
primitive checks).

Validated on Windows x86 Checked / Comprehensive (DOTNET_CdacStress=0x201):
  ARG_SKIP: 41 -> 0
  ARG_PASS: 267 -> 297 (+30)
  ARG_FAIL: 2 -> 7 (former SKIPs now run end-to-end and surface real Span<T>
            ByRefLike encoding gaps -- tracked separately for Phase 9)
  exit 100, 0 ERROR

x64 regression check (BasicAlloc, Comprehensive): unchanged at 0 FAIL / 0 SKIP / 0 ERROR.

> [!NOTE]
> This commit was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds two small additions to the IRuntimeTypeSystem contract that the
cdacstress ArgIterator sub-check needs to mirror the runtime's
ByRefPointerOffsetsReporter (siginfo.cpp) when computing argument
GCRefMaps:

* IsByRefLike(TypeHandle): wraps the existing MethodTableFlags low-bit
  enum_flag_IsByRefLike (0x1000). Mirrors MethodTable::IsByRefLike from
  methodtable.h. Required to detect Span<T>, ReadOnlySpan<T>, and other
  ref-struct arguments where the runtime emits INTERIOR tokens per byref
  field via ByRefPointerOffsetsReporter.

* GetFieldDescApproxTypeHandle(TargetPointer): mirrors the runtime's
  FieldDesc::GetApproxFieldTypeHandleThrowing. Resolves the TypeHandle
  referenced by a field's metadata signature in the context of the
  field's enclosing class. Returns default on any failure (missing
  metadata, signature decode error, unloaded type) so callers can fall
  back conservatively. Implementation reuses the same
  Loader/EcmaMetadata/Signature pipeline that SOSDacImpl and DacDbiImpl
  already use for field-type resolution.

Used in the next commit to walk nested value-type fields for both
trivial-pointer-sized-struct recognition (x86 register passing) and
ByRefLike interior emission (recursive ref-struct GC scanning).

> [!NOTE]
> This change was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…e types

Mirrors the runtime's ByRefPointerOffsetsReporter (siginfo.cpp) and
GetApproxFieldTypeHandleThrowing-driven walks so the cdacstress ArgIter
sub-check produces byte-identical GCRefMap blobs for arguments whose
shape pulls in nested type metadata.

CdacTypeHandle.IsTrivialPointerSizedStruct on x86 now recurses through
single-field value-type wrappers via the new
IRuntimeTypeSystem.GetFieldDescApproxTypeHandle. A struct whose only
field is itself a trivial pointer-sized struct (e.g.
`struct Outer { Inner I; }` where Inner is `struct Inner { int A; }`)
is once again classified as register-passable; without this, the cDAC
over-reported the WriteStackPop prefix relative to the runtime.

CallingConventionGCRefMapBuilder gains a ByRefLike branch:

* New ArgumentLocation.IsByRefLikeStruct / OpenGenericType fields,
  populated by CallingConvention_1.EnumerateArguments. The
  ParamMetadataProvider records the open generic MethodTable during
  signature decoding so the encoder still has something to walk when
  the closed instantiation (Span<int>, ReadOnlySpan<char>, ...) hasn't
  been cached and resolves to a null TypeHandle.

* EmitByRefLikeInteriorRecursive walks instance fields:
  * ELEMENT_TYPE_BYREF fields -> one INTERIOR token at
    arg.Offset + fieldOffset.
  * Nested ELEMENT_TYPE_VALUETYPE fields -> resolve via
    rts.GetFieldDescApproxTypeHandle and recurse only if the nested
    MT is itself ByRefLike. ELEMENT_TYPE_PTR / IntPtr / void* fields
    are deliberately skipped, matching the runtime's silence on
    QCallTypeHandle / ObjectHandleOnStack and friends.
  * Bounded recursion depth (16) defends against corrupted dumps.

Net effect: Span<T> / ReadOnlySpan<T> args, ref structs containing
byref fields, and ref structs containing nested Span fields all
encode correctly on both x86 and x64; QCall/ObjectHandleOnStack
handles continue to encode as empty.

cdacstress comprehensive runs are clean across all four matrices
(x86/x64 x Comprehensive/StructScenarios).

> [!NOTE]
> This change was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two additions to the cdacstress debuggee fleet to expand ArgIterator
sub-check coverage:

1. StructScenarios gains a NestedStructScenario covering:
   * Plain nested value types (`struct { struct { int A } }`) -- exercises
     x86 IsTrivialPointerSizedStruct recursion through single-field
     value-type wrappers.
   * Nested value type with GC refs at non-zero offsets -- exercises
     GCDesc series offset arithmetic for embedded refs.
   * Three levels of nesting with a ref at the deepest level.
   * Ref struct containing a Span<byte> -- exercises
     ByRefPointerOffsetsReporter recursion through nested ByRefLike
     value-type fields.

2. New CallSignatures debuggee: a single program that exhaustively
   covers the calling-convention surface the cdacstress ArgIterator
   sub-check has to encode. Thirteen categories:
   * Argument count / register vs stack spilling
   * ByRef / in / out parameters
   * Native pointers and function pointers
   * Single- and multi-dimensional arrays
   * Structs by value (empty / tiny / int-sized / double / large / refs
     at start, middle, end / two refs / alternating refs / ref + array)
   * Nested structs (plain, with refs, three levels deep, mixed)
   * Value-type 'this' (instance methods on structs; interface dispatch
     on a generic struct)
   * ByRefLike (Span / ROSpan / two-Span mix / ptr-only ref struct /
     two-byref ref struct / nested ref struct containing Span)
   * Generic methods (ref T, value T, multi-arg, constrained, generic
     value-type instance methods)
   * Enums (Int32-, Int64-, byte-backed; enum inside a struct)
   * Returns (ref, large struct -> HasRetBuffArg, Span)
   * __arglist vararg method (expected to SKIP today -- VASigCookie
     token is the next encoder gap)
   * Mutually-recursive deep stack with mixed signatures

Each test method begins with AllocBurst(), a NoInlining helper that
issues 32 allocations so the cdacstress allocation trigger reliably
fires while the caller's frame is on the stack. Without that pattern
the per-MD dedup misses methods that complete between triggers.

> [!NOTE]
> This change was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…lementType

CdacTypeHandle.GetCorElementType now mirrors the runtime's
MetaSig::PeekArgNormalized by resolving value-type arguments to their
MethodTable::GetInternalCorElementType. For enums this collapses to the
underlying primitive (e.g. byte enum -> ELEMENT_TYPE_U1, int enum ->
ELEMENT_TYPE_I4) which is what the shared ArgIterator's x86
IsArgumentInRegister relies on to recognise sub-pointer-size enums as
register-passable.

Previously, for `void M(SomeByteEnum)` on x86, the cdacstress
ArgIterator sub-check produced WriteStackPop=1 (cDAC saw VALUETYPE,
fell into IsTrivialPointerSizedStruct, rejected the byte because
GetSize != PointerSize, and accounted for the arg as a 4-byte stack
slot) while the runtime correctly passed it in ECX with stack_pop=0.

The change is safe for regular user value types: their
InternalCorElementType is still ELEMENT_TYPE_VALUETYPE, so the encoder
continues to walk GCDesc / ByRefPointerOffsetsReporter as before.

> [!NOTE]
> This change was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds the last missing GCRefMapToken (VASigCookie, value 5) to the cDAC
ArgIterator encoder so vararg (__arglist) methods produce byte-identical
GCRefMap blobs to the runtime's ComputeCallRefMap.

Mirrors the runtime's FakeGcScanRoots short-circuit (frames.cpp):
when ArgIterator::IsVarArg is true, the runtime emits
GCREFMAP_VASIG_COOKIE at argit.GetVASigCookieOffset() and stops --
the variadic tail is reported through the cookie's signature at GC
scan time, not via per-fixed-arg tokens.

Changes:
* New ArgumentLocation.IsVASigCookie flag.
* CallingConvention_1.EnumerateArguments stops throwing for VarArgs
  signatures; instead passes isVarArg through to the shared
  ArgIterator, yields a VASigCookie ArgumentLocation at
  argit.GetVASigCookieOffset(), and breaks before iterating fixed
  args. HasThis / HasParamType / HasAsyncContinuation continue to be
  emitted first, matching the runtime's ordering.
* CallingConventionGCRefMapBuilder maps IsVASigCookie to
  GCRefMapToken.VASigCookie alongside the existing IsThis /
  IsParamType / IsValueTypeThis handlers.

cdacstress runs across Comprehensive / StructScenarios /
CallSignatures debuggees: x86 and x64 both now report
0 fail / 0 skip / 0 error.

> [!NOTE]
> This change was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Splits the cdacstress xunit harness so it runs the GCREFS and ARGITER
sub-checks as independent xunit theories rather than always combining
them into one run. The two share the same Helix work item / debuggee
build, but each maps to its own DOTNET_CdacStress value
(0x101 = ALLOC+GCREFS, 0x201 = ALLOC+ARGITER) and its own assertion
helper so a regression in one does not mask a regression in the other.

Native side (src/coreclr/vm/cdacstress.cpp):
* Emit `[GC_STATS] verifications=N pass=N fail=N known_issue=N` in the
  shutdown summary iff GCREFS ran, mirroring the existing
  `[ARG_STATS]` line that's emitted iff ARGITER ran. Presence of each
  marker is the authoritative signal that its sub-check ran -- the
  human-readable counters above it are always printed, always
  zero-initialized, and cannot be told apart from "ran with zero
  results".

Test harness (src/native/managed/cdac/tests/StressTests/):
* CdacStressResults parses both `[GC_STATS]` and `[ARG_STATS]` and
  exposes `AnyGcRefsRecorded` / `AnyArgIterRecorded` so the assertion
  helpers can distinguish "sub-check did not run" from "ran but
  recorded zero verifications". `[ARG_FAIL]` / `[ARG_ERROR]` lines
  are captured verbatim for inclusion in failure messages.
* CdacStressTestBase factored into a single RunStressAsync that takes
  a StressMode enum (GcRefs / ArgIter), with thin RunGCStressAsync /
  RunArgIterStressAsync shims. Adds AssertAllArgIterPassed parallel
  to AssertAllPassed. Adds GetTargetArchitecture() that derives the
  arch from the CORE_ROOT path's `<os>.<arch>.<config>` segment so a
  cross-arch local run (x64 dotnet.exe driving an x86 testhost) sees
  the target arch, not the host arch.
* BasicCdacStressTests gains two new xunit theories:
  * ArgIterStress_AllVerificationsPass over the unified Debuggees
    list, plus a separate WindowsOnly variant for PInvoke.
  * ArgIterStress_ArgIterOnly_AllVerificationsPass over a new
    ArgIterOnlyDebuggees list (currently just CallSignatures, which
    intentionally includes __arglist methods that hit a real cDAC
    GCREFS gap -- GetStackReferences doesn't walk the VASigCookie's
    signature blob).
  * GCStress_* theories become ConditionalTheory and SkipTestException
    on Architecture.X86 (the GCREFS sub-check has not been validated
    on x86 yet; ARGITER runs there).
* CallSignatures debuggee absorbs the vararg category back (a separate
  ArglistVararg debuggee was prototyped but a separate test binary
  isn't justified for one test method; the ArgIterOnlyDebuggees list
  is the simpler shape).

README updated with the new sub-check semantics, the marker contract,
and the new common flag combinations.

> [!NOTE]
> This change was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The shared CdacBuild job in runtime-diagnostics.yml already runs on
windows_x86 (it's in cdacDumpPlatforms), so adding windows_x86 to
cdacStressPlatforms only requires the platform entry -- the upstream
testhost + cDAC binaries are already published.

On x86 the GCREFS sub-check is skipped at the xunit layer (cDAC GC
root enumeration is not yet validated there) and only the ARGITER
sub-check actually runs. ARGITER passes cleanly across the entire
debuggee fleet on x86 today.

> [!NOTE]
> This change was authored with assistance from GitHub Copilot.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the cDAC calling convention surface (via a new ICallingConvention contract and supporting runtime-type-system helpers) and adds an ARGITER cdacstress sub-check that compares cDAC-produced argument GCRefMap blobs against the runtime’s ComputeCallRefMap, with Helix wiring to catch regressions (including x86 coverage).

Changes:

  • Add new cDAC contract/API surface (ICallingConvention, ArgumentLocation) plus RTS helpers to enable ArgIterator-driven argument enumeration and GCRefMap encoding.
  • Implement and wire an ARGITER verification path end-to-end: CoreCLR stress harness + cDAC private request plumbing + xUnit stress harness + new debuggee coverage.
  • Refactor/shared-in ArgIterator/TransitionBlock calling convention code to be reusable by both crossgen2 and the cDAC implementation; update CI matrix to include windows_x86.

Reviewed changes

Copilot reviewed 41 out of 41 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
eng/pipelines/runtime-diagnostics.yml Adds windows_x86 to the cdac stress platform matrix.
src/coreclr/inc/clrconfigvalues.h Updates DOTNET_CdacStress config description to the new WHERE/WHAT/MODIFIER layout.
src/coreclr/inc/dacprivate.h Adds a new private DAC request opcode and wire structs for arg GCRefMap blob exchange.
src/coreclr/vm/cdacstress.cpp Adds ARGITER sub-check, counters, log markers, and verification implementation using ComputeCallRefMap vs cDAC.
src/coreclr/vm/datadescriptor/datadescriptor.inc Registers the new CallingConvention cDAC global contract.
src/coreclr/tools/Common/CallingConvention/ArgIterator.cs Refactors ArgIterator into a shared Internal.CallingConvention form using ITypeHandle.
src/coreclr/tools/Common/CallingConvention/FpStructInRegistersInfo.cs Adds shared RISC-V/LoongArch64 FP-struct metadata types for calling convention classification.
src/coreclr/tools/Common/CallingConvention/ITypeHandle.cs Introduces ITypeHandle abstraction for ArgIterator/TransitionBlock type queries.
src/coreclr/tools/Common/CallingConvention/SystemVAmd64PassingDescriptor.cs Adds shared SystemV AMD64 struct classification descriptor types for standalone use.
src/coreclr/tools/Common/CallingConvention/TransitionBlock.cs Refactors TransitionBlock selection logic to be parameter-based and shared.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/GCRefMapBuilder.cs Updates crossgen2 GCRefMapBuilder to use the shared calling convention types.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/TypeHandle.cs Adds crossgen2 TypeHandle : ITypeHandle implementation for the shared calling convention code.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmImportThunk.cs Updates to use shared ArgIterator namespace/type imports.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmInterpreterToR2RThunkNode.cs Updates to use shared ArgIterator namespace/type imports.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/Compiler/DependencyAnalysis/ReadyToRun/WasmR2RToInterpreterThunkNode.cs Updates to use shared ArgIterator namespace/type imports.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/ILCompiler.ReadyToRun.csproj Links in the shared calling convention sources; removes old per-project copies.
src/coreclr/tools/aot/ILCompiler.ReadyToRun/JitInterface/WasmLowering.ReadyToRun.cs Adjusts ArgIterator type-handle plumbing for WASM lowering with the new abstractions.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/ContractRegistry.cs Exposes CallingConvention contract from ContractRegistry.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/ICallingConvention.cs Adds new public ICallingConvention + ArgumentLocation API surface for argument enumeration / GCRefMap building.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Abstractions/Contracts/IRuntimeTypeSystem.cs Adds new RTS APIs (IsByRefLike, IsUnboxingStub, GetFieldDescApproxTypeHandle) needed by the encoder.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/CoreCLRContracts.cs Registers the CallingConvention contract implementation.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Microsoft.Diagnostics.DataContractReader.Contracts.csproj Links shared calling convention sources into the cDAC contracts project.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConvention_1.cs Implements ICallingConvention using shared ArgIterator + metadata decoding and wrapper tracking.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CallingConventionGCRefMapBuilder.cs Implements blob encoder intended to match runtime ComputeCallRefMap output byte-for-byte.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/CallingConvention/CdacTypeHandle.cs Implements ITypeHandle backed by cDAC RTS/metadata for calling convention computation.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/RuntimeTypeSystem_1.cs Implements the new RTS helper APIs in the cDAC contract implementation.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Signature/SignatureTypeProvider.cs Makes generic parameter resolution methods virtual to allow context-aware overrides.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/GC/GcScanner.cs Uses ICallingConvention.EnumerateArguments to promote caller stack refs (value-type + byref handling).
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/StackWalk_1.cs Treats NotImplementedException as an expected per-frame “unsupported” condition and skips that frame.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/RuntimeTypeSystemHelpers/MethodTableFlags_1.cs Adds IsByRefLike flag interpretation and exposes it via helper property.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Legacy/SOSDacImpl.IXCLRDataProcess.cs Adds a new private request handler to compute and return argument GCRefMap blobs from the cDAC.
src/native/managed/cdac/tests/DumpTests/CallingConventionDumpTests.cs Adds dump-based integration validation comparing EnumerateArguments against R2R import GCRefMap blobs.
src/native/managed/cdac/tests/StressTests/BasicCdacStressTests.cs Adds parallel xUnit stress theories for GCREFS and ARGITER modes plus ARGITER-only debuggee list.
src/native/managed/cdac/tests/StressTests/CdacStressResults.cs Parses new [GC_STATS]/[ARG_STATS] markers and exposes per-sub-check presence/counters.
src/native/managed/cdac/tests/StressTests/CdacStressTestBase.cs Adds mode selection (GcRefs vs ArgIter) and sets corresponding DOTNET_CdacStress flags/log naming.
src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/CallSignatures.csproj Adds new ARGITER-focused debuggee project with StyleCop suppressions suitable for scaffolding types.
src/native/managed/cdac/tests/StressTests/Debuggees/CallSignatures/Program.cs Adds exhaustive signature-shape ARGITER debuggee (including varargs coverage).
src/native/managed/cdac/tests/StressTests/Debuggees/StructScenarios/Program.cs Extends StructScenarios debuggee with nested-struct argument scenarios.
src/native/managed/cdac/tests/StressTests/known-issues.md Updates docs to new default flags and verbose mode value.
src/native/managed/cdac/tests/StressTests/README.md Documents the new byte-region flag layout and per-sub-check summary markers.
src/native/managed/cdac/tests/StressTests/RunStressTests.ps1 Updates parameter docs/defaults/examples for the new DOTNET_CdacStress layout and defaults.

Comment thread src/native/managed/cdac/tests/DumpTests/CallingConventionDumpTests.cs Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 48 out of 48 changed files in this pull request and generated 5 comments.

Comment thread src/native/managed/cdac/tests/StressTests/CdacStressTests.cs
Comment thread src/native/managed/cdac/tests/StressTests/CdacStressTests.cs
Max Charlamb and others added 2 commits June 24, 2026 22:42
* CdacStressTests.Debuggees:
  - Add CrossModule (was auto-discovered into the Helix payload via the
    StressTests.targets glob but never exercised by xunit).
  - Extend the Debuggee record with a SkipGCRefs flag and set it on VarArgs.
    Pre-fix, GCRefStress_AllVerificationsPass(VarArgs) was running on
    Windows and failing with ~40 false GCREFS mismatches per run because
    the cDAC's GetStackReferences does not yet walk the VASigCookie
    signature blob to enumerate variadic-tail GC refs. ARGITER on VarArgs
    still runs (the encoder handles VASigCookie correctly).

* Debuggees/VarArgs/Program.cs: refresh the docstring to describe the
  SkipGCRefs flag mechanism (was stale, referring to a separate
  WindowsOnlyDebuggees list that was consolidated in df2b289).

* StressTests/README.md: bring the debuggee catalog up to date
  (CallSignatures, CrossModule, VarArgs) and fix a stale reference to
  BasicStressTests (renamed to CdacStressTests).

Verified locally on windows.x64.Checked:
  CrossModule + 0x301: GCREFS 5094/0/8(known), ARGITER 274/0/0/0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The ArgIterator constructor previously made the three TypeSystemContext-replacement
params (isWindows, objectTypeHandle, intPtrTypeHandle) optional with default values
(false / null / null). The cDAC was relying on the defaults because its current
call sites set extraObjectFirstArg=false and extraFunctionPointerArg=false (the
only conditions under which the iterator dereferences the type handles).

That worked today but left a latent NullReferenceException waiting for any future
cDAC code path that flips either of those bools to true. Drop the defaults; both
projects now pass the three params explicitly.

* ArgIterator.cs: remove '= null' / '= false' defaults from the constructor.
  Also apply leftover SA1129 'new T()' -> 'default' substitutions for
  ArgLocDesc and FpStructInRegistersInfo that should have landed with the
  round-3 pragma removal but didn't reach disk.
* CallingConvention_1.cs: two new private helpers,
  GetObjectTypeHandle(rts) and GetIntPtrTypeHandle(rts), supply the handles
  via IRuntimeTypeSystem.GetWellKnownMethodTable(Object) and
  GetPrimitiveType(ELEMENT_TYPE_I). Both call sites pass them.
* GCRefMapBuilder.cs (crossgen2): no change -- it was already passing all three.

Verified on windows.x64.Checked with rebuilt cDAC NAOT binary:
  BasicAlloc:    ArgIter 265/0/0/0
  CallSignatures: ArgIter 345/0/0/0
  CrossModule:   ArgIter 277/0/0/0
  VarArgs:       ArgIter 272/0/0/0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

Comment thread src/coreclr/tools/Common/CallingConvention/ArgIterator.cs Outdated
max-charlamb pushed a commit to max-charlamb/runtime that referenced this pull request Jun 25, 2026
Per @davidwrighton's review comment
(dotnet#129769 (comment)),
ArgIterator was using ITypeHandle interface references everywhere, which
forced the underlying TypeHandle / CdacTypeHandle structs to be boxed on
every field assignment, array store, out-parameter return, etc.

Make ArgIterator and ArgIteratorData generic over the type handle:

  internal struct ArgIterator<TTypeHandle> where TTypeHandle : ITypeHandle
  internal class  ArgIteratorData<TTypeHandle> where TTypeHandle : ITypeHandle

All ITypeHandle references inside these two types become TTypeHandle. The
fields, parameter array, return type, out parameters, and the
_objectTypeHandle / _intPtrTypeHandle slots are now typed directly to the
concrete struct -- no boxing.

ITypeHandle.GetElemSize is also made generic so the two callers inside
ArgIterator pass TTypeHandle without boxing.

* ArgIterator.cs: ArgIterator / ArgIteratorData are now generic. null
  fallbacks become 'default' (TTypeHandle has no struct constraint, but
  IsNull() handles both reference- and value-typed defaults).
* ITypeHandle.cs: GetElemSize<T>(...) where T : ITypeHandle.
* GCRefMapBuilder.cs (crossgen2): parameterTypes typed TypeHandle[]
  instead of ITypeHandle[]; ArgIterator/ArgIteratorData type references
  carry <TypeHandle>. Tuple return type, FakeGcScanRoots signature, and
  GetCallRefMap deconstruction updated.
* Wasm{Lowering,ImportThunk,InterpreterToR2RThunkNode,R2RToInterpreterThunkNode}.cs:
  ArgIterator alias updated to ArgIterator<TypeHandle>. WasmLowering's
  ((TypeHandle)typeHandle).GetRuntimeTypeHandle() downcast becomes a
  direct typeHandle.GetRuntimeTypeHandle() -- nice side-benefit.
* CallingConvention_1.cs (cDAC): ArgIterator/ArgIteratorData aliases
  bound to <CdacTypeHandle>; parameterTypes/returnType locals typed
  CdacTypeHandle/CdacTypeHandle[].

ArgDestination.ReportPointersFromStructInRegisters intentionally keeps
its ITypeHandle parameter -- it is called from GcScanValueType in
GCRefMapBuilder with a freshly-constructed TypeHandle (one box per
struct-in-registers arg, infrequent), and making ArgDestination generic
too would push generics further out into GCRefMapBuilder for marginal
benefit.

Verified on windows.x64.Checked with rebuilt cDAC NAOT binary:
  BasicAlloc:     ArgIter 267/0/0/0
  CallSignatures: ArgIter 347/0/0/0
  CrossModule:    ArgIter 272/0/0/0
  VarArgs:        ArgIter 271/0/0/0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 25, 2026 18:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 48 out of 48 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

src/native/managed/cdac/tests/StressTests/CdacStressResults.cs:108

  • The exception message still says “GC stress results log not found”, but this parser is now used for both GCREFS and ARGITER runs. Updating the text makes failures clearer.

Comment thread src/coreclr/vm/cdacstress.cpp
Comment thread src/coreclr/inc/clrconfigvalues.h
Comment thread src/coreclr/inc/dacprivate.h Outdated
Per @davidwrighton's review comment
(dotnet#129769 (comment)),
ArgIterator was using ITypeHandle interface references everywhere, which
forced the underlying TypeHandle / CdacTypeHandle structs to be boxed on
every field assignment, array store, out-parameter return, etc.

Make ArgIterator and ArgIteratorData generic over the type handle:

  internal struct ArgIterator<TTypeHandle> where TTypeHandle : ITypeHandle
  internal class  ArgIteratorData<TTypeHandle> where TTypeHandle : ITypeHandle

All ITypeHandle references inside these two types become TTypeHandle. The
fields, parameter array, return type, out parameters, and the
_objectTypeHandle / _intPtrTypeHandle slots are now typed directly to the
concrete struct -- no boxing.

ITypeHandle.GetElemSize is also made generic so the two callers inside
ArgIterator pass TTypeHandle without boxing.

Consumers spell the constructed type explicitly -- ArgIterator<TypeHandle>
for crossgen2, ArgIterator<CdacTypeHandle> for the cDAC. The previous
'using ArgIterator = ...ArgIterator' aliases existed to disambiguate from
System.ArgIterator (non-generic); with the rename to ArgIterator<T> the
arity already disambiguates, so the aliases are removed in favor of the
plain namespace import. Same for ArgIteratorData.

* ArgIterator.cs: ArgIterator / ArgIteratorData are now generic. null
  fallbacks become 'default' (TTypeHandle has no struct constraint, but
  IsNull() handles both reference- and value-typed defaults).
* ITypeHandle.cs: GetElemSize<T>(...) where T : ITypeHandle.
* GCRefMapBuilder.cs (crossgen2): parameterTypes typed TypeHandle[]
  instead of ITypeHandle[]; type references spelled ArgIterator<TypeHandle>
  / ArgIteratorData<TypeHandle> throughout (no alias).
* Wasm{Lowering,ImportThunk,InterpreterToR2RThunkNode,R2RToInterpreterThunkNode}.cs:
  alias removed; references spelled ArgIterator<TypeHandle>. WasmLowering's
  ((TypeHandle)typeHandle).GetRuntimeTypeHandle() downcast becomes a
  direct typeHandle.GetRuntimeTypeHandle() -- nice side-benefit.
* CallingConvention_1.cs (cDAC): aliases removed; references spelled
  ArgIterator<CdacTypeHandle> / ArgIteratorData<CdacTypeHandle>;
  parameterTypes / returnType locals typed CdacTypeHandle[] / CdacTypeHandle.

ArgDestination.ReportPointersFromStructInRegisters intentionally keeps
its ITypeHandle parameter -- it is called from GcScanValueType in
GCRefMapBuilder with a freshly-constructed TypeHandle (one box per
struct-in-registers arg, infrequent), and making ArgDestination generic
too would push generics further out into GCRefMapBuilder for marginal
benefit.

Verified on windows.x64.Checked with rebuilt cDAC NAOT binary:
  BasicAlloc:     ArgIter 267/0/0/0
  CallSignatures: ArgIter 347/0/0/0
  CrossModule:    ArgIter 272/0/0/0
  VarArgs:        ArgIter 271/0/0/0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@max-charlamb max-charlamb force-pushed the cdac-shared-argiterator branch from cd573b3 to be6aaf3 Compare June 25, 2026 19:10
GetArgumentLayout (formerly EnumerateArguments) now returns a single
ArgumentLayout record bundling the per-argument locations AND the x86
callee-pop stack-byte count. Both come from one ArgIterator walk
instead of two -- the previous design constructed ArgIterator twice per
method (once eagerly in GetCbStackPop, once lazily via the IEnumerable
yield in EnumerateArguments).

* Add private readonly record struct ArgumentLayout(IReadOnlyList<ArgumentLocation>, uint CbStackPop).
* Rewrite EnumerateArguments -> GetArgumentLayout: eager, returns the record.
* Delete GetCbStackPop / GetCbStackPopCore (the entire 73-line pair).
* Update ComputeArgGCRefMapBlobCore to consume layout.Arguments and layout.CbStackPop.
* Both helpers now private (were internal as a legacy of the old
  CallingConventionGCRefMapBuilder separation, which was folded in earlier).

VarArgs continues to short-circuit: emit VASigCookie, return CbStackPop=0
without calling argit.CbStackPop() (which the prior code never did either,
just structured as a separate guard in GetCbStackPopCore).

Net -54 LOC in CallingConvention_1.cs.

Verified on windows.x64.Checked with rebuilt cDAC NAOT binary:
  BasicAlloc:     ArgIter 264/0/0/0
  CallSignatures: ArgIter 351/0/0/0
  CrossModule:    ArgIter 276/0/0/0
  VarArgs:        ArgIter 276/0/0/0

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings June 25, 2026 19:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 48 out of 48 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/inc/dacprivate.h Outdated
* Debuggee csprojs (CallSignatures, CrossModule, VarArgs): drop the
  redundant <LangVersion>latest</LangVersion> entries -- the shared
  Debuggees/Directory.Build.props doesn't set LangVersion and these
  three didn't need a specific language feature.
* CdacTypeHandle.cs: remove unused '_os' field + assignment (no read
  site). Fix the misleading 'CdacCorElementType.End' reference in the
  _kindOverride doc -- the cDAC CorElementType enum starts at Void = 1
  so there is no End member; document as 'default (the enum's 0 value)'.
* dacprivate.h: ERROR_INSUFFICIENT_BUFFER comment said
  'cbFilled = required size', but the managed handler and native caller
  use cbNeeded for that. Update to match the implemented ABI.
* CdacStressApi.cs: prefer sizeof(DacStressArgGCRefMapRequest) over
  Unsafe.SizeOf<DacStressArgGCRefMapRequest>() -- the file is already
  unsafe and the struct is blittable, matching repo guidance.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@max-charlamb max-charlamb marked this pull request as ready for review June 25, 2026 20:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 48 out of 48 changed files in this pull request and generated 3 comments.

Comment on lines +8 to +19
public interface ICallingConvention : IContract
{
static string IContract.Name => nameof(CallingConvention);

bool TryComputeArgGCRefMapBlob(MethodDescHandle methodDesc, out byte[] blob)
=> throw new NotImplementedException();
}

public readonly struct CallingConvention : ICallingConvention
{
// Everything throws NotImplementedException
}
Comment on lines +458 to +462
// Helpers: Wrap stamps Underlying but leaves OutermostKind == End so
// callers know to fall back to GetSignatureCorElementType on Underlying.
// The constructed-type overrides (ByRef/Ptr/SzArray/Array) override
// OutermostKind explicitly.
private static TrackedType Wrap(TypeHandle th)
Comment on lines +80 to +87
// Scope of this PR: ARGITER is validated on Windows x86 / x64
// only. Other architectures hit known gaps that need follow-up
// work (SystemV-AMD64 / ARM64 struct-in-register classification,
// arm32 ABI port). Skip there until those land.
if (os != OSPlatform.Windows || arch is not (Architecture.X86 or Architecture.X64))
throw new SkipTestException(
"ARGITER stress is validated for windows-x86 / windows-x64 in this PR; " +
"other targets need follow-up work (SystemV / ARM64 struct-in-registers, ARM32 ABI port).");
public CdacTypeHandle(TypeHandle typeHandle, Target target, CdacCorElementType kindOverride)
{
_typeHandle = typeHandle;
_target = target;

@jkotas jkotas Jun 25, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is odd that _arch is not part of Target. Should it be?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants