Skip to content

Fix require() of ESM with diamond through barrel deadlocking (#30493)#30497

Closed
robobun wants to merge 1 commit into
mainfrom
farm/627f90b2/fix-30493-diamond-deadlock
Closed

Fix require() of ESM with diamond through barrel deadlocking (#30493)#30497
robobun wants to merge 1 commit into
mainfrom
farm/627f90b2/fix-30493-diamond-deadlock

Conversation

@robobun

@robobun robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Fixes #30493. Requires oven-sh/WebKit#223 to land first.

Repro

require('./app.js') where:

  • app.js imports a.js, b.js, and shared.js directly
  • a.js and b.js both import from barrel.js
  • barrel.js re-exports from shared.js
  • shared.js imports a synthetic builtin like node:path

On Linux debug: ASSERT(m_status == Status::Fetching) at vendor/WebKit/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp:254. On Darwin: process hangs. Regressed by the module-loader rewrite in #29393.

Root cause

During the synchronous drain that services require(esm), hostLoadImportedModule has a BUN_JSC_ADDITIONS inline fast path (JSModuleLoader.cpp:712) that handles a dep whose fetchPromise is already fulfilled but whose modulePromise is still pending: it calls makeModule, then entry->fetchComplete(record), then modulePromise->fulfillPromise(record).

Meanwhile, the async pipeline had queued a ModuleRegistryFetchSettled microtask for the same entry. That handler has a correct bail-out (modulePromise->status() != Pending \u2192 return) \u2014 but it checks this after calling makeModule and attaching ModuleRegistryModuleSettled to the new makeModulePromise. Wait, even better: actually the fetch-settled handler does bail early. The leak is that a separate, earlier invocation of ModuleRegistryFetchSettled (queued before the inline path ran) already created its own makeModulePromise and attached ModuleRegistryModuleSettled to it. That queued moduleRegistryModuleSettled has no bail-out of its own, so it runs and calls entry->fetchComplete(otherRecord) a second time \u2014 the ASSERT trips; in release it silently overwrites the entry's record with a duplicate parse that nothing else references.

The specific trigger for node:path is that it's a SyntheticSourceProvider: makeModule runs the generator (InternalModuleRegistry::requireId) synchronously, which executes JS and gives the inline re-entry path plenty of opportunity to hit.

Fix

The WebKit-side fix (oven-sh/WebKit#223) mirrors the moduleRegistryFetchSettled guard into moduleRegistryModuleSettled: skip when modulePromise->status() != Pending. The inline path has already produced a valid record and resolved the promise; the queued copy is redundant.

This Bun PR adds the regression test. It needs to land together with the WebKit bump (will follow once #223 merges + preview/release tarballs are built).

Test

test/regression/issue/30493.test.ts \u2014 spawns bun run entry.js on the diamond+synthetic-builtin setup and asserts the expected JSON output. Verified locally with a debug-local Bun built against the WebKit PR branch:

bun test v1.3.14 (450072ba)
test/regression/issue/30493.test.ts:
(pass) require() of ESM with diamond dependency through barrel does not deadlock [1047ms]
1 pass, 0 fail, 3 expect() calls

Fail-before verified by reverting the WebKit change only (no Bun source changes needed to exercise the bug):

(fail) require() of ESM with diamond dependency through barrel does not deadlock
  ASSERTION FAILED: m_status == Status::Fetching
  vendor/WebKit/Source/JavaScriptCore/runtime/ModuleRegistryEntry.cpp(254)

Existing ESM-require tests (test/js/bun/resolve/require-esm-*.test.ts, test/js/node/module/require-extensions.test.ts) all still pass.

Adds regression test for #30493. The fix is in oven-sh/WebKit#223: add
the 'modulePromise already settled' bail to moduleRegistryModuleSettled
so the queued fetch-chain reaction doesn't call fetchComplete a second
time after hostLoadImportedModule's inline sync path already handled
the entry.

Repro: require('./app.js') where app.js directly imports shared.js AND
reaches it through a.js/b.js -> barrel.js -> shared.js, with shared.js
importing a synthetic builtin (e.g. 'import path from "path"'). On
Linux debug the assertion in ModuleRegistryEntry::fetchComplete trips;
on Darwin the process deadlocks waiting on a promise chain whose
microtask already ran.
@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:01 AM PT - May 11th, 2026

❌ Your commit 97760642 has 1 failures in Build #53333 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30497

That installs a local version of the PR into your bun-30497 executable, so you can run:

bun-30497 --bun

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds a regression test for Bun's module loader to ensure CommonJS require() can successfully load ESM dependency graphs with diamond imports resolved through a re-exporting barrel without triggering deadlock or internal assertion failures. The test validates successful execution and correct output structure.

Changes

Module Loader Regression Test

Layer / File(s) Summary
Regression Test
test/regression/issue/30493.test.ts
Test spawns a subprocess running CommonJS entry that requires an ESM app module containing a diamond dependency through a re-exporting barrel. Asserts successful completion with no stderr, validates JSON output shape with normalized path separators, and confirms exit code 0.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a regression test for a require() deadlock with ESM diamond dependencies through a barrel.
Description check ✅ Passed The description comprehensively covers the root cause, fix details, test verification, and includes the issue reference, but omits the template's 'How did you verify your code works?' section heading.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 `@test/regression/issue/30493.test.ts`:
- Line 37: The assertion hardcodes "/" as the path separator which fails on
Windows; update the test that calls
expect(JSON.parse(stdout.trim())).toEqual(...) to use a platform-aware separator
(e.g., import isWindows from the test harness or use path.sep) and build the
expected object conditionally (use "\\" when isWindows is true, otherwise "/")
so the expected values for fields "a" and "shared" use the correct separator
across platforms.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d79c5f2a-208e-4b4d-99d0-4475c05705b8

📥 Commits

Reviewing files that changed from the base of the PR and between 450072b and 9776064.

📒 Files selected for processing (1)
  • test/regression/issue/30493.test.ts


expect(stderr).toBe("");
// shared: path.sep on darwin/linux is "/", on windows "\\".
expect(JSON.parse(stdout.trim())).toEqual({ a: "/", b: "barrel", shared: "/" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Assertion will fail on Windows due to hardcoded path separator.

The comment on line 36 correctly notes that path.sep is "/" on Unix and "\\" on Windows, but the assertion hardcodes "/" for both a and shared fields. This test will fail on Windows CI.

🔧 Proposed fix for cross-platform compatibility

Import isWindows from harness and use a platform-conditional expectation:

 import { expect, test } from "bun:test";
-import { bunEnv, bunExe, tempDir } from "harness";
+import { bunEnv, bunExe, isWindows, tempDir } from "harness";

Then update the assertion:

+  const sep = isWindows ? "\\\\" : "/";
   expect(stderr).toBe("");
   // shared: path.sep on darwin/linux is "/", on windows "\\".
-  expect(JSON.parse(stdout.trim())).toEqual({ a: "/", b: "barrel", shared: "/" });
+  expect(JSON.parse(stdout.trim())).toEqual({ a: sep, b: "barrel", shared: sep });
   expect(exitCode).toBe(0);
🤖 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 `@test/regression/issue/30493.test.ts` at line 37, The assertion hardcodes "/"
as the path separator which fails on Windows; update the test that calls
expect(JSON.parse(stdout.trim())).toEqual(...) to use a platform-aware separator
(e.g., import isWindows from the test harness or use path.sep) and build the
expected object conditionally (use "\\" when isWindows is true, otherwise "/")
so the expected values for fields "a" and "shared" use the correct separator
across platforms.

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. regression (canary 1.3.14): require()-of-ESM that statically imports react + @mui/material/Typography aborts with PAC IB trap (silent SIGTRAP/SIGABRT) #30281 - Same regression from Upgrade WebKit to 87fd0daba19a (module-loader rewrite) #29393 (module-loader rewrite): require()-of-ESM crashes with SIGTRAP/SIGABRT when loading react + MUI modules, caused by the same CommonJS-require-of-ESM loader path bug

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #30281

🤖 Generated with Claude Code

@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #30283 (which fixes the same root-cause double-fire of moduleRegistryModuleSettled). Pointed my status comment on #30493 at the non-dup pair.

@robobun robobun closed this May 11, 2026
@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #30283 — same bug, same fix. That PR uses a runtime-probe skip so the regression test stays CI-green until WEBKIT_VERSION is bumped past oven-sh/WebKit#217.

Comment on lines +36 to +37
// shared: path.sep on darwin/linux is "/", on windows "\\".
expect(JSON.parse(stdout.trim())).toEqual({ a: "/", b: "barrel", shared: "/" });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔴 This assertion will fail on Windows CI: shared.js exports path.sep, which is "\\" on Windows, so the actual output will be { a: "\\", b: "barrel", shared: "\\" } rather than { a: "/", ... }. The comment on line 36 even notes this — import path from node:path and assert against path.sep (or branch on isWindows from harness).

Extended reasoning...

What the bug is

The test fixture's shared.js does import path from 'path'; export const SHARED = path.sep;. a.js returns SHARED from a(), and app.js exports { a: a(), b: b(), shared: SHARED }. On POSIX platforms path.sep is "/", but on Windows it is "\\" (a single backslash). The assertion on line 37 hard-codes { a: "/", b: "barrel", shared: "/" }, so on Windows the toEqual will fail even though the module loader behaved correctly.

The comment on line 36 explicitly acknowledges this — “on windows "\\"” — but the assertion immediately below it doesn't account for it.

Code path

  1. entry.js does require('./app.js') and prints JSON.stringify(mod.default).
  2. app.js evaluates a() → returns SHAREDpath.sep.
  3. On Windows, node:path resolves to the win32 implementation where sep === '\\'.
  4. JSON.stringify produces {"a":"\\\\","b":"barrel","shared":"\\\\"} on stdout.
  5. JSON.parse(stdout.trim()) yields { a: '\\', b: 'barrel', shared: '\\' }.
  6. expect(...).toEqual({ a: '/', b: 'barrel', shared: '/' }) fails: '\\''/'.

Why nothing prevents it

There is no test.skipIf(isWindows) and no platform branching in the assertion. Bun's regression suite runs on Windows CI (test/harness.ts:24 exports isWindows = process.platform === "win32", and many sibling regression tests like 26298.test.ts / 25628.test.ts use it), so this file will execute there.

Impact

The new regression test will be red on Windows CI, blocking the merge (or, if landed, breaking Windows CI for everyone) for a reason unrelated to the actual deadlock fix being tested.

Fix

Import path (or isWindows) in the test and assert the platform-correct separator:

import path from "node:path";
// ...
expect(JSON.parse(stdout.trim())).toEqual({ a: path.sep, b: "barrel", shared: path.sep });

Alternatively, since the specific value of SHARED is irrelevant to the deadlock repro (it just needs shared.js to import a synthetic builtin), change shared.js to export a platform-invariant constant like path.delimiter-agnostic literal, e.g. export const SHARED = path.posix.sep; and keep the assertion as-is.

Step-by-step proof

  • Given: Windows runner, process.platform === 'win32'.
  • import path from 'path' in shared.js loads the win32 path module → path.sep === '\\'.
  • SHARED = '\\'; a() returns '\\'; app.js default export = { a: '\\', b: 'barrel', shared: '\\' }.
  • entry.js prints {"a":"\\\\","b":"barrel","shared":"\\\\"}.
  • Test parses it back to { a: '\\', b: 'barrel', shared: '\\' }.
  • toEqual({ a: '/', b: 'barrel', shared: '/' })fails on keys a and shared.
  • Test reports failure; Windows CI goes red.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

require() of ESM module with diamond dependency through barrel deadlocks

1 participant