The last few issues on this repo have brought up the security question of whether or not Web Assembly provides a more secure execution environment in comparison to JS executions.
In addition, there've been much discussion over the exact workflows for the Web Assembly start fuction and binding process in terms of ensuring that the ESM import workflows provide the major use cases achieved with the declarative WebAssembly.instantiate
APIs that are currently used today in Web Assembly applications.
Originally in this repo, @alexcrichton suggested in #14 an API for the Web Assembly ESM integration to support importing a WebAssembly.Module
object directly, in order to allow more easily and flexibly working with a compiled module.
Along the lines above, I'd like to propose changing the ESM semantics of importing Web Assembly in order to better achieve security and use case flexibility for Web Assembly applications.
Proposal
Web Assembly offers some highly compelling execution security properties in providing a strictly defined secure execution sandbox, down to the imported bindings provided to it.
By default the ESM integration would not naturally benefit from these security properties since it permits arbitrary JS imports from Web Assembly modules.
Instead of following the naive ESM integration, the proposal would be for all Web Assembly modules that are imported, to be imported as compiled Module
objects, leaving the binding process to the JS wrapper code entirely:
import module from './app.wasm';
// true
module instanceof WebAssembly.Module
The imported value of the Web Assembly module is only a { default: Module }
ES module, that can then be instantiated with a JS call to WebAssembly.instantiate
:
const { exports } = await WebAssembly.instantiate(mod, { env: wasmEnv ));
wasmEnv.bind(exports.memory);
export function wasmFn (arg) {
return exports.method(arg)
)
By only providing the uninstantiated Module
, this supports a number of useful properties:
- Run-to-completion Web Assembly binaries can be easily reinstantiated or rebound multiple times during the lifetime of the application.
- Many common Web Assembly binaries today would not support being imported in the Web Assembly ESM integration. With the above, all Web Assembly binaries that exist today can be imported and used in applications
- The import of Web Assembly is now a secure operation in itself, and the security properties of the Web Assembly module sandboxing are fully maintained for the user.
- Now that Web Assembly imports are a secure operation, it then also makes sense to support an import assertion for this case
How is this better than workflows today
The first criticism of this proposal might be - what benefit exactly does the ESM integration provide at all, if this is the case?
How is this a benefit over just fetching and compiling the Wasm module directly? Eg via:
const res = await fetch(new URL('./app.wasm', import.meta.url));
const module = await WebAssembly.compileStreaming(res);
And this is true, the benefit is exactly in just reifying the above pattern into an import pattern, including:
- Not all JS environments provide a
fetch
global. This means that Web Assembly instantiations are inconsistent and there exists no "universal" easy pattern.
- As a result of the above, build tools are not able to easily analyze Web Assembly compilations, when Web Assembly compilations should clearly be first-class constructs for JS / Wasm source analysis.
- The
import.meta.url
relative fetch pattern is still very new and not many tools support it. Very few JS build tools today will properly detect that app.wasm
is a binary that needs to be relocated included in the build folder in the above pattern.
- There is a common problem with JS applications importing Web Assembly due to the above, where many libraries explicitly allow defining the Wasm binary location via a global or configuration option explicitly to get around this problem of locating the Wasm binary. The encapsulated modularity of the binary is not carried through in npm library import workflows. Some packages even ship base64 Wasm encodings to get around this issue. It is clearly a pain point with JS ecosystem integrations today.
Interaction with Import Assertions
Since importing Wasm becomes a non-executing and secure operation, it then makes sense that a Wasm module import assertion can verify this property:
import module from './app.wasm' assert { type: 'wasm-module' }
This fits the definition of import assertions in being entirely validation based, and importantly not splitting the interpretation of the module depending on the assertion / mode used.
Interaction with Module Linking Proposal
The module linking proposal provides a way for Web Assembly to handle instantiation and binding setup between multiple Web Assembly modules to provide a richer end-user API directly.
These Web Assembly modules can effectively be thought of as a new type of higher-order Web Assembly module for these wiring needs.
In the ESM integration for the module linking proposal, it would be possible to define that these modules are treated differently, such that you would get the exports and imports applying as one might expect with a JS import or the naive ESM integration.
These modules would lose the sandboxing properties so the import assertion would not apply to these forms of modules making the distinction between these two different ESM integration cases clearer.