Skip to content

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

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

lewing wants to merge 14 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 packed wasm register locations as symbolic WasmLocal(Index, WasmDebugValueType) / 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 packed ICorDebugInfo::VarLoc data through DacDbi.
  • Distinguish engine-private locals, unreadable/null byref indirections, and readable stack-homed null references.

Temporary stack

Important

This draft depends on both #133917 and #133086. Their head branches live in the lewing/runtime fork, so this upstream PR cannot target either branch directly and temporarily contains both lower layers. After those PRs merge, their commits will disappear when this branch is rebased onto main.

Lower-layer heads:

The temporary history is exactly:

  1. Five [cDAC][wasm] Resolve ReadyToRun virtual IP ranges #133917 virtual-IP commits through 443fb559f47.
  2. Six [wasm] Preserve R2R variable debug information #133086 producer commits through replayed a7a349ecf4d.
  3. Three consumer commits:
    • 4aac9657b3c[cdac][wasm] Maintain frame context and expose function identity
    • 657f254f921[cdac][wasm] Decode ReadyToRun variable locations
    • 7e72c0e0d16[cdac][wasm] Handle unavailable variable values

Producer constants, encoding globals, optimized-record tests, GC specimens, and stack-base invariants remain owned by #133086. Virtual-IP descriptor/traversal and filter-funclet classification remain owned by #133917.

Corrected virtual-IP dependency

#133917 fixes the live runtime model that #130988's mock test did not represent: WASM ReadyToRun virtual IPs are registered in ExecutionManager::s_pVirtualIPRangeList, not RangeSectionMap.

This PR consumes that shared correction rather than adding a parallel lookup. The integrated reader:

  • resolves real virtual IP ranges before ordinary RangeSectionMap lookup;
  • masks the WASM funclet bit for runtime-function ordering/address arithmetic while retaining funclet identity;
  • keeps synthetic virtual entrypoint bases separate from loaded-image bases used for RVA reads;
  • uses the actual WASM descriptor shape;
  • resolves an executable FilterOffset to its containing runtime function before comparing the masked funclet start.

The last point reflects corrected #133917 head 443fb559f47. A filter entry may be inside the filter runtime function rather than exactly at its start; raw FilterOffset == funcletStart equality is intentionally not used.

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 derives IP/FP from CallSiteSP rather than treating marker 1 as an IP.
  • 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.

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

RyuJIT packs a local index and JIT debug value type into the 32-bit RegNum payload. cDAC receives the encoding from target globals:

  • WasmDebugRegisterTypeShift
  • WasmDebugValueTypeCount

A non-empty WASM Vars stream with missing, mismatched, impossible, or unknown encoding metadata fails explicitly. Invalid packed registers do not fall back to native register locations.

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.

WasmDebugValueType is JIT debug-encoding vocabulary, not the full stable WebAssembly specification type set. Managed references currently use the JIT's machine I32/I64 representation.

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.

Producer validation from #133086

The producer now has exact MinOpts and FullOpts coverage. This is not a minopts-only feature, and optimized-away locals are explicitly absent rather than required to appear.

Representative exact records include:

GcSlotIdentity
local0 [0x36,0x2B1) VLT_STK base=2 offset=0x48  GcMarker(Value=17)
local1 [0x36,0x2B1) VLT_STK base=2 offset=0x44  GcMarker(Value=29)
local2 [0x36,0x2B1) VLT_STK base=2 offset=0x40  legitimate null
local3 [0x36,0x2B1) VLT_STK base=2 offset=0x3C

The reference slots are producer-verified GC_FRAMEREG_REL, pinned/untracked roots across an actual GC. Their common method-wide range is a documented current producer limitation; this PR does not claim per-local range-end fidelity.

Existing live validation

The previous live non-filter validations remain applicable to the equivalent consumer behavior:

Sample.Test.PrintMeaning
module-local function index 6
WasmLocal(Index=1, I32)
live V8 $var1: i32, nonzero

A no-opt SumWithFinally finally-funclet pause also demonstrated parent-FP stack resolution and a known value read from FP + offset.

These runs did not exercise filter-funclet classification. The corrected 443fb559f47 filter behavior is current through focused cDAC tests, including the non-equal filter-entry case. No claim is made that the old full-stack live artifact validates that corrected filter path.

Live two-marker/null C2 validation remains a follow-up on the coherent combined reader/runtime artifact; the producer descriptor and root-slot oracle are now fixed by #133086.

Tests after corrected restack

  • Exact filter-funclet test group: 3 passed, 0 failed.
  • Focused corrected integration group: 67 passed, 0 failed.
  • cDAC UnitTests: 3,231 passed, 0 failed, 0 skipped.
  • tools.cdactests:
    • UnitTests: passed;
    • DataGeneratorTests: passed;
    • UsageTests/generated documentation: passed.
  • Generated StackWalk.md usage section was refreshed and verified current.
  • Post-restack code review found one missing null-IP boundary test/documentation issue; both were added before the final green run.

Existing mutation evidence includes packed-register mismatch, unavailable-value regression, frame-kind identity gating, stale funclet FP, C3 DereferenceOrZero fallback, and #133917's filter-entry raw-equality mutation.

Limitations and readiness

The corrected lower-layer integration is complete, but this PR remains draft pending lower-PR readiness and Larry's decision. It is not being marked ready by this update.

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
Expose WebAssembly ReadyToRun virtual IP ranges through the runtime data descriptor and resolve them before the RangeSectionMap. Mask funclet flags and keep virtual code identity separate from loaded-image RVA reads.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
lewing and others added 2 commits September 14, 2026 19:25
Handle feature-gated hot/cold metadata, isolate candidate module validation during registration, and remove the unsupported virtual-IP list length limit. Match WASM descriptor layouts and cover long lists, partial registration, and root/funclet image metadata.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Allow up to 65,536 nodes while retaining full-list ambiguity and cycle checks. Reject an over-budget chain before reading its next node, even after a match. Document the budget as reader policy and cover exact-budget success, budget+1 rejection, and the read boundary with a compact fixture.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@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 and others added 11 commits September 15, 2026 13:41
Cover the shared native and cDAC invariant that a filter clause offset equals the flagged funclet start relative to its controlling method.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Map the executable filter entry to its containing runtime function before comparing funclet starts, matching native WASM behavior while preserving non-WASM offset comparisons.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
Keep the WASM stack-walk context's SP, virtual IP, and logical frame pointer
coherent with the native RtlVirtualUnwind and Frame::UpdateRegDisplay paths.

- Derive IP and logical FP for the R2R InlinedCallFrame marker.
- 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 with an encoding-specific value
type. Engine-owned locals remain symbolic; stack locations resolve from the
logical frame pointer already maintained by the StackWalk context.

Advertise the register type shift and supported value count as WASM cDAC data
descriptor globals. Require and validate those values before reading a
non-empty variable stream so a reader/target format mismatch fails explicitly
instead of plausibly decoding the wrong local or type. Reject malformed packed
registers and impossible shift/count combinations.

Keep current stack encoding exact: base register 2 means context FP. Remove
speculative decoded stack-base locals whose values the reader could not honor.
Preserve the original packed 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 69ad34f to 7e72c0e Compare September 16, 2026 01:48
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