Repository navigation
Conversation
… caller On WASM a transition helper called from R2R code (for example the portable-entry-point thunk into the interpreter) records the caller's R2R linear-stack pointer in TransitionBlock.m_StackPointer and may leave m_ReturnAddress 0. The cDAC read neither, so a walk leaving an InterpreterFrame reported a native marker at the end of the TransitionBlock and skipped every R2R frame up to the next explicit Frame. Describe TransitionBlock.StackPointer on WASM and mirror native: - FramedMethodFrame::GetTransitionBlock_Impl: a 0 return address is the R2R virtual IP of the frame at m_StackPointer (FrameHelpers.GetReturnAddress). - TransitionFrame::GetSP / UpdateRegDisplay_Impl: with a stack pointer and a return address, the caller's SP is m_StackPointer and its FP that frame's base (WasmFrameHandler.HandleTransitionFrame). Fixes #135137 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
|
Azure Pipelines: Successfully started running 5 pipeline(s). 11 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
Contributor
|
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag |
lewing
requested review from
max-charlamb and
rcj1
and
a balanced review from Copilot
October 2, 2026 22:57
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The recovered frame pointer becomes incorrect for funclets and subsequent R2R callers.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Fixes cDAC WASM stack walks so interpreter transitions resume through ReadyToRun callers.
Changes:
- Describes the WASM transition stack pointer.
- Restores R2R IP/SP/FP state from transition frames.
- Adds documentation and regression coverage.
| File | Description |
|---|---|
StackWalkTests.cs |
Adds WASM transition and stack-walk tests. |
TransitionBlock.cs |
Exposes the optional WASM stack pointer. |
WasmFrameHandler.cs |
Restores R2R context from transitions. |
FrameHelpers.cs |
Derives missing R2R return addresses. |
datadescriptor.inc |
Publishes the WASM field descriptor. |
StackWalk.md |
Documents transition behavior. |
data-descriptor-meanings.json |
Adds the generated field description. |
WasmContext.Unwind advanced SP and IP but left FP alone, so each R2R caller after the first was reported with the previous frame's frame pointer. Native WasmUnwindStackFrame recomputes it from the caller's stack pointer. Use the root-function frame base; the funclet-aware logical frame pointer is not modeled yet. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Model the linear-stack layouts RyuJIT and the VM produce for wasm funclets (genFuncletProlog 16-byte frame, funclet VIP at $sp[4], genCallFinally SP/FP passing, localloc indirection, CallFuncletWith[out]Throwable terminator + establishing FP) and assert SP/IP/logical FP for each frame across WasmContext.Unwind using the real WasmR2RInfo function-table lookup. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
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>
…R frames The R2R InlinedCallFrame and TransitionFrame paths seeded FP with the frame's own base, which is wrong when the R2R frame is a funclet. Use the logical frame pointer, as native GetWasmFramePointerFromStackPointer does, and document the WASM logical frame pointer in the StackWalk spec. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Resolve conflicts with the x86 TransitionFrame changes in FrameHelpers.GetReturnAddress and StackWalk.md; keep both. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This was referenced Oct 8, 2026
Open
This was referenced Oct 8, 2026
Draft
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

On WASM, a cDAC
IStackWalkwalk drops every R2R frame between anInterpreterFrameand the next explicit Frame when R2R code entered the interpreter through a transition helper, such as the portable-entry-point thunkWasmR2RToInterpreterThunk→ExecuteInterpretedMethodWithArgs_PortableEntryPoint. The walk covers the interpreted chain, reportsNativeMarker ip=0 sp=<end of TransitionBlock>, and jumps to the next Frame. The walk still ends and every reported frame is named, so frames go missing without any error.Cause
On WASM such a helper records the R2R caller's linear-stack pointer in
TransitionBlock.m_StackPointer, and may leavem_ReturnAddress0. Native handles that insrc/coreclr/vm/frames.handsrc/coreclr/vm/wasm/helpers.cpp:FramedMethodFrame::GetTransitionBlock_Implfills a 0 return address lazily withGetWasmVirtualIPFromStackPointer(m_StackPointer).TransitionFrame::GetSPreturnsm_StackPointerwhen both fields are set, andUpdateRegDisplay_Impluses those values plus that frame's frame pointer.The cDAC didn't describe
m_StackPointer, andBaseFrameHandler.HandleTransitionFrameused the raw return address (0) and the end of the TransitionBlock.Changes
datadescriptor.inc: describeTransitionBlock.StackPointerunderTARGET_WASM; it's an optional field (Data.TransitionBlock.StackPointer).FrameHelpers.GetTransitionBlockReturnAddress: if the return address is 0 andStackPointeris set, derive the R2R virtual IP from it (WasmUnwinder.GetVirtualIP).GetReturnAddressuses this for all TransitionFrame types.WasmFrameHandler.HandleTransitionFrame: whenStackPointeris set and a return address is known, the caller's SP isStackPointerand its FP is the WASM logical frame pointer there (see below). Otherwise the SP is the end of the TransitionBlock and the FP is null. The same path runs when the interpreted chain under anInterpreterFrameis exhausted (InterpreterVirtualUnwind→ApplyInterpreterFrameTransition).WasmContext.Unwind: recompute the frame pointer from each caller's stack pointer, as nativeWasmUnwindStackFramedoes. Before, every R2R caller after the first kept the previous frame's frame pointer.GetWasmFramePointerFromStackPointer, adapted from [cDAC][wasm] Decode R2R variable locations and expose function identity #133890.IWasmR2RInfo.TryIsFuncletreads theRUNTIME_FUNCTIONfunclet bit.WasmUnwinder.TryGetLogicalFramePointerreturns a method's own frame base. For a funclet, it unwinds out to the establishing frame: either the parent method or funclet, or the frame pointer stored beside theTERMINATE_R2R_STACK_WALKmarker when the VM invoked the funclet throughCallFuncletWith[out]Throwable. Each step must move toward the caller.WasmContext.Unwindand both R2R seeding paths (theInlinedCallFramemarker andHandleTransitionFrame) use it.StackWalk.mdTransitionFrame section updated, and the generated tables regenerated.#133890 carries an overlapping version of this fix and the logical frame pointer, and will drop its copy when it restacks.
Validation
./dotnet.sh test src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj: passed, 3266/3266.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 descriptor lays outTransitionBlockasReturnAddress@0,StackPointer@4, size 8.New tests:
UpdateContextFromFrame_WasmTransitionFrame_MirrorsNativeTransitionBlock: four cases, return address and stack pointer each set or 0.CreateStackWalk_WasmInterpreterFrameEnteredFromR2R_ContinuesIntoR2RCaller: the walk continues from an exhausted interpreted chain into two R2R callers, checking each one's IP, SP and frame pointer.WasmUnwinderTests.Unwind_ProducerLayout_*(six cases): funclet linear-stack layouts taken from the JIT and runtime producers. They cover a finally called by its parent (with and without localloc), a funclet invoked by the VM (catch, and filter with the throwing frames still live), and nested finallys. Each uses real R2R function-table data and checks SP, IP and FP for every frame throughWasmContext.Unwind.UpdateContextFromFrame_WasmFuncletCaller_ReportsEstablishingFramePointer: both R2R seeding paths report the establishing frame pointer for a VM-invoked funclet.Each new test fails with its fix removed:
WasmContext.UnwindFP change reverted: the walk test fails on the outer caller's frame pointer.main@25c3eaec650).Live browser-wasm validation through Blazor-Playground/nesm, with nesm's workarounds and frame guard off: Release runtime packs built from
9977dd27dc2(descriptorTransitionBlock.StackPointer@4), with the reader atdd4ec1e0b6eand again at30047665a62(funclet commits included; no reader-attributable differences; only interpreter bytecode addresses and async-pause depth vary between runs).$sp(21 frames). The 8 R2R CoreLib frames the issue reported missing (IntrinsicInvokeHelper.InvokeEmitted…<BindManagedFunction>b__0) now appear between the interpreted wrapper and the nextInterpreterFrame, in wasm-stack order. Their names match a separate static ReadyToRun lookup. TheNativeMarker ip=0 sp=<TransitionBlock end>is gone.[JSImport]call (23 frames), and interpreter-only paused in a tight loop (18) and in a[JSImport]call (22): pass. Every walk completes with no errors.30047665a62: an R2R app breaking inside each funclet layout: finally called by its parent (with and without localloc), catch and finally invoked by the VM, a finally nested in a VM-invoked catch, and a filter. Both seeds complete in every scenario. Each unwound funclet frame reports its establishing frame's frame pointer, and the localloc parent reports FP ≠ SP from its indirect slot. Finallys called by their parent were markedNoOptimization, because otherwise the JIT clones them and no funclet runs. This checks consistency with the parent frame's frame pointer, not a direct comparison with native.Not covered: a leaf funclet that makes no calls has no frame record. Neither native nor the cDAC can walk it, so it's only visible at a debugger pause inside it. Open #134608 may change that layout. Starting a walk inside a funclet still takes the leaf frame pointer from the frame base, tracked in #135147.
Resolves #135137
Note
This PR description was generated with assistance from GitHub Copilot.