From a7646ebf257ecaff88cfbeb412197cf6d8b33653 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:53:52 +0000 Subject: [PATCH 1/2] JSC: evaluate dynamically imported modules under the importer's async context A dynamic import() starts its module load synchronously from requestImportModule, but the graph's link+evaluate runs several internal microtasks later, by which point the async context slot (m_asyncContextData, backing Bun's AsyncLocalStorage) has been reset. The imported module's top-level code, and the static dependencies it pulls in, therefore evaluate with no active store, where Node reports the store that was active at the import() call site. Capture the slot in loadModule() when ModuleLoadFlag::Dynamic is set, carry it on ModuleLoadingContext into ModuleLoaderPayload (next to referrerAsyncOrder, which already travels the same route), and install it with AsyncContextSwapScope around module->evaluate() in ContinueDynamicImport's linkAndEvaluateClosure, and around the eager async-dependency evaluation on the import.defer() path. Top-level await inside the imported module keeps working without further changes: resolveWithInternalMicrotaskForAsyncAwait snapshots the slot at the await, and AsyncModuleExecutionResume reinstalls it. Fixes oven-sh/bun#32693. --- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 15 +++++++++++++-- Source/JavaScriptCore/runtime/JSModuleLoader.cpp | 14 +++++++++++++- .../runtime/ModuleLoaderPayload.cpp | 11 ++++++++--- .../JavaScriptCore/runtime/ModuleLoaderPayload.h | 7 +++++-- .../runtime/ModuleLoadingContext.cpp | 11 ++++++++--- .../JavaScriptCore/runtime/ModuleLoadingContext.h | 7 +++++-- 6 files changed, 52 insertions(+), 13 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 7bf56f7caa3a..ec21017c9d7d 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -1192,7 +1192,7 @@ static void moduleLoadTopSettled(JSGlobalObject* globalObject, VM& vm, ThrowScop innerLoadFlags.add(ModuleLoadFlag::UseImportMap); if (context->dynamic()) { #if USE(BUN_JSC_ADDITIONS) - combinedCell = ModuleLoaderPayload::create(vm, statePromise, context->deferred(), context->referrerAsyncOrder()); + combinedCell = ModuleLoaderPayload::create(vm, statePromise, context->deferred(), context->referrerAsyncOrder(), context->importerAsyncContext()); #else combinedCell = ModuleLoaderPayload::create(vm, statePromise, context->deferred()); #endif @@ -1485,7 +1485,13 @@ static void dynamicImportLoadSettled(JSGlobalObject* globalObject, VM& vm, Throw if (!deferred) { // 6.c. Let evaluatePromise be module.Evaluate(). #if USE(BUN_JSC_ADDITIONS) - JSPromise* evaluatePromise = module->evaluate(globalObject, dynamicPayload->referrerAsyncOrder()); + JSPromise* evaluatePromise = nullptr; + { + // The module bodies in this graph are the continuation of the import() call, so + // they run under the async context captured there (JSModuleLoader::loadModule). + AsyncContextSwapScope asyncContextScope(vm, globalObject, dynamicPayload->importerAsyncContext()); + evaluatePromise = module->evaluate(globalObject, dynamicPayload->referrerAsyncOrder()); + } #else JSPromise* evaluatePromise = module->evaluate(globalObject); #endif @@ -1516,6 +1522,11 @@ static void dynamicImportLoadSettled(JSGlobalObject* globalObject, VM& vm, Throw // For each Module Record dep of evaluationList, append dep.Evaluate() to asyncDepsEvaluationPromises. MarkedArgumentBuffer asyncDepsEvaluationPromises; +#if USE(BUN_JSC_ADDITIONS) + // As above: the eagerly evaluated async dependencies are the continuation of the + // import.defer() call, so they run under the async context captured there. + AsyncContextSwapScope asyncContextScope(vm, globalObject, dynamicPayload->importerAsyncContext()); +#endif for (AbstractModuleRecord* dep : evaluationList) { #if USE(BUN_JSC_ADDITIONS) JSPromise* depPromise = dep->evaluate(globalObject, dynamicPayload->referrerAsyncOrder()); diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index 8f36cd3c96f5..d8e48a8ef306 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -50,6 +50,9 @@ #include "SyntheticModuleRecord.h" #include "TopExceptionScope.h" #include "VMTrapsInlines.h" +#if USE(BUN_JSC_ADDITIONS) +#include "AsyncContextSwapScope.h" +#endif #include namespace JSC { @@ -354,10 +357,19 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi JSPromise* promise = nullptr; ScriptFetchParameters::Type type = parameters ? parameters->type() : ScriptFetchParameters::Type::JavaScript; + JSValue importerAsyncContext = jsUndefined(); #if USE(BUN_JSC_ADDITIONS) // Preserve the caller's ScriptFetchParameters (HostDefined data) into the loading context // before `parameters` is moved into fetch() below. RefPtr contextParameters = parameters ? parameters : ScriptFetchParameters::create(type); + + // A dynamic import() evaluates its module graph under the async context (AsyncLocalStorage + // store) that was active at the import() call site. We are still synchronous with that call + // here, so the slot holds the importer's context; the graph is linked and evaluated several + // internal microtasks later, by which point the slot has been reset. Capture it now, before + // fetch() below can run host code, and carry it through to dynamicImportLoadSettled. + if (flags.contains(ModuleLoadFlag::Dynamic)) + importerAsyncContext = AsyncContextSwapScope::current(globalObject); #endif if (ModuleRegistryEntry* entry = getRegisteredMayBeNull(specifier, type)) { @@ -384,7 +396,7 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Identi #else AbstractModuleRecord::ModuleRequest request { specifier, ScriptFetchParameters::create(type) }; #endif - auto* context = ModuleLoadingContext::create(vm, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder); + auto* context = ModuleLoadingContext::create(vm, request, WTF::move(scriptFetcher), flags, referrerAsyncOrder, importerAsyncContext); JSPromise* intermediatePromise = JSPromise::create(vm, globalObject->promiseStructure()); intermediatePromise->markAsHandled(); diff --git a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp index f171c676eafa..1b587332ec94 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp +++ b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.cpp @@ -33,16 +33,18 @@ namespace JSC { const ClassInfo ModuleLoaderPayload::s_info = { "ModuleLoaderPayload"_s, nullptr, nullptr, nullptr, CREATE_METHOD_TABLE(ModuleLoaderPayload) }; -ModuleLoaderPayload::ModuleLoaderPayload(VM& vm, Structure* structure, JSPromise* promise, bool deferred, int64_t referrerAsyncOrder) +ModuleLoaderPayload::ModuleLoaderPayload(VM& vm, Structure* structure, JSPromise* promise, bool deferred, int64_t referrerAsyncOrder, JSValue importerAsyncContext) : Base(vm, structure) , m_promise(promise, WriteBarrierEarlyInit) #if USE(BUN_JSC_ADDITIONS) + , m_importerAsyncContext(importerAsyncContext, WriteBarrierEarlyInit) , m_referrerAsyncOrder(referrerAsyncOrder) #endif , m_deferred(deferred) { #if !USE(BUN_JSC_ADDITIONS) UNUSED_PARAM(referrerAsyncOrder); + UNUSED_PARAM(importerAsyncContext); #endif } @@ -60,13 +62,16 @@ 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_importerAsyncContext); +#endif } DEFINE_VISIT_CHILDREN(ModuleLoaderPayload); -ModuleLoaderPayload* ModuleLoaderPayload::create(VM& vm, JSPromise* promise, bool deferred, int64_t referrerAsyncOrder) +ModuleLoaderPayload* ModuleLoaderPayload::create(VM& vm, JSPromise* promise, bool deferred, int64_t referrerAsyncOrder, JSValue importerAsyncContext) { - ModuleLoaderPayload* instance = new (NotNull, allocateCell(vm)) ModuleLoaderPayload(vm, vm.moduleLoaderPayloadStructure.get(), promise, deferred, referrerAsyncOrder); + ModuleLoaderPayload* instance = new (NotNull, allocateCell(vm)) ModuleLoaderPayload(vm, vm.moduleLoaderPayloadStructure.get(), promise, deferred, referrerAsyncOrder, importerAsyncContext); instance->finishCreation(vm); return instance; } diff --git a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h index 5e91d69c2440..5ae973c9e308 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h +++ b/Source/JavaScriptCore/runtime/ModuleLoaderPayload.h @@ -49,7 +49,7 @@ class ModuleLoaderPayload final : public JSCell { } inline static Structure* createStructure(VM&, JSGlobalObject*, JSValue); - static ModuleLoaderPayload* create(VM&, JSPromise*, bool deferred = false, int64_t referrerAsyncOrder = -1); + static ModuleLoaderPayload* create(VM&, JSPromise*, bool deferred = false, int64_t referrerAsyncOrder = -1, JSValue importerAsyncContext = jsUndefined()); JSPromise* promise() const { return m_promise.get(); } @@ -59,6 +59,8 @@ class ModuleLoaderPayload final : public JSCell { bool deferred() const { return m_deferred; } #if USE(BUN_JSC_ADDITIONS) int64_t referrerAsyncOrder() const { return m_referrerAsyncOrder; } + // The async context (AsyncLocalStorage store) active at the import() call site. + JSValue importerAsyncContext() const { return m_importerAsyncContext.get(); } #endif bool decrementRemaining() @@ -68,13 +70,14 @@ class ModuleLoaderPayload final : public JSCell { } private: - ModuleLoaderPayload(VM&, Structure*, JSPromise*, bool deferred, int64_t referrerAsyncOrder); + ModuleLoaderPayload(VM&, Structure*, JSPromise*, bool deferred, int64_t referrerAsyncOrder, JSValue importerAsyncContext); void finishCreation(VM&); WriteBarrier m_promise; WriteBarrier m_fulfillment; #if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_importerAsyncContext; int64_t m_referrerAsyncOrder { -1 }; #endif uint8_t m_remainingFulfillments { 2 }; diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp b/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp index e619c0f64fd5..4bae68ff8403 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp @@ -62,24 +62,26 @@ ModuleLoadingContext* ModuleLoadingContext::create(VM& vm, Step step, const JSMo return context; } -ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, AbstractModuleRecord::ModuleRequest&& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder) +ModuleLoadingContext::ModuleLoadingContext(VM& vm, Structure* structure, AbstractModuleRecord::ModuleRequest&& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder, JSValue importerAsyncContext) : Base(vm, structure) , m_moduleRequest(WTF::move(moduleRequest)) , m_scriptFetcher(WTF::move(scriptFetcher)) #if USE(BUN_JSC_ADDITIONS) + , m_importerAsyncContext(importerAsyncContext, WriteBarrierEarlyInit) , m_referrerAsyncOrder(referrerAsyncOrder) #endif , m_flags(flags) { #if !USE(BUN_JSC_ADDITIONS) UNUSED_PARAM(referrerAsyncOrder); + UNUSED_PARAM(importerAsyncContext); #endif } -ModuleLoadingContext* ModuleLoadingContext::create(VM& vm, const AbstractModuleRecord::ModuleRequest& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder) +ModuleLoadingContext* ModuleLoadingContext::create(VM& vm, const AbstractModuleRecord::ModuleRequest& moduleRequest, RefPtr scriptFetcher, OptionSet flags, int64_t referrerAsyncOrder, JSValue importerAsyncContext) { AbstractModuleRecord::ModuleRequest requestCopy { moduleRequest }; - auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), WTF::move(requestCopy), WTF::move(scriptFetcher), flags, referrerAsyncOrder); + auto* context = new (NotNull, allocateCell(vm)) ModuleLoadingContext(vm, vm.moduleLoadingContextStructure.get(), WTF::move(requestCopy), WTF::move(scriptFetcher), flags, referrerAsyncOrder, importerAsyncContext); context->finishCreation(vm); return context; } @@ -104,6 +106,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_importerAsyncContext); +#endif } DEFINE_VISIT_CHILDREN(ModuleLoadingContext); diff --git a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h index 5de814a243a1..c50fc7ad8ea7 100644 --- a/Source/JavaScriptCore/runtime/ModuleLoadingContext.h +++ b/Source/JavaScriptCore/runtime/ModuleLoadingContext.h @@ -61,7 +61,7 @@ class ModuleLoadingContext final : public JSCell { }; static ModuleLoadingContext* create(VM&, Step, const JSModuleLoader::ModuleReferrer&, const AbstractModuleRecord::ModuleRequest&, JSCell* payload, ModuleRegistryEntry*, RefPtr); - static ModuleLoadingContext* create(VM&, const AbstractModuleRecord::ModuleRequest&, RefPtr, OptionSet, int64_t referrerAsyncOrder = -1); + static ModuleLoadingContext* create(VM&, const AbstractModuleRecord::ModuleRequest&, RefPtr, OptionSet, int64_t referrerAsyncOrder = -1, JSValue importerAsyncContext = jsUndefined()); Step step() const { return m_step; } void setStep(Step s) { m_step = s; } @@ -80,11 +80,13 @@ class ModuleLoadingContext final : public JSCell { bool deferred() const { return m_flags.contains(ModuleLoadFlag::Deferred); } #if USE(BUN_JSC_ADDITIONS) int64_t referrerAsyncOrder() const { return m_referrerAsyncOrder; } + // The async context (AsyncLocalStorage store) active at the import() call site. + JSValue importerAsyncContext() const { return m_importerAsyncContext.get(); } #endif private: ModuleLoadingContext(VM&, Structure*, Step, const JSModuleLoader::ModuleReferrer&, AbstractModuleRecord::ModuleRequest&&, JSCell* payload, ModuleRegistryEntry*, RefPtr); - ModuleLoadingContext(VM&, Structure*, AbstractModuleRecord::ModuleRequest&&, RefPtr, OptionSet, int64_t referrerAsyncOrder); + ModuleLoadingContext(VM&, Structure*, AbstractModuleRecord::ModuleRequest&&, RefPtr, OptionSet, int64_t referrerAsyncOrder, JSValue importerAsyncContext); Step m_step { Step::Main }; AbstractModuleRecord::ModuleRequest m_moduleRequest; @@ -94,6 +96,7 @@ class ModuleLoadingContext final : public JSCell { WriteBarrier m_referrer; WriteBarrier m_module; #if USE(BUN_JSC_ADDITIONS) + WriteBarrier m_importerAsyncContext; int64_t m_referrerAsyncOrder { -1 }; #endif OptionSet m_flags; From a919cd42696556a168fefc895decc119b9257480 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 12 Aug 2026 19:56:07 +0000 Subject: [PATCH 2/2] JSC: keep the importer's async context across top-level-await dependencies InnerModuleEvaluation does not execute a module whose dependency is still evaluating asynchronously; it only records an evaluation order and leaves the body to AsyncModuleExecutionFulfilled, which runs from the AsyncModuleExecutionDone microtask once the dependency settles. That microtask carried no async context, so a dynamically imported module with a top-level-await dependency still evaluated with no store even though its dependency ran under the importer's context. Snapshot the current context in executeAsync, wrapping it with the module in an InternalFieldTuple the same way resolveWithInternalMicrotaskForAsyncAwait does for AsyncModuleExecutionResume, and reinstall it in AsyncModuleExecutionDone. executeAsync runs inside the dynamicImportLoadSettled scope (directly or from an earlier AsyncModuleExecutionFulfilled), so the snapshot is the importer's context. With no context active, wrapWithCurrent returns the module itself and nothing is allocated. --- .../JavaScriptCore/runtime/CyclicModuleRecord.cpp | 10 ++++++++++ Source/JavaScriptCore/runtime/JSMicrotask.cpp | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp index 4a808be9dc80..04a937e2e74f 100644 --- a/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp +++ b/Source/JavaScriptCore/runtime/CyclicModuleRecord.cpp @@ -41,6 +41,7 @@ #include "SourceProfiler.h" #include "SymbolTableInlines.h" #if USE(BUN_JSC_ADDITIONS) +#include "AsyncContextSwapScope.h" #include "SyntheticModuleRecord.h" #endif #include "UnlinkedModuleProgramCodeBlock.h" @@ -555,7 +556,16 @@ void CyclicModuleRecord::executeAsync(JSGlobalObject* globalObject) // 7. Let onRejected be CreateBuiltinFunction(rejectedClosure, 0, "", « »). // Also handled in JSMicrotask.cpp. // 8. Perform PerformPromiseThen(capability.[[Promise]], onFulfilled, onRejected). +#if USE(BUN_JSC_ADDITIONS) + // AsyncModuleExecutionFulfilled runs the bodies of the modules waiting on this one, so + // it has to run under the async context this evaluation was started under (for a + // dynamic import(), the importer's context installed by dynamicImportLoadSettled). + // Snapshot it alongside the module, as a promise reaction would; the + // AsyncModuleExecutionDone microtask unwraps the tuple and reinstalls it. + promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, AsyncContextSwapScope::wrapWithCurrent(vm, globalObject, this)); +#else promise->performPromiseThenWithInternalMicrotask(vm, InternalMicrotask::AsyncModuleExecutionDone, nullptr, this); +#endif // 9. Perform ! module.ExecuteModule(capability). execute(globalObject, promise); RETURN_IF_EXCEPTION(scope, void()); diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index ec21017c9d7d..af1103b7952e 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -1489,6 +1489,9 @@ static void dynamicImportLoadSettled(JSGlobalObject* globalObject, VM& vm, Throw { // The module bodies in this graph are the continuation of the import() call, so // they run under the async context captured there (JSModuleLoader::loadModule). + // Modules held back by a top-level-await dependency execute later, from + // AsyncModuleExecutionDone, which reinstalls the context executeAsync snapshots + // while running inside this scope. AsyncContextSwapScope asyncContextScope(vm, globalObject, dynamicPayload->importerAsyncContext()); evaluatePromise = module->evaluate(globalObject, dynamicPayload->referrerAsyncOrder()); } @@ -2199,7 +2202,15 @@ void runInternalMicrotask(JSGlobalObject* globalObject, VM& vm, InternalMicrotas #endif case InternalMicrotask::AsyncModuleExecutionDone: { - auto* module = uncheckedDowncast(arguments[2]); + // CyclicModuleRecord::executeAsync wraps the module together with Bun's + // async context in an InternalFieldTuple when a context was active as the + // module started executing, so that the waiting ancestors executed by + // asyncExecutionFulfilled run under that context too. + JSValue contextArg = arguments[2]; +#if USE(BUN_JSC_ADDITIONS) + AsyncContextSwapScope asyncContextScope(vm, globalObject, AsyncContextSwapScope::unwrapContextTuple(contextArg)); +#endif + auto* module = uncheckedDowncast(contextArg); RELEASE_AND_RETURN(scope, asyncModuleExecutionDone(module->realm(), module, arguments[1], static_cast(payload))); }