Skip to content

[cDAC][wasm] Stop R2R unwinding at reverse P/Invoke frames and restore exception-frame FP - #135425

Draft
lewing wants to merge 3 commits into
dotnet:lewing-cdac-wasm-stackwalk-fixesfrom
lewing:lewing-cdac-wasm-reverse-pinvoke-unwind
Draft

lewing wants to merge 3 commits into
dotnet:lewing-cdac-wasm-stackwalk-fixesfrom
lewing:lewing-cdac-wasm-reverse-pinvoke-unwind

Conversation

@lewing

@lewing lewing commented Oct 8, 2026

Copy link
Copy Markdown
Member

Stacked on #135140 (base lewing-cdac-wasm-stackwalk-fixes @ 6d044c16421). This PR closes stack-walk gaps that #135140 leaves out of scope.

Changes

Reverse P/Invoke boundary. Native RtlVirtualUnwind (src/coreclr/vm/wasm/helpers.cpp) sets callerIsNative when the frame being unwound has a reverse P/Invoke frame in its GC info and is not a funclet. WasmUnwindStackFrameCore then reports IP 0 instead of reading the native caller's stack as an R2R frame. The cDAC had no such check, so unwinding out of an UnmanagedCallersOnly R2R method could make up a managed caller from native stack bytes.

  • WasmUnwinder.TryUnwindOneFrame takes the current control PC and mirrors this check.
  • Like native, it now always advances SP to the caller's SP and reports a null IP when the caller isn't R2R code (an interpreter transition, native code, or the stack top). Previously it nulled SP and returned false, which lost the caller's stack position.
  • WasmContext.Unwind recomputes FP only for an R2R caller, as native does.
  • GCInfoHeader.HasReversePInvokeFrame is new and set by both the common decoder and the x86 decoder (InfoHdr.RevPInvokeOffset).

SoftwareExceptionFrame FP. On WASM the callee-saved register set is InterpreterFP (ENUM_CALLEE_SAVED_REGISTERS in vm/wasm/cgencpu.h), so native SoftwareExceptionFrame::UpdateRegDisplay_Impl restores FP along with IP and SP. The cDAC base handler copies only the registers listed in the CalleeSavedRegisters descriptor, which is empty on WASM, so FP was left over from the previous frame. WasmFrameHandler now restores all three from the saved context.

Inactive InlinedCallFrame. Native returns before any update when the frame has no active call. WasmFrameHandler checked only after it had already written the interpreter-frame stash.

StackWalk.md and GCInfo.md are updated.

Not included

Validation

  • ./.dotnet/dotnet test src/native/managed/cdac/tests/UnitTests/Microsoft.Diagnostics.DataContractReader.Tests.csproj: passed, 3312/3312.
  • ./build.sh -s tools.cdactests -test: passed (UnitTests, DataGeneratorTests, UsageTests).
  • pwsh src/native/managed/cdac/tools/CdacUsageGraph/generate-docs.ps1 -Check: passed.
  • New MockTarget tests:
    • TryUnwindOneFrame_CallerWithoutVirtualIp_PreservesCallerStackPointer
    • TryUnwindOneFrame_ReversePInvokeFrame_DoesNotReadNativeCallerAsR2RFrame: reverse P/Invoke, funclet with reverse P/Invoke GC info, and ordinary method
    • Unwind_ReversePInvokeFrame_KeepsCallerStackPointerAndClearsInstructionAndFramePointers
    • GCInfoHeader_ReportsReversePInvokeFrame: hand-encoded WASM fat header and x86 header, with and without the frame
    • UpdateContextFromFrame_WasmSoftwareExceptionFrame_RestoresFramePointer
    • UpdateContextFromFrame_WasmInactiveInlinedCallFrameOverInterpreterFrame_LeavesContextUnchanged
  • Each mutation below was confirmed applied by diff, then failed the tests on exact values, then was restored:
Mutation Fails
Drop the reverse P/Invoke check reverse-P/Invoke theory case and the Unwind boundary test
Drop the funclet exclusion funclet theory case
Restore "null SP on non-R2R caller" 3 tests
Compute FP without an R2R caller Unwind boundary test
x86 HasReversePInvokeFrame always false x86 header case
Common decoder always false WASM header case
Remove the SoftwareExceptionFrame override expected FP 0x41040, got the stale 0x70000
Remove the inactive-ICF early return stash expected 0, got 0x70000

Note

This PR description was generated with GitHub Copilot.

lewing and others added 2 commits October 8, 2026 17:12
Mirror native RtlVirtualUnwind and WasmUnwindStackFrameCore when unwinding a
WASM ReadyToRun frame:

- Always advance SP to the caller's stack pointer. When the caller is not R2R
  code, report a null IP and FP instead of nulling SP, so the walk continues
  through the Frame chain from the right stack position.
- When the frame being unwound has a reverse P/Invoke frame in its GC info and
  is not a funclet, its caller is native code. Report no R2R caller rather than
  reading the native caller's stack bytes as an R2R frame record.

Expose GCInfoHeader.HasReversePInvokeFrame from both the common decoder and the
x86 decoder.

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

Mirror native SoftwareExceptionFrame::UpdateRegDisplay_Impl and
InlinedCallFrame::UpdateRegDisplay_Impl on WASM:

- WASM's callee-saved register set is InterpreterFP, so restore IP, SP, and FP
  from a SoftwareExceptionFrame's saved context. The CalleeSavedRegisters data
  descriptor is empty on WASM, so the base handler left FP from the previous
  frame.
- Return before any update for an inactive InlinedCallFrame, including the
  interpreter-frame stash, as native does.

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

Copy link
Copy Markdown
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.

@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.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant