Skip to content

[cDAC][wasm] Decode R2R variable locations and expose function identity - #133890

Draft
lewing wants to merge 15 commits into
dotnet:mainfrom
lewing:lewing-r2r-cdac-for-wasm
Draft

lewing wants to merge 15 commits into
dotnet:mainfrom
lewing:lewing-r2r-cdac-for-wasm

Conversation

@lewing

@lewing lewing commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Decode ReadyToRun WebAssembly variable locations through cDAC while keeping wasm unwind state and function identity in the shared StackWalk contract.

  • Maintain coherent WASM SP/IP/logical-FP context across R2R unwind and explicit Frame seeding.
  • Decode wasm register locations (localIndex + WASM_LOCAL_REGNUM_BASE) as symbolic WasmLocal(Index) / WasmLocalPair values.
  • Resolve VLT_STK from the logical frame pointer already stored in the frame context.
  • Expose module-qualified R2R function identity through IStackWalk.
  • Preserve the original ICorDebugInfo::VarLoc register values through DacDbi.
  • Distinguish engine-private locals, unreadable/null byref indirections, and readable stack-homed null references.

Temporary stack

Important

This draft depends on #133086, whose head branch lives in the lewing/runtime fork, so this upstream PR temporarily carries that lower layer. Its commits will drop out when this branch is rebased after #133086 merges.

Base and lower layer:

Three consumer commits:

  • 019c45385b8: [cdac][wasm] Maintain frame context and expose function identity
  • a90393058e5: [cdac][wasm] Decode ReadyToRun variable locations
  • a9f61e79341: [cdac][wasm] Handle unavailable variable values

Producer constants, register/stack encodings, and producer invariant tests are owned by #133086. Virtual-IP traversal and filter-funclet classification came from #133917, function-table-index lookup from #134827, and consumer identity forwards through #134827's WasmFunctionTableIndexLookup.

Integration with merged PRs

StackWalk context and function identity

Native WASM RtlVirtualUnwind updates InterpreterSP, InterpreterIP, and the funclet-resolved InterpreterFP together. cDAC now maintains the same invariant:

  • R2R InlinedCallFrame marker INLINED_PINVOKE_FROM_R2R: shared handling ([cDAC][wasm] Fix stack walks and MethodDesc naming on WebAssembly #135044) derives SP/IP from CallSiteSP, and this PR supplies the funclet-aware logical FP.
  • TransitionFrame uses its saved R2R SP, lazily derives a zero return address, and derives FP only when the saved SP/IP are valid. The generic argument-area fallback is never parsed as a shadow frame.
  • Software exception frames copy the full serialized WASM context.
  • Reverse P/Invoke does not probe native caller bytes as a shadow frame. Caller SP is retained with IP/FP cleared.
  • A successful unwind into a non-R2R caller likewise retains caller SP with a null IP; a direct regression test pins this boundary.
  • Reverse P/Invoke detection reads HasReversePInvokeFrame from the GC-info header, decoded by main's WasmGCInfoTraits ([cDAC][wasm] Register platform GC-info decoding #135055).

IStackWalk.GetWasmFunctionIdentity is valid only for ReadyToRun frameless frames and returns:

FunctionTableIndex      raw runtime-global shared-table index from the frame
Module?                 owning R2R Module
RuntimeFunctionIndex?   RUNTIME_FUNCTION index within that image
IsFunclet?              nullable classification when the range/entry is unresolved

Runtime table indices are globally relocated by each module's tableBase, but V8 func_index values are module-local. Consumers therefore need the owning module plus image-relative runtime-function index to select the correct script and translate through its element section.

Logical frame pointer and stack locations

Current producer stack records encode base register 2; REG_FPBASE, REG_SPBASE, and REGNUM_AMBIENT_SP collapse to that value on WASM. The record identifies a logical frame-relative home, not a particular V8 local.

The absolute frame pointer is reconstructed from shadow-stack memory by the shared unwinder:

  • root without localloc: logical FP aliases SP;
  • root with localloc: logical FP remains the fixed pre-adjustment frame base;
  • funclet: logical FP is the parent/establishing method frame.

Measured engine-local indices ($0, $1, $3) are current codegen observations, not cDAC format or API. No FP-local metadata is required. Variable resolution consumes WasmContext.FramePointer.

Variable locations

Following #133086 c486ea8b02d, every register field in the WASM Vars stream holds a WebAssembly local index biased past the reserved register numbers:

register = localIndex + WASM_LOCAL_REGNUM_BASE   // 3, one past REGNUM_AMBIENT_SP

As on other targets, the value type is not encoded; it follows from the variable's type and the local's declaration. The earlier packed (index, value type) form, the WasmDebugValueType enum, and the WasmDebugRegisterTypeShift/WasmDebugValueTypeCount descriptor globals are gone from both producer and consumer. A register-only location whose register is a reserved pseudo-register (0–2) names no local and is rejected rather than reported as local $0 or a readable register.

The contract reports:

  • VLT_REG / VLT_REG_BYREF as WasmLocal;
  • VLT_REG_REG as WasmLocalPair;
  • stack-based kinds as linear-memory locations resolved from the logical context FP.

For WASM, GetMethodVarInfo uses the ReadyToRun CodeBlock's controlling-method-relative ExecutionManager.GetRelativeOffset result, not the generic CodeVersions path that rejects portable-entrypoint MethodDescs.

Unavailable and null values

Three cases remain intentionally distinct:

  1. Engine-private WASM local: zero physical locations; GetBytes/GetAddress fail with E_NOINTERFACE.
  2. Unreadable or null VLT_STK_BYREF indirection: one logical location remains, matching native location count, while address/value/object access fails with CORDBG_E_READVIRTUAL_FAILURE.
  3. Direct stack-homed null reference: one readable location; reading its bytes succeeds and returns zero.

The native DAC converts a failed byref read to address zero and subsequently fails because native address zero is unmapped. WASM linear address zero is readable, so carrying that fallback forward would fabricate a successful unrelated value. Non-WASM behavior is unchanged.

Live validation

The live validator was rewritten to check contract invariants instead of the codegen values pinned by the previous run. It no longer hardcodes slot offsets, variable ranges, break offsets, V8 function indices, $varN frame-pointer locals, or asset file names:

  • GcSlotIdentity and CollectAtGcSafepoint are located through the loaded module's name and code sections.
  • With [cDAC][wasm] Fix stack walks and MethodDesc naming on WebAssembly #135044 merged, the paused frame's MethodDesc token is checked strictly against the specimen's IL token.
  • The pause is reached by breaking at CollectAtGcSafepoint entry and stepping out until execution returns to GcSlotIdentity. Only same-module runtime helper frames may sit between them; this run had one R2R delay-load helper. The pause is therefore after the blocking GC.Collect() returns.
  • local → value bindings come from decoding the specimen's IL (ldc.i4 N; newobj GcMarker::.ctor; stloc X and ldnull; stloc Y).
  • SP is $0 by the R2R calling convention. The logical FP and caller SP are reconstructed by cDAC from shadow-stack memory.
  • For each IL local: every record uses the documented frame base 2 with one unambiguous offset. The resolved slot equals FP + offset, lies within [SP, callerSP), and the GC info reports the same FRAMEREG_REL slot as live for the whole method.

Build: browser clr+libs at this head (0 warnings, 0 errors), rebuilt because main changed runtime descriptors (#135044). The specimen file from #133086 is linked unmodified into the browser sample. Optimized R2R publish with -p:PublishReadyToRunStripDebugInfo=false (see below). Module SHA-256 6921b8c3ca5ec40bae5b92c538bab45b81e25229da9fb7e5c07f4e107f669416.

PAUSE    entry=0x406 call=0x58E helpers_between=1 step_outs=2 post_gc=0x591
FRAME    sp=0x1FD350 fp=0x1FD350 caller_sp=0x1FD3A0 vip=0x800104B5
IDENTITY raw=25424 module=0x40E9F4C rf=4 funclet=False method=token-0x06000005 (matches IL)
LOCAL0   [0x36,0x2A5) base=2 +0x48 slot=0x1FD398 GcMarker Value=17
LOCAL1   [0x36,0x2A5) base=2 +0x44 slot=0x1FD394 GcMarker Value=29
LOCAL2   [0x36,0x2A5) base=2 +0x40 slot=0x1FD390 null, one location, bytes 00000000
CONTROL  permuted homes keep the type but fail the 17/29 oracle
UNAVAILABLE engine local: 0 locations, GetBytes = E_NOINTERFACE
RESULT   PASS

The variable range end moved from 0x2B1 to 0x2A5 with the producer update; the previous pinned validator would have failed on that alone. On the previous base, a live mutation adding 4 to the consumer's stack offset failed (resolved to 0x1FDFDC, not FP+0x48), and the restored run was green again. That code path is unchanged by this restack.

What this run does not establish:

  • FP equals SP at this pause (root method without localloc), so the SP-versus-FP base-selection control is not discriminating here. The earlier GcLocalAcrossFinally funclet run with FP ≠ SP remains the evidence for that.
  • DebugInfo ranges are in JIT native-offset space, while WASM R2R GC info and the shadow frame use the synthetic virtual-IP space (GC code length here is 0x4). The validator does not compare across those spaces. General CDP byte-offset → DebugInfo-offset translation remains a follow-up.

Findings for consumers

Tests

  • cDAC UnitTests: 3,309 passed, 0 failed.
  • ./build.sh -s tools.cdactests -test: UnitTests, DataGeneratorTests, and UsageTests passed; generated docs up to date.
  • Consumer commit 1 builds alone and passes all unit tests (3,278). Commit 2 also passed alone (3,303) before the funclet test was folded into commit 1; that fold only adds a test, so its tree is otherwise unchanged.
  • Mutations, each confirmed red then restored green:
    • InlinedCallFrame FP reverted to the root frame base: the new funclet test fails (expected 0x200020, got 0x200000);
    • on the previous base: decode base 3 → 2 (8 tests fail), dropping the bias (6 tests fail), and the live stack-offset corruption above.
  • Earlier mutation evidence (unavailable values, frame-kind identity gating, stale funclet FP, C3 fallback, swapped C2 slots, CodeVersions offset path) still applies to logic unchanged by this restack.

Limitations and readiness

  • No native DAC is built on WASM, so DacDbi parity is structural managed unit coverage rather than live _legacyImpl comparison.
  • v128 and reference register locals have synthetic decode coverage only; their live V8 representations remain unproven.
  • Browser-wasm cDAC CI remains desirable.

This PR remains draft pending #133086 and Larry's decision.

Note

This pull request description was generated with GitHub Copilot.

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 7 pipeline(s).
9 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: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.

@lewing lewing added the arch-wasm WebAssembly architecture label Sep 14, 2026
@lewing lewing changed the title [cDAC][wasm] Decode R2R variable locations and expose function identity [wip][cDAC][wasm] Decode R2R variable locations and expose function identity Sep 14, 2026
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.

@lewing
lewing force-pushed the lewing-r2r-cdac-for-wasm branch 2 times, most recently from d5f26e6 to 69ad34f Compare September 14, 2026 18:53
@lewing lewing changed the title [wip][cDAC][wasm] Decode R2R variable locations and expose function identity [cDAC][wasm] Decode R2R variable locations and expose function identity Sep 15, 2026
@lewing
lewing force-pushed the lewing-r2r-cdac-for-wasm branch from 69ad34f to 7e72c0e Compare September 16, 2026 01:48
@lewing
lewing force-pushed the lewing-r2r-cdac-for-wasm branch from 7e72c0e to 1166442 Compare September 25, 2026 19:52
lewing added a commit that referenced this pull request Sep 26, 2026
…ish (#134689)

## Summary

Pass `--strip-debug-info` to crossgen2 for CoreCLR browser-wasm
ReadyToRun publish. This drops the R2R `DebugInfo` section (native-to-IL
offset maps and variable locations) from shipped images. Opt out with
`PublishReadyToRunStripDebugInfo=false` to keep the data for debugging
R2R code (e.g. the cDAC work in #133086 / #133890).

Also makes crossgen2 argument changes invalidate per-app R2R images.
`_CreateR2RImages` only tracks file inputs, so toggling
`PublishReadyToRunStripDebugInfo` (or any
`PublishReadyToRunCrossgen2ExtraArgs` change) previously left stale
images in `obj/R2R`. The arguments are now written to
`obj/wasm-r2r-args.stamp` (only when different) and added to
`_ReadyToRunCompilerInputs`, mirroring the existing P/Invoke manifest
input.

> [!IMPORTANT]
> Stacked on #134618, which rewrites the same targets file. Retarget to
`main` after it merges.

## Size impact

Per-assembly R2R images compiled directly with crossgen2 using the SDK's
browser-wasm arguments (`--obj-format:wasm --opt-cross-module:*
--codegenopt:JitWasm*NyiToR2RUnsupported=1`), with and without
`--strip-debug-info`. Total for System.Private.CoreLib,
System.Text.Json, System.Linq, and System.Collections:

| Compiler | Raw saved | gzip -9 saved | brotli -q 11 saved |
| --- | --- | --- | --- |
| Current (#134618 base) | 859,712 B (2.4%) | 624,690 B (7.1%) | 565,366
B (9.4%) |
| With #133086 variable info | 2,105,520 B (5.6%) | 1,332,733 B (13.9%)
| 1,121,574 B (17.0%) |

<details>
<summary>Per-assembly brotli sizes (bytes, keep → strip)</summary>

| Assembly | Current | With #133086 |
| --- | --- | --- |
| System.Private.CoreLib | 4,822,231 → 4,347,541 | 5,331,799 → 4,403,849
|
| System.Text.Json | 823,314 → 761,533 | 887,982 → 754,444 |
| System.Linq | 219,177 → 200,471 | 238,853 → 200,441 |
| System.Collections | 119,635 → 109,446 | 130,742 → 109,068 |

The #133086 compiler is based on an older commit, so compare keep vs.
strip within a column rather than across columns.
</details>

## Validation

- MSBuild evaluation: `--strip-debug-info` is present by default, absent
with `PublishReadyToRunStripDebugInfo=false`, and absent when
`PublishReadyToRun` is off.
- Incremental harness: `_CreateR2RImages` runs on first build, skips
when unchanged, reruns on opt-out, skips when repeated, and reruns when
switching back.
- Not yet run: an end-to-end browser-wasm publish loading stripped
images in a browser (Wasm.Build.Tests in CI will cover this).

Runtime-pack framework R2R images (used only by the dev-loop build) are
unchanged; publish recompiles the whole closure through these targets.
`--strip-inlining-info` is intentionally not included: it removes
`CrossModuleInlineInfo`, and cross-module inlining is load-bearing on
wasm, so it needs separate validation.

> [!NOTE]
> This pull request was created with assistance from GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing added a commit that referenced this pull request Sep 26, 2026
## Summary

Fix cDAC live resolution of WebAssembly ReadyToRun virtual IPs by
matching the runtime's existing lookup model:

- expose `ExecutionManager::s_pVirtualIPRangeList` and
`VirtualIPRangeSection` through the data descriptor;
- resolve encoded virtual IPs through that intrusive list using cycle
detection and a 65,536-node per-lookup reader resource budget, with no
map fallback when an encoded VIP is absent;
- mask the WebAssembly funclet flag from `RUNTIME_FUNCTION.BeginAddress`
for ordering and address arithmetic while preserving funclet identity;
- keep the virtual code base (`MinVirtualIP`) separate from the
loaded-image base used for unwind, debug, GC, exception, and thunk RVA
reads;
- handle the actual WASM descriptor shape, where hot/cold metadata and
delay-load thunk metadata are absent;
- classify WASM filter funclets by mapping the executable filter entry
to its containing runtime function.

## Root cause

The model added in #130988 was already false when that PR merged. On
`TARGET_WASM`, ReadyToRun modules are not added to `RangeSectionMap`;
`ReadyToRunInfo::RegisterVirtualIPRange` registers them in
`ExecutionManager::s_pVirtualIPRangeList`, and native `FindCodeRange`
checks that list first.

The prior unit test synthesized a `RangeSectionMap` entry with an
address that did not satisfy native `IsVirtualIP`, so it validated a
mock-only model rather than the live runtime layout. This is a
test-model gap, not a reviewer fault. The prior review explicitly noted
that the WebAssembly specifics had not been run locally and should be
added to cDAC CI:
#130988 (review).

## Blast radius and scope

This affects ReadyToRun code on all CoreCLR WebAssembly hosts, including
browser and WASI. Interpreter code is unaffected.

The list lookup, descriptor feature gating, funclet masking, and
image-base separation are inseparable: exposing the list alone would
still throw while reading absent WASM fields, or could return the wrong
method or read RVA data from the synthetic virtual address space.

This PR is independent of #133086 and intentionally excludes variable
producer/decoder work. #133890 depends on this PR for correct shared
code lookup and function identity.

On WASM, `FilterOffset` is the executable filter entry and can follow a
synthetic funclet prolog. cDAC now mirrors the corrected native
classification in #133932 by resolving that entry to its containing
runtime function before comparing funclet starts. The PRs remain
independent; #133917 does not depend on changing the producer offset.

## Validation

- `./build.sh clr+libs+host`
- `PATH="/opt/homebrew/bin:$PATH" ./build.sh -os browser -c Debug
-subset clr+libs`
- cDAC UnitTests: **3162 passed**
- cDAC DataGeneratorTests: **46 passed**
- cDAC UsageTests: **4 passed**
- generated contract documentation check: **up to date**
- focused ExecutionManager / RuntimeFunction / WasmR2R tests: **221
passed**

The durable tests cover:

- captured/live-shaped VIP `0x80010109`, exact `MethodDesc`, module, and
runtime-function index;
- the actual WASM descriptor shape: 8-byte `RUNTIME_FUNCTION` records
with no `EndAddress`, and absent hot/cold and delay-load thunk fields;
- start/end boundaries and adjacent ranges;
- encoded VIP absent from the list with no `RangeSectionMap` fallback;
- self-cycle, two-node cycle, inverted range, null module, and
overlapping ambiguity;
- unrelated partially registered nodes not blocking initialized ranges,
while an uninitialized candidate fails closed;
- valid 1,024/1,025-node lists, exact 65,536-node budget success, and
budget+1 fail-closed behavior even when the head matches, with a read
counter proving the extra node is never dereferenced;
- root/funclet resolution with a flagged funclet entry that breaks raw
ordering;
- exact loaded-image debug, unwind, GC, and exception-clause reads while
entrypoint lookup uses `MinVirtualIP`;
- filter-funclet classification where `FilterOffset` follows the flagged
funclet start but resolves to the same containing runtime function;
- missing list capability and unchanged ordinary architecture behavior.

Mutation proofs were applied, confirmed in source, run red, restored,
and rerun green:

1. Removing the VIP-list branch fails the captured `0x80010109` test at
the exact code-block assertion.
2. Using raw `BeginAddress` fails the funclet identity test.
3. Using `startVIP` as the loaded-image base fails the GC/unwind test
with a read at `0x80010081` instead of the loaded image.
4. Raising the reader budget from 65,536 to 65,537 makes the budget+1
test fail at its read-boundary assertion (highest node index 65,536
instead of 65,535); restoring the budget returns the suite to green.
5. Replacing WASM filter-entry containing-function resolution with raw
`FilterOffset == funcletStartOffset` comparison makes the filter
regression fail with expected `true` and actual `false`.

The finite list cutoff is an intentional diagnostic-reader resource
policy, not a native registration limit or a claim that an over-budget
list is corrupt.

> [!NOTE]
> This pull request description was generated with GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing added a commit that referenced this pull request Sep 29, 2026
…rtual IPs (#134756)

> [!IMPORTANT]
> Stacked on #134754 (targets its branch). Only the commits above
#134754's head belong to this PR; retarget to `main` once #134754
merges.

## Problem

On `FEATURE_PORTABLE_ENTRYPOINTS` targets (WebAssembly), a method's
entry point slot holds a `PortableEntryPoint` address. Native maps it
before handing it to diagnostics
(`GetInterpreterCodeFromEntryPointIfPresent` /
`GetDiagnosticCodeStartFromEntryPoint` in `precode.cpp`), but the cDAC
returned the raw address. SOS values such as
`DacpMethodDescData.NativeCodeAddr` and rejit `NativeCodeAddr`, plus the
DBI/`ClrDataMethodInstance` paths, therefore reported an address that
doesn't resolve through `ExecutionManager` for both interpreted and R2R
methods.

## Change

Adds `IExecutionManager.GetDiagnosticCodeStartFromEntryPoint`, which
mirrors the native `GetDiagnosticCodeStartFromEntryPoint`:

- **Without portable entrypoints:** delegates to
`PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent`.
Behavior is unchanged.
- **With portable entrypoints:**
1. Returns the address unchanged if it lies in a code range. This is the
native `FindCodeRange` check and includes Wasm R2R virtual-IP ranges
from #133917.
  2. Maps interpreted methods to `MethodDesc::m_interpreterCode`.
3. Maps native R2R methods from the function-table index in
`PortableEntryPoint._pActualCode` to the synthetic virtual IP, stepping
back past funclet entries. This matches
`ExecutionManager::GetWasmVirtualIPFromFunctionTableIndex`. As in native
code, it applies only to the method's own (temporary) entry point, and
only when the entry point doesn't prefer the interpreter.

The mapping lives in ExecutionManager rather than PrecodeStubs because
ExecutionManager owns the virtual-IP ranges and R2R lookup.
ExecutionManager already depends on PrecodeStubs, so this adds no
contract cycle.

Function-table-index resolution moves into
`ExecutionManagerHelpers.WasmFunctionTableIndexLookup`, which now bounds
the list walk and detects cycles, like the virtual-IP list walk. The
stack walk's `WasmR2RInfo` becomes a thin wrapper over it.

The Legacy SOS/DBI callers (`SOSDacImpl`, `ClrDataMethodInstance`,
`DacDbiImpl`) now call the new API. `DacDbiImpl.EnumerateAsyncLocals`
also maps its code address before querying async debug info. The native
DAC's `EnumerateAsyncLocals` gets the matching
`GetInterpreterCodeFromEntryPointIfPresent` mapping (as
`GetMethodVarInfo` already does), so the debug-build cDAC/DAC
cross-check stays consistent.

### Data descriptors

- `PortableEntryPoint.ActualCode` and `PortableEntryPoint.Flags` (only
under `FEATURE_PORTABLE_ENTRYPOINTS`).
- `MethodDesc.InterpreterCode` (only under `FEATURE_INTERPRETER`).
- The contract relies on `kPrefersInterpreterEntryPoint` and
`INTERPRETER_CODE_POISON`; the native side now has `[cDAC]` comments
marking that dependency.

### Interaction with #133890

#133890 adds `TryGetFunctionIdentity` and `TryIsFunclet` to
`WasmR2RInfo`. Whichever PR lands second should add those two methods to
`WasmFunctionTableIndexLookup` and have `WasmR2RInfo` forward to them.
The `FunctionTableIndexRange*` descriptor meanings here already use
#133890's exact wording, so that JSON should merge cleanly.

## Validation

- cDAC: `./build.sh -s tools.cdac+tools.cdactests -c Debug -test`
passes: 3196 unit tests (17 new), usage tests (contract cycles and
generated docs up to date), and generator tests.
- New ExecutionManager tests cover:
- interpreted, R2R, funclet, poison, prefers-interpreter,
not-own-entry-point and unknown-index cases;
- an end-to-end check that the resolved virtual IP maps back to the
MethodDesc through `GetCodeBlockHandle`;
- readable `PortableEntryPoint`-shaped bytes inside a registered code
range staying unchanged. This test fails if the range check is removed.
  - a cyclic function-table range list;
  - the non-portable delegation path.
- CoreCLR `clr.runtime` builds for osx-arm64 Debug (with
`FEATURE_INTERPRETER`) and browser-wasm Debug. The generated wasm
descriptor has `PortableEntryPoint {ActualCode@0, MethodDesc@4,
Flags@12}`.
- A new `DacDbiImplTests` test covers `EnumerateAsyncLocals` mapping for
both the code-address and MethodDesc paths. It fails without the fix.
- cDAC dump tests (osx-arm64, local runtime): 260 passed, 0 failed. The
746 skips are net10.0 configurations plus by-design skips (Windows-only
COM debuggees, and dump types a debuggee doesn't produce).
- CoreCLR `clr.runtime` Release (osx-arm64) builds with the
`dacdbiimpl.cpp` change.
- Not run: any check against a live wasm target.

Fixes #134753

> [!NOTE]
> This PR description was generated with GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing added a commit that referenced this pull request Sep 29, 2026
…rtual IPs (#134827)

This replaces #134756, which was merged into #134754's branch by mistake
and reverted there. The change is otherwise identical (cherry-picked
onto `main`).

## Problem

On `FEATURE_PORTABLE_ENTRYPOINTS` targets (WebAssembly), a method's
entry point slot holds a `PortableEntryPoint` address. Native maps it
before handing it to diagnostics
(`GetInterpreterCodeFromEntryPointIfPresent` /
`GetDiagnosticCodeStartFromEntryPoint` in `precode.cpp`), but the cDAC
returned the raw address. SOS values such as
`DacpMethodDescData.NativeCodeAddr` and rejit `NativeCodeAddr`, plus the
DBI/`ClrDataMethodInstance` paths, therefore reported an address that
doesn't resolve through `ExecutionManager` for both interpreted and R2R
methods.

## Change

Adds `IExecutionManager.GetDiagnosticCodeStartFromEntryPoint`, which
mirrors the native `GetDiagnosticCodeStartFromEntryPoint`:

- **Without portable entrypoints:** delegates to
`PrecodeStubs.GetInterpreterCodeFromInterpreterPrecodeIfPresent`.
Behavior is unchanged.
- **With portable entrypoints:**
1. Returns the address unchanged if it lies in a code range. This is the
native `FindCodeRange` check and includes Wasm R2R virtual-IP ranges
from #133917.
  2. Maps interpreted methods to `MethodDesc::m_interpreterCode`.
3. Maps native R2R methods from the function-table index in
`PortableEntryPoint._pActualCode` to the synthetic virtual IP, stepping
back past funclet entries. This matches
`ExecutionManager::GetWasmVirtualIPFromFunctionTableIndex`. As in native
code, it applies only to the method's own (temporary) entry point, and
only when the entry point doesn't prefer the interpreter.

The mapping lives in ExecutionManager rather than PrecodeStubs because
ExecutionManager owns the virtual-IP ranges and R2R lookup.
ExecutionManager already depends on PrecodeStubs, so this adds no
contract cycle.

Function-table-index resolution moves into
`ExecutionManagerHelpers.WasmFunctionTableIndexLookup`, which now bounds
the list walk and detects cycles, like the virtual-IP list walk. The
stack walk's `WasmR2RInfo` becomes a thin wrapper over it.

The Legacy SOS/DBI callers (`SOSDacImpl`, `ClrDataMethodInstance`,
`DacDbiImpl`) now call the new API. `DacDbiImpl.EnumerateAsyncLocals`
also maps its code address before querying async debug info. The native
DAC's `EnumerateAsyncLocals` gets the matching
`GetInterpreterCodeFromEntryPointIfPresent` mapping (as
`GetMethodVarInfo` already does), so the debug-build cDAC/DAC
cross-check stays consistent.

### Data descriptors

- `PortableEntryPoint.ActualCode` and `PortableEntryPoint.Flags` (only
under `FEATURE_PORTABLE_ENTRYPOINTS`).
- `MethodDesc.InterpreterCode` (only under `FEATURE_INTERPRETER`).
- The contract relies on `kPrefersInterpreterEntryPoint` and
`INTERPRETER_CODE_POISON`; the native side now has `[cDAC]` comments
marking that dependency.

### Interaction with #133890

#133890 adds `TryGetFunctionIdentity` and `TryIsFunclet` to
`WasmR2RInfo`. Whichever PR lands second should add those two methods to
`WasmFunctionTableIndexLookup` and have `WasmR2RInfo` forward to them.
The `FunctionTableIndexRange*` descriptor meanings here already use
#133890's exact wording, so that JSON should merge cleanly.

## Validation

- cDAC: `./build.sh -s tools.cdac+tools.cdactests -c Debug -test`
passes: 3196 unit tests (17 new), usage tests (contract cycles and
generated docs up to date), and generator tests.
- New ExecutionManager tests cover:
- interpreted, R2R, funclet, poison, prefers-interpreter,
not-own-entry-point and unknown-index cases;
- an end-to-end check that the resolved virtual IP maps back to the
MethodDesc through `GetCodeBlockHandle`;
- readable `PortableEntryPoint`-shaped bytes inside a registered code
range staying unchanged. This test fails if the range check is removed.
  - a cyclic function-table range list;
  - the non-portable delegation path.
- CoreCLR `clr.runtime` builds for osx-arm64 Debug (with
`FEATURE_INTERPRETER`) and browser-wasm Debug. The generated wasm
descriptor has `PortableEntryPoint {ActualCode@0, MethodDesc@4,
Flags@12}`.
- A new `DacDbiImplTests` test covers `EnumerateAsyncLocals` mapping for
both the code-address and MethodDesc paths. It fails without the fix.
- cDAC dump tests (osx-arm64, local runtime): 260 passed, 0 failed. The
746 skips are net10.0 configurations plus by-design skips (Windows-only
COM debuggees, and dump types a debuggee doesn't produce).
- CoreCLR `clr.runtime` Release (osx-arm64) builds with the
`dacdbiimpl.cpp` change.
- Re-validated after cherry-picking onto `main`: `./build.sh clr -c
Debug` (osx-arm64) builds, and `./build.sh -s tools.cdac+tools.cdactests
-c Debug -test` passes (3196 unit tests, 46 generator tests, 4 usage
tests; 0 failed).
- Not run: any check against a live wasm target.

Resolves #134753

> [!NOTE]
> This PR description was generated with GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing added a commit that referenced this pull request Sep 30, 2026
## Summary

Fixes a GC hole during exception dispatch in Wasm R2R code. It shows up
in CI as a rare `SanityCheck()` assert in nested EH tests on the
browser-wasm R2R leg, for example `Test_throwinfinallynestedintry_30`.

## Root cause

Wasm R2R code has no interruptible ranges, and it reports every GC ref
on the frame as an untracked (pinned) slot. A method and its funclets
share those frame slots.

When an exception is thrown from a funclet, that funclet's frame is
`ExecutionAborted`. `GcInfoDecoder::EnumerateLiveSlots` finds no
interruptible range covering the offset and returns without reporting
anything, including the untracked slots. The parent frames (the caller
of an out-of-line finally, and the main method body) are skipped as
already reported by the funclet.

So during dispatch nothing reports the method's untracked slots. A GC in
that window can free an object still held in one of them. In the repro
it's a boxed `Int32`. The next time the slot is reported, it points at
freed memory and hits `SanityCheck()`.

Other targets avoid this in the JIT.
`CodeGenInterface::setFramePointerRequiredEH` forces every method with
EH to be fully interruptible, because `EnumGcRefs` only reports slots in
aborted frames that are fully interruptible. Wasm is explicitly excluded
there, and Wasm R2R code can't be fully interruptible.

## Fix

Add a `HAS_INTERRUPTIBLE_RANGES` trait to each `GcInfoEncoding`, `false`
only for `Wasm32GcInfoEncoding`.

- `EnumerateLiveSlots` skips the interruptible-range handling for that
encoding and reports only untracked slots outside safe points, including
for aborted frames.
- The count of interruptible ranges is no longer serialized for that
encoding. The encoder, the runtime decoder, the cDAC decoder and R2RDump
all skip it, and `DefineInterruptibleRange` asserts it's never used for
that encoding.
- There's no R2R version bump: Wasm hasn't shipped, so Wasm-specific
format changes don't need one.

The gate is on the encoding rather than `TARGET_WASM`: on Wasm the
interpreter's GC info goes through the same decoder, and interpreter
code does define an interruptible range. Native targets are unchanged.

The cDAC `GCInfoDecoder` mirrors the change through the same trait
(default `true`), and `docs/design/datacontracts/GCInfo.md` documents
it. `main` has no Wasm32 GC info traits in the cDAC yet; #133890 adds
them.

I fixed this in the decoder rather than having the JIT declare Wasm
methods interruptible over their whole range. That claim isn't true for
Wasm, and other checks rely on it (GC stress, the safe-point asserts).

## Size

On browser-wasm `System.Private.CoreLib.wasm` (CI crossgen options),
omitting the count saves 1,088 bytes (0.003%). A GC info blob only
shrinks when the 2 saved bits cross a byte boundary (11,606 of 58,253
methods), and identical blobs are shared (5,331 distinct unwind entries
across 60,323 methods and funclets).

## Validation

I ran the CI Helix payload for `Methodical_d1` from build 1613981
locally, with crossgen2 built from this branch and a browser-wasm
`corerun` built with and without the fix. For the final format I
recompiled CoreLib and the test assemblies with this branch's crossgen2.

| | Without fix | With fix |
|---|---|---|
| `Test_throwinfinallynestedintry_30`, `DOTNET_GCStress=0x1` | same
`SanityCheck()` assert as CI, every run | pass |
| `throwincascadedexcept_d`, `throwincascadedexceptnofin_d`,
`DOTNET_GCStress=0x1` | assert, then timeout | pass |
| `Methodical_d1` merged runner, no stress | 128 passed | 128 passed,
same results |

- Native osx-arm64 and browser-wasm checked builds pass.
- R2RDump and the cDAC unit tests pass (3179/3179).
- The final format gives the same no-stress results as a control that
still writes and reads the count.
- All 84 `Methodical_d1` assemblies, R2R-compiled with the checked Wasm
JIT, never hit the new encoder assert.
- In every full run, the same 18 out-of-process tests fail because my
local runner doesn't handle out-of-process tests.
- The final revert of the version bump only restores `readytorun.h` and
the two `ModuleHeaders` files to `main`; it wasn't rebuilt.

With this fix, the full `Methodical_d1` suite under
`DOTNET_GCStress=0x1` was clean in 3 of 4 runs. The 4th hit a
`RawGetMethodTable()` assert in `throw_SEH`. That's a separate,
pre-existing hole, #134777: `CallDescrWorkerInternal` leaves the call's
arguments unreported while `DoPrestub` runs. It reproduces identically
with and without this change and is fixed in #134779. With both fixes,
the full suite under stress passed 4 of 4 runs (126 passed), in a run
before the count was removed from the format.

I also checked native (osx-arm64, release 11.0 rc1 runtime, FullOpts
JIT) with a small app. An object is held only in an address-exposed
local, a `finally` throws, and the same method catches it with a filter
that forces a GC. The object stayed alive 20/20 times, and JitDisasm
shows the method as `; fully interruptible`, as
`setFramePointerRequiredEH` requires.

CI doesn't run GC stress on the Wasm R2R leg, which is why this showed
up as a ~0.5% flake rather than a hard failure.

Resolves #134768

> [!NOTE]
> This PR description was drafted with GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing
lewing force-pushed the lewing-r2r-cdac-for-wasm branch from 1166442 to df98928 Compare October 1, 2026 21:34
lewing added a commit that referenced this pull request Oct 2, 2026
… section (#135059)

Native `ReadyToRunInfo::GetDebugInfo`
(`src/coreclr/vm/readytoruninfo.cpp`) returns `NULL` immediately when
`m_pSectionDebugInfo == NULL`. The cDAC mirror,
`ReadyToRunJitManager.GetDebugInfo`, dereferenced
`ReadyToRunInfo.DebugInfoSection` without that check and built a
`NativeArray` from whatever was at address 0.

This now matters because #134690 makes Browser/WASI ReadyToRun publishes
pass `--strip-debug-info` by default, including CoreLib and the
framework. iOS, tvOS, and MacCatalyst already default to it. On wasm,
linear address 0 is readable, so cDAC decoded garbage. A live run
against a stripped browser image threw `BadImageFormatException: offset
out of bounds` (`NativeReader.DecodeUnsigned` ← `NativeArray..ctor` ←
`ReadyToRunJitManager.GetDebugInfo` ← `DebugInfo_1.GetMethodVarInfo`)
instead of reporting that there was no debug info. On native targets,
the same path fails with a read exception.

## Changes

- `ReadyToRunJitManager.GetDebugInfo` returns `TargetPointer.Null` (with
`hasFlagByte = false`) when `DebugInfoSection` is null, matching native.
- `docs/design/datacontracts/ExecutionManager.md`: the R2R
`GetDebugInfo` description now includes the null-section early return.
- No caller changes were needed. `DebugInfo_1.HasDebugInfo`,
`GetMethodNativeMap`, `GetMethodVarInfo`, and `GetAsyncSuspensionPoints`
already treat a null debug-info pointer as "no debug info". The two
map/var methods still compute `codeOffset`.

## Tests

New test
`ExecutionManagerTests.GetDebugInfo_R2R_NoDebugInfoSection_ReturnsNull`
runs as a `[Theory]` over `StdArchAllVersions` (4 arch cases). It builds
an R2R module whose `DebugInfoSection` is null and makes the low 4 KB of
the address space readable as zeros, the way wasm linear memory is, so
the unfixed code decodes instead of hitting a read fault. It asserts
that:
- `IExecutionManager.GetDebugInfo` returns `TargetPointer.Null` and
`hasFlagByte == false`
- `IDebugInfo.HasDebugInfo` is `false`
- `GetMethodVarInfo` and `GetMethodNativeMap` return empty sequences
with `codeOffset == 4`

Results:
- `./build.sh -s tools.cdactests -test`: passed. UnitTests 3236/3236,
DataGeneratorTests 46/46, UsageTests 4/4 (no stale generated docs).
- `./.dotnet/dotnet test src/native/managed/cdac/tests/UnitTests
--filter "FullyQualifiedName~GetDebugInfo_R2R_NoDebugInfoSection"`:
passed, 4/4.
- Mutation check: I removed the null check and reran the same filtered
command. It failed 4/4 with `System.BadImageFormatException : offset out
of bounds` at `NativeReader.DecodeUnsigned` ← `NativeArray..ctor` ←
`ReadyToRunJitManager.GetDebugInfo`, the same failure as the live wasm
run. With the fix restored, it passes 4/4.

This PR has a single concern. It leaves wasm stack-walk and
variable-location work to #133890, #135044, and #135055.

> [!NOTE]
> This PR description was generated with GitHub Copilot.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing added a commit that referenced this pull request Oct 2, 2026
…35044)

Fixes four cDAC failures found by walking stacks on a live CoreCLR
browser-wasm target (nightly `12.0.0-alpha.1.26480.103`) through
`IStackWalk`. On `main`, the walk fails on its first step, or never
ends, and no MethodDesc can be named.

## Changes

**MethodDesc validation rejects every MethodDesc (#135035).** With
`FEATURE_PORTABLE_ENTRYPOINTS`, the runtime has no precode stubs and
doesn't describe `PrecodeMachineDescriptor`, so constructing
`PrecodeStubs` threw. `MethodValidation` swallowed the exception and
reported every MethodDesc as invalid.
- New `PrecodeStubs` version `c2` (`PrecodeStubs_2`) for portable entry
points. `GetMethodDescFromStubAddress` reads
`PortableEntryPoint.MethodDesc`, matching native
`MethodDesc::GetMethodDescFromPrecode`. The runtime advertises `c2`
under `FEATURE_PORTABLE_ENTRYPOINTS`, and `PrecodeStubs_1` is unchanged.

**Stack walks require the Debugger contract (#135034).** WASM didn't
advertise `Debugger`, but `StackWalk_1` calls it for every
native-context frame.
- WASM now advertises the existing `Debugger` `c1` contract. The
in-process debugger isn't built there, so `g_pDebugger` stays null and
`CLRJitAttachState` stays 0. `Debugger_1` already reports that as "not
initialized": no debugger data, no hijacks.
- The mistyped `int g_pDebugger` linker stub is replaced with correctly
typed definitions.

**Walk never ends on an R2R InlinedCallFrame (#135036).**
`WasmFrameHandler` now handles the `INLINED_PINVOKE_FROM_R2R` marker as
native `InlinedCallFrame::UpdateRegDisplay_Impl` does. SP comes from
`CallSiteSP`, IP is the R2R virtual IP of the shadow frame there, and FP
is that frame's base. The frame pointer doesn't account for funclets
yet; that comes with #133890. If an active InlinedCallFrame's context
still isn't managed code (no virtual IP could be recovered), the walk
fails with `StackWalkState.Error`, as native `NextRaw` returns
`SWA_FAILED`, rather than repeating.

**Interpreter-only walk repeats forever (#135037).** This now matches
native `StackFrameIterator`:
- An active InlinedCallFrame for an interpreted P/Invoke
(`InlinedCallFrame::IsInInterpreter`) moves straight to the
InterpreterFrame that owns it, without touching the context.
- A walk that starts in interpreted code moves the Frame cursor to the
`Next` of the owning InterpreterFrame named in the first-argument
register, as native `Init`/`ResetRegDisp` do. If the register is null or
doesn't name an InterpreterFrame, the walk throws, where native asserts.

The `StackWalk.md`, `PrecodeStubs.md` (new Version 2 section) and
`Debugger.md` specs are updated, and the generated usage tables are
regenerated.

WASM isn't a shipping cDAC scenario for .NET 11, so readers built from
this PR don't support WASM runtimes built before it: those don't
advertise `Debugger` and still advertise `PrecodeStubs` `c1` with
portable entry points.

Follow-up: moving the ExecutionManager portable-entry-point special
cases (`NonVirtualEntry2MethodDesc`,
`GetDiagnosticCodeStartFromEntryPoint`) next to `PrecodeStubs_2`. I've
left that out of this PR to avoid colliding with #133890's
ExecutionManager changes.

## Validation

- `./dotnet.sh test
src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj`:
**passed**, 3245/3245.
- `pwsh src/native/managed/cdac/tools/CdacUsageGraph/generate-docs.ps1
-Check`: **passed**, docs up to date.
- `./build.sh -os browser -c Debug -subset clr.runtime`: **passed**. The
generated WASM contract descriptor advertises `Debugger` `c1` with its
globals and `PrecodeStubs` `c2`.
- `./build.sh -os browser -subset clr+libs+packs -c Release
/p:BuildCrossgen2HostPackForWorkloadTesting=true`: **passed**. These are
the packs used for the live run below.
- New tests:
  - portable entry point lookup through `c2`;
  - the R2R InlinedCallFrame virtual IP;
- a WASM walk ending, with the `Debugger` contract advertised and a null
`g_pDebugger`;
- an interpreted P/Invoke chain walked once from both kinds of starting
point.

The tests from the original fixes each failed with the corresponding fix
removed. I didn't repeat that check after the review changes. The throw
for a missing owning InterpreterFrame has no dedicated test.
- Live browser-wasm runs at `289085e0e06` (before the last review round
removed the older-runtime fallbacks) through Blazor-Playground/nesm,
with nesm's workarounds and its frame guard turned off. Five configs, 5
walks each: R2R paused at a breakpoint (seeded from the frame chain and
from a leaf `$sp`), R2R paused in a `[JSImport]` call, and
interpreter-only paused in a tight loop and in a `[JSImport]` call.
- Against nightly `12.0.0-alpha.1.26480.103`, which advertises no
`Debugger` contract and `PrecodeStubs` `c1`, so the since-removed
fallback paths ran: **all pass**.
- Against Release packs built from this branch, which advertise
`Debugger` `c1` and `PrecodeStubs` `c2`: **all pass**. Frame names,
states and kinds match the nightly run exactly, and nesm's own
`Debugger` and `PrecodeStubs` workarounds are never called.
- In every run, walks complete with no errors and every frame is named.
The R2R names match a separate static ReadyToRun lookup.
- Live re-run at the current head `b996b7187da` with the same branch
packs (the runtime code is unchanged since `289085e0e06`), nesm
workarounds and frame guard off: R2R paused at a breakpoint (all seeds)
and interpreter-only paused in a `[JSImport]` call **pass**, including
the interpreted-code seed that now requires the owning InterpreterFrame.
Against the nightly, the reader fails with `ContractMissingException`
(`Debugger`) unless nesm's workarounds are on, as expected now that
older WASM runtimes aren't supported.

`TestPlaceholderTarget.TryGetThreadContext` now returns `false` (no OS
context, as on WASM) instead of throwing, so tests can exercise the
walk's fallback to the Frame chain.

Resolves #135034
Resolves #135035
Resolves #135036
Resolves #135037

> [!NOTE]
> This PR description was generated with assistance from GitHub Copilot.

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing and others added 15 commits October 2, 2026 16:59
Preserve variable debug information produced by RyuJIT for ReadyToRun WebAssembly code, including scope ranges across relooper block ordering and packed wasm local register locations.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The end-to-end variable-debug-info validation exposed the hidden wasm
portable-entry-pointer argument as a source local. The argument is appended
after user arguments, but unlike the wasm stack-pointer argument it was not
recorded or excluded by compMap2ILvarNum. AddDoubles therefore reported the
hidden i32 argument as source local 0, alongside the real f64 parameters.

Record the argument's local number when it is created, map it to
UNKNOWN_ILNUM, and account for it when mapping later internal locals back to
IL variable numbers.

Replace the count-only wasm R2R checks with complete exact records for the
AddDoubles parameters and SumWithFinally local: variable identity, native
range, location kind, packed wasm local, and frame-pointer-relative offset.
Mutate the local-index bits of one packed register and prove the exact oracle
rejects the corrupted record.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Define the packed WASM debug-register bit layout in ICorDebugInfo and have
the JIT derive its register masks from that shared encoding contract. Assert
that the JIT register representation and WasmValueType count remain
compatible with the debug-info format.

Document that the static ReadyToRun reader's compiled-in shift must move
with a versioned R2R debug-info format change, since it has no live target
descriptor from which to discover a different layout.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Publish the shared WASM register type shift and value-type count through the target data descriptor so version-skewed readers can reject incompatible variable debug information.

Document the producer-owned encoding and extend the static ReadyToRun reader coverage for reserved and unsupported value-type codes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add a no-opt object local that remains live across a GC call in a finally funclet. Assert its IL class type, complete ReadyToRun variable tuples, frame-relative GC slot, and safepoint coverage.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Add exact optimized tracked-variable coverage, frame-base ABI variations including localloc and funclets, and same-type GC slot identity with a legitimate null reference.

Pin the current stack VarLoc base encoding and document that absolute frame reconstruction is independent of unstable wasm local indices.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Jakob Botsch Nielsen <Jakob.botsch.nielsen@gmail.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Encode wasm locals in debug info as the local index biased past the reserved RegNum values instead of packing the JIT value type, matching other targets where the type is implied by the variable. Remove the now-unneeded cDAC descriptor globals.

Wasm is only compiled ahead of time, where every variable scope covers the whole method, so open all scopes in the first emitted block instead of scanning per block.

Update codegen-shape test pins for drift from main.

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

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Replace exact native offsets, frame sizes, local counts, and instruction byte pins with checks of the debug-info contract: register records name a declared wasm local of the expected type, stack records are frame-relative and their slots are accessed, parameters are homed after their local range, ranges do not overlap, and GC locals cover the safepoint. Unrelated JIT codegen changes no longer break the test.

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

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Add an optimized Vector128 method and verify its parameters and local are recorded as wasm v128 locals.

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

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 51b56d7e-45e5-464d-8c2b-78c744680ff8
Keep the WASM stack-walk context's SP, virtual IP, and logical frame pointer
coherent with the native RtlVirtualUnwind and Frame::UpdateRegDisplay paths.

- Use the funclet-aware logical frame pointer for the R2R InlinedCallFrame
  marker, matching native GetWasmFramePointerFromStackPointer; IP/SP and the
  failed-walk policy come from the shared InlinedCallFrame handling.
- Use TransitionBlock's saved R2R SP, including lazy return-address recovery,
  and avoid treating the generic fallback argument area as a shadow frame.
- Copy the full serialized WASM context for software exception frames.
- Detect reverse P/Invoke from GC info so native caller bytes are never probed
  as a possible R2R shadow frame; retain caller SP with IP/FP cleared.
- Expose a documented StackWalk function identity containing the raw shared
  table index, owning module, image runtime-function index, and nullable
  funclet classification. Reject native/interpreter handles before reading
  shadow-frame memory while preserving a known raw index when range lookup
  itself fails.

Add native-layout transition fixtures, real adjacent root/funclet range
coverage, parent/nested/terminator/localloc unwind coverage, and false-frame
reverse-P/Invoke tests.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Decode the WASM register representation produced by ReadyToRun debug info as
symbolic WasmLocal/WasmLocalPair locations. Each register field holds a WASM
local index biased by ICorDebugInfo::WASM_LOCAL_REGNUM_BASE (3); values 0-2
remain REGNUM_PC, REGNUM_COUNT, and REGNUM_AMBIENT_SP and never name a local.
As on other targets the value type is not encoded. Register-only locations
whose register is a reserved pseudo-register are rejected rather than decoded
as a local. Engine-owned locals remain symbolic; stack locations resolve from
the logical frame pointer already maintained by the StackWalk context.

Keep current stack encoding exact: base register 2 means context FP. Preserve
the original biased VarLoc through DacDbi for consumers that need the
ICorDebugInfo representation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Keep engine-private WASM locals unavailable rather than fabricating a zero
value, and distinguish an unreadable VLT_STK_BYREF pointer from a legitimate
stack-homed null reference.

The native DAC retains one logical location when indirect pointer reads fail,
but its subsequent address-zero memory read fails on native platforms. WASM
linear address zero is readable, so carrying that fallback forward can return
successful irrelevant bytes. Preserve the logical location count while making
all address/value/object access report CORDBG_E_READVIRTUAL_FAILURE when the
indirection is unreadable or resolves to null.

Non-WASM dereference behavior remains unchanged. Exact controls cover an
unreadable byref, a null byref pointer, a readable null reference, and a
symbolic engine local with no physical location.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@lewing
lewing force-pushed the lewing-r2r-cdac-for-wasm branch from df98928 to a9f61e7 Compare October 2, 2026 22:18
lewing added a commit that referenced this pull request Oct 2, 2026
Adapted from #133890: add IWasmR2RInfo.TryIsFunclet and
WasmUnwinder.TryGetLogicalFramePointer, mirroring native
GetWasmFramePointerFromStackPointer, and set WasmContext.FramePointer
to the establishing method's frame base after each unwind.

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

This branch has not been deployed

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

Labels

arch-wasm WebAssembly architecture area-Diagnostics-cdac

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant