Skip to content

Fix crash in Bun.build when passing many custom conditions - #30660

Closed
robobun wants to merge 1 commit into
mainfrom
farm/07d1553a/fix-esm-conditions-capacity
Closed

Fix crash in Bun.build when passing many custom conditions#30660
robobun wants to merge 1 commit into
mainfrom
farm/07d1553a/fix-esm-conditions-capacity

Conversation

@robobun

@robobun robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a debug assertion / capacity underflow in ESMConditions.init triggered by passing several custom conditions to Bun.build().

The capacity reservation used:

defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len

which Zig parses as:

defaults.len + 2 + (if (allow_addons) 1 else (0 + conditions.len))

When allow_addons is true (the default), conditions.len was never counted toward the reserved capacity, so the subsequent putAssumeCapacity calls overflowed the map once a handful of custom conditions were supplied.

Replaced the ternary with @intFromBool to avoid the precedence footgun.

How did you verify your code works?

Repro that panics on main and passes with this change:

await Bun.build({
  conditions: ["a", "b", "c", "d", "e", "f", "g"],
  entrypoints: ["./index.ts"],
});

Added a regression test in test/bundler/bun-build-api.test.ts.

Found by Fuzzilli (fingerprint 4b17c3a7de281a5f).

The expression 'if (allow_addons) 1 else 0 + conditions.len' was parsed
as 'if (allow_addons) 1 else (0 + conditions.len)', so when allow_addons
was true (the default), conditions.len was never counted toward the map
capacity. Passing more than a couple of custom conditions to Bun.build
would trip the putAssumeCapacity assert in debug builds and write past
the reserved capacity in release builds.
@robobun

robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 9:24 PM PT - May 13th, 2026

@robobun, your commit 859785a has 1 failures in Build #54178 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30660

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

bun-30660 --bun

@coderabbitai

coderabbitai Bot commented May 14, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6d140774-408e-4702-b8f4-6aa43e22edf3

📥 Commits

Reviewing files that changed from the base of the PR and between b8ecc78 and 859785a.

📒 Files selected for processing (2)
  • src/bundler/options.zig
  • test/bundler/bun-build-api.test.ts

Walkthrough

This PR refactors capacity calculation in the bundler's ESM conditions initialization to use type-cast arithmetic instead of conditional expressions, then adds a regression test to verify the bundler handles many custom conditions without crashing.

Changes

Custom ESM Conditions Capacity Fix

Layer / File(s) Summary
Capacity calculation and regression test
src/bundler/options.zig, test/bundler/bun-build-api.test.ts
ESMConditions.init computes optional addon capacity using @as(usize, @intFromBool(allow_addons)) instead of an if-else pattern. New regression test confirms Bun.build succeeds when provided a large custom conditions array.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main fix: preventing a crash in Bun.build when many custom conditions are passed, which directly matches the changeset's primary objective.
Description check ✅ Passed The description comprehensively covers both required template sections: it clearly explains what the PR does (fixes capacity underflow in ESMConditions.init with detailed technical explanation of the bug and fix) and how verification was performed (reproduction case and regression test).
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.

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. bun --conditions: export condition matching breaks with 4+ conditions (falls back to default) #30619 - Export condition matching breaks with 4+ conditions due to the same operator precedence bug in ESMConditions capacity calculation

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

Fixes #30619

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Fix crash in Bun.build with many conditions #30466 - Fixes the same operator precedence bug in ESMConditions.init capacity calculation that causes a crash in Bun.build() with many custom conditions

🤖 Generated with Claude Code

@robobun

robobun commented May 14, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #30466, which already has this fix plus an additional precedence fix in loadersFromTransformOptions.

@robobun robobun closed this May 14, 2026
@robobun
robobun deleted the farm/07d1553a/fix-esm-conditions-capacity branch May 14, 2026 04:23

@claude claude 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.

LGTM — correct and minimal fix for the if/else precedence bug, with a regression test.

Extended reasoning...

Overview

This PR changes 3 lines in src/bundler/options.zig (ESMConditions.init), replacing if (allow_addons) 1 else 0 + conditions.len with @as(usize, @intFromBool(allow_addons)) + conditions.len to fix a Zig precedence bug where the else branch greedily consumed + conditions.len. When allow_addons was true (the default), the user-supplied conditions were not counted toward the reserved capacity, and the subsequent putAssumeCapacity loop over conditions overflowed the map. A regression test in test/bundler/bun-build-api.test.ts exercises 16 custom conditions.

Correctness

I verified the new capacity expression against the actual inserts: import/require maps insert exactly defaults.len + conditions.len + @intFromBool(allow_addons) + 2 entries (initial sentinel, conditions loop, defaults loop, optional node-addons, default), which matches the reservation. default_condition_amp inserts one fewer (no initial sentinel), so it's slightly over-reserved — harmless. The unchanged style line was already correct for its insert count.

Security risks

None. This is a capacity-hint arithmetic fix in bundler options initialization with no user-controlled data flowing into unsafe sinks.

Level of scrutiny

Low. The change is a 3-line arithmetic correction whose intent and effect are fully explained by the PR description and verifiable by inspection. No CODEOWNERS match for these paths. The added test follows the existing test.concurrent + tempDirWithFiles pattern in the same file.

Other factors

The one inline comment flags a pre-existing, harmless instance of the same precedence pattern in loadersFromTransformOptions (which uses getOrPutValue so cannot crash). It's explicitly non-blocking and doesn't affect approval.

Comment thread src/bundler/options.zig
Comment on lines +1090 to +1092
try default_condition_amp.ensureTotalCapacity(defaults.len + 2 + @as(usize, @intFromBool(allow_addons)) + conditions.len);
try import_condition_map.ensureTotalCapacity(defaults.len + 2 + @as(usize, @intFromBool(allow_addons)) + conditions.len);
try require_condition_map.ensureTotalCapacity(defaults.len + 2 + @as(usize, @intFromBool(allow_addons)) + conditions.len);

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.

🟣 Pre-existing, non-blocking: the identical if (cond) X else 0 + Y precedence footgun also exists in loadersFromTransformOptions in this same file (lines 1574-1577). It's harmless there — the under-reserved capacity is followed by getOrPutValue calls that grow the map, so it only costs an extra rehash — but since this PR exists specifically to eliminate this pattern, you may want to apply the same @intFromBool fix there for consistency.

Extended reasoning...

What the issue is

This PR fixes a Zig precedence footgun in ESMConditions.init where if (allow_addons) 1 else 0 + conditions.len parses as if (allow_addons) 1 else (0 + conditions.len). The exact same construct appears ~480 lines later in the same file, in loadersFromTransformOptions (src/bundler/options.zig:1574-1577):

input_loaders.extensions.len +
    if (target.isBun()) default_loader_ext_bun.len else 0 +
        if (target == .browser) default_loader_ext_browser.len else 0 +
            default_loader_ext.len,

Zig's else branch is greedy, so this parses as:

input_loaders.extensions.len +
    (if (target.isBun()) default_loader_ext_bun.len
     else (0 + (if (target == .browser) default_loader_ext_browser.len
                else (0 + default_loader_ext.len))))

Step-by-step walkthrough

Take target = .bun (so target.isBun() == true):

  1. The outer if evaluates to default_loader_ext_bun.len (= 2).
  2. The entire else branch — including the nested browser check and default_loader_ext.len (= 16) — is skipped.
  3. Reserved capacity = input_loaders.extensions.len + 2.
  4. The intended capacity was input_loaders.extensions.len + 2 + 0 + 16 = input_loaders.extensions.len + 18.

Similarly for target = .browser: the inner if returns default_loader_ext_browser.len (= 1) and again drops default_loader_ext.len, giving extensions.len + 0 + 1 instead of extensions.len + 0 + 1 + 16. Only target = .node accidentally gets the right answer (both conditions false → 0 + 0 + 16).

Why this is not a crash (unlike the ESMConditions case)

The fixed bug in ESMConditions.init was dangerous because the under-reserved map was followed by putAssumeCapacity calls for the user-supplied conditions. Here the safety story is different:

  • stringHashMapFromArrays only calls putAssumeCapacity for input_loaders.extensions, and input_loaders.extensions.len is always the first addend, so it is always covered regardless of how the rest mis-parses.
  • The 16 default extensions, the 2 bun extensions, and the 1 browser extension are all inserted afterward via try loaders.getOrPutValue(...), which grows the map as needed.
  • Additionally, when no user loaders are passed (keys.len == 0, the common case), stringHashMapFromArrays skips ensureTotalCapacity entirely, so the hint is unused.

So the only effect is a wrong capacity hint causing one or two extra rehashes on a ~20-entry map during one-time options initialization — no observable behavior change, no crash, not measurable.

Addressing the "out of scope" objection

It's fair to note that this PR doesn't touch loadersFromTransformOptions, doesn't call into it, and the under-reservation there has zero functional impact — on its own it would not warrant a comment. The reason it's worth a non-blocking mention here is narrower than "same file": it is literally the same syntactic trap (else 0 + ...) that this PR's title and description identify as the root cause of a crash. Leaving an identical instance of the pattern a few hundred lines down means the next person to change those getOrPutValue calls to putAssumeCapacity (a plausible micro-optimization) reintroduces the exact crash this PR fixes. Applying the same @intFromBool rewrite there is a one-line consistency change, and the author already has the pattern in their editor.

Suggested fix

input_loaders.extensions.len +
    @as(usize, @intFromBool(target.isBun())) * default_loader_ext_bun.len +
    @as(usize, @intFromBool(target == .browser)) * default_loader_ext_browser.len +
    default_loader_ext.len,

(or equivalently, parenthesize each ternary). This is pre-existing and should not block the PR — purely a "while you're here" suggestion.

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.

1 participant