Description
On WASM, an active InlinedCallFrame pushed by R2R code stores the sentinel INLINED_PINVOKE_FROM_R2R (1, src/coreclr/vm/frames.h) in m_pCallerReturnAddress. Native InlinedCallFrame::UpdateRegDisplay_Impl (src/coreclr/vm/wasm/helpers.cpp) treats that as a marker: it sets SP from CallSiteSP and derives IP via GetWasmVirtualIPFromStackPointer(CallSiteSP).
On main, the cDAC's WasmFrameHandler.HandleInlinedCallFrame calls BaseFrameHandler.HandleInlinedCallFrame, which copies CallerReturnAddress verbatim. The context IP becomes 0x1, which is not managed code, so in StackWalk_1.Next():
case StackWalkState.Frame: isActiveICF is true (return address ≠ 0), so FrameIter.Next() is not called.
UpdateState: IP 0x1 is not managed → State = Frame again, on the same frame.
The IEnumerable never terminates. It keeps yielding the same Frame (same frame address, ip 0x1, same SP) forever. A consumer that enumerates without a cap hangs.
On a live browser-wasm target paused during a [JSExport] call, this triggered on the very first walk. Seeding with CreateStackWalk(threadData) gives InitialNativeContext from the InlinedCallFrame, then an endless run of Frame [InlinedCallFrame] ip=0x1 sp=0x31B5B0.
Status
The open PR #133890 already handles CallerReturnAddress == INLINED_PINVOKE_FROM_R2R in WasmFrameHandler. #133890 depends on #133086, so this issue tracks the gap in case that stack is split or delayed. It would be worth landing the sentinel handling on its own.
Separately, consider a progress guard in StackWalk_1 (an identical frame yielded twice in a row → Error/terminate). Then a future frame-handler gap degrades to a truncated walk instead of a non-terminating enumeration.
Repro
Notes
As a workaround, Blazor-Playground/nesm detects the stall, rebuilds the context the native way (SP = CallSiteSP, IP = virtual IP from the R2R frame record), and re-seeds with CreateStackWalk(threadData, context, isFirst: false). The stitched walk then continues correctly through R2R CoreLib frames, the InterpreterFrame and the interpreted JavaScriptExports.CallJSExport.
Description
On WASM, an active InlinedCallFrame pushed by R2R code stores the sentinel
INLINED_PINVOKE_FROM_R2R(1,src/coreclr/vm/frames.h) inm_pCallerReturnAddress. NativeInlinedCallFrame::UpdateRegDisplay_Impl(src/coreclr/vm/wasm/helpers.cpp) treats that as a marker: it sets SP fromCallSiteSPand derives IP viaGetWasmVirtualIPFromStackPointer(CallSiteSP).On
main, the cDAC'sWasmFrameHandler.HandleInlinedCallFramecallsBaseFrameHandler.HandleInlinedCallFrame, which copiesCallerReturnAddressverbatim. The context IP becomes0x1, which is not managed code, so inStackWalk_1.Next():case StackWalkState.Frame:isActiveICFis true (return address ≠ 0), soFrameIter.Next()is not called.UpdateState: IP0x1is not managed →State = Frameagain, on the same frame.The
IEnumerablenever terminates. It keeps yielding the sameFrame(same frame address, ip0x1, same SP) forever. A consumer that enumerates without a cap hangs.On a live browser-wasm target paused during a
[JSExport]call, this triggered on the very first walk. Seeding withCreateStackWalk(threadData)givesInitialNativeContextfrom the InlinedCallFrame, then an endless run ofFrame [InlinedCallFrame] ip=0x1 sp=0x31B5B0.Status
The open PR #133890 already handles
CallerReturnAddress == INLINED_PINVOKE_FROM_R2RinWasmFrameHandler. #133890 depends on #133086, so this issue tracks the gap in case that stack is split or delayed. It would be worth landing the sentinel handling on its own.Separately, consider a progress guard in
StackWalk_1(an identical frame yielded twice in a row →Error/terminate). Then a future frame-handler gap degrades to a truncated walk instead of a non-terminating enumeration.Repro
12.0.0-alpha.1.26480.103(runtimecd42bb5).main@bb1b237b389. The Debugger-contract issue [cDAC][wasm] StackWalk_1 requires the Debugger contract, which WASM targets do not advertise #135034 has to be worked around first.[JSExport], then enumerateCreateStackWalk(threadData).Notes
As a workaround, Blazor-Playground/nesm detects the stall, rebuilds the context the native way (SP =
CallSiteSP, IP = virtual IP from the R2R frame record), and re-seeds withCreateStackWalk(threadData, context, isFirst: false). The stitched walk then continues correctly through R2R CoreLib frames, the InterpreterFrame and the interpretedJavaScriptExports.CallJSExport.