Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unintended Functionality: Trap Catching in Wasm #22

Open
RossTate opened this issue Sep 6, 2023 · 5 comments
Open

Unintended Functionality: Trap Catching in Wasm #22

RossTate opened this issue Sep 6, 2023 · 5 comments

Comments

@RossTate
Copy link
Collaborator

RossTate commented Sep 6, 2023

Issue: This proposal was designed with the intention that eventually the "suspending" and "promising" functions it produces would be implementable using just WebAssembly extended with some core stack-switching design (and some JS imports or manipulating JS Suspender objects and testing for and adding listeners to JS Promises). However, as an unintended consequence of semantic corner cases, the current semantics makes this impossible without also adding support for catching traps in wasm. In particular, given a wasm function "foo", you can use simple wasm modules and WebAssembly.Function with "suspending" and "promising" options (and no JS code beyond simple linking) to build a WebAssembly.Function that behaves just like "foo" except it throws a wasm exception (with your choice of tag of type externref) instead of trapping.

Cause: The issue is caused by the fact that a "promising" function both calls the wrapped function immediately and returns a rejected promise immediately if that call traps before suspending.

Fix: The simplest and most flexible way to resolve the issue is to have the "promising" function also trap if that call traps before suspending. (We might also consider taking the change a step further to make "promising" symmetric with "suspending" and have the "promising" function return a promise only if a corresponding suspend occurs, and have it otherwise simply return the tuple of values coerced into an externref.)

@RossTate
Copy link
Collaborator Author

Unfortunately, the strategy for polyfilling JSPI with core stack-switching that was presented yesterday was unsound; it would cause non-trapping JSPI programs to trap. The key issue lies in the export wrapper:

function exported_fun(...args) {
  return new Promise((resolve,reject) => {
    try{
      intro_wasm_fun(resolve,reject,..args);
    } catch (E){
      reject(E)
    });
};

This wrapper is introduced to address the issue I identified here. In particular, it essentially has the export wrapper install a JS-frame to catch the trap. However, the export wrapper is itself supposed to be "suspendable", since the idea was that export wrappers would eventually be implementable in WebAssembly and would consequently be suspendable. Unfortunately, installing this JS-frame would make this polyfilling of the export wrapper not suspendable, which could cause previously-non-trapping JSPI programs to trap.

So the issue identified above remains unresolved.

@fgmccabe
Copy link
Collaborator

I do not follow this argument. There is no requirement that the wrapper be suspendable. The requirement is to polyfill the API.

@RossTate
Copy link
Collaborator Author

The spec says "A function is suspendable if it was ... returned by WebAssembly.function" (among other cases). The wrapping functions are intentionally included by this, which was an explicit intent of the design since the expectation was that they would eventually be implementable in wasm with core stack-switching.

@RossTate
Copy link
Collaborator Author

Is there a problem with the simple fix of not catching a trap before suspending? If someone really wants a rejected promise when that occurs, they can wrap the promising export with the simple JS code that does so.

@RossTate
Copy link
Collaborator Author

RossTate commented Nov 3, 2023

If we change the spec so that the wrapped export always returns a promise, as seems to be the resolution in #11, then this problem is resolved: the wrapped export creates a promise with resolves (i.e. Promise.withResolvers or some polyfill thereof), uses queueMicrotask to queue a JS call to the export, and then returns the promise. The queued JS call is given the promise's resolvers and the arguments to use and executes the wasm function on a new stack. If that call traps before suspending, it can catch that trap as a JS value and reject the promise with it.

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

No branches or pull requests

2 participants