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

ESM Phase Imports: Module sources in import(), new Worker() and serialization #11152

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

guybedford
Copy link
Contributor

@guybedford guybedford commented Mar 21, 2025

This draft PR specifies the ESM Phase Imports proposal, supporting JS and Wasm module representations supported in import(), new Worker() and serialization.

Background

The Phase 3 WebAssembly ESM Integration and the Stage 3 Source Phase Imports Proposal landed previously in #10380.

This PR extends these to support the Stage 2.7 ESM Phase Imports Proposal.

As well as relying on the ESM Phase Imports spec, this spec also relies on the WebAssembly ESM Integration PR WebAssembly/esm-integration#106.

Summary of the Proposal

The ESM Phase Imports proposal specifies a source phase for JavaScript modules - i.e. while we have support for import source mod from './mod.wasm' today, it is extending that support to JavaScript via import source mod form './mod.js' having a higher-order module representation in JavaScript supporting a ModuleSource object like WebAssembly's WebAssembly.Module object.

Further, the goal in the TC39 module harmony effort is for source phase imports to also form the basis of module declarations and module expressions such that this same ModuleSource object can also be the runtime representation for module foo { export var bar = 5; } inline modules.

Given one of these modules mod, either imported in its source phase, or defined as an inline module, the ESM Phase Imports proposal is designed to support the following use cases:

  • Support in serialization and structured clone: postMessage(mod), structuedClone(mod)
  • Support in dynamic import: import(mod)
  • Support in worker construction: new Worker(mod)

This specification specifies all of the above for both WebAssembly.Module and the new JavaScript ModuleSource representation specified in the ESM Phase Imports proposal.

For more of the semantic detail background for the behaviours of the above, see the slides linked above in the background.

Security Model

Integrating with the web security model is one of the primary questions here, and if done right we can in fact create stronger guarantees around what it means to have the capability to execute a module.

We build off the security guarantee on the web existing already today - if you have already imported a module and got a handle to it (either its namespace or its source phase), then you have already passed security checks to be able to use it. That is, import(mod) does not imply any new security checks in the current design.

Rather, we effectively design some new security model handling around postMessage(mod) and new Worker(mod) to integrate into the existing security model for workers.

We do this by distinguishing between two cases of module - rooted and unrooted.

Rooted modules are modules that are obtained through static means, such that we know that the module came from a possibly securely checked original URL in the JS module registry. We add a new rooted source property on the module record to imply this. In addition, for now, we treat modules whose ResponseURL is different from their RequestURL as unrooted as well to ensure consistency in the URL handling.

When we postMessage or pass to new Worker a rooted module, we not only serialize the module source text or source bytes (for JS and Wasm respectively), but we also serialize the URL of the module, as a secure identifier for that module. A worker constructed from a module takes its URL from this URL as well. We then aim to fully integrate with the existing CSP checks on these URLs (and further review here would help a lot to ensure we're catching all the cases).

When we pass an unrooted module, we then fall back to requiring an eval or unsafe-wasm-eval based policy. Unrooted modules effectively being treated as "evalish", that their contents came from user provided bytes or source that has not been verified to come from a URL.

Specification Approach

The specification integrates all aspects of the proposal, from new Worker(mod) construction, to serialization to import(mod), supporting the new JS ModuleSource and also adding this same support for WebAssembly.Module objects.

new Worker(module)

Worker construction works by using the new HostGetModuleSourceModuleRecord host hook introduced by the ESM Phase Imports proposal which allows identifying when an object is a module source object.

The worker constructor is then extended to support this object when provided. Shared workers and worklets are not currently supported but could be nice additions.

The cross-origin isolated capability only applies for rooted sources in worker construction. And a worker created from an unrooted source is treated as having a null URL.

Since source transfer only transfers the direct source and none of its dependencies, in order for module resolution to work in workers, we are seeking to have this constructor also automatically set the importMap: 'inherit' option in the worker construction.

It is therefore a goal for this spec to land after an importMap: 'inherit' option is specified in worker construction. There is already a specification PR for this in #10858. Once landed this specification can be updated to set this option by default in the worker constructor if not otherwise provided.

Serialization

We add a new serialization case for the JS ModuleSource based on detecting objects with its [[SourceTextModuleRecord]] internal slot. Wasm serialization remains separately defined in the Wasm Web API, and this is amended to support rooted source transfer in the ESM Integraiton PR WebAssembly/esm-integration#106.

To properly support creating a WebAssembly module script eagerly on transfer in that, the Wasm parse function needed to be refactored slightly here as well.

Modules that are not rooted do not transfer their URLs in the current design, so that relative imports would might break if deserializing into a different base URL page context. This was for the convenience in the structure that rooted sources have associated module records, while unrooted sources do not have associated module records. If all sources had module records, or if we separaetly maintained the baseURL it might be possible to relax this, but the tradeoff might be fully requiring every WebAssembly.Module to immediately be associated with a module record if it doesn't need to be.

import(module)

The ESM Phase Imports proposal adds support for import(module), where the loading pipeline is able to accept a direct module record import. The HTML spec changes here add support for this new path and ensure that it provides the correct module keying semantics.

In cases of import(new WebAssembly.Module(bytes)) and in future maybe cases like import(eval('module { }')) we need to lazily create module records for source objects that weren't previously associated with a module record.

The keying semantics for import(module) while quite complex do come down to a simple check based on a new ModuleSourcesEqual call in the ESM Phase Imports spec, which is then used here. With these keying semantics importing the same module source always gives the same module instance in the registry. Even with serializing and deserializing, a rooted source will always get the same module instance in the registry when importing it. Unrooted sources on the other hand always have their identity tied to the identity of their unrooted source object. See the slides and notes for more details here.

Next Steps

This work is a spec complete draft and ready for early review of the design and security model.

Once we've obtained further implementer and design feedback, then we can complete the remaining steps to creating a full spec PR.

At this point in time, we are primarily seeking interest and feedback from reviewers and implementers.

  • At least two implementers are interested (and none opposed):
  • Tests are written and can be reviewed and commented upon at:
  • Implementation bugs are filed:
    • Chromium: …
    • Gecko: …
    • WebKit: …
    • Deno (only for timers, structured clone, base64 utils, channel messaging, module resolution, web workers, and web storage): …
    • Node.js (only for timers, structured clone, base64 utils, channel messaging, and module resolution): …
  • Corresponding HTML AAM & ARIA in HTML issues & PRs:
  • MDN issue is filed: …
  • The top of this comment includes a clear commit message to use.

(See WHATWG Working Mode: Changes for more details.)


/infrastructure.html ( diff )
/references.html ( diff )
/structured-data.html ( diff )
/webappapis.html ( diff )
/workers.html ( diff )

@bakkot
Copy link
Contributor

bakkot commented Mar 22, 2025

When we pass an unrooted module, we then fall back to requiring an eval or unsafe-wasm-eval based policy. Unrooted modules effectively being treated as "evalish", that their contents came from user provided bytes or source that has not been verified to come from a URL.

Copying this point out from a larger and somewhat meandering discussion in the matrix room:

CSP already gates creation of module objects through eval-ish means, so I'm not sure we need a second check to gate their use. I'm not sure there's a difference in practice (unless Trusted Types get involved somehow, which I have not thought enough about).


Separately, it would be a little bit odd if script-src 'unsafe-eval'; worker-src 'none' allowed you to create a worker (via new Worker(WebAssembly.compile(...)), say). I'm not totally sure what to do about that case. I think this applies regardless of whether there's a second check per above, since the capability to eval is always derived from the script-src directive rather than worker-src.

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

Successfully merging this pull request may close these issues.

2 participants