JSPI reentrancy primitive - the current promising stack base#27372
JSPI reentrancy primitive - the current promising stack base#27372guybedford wants to merge 5 commits into
Conversation
|
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. |
|
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.
|
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? |
|
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, 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.
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. |
sbc100
left a comment
There was a problem hiding this comment.
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?)
|
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. |
Added the asyncify support.
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? |
|
Interestingly, unlike #27377 this remains a sound primitive for a reentrant JSPI implementation, so I will leave it open here.
For C++ I think it is mostly common sense, but in Rust, the main issue is more handling Rust's lifetime soundness over the suspension point. In https://github.com/guybedford/jspi#example I achieved this by making the promising closure |
A better example would be to consider a Rust program that holds a mutable borrow to something on the heap, then calls a suspending function while holding that borrow, breaking singular aliasing. I think if trying to build a proper Rust primitive around JSPI, it would need to take into account the suspending function behaving like an |
|
I'm going to close this one as well, since the The alternative scheme is to offset the suspension restore stack, but that does lead to invalid stack pointers which simply is not going to fly. Just handling the dead zone correctly as a Binaryen instrumentation is the right and correct implementation approach IMO. |
This adds tracking of the shadow stack pointer on entry to each
WebAssembly.promisingexport 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.lastPromisingStackBaserecords the stack pointer on entry to every promising call - covering wrapped exports, table-based async calls, asyncdynCalland embind async bindings (now all routed throughAsyncify.makeAsyncFunction).Suspension can be implemented on top of this primitive based on the following scheme:
lastPromisingStackBaseto the current stack position. This is becauseWebAssembly.promisinginvocations can under reentrant Wasm have existing sync execution parent stacks, and allows a minor optimization in only storing the region actually needed instead of the whole stack to the base.lastPromisingStackBasewould have unwound already)lastPromisingStackBaseto ensure the resume stack dead zone doesn't spill.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_baseverifies 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