Skip to content

[cDAC] WebAssembly: real-data R2R stack-walk validation tests - #131161

Open
lewing wants to merge 6 commits into
dotnet:mainfrom
lewing:lewing-cdac-wasm-r2r-validation
Open

[cDAC] WebAssembly: real-data R2R stack-walk validation tests#131161
lewing wants to merge 6 commits into
dotnet:mainfrom
lewing:lewing-cdac-wasm-r2r-validation

Conversation

@lewing

@lewing lewing commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

Follow-up to #130988 (cDAC WebAssembly support). That PR added the WASM cDAC stack-walk plug-in (WasmContext / WasmUnwinder / WasmR2RInfo / WasmFrameHandler) and the metadata-resolution contract fixes, with unit coverage built on synthetic mock values. This PR anchors the R2R forward path to real runtime behavior and corrects several inaccurate comments.

Tests

  1. Empty range-list guards_pFunctionTableIndexRangeList is an empty (null) list head until the runtime populates it per module while applying eager fixups at module load. That empty state is legitimate — an interpreter-only process, or before any ReadyToRun module has loaded — so WasmR2RInfo.FindSection must treat it as a clean "no R2R sections" answer rather than dereferencing the null head. EmptyRangeList_ResolvesToNoSection locks that in.

  2. Real-data R2R validation — two tests anchored to values captured from a live dispatching composite ReadyToRun WASM build (a merged CoreLib R2R image):

    • The live FunctionTableIndexRangeSection node: MinFunctionTableIndex = 6259, NumRuntimeFunctions = 45283, single node (pNext = 0).
    • ReadyToRunInfo.MinVirtualIP = 0 (single-image composite → base virtual IP reduces to BeginAddress).
    • Per-frame values from disassembling that composite's R2R prolog for System.Exception..ctor: on entry it stores its function-table index (6259 + 1040) at frame+0 and its function-local virtual IP / 2 at frame+4.

    RealDispatchingBuild_ResolvesFunctionTableIndexInRange validates FindSection / TryGetVirtualIPBase against the real range (including the exclusive [Min, Min+Num) top boundary). RealDispatchingBuild_UnwinderDecodesRealFrameVirtualIP drives WasmUnwinder.GetVirtualIP through the real WasmR2RInfo end-to-end — the first test exercising the unwinder against the real section reader rather than a fake — validating the × 2 encoding on the frame field with no shift on BeginAddress.

Comment corrections (no behavior change)

  • WasmR2RInfo: the class summary still listed ExecutionManager::IsFuncletFunctionIndex among the mirrored native APIs after that member was removed from IWasmR2RInfo in review. The class still reads the funclet flag internally, so only the API list changes.
  • WasmUnwinder: now also points at src/coreclr/vm/wasm/callhelpers.hpp, which defines the frame offsets and sentinels the unwinder mirrors, alongside helpers.cpp (the walk itself).
  • WasmR2RInfoTests: an earlier draft of the empty-list test claimed the runtime registers virtual-IP ranges "lazily ... until the first managed stack walk". That is incorrect — ReadyToRunInfo::RegisterVirtualIPRange runs per module during eager fixups at module load (Module::RunEagerFixupsUnlocked). The test stands; only its rationale was wrong.

Context

The captured values come from cross-session live debugging of a dispatching R2R-WASM build. A notable finding from that work: on WASM the R2R virtual-IP walk is DAC/reader-driven — managed StackTrace / EH / GC do not invoke it in-process; the WasmUnwinder itself is the walker, driven against a paused target. So these fixture-based tests (replaying captured structure values) are the right CI shape and require no WASM runtime in CI. A future PR can add a live reader-drives-debugger pass once broader R2R dispatch is available.

Testing

  • Full cDAC unit suite: 2766 passed / 0 failed / 16 skipped.
  • The integration test is mutation-verified: changing the unwinder's × 2 to × 3 fails it (9746 vs 9747), confirming it guards the real encoding rather than passing vacuously.

Note

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

lewing and others added 2 commits July 21, 2026 11:59
The runtime registers R2R virtual-IP ranges lazily, so s_pFunctionTableIndexRangeList
is an empty (null) list head until the first managed stack walk — which is the actual
runtime state today. Add a test asserting FindSection treats an empty list as a clean
"no R2R sections" (TryGetVirtualIPBase / TryGetUnwindData return false) rather than
dereferencing the null head.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: afa9efc6-f596-41bd-9070-dc651db8b7b4
Anchors the WasmR2RInfo / WasmUnwinder forward path to values captured from a live
dispatching composite ReadyToRun WASM build (a merged CoreLib R2R image): the
FunctionTableIndexRangeSection node { MinFunctionTableIndex = 6259,
NumRuntimeFunctions = 45283 }, ReadyToRunInfo.MinVirtualIP = 0, and a dispatched
System.Exception..ctor frame storing its function-table index (6259 + 1040) at
frame+0 and its function-local virtual IP / 2 at frame+4.

- RealDispatchingBuild_ResolvesFunctionTableIndexInRange: a real function-table index
  resolves through the single range node; base virtual IP = MinVirtualIP + BeginAddress;
  the [Min, Min + Num) boundaries are exclusive at the top.
- RealDispatchingBuild_UnwinderDecodesRealFrameVirtualIP: end-to-end GetVirtualIP over
  the real frame through the real WasmR2RInfo, validating the * 2 encoding on the frame
  field with no shift on BeginAddress.

Full cDAC suite: 2766 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 21, 2026 17:04
@azure-pipelines

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

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @agocke
See info in area-owners.md if you want to be subscribed.

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 strengthens cDAC’s WebAssembly ReadyToRun (R2R) stack-walk coverage by adding unit tests that validate WasmR2RInfo / WasmUnwinder behavior against an empty range-list state and against a “real-data” fixture modeled after a dispatching composite R2R WASM build.

Changes:

  • Extend the test target builder to optionally model an empty FunctionTableIndexRangeList (null list head).
  • Add EmptyRangeList_ResolvesToNoSection to lock in “empty list => clean miss” behavior for both TryGetVirtualIPBase and TryGetUnwindData.
  • Add two “real-data” fixture tests that validate range inclusion boundaries and end-to-end virtual IP reconstruction through WasmUnwinder.GetVirtualIP.

Comment-only; no behavior change.

- WasmR2RInfoTests: the empty-range-list test claimed the runtime registers
  virtual-IP ranges "lazily ... until the first managed stack walk". That is
  wrong: ReadyToRunInfo::RegisterVirtualIPRange runs per module while applying
  eager fixups at module load (Module::RunEagerFixupsUnlocked). An empty list is
  still a legitimate state -- an interpreter-only process, or before any
  ReadyToRun module has loaded -- so the test stands; only its rationale was
  incorrect.
- WasmR2RInfoTests: the captured-value comment implied the per-frame values were
  read from an observed live frame. They come from disassembling the composite's
  R2R prolog for System.Exception..ctor; say so.
- WasmR2RInfo: the class summary still listed ExecutionManager::IsFuncletFunctionIndex
  among the mirrored native APIs after that member was removed from IWasmR2RInfo.
  The class still reads the funclet flag internally, so only the API list changes.
- WasmUnwinder: point at src/coreclr/vm/wasm/callhelpers.hpp, which defines the
  frame offsets and sentinels the unwinder mirrors, alongside helpers.cpp (the walk).

Full cDAC suite: 2766 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 24, 2026 18:20

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

lewing added a commit that referenced this pull request Jul 25, 2026
…(wasi-sdk) (#131241)

## Summary

On non-emscripten wasm (wasi-sdk), the cDAC contract descriptor getter
`GetDotNetRuntimeContractDescriptor` is not exported, so `--gc-sections`
strips the descriptor from the final module. This prevents a native host
/ the cDAC reader from discovering the runtime data-contract descriptor
on WASI.

The emscripten path already handles this via `EMSCRIPTEN_KEEPALIVE`,
which both exports the getter and keeps the descriptor alive. There was
no equivalent for the wasi-sdk (`__wasm__`, non-emscripten) toolchain.

## Fix

Add an `#elif defined(EXPORT_CONTRACT) && defined(__wasm__)` branch that
emits the getter with `__attribute__((export_name("Get"
#CONTRACT_NAME)))`. The explicit wasm export both makes the symbol
reachable by a native host and roots it so `--gc-sections` does not
remove the descriptor — mirroring what `EMSCRIPTEN_KEEPALIVE` does on
the emscripten path.

The `GETTER_NAME` token-paste macros are hoisted above the `#if` (shared
by both branches) and a `GETTER_NAME_STR` stringize pair is added to
produce the literal export name `"GetDotNetRuntimeContractDescriptor"`.

## Validation

- WASI CoreCLR build (`clr -os wasi -c Release`): compiles clean
(0W/0E).
- Confirmed the compiled object exports
`GetDotNetRuntimeContractDescriptor` with the `no_strip` flag, so it
survives `--gc-sections`.

This unblocks live cDAC discovery on WASI (non-emscripten) — the
runtime-side counterpart to the cDAC-wasm validation work (#131161).
Fixture-based cDAC tests do not depend on it.

> [!NOTE]
> This PR was authored with the assistance of GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot-Session: 2902374a-f352-4c46-bcec-8a35c97733ae
Copilot-Session: 50ddfb2c-b6f8-4847-81e7-44d37d44a175
@lewing
lewing marked this pull request as ready for review July 25, 2026 18:51
Copilot AI review requested due to automatic review settings July 25, 2026 18:51
@azure-pipelines

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

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

Comment on lines +169 to +173
// A contiguous runtime function table covering local indices [0, RealLocalIndex]. Only the
// entry at RealLocalIndex carries a non-zero (non-funclet) BeginAddress; the rest read as zero.
uint runtimeFunctionStride = runtimeFunctionLayout.Stride;
uint tableEntries = RealLocalIndex + 1;
var runtimeFuncTableFrag = allocator.Allocate((ulong)(tableEntries * runtimeFunctionStride), "RuntimeFunctions");
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.

2 participants