Skip to content

Comments

fix(linker): resolve race condition in side effects computation for export-star#7728

Merged
IWANABETHATGUY merged 3 commits intorolldown:mainfrom
camc314:c/fix-7646
Jan 1, 2026
Merged

fix(linker): resolve race condition in side effects computation for export-star#7728
IWANABETHATGUY merged 3 commits intorolldown:mainfrom
camc314:c/fix-7646

Conversation

@camc314
Copy link
Contributor

@camc314 camc314 commented Jan 1, 2026

NOTE: I used AI for this, as I'm not familiar with the rolldown codebase. So feel free to close if this is the wrong fix. (That said, I manually validate each of the commits, including making sure that this fixes the issue, so I think it's OK.)

I've split this change into three commits

Firstly, I got AI to identify the cause of the issue, I cloned the repro as a submodule and got claude to investigate it. Eventually, this lead to identifying a race condition in reference_needed_symbols. This function runs in parallel across all modules and was both reading importee.side_effects and writing to importer.side_effects simultaneously. When module A reads module B's side effects while another thread is upgrading B's side effects, A could see stale data.

Commit 1: Validating the race condition

After working out what the problem was, I added debug assertions to validate that the race condition was real and reproducible. This involved adding assertions in the relevant parts of the code to check for inconsistent states.

I then ran this against the reproduction repo, and was able to consistently trigger the panics that I added.

Commit 2: First fix attempt

Given that the cause (race condition) was identified, I made an initial attempt to fix it (in that same file), the approach being:

  1. Pre-compute which modules needed side_effects upgraded to true before entering the parallel section
  2. Build a snapshot of all side effects values (side_effects_snapshot: Vec)
  3. Use the snapshot for reads during parallel processing instead of reading live data
  4. Defer the writes until after the parallel section completed

This eliminates the race condition.

However, this doensn't really correctly handle `transitive side effects e.g:

A.js: export * from 'B' // B has WrapKind::None
B.js: export * from 'C' // C is WrapKind::Cjs
C.js: module.exports = {}

B gets upgraded because of C, but A's snapshot of B was captured before B was upgraded. The fix worked for direct dependencies but could miss chains.

Commit 3: Better solution

Rather than adding complexity to handle transitive cases in the snapshot approach (commit 2), I refactored for a cleaner architecture.

During the link stage, there is a dedicated determine_side_effects. This runs before reference_needed_symbold, already handling transitive effects via import.


I've got no idea how to write a test for this given how flaky it is, so suggestions are appreciated.

fixes #7646

Copilot AI review requested due to automatic review settings January 1, 2026 13:45
…ce_needed_symbols

During parallel processing in reference_needed_symbols, one thread may write
to a module's side_effects while another thread reads from it. This adds
atomic tracking to detect and panic on this race condition.

The race occurs between:
- Writes at lines 151-154, 168-172, 229-233 (setting importer.side_effects)
- Reads at lines 187 and 217-218 (reading importee.side_effects)

When a module both has `export * from 'wrapped-module'` (triggering a write)
AND is imported by another module (triggering a read), the race can cause
non-deterministic tree-shaking decisions.
…snapshot

Fixes rolldown#7646

The race condition occurred because during parallel processing:
- One thread reads `importee.side_effects` to determine statement side effects
- Another thread writes to `importer.side_effects` via write_volatile

When a module both has `export * from 'wrapped-module'` (making it a write target)
AND is imported by another module (making it a read target), non-deterministic
results occur.

The fix:
1. Pre-compute which modules need side_effects upgraded to true before parallel section
2. Create a snapshot of side_effects values (including upgrades) for safe parallel reads
3. Defer side_effects writes to after the parallel section

This ensures all parallel reads see a consistent view of side_effects values.
…de_effects phase

Move the side effects computation for `export * from 'wrapped-module'` patterns
from reference_needed_symbols to determine_side_effects. This:

- Enables proper transitive side effects propagation through module chains
- Eliminates the need for pre-computation and snapshot logic in parallel processing
- Removes race condition concerns by computing all side effects before parallel section
- Simplifies reference_needed_symbols by removing ~45 lines of deferred update code
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a race condition in side effects computation for export-star patterns that caused non-deterministic CJS bundle output. The fix moves side effects computation from parallel processing during reference_needed_symbols to the sequential determine_side_effects phase, eliminating the race where one thread could read stale side effects data while another thread was updating it.

  • Adds export-star handling logic to determine_side_effects to mark modules with export * from 'wrapped-module' patterns as having side effects
  • Removes unsafe parallel writes to importer_side_effects from reference_needed_symbols
  • Ensures proper initialization order by computing side effects before parallel symbol referencing

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
crates/rolldown/src/stages/link_stage/tree_shaking/determine_side_effects.rs Added logic to detect export-star patterns with wrapped modules and mark them as having side effects based on wrap kind and dynamic exports
crates/rolldown/src/stages/link_stage/reference_needed_symbols.rs Removed unsafe parallel writes to side effects field and related volatile pointer operations, now only reads pre-computed side effects

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Member

@IWANABETHATGUY IWANABETHATGUY left a comment

Choose a reason for hiding this comment

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

Thanks

@IWANABETHATGUY IWANABETHATGUY merged commit 15e9bfc into rolldown:main Jan 1, 2026
31 checks passed
This was referenced Jan 7, 2026
shulaoda added a commit that referenced this pull request Jan 7, 2026
## [1.0.0-beta.59] - 2026-01-07

⚡ Inline Dynamic Imports for Statically Imported Modules

- When a module is already statically imported, dynamic imports to that same module are now inlined instead of creating a separate chunk

### 🚀 Features

- plugin_timings: add 3s threshold and doc link to warning message (#7741) by @shulaoda
- improve treeshaking logic to handle empty parameter list in dynamic import .then() callbacks (#7781) by @Copilot
- dev/lazy: don't include already executed modules (#7745) by @hyf0
- dev/lazy: support dynamic `import(..)` (#7726) by @hyf0
- inline dynamic imports that imports statically imported modules (#7742) by @IWANABETHATGUY
- option: add experimental option to control chunk optimization (#7738) by @IWANABETHATGUY

### 🐛 Bug Fixes

- inline dynamic entry to user defined entry with esm wrap kind (#7783) by @IWANABETHATGUY
- use canonical namespace reference for property access (#7777) by @IWANABETHATGUY
- dynamic entry merged into common chunk with cjs and esm wrap kind (#7771) by @IWANABETHATGUY
- tla: should not await non-tla-related modules (#7768) by @hyf0
- dynamic entry captured by common chunk with CJS format (#7757) by @IWANABETHATGUY
- module_loader: mark emitted chunks as user-defined entry when already loaded (#7765) by @shulaoda
- normalize preserveModulesRoot path (#7737) by @IWANABETHATGUY
- linker: resolve race condition in side effects computation for export-star (#7728) by @camc314

### 🚜 Refactor

- plugin_timings: filter out plugins with duration < 1s from timing warnings (#7785) by @shulaoda
- module_loader: remove unnecessary collect before extend (#7769) by @shulaoda
- rename _id suffixes to _idx for oxc_index types (#7767) by @IWANABETHATGUY
- remove duplicate `preserve_entry_signatures` from `AddEntryModuleMsg` (#7762) by @shulaoda
- module_loader: pass `user_defined_entries` by reference (#7756) by @shulaoda
- dev/lazy: get proxy entry's `ResolvedId` correctly (#7746) by @hyf0
- simplify try_rewrite_import_expression control flow (#7753) by @IWANABETHATGUY
- module_loader: remove unnecessary dynamic import handling for runtime module (#7754) by @shulaoda
- inline __toDynamicImportESM  (#7747) by @IWANABETHATGUY
- use From impl for ModuleLoaderOutput conversion (#7732) by @shulaoda
- remove duplicate fields from `ModuleLoader` (#7731) by @shulaoda
- tweak `resolve_user_defined_entries` (#7727) by @shulaoda

### 📚 Documentation

- add rolldown-string reference to native MagicString compatibility section (#7778) by @Copilot
- improve comments for export star side effects handling (#7730) by @IWANABETHATGUY

### 🧪 Testing

- use assertion instead of console.log for some testcase (#7744) by @IWANABETHATGUY

### ⚙️ Miscellaneous Tasks

- tweak some `output.dynamicImportInCjs` related rollup test results (#7776) by @sapphi-red
- mark esbuild/dce/dce_of_symbol_ctor_call as passed (#7775) by @sapphi-red
- deps: update oxc apps (#7772) by @renovate[bot]
- vite-tests: allow running on PRs with `test: vite-tests` label (#7770) by @shulaoda
- deps: update oxc apps (#7760) by @renovate[bot]
- deps: update rollup submodule for tests to v4.55.1 (#7763) by @sapphi-red
- deps: update test262 submodule for tests (#7764) by @sapphi-red
- deps: update oxc to v0.107.0 (#7758) by @camc314
- deps: update taiki-e/install-action action to v2.65.13 (#7751) by @renovate[bot]
- deps: update rust crates (#7750) by @renovate[bot]
- deps: update npm packages (#7749) by @renovate[bot]
- deps: update github-actions (#7748) by @renovate[bot]
- deps: update dependency oxlint-tsgolint to v0.10.1 (#7729) by @renovate[bot]
- deps: update crate-ci/typos action to v1.41.0 (#7725) by @renovate[bot]

Co-authored-by: shulaoda <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rolldown creates a different CJS bundle in ~50% runs

2 participants