Skip to content

fix: prevent double-fulfill when import() and require() race on same ESM module#27323

Open
dylan-conway wants to merge 1 commit intomainfrom
claude/fix-12910-import-require-double-fulfill
Open

fix: prevent double-fulfill when import() and require() race on same ESM module#27323
dylan-conway wants to merge 1 commit intomainfrom
claude/fix-12910-import-require-double-fulfill

Conversation

@dylan-conway
Copy link
Member

Summary

Fixes a ASSERT(status() == Status::Pending) crash in JSPromise::fulfillPromise when import() and require() target the same ESM module that throws during evaluation.

Root cause: When import() starts fetching an ESM module, JSC's requestFetch creates entry.fetch as a .then() chained promise with a pending microtask reaction. If require() then loads the same module, fetchCommonJSModuleNonBuiltin called provideFetch which fulfilled that same promise directly. When the pending microtask reaction later fired, it attempted to resolve the already-fulfilled promise, crashing on the assertion.

This was exposed by #27288 which added Loader.registry.$delete(id) to the require() error path.

Fix (two parts):

  • ModuleLoader.cpp: Extend the existing hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice guard to also detect when import() has a fetch in progress (entry exists with a fetch promise but state is still Fetch). This prevents fetchCommonJSModuleNonBuiltin from calling provideFetch on that entry.
  • CommonJS.ts: In loadEsmIntoCjs, clear entry.fetch before calling $fulfillModuleSync, so that provideFetchfulfillFetch creates a fresh promise instead of fulfilling import()'s pending chained promise.

Test plan

Closes #12910

🤖 Generated with Claude Code

…e() race on the same ESM module

When `import()` starts fetching an ESM module, JSC's `requestFetch` creates
`entry.fetch` as a `.then()` chained promise with a pending microtask reaction.
If `require()` then loads the same module, `fetchCommonJSModuleNonBuiltin`
called `provideFetch` which fulfilled that same promise directly via
`fulfillFetch`. When the pending microtask reaction later fired during
microtask draining, it attempted to resolve the already-fulfilled promise,
triggering `ASSERT(status() == Status::Pending)` in `JSPromise::fulfillPromise`.

This was exposed by commit 21c3439 which added `Loader.registry.$delete(id)`
to the require() error path. The underlying issue is that Bun was calling
`provideFetch` on a module whose fetch pipeline was already owned by import().

The fix has two parts:

1. **ModuleLoader.cpp**: Extend the existing
   `hasAlreadyLoadedESMVersionSoWeShouldntTranspileItTwice` guard to also
   detect when `import()` has a fetch in progress (entry exists with a fetch
   promise but state is still `Fetch`). This prevents
   `fetchCommonJSModuleNonBuiltin` from calling `provideFetch` on that entry.

2. **CommonJS.ts**: In `loadEsmIntoCjs`, clear `entry.fetch` before calling
   `$fulfillModuleSync`, so that `provideFetch` → `fulfillFetch` creates a
   fresh promise instead of fulfilling import()'s pending chained promise.

Closes #12910

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@robobun
Copy link
Collaborator

robobun commented Feb 21, 2026

Updated 1:53 AM PT - Feb 21st, 2026

@dylan-conway, your commit 88c52fc has 30 failures in Build #37854 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 27323

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

bun-27323 --bun

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

No actionable comments were generated in the recent review. 🎉


Walkthrough

Modified module loading logic to improve handling of in-progress fetch promises. Extended fetchCommonJSModule's registry check to detect in-progress imports and added a guard in loadEsmIntoCjs to clear stale fetch promises before forcing module sync, preventing promise reuse and associated errors.

Changes

Cohort / File(s) Summary
Module Loading Fetch Promise Handling
src/bun.js/bindings/ModuleLoader.cpp, src/js/builtins/CommonJS.ts
Enhanced in-progress fetch detection and promise lifecycle management. ModuleLoader now checks for fetch property in registry entries to skip duplicate transpilation. CommonJS loader clears stale entry.fetch promises before forcing module sync to ensure fresh fetch promise creation and prevent reuse of in-flight promises.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately describes the main issue: preventing double-fulfill when import() and require() race on the same ESM module, which directly matches the core changes in both modified files.
Description check ✅ Passed The PR description provides comprehensive context including root cause analysis, two-part fix explanation, test plan, and issue references, fully addressing the template structure.
Linked Issues check ✅ Passed The code changes directly address issue #12910 by fixing the segmentation fault caused by double-fulfillment of promises when import() and require() race on the same ESM module during evaluation.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the double-fulfill crash: ModuleLoader.cpp adds a guard to detect in-progress import() fetches, and CommonJS.ts clears the fetch promise before fulfilling; no unrelated alterations present.

✏️ 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.

@claude
Copy link
Contributor

claude bot commented Feb 21, 2026

88c52 — Looks good!

Reviewed 2 files across src/bun.js/bindings/ and src/js/builtins/: Fixes a double-fulfill assertion crash when import() and require() race on the same ESM module by preventing provideFetch from being called on a module whose fetch pipeline is already owned by an in-progress import() call.

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.

panic(main thread): Segmentation fault at address 0x1A

2 participants