← 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();
+}

JSC module 시스템에서 최근 몇 년간 가장 규모가 큰 아키텍처 변경 중 하나입니다. module loader 전체를 새로 구현한 작업입니다. 기존 구현은 로직이 C++ host hook과 엔진에 컴파일된 privileged JS builtin 파일(ModuleLoader.js)에 분산되어 있었습니다. 이 구조에서는 JS 수준의 로직이 엔진 내부 권한으로 실행되었습니다. 새 아키텍처는 순수 C++로 구성됩니다. 새롭게 도입된 타입은 여섯 가지입니다. CyclicModuleRecord는 ECMAScript 명세의 DFS 기반 linking/evaluation 알고리즘을 구현하고, ModuleRegistryEntry는 모듈별 fetch/instantiation/evaluation 상태를 담당합니다. ModuleGraphLoadingState는 진행 중인 import graph를 추적하며, 이 외에 ModuleLoaderPayload, ModuleLoadingContext, 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

이번 재작성으로 유효한 ES 모듈에서 발생하던 assertion failure, 잘못된 top-level await 평가 순서, 그리고 concurrent dynamic import 오작동이 수정되었습니다. 새로운 ensureFetchPromise/ensureModulePromise deduplication 패턴은 concurrent import() 호출을 처리하던 기존 JS 측 registry를 대체합니다. Top-level await는 이제 asyncCapability, pendingAsyncDependencies, gatherAvailableAncestors를 통해 처리되며, 명세의 async evaluation 순서를 구현합니다. Resolution failure 캐싱(addResolutionFailure)은 이제 import 컨텍스트마다 별도의 Error 객체를 생성하도록 변경되었습니다. 테스트 파일 bare-resolution-failure.jse1 !== e2e1.message !== e2.message를 명시적으로 검증하며, 기존에 깨져 있던 invariant를 포착합니다.

이 변경은 핵심적인 점검 대상입니다. module 시스템에서 보안에 중요한 모든 동작 — resolution, fetching, linking, evaluation, error propagation — 이 이제 async continuation 전반에 걸쳐 GC-heap 객체를 직접 관리하는 새 C++ 코드로 구현되어 있습니다.

Concurrent import deduplication 경로를 면밀히 점검해야 합니다. 대상은 ensureFetchPromise/ensureModulePromiseModuleLoaderPayloaddecrementRemaining 카운터입니다. promise가 완료되는 시점에 다른 호출자가 동일한 작업을 진행 중이라면, off-by-one 오류나 use-after-free가 발생할 가능성이 있습니다. gatherAvailableAncestors graph traversal과 pendingAsyncDependencies, asyncEvaluationOrder, cycleRoot 간의 상호 작용은 복잡한 구조입니다. traversal에 오류가 있으면 evaluation 순서가 어긋나거나 일부 모듈이 아예 건너뛰어질 가능성이 있습니다. 한편, 기존 JS builtin은 GC-safe 컨텍스트에서 실행되었습니다. 새 C++ 코드는 microtask로 저장된 async continuation 전반에 걸쳐 JSPromiseCyclicModuleRecord 객체를 직접 관리합니다. 새 microtask callback에서 WriteBarrier 누락이나 잘못된 rooting이 있으면 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.

더 확인하려면 구독해 주세요