diff --git a/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp b/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp index 912df5f064e33..1edbc50d481d8 100644 --- a/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp @@ -1347,9 +1347,31 @@ unsigned AbstractModuleRecord::innerModuleEvaluation(JSGlobalObject* globalObjec // sibling whose post-await bindings are still TDZ (#30259) — // and its body has already been entered // (pendingAsyncDependencies == 0). + // + // The watermark alone is insufficient for the cross-Evaluate() + // dynamic-import case (#30651): two independent dynamic + // imports of the same TLA dep get fresh watermarks each, so + // the second one sees `order < watermark` and skips even + // though no deadlock is possible. Narrow further when the + // embedder is feeding us dynamic-import referrers (the VM + // set is non-empty): the Nitro self-deadlock the skip was + // written for only happens when the dep is the initiator + // of *this* Evaluate(), i.e. the module whose body is + // paused at `await import()`. For any other kind of + // await (setTimeout, fetch, another promise) or an + // unrelated parallel dynamic import, the dep will resume + // on its own and the spec wait is what we want. + // + // Gate the fourth condition on `hasPendingDynamicImport()` + // so embedders that don't yet plumb the referrer through + // fall back to the looser pre-#30651 behaviour — otherwise + // the spec wait would deadlock the Nitro-style tests. + bool discriminateByInitiator = vm.hasPendingDynamicImport(); + bool depIsAwaitingDynamicImport = discriminateByInitiator && vm.isModuleAwaitingDynamicImport(cyclic); if (!depWasAlreadyEvaluatingAsync || cyclic->asyncEvaluationOrder().order() >= asyncOrderWatermark - || cyclic->pendingAsyncDependencies().value_or(1)) { + || cyclic->pendingAsyncDependencies().value_or(1) + || (discriminateByInitiator && !depIsAwaitingDynamicImport)) { #endif // 12.b.v.1. Set module.[[PendingAsyncDependencies]] to module.[[PendingAsyncDependencies]] + 1. module->setPendingAsyncDependencies(module->pendingAsyncDependencies().value() + 1); diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index fe55ebd15bc8f..bdac450b225cd 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -1074,7 +1074,16 @@ static void moduleLoadTopSettled(JSGlobalObject* globalObject, VM& vm, ThrowScop if (context->useImportMap()) innerLoadFlags.add(ModuleLoadFlag::UseImportMap); if (context->dynamic()) { - combinedCell = ModuleLoaderPayload::create(vm, statePromise, context->deferred()); + auto* payload = ModuleLoaderPayload::create(vm, statePromise, context->deferred()); +#if USE(BUN_JSC_ADDITIONS) + // Carry the initiator (the module that did `import(X)`) from + // the creating ModuleLoadingContext to the ModuleLoaderPayload + // that outlives it. dynamicImportLoadSettled reads it back to + // push/pop around the target's Evaluate(). See #30651. + if (auto* initiator = context->dynamicImportInitiator()) + payload->setDynamicImportInitiator(vm, initiator); +#endif + combinedCell = payload; loadPromise = globalObject->moduleLoader()->loadModule(globalObject, globalObject, request, combinedCell, scriptFetcher, innerLoadFlags); } else { combinedCell = ModuleGraphLoadingState::create(vm, statePromise, scriptFetcher); @@ -1346,10 +1355,16 @@ static void dynamicImportLoadSettled(JSGlobalObject* globalObject, VM& vm, Throw // Step-4 rejectedClosure or Step-6 linkAndEvaluateClosure // // continueDynamicImport: loadPromise settled - // arguments[0] = capabilityPromise + // arguments[0] = capabilityPromise (Bun: ModuleLoaderPayload carrying + // capabilityPromise + dynamic-import initiator) // arguments[1] = resolution or error // arguments[2] = AbstractModuleRecord* +#if USE(BUN_JSC_ADDITIONS) + auto* dynamicPayload = uncheckedDowncast(arguments[0]); + auto* capabilityPromise = dynamicPayload->promise(); +#else auto* capabilityPromise = uncheckedDowncast(arguments[0]); +#endif auto* module = uncheckedDowncast(arguments[2]); auto status = static_cast(payload); if (status != JSPromise::Status::Fulfilled) { @@ -1372,8 +1387,25 @@ static void dynamicImportLoadSettled(JSGlobalObject* globalObject, VM& vm, Throw } if (!deferred) { +#if USE(BUN_JSC_ADDITIONS) + // Push the initiator (the module whose body is awaiting this + // dynamic import) onto the VM set for the duration of the target's + // Evaluate(). innerModuleEvaluation's 11.c.v reads this to tell + // the Nitro self-deadlock (dep == initiator) from unrelated + // parallel dynamic imports (no match) — see #30651. + CyclicModuleRecord* initiator = dynamicPayload->dynamicImportInitiator(); + if (initiator) + vm.pushDynamicImportInitiator(initiator); +#endif + // 6.c. Let evaluatePromise be module.Evaluate(). JSPromise* evaluatePromise = module->evaluate(globalObject); + +#if USE(BUN_JSC_ADDITIONS) + if (initiator) + vm.popDynamicImportInitiator(initiator); +#endif + if (scope.exception()) [[unlikely]] { capabilityPromise->rejectWithCaughtException(vm, scope); return; diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 96144ec748e22..67cad5df1fb54 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -346,7 +346,11 @@ void JSModuleLoader::provideFetch(JSGlobalObject* globalObject, const Identifier entry->provideFetch(globalObject, jsSourceCode); // can throw } +#if USE(BUN_JSC_ADDITIONS) +JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identifier& specifier, RefPtr parameters, RefPtr scriptFetcher, OptionSet flags, CyclicModuleRecord* dynamicImportInitiator) +#else JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identifier& specifier, RefPtr parameters, RefPtr scriptFetcher, OptionSet flags) +#endif { VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); @@ -381,6 +385,10 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi AbstractModuleRecord::ModuleRequest request { specifier, ScriptFetchParameters::create(type) }; #endif auto* context = ModuleLoadingContext::create(vm, request, WTF::move(scriptFetcher), flags); +#if USE(BUN_JSC_ADDITIONS) + if (flags.contains(ModuleLoadFlag::Dynamic) && dynamicImportInitiator) + context->setDynamicImportInitiator(vm, dynamicImportInitiator); +#endif JSPromise* intermediatePromise = JSPromise::create(vm, globalObject->promiseStructure()); intermediatePromise->markAsHandled(); @@ -448,7 +456,31 @@ JSPromise* JSModuleLoader::requestImportModule(JSGlobalObject* globalObject, con OptionSet flags { ModuleLoadFlag::Evaluate, ModuleLoadFlag::Dynamic }; if (deferred) flags.add(ModuleLoadFlag::Deferred); +#if USE(BUN_JSC_ADDITIONS) + // Resolve the referrer URL to a CyclicModuleRecord (the caller of + // `import()`) so loadModule can stash it on the ModuleLoadingContext. + // dynamicImportLoadSettled pushes it onto the VM's + // m_modulesAwaitingDynamicImport set around the target's Evaluate(), + // which lets innerModuleEvaluation 11.c.v tell the Nitro self-deadlock + // (cycle initiator == dep) from an unrelated parallel dynamic import + // (no match — spec wait is correct). See #30651. + CyclicModuleRecord* initiator = nullptr; + if (!referrer.isNull() && !referrer.isSymbol() && !referrer.isEmpty()) { + for (auto type : { ScriptFetchParameters::Type::JavaScript, ScriptFetchParameters::Type::WebAssembly, ScriptFetchParameters::Type::JSON, ScriptFetchParameters::Type::HostDefined }) { + if (ModuleRegistryEntry* entry = getRegisteredMayBeNull(referrer, type)) { + if (auto* record = entry->record()) { + if (auto* cyclic = dynamicDowncast(record)) { + initiator = cyclic; + break; + } + } + } + } + } + JSPromise* promise = loadModule(globalObject, resolved, WTF::move(parameters), WTF::move(scriptFetcher), flags, initiator); +#else JSPromise* promise = loadModule(globalObject, resolved, WTF::move(parameters), WTF::move(scriptFetcher), flags); +#endif RETURN_IF_EXCEPTION(scope, nullptr); JSPromise* resultPromise = JSPromise::create(vm, globalObject->promiseStructure()); @@ -956,7 +988,11 @@ void JSModuleLoader::finishLoadingImportedModule(JSGlobalObject* globalObject, c } else { // 3.a. Perform ContinueDynamicImport(payload, result). auto* dynamicPayload = uncheckedDowncast(payload); +#if USE(BUN_JSC_ADDITIONS) + continueDynamicImport(globalObject, dynamicPayload, result, WTF::move(scriptFetcher), dynamicPayload->deferred()); +#else continueDynamicImport(globalObject, dynamicPayload->promise(), result, WTF::move(scriptFetcher), dynamicPayload->deferred()); +#endif RETURN_IF_EXCEPTION(scope, void()); } @@ -1008,7 +1044,11 @@ void JSModuleLoader::continueModuleLoading(JSGlobalObject* globalObject, ModuleG scope.release(); } +#if USE(BUN_JSC_ADDITIONS) +void JSModuleLoader::continueDynamicImport(JSGlobalObject* globalObject, ModuleLoaderPayload* dynamicPayload, ModuleCompletion completion, RefPtr scriptFetcher, bool deferred) +#else void JSModuleLoader::continueDynamicImport(JSGlobalObject* globalObject, JSPromise* promise, ModuleCompletion completion, RefPtr scriptFetcher, bool deferred) +#endif { // ContinueDynamicImport(promiseCapability, moduleCompletion) // https://tc39.es/ecma262/#sec-ContinueDynamicImport @@ -1016,6 +1056,10 @@ void JSModuleLoader::continueDynamicImport(JSGlobalObject* globalObject, JSPromi VM& vm = globalObject->vm(); auto scope = DECLARE_THROW_SCOPE(vm); +#if USE(BUN_JSC_ADDITIONS) + JSPromise* promise = dynamicPayload->promise(); +#endif + // 1. If moduleCompletion is an abrupt completion, then if (Exception** exception = std::get_if(&completion)) { // 1.a. Perform ! Call(promiseCapability.[[Reject]], undefined, « moduleCompletion.[[Value]] »). @@ -1030,7 +1074,13 @@ void JSModuleLoader::continueDynamicImport(JSGlobalObject* globalObject, JSPromi JSPromise* loadPromise = loadRequestedModules(globalObject, module, WTF::move(scriptFetcher)); RETURN_IF_EXCEPTION(scope, void()); // 4-8. Link and evaluate using microtask dispatch instead of closures. +#if USE(BUN_JSC_ADDITIONS) + // Route via the payload (not the promise) so dynamicImportLoadSettled + // can recover the initiator for #30651's push/pop. + loadPromise->performPromiseThenWithInternalMicrotask(vm, deferred ? InternalMicrotask::DynamicImportDeferLoadSettled : InternalMicrotask::DynamicImportLoadSettled, dynamicPayload, module); +#else loadPromise->performPromiseThenWithInternalMicrotask(vm, deferred ? InternalMicrotask::DynamicImportDeferLoadSettled : InternalMicrotask::DynamicImportLoadSettled, promise, module); +#endif // 9. Return UNUSED. scope.release(); } diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.h b/Source/JavaScriptCore/runtime/JSModuleLoader.h index 3d3222d262fa7..669555be464d9 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.h +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.h @@ -36,11 +36,13 @@ namespace JSC { +class CyclicModuleRecord; class ErrorInstance; class JSPromise; class JSModuleNamespaceObject; class JSModuleRecord; class JSSourceCode; +class ModuleLoaderPayload; class ModuleRegistryEntry; class SourceOrigin; @@ -92,7 +94,11 @@ class JSModuleLoader final : public JSCell { // APIs to control the module loader. void provideFetch(JSGlobalObject*, const Identifier& key, ScriptFetchParameters::Type, SourceCode&&); void provideFetch(JSGlobalObject*, const Identifier& key, ScriptFetchParameters::Type, JSSourceCode*); +#if USE(BUN_JSC_ADDITIONS) + JSPromise* loadModule(JSGlobalObject*, const Identifier& moduleName, RefPtr, RefPtr, OptionSet, CyclicModuleRecord* dynamicImportInitiator = nullptr); +#else JSPromise* loadModule(JSGlobalObject*, const Identifier& moduleName, RefPtr, RefPtr, OptionSet); +#endif JSPromise* linkAndEvaluateModule(JSGlobalObject*, const Identifier& moduleKey, RefPtr, RefPtr); JSPromise* requestImportModule(JSGlobalObject*, const Identifier& moduleName, const Identifier& referrer, RefPtr, RefPtr, bool deferred = false); @@ -159,7 +165,11 @@ class JSModuleLoader final : public JSCell { JSPromise* hostLoadImportedModule(JSGlobalObject*, const ModuleReferrer&, const ModuleRequest&, JSCell* payload, RefPtr, bool useImportMap); JSPromise* loadModule(JSGlobalObject*, const ModuleReferrer&, const ModuleRequest&, JSCell* payload, RefPtr, OptionSet); void continueModuleLoading(JSGlobalObject*, ModuleGraphLoadingState*, ModuleCompletion result); +#if USE(BUN_JSC_ADDITIONS) + void continueDynamicImport(JSGlobalObject*, ModuleLoaderPayload*, ModuleCompletion, RefPtr, bool deferred); +#else void continueDynamicImport(JSGlobalObject*, JSPromise*, ModuleCompletion, RefPtr, bool deferred); +#endif JSPromise* loadRequestedModules(JSGlobalObject*, AbstractModuleRecord*, RefPtr); static JSPromise* makeModule(JSGlobalObject*, const Identifier& moduleKey, JSSourceCode*); diff --git a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp index 565b499e0df52..d19681134cb48 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp +++ b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp @@ -54,6 +54,9 @@ void ModuleLoaderPayload::visitChildrenImpl(JSCell* cell, Visitor& visitor) Base::visitChildren(thisObject, visitor); visitor.append(thisObject->m_promise); visitor.append(thisObject->m_fulfillment); +#if USE(BUN_JSC_ADDITIONS) + visitor.append(thisObject->m_dynamicImportInitiator); +#endif } DEFINE_VISIT_CHILDREN(ModuleLoaderPayload); @@ -65,4 +68,16 @@ ModuleLoaderPayload* ModuleLoaderPayload::create(VM& vm, JSPromise* promise, boo return instance; } +#if USE(BUN_JSC_ADDITIONS) +CyclicModuleRecord* ModuleLoaderPayload::dynamicImportInitiator() const +{ + return m_dynamicImportInitiator.get(); +} + +void ModuleLoaderPayload::setDynamicImportInitiator(VM& vm, CyclicModuleRecord* module) +{ + m_dynamicImportInitiator.set(vm, this, module); +} +#endif + } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h index e98d1c241ff49..7824f7ee323d7 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h +++ b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h @@ -30,6 +30,8 @@ namespace JSC { +class CyclicModuleRecord; + // Wraps the dynamic-import target promise for top-level dynamic loadModule. Acts as the // host-defined "payload" passed back via FinishLoadingImportedModule, and additionally // holds the AND-join state used to combine loadPromise and statePromise. @@ -64,6 +66,16 @@ class ModuleLoaderPayload final : public JSCell { return !--m_remainingFulfillments; } +#if USE(BUN_JSC_ADDITIONS) + // The initiator CyclicModuleRecord whose body is awaiting this import's + // result. Used by dynamicImportLoadSettled to push/pop around the + // target's Evaluate() so innerModuleEvaluation 11.c.v can tell the + // Nitro self-deadlock from an unrelated parallel dynamic import. See + // #30651. + CyclicModuleRecord* dynamicImportInitiator() const; + void setDynamicImportInitiator(VM&, CyclicModuleRecord*); +#endif + private: ModuleLoaderPayload(VM&, Structure*, JSPromise*, bool deferred); @@ -71,6 +83,9 @@ class ModuleLoaderPayload final : public JSCell { WriteBarrier m_promise; WriteBarrier m_fulfillment; +#if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_dynamicImportInitiator; +#endif uint8_t m_remainingFulfillments { 2 }; bool m_deferred { false }; }; diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp b/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp index babfdf32caaa9..fb3bf6c25ea2e 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp @@ -88,6 +88,18 @@ JSModuleLoader::ModuleReferrer ModuleLoadingContext::referrer() const return uncheckedDowncast(ref); } +#if USE(BUN_JSC_ADDITIONS) +void ModuleLoadingContext::setDynamicImportInitiator(VM& vm, CyclicModuleRecord* module) +{ + m_dynamicImportInitiator.set(vm, this, module); +} + +CyclicModuleRecord* ModuleLoadingContext::dynamicImportInitiator() const +{ + return m_dynamicImportInitiator.get(); +} +#endif + template void ModuleLoadingContext::visitChildrenImpl(JSCell* cell, Visitor& visitor) { @@ -98,6 +110,9 @@ void ModuleLoadingContext::visitChildrenImpl(JSCell* cell, Visitor& visitor) visitor.append(thisObject->m_entry); visitor.append(thisObject->m_referrer); visitor.append(thisObject->m_module); +#if USE(BUN_JSC_ADDITIONS) + visitor.append(thisObject->m_dynamicImportInitiator); +#endif } DEFINE_VISIT_CHILDREN(ModuleLoadingContext); diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h index cfdf6a2c9a4cb..77053bf97ed65 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h @@ -32,6 +32,7 @@ namespace JSC { +class CyclicModuleRecord; class ModuleRegistryEntry; class ScriptFetcher; @@ -78,6 +79,16 @@ class ModuleLoadingContext final : public JSCell { bool dynamic() const { return m_flags.contains(ModuleLoadFlag::Dynamic); } bool useImportMap() const { return m_flags.contains(ModuleLoadFlag::UseImportMap); } bool deferred() const { return m_flags.contains(ModuleLoadFlag::Deferred); } +#if USE(BUN_JSC_ADDITIONS) + // Dynamic-import-only: the CyclicModuleRecord whose body is awaiting + // this import's result. Set by loadModule's dynamic overload if the + // caller can name a module referrer; used by dynamicImportLoadSettled + // to push/pop around the target's Evaluate() so innerModuleEvaluation's + // 11.c.v can distinguish the Nitro self-deadlock from a parallel + // unrelated dynamic import (#30651). + void setDynamicImportInitiator(VM&, CyclicModuleRecord*); + CyclicModuleRecord* dynamicImportInitiator() const; +#endif private: ModuleLoadingContext(VM&, Structure*, Step, const JSModuleLoader::ModuleReferrer&, AbstractModuleRecord::ModuleRequest&&, JSCell* payload, ModuleRegistryEntry*, RefPtr); @@ -91,6 +102,9 @@ class ModuleLoadingContext final : public JSCell { WriteBarrier m_referrer; WriteBarrier m_module; OptionSet m_flags; +#if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_dynamicImportInitiator; +#endif }; } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/VM.cpp b/Source/JavaScriptCore/runtime/VM.cpp index 400172f6cc837..821c12c65420d 100644 --- a/Source/JavaScriptCore/runtime/VM.cpp +++ b/Source/JavaScriptCore/runtime/VM.cpp @@ -2117,4 +2117,23 @@ Wasm::DebugState* VM::debugState() } #endif +#if USE(BUN_JSC_ADDITIONS) +void VM::pushDynamicImportInitiator(CyclicModuleRecord* module) +{ + if (module) + m_modulesAwaitingDynamicImport.add(module); +} + +void VM::popDynamicImportInitiator(CyclicModuleRecord* module) +{ + if (module) + m_modulesAwaitingDynamicImport.remove(module); +} + +bool VM::isModuleAwaitingDynamicImport(CyclicModuleRecord* module) const +{ + return module && m_modulesAwaitingDynamicImport.contains(module); +} +#endif + } // namespace JSC diff --git a/Source/JavaScriptCore/runtime/VM.h b/Source/JavaScriptCore/runtime/VM.h index 13f77f2430ac4..6dc5b9ba90824 100644 --- a/Source/JavaScriptCore/runtime/VM.h +++ b/Source/JavaScriptCore/runtime/VM.h @@ -55,6 +55,7 @@ WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN #include #include #include +#include #include #include #include @@ -115,6 +116,7 @@ class CompactTDZEnvironmentMap; class ConservativeRoots; class ControlFlowProfiler; class CrossTaskToken; +class CyclicModuleRecord; class Exception; class ExceptionScope; class FuzzerAgent; @@ -1158,6 +1160,26 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { int64_t incrementModuleAsyncEvaluationCount() { return m_moduleAsyncEvaluationCount++; } #if USE(BUN_JSC_ADDITIONS) int64_t moduleAsyncEvaluationCount() const { return m_moduleAsyncEvaluationCount; } + + // Track initiator modules of dynamic imports whose target Evaluate() + // is currently in progress. The AbstractModuleRecord re-entrancy skip + // at innerModuleEvaluation 11.c.v uses this to tell apart the Nitro + // self-deadlock (target of `await import()` is currently evaluating, + // and the DFS encounters the initiator as a still-EvaluatingAsync TLA + // dep — skipping is required, otherwise spec-mandated wait deadlocks) + // from an unrelated parallel dynamic import that happens to walk into + // a TLA dep left suspended by an earlier Evaluate() (no deadlock risk; + // the spec wait is correct — see #30651). + // + // hasPendingDynamicImport() gates whether the new initiator-based + // discriminator fires at all: embedders that don't plumb the referrer + // through to requestImportModule leave the set empty, and should keep + // the pre-#30651 behaviour (deadlock avoidance without the fourth + // narrowing condition). + void pushDynamicImportInitiator(CyclicModuleRecord* module); + void popDynamicImportInitiator(CyclicModuleRecord* module); + bool isModuleAwaitingDynamicImport(CyclicModuleRecord* module) const; + bool hasPendingDynamicImport() const { return !m_modulesAwaitingDynamicImport.isEmpty(); } #endif #if ENABLE(WEBASSEMBLY_DEBUGGER) @@ -1323,6 +1345,12 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { SynchronousModuleQueue* prev { nullptr }; }; SynchronousModuleQueue* m_synchronousModuleQueue { nullptr }; + + // Raw pointers are safe because every inserter pops before the module + // record becomes unreachable (the set entry is tied to an in-flight + // `await import()` whose C++-side owner keeps the module alive). Small + // expected size (1-2 entries) keeps contains() O(1) in practice. + HashCountedSet m_modulesAwaitingDynamicImport; private: #endif