Skip to content

[wasm] Publish __stack_pointer before SuppressGCTransition native calls#130924

Open
lewing wants to merge 2 commits into
dotnet:mainfrom
lewing:wasm-r2r-eh-suppressgc-sp
Open

[wasm] Publish __stack_pointer before SuppressGCTransition native calls#130924
lewing wants to merge 2 commits into
dotnet:mainfrom
lewing:wasm-r2r-eh-suppressgc-sp

Conversation

@lewing

@lewing lewing commented Jul 16, 2026

Copy link
Copy Markdown
Member

Summary

On FEATURE_PORTABLE_ENTRYPOINTS (wasm) R2R, generated code keeps its shadow stack pointer in a local and leaves the __stack_pointer global stale. The P/Invoke prolog (JIT_PInvokeBegin) normally publishes it before native code runs, but that prolog is skipped for SuppressGCTransition calls. A native SuppressGCTransition callee (emscripten, which uses __stack_pointer) then allocates its shadow frame from the stale global — the caller's SP, above the R2R frame — and clobbers the caller's address-taken locals.

This manifests in R2R exception handling: FindFirstPassHandler spills its by-ref StackFrameIterator, calls the SuppressGCTransition QCall RhpEHEnumInitFromStackFrameIterator, the callee clobbers the spilled pointer, and the next frameIter.ControlPC read faults with a spurious NullReferenceException. EH dispatch can't GC-transition mid-unwind, which is why its QCalls are SuppressGCTransition and hit this path.

Fixes #130923.

Fix

Publish the shadow SP to the __stack_pointer global just before the call, so the callee allocates its shadow frame below the current frame. The publish is net-zero on the wasm operand stack, so it is safe to emit after the call arguments are pushed. The stackPointer global handle comes from the getWasmWellKnownGlobals JIT-EE API (added in #129717) and is referenced via a WASM_GLOBAL_INDEX_LEB relocation, matching the existing global.get uses in wasm codegen.

Validation

The bug only reproduces with the (not-yet-merged) R2R-on-wasm bring-up stack, so there is no wasm-R2R CI leg on main yet. Verified codegenwasm.cpp compiles clean in the wasm JIT (clr.jit, 0 errors). Behaviorally validated on the R2R-on-wasm prototype: unfixed crashes, fixed passes, plus a byte-identical R2R↔interpreter battery.

Notes for reviewers

Note

This pull request was authored with the assistance of GitHub Copilot.

On FEATURE_PORTABLE_ENTRYPOINTS (wasm) R2R, generated code keeps its shadow
stack pointer in a local and leaves the __stack_pointer global stale. The value
is normally published to the global by the P/Invoke prolog (JIT_PInvokeBegin)
before native code runs, but that prolog/epilog is skipped for
SuppressGCTransition calls. Without a publish, a native SuppressGCTransition
callee (emscripten C/C++, which uses the __stack_pointer global) allocates its
shadow frame from the stale global -- the caller's SP, above the R2R frame --
and overlaps/clobbers the R2R caller's address-taken locals.

This manifests in R2R exception handling: FindFirstPassHandler spills its by-ref
StackFrameIterator to its frame and calls the SuppressGCTransition QCall
RhpEHEnumInitFromStackFrameIterator; the host callee clobbers the spilled
pointer, and the subsequent frameIter.ControlPC read faults with a spurious
NullReferenceException. EH dispatch cannot do a GC transition mid-unwind, which
is why its QCalls are SuppressGCTransition and hit this path.

Publish the shadow SP to the __stack_pointer global just before the call, so the
callee allocates its shadow frame below the current frame. The publish is a
net-zero operation on the wasm operand stack, so it is safe to emit after the
call arguments are already pushed. The stackPointer global handle comes from the
getWasmWellKnownGlobals JIT-EE API and is referenced via a WASM_GLOBAL_INDEX_LEB
relocation, matching the existing global.get uses in wasm codegen.
Copilot AI review requested due to automatic review settings July 16, 2026 21:13
@github-actions github-actions Bot added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 16, 2026
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@lewing

lewing commented Jul 16, 2026

Copy link
Copy Markdown
Member Author

cc @dotnet/wasm-contrib

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 updates the WASM JIT call sequence to publish the current shadow stack pointer to the __stack_pointer global immediately before SuppressGCTransition unmanaged calls, ensuring native callees that rely on __stack_pointer observe the correct stack position.

Changes:

  • Emit local.get <shadowSP> + global.set <__stack_pointer> just before IsUnmanaged() && IsSuppressGCTransition() calls when the method has a shadow SP local.
  • Use the existing eeGetWasmWellKnownGlobals()->stackPointer handle (via WASM_GLOBAL_INDEX_LEB relocation) to reference the well-known global consistently with existing wasm codegen.

@lewing lewing added the arch-wasm WebAssembly architecture label Jul 16, 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.

Comment thread src/coreclr/jit/codegenwasm.cpp Outdated
The wasm register allocator unconditionally allocates the shadow stack
pointer for every funclet (IdentifyCandidates -> InitializeStackPointer ->
AllocateStackPointer), so GetStackPointerReg is never REG_NA in codegen and
GetStackPointerRegIndex already asserts it. Drop the guard to match the other
shadow-SP emit sites, per review feedback.
Copilot AI review requested due to automatic review settings July 16, 2026 22:18

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

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.

@lewing
lewing marked this pull request as ready for review July 16, 2026 22:27
@lewing
lewing enabled auto-merge (squash) July 16, 2026 22:27
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 5 pipeline(s).
10 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

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

Labels

arch-wasm WebAssembly architecture area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

3 participants