Skip to content

[cDAC] WebAssembly support: stack walking and managed metadata resolution#130988

Draft
lewing wants to merge 19 commits into
dotnet:mainfrom
lewing:lewing-cdac-wasm-support
Draft

[cDAC] WebAssembly support: stack walking and managed metadata resolution#130988
lewing wants to merge 19 commits into
dotnet:mainfrom
lewing:lewing-cdac-wasm-support

Conversation

@lewing

@lewing lewing commented Jul 17, 2026

Copy link
Copy Markdown
Member

Brings the managed data contract reader (cDAC) up on CoreCLR/WebAssembly, so diagnostic tools can inspect live managed state of a browser/WASI corerun.wasm target the same way they do on other platforms. This covers two areas: the WASM stack walk, and the module/metadata resolution path that type-name lookup depends on.

Background

WASM CoreCLR differs from other targets in ways the cDAC contracts did not yet account for:

  • No native register context — the WASM T_CONTEXT has no real register file; execution is a mix of ReadyToRun frames unwound over the linear stack ($sp) with synthetic virtual IPs, and interpreter frames walked via the explicit InterpreterFrame/InterpMethodContextFrame chain.
  • Feature-gated fields are absent from the emitted data descriptor (e.g. code versioning is off), which several contracts read unconditionally.
  • Webcil images — corelib ships as a webcil-wrapped ReadyToRun image that is loaded flat (never mapped), and whose header is a stripped/rewrapped PE that System.Reflection.Metadata.PEReader cannot parse.

Stack walking

Builds on the WASM WasmContext / WasmUnwinder / WasmR2RInfo foundation and wires it into the shared StackWalk_1 driver — no WASM-specific contract surface is added (an earlier context-free IStackWalk.GetInterpretedFrames experiment is reverted); everything flows through the existing driver and the IsInterpreterCode → InterpreterVirtualUnwind path.

  • WasmContext now mirrors the native T_CONTEXT (src/coreclr/pal/inc/pal.h, HOST_WASM): ContextFlags, InterpreterWalkFramePointer, InterpreterSP, InterpreterFP, InterpreterIP — five 32-bit fields, 20 bytes. The context is serialized to/from target memory and handed to SOS, so the managed layout must match byte-for-byte (it previously held only three IP/SP/FP slots).
  • WasmFrameHandler seeds the initial stack-walk context from the Frame chain (WASM has no captured DT_CONTEXT): the innermost InlinedCallFrame's CallSiteSP provides $sp. On the P/Invoke-into-interpreter transition it stashes the owning InterpreterFrame address in InterpreterWalkFramePointer, matching native SetFirstArgReg (src/coreclr/vm/wasm/cgencpu.h); GetFirstArgRegisterName returns that slot for WASM.
  • The R2R virtual-IP → MethodDesc resolution reuses the generic RangeSectionMap path (a virtual IP is just an address in a registered range); no WASM-specific IP→MethodDesc path is required.

Managed metadata resolution (contract fixes)

Three feature/format gaps blocked managed-object → type-name resolution on WASM. Each is fixed following existing patterns:

  1. Module.MethodDefToILCodeVersioningStateMap optional — omitted from the descriptor when FEATURE_CODE_VERSIONING is off (WASM). Made nullable (matching the EnCClassList pattern) so Loader.GetLookupTables reads it as an empty table instead of throwing "Field not found in any layout".
  2. Expose PEImage.FlatImageLayoutcdac_data<PEImage> only exposed the loaded layout (m_pLayouts[IMAGE_LOADED]), which is null for images that are never mapped. Add FlatImageLayout (m_pLayouts[IMAGE_FLAT]) and fall back to it in Loader.TryGetLoadedImageContents (and, factored into a shared helper, in GetRvaData/GetILAddr), so webcil-on-WASM metadata is reachable instead of "Module is not loaded".
  3. Webcil-aware metadata readEcmaMetadata.GetReadOnlyMetadataAddress fed the flat webcil bytes to PEReader (→ "BadImageFormatException: Unknown file format"). It now detects the webcil magic (WbIL) and locates the ECMA-335 metadata via the webcil header's PeCliHeaderRva → CLI (COR20) header → metadata directory, resolving RVAs with the loader's webcil-aware GetILAddr. Non-webcil images keep the PEReader path.

The only native change is the PEImage.FlatImageLayout descriptor field (peimage.h + datadescriptor.inc); the rest is managed.

Testing

  • New unit tests (MockTarget): WASM context seeding from the Frame chain, the native WasmContext layout + InterpreterWalkFramePointer register round-trip, the InlinedCallFrame-over-InterpreterFrame stash, the interpreter virtual unwind chain-step/exhaustion, virtual-IP → R2R MethodDesc resolution, the Module code-versioning-absent path, the PEImage flat-layout fallback, and GetILAddr resolving through a flat webcil layout. Full cDAC suite green.
  • docs/design/datacontracts/{Loader,EcmaMetadata}.md updated to match the contract changes.
  • clr.native builds clean with the descriptor change.

End-to-end validation

Validated against a live browser-wasm CoreCLR target over CDP (rebuilt corerun.wasm for the FlatImageLayout descriptor field; managed reader hot-swapped for the rest). With all three fixes:

  • managed_object(String.Empty)full_type_name: "System.String", module: "System.Private.CoreLib", type_def_token: 0x0200007E, type_name_complete: true — the ECMA metadata is read from the webcil ReadyToRun image via the WbIL-header path and the TypeDef resolves.
  • managed_threads enumerates the managed thread (the trap thread's LastThrownObject type resolves through the same metadata path).

The full read → walk → enumerate → type-name path is green on live WASM managed state.

Out of scope / follow-ups

Deferred (each with its own required trap/repro shape or larger effort): live InlinedCallFrame CallSiteSP read on the P/Invoke seam, exception-handling/funclet unwind, GC stack-ref reporting during the walk, localloc indirect frames, wasm64, and DacDbi/debugger paths (disabled on WASM).

Note

This pull request was authored with the assistance of GitHub Copilot.

lewing and others added 15 commits July 16, 2026 20:15
WebAssembly has no native register context: DT_CONTEXT is an empty
struct and REGDISPLAY is zeroed. Managed execution on wasm is fully
interpreted, so a managed stack walk is a pointer-chase over the
interpreter frame chain rather than a register-based unwind.

Add a minimal WasmContext (IPlatformContext) so
IPlatformAgnosticContext.GetContextForPlatform resolves for wasm
targets instead of throwing. The IP/SP/FP slots are synthetic (populated
by the interpreter frame-chain walker, not read from a native context
blob, hence Size == 0), and register-unwind operations are intentionally
unsupported.

This unblocks the stack-walk entry point for wasm. The context-free
contracts (RuntimeInfo, Object, RuntimeTypeSystem, Loader, Thread, GC)
do not depend on this and are reachable via memory reads alone.

Contributes to dotnet#120646.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 272b3faa-2ba5-4287-bc0a-753b59d2d3d2
The stub ContractDescriptor computed its flags with a bitwise-AND where
an OR was intended:

    .flags = 0x1u & (sizeof(void*) == 4 ? 0x02u : 0x00u)

This evaluates to 0x00 in every case (0x1 & 0x02 == 0 on 32-bit,
0x1 & 0x00 == 0 on 64-bit), so the stub never set the mandatory low bit
(bit 0, always 1 per the contract-descriptor spec) and always masked
away the pointer-size bit (bit 1).

Use OR to match the real descriptor's computation in datadescriptor.cpp:

    .flags = 0x1u | (sizeof(void*) == 4 ? 0x02u : 0x00u)

The stub is only linked when CDAC_BUILD_TOOL_BINARY_PATH is unset (builds
without a .NET SDK) and carries an empty descriptor, so impact is limited,
but a consumer inspecting the flags would previously see an invalid
descriptor and the wrong pointer size.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 272b3faa-2ba5-4287-bc0a-753b59d2d3d2
CoreCLR on WebAssembly has no native register context (empty DT_CONTEXT,
zeroed REGDISPLAY), so the register-driven CreateStackWalk path cannot
run. But managed execution on wasm is fully interpreted, and the
interpreter maintains an explicit in-memory frame chain
(InterpreterFrame.TopInterpMethodContextFrame -> InterpMethodContextFrame
.pParent), so a managed stack walk is a pointer-chase rather than an
unwind.

Add IStackWalk.GetInterpretedFrames(threadPointer): it walks the Thread's
explicit Frame chain and, for each InterpreterFrame, expands the
InterpMethodContextFrame chain (reusing the existing
WalkInterpreterFrameChain head-resolution), resolving each frame's
MethodDesc via StartIp -> InterpByteCodeStart.Method ->
InterpMethod.MethodDesc. It is context-free (pure memory reads) and makes
no GetContextForPlatform / Unwind calls, so it works on interpreter-only
targets such as wasm.

The existing GetFrames (explicit Frame-chain enumeration, consumed by
DacDbi internal-frame reporting) is intentionally left unchanged.

Also adds the InterpretedFrameData record, a FrameHelpers helper for the
StartIp -> MethodDesc resolution, mock infrastructure for the interpreter
frame chain, and unit tests. Fixes an incorrect <paramref> name on
MockFrameBuilder.LinkChain.

Contributes to dotnet#120646.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 272b3faa-2ba5-4287-bc0a-753b59d2d3d2
Rubber-duck review noted WasmContext.Size returned 0 while
ContextHolder.GetBytes() serializes the full 12-byte synthetic struct,
so a consumer that allocates Size bytes and one that reads GetBytes()
would disagree. Return the actual synthetic-context size (three 32-bit
IP/SP/FP slots) so Size and GetBytes() are consistent.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 272b3faa-2ba5-4287-bc0a-753b59d2d3d2
CoreCLR on WASM will use ReadyToRun for acceptable perf, so a real stack
is always a mix of R2R and interpreter frames. R2R frames are walked over
the managed linear stack ($sp) via a frameSize-based virtual unwind with
synthetic "virtual IPs" (there is no register context and code is not in
linear memory).

Add WasmUnwinder, the cDAC mirror of the native WASM R2R stack walk in
src/coreclr/vm/wasm/helpers.cpp (and the ABI in clr-abi.md):
- TryGetFramePointer: NormalizeFrameBase, incl. localloc indirection
  (STACK_WALK_INDIRECT_TO_FRAMEPOINTER) and the TERMINATE_R2R_STACK_WALK
  boundary where the walk hands off to the Frame / interpreter chain.
- GetVirtualIP: base virtual IP + function-local virtual IP (stored /2).
- GetEstablishingFramePointerFromTerminator: recovers the establishing
  frame pointer stored beside the terminator by CallFunclet* (funclet EH).
- TryUnwindOneFrame: advance $sp by the ULEB128 fixed frame size and
  produce the caller's virtual IP (mirrors WasmUnwindStackFrameCore).

The ExecutionManager-backed lookups (virtual-IP base, funclet check,
per-function unwind data) are abstracted behind IWasmR2RInfo; they are
implemented against the cDAC ExecutionManager once the WASM R2R
virtual-IP data descriptors are emitted. Two-pass EH / funclet reporting
is then inherited from the existing architecture-agnostic EH stackwalk
(IsFunclet / FindParentStackFrame operate on CodeBlockHandle, not
registers) once virtual IPs resolve to code blocks.

Adds unit tests covering frame-base normalization (floor, localloc
indirection, terminator), establishing-FP recovery, virtual IP
resolution, and single/multi-byte ULEB128 frame-size unwinding.

Contributes to dotnet#120646.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 272b3faa-2ba5-4287-bc0a-753b59d2d3d2
…dFrames)"

This reverts commit e01be9e.

On WASM, ReadyToRun is required for acceptable perf even in developer
builds, so a real managed stack is always a mix of R2R and interpreter
frames. A standalone, public "interpreter frames only" API therefore
surfaces a partial stack that cannot position R2R frames in call order,
and risks consumers building on a half-stack.

The correct shape is a single interleaved walk (CreateStackWalk-style,
driven by $sp), with the interpreter frame chain as one arm and the R2R
virtual-IP unwind (see WasmUnwinder) as the other. The reusable interpreter
primitives (FrameHelpers.WalkInterpreterFrameChain /
ResolveTopInterpMethodContextFrame) remain available for that unified walk;
the StartIp->MethodDesc helper and the public IStackWalk.GetInterpretedFrames
surface are withdrawn until the interleaved walk lands so the contract is
not committed prematurely.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 272b3faa-2ba5-4287-bc0a-753b59d2d3d2
Adds the descriptor data and reader needed to resolve a WASM R2R function
table index to its base virtual IP, funclet status, and unwind data -
backing the IWasmR2RInfo seam consumed by WasmUnwinder.

Descriptor emission (TARGET_WASM only):
- New FunctionTableIndexRangeSection type (MinFunctionTableIndex,
  NumRuntimeFunctions, R2RModule, Next) + cdac_data in codeman.h.
- FunctionTableIndexRangeList global (ExecutionManager::
  s_pFunctionTableIndexRangeList).
- MinVirtualIP field on ReadyToRunInfo (m_minVirtualIP).

Reader:
- WasmR2RInfo implements IWasmR2RInfo, mirroring ExecutionManager::
  {FindFunctionTableIndexRangeSection, IsFuncletFunctionIndex,
  GetWasmVirtualIPFromFunctionTableIndex} in codeman.cpp - including the
  funclet backward-index-to-controlling-function walk and the
  RUNTIME_FUNCTION funclet high-bit (0x80000000) - reusing the existing
  RuntimeFunctionLookup / ReadyToRunInfo / Module infra.

Verified by a CoreCLR browser-wasm build: datadescriptor.cpp compiles
under TARGET_WASM and cdac-build-tool emits the type
(fields at offsets 0/4/8/12), the FunctionTableIndexRangeList pointer
global, and ReadyToRunInfo.MinVirtualIP into the contract descriptor -
matching the managed reader's field names and shapes.

Contributes to dotnet#120646.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 272b3faa-2ba5-4287-bc0a-753b59d2d3d2
WasmContext.Unwind previously threw. Now it advances the context by one
ReadyToRun frame over the managed linear stack using
WasmUnwinder + WasmR2RInfo: it reads StackPointer ($sp), unwinds one frame
(nextSp + caller virtual IP), and updates StackPointer/InstructionPointer.
When the R2R walk terminates (an interpreter transition or the stack top),
StackPointer is set to null so the stack walker can fall back to the
explicit Frame chain / interpreter frame chain.

This makes WasmContext a real $sp-driven context (mirroring how
X86Context.Unwind drives X86Unwinder) rather than a degenerate null-object.
The full interleaved CreateStackWalk integration (per-thread $sp seed,
virtual-IP -> method reverse mapping, interleaving with
InterpreterVirtualUnwind) is the remaining step.

Contributes to dotnet#120646.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 272b3faa-2ba5-4287-bc0a-753b59d2d3d2
WebAssembly has no native register context (DT_CONTEXT is empty and
REGDISPLAY is zeroed), so the initial managed stack walk context is seeded
from the explicit Frame chain rather than a captured thread context. The
shared StackWalk_1 driver already walks the FrameChain to the innermost
transition frame in GetContextFromFrames; wire the degenerate WasmContext
into that path so seeding works on WASM:

- Add WasmFrameHandler, routing ContextHolder<WasmContext> through the base
  frame handler. The base HandleInlinedCallFrame already reads
  InlinedCallFrame.CallSiteSP / CallerReturnAddress / CalleeSavedFP into the
  synthetic IP/SP/FP slots -- the common P/Invoke-boundary seeding path.
  Hijack frames (a debugger / GC-suspension concept) throw NotSupported.
- Dispatch ContextHolder<WasmContext> to WasmFrameHandler in
  FrameHelpers.GetFrameHandler (previously threw for WASM).
- Test the seam end-to-end through FrameHelpers.UpdateContextFromFrame, plus
  the mock-infra additions (InlinedCallFrame CallSiteSP / CalleeSavedFP) it
  needs.

Everything flows through the shared StackWalk_1 driver and the existing
IsInterpreterCode -> InterpreterVirtualUnwind path; no WASM-specific contract
surface is added.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
…on stash

The WASM WasmContext previously held only three synthetic IP/SP/FP slots,
which did not match the runtime's wasm T_CONTEXT (src/coreclr/pal/inc/pal.h,
HOST_WASM): { ContextFlags, InterpreterWalkFramePointer, InterpreterSP,
InterpreterFP, InterpreterIP } -- five 32-bit fields, 20 bytes. Because the
cDAC serializes this context to/from target memory (thread context and
*ExceptionFrame TargetContext blobs) and returns it to SOS, the managed
layout must match the native struct byte-for-byte.

- Rework WasmContext to mirror the native field order and size (Size is now
  5 * sizeof(uint)), map StackPointer/InstructionPointer/FramePointer to the
  Interpreter{SP,FP,IP} slots, and back RawContextFlags with ContextFlags.
- Expose the InterpreterWalkFramePointer slot as the synthetic first-argument
  register the interpreter virtual unwind uses: WasmContext supports it in
  Try{Set,Read}Register, GetFirstArgRegisterName returns it for WASM, and
  WasmFrameHandler.HandleInlinedCallFrame stashes the InterpreterFrame address
  there when a P/Invoke transition is directly below an InterpreterFrame --
  matching native SetFirstArgReg (src/coreclr/vm/wasm/cgencpu.h) and the
  per-architecture handlers.
- Unit-test the native layout / register round-trip and the
  InlinedCallFrame-over-InterpreterFrame stash.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
Add mock-target coverage for the interpreter side of a mixed R2R/interpreter
WASM stack walk, exercising the real InterpreterVirtualUnwind path with a
WasmContext:

- Stepping a multi-node InterpMethodContextFrame chain: each unwind follows
  pParent and takes the parent frame's IP/SP/FP (mirrors native
  VirtualUnwindInterpreterCallFrame).
- Graceful termination when the chain is exhausted and no owning
  InterpreterFrame is stashed in the synthetic first-argument register. This
  also guards the WASM first-argument-register wiring: the exhaustion path
  routes through GetFirstArgRegisterName, which returned NotSupported for WASM
  before it was mapped to InterpreterWalkFramePointer.

Re-adds the MockInterpMethodContextFrame test descriptor and lets the shared
CreateTarget register an IRuntimeInfo architecture for paths that consult it.

The full IStackWalk.CreateStackWalk driver loop is validated end to end via
dump/live targets (no architecture mock-tests it); these unit tests cover the
WASM-specific interpreter virtual unwind that feeds it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
On WASM a "code address" is a synthetic virtual IP
(ExecutionManager::GetWasmVirtualIPFromStackPointer = base + function-local
offset). R2R modules are registered in the RangeSectionMap by their virtual-IP
range, so resolving a virtual IP to its MethodDesc uses the same generic
RangeSection.Find -> ReadyToRunJitManager path as every other architecture --
there is no WASM-specific IP->MethodDesc code path. (MinVirtualIP and
FunctionTableIndexRangeSection are consumed only by the unwinder's
function-table-index -> base-virtual-IP mapping in WasmR2RInfo.)

Add a test that resolves a wasm32 virtual IP to its R2R MethodDesc and confirms
the ReadyToRun classification, mirroring GetMethodDesc_R2R_OneRuntimeFunction
with explicit virtual-IP framing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
On builds without code versioning (FEATURE_CODE_VERSIONING off, e.g. WASM), the
runtime omits Module::MethodDefToILCodeVersioningStateMap from the emitted Module
data descriptor (src/coreclr/vm/datadescriptor/datadescriptor.inc, guarded by
FEATURE_CODE_VERSIONING). The DataContractReader Loader contract read that field
unconditionally, so any managed-object -> type-name resolution on WASM threw
"Field not found in any layout" the moment it needed the module lookup tables.

Make the field optional, matching the existing EnCClassList pattern (also
feature-gated):

- Data.Module: declare MethodDefToILCodeVersioningStateMap as TargetPointer?
  so an absent field reads as null instead of throwing.
- Loader.GetLookupTables: coalesce the absent map to TargetPointer.Null, which
  GetModuleLookupMapElement already treats as an empty table (no code-versioning
  lookups, the correct behavior when the feature is off).
- Update Loader.md GetLookupTables pseudo-code to reflect the optional read.
- Test: a Module layout without the field resolves the map to null rather than
  faulting; the mock Module layout gains an includeCodeVersioning toggle.

Discovered via live browser-wasm cDAC validation: type resolution on a real
frozen String.Empty failed here before this fix.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
EcmaMetadata resolution calls Loader.TryGetLoadedImageContents to locate a
module's metadata. It only consulted PEImage.LoadedImageLayout
(m_pLayouts[IMAGE_LOADED]), which is null for images that are never mapped/
loaded -- notably webcil ReadyToRun images on WASM, whose metadata lives in the
flat layout (m_pLayouts[IMAGE_FLAT]). As a result any managed-object -> type
name resolution on WASM failed with "Module is not loaded" once it reached the
metadata step.

- peimage.h / datadescriptor.inc: expose cdac_data<PEImage>::FlatImageLayout
  (m_pLayouts[IMAGE_FLAT]) as a new PEImage.FlatImageLayout descriptor field,
  alongside the existing LoadedImageLayout.
- Data.PEImage: add FlatImageLayout (nullable; older descriptors without it read
  as null).
- Loader.TryGetLoadedImageContents: when LoadedImageLayout is null, fall back to
  the flat layout. Its flags lack FLAG_MAPPED, so the metadata reader treats it
  as a flat (non-mapped) image, which is correct for webcil.
- Update Loader.md and add a MockTarget test for the flat fallback.

Surfaced by live browser-wasm cDAC validation: type resolution on a frozen
String reached "Module is not loaded" before this fix.

NOTE: the managed changes are unit-tested; the native datadescriptor change
(peimage.h / datadescriptor.inc) requires a runtime build to validate.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
Once the flat-layout fallback lets EcmaMetadata reach a WASM webcil ReadyToRun
image, it fed the flat bytes to System.Reflection.Metadata's PEReader, which
can't parse the stripped/rewrapped webcil header -> "BadImageFormatException:
Unknown file format." Make the read-only metadata path webcil-aware:

- EcmaMetadata.GetReadOnlyMetadataAddress: when the image begins with the webcil
  magic ('WbIL'), locate the ECMA-335 metadata via the webcil header's
  PeCliHeaderRva -> CLI (COR20) header -> metadata directory, resolving RVAs with
  the loader's webcil-aware GetILAddr instead of PEReader. Non-webcil images keep
  the existing PEReader path.
- Data.WebcilHeader: expose PeCliHeaderRva (RawOffset, no descriptor change).
- Loader: GetRvaData / GetILAddr had the same loaded-layout assumption as
  TryGetLoadedImageContents and threw for webcil-on-WASM (LoadedImageLayout null).
  Factor the loaded-or-flat layout selection into a shared helper so both fall
  back to the flat layout.
- Update Loader.md / EcmaMetadata.md and add a GetILAddr flat-webcil-layout test
  (the webcil header + RVA resolution the metadata path relies on).

Purely managed (WebcilHeader uses RawOffset; depends on the PEImage.FlatImageLayout
descriptor field from the previous change). Surfaced by live browser-wasm cDAC
validation, which advanced to "Unknown file format" after the flat-layout fix.
The EcmaMetadata webcil branch itself is validated end to end on a live wasm
target; the loader flat-fallback it relies on is unit-tested here.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@lewing lewing added the arch-wasm WebAssembly architecture label Jul 17, 2026
@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.

@lewing

lewing commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

cc @dotnet/wasm-contrib

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

Enables cDAC to operate against CoreCLR/WebAssembly targets by adding WASM-aware stack-walking support (context + R2R unwinding + interpreter transition seeding) and by fixing managed metadata resolution for WASM’s webcil/flat-image layout.

Changes:

  • Add WASM stack-walk context and ReadyToRun linear-stack unwinder plumbing, including function-table-index range lookup.
  • Make loader/metadata paths resilient to WASM specifics (optional code-versioning table, flat PEImage layout fallback, webcil-aware metadata location).
  • Add/extend unit tests and update relevant contract design docs for Loader/EcmaMetadata.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/native/managed/cdac/tests/UnitTests/WasmUnwinderTests.cs New unit tests for WASM R2R unwinder behaviors (frame pointer, VIP, ULEB128 frame sizes).
src/native/managed/cdac/tests/UnitTests/StackWalkTests.cs Adds WASM-specific stack-walk tests (frame-chain seeding, interpreter virtual unwind, context layout/register behavior).
src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Loader.cs Allows mocking Module layouts with/without code versioning field.
src/native/managed/cdac/tests/UnitTests/MockDescriptors/MockDescriptors.Frame.cs Extends frame mocks for WASM seeding and adds InterpMethodContextFrame mock layout/builder support.
src/native/managed/cdac/tests/UnitTests/LoaderTests.cs Tests for missing code-versioning map and flat-layout/webcil RVA resolution + image contents fallback.
src/native/managed/cdac/tests/UnitTests/ExecutionManager/ExecutionManagerTests.cs Adds WASM “virtual IP” ReadyToRun MethodDesc resolution coverage.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/DataType.cs Adds FunctionTableIndexRangeSection data type id.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/WebcilHeader.cs Adds PeCliHeaderRva field to support webcil metadata location.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ReadyToRunInfo.cs Adds nullable MinVirtualIP field for WASM virtual-IP base computation.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/PEImage.cs Exposes nullable FlatImageLayout to support non-mapped (flat) images.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/Module.cs Makes MethodDefToILCodeVersioningStateMap nullable for FEATURE_CODE_VERSIONING-off builds.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/FunctionTableIndexRangeSection.cs New data model for WASM function-table-index range list nodes.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/WasmFrameHandler.cs New WASM frame handler for seeding/stashing interpreter walk frame pointer.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/FrameHandling/FrameHelpers.cs Wires WasmFrameHandler and maps WASM “first arg register” name.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/WasmContext.cs New WASM context struct mirroring native T_CONTEXT layout and providing unwind entrypoint.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmUnwinder.cs New R2R linear-stack unwinder (frame pointer, virtual IP, ULEB128 frame size decode).
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/Wasm/WasmR2RInfo.cs Implements function-table-index → R2R module/runtime function lookup via new descriptors.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/StackWalk/Context/IPlatformAgnosticContext.cs Adds WASM context selection based on RuntimeInfoArchitecture.Wasm.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Loader_1.cs Adds flat-layout fallback for image access and treats missing code-versioning map as empty.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/EcmaMetadata_1.cs Adds webcil-aware metadata resolution path; keeps PEReader path for normal PEs.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Constants.cs Adds FunctionTableIndexRangeList global name.
src/coreclr/vm/readytoruninfo.h Exposes MinVirtualIP in cdac_data under TARGET_WASM.
src/coreclr/vm/peimage.h Adds FlatImageLayout descriptor field (m_pLayouts[IMAGE_FLAT]).
src/coreclr/vm/datadescriptor/datadescriptor.inc Emits new PEImage/ReadyToRunInfo fields and WASM-only type/global for function table index ranges.
src/coreclr/vm/codeman.h Adds cdac_data for FunctionTableIndexRangeSection and exposes list head address under TARGET_WASM.
src/coreclr/debug/datadescriptor-shared/contractdescriptorstub.c Fixes stub ContractDescriptor flags initialization.
docs/design/datacontracts/Loader.md Documents FlatImageLayout fallback and optional code-versioning table behavior.
docs/design/datacontracts/EcmaMetadata.md Documents webcil magic/header-based metadata location approach.

Comment thread src/native/managed/cdac/tests/UnitTests/LoaderTests.cs
Comment thread src/coreclr/vm/datadescriptor/datadescriptor.inc
- WasmContext.UnsetSingleStepFlag: no-op instead of throwing. WASM has no
  hardware single-step flag (like ARM/LoongArch64/RISC-V), and callers such as
  Debugger_1.PrepareExceptionHijack invoke it unconditionally.
- WasmUnwinder.TryGetFramePointer: re-apply the linear-stack floor after
  following the localloc frame-pointer indirection, so a null/invalid saved
  pointer returns false instead of reading at a bad address.
- WasmUnwinder.TryUnwindOneFrame: terminate the R2R walk when the decoded frame
  size is 0 (no progress) or the caller's virtual IP is null (interpreter
  transition / stack top), rather than reporting a bogus advanced frame.
- WasmUnwinder.DecodeULEB128: bound the decode to 5 bytes (uint32 max) and fail
  on a malformed, unterminated encoding.
- Document the WASM ExecutionManager descriptors (ReadyToRunInfo.MinVirtualIP,
  FunctionTableIndexRangeSection, FunctionTableIndexRangeList) in
  ExecutionManager.md.
- Fix an accidental single-line brace/declaration in a LoaderTests method.
- Add unit tests for the new unwinder guards (below-floor localloc, zero frame
  size, malformed ULEB128).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
Copilot AI review requested due to automatic review settings July 17, 2026 19: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 29 out of 29 changed files in this pull request and generated 4 comments.

- WasmR2RInfo.FindSection: dereference the FunctionTableIndexRangeList global.
  CDAC_GLOBAL_POINTER globals hold the address of the pointer slot
  (s_pFunctionTableIndexRangeList), so the value must be read through once to
  obtain the actual list head (matching the ThreadStore/FinalizerThread pattern)
  rather than walking from the slot address itself.
- WasmFrameHandler.HandleHijackFrame: throw PlatformNotSupportedException, the
  standard exception for platform limitations in this codebase.
- WasmContext.Try{Set,Read}Register: compare register names with
  StringComparison.OrdinalIgnoreCase instead of name.ToLowerInvariant(), which
  allocated on every call on the stack-walk hot path (matching the other
  platform contexts).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
Copilot AI review requested due to automatic review settings July 17, 2026 19:28

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 29 out of 29 changed files in this pull request and generated 1 comment.

Add a MockTarget harness for WasmR2RInfo that resolves a function-table index
through the FunctionTableIndexRangeList global -> range section -> module ->
ReadyToRunInfo -> RuntimeFunction chain. The global is modeled as a
CDAC_GLOBAL_POINTER (address of the list-head slot), so the tests fail if
FindSection walks from the slot address instead of dereferencing it.

Covers TryGetVirtualIPBase (base virtual IP), TryGetUnwindData
(LoadedImageBase + unwind RVA), the non-funclet flag, and an index outside any
section. Extends MockReadyToRunInfo with the WASM MinVirtualIP field and
LoadedImageBase/MinVirtualIP accessors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
Copilot AI review requested due to automatic review settings July 17, 2026 19:38

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 31 out of 31 changed files in this pull request and generated 5 comments.

…egister

- Make WasmContext, IWasmR2RInfo, WasmUnwinder, WasmR2RInfo `internal` to
  match their sibling context/unwinder types (AMD64Context/AMD64Unwinder are
  internal; only IPlatformContext is public). Tests reach them via InternalsVisibleTo.
- Remove IWasmR2RInfo.IsFuncletFunctionIndex and its WasmR2RInfo implementation:
  unused in production (funclet detection is deferred with EH unwind). Drop the
  corresponding fake/test members and now-unused constants.
- WasmFrameHandler.HandleInlinedCallFrame: check the TrySetRegister return value
  and throw on failure, consistent with the other frame handlers.

Full cDAC suite: 2735 passed / 0 failed / 16 skipped.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
Copilot AI review requested due to automatic review settings July 17, 2026 19:51
@lewing

lewing commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

@copilot review

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 31 out of 31 changed files in this pull request and generated 1 comment.

Comment on lines 98 to 104
ThisPtrRetBufPrecodeData,
InterpreterPrecodeData,
InterpByteCodeStart,
InterpMethod,
InterpMethodContextFrame,
FunctionTableIndexRangeSection,
Array,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants