[wasm] Publish __stack_pointer before SuppressGCTransition native calls#130924
Open
lewing wants to merge 2 commits into
Open
[wasm] Publish __stack_pointer before SuppressGCTransition native calls#130924lewing wants to merge 2 commits into
lewing wants to merge 2 commits into
Conversation
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.
|
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. |
Member
Author
|
cc @dotnet/wasm-contrib |
Contributor
There was a problem hiding this comment.
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 beforeIsUnmanaged() && IsSuppressGCTransition()calls when the method has a shadow SP local. - Use the existing
eeGetWasmWellKnownGlobals()->stackPointerhandle (viaWASM_GLOBAL_INDEX_LEBrelocation) to reference the well-known global consistently with existing wasm codegen.
Contributor
|
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara |
AndyAyersMS
reviewed
Jul 16, 2026
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.
AndyAyersMS
approved these changes
Jul 16, 2026
lewing
marked this pull request as ready for review
July 16, 2026 22:27
lewing
enabled auto-merge (squash)
July 16, 2026 22:27
|
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. |
Open
3 tasks
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.
Summary
On
FEATURE_PORTABLE_ENTRYPOINTS(wasm) R2R, generated code keeps its shadow stack pointer in a local and leaves the__stack_pointerglobal stale. The P/Invoke prolog (JIT_PInvokeBegin) normally publishes it before native code runs, but that prolog is skipped forSuppressGCTransitioncalls. A nativeSuppressGCTransitioncallee (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:
FindFirstPassHandlerspills its by-refStackFrameIterator, calls theSuppressGCTransitionQCallRhpEHEnumInitFromStackFrameIterator, the callee clobbers the spilled pointer, and the nextframeIter.ControlPCread faults with a spuriousNullReferenceException. EH dispatch can't GC-transition mid-unwind, which is why its QCalls areSuppressGCTransitionand hit this path.Fixes #130923.
Fix
Publish the shadow SP to the
__stack_pointerglobal 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. ThestackPointerglobal handle comes from thegetWasmWellKnownGlobalsJIT-EE API (added in #129717) and is referenced via aWASM_GLOBAL_INDEX_LEBrelocation, matching the existingglobal.getuses 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
mainyet. Verifiedcodegenwasm.cppcompiles 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
mainuntil the R2R-on-wasm consumption path lands.getWasmWellKnownGlobalsAPI from [WASM] Use relocs for tableBase / imageBase / stackPointer globals #129717 (fixes [RyuJit Wasm] Emit relocations for WebCIL globals rather than hardcoding global table indices #129712), now onmain.function signature mismatchon the first (cold) R2R call to a method whose native code is not yet on its portable entry point #130634.Note
This pull request was authored with the assistance of GitHub Copilot.