module-loader: narrow TLA re-entrancy skip to dynamic-import initiator - #230
module-loader: narrow TLA re-entrancy skip to dynamic-import initiator#230robobun wants to merge 2 commits into
Conversation
WalkthroughThreads a dynamic-import initiator CyclicModuleRecord through loader/context/payload, records initiators in VM tracking, brackets evaluation with VM push/pop during dynamic-import settlement, and narrows the top-level-await self-skip to only apply when VM indicates the dependency is awaiting a dynamic import. ChangesDynamic Import Initiator Tracking
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Warning Review ran into problems🔥 ProblemsGit: Failed to clone repository. Please run the Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Source/JavaScriptCore/runtime/AbstractModuleRecord.cpp`:
- Around line 1220-1240: The four-way condition combining
depWasAlreadyEvaluatingAsync, cyclic->asyncEvaluationOrder().order() <
asyncOrderWatermark, cyclic->pendingAsyncDependencies().value_or(1), and
vm.isModuleAwaitingDynamicImport(cyclic) is fragile and hard to reason about;
extract this predicate into a clearly named helper (e.g., shouldSkipTLA(cyclic,
asyncOrderWatermark, vm)) that returns the boolean and replace the inline
expression with that call, and add focused integration tests covering the
cross-Evaluate dynamic-import case (`#30651`) and the Nitro self-deadlock scenario
to assert the branch taken in both situations (use depWasAlreadyEvaluatingAsync,
asyncEvaluationOrder().order(), pendingAsyncDependencies(), and
vm.isModuleAwaitingDynamicImport(cyclic) states to construct the test cases).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0094871d-1929-480e-a6ba-f70504b8c879
📥 Commits
Reviewing files that changed from the base of the PR and between 5488984 and 6f24f5f7bcac2abec37a0030bbda17d75c197f66.
📒 Files selected for processing (10)
Source/JavaScriptCore/runtime/AbstractModuleRecord.cppSource/JavaScriptCore/runtime/JSMicrotask.cppSource/JavaScriptCore/runtime/JSModuleLoader.cppSource/JavaScriptCore/runtime/JSModuleLoader.hSource/JavaScriptCore/runtime/ModuleLoaderPayload.cppSource/JavaScriptCore/runtime/ModuleLoaderPayload.hSource/JavaScriptCore/runtime/ModuleLoadingContext.cppSource/JavaScriptCore/runtime/ModuleLoadingContext.hSource/JavaScriptCore/runtime/VM.cppSource/JavaScriptCore/runtime/VM.h
Preview Builds
|
The referrer is how JSC's requestImportModule finds the initiator CyclicModuleRecord (see oven-sh/WebKit#230 / #30651). Bun had been passing an empty Identifier, which leaves the initiator unresolved — so the WebKit-side discriminator can't fire and the TLA re-entrancy skip continues to produce TDZ reads across independent dynamic imports. The registry key is the file-system path (for file:// sources) or the substring after builtin:// (for builtins), mirroring what the resolve() path above uses. Adds a regression test for #30651 covering the parallel dynamic-import case (two independent imports of the same TLA dep; the second one must wait, not run against post-await TDZ bindings). Ships with the matching WEBKIT_VERSION bump to oven-sh/WebKit#230's preview build.
298067b to
783ba19
Compare
The referrer is how JSC's requestImportModule finds the initiator CyclicModuleRecord (see oven-sh/WebKit#230 / #30651). Bun had been passing an empty Identifier, which leaves the initiator unresolved — so the WebKit-side discriminator can't fire and the TLA re-entrancy skip continues to produce TDZ reads across independent dynamic imports. The registry key is the file-system path (for file:// sources) or the substring after builtin:// (for builtins), mirroring what the resolve() path above uses. Adds a regression test for #30651 covering the parallel dynamic-import case (two independent imports of the same TLA dep; the second one must wait, not run against post-await TDZ bindings). Ships with the matching WEBKIT_VERSION bump to oven-sh/WebKit#230's preview build.
Rebased oven-sh/WebKit#230 on top of the newly-merged WebKit upgrade (#231 / 2b257999). The module-loader changes cherry-picked cleanly; only VM.h had a trivial forward-decl conflict (EagerIIFERegistry was removed upstream, CyclicModuleRecord decl stayed). Preview tarball: autobuild-preview-pr-230-783ba19a.
783ba19 to
d57ee3d
Compare
The referrer is how JSC's requestImportModule finds the initiator CyclicModuleRecord (see oven-sh/WebKit#230 / #30651). Bun had been passing an empty Identifier, which leaves the initiator unresolved — so the WebKit-side discriminator can't fire and the TLA re-entrancy skip continues to produce TDZ reads across independent dynamic imports. The registry key is the file-system path (for file:// sources) or the substring after builtin:// (for builtins), mirroring what the resolve() path above uses. Adds a regression test for #30651 covering the parallel dynamic-import case (two independent imports of the same TLA dep; the second one must wait, not run against post-await TDZ bindings). Ships with the matching WEBKIT_VERSION bump to oven-sh/WebKit#230's preview build.
Rebased oven-sh/WebKit#230 onto the latest WebKit main (post-#236 upgrade to 39d4ce1f12ea). The two commits cherry-picked with conflicts in ModuleLoadingContext.h (bool fields refactored into OptionSet<ModuleLoadFlag>), JSModuleLoader.{h,cpp} (signatures updated to take the OptionSet), and JSMicrotask.cpp (import defer split evaluate/load into an !deferred branch). All resolved by adopting upstream's OptionSet/defer structure and keeping the #30651 initiator plumbing gated under USE(BUN_JSC_ADDITIONS). Preview tarball: autobuild-preview-pr-230-d57ee3d2.
d57ee3d to
36cc128
Compare
The referrer is how JSC's requestImportModule finds the initiator CyclicModuleRecord (see oven-sh/WebKit#230 / #30651). Bun had been passing an empty Identifier, which leaves the initiator unresolved — so the WebKit-side discriminator can't fire and the TLA re-entrancy skip continues to produce TDZ reads across independent dynamic imports. The registry key is the file-system path (for file:// sources) or the substring after builtin:// (for builtins), mirroring what the resolve() path above uses. Adds a regression test for #30651 covering the parallel dynamic-import case (two independent imports of the same TLA dep; the second one must wait, not run against post-await TDZ bindings). Ships with the matching WEBKIT_VERSION bump to oven-sh/WebKit#230's preview build.
Rebased oven-sh/WebKit#230 onto the latest WebKit main (cf8fb22b7011 — LTO build config only, no source changes since my previous rebase onto #236). Clean cherry-pick, no conflicts. Preview tarball: autobuild-preview-pr-230-36cc1283.
…n 1.4.0 GA (#2827) Re-diagnosed the slice 6 blocker as a 1.3.14 TLA-under-`--isolate` TDZ regression (not a `mock.module()` issue). Filed oven-sh/bun#31410, bisected to `73e8889f8c` (WebKit module-loader rewrite, oven-sh/bun#29393), verified fixed on bun canary `1.4.0-canary.1+0974d031c` via oven-sh/bun#30656 + oven-sh/WebKit#230. Slice 6 cutover (#2802) now waits on bun 1.4.0 GA so the engine pin lifts in the same PR. Wall-clock plan posted on #2802: keep the 4-shard matrix (collapsing is slower than the current 95s slowest-shard critical path), add `--changed=origin/main` for PR runs (~95s → ~20s), duration-balanced shard partitioning, native `--parallel` worker pool. Refs #2811 (closed), #2802, #2796.
36cc128 to
d8ec41d
Compare
The referrer is how JSC's requestImportModule finds the initiator CyclicModuleRecord (see oven-sh/WebKit#230 / #30651). Bun had been passing an empty Identifier, which leaves the initiator unresolved — so the WebKit-side discriminator can't fire and the TLA re-entrancy skip continues to produce TDZ reads across independent dynamic imports. The registry key is the file-system path (for file:// sources) or the substring after builtin:// (for builtins), mirroring what the resolve() path above uses. Adds a regression test for #30651 covering the parallel dynamic-import case (two independent imports of the same TLA dep; the second one must wait, not run against post-await TDZ bindings). Ships with the matching WEBKIT_VERSION bump to oven-sh/WebKit#230's preview build.
Rebased oven-sh/WebKit#230 onto the latest WebKit main (cf8fb22b7011 — LTO build config only, no source changes since my previous rebase onto #236). Clean cherry-pick, no conflicts. Preview tarball: autobuild-preview-pr-230-36cc1283.
d8ec41d to
c7f2914
Compare
The referrer is how JSC's requestImportModule finds the initiator CyclicModuleRecord (see oven-sh/WebKit#230 / #30651). Bun had been passing an empty Identifier, which leaves the initiator unresolved — so the WebKit-side discriminator can't fire and the TLA re-entrancy skip continues to produce TDZ reads across independent dynamic imports. The registry key is the file-system path (for file:// sources) or the substring after builtin:// (for builtins), mirroring what the resolve() path above uses. Adds a regression test for #30651 covering the parallel dynamic-import case (two independent imports of the same TLA dep; the second one must wait, not run against post-await TDZ bindings). Ships with the matching WEBKIT_VERSION bump to oven-sh/WebKit#230's preview build.
Rebased oven-sh/WebKit#230 onto the latest WebKit main (cf8fb22b7011 — LTO build config only, no source changes since my previous rebase onto #236). Clean cherry-pick, no conflicts. Preview tarball: autobuild-preview-pr-230-36cc1283.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Source/JavaScriptCore/runtime/VM.cpp`:
- Around line 2097-2112: Add VM-thread/API-lock assertions and a membership
check to harden dynamic-import helpers: in VM::pushDynamicImportInitiator,
VM::popDynamicImportInitiator and VM::isModuleAwaitingDynamicImport assert that
the VM/API lock (the VM thread ownership) is held before touching
m_modulesAwaitingDynamicImport; additionally, in VM::popDynamicImportInitiator
assert that m_modulesAwaitingDynamicImport.contains(module) is true before
calling remove so an unmatched pop immediately fails rather than silently no-op.
Use the project’s existing VM/API lock assertion macro or helper when adding
these checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: a2e5ed28-b367-4f77-803a-1c8648f995cb
📥 Commits
Reviewing files that changed from the base of the PR and between 6f24f5f7bcac2abec37a0030bbda17d75c197f66 and c7f2914.
📒 Files selected for processing (10)
Source/JavaScriptCore/runtime/AbstractModuleRecord.cppSource/JavaScriptCore/runtime/JSMicrotask.cppSource/JavaScriptCore/runtime/JSModuleLoader.cppSource/JavaScriptCore/runtime/JSModuleLoader.hSource/JavaScriptCore/runtime/ModuleLoaderPayload.cppSource/JavaScriptCore/runtime/ModuleLoaderPayload.hSource/JavaScriptCore/runtime/ModuleLoadingContext.cppSource/JavaScriptCore/runtime/ModuleLoadingContext.hSource/JavaScriptCore/runtime/VM.cppSource/JavaScriptCore/runtime/VM.h
| 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); | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial | ⚡ Quick win
Assert VM-thread ownership and balanced push/pop here.
These helpers drive the re-entrancy exception, but they currently fail open: cross-thread access to m_modulesAwaitingDynamicImport is unchecked, and an unmatched popDynamicImportInitiator() silently removes nothing. Add API-lock assertions to all three helpers and assert membership before removing so a mis-bracketed caller trips immediately instead of corrupting the skip state.
Proposed hardening
`#if` USE(BUN_JSC_ADDITIONS)
void VM::pushDynamicImportInitiator(CyclicModuleRecord* module)
{
+ ASSERT(currentThreadIsHoldingAPILock());
if (module)
m_modulesAwaitingDynamicImport.add(module);
}
void VM::popDynamicImportInitiator(CyclicModuleRecord* module)
{
- if (module)
+ ASSERT(currentThreadIsHoldingAPILock());
+ if (module) {
+ ASSERT(m_modulesAwaitingDynamicImport.contains(module));
m_modulesAwaitingDynamicImport.remove(module);
+ }
}
bool VM::isModuleAwaitingDynamicImport(CyclicModuleRecord* module) const
{
+ ASSERT(currentThreadIsHoldingAPILock());
return module && m_modulesAwaitingDynamicImport.contains(module);
}
`#endif`📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 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); | |
| } | |
| `#if` USE(BUN_JSC_ADDITIONS) | |
| void VM::pushDynamicImportInitiator(CyclicModuleRecord* module) | |
| { | |
| ASSERT(currentThreadIsHoldingAPILock()); | |
| if (module) | |
| m_modulesAwaitingDynamicImport.add(module); | |
| } | |
| void VM::popDynamicImportInitiator(CyclicModuleRecord* module) | |
| { | |
| ASSERT(currentThreadIsHoldingAPILock()); | |
| if (module) { | |
| ASSERT(m_modulesAwaitingDynamicImport.contains(module)); | |
| m_modulesAwaitingDynamicImport.remove(module); | |
| } | |
| } | |
| bool VM::isModuleAwaitingDynamicImport(CyclicModuleRecord* module) const | |
| { | |
| ASSERT(currentThreadIsHoldingAPILock()); | |
| return module && m_modulesAwaitingDynamicImport.contains(module); | |
| } | |
| `#endif` |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Source/JavaScriptCore/runtime/VM.cpp` around lines 2097 - 2112, Add
VM-thread/API-lock assertions and a membership check to harden dynamic-import
helpers: in VM::pushDynamicImportInitiator, VM::popDynamicImportInitiator and
VM::isModuleAwaitingDynamicImport assert that the VM/API lock (the VM thread
ownership) is held before touching m_modulesAwaitingDynamicImport; additionally,
in VM::popDynamicImportInitiator assert that
m_modulesAwaitingDynamicImport.contains(module) is true before calling remove so
an unmatched pop immediately fails rather than silently no-op. Use the project’s
existing VM/API lock assertion macro or helper when adding these checks.
WebKit#30651) The post-WebKit#30259 re-entrancy skip in innerModuleEvaluation's 11.c.v fires on `asyncEvaluationOrder < asyncOrderWatermark`, which is true whenever the TLA dep first suspended in a *prior* Evaluate() pass. An independent dynamic import of a module that transitively depends on that still- suspended TLA gets a fresh watermark, so the second Evaluate() ends up skipping the spec wait and running against the dep's post-await TDZ bindings: ReferenceError: Cannot access 'foo' before initialization. This is the same TDZ hole as WebKit#30259 but reached through two *separate* dynamic imports rather than static siblings within one Evaluate(). Fix: track the dynamic-import initiator — the CyclicModuleRecord whose JS body is awaiting the import's result — on the VM for the duration of the target's Evaluate(). innerModuleEvaluation's 11.c.v now also requires `dep == initiator` (via VM::isModuleAwaitingDynamicImport) to skip. That keeps the Nitro self-deadlock working (target of `await import()` re-imports the initiator statically — match, skip is required) while an unrelated parallel dynamic import that happens to walk into a suspended TLA no longer matches — the spec wait fires and the importer correctly blocks until the dep settles. Plumbing: the initiator is resolved from the referrer URL in JSModuleLoader::requestImportModule, stashed on the ModuleLoadingContext at loadModule time, carried onto the ModuleLoaderPayload that outlives the context, and finally pushed/popped around module->evaluate() inside dynamicImportLoadSettled. All under USE(BUN_JSC_ADDITIONS); non-Bun builds are unchanged.
Embedders that haven't updated to pass a non-empty referrer to requestImportModule end up with an empty m_modulesAwaitingDynamicImport set and, under the first draft of this patch, always took the spec wait at 11.c.v. That regressed the Nitro-style self-deadlock tests (dep is paused at `await import()` evaluating us; waiting on its TLA promise deadlocks the import's own continuation). Gate the new initiator-based narrowing on hasPendingDynamicImport(): when the set is empty the VM is being driven by an embedder that doesn't know about referrer tracking, so fall back to the looser pre-WebKit#30651 three-condition skip. The new narrowing only kicks in once the embedder actually pushes something.
c7f2914 to
7dea873
Compare
The referrer is how JSC's requestImportModule finds the initiator CyclicModuleRecord (see oven-sh/WebKit#230 / #30651). Bun had been passing an empty Identifier, which leaves the initiator unresolved — so the WebKit-side discriminator can't fire and the TLA re-entrancy skip continues to produce TDZ reads across independent dynamic imports. The registry key is the file-system path (for file:// sources) or the substring after builtin:// (for builtins), mirroring what the resolve() path above uses. Adds a regression test for #30651 covering the parallel dynamic-import case (two independent imports of the same TLA dep; the second one must wait, not run against post-await TDZ bindings). Ships with the matching WEBKIT_VERSION bump to oven-sh/WebKit#230's preview build.
Rebased oven-sh/WebKit#230 onto the latest WebKit main (cf8fb22b7011 — LTO build config only, no source changes since my previous rebase onto #236). Clean cherry-pick, no conflicts. Preview tarball: autobuild-preview-pr-230-36cc1283.
|
Superseded by the Verified the WebKit#30651 / WebKit#30634 repro is fixed on current bun main. Closing this and the Bun-side oven-sh/bun#30656 as superseded. |
Fix for oven-sh/bun#30651.
What
The post-WebKit#30259 re-entrancy skip in
innerModuleEvaluation's 11.c.v fires onasyncEvaluationOrder < asyncOrderWatermark, which is true whenever the TLA dep first suspended in a priorEvaluate()pass. An independent dynamicimport()of a module that transitively depends on that still-suspended TLA gets a fresh watermark, so the secondEvaluate()ends up skipping the spec wait and running against the dep's post-await TDZ bindings:Same TDZ hole as WebKit#30259 (fix for oven-sh/bun#30259), but reached through two separate dynamic imports rather than static siblings inside one
Evaluate().Minimal reproducer
On canary,
entry2throwsReferenceError: Cannot access 'foo' before initialization. Node runs it correctly. With this PR + the matching bun change, bun runs it correctly too.Why the watermark alone is insufficient
All three existing conditions fire identically in both scenarios because the discriminator was designed for same-
Evaluate()re-entrancy, not cross-Evaluate()re-entrancy.Fix
Add a fourth discriminator:
dep == dynamic-import-initiator. The initiator is theCyclicModuleRecordwhose JS body is awaiting the import's result. In the Nitro self-deadlock, the target statically re-imports the initiator → match → keep skipping. In an unrelated parallel dynamic import, dep and initiator are different modules → no match → spec wait fires, and the importer correctly blocks until the dep settles.Plumbing:
JSModuleLoader::requestImportModuleresolves the referrer URL to aCyclicModuleRecordvia the module registry.loadModule(dynamic overload) stashes it on the freshly createdModuleLoadingContext.moduleLoadTopSettledcopies it from the context onto theModuleLoaderPayloadthat outlives the context.dynamicImportLoadSettledpushes it ontoVM::m_modulesAwaitingDynamicImport(aHashCountedSet) immediately before the target'sevaluate()call, and pops after.AbstractModuleRecord::innerModuleEvaluation11.c.v readsvm.isModuleAwaitingDynamicImport(cyclic)and only skips the spec wait when all four conditions hold.All new state and logic is gated
#if USE(BUN_JSC_ADDITIONS); non-Bun builds are unchanged.Bun-side PR (including the referrer plumbing in
ZigGlobalObject::moduleLoaderImportModuleand theWEBKIT_VERSIONbump once this PR lands) to follow.