From ae5228e01004284e5f64284fff6ce7ee8035a69f Mon Sep 17 00:00:00 2001 From: robobun Date: Tue, 5 May 2026 14:26:41 +0000 Subject: [PATCH] module-loader: don't double-fire moduleRegistryModuleSettled after inline sync replay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hostLoadImportedModule's synchronous-replay path (JSModuleLoader.cpp branch `fetchPromise is Fulfilled`, line 712-725) calls makeModule + fetchComplete + modulePromise->fulfillPromise inline while a require(esm) drains the synchronous module queue. If a ModuleRegistryFetchSettled reaction had already run on the *normal* microtask queue for that same entry before the require(esm) entered sync mode, it left a ModuleRegistryModuleSettled reaction queued there too. When the normal queue later drained, that stale reaction re-entered fetchComplete on an entry whose status was now Fetched, tripping the "m_status == Fetching" assertion at ModuleRegistryEntry.cpp:254 (SIGABRT on Linux, arm64 PAC IB trap / SIGTRAP on macOS). moduleRegistryFetchSettled already guards this with `if (modulePromise->status() != Pending) return;` — apply the same guard symmetrically to moduleRegistryModuleSettled. Fixes oven-sh/bun#30281. --- Source/JavaScriptCore/runtime/JSMicrotask.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Source/JavaScriptCore/runtime/JSMicrotask.cpp b/Source/JavaScriptCore/runtime/JSMicrotask.cpp index 2afe45a3d9f4..ce797f31b0d8 100644 --- a/Source/JavaScriptCore/runtime/JSMicrotask.cpp +++ b/Source/JavaScriptCore/runtime/JSMicrotask.cpp @@ -861,6 +861,18 @@ static void moduleRegistryModuleSettled(JSGlobalObject* globalObject, VM& vm, st auto* entry = uncheckedDowncast(arguments[2]); auto* modulePromise = uncheckedDowncast(arguments[0]); auto status = static_cast(payload); +#if USE(BUN_JSC_ADDITIONS) + // hostLoadImportedModule's synchronous-replay path (JSModuleLoader.cpp + // branch `fetchPromise is Fulfilled`) may have already called + // fetchComplete + fulfilled modulePromise inline while a require(esm) + // was draining the synchronous queue. The normal-queue copy of this + // reaction — queued when moduleRegistryFetchSettled ran earlier on + // the normal microtask queue — would otherwise re-enter fetchComplete + // and trip its "m_status == Fetching" assertion. Match the same + // pending-modulePromise guard used in moduleRegistryFetchSettled. + if (modulePromise->status() != JSPromise::Status::Pending) + return; +#endif if (status == JSPromise::Status::Fulfilled) { auto* moduleRecord = downcast(arguments[1]); entry->fetchComplete(globalObject, moduleRecord);