Skip to content

Fix wasm EH resume for handlers nested inside VM-invoked funclets#130089

Merged
davidwrighton merged 4 commits into
dotnet:mainfrom
davidwrighton:FixWasmFuncletEHNestedResume
Jul 1, 2026
Merged

Fix wasm EH resume for handlers nested inside VM-invoked funclets#130089
davidwrighton merged 4 commits into
dotnet:mainfrom
davidwrighton:FixWasmFuncletEHNestedResume

Conversation

@davidwrighton

Copy link
Copy Markdown
Member

Summary

Fixes wasm exception-handling resume for a handler that is lexically nested inside a funclet (e.g. a catch inside a finally) when the runtime is R2R-compiled and the funclet was invoked by the VM via EECodeManager::CallFunclet.

This PR contains three commits:

  1. Fix wasm EH resume for handlers nested inside funclets (@AndyAyersMS) — cherry-picked from fix-wasm-funclet-eh-resume. When invoking a funclet whose handler is lexically nested inside another funclet, it natively unwinds out of the funclet frames to recover the enclosing method's establishing frame.

  2. Add EH test interleaving interpreter and R2R funclets — a Methodical EH test (InterpR2RFunclets) that throws out of an interpreted method (forced via a test-local BypassReadyToRun attribute) and unwinds through compiled funclets, including a finally funclet that itself catches an exception thrown by interpreted code. Registered in the eh-bearing merged variants (_d1, _do, _r1, _ro).

  3. Fix wasm funclet establishing frame for handlers nested in VM-invoked funclets — corrects a defect in commit 1.

The defect being fixed

There are two ways a funclet can be reached:

  • Inline within the method's own native frame — Andy's native unwind loop correctly lands on the method frame and GetWasmFramePointerFromStackPointer yields the right establishing frame.
  • Via CallFuncletWith[out]Throwable — these helpers push a synthetic 16-byte frame marked with TERMINATE_R2R_STACK_WALK. Native unwinding terminates at that synthetic frame (EECodeInfo::IsValid() becomes false) before reaching the method frame, so calling GetWasmFramePointerFromStackPointer on that SP produces a bogus establishing frame.

The fix

  • CallFuncletWith[out]Throwable now store the establishing (method) frame pointer immediately after the TERMINATE_R2R_STACK_WALK marker (at TERMINATE_R2R_STACK_WALK_FP_OFFSET).
  • When the unwind loop lands on the terminator (!ci.IsValid()), the establishing frame is recovered via the new GetWasmEstablishingFramePointerFromTerminator helper instead of GetWasmFramePointerFromStackPointer. The inline case (ci.IsValid() && !ci.IsFunclet()) keeps the original behavior.

Validation

Verified on the standalone form of the test under wasm R2R (crossgen2):

Mode Before fix After fix
R2R (crossgen2) Actual: 1 (unhandled exception escapes) Actual: 100 (PASS)
Interpreted Actual: 100 Actual: 100 (PASS)

A temporary printf confirmed the new GetWasmEstablishingFramePointerFromTerminator code path is the one exercised by the nested-funclet scenario.

Known limitation

The test is included in the merged Methodical assemblies rather than as a process-isolation test (merged tests are generally preferred). Crossgen2 currently emits an invalid R2R wasm image for the merged Methodical assembly due to an unrelated codegen issue, so under R2R the merged image is discarded and the assembly runs interpreted — meaning the merged test does not yet exercise the R2R funclet path. That unrelated invalid-R2R-method issue will be addressed separately; the standalone validation above confirms the fix.

Note

This pull request was authored with assistance from GitHub Copilot.

AndyAyersMS and others added 3 commits June 30, 2026 17:30
On wasm, a handler lexically nested inside a funclet (e.g. a catch inside
a finally) was run with the funclet's own frame as its establishing frame
instead of the enclosing method frame, so it could not see method locals
and wrote its resume IP into the wrong frame. Walk up to the method frame.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds a Methodical EH test that throws out of an interpreted method (forced via
a test-local BypassReadyToRun attribute) and unwinds through compiled funclets,
including a finally funclet that itself catches an exception thrown by
interpreted code. Exercises the interpreter/compiled boundary repeatedly during
a single exception dispatch.

Registered in the eh-bearing merged variants (_d1, _do, _r1, _ro).

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

Andy's fix recovers the establishing (method) frame for a handler that is
lexically nested inside a funclet by natively unwinding until it leaves the
funclet frames. That works when the funclet runs inline within the method's
own native frame, but not when the VM invoked the funclet through
CallFuncletWith[out]Throwable: native unwinding terminates at the synthetic
frame those helpers push (marked with TERMINATE_R2R_STACK_WALK) before ever
reaching the method frame, so calling GetWasmFramePointerFromStackPointer on
that SP yields a bogus establishing frame.

Have CallFuncletWith[out]Throwable store the establishing frame pointer right
after the TERMINATE_R2R_STACK_WALK marker (at TERMINATE_R2R_STACK_WALK_FP_OFFSET),
and, when the unwind loop lands on that terminator (ci.IsValid() == false),
recover the frame via GetWasmEstablishingFramePointerFromTerminator instead of
GetWasmFramePointerFromStackPointer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/coreclr/vm/wasm/helpers.cpp Outdated
Comment thread src/tests/JIT/Methodical/eh/interp/interpr2rfunclets.cs Outdated

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 fixes WebAssembly exception-handling resume when an EH handler is lexically nested inside a VM-invoked funclet (e.g., catch inside a finally) by ensuring the correct establishing (method) frame pointer is available when native unwinding terminates at a synthetic “terminate R2R stack walk” marker frame.

Changes:

  • Persist the establishing frame pointer in the synthetic terminator frame created by CallFuncletWith[out]Throwable, and add a helper to recover it.
  • Update EECodeManager::CallFunclet (wasm) to unwind out of nested funclet frames and choose the right establishing frame source depending on whether unwinding ends at the terminator or reaches the method frame.
  • Add a Methodical EH test (InterpR2RFunclets) and register it in merged Methodical variants.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/coreclr/vm/wasm/helpers.cpp Stores establishing FP next to TERMINATE_R2R_STACK_WALK and adds GetWasmEstablishingFramePointerFromTerminator.
src/coreclr/vm/wasm/callhelpers.hpp Defines TERMINATE_R2R_STACK_WALK_FP_OFFSET and documents the synthetic frame layout.
src/coreclr/vm/eetwain.cpp Adjusts wasm EECodeManager::CallFunclet establishing-frame selection for handlers nested in funclets.
src/tests/JIT/Methodical/eh/interp/interpr2rfunclets.cs Adds EH test interleaving interpreted throws with compiled funclets and nested handler behavior.
src/tests/JIT/Methodical/Methodical_ro.csproj Includes the new Methodical test in the _ro merged variant.
src/tests/JIT/Methodical/Methodical_r1.csproj Includes the new Methodical test in the _r1 merged variant.
src/tests/JIT/Methodical/Methodical_do.csproj Includes the new Methodical test in the _do merged variant.
src/tests/JIT/Methodical/Methodical_d1.csproj Includes the new Methodical test in the _d1 merged variant.

Comment thread src/tests/JIT/Methodical/eh/interp/interpr2rfunclets.cs
- Move the test-local BypassReadyToRunAttribute out of interpr2rfunclets.cs into
  its own file (src/tests/JIT/Methodical/BypassReadyToRunAttribute.cs) so it can be
  shared by other Methodical tests, and register it in the merged _d1/_do/_r1/_ro
  csproj files.
- Remove a stray blank line in wasm/helpers.cpp.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/coreclr/vm/eetwain.cpp

@AndyAyersMS AndyAyersMS left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, just wondering if there are cases where finding the method frame might be more involved...

@davidwrighton

Copy link
Copy Markdown
Member Author

/ba-g unrelated infra failures.

@davidwrighton davidwrighton merged commit 1bce750 into dotnet:main Jul 1, 2026
127 of 133 checks passed
@pavelsavara pavelsavara added the arch-wasm WebAssembly architecture label Jul 2, 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.

@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-preview7 milestone Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

arch-wasm WebAssembly architecture area-VM-coreclr

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants