From 4e9f0037b575b58e9caa070e57d3ae5665759c70 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 29 Jul 2026 04:42:35 +0000 Subject: [PATCH] SynchronousModuleQueue: replay diverted reactions against their own realm drainSynchronousModuleQueue ran every captured reaction with the caller's globalObject. That was fine for the original require(esm) path (one realm), but when a ShadowRealm's module loader is entered while the queue is active, its reactions were replayed against the main realm's module registry instead of the ShadowRealm's. The module ended up registered in the wrong place, so a second importValue for the same specifier fetched a fresh instance instead of the cached one. Store the diverting realm on SynchronousModuleTask (it is already in scope at all four diversion points as the value queueMicrotask would have used), replay against it in drainSynchronousModuleQueue, and visit it in visitAggregateImpl. --- Source/JavaScriptCore/runtime/JSModuleLoader.cpp | 8 ++++++-- Source/JavaScriptCore/runtime/JSPromise.cpp | 8 ++++---- Source/JavaScriptCore/runtime/VM.cpp | 1 + Source/JavaScriptCore/runtime/VM.h | 1 + 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp index f7c2294e7ab3a..1b3586fee0ec0 100644 --- a/Source/JavaScriptCore/runtime/JSModuleLoader.cpp +++ b/Source/JavaScriptCore/runtime/JSModuleLoader.cpp @@ -1189,7 +1189,11 @@ void JSModuleLoader::drainSynchronousModuleQueue(JSGlobalObject* globalObject) while (i < tasks.size()) { auto t = tasks[i++]; std::array args { { t.arg0, t.arg1, t.arg2, jsUndefined() } }; - runInternalMicrotask(globalObject, vm, t.task, t.payload, args); + // The reaction may belong to a different realm than the drain's caller + // (e.g. a ShadowRealm module load that started while this queue was + // active). Run it against the realm it was diverted from so its module + // loader sees its own registry, matching what queueMicrotask would do. + runInternalMicrotask(t.globalObject, vm, t.task, t.payload, args); if (scope.exception()) [[unlikely]] { // The remaining entries are reactions that performPromiseThen…/ // triggerPromiseReactions diverted off the global microtask queue. @@ -1199,7 +1203,7 @@ void JSModuleLoader::drainSynchronousModuleQueue(JSGlobalObject* globalObject) // the exception. They run on the next normal microtask drain. while (i < tasks.size()) { auto rest = tasks[i++]; - globalObject->queueMicrotask(vm, rest.task, rest.payload, rest.arg0, rest.arg1, rest.arg2); + rest.globalObject->queueMicrotask(vm, rest.task, rest.payload, rest.arg0, rest.arg1, rest.arg2); } tasks.shrink(0); return; diff --git a/Source/JavaScriptCore/runtime/JSPromise.cpp b/Source/JavaScriptCore/runtime/JSPromise.cpp index d73cf3cf298cc..cb15817414a8b 100644 --- a/Source/JavaScriptCore/runtime/JSPromise.cpp +++ b/Source/JavaScriptCore/runtime/JSPromise.cpp @@ -491,7 +491,7 @@ void JSPromise::performPromiseThenWithInternalMicrotask(VM& vm, InternalMicrotas #if USE(BUN_JSC_ADDITIONS) if (vm.m_synchronousModuleQueue && isModuleLoaderInternalMicrotask(task)) [[unlikely]] { markAsHandled(); - vm.m_synchronousModuleQueue->tasks.append({ task, static_cast(Status::Rejected), cellValue, settled, context }); + vm.m_synchronousModuleQueue->tasks.append({ task, static_cast(Status::Rejected), globalObject, cellValue, settled, context }); break; } #endif @@ -504,7 +504,7 @@ void JSPromise::performPromiseThenWithInternalMicrotask(VM& vm, InternalMicrotas JSValue settled = settlementValue(); #if USE(BUN_JSC_ADDITIONS) if (vm.m_synchronousModuleQueue && isModuleLoaderInternalMicrotask(task)) [[unlikely]] { - vm.m_synchronousModuleQueue->tasks.append({ task, static_cast(Status::Fulfilled), cellValue, settled, context }); + vm.m_synchronousModuleQueue->tasks.append({ task, static_cast(Status::Fulfilled), globalObject, cellValue, settled, context }); break; } #endif @@ -576,7 +576,7 @@ ALWAYS_INLINE void JSPromise::settleInlineInternalMicrotask(VM& vm, JSGlobalObje setPackedCell(vm, settledFlags, nullptr); #if USE(BUN_JSC_ADDITIONS) if (vm.m_synchronousModuleQueue && isModuleLoaderInternalMicrotask(task)) [[unlikely]] { - vm.m_synchronousModuleQueue->tasks.append({ task, static_cast(newStatus), cellValue, argument, context }); + vm.m_synchronousModuleQueue->tasks.append({ task, static_cast(newStatus), globalObject, cellValue, argument, context }); return; } #endif @@ -892,7 +892,7 @@ void JSPromise::triggerPromiseReactions(VM& vm, JSGlobalObject* globalObject, St arg = slimReaction->handlerOrContext(); #if USE(BUN_JSC_ADDITIONS) if (vm.m_synchronousModuleQueue && isModuleLoaderInternalMicrotask(task)) [[unlikely]] { - vm.m_synchronousModuleQueue->tasks.append({ task, static_cast(status), promise, handler, arg }); + vm.m_synchronousModuleQueue->tasks.append({ task, static_cast(status), globalObject, promise, handler, arg }); return; } #endif diff --git a/Source/JavaScriptCore/runtime/VM.cpp b/Source/JavaScriptCore/runtime/VM.cpp index 9647ab39e2170..ed5a1d87bf14f 100644 --- a/Source/JavaScriptCore/runtime/VM.cpp +++ b/Source/JavaScriptCore/runtime/VM.cpp @@ -1900,6 +1900,7 @@ void VM::visitAggregateImpl(Visitor& visitor) // module evaluates) and mark every pending task's arguments. for (auto* q = m_synchronousModuleQueue; q; q = q->prev) { for (auto& t : q->tasks) { + visitor.appendUnbarriered(t.globalObject); visitor.appendUnbarriered(t.arg0); visitor.appendUnbarriered(t.arg1); visitor.appendUnbarriered(t.arg2); diff --git a/Source/JavaScriptCore/runtime/VM.h b/Source/JavaScriptCore/runtime/VM.h index e57882627c62c..06025fc99681c 100644 --- a/Source/JavaScriptCore/runtime/VM.h +++ b/Source/JavaScriptCore/runtime/VM.h @@ -1312,6 +1312,7 @@ class VM : public ThreadSafeRefCountedWithSuppressingSaferCPPChecking { struct SynchronousModuleTask { InternalMicrotask task; uint8_t payload; + JSGlobalObject* globalObject; JSValue arg0; JSValue arg1; JSValue arg2;