← All issues

JSC Module Loader Rewrite: WHATWG-Era JS Builtins Replaced with Pure C++

4a63810

Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp

+void CyclicModuleRecord::asyncExecutionFulfilled(JSGlobalObject* globalObject)
+{
+ VM& vm = globalObject->vm();
+ auto scope = DECLARE_THROW_SCOPE(vm);
+
+ ASSERT(status() == ModuleStatus::EvaluatingAsync || status() == ModuleStatus::Evaluated);
+ if (status() == ModuleStatus::Evaluated) {
+ ASSERT(!evaluationError());
+ return;
+ }
+ ASSERT(pendingAsyncDependencies() == 0);
+ ASSERT(!evaluationError());
+ execute(globalObject);
+ ...
+ Vector<CyclicModuleRecord*> execList;
+ gatherAvailableAncestors(vm, this, execList);
+ ...
+}

Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp

+JSPromise* ModuleRegistryEntry::ensureFetchPromise(JSGlobalObject* globalObject)
+{
+ if (m_fetchPromise)
+ return m_fetchPromise.get();
+ VM& vm = globalObject->vm();
+ m_fetchPromise.set(vm, JSPromise::create(vm, globalObject->promiseStructure()));
+ return m_fetchPromise.get();
+}

This is one of the largest architectural changes to JSC's module system in years — a complete replacement of the module loader. The old implementation split logic across C++ host hooks and a privileged JS builtin file (ModuleLoader.js) compiled into the engine, meaning JS-level logic ran with engine-internal privileges. The new architecture is pure C++, introducing six new types: CyclicModuleRecord (implementing the ECMAScript spec's DFS-based linking/evaluation algorithms), ModuleRegistryEntry (per-module fetch/instantiation/evaluation state), ModuleGraphLoadingState (in-flight import graph tracking), ModuleLoaderPayload, ModuleLoadingContext, and ModuleMap.

Old architecture:                         New architecture:

JS call: import('foo')                   JS call: import('foo')
  │                                          │
  ▼                                          ▼
C++ JSModuleLoader                       C++ JSModuleLoader
  │                                          │
  └──► ModuleLoader.js (builtin JS)          └──► ModuleRegistryEntry
         │  resolve()                               │  ensureFetchPromise()
         │  fetch()                                 │  ensureModulePromise()
         │  instantiate()                           │
         │  evaluate()                         ModuleGraphLoadingState
         │                                          │  innerModuleLoading()
         └──► back to C++ host hooks               │
                                             CyclicModuleRecord
                                                   │  innerModuleLinking()  [DFS]
                                                   │  innerModuleEvaluation() [DFS]
                                                   │  gatherAvailableAncestors() [TLA]
                                                   ▼
                                             Promise resolution / rejection

The rewrite fixes assertion failures on valid ES modules, incorrect top-level await evaluation ordering, and broken concurrent dynamic import behavior. The new ensureFetchPromise/ensureModulePromise deduplication pattern replaces the old JS-side registry for concurrent import() calls. Top-level await is now handled through asyncCapability, pendingAsyncDependencies, and gatherAvailableAncestors, implementing the spec's async evaluation ordering. Resolution failure caching (addResolutionFailure) now produces distinct Error objects per importing context — the test bare-resolution-failure.js explicitly checks e1 !== e2 and e1.message !== e2.message, catching a previously broken invariant.

This is a high-value audit target: every security-critical operation in the module system — resolution, fetching, linking, evaluation, and error propagation — is now implemented in fresh C++ code managing GC-heap objects across async continuations.

The concurrent import deduplication path (ensureFetchPromise/ensureModulePromise and the decrementRemaining counter in ModuleLoaderPayload) needs careful review — off-by-one errors or use-after-free if a promise settles while another caller is mid-operation. The gatherAvailableAncestors graph traversal and its interaction with pendingAsyncDependencies, asyncEvaluationOrder, and cycleRoot is complex — incorrect traversal could trigger evaluation out of order or skip modules entirely. The old JS builtins ran in a GC-safe context; the new C++ code directly manages JSPromise and CyclicModuleRecord objects across async continuations stored as microtasks — any missing WriteBarrier or incorrect rooting in the new microtask callbacks could produce heap corruption.

🔒

New C++ async state machines managing module graph traversal and concurrent imports — several edge cases in ownership and error propagation are worth auditing.

Subscribe to read more