From 1d83d2d4183f909e1aeb08f58acf013818da2f86 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 16 Jul 2026 16:12:27 -0500 Subject: [PATCH 1/2] [wasm] Publish __stack_pointer before SuppressGCTransition native calls 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. --- src/coreclr/jit/codegenwasm.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index d5860d5495ad66..94b783792a5de2 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2906,6 +2906,21 @@ void CodeGen::genCallInstruction(GenTreeCall* call) params.wasmSignature = m_compiler->info.compCompHnd->getWasmTypeSymbol(typeStack.Data(), typeStack.Height()); + // R2R keeps its shadow SP in a local and leaves the __stack_pointer global stale; the PInvoke + // prolog (JIT_PInvokeBegin) normally publishes the current SP to __stack_pointer before native + // code runs, but that prolog/epilog is skipped for SuppressGCTransition calls (see Lowering). + // Without a publish, the native SuppressGCTransition callee allocates its shadow frame from the + // stale global (our caller's SP, above our frame) and overlaps/clobbers our address-taken locals. + // Publish our shadow SP here so the callee allocates below our frame. This is a net-zero operation + // on the Wasm operand stack, so it is safe to emit with the call arguments already pushed. + if (call->IsUnmanaged() && call->IsSuppressGCTransition() && + (GetStackPointerReg(m_compiler->funCurrentFuncIdx()) != REG_NA)) + { + GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetStackPointerRegIndex()); + GetEmitter()->emitIns_I(INS_global_set, EA_HANDLE_CNS_RELOC, + (cnsval_ssize_t)(size_t)m_compiler->eeGetWasmWellKnownGlobals()->stackPointer); + } + // A non-null target expression always indicates an indirect call on Wasm, // as currently the only possible result of the target expression would be a // table index which must be used via call_indirect From 7c75d2509c68a986cd604a7bb523d5cd101ec91b Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Thu, 16 Jul 2026 17:17:44 -0500 Subject: [PATCH 2/2] Drop redundant stack-pointer REG_NA guard 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. --- src/coreclr/jit/codegenwasm.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/coreclr/jit/codegenwasm.cpp b/src/coreclr/jit/codegenwasm.cpp index 94b783792a5de2..14ce670550c2e7 100644 --- a/src/coreclr/jit/codegenwasm.cpp +++ b/src/coreclr/jit/codegenwasm.cpp @@ -2913,8 +2913,7 @@ void CodeGen::genCallInstruction(GenTreeCall* call) // stale global (our caller's SP, above our frame) and overlaps/clobbers our address-taken locals. // Publish our shadow SP here so the callee allocates below our frame. This is a net-zero operation // on the Wasm operand stack, so it is safe to emit with the call arguments already pushed. - if (call->IsUnmanaged() && call->IsSuppressGCTransition() && - (GetStackPointerReg(m_compiler->funCurrentFuncIdx()) != REG_NA)) + if (call->IsUnmanaged() && call->IsSuppressGCTransition()) { GetEmitter()->emitIns_I(INS_local_get, EA_PTRSIZE, GetStackPointerRegIndex()); GetEmitter()->emitIns_I(INS_global_set, EA_HANDLE_CNS_RELOC,