Dynamic `import.defer()` semantics
JSC ships dynamic import.defer() — a new module phase whose namespace executes synchronously the first time a string-keyed property is touched.
JSTests/modules/import-defer-dynamic-evaluation.js
TC39 Deferred Module Evaluation 제안은 모듈 링킹과 모듈 실행을 분리합니다. deferred namespace proxy는 즉시 반환되며, 모듈 본문은 첫 번째 non-Symbol 프로퍼티 접근 시에만 실행됩니다. JSC의 모듈 파이프라인은 엔진 측(graph 탐색, bytecode, microtask 스케줄링)과 WebCore 바인딩 계층(ScriptModuleLoader, JSDOMGlobalObject)으로 나뉘는데, 새로운 "import phase" 파라미터는 두 계층 모두를 거쳐 전달되어야 합니다.
이 commit은 동적 import.defer(specifier) 형식을 구현합니다. 모듈 그래프는 로드 및 링크 단계까지 진행하되, deferred root 자체는 즉시 실행하지 않습니다. GatherAsynchronousTransitiveDependencies()는 실행되지 않은 top-level-await (TLA) 모듈을 post-order로 수집합니다. 수집된 모듈은 두 개의 새로운 내부 microtask(DynamicImportDeferLoadSettled, DynamicImportDeferDependencySettled)를 통해 평가되며, 모든 의존성이 settle되면 반환된 promise가 deferred module namespace로 resolve됩니다.
모든 TLA 의존성 promise를 대기하는 AND-join은 JSPromiseCombinatorsGlobalContext를 shared counter cell로 재사용합니다. Promise.all 내부 구현에서 차용한 패턴이지만, 사양상 핵심 제약이 있습니다. 의존성 promise에 대해 then을 조회해서는 안 됩니다.
import.defer(specifier)
├─► load() + link()
├─► GatherAsyncTransitiveDeps() → [dep1, dep2, ...]
├─► evaluate(depN) ──► AND-join counter (DynamicImportDeferDependencySettled)
└─► resolve(deferredNamespace)
Namespace proxy dispatch:
Symbol key ──► return value (NO evaluation)
String key ──► evaluate() ──► return value
Significance
이 변경은 새로운 JS-visible API와 새로운 microtask 유형을 도입하며, promise AND-join과 namespace proxy dispatch 동작 방식이 JSC 모듈 시스템의 모든 계층에 걸쳐 새롭게 추가됩니다. 이 영역은 JS 엔진에서 역사적으로 보안 버그 밀도가 가장 높은 곳에 해당합니다.
Audit directions
-
AND-join counter 재사용 (
DynamicImportDeferDependencySettled). 구현에서는JSPromiseCombinatorsGlobalContext를 shared counter cell로 재사용합니다. initial dep list가 비어 있을 때 발생하는 integer underflow, double-decrement, 혹은 조기 resolve는Promise.all형식 구현의 고전적인 버그 패턴입니다. 더 중요한 것은, 사양이 dep promise에 대해then을 조회하지 않도록 join을 제약한다는 점입니다. 이를 어기면 공격자가 제어하는.thengetter를 통해 combinator 내부에 코드를 주입할 수 있습니다. -
Deferred namespace proxy Symbol/string dispatch. Symbol 키를 통한 접근은 evaluation을 유발해서는 안 되며, Symbol이 아닌 string 접근은 반드시 evaluation을 발생시켜야 합니다. edge case로는 dispatch 경계를 넘는 computed property 접근이 있습니다. symbol 형태의 string을 사용하는
Reflect.get호출이나, proxy의 own-property guard를 우회하는 prototype chain 조회도 살펴볼 필요가 있습니다. -
순환 그래프에서의
GatherAsynchronousTransitiveDependencies(). post-order 탐색은 cycle을 올바르게 처리해야 합니다. visited-set이 제대로 초기화되지 않거나, 그래프가 겹치는import.defer()호출 간에 공유될 경우, 노드가 누락되거나 이중 평가되거나 무한 탐색이 발생할 수 있습니다. -
바인딩 계층을 통한 Import phase 전달.
importPhase는JSGlobalObjectFunctions.cpp에서 시작하여 여러 계층을 거쳐 전달됩니다. 전달 경로는JSModuleLoader::continueDynamicImport,ScriptModuleLoader::importModule,JSDOMGlobalObject::moduleLoaderImportModule,JSAPIGlobalObject::moduleLoaderImportModule순입니다. 어느 지점에서든 phase가 기본값으로 처리되거나 잘못 전달되면, deferred 상태를 유지해야 하는 모듈이 조용히 실행될 수 있습니다. -
Microtask 순서와 GC 상호작용. link 단계와 gather 단계 사이에 GC가 실행될 경우를 고려해야 합니다.
ModuleLoaderPayload/ModuleLoadingContext내부의 deferred namespace 객체나 의존성 목록에 대한 참조가 stale 상태가 되거나 수집될 가능성이 있습니다. 새로운 payload 유형이 GC marking을 완전하게 처리하는지 살펴볼 필요가 있습니다.