Skip to content

JSPI reentrancy primitive - the current promising stack base#27372

Open
guybedford wants to merge 5 commits into
emscripten-core:mainfrom
guybedford:jspi-promising-top
Open

JSPI reentrancy primitive - the current promising stack base#27372
guybedford wants to merge 5 commits into
emscripten-core:mainfrom
guybedford:jspi-promising-top

Conversation

@guybedford

@guybedford guybedford commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

This adds tracking of the shadow stack pointer on entry to each WebAssembly.promising export call, as described in #27364.

Libraries implementing shadow stack switching on top of JSPI (per https://blog.pyodide.org/posts/jspi-with-c-runtime/, and as landing for wasm-bindgen in wasm-bindgen/wasm-bindgen#5193) that have reentrancy need the base of the stack range in use by the current promising call, in order to save it on suspend and restore it on resume. This stack base won't necessarily be the initial stack base because promising calls may be made from reentrant synchronous Wasm stacks in JavaScript, such that the shadow stack is not empty. Stack saving needs this information it would otherwise not have access to.

  • Asyncify.lastPromisingStackBase records the stack pointer on entry to every promising call - covering wrapped exports, table-based async calls, async dynCall and embind async bindings (now all routed through Asyncify.makeAsyncFunction).
  • The marker is only valid before suspension and suspensions are not guarded, so stack switching libraries must intercept suspending imports and promising call sites themselves to use the stack base instead after suspension. But all stack switching libraries already need to track all suspensions anyway so this works out fine.

We could expose this as a direct function, but given the suspension treatment, having the field as an implicit over an explicit valid stack pointer seems to make more sense to me with these caveats.

Test coverage in other.test_jspi_promising_base verifies the marker at entry, its stability across call depth, and that a nested promising export entered at depth (while the outer export is suspended) updates it.

Made with AI assistance under my review

@guybedford

Copy link
Copy Markdown
Collaborator Author

The heavier alternative to this would be to instrument all suspend point injections as well, with full stack save / restore applied on suspend, per (1) in the original issue. I'm open to that as well, but didn't want to assume.

@guybedford

Copy link
Copy Markdown
Collaborator Author

Of course the heavy approach isn't needed when there is an exclusive JSPI suspension point, so not assuming shared JSPI and allowing the singular stack optimization, and then shipping this little hook to allow libraries to build on top their own systems might be the right start.

Under SPLIT_MODULE, exports can be lazy-loading JSPI trampolines which
cannot be called outside of a promising context.
@sbc100
sbc100 requested review from brendandahl and kripken July 17, 2026 22:36
@sbc100

sbc100 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

Do this is the first step in allowing re-entrancy with JSPI and/or ASYNCIFY?

Can/should emscripten not implement this itself and make it "just work"? Right now IIUC emscirpten to error out if you try to re-enter a suspended function? Could we build an option that replaces that error with shadow-stack-swapping? Of course we probably still want it to be opt-in because more C programs are not prepared for re-entrancy in this way?

@sbc100

sbc100 commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator

If I'm understanding correctly that this PR (and the issue it points to) are supposed to work around the current lack of rentrancy with JSPI perhaps that could be made more clear in titles/descriptions. Or maybe I'm wrong there is something else going on here?

@guybedford

guybedford commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Do this is the first step in allowing re-entrancy with JSPI and/or ASYNCIFY?

Right, this would be the required primitive to build re-entrancy, where the actual implementation is to support the stack save restore around the suspend for the stack up to this base.

If I'm understanding correctly that this PR (and the issue it points to) are supposed to work around the current lack of rentrancy with JSPI perhaps that could be made more clear in titles/descriptions. Or maybe I'm wrong there is something else going on here?

Right, I didn't think to use that framing, but that is exactly the right way to look at it. And if we did want to fully handle reentrancy that would be what I referred to as (1) in the original issue with stack save and restore per "thread" to make it a first-class thing in Emscripten.

@guybedford guybedford changed the title Track the last promising stack top under JSPI JSPI reentrancy primitive - the current promising stack base Jul 18, 2026

@sbc100 sbc100 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I see yes, I agree it seems reasonable to expose a primitive and I also think emscripten itself can/should optionally use this primitive to implement some kind -sASYNCIFY_REENTRANT setting. I think implementing that setting would be a good proof that the primitive worked. I'd be OK with landing PR standalone and the its usage as a followup (assuming you are confident this is enough?)

Comment thread src/lib/libasync.js Outdated
@sbc100

sbc100 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

Also, could we make this work with ASYNCIFY=1 too?

CC @tlively, @fgmccabe and @brendandahl who have been in discussions in the past about a reentrancy model for JSPI/ASYNCIFY.

@guybedford

guybedford commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

Also, could we make this work with ASYNCIFY=1 too?

Added the asyncify support.

I see yes, I agree it seems reasonable to expose a primitive and I also think emscripten itself can/should optionally use this primitive to implement some kind -sASYNCIFY_REENTRANT setting. I think implementing that setting would be a good proof that the primitive worked. I'd be OK with landing PR standalone and the its usage as a followup (assuming you are confident this is enough?)

It is of course up to implementers to verify the ownership model differences - in particular that there are no pointers against the stack region currently shared, but otherwise yes it is sufficient.

@sbc100

sbc100 commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

It is of course up to implementers to verify the ownership model differences - in particular that there are no pointers against the stack region currently shared, but otherwise yes it is sufficient.

Could you elaborate on this danger / risk? Can you describe an example of program that would break in this way?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants