Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion Source/JavaScriptCore/runtime/JSMicrotask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1177,6 +1177,10 @@ static void moduleLoadTopSettled(JSGlobalObject* globalObject, VM& vm, ThrowScop
}
#if USE(BUN_JSC_ADDITIONS)
}
// The entry the key holds now, registered by provideFetch() just above or
// by provideModule() before the load, is the one the loadModule below
// loads; moduleLoadTopRejected stores into it.
context->setEntry(vm, globalObject->moduleLoader()->ensureRegistered(globalObject, specifier, type));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 ensureRegistered() re-registers a removed provideModule() key with a fresh Status::New entry, tripping the hostLoadImportedModule assert

Extended reasoning...

In the Bun provideModule() path, the outer context's entry is now recorded via ensureRegistered(specifier, type) after the !inherits<AbstractModuleRecord>() guard closes but before the inner loadModule call. Unlike the sibling read this PR adds at JSModuleLoader.cpp:852 (getRegisteredMayBeNull + null-check at :860-861), ensureRegistered() creates and inserts a brand-new Status::New entry when the key is absent. That happens whenever an embedder-preregistered entry (provideModule → status Fetched, so ensureFetchPromise fulfilled the fetch promise with the record) is removeEntry()'d in the window between the first loadModule overload scheduling ModuleLoadTopSettled and this microtask running. The freshly-inserted New entry is then found by hostLoadImportedModule (either via the symbol fast path at JSModuleLoader.cpp:674-675 or via the resolved-key lookup at :721) and hits ASSERT(mapEntry->status() != ModuleRegistryEntry::Status::New) at JSModuleLoader.cpp:727. On the base branch nothing was registered here, so hostLoadImportedModule fell through to the…

Verification: normal — the new ensureRegistered() call can insert a Status::New entry that the immediately-following hostLoadImportedModule then finds and asserts against; the base branch inserted nothing at this point. Trigger (provideModule path with removal during the fetch window): 1. Bun pre-registers key K via provideModule() (entry status Fetched, has a record) — the PR body itself cites this…

#endif

JSPromise* statePromise = JSPromise::create(vm, globalObject->promiseStructure());
Expand Down Expand Up @@ -1259,11 +1263,19 @@ static void moduleLoadTopRejected(JSGlobalObject* globalObject, VM& vm, std::spa
if (status == JSPromise::Status::Fulfilled)
resultPromise->fulfill(vm, arguments[1]);
else {
#if USE(BUN_JSC_ADDITIONS)
// The entry this load loaded (ModuleLoadingContext::setEntry). Null when
// the load failed at its fetch: moduleLoadTopSettled registers nothing for
// that, or has stored the error itself, so there is nothing left to store.
ModuleRegistryEntry* entry = context->entry();
#else
const Identifier& specifier = context->moduleRequest().m_specifier;
auto type = context->moduleRequest().type();
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script step 13.1
// Only set an error if the entry already exists.
if (ModuleRegistryEntry* entry = globalObject->moduleLoader()->getRegisteredMayBeNull(specifier, type))
ModuleRegistryEntry* entry = globalObject->moduleLoader()->getRegisteredMayBeNull(specifier, type);
#endif
if (entry)
entry->setEvaluationError(globalObject, arguments[1]);
resultPromise->reject(vm, arguments[1]);
}
Expand Down Expand Up @@ -1416,9 +1428,15 @@ static void moduleLoadStoreError(JSGlobalObject* globalObject, std::span<const J
JSValue errorValue = arguments[1];
const Identifier& specifier = context->moduleRequest().m_specifier;
auto type = context->moduleRequest().type();
#if USE(BUN_JSC_ADDITIONS)
// The entry this load loaded (ModuleLoadingContext::setEntry), recorded by
// the loadModule overload that created this context.
ModuleRegistryEntry* entry = context->entry();
#else
// https://html.spec.whatwg.org/multipage/webappapis.html#fetch-a-single-module-script step 13.1
// Only set an error if the entry already exists.
ModuleRegistryEntry* entry = globalObject->moduleLoader()->getRegisteredMayBeNull(specifier, type);
#endif
if (!entry)
return;
if (auto* error = dynamicDowncast<ErrorInstance>(errorValue)) {
Expand Down
12 changes: 12 additions & 0 deletions Source/JavaScriptCore/runtime/JSModuleLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -844,10 +844,22 @@ JSPromise* JSModuleLoader::loadModule(JSGlobalObject* globalObject, const Module
VM& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

#if USE(BUN_JSC_ADDITIONS)
// Every caller has just registered the key (provideFetch()). That entry is the
// one this load links and evaluates, and the one moduleLoadStoreError stores
// its failure into. Read before hostLoadImportedModule(), whose resolve() hook
// runs host code, so that it is the entry moduleLoadTopSettled recorded too.
ModuleRegistryEntry* entry = getRegisteredMayBeNull(moduleRequest.m_specifier, moduleRequest.type());
#endif

JSPromise* promise = hostLoadImportedModule(globalObject, referrer, moduleRequest, payload, scriptFetcher, flags.contains(ModuleLoadFlag::UseImportMap));
RETURN_IF_EXCEPTION(scope, nullptr);

auto* context = ModuleLoadingContext::create(vm, moduleRequest, WTF::move(scriptFetcher), flags);
#if USE(BUN_JSC_ADDITIONS)
if (entry)
context->setEntry(vm, entry);
#endif
JSPromise* resultPromise = JSPromise::create(vm, globalObject->promiseStructure());
resultPromise->markAsHandled();

Expand Down
6 changes: 4 additions & 2 deletions Source/JavaScriptCore/runtime/JSModuleLoader.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,10 @@ class JSModuleLoader final : public JSCell {
// delete every (specifier, type) variant — text/json/HostDefined etc.
//
// A load of the removed entry may still be in flight. Its steps keep
// the detached entry, and isCacheableLoadedModule() keeps the record
// they produce out of m_loadedModules. Callers hold cellLock().
// the detached entry (ModuleLoadingContext::setEntry for the top-level
// ones) and store its outcome there, and isCacheableLoadedModule()
// keeps the record they produce out of m_loadedModules. Callers hold
// cellLock().
auto* impl = key.impl();
m_loadedModules.removeIf([&](auto& entry) { return entry.key.first == impl; });
m_resolutionFailures.removeIf([&](auto& entry) { return entry.key.first == impl || entry.key.second == impl; });
Expand Down
7 changes: 7 additions & 0 deletions Source/JavaScriptCore/runtime/ModuleLoadingContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ ModuleLoadingContext* ModuleLoadingContext::create(VM& vm, const AbstractModuleR
return context;
}

#if USE(BUN_JSC_ADDITIONS)
void ModuleLoadingContext::setEntry(VM& vm, ModuleRegistryEntry* entry)
{
m_entry.set(vm, this, entry);
}
#endif

JSModuleLoader::ModuleReferrer ModuleLoadingContext::referrer() const
{
JSValue ref = m_referrer.get();
Expand Down
11 changes: 11 additions & 0 deletions Source/JavaScriptCore/runtime/ModuleLoadingContext.h
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,17 @@ class ModuleLoadingContext final : public JSCell {
const AbstractModuleRecord::ModuleRequest& moduleRequest() const { return m_moduleRequest; }
JSCell* payload() const { return m_payload.get(); }
ModuleRegistryEntry* entry() const { return m_entry.get(); }
#if USE(BUN_JSC_ADDITIONS)
// A top-level load (second create() overload) has no entry until its fetch
// has settled and moduleLoadTopSettled has registered the key. The loader
// then records the entry the load goes on to load, and moduleLoadTopRejected
// and moduleLoadStoreError store the load's failure into that entry instead
// of into whatever entry the key holds by then. The two differ once the host
// has removed the entry while the load was in flight (removeEntry()): the key
// may by then hold the entry of a replacement load, which must not inherit
// the removed load's error.
void setEntry(VM&, ModuleRegistryEntry*);
#endif
ScriptFetcher* scriptFetcher() const { return m_scriptFetcher.get(); }
AbstractModuleRecord* module() const { return m_module.get(); }
void module(VM& vm, AbstractModuleRecord* mod) { m_module.set(vm, this, mod); }
Expand Down
Loading