Skip to content

Fix capacity miscalculation in ESMConditions.init - #30604

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

Fix capacity miscalculation in ESMConditions.init#30604
robobun wants to merge 1 commit into
mainfrom
farm/2373a045/fix-esm-conditions-capacity

Conversation

@robobun

@robobun robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a debug assert / potential OOB write in Bun.build() / bun build when passing several entries in conditions.

In ESMConditions.init:

try import_condition_map.ensureTotalCapacity(defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len);

Zig parses if (allow_addons) 1 else 0 + conditions.len as if (allow_addons) 1 else (0 + conditions.len). Since allow_addons defaults to true, conditions.len was effectively dropped from the reserved capacity for the default/import/require condition maps. With enough user-supplied conditions (≥4 for target: "bun"), putAssumeCapacity overflows the backing array.

Repro:

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

How did you verify your code works?

Added a regression test in test/bundler/bun-build-api.test.ts that builds with 0–16 conditions. Verified it crashes on main and passes with this fix.

fuzzer

The expression `if (allow_addons) 1 else 0 + conditions.len` parses as
`if (allow_addons) 1 else (0 + conditions.len)`, so when allow_addons
is true (the default), conditions.len was dropped from the reserved
capacity. With enough user-supplied conditions, putAssumeCapacity would
overflow the backing array and hit a debug assert / write OOB.
@robobun

robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:18 PM PT - May 12th, 2026

@robobun, your commit ef96245 has 1 failures in Build #53946 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30604

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

bun-30604 --bun

@coderabbitai

coderabbitai Bot commented May 13, 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: 058a3420-3c5f-451d-9d96-7caa5d8d12c3

📥 Commits

Reviewing files that changed from the base of the PR and between 3bf4b33 and ef96245.

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

Walkthrough

The PR refactors ESMConditions.init to extract a single addons_count computation and adds a regression test ensuring Bun.build succeeds across condition counts from 0 to 16.

Changes

ESM Conditions Refactoring

Layer / File(s) Summary
Simplify addons_count computation
src/bundler/options.zig
ESMConditions.init extracts the repeated if (allow_addons) 1 else 0 logic into a single computed addons_count variable and reuses it across three condition map capacity calculations.
Test many conditions edge case
test/bundler/bun-build-api.test.ts
New concurrent test verifies that Bun.build succeeds with condition counts ranging from 0 through 16.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main fix: a capacity miscalculation bug in ESMConditions.init.
Description check ✅ Passed The description fully addresses both template sections with clear problem explanation, root cause analysis, reproduction steps, and verification details.
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

This PR may be a duplicate of:

  1. Fix crash in Bun.build with many conditions #30466 - Also fixes Bun.build crash with many conditions by correcting the capacity calculation in ESMConditions.init

🤖 Generated with Claude Code

@robobun

robobun commented May 13, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #30466, which also fixes a second instance of the same precedence bug in loadersFromTransformOptions.

@robobun robobun closed this May 13, 2026
@robobun
robobun deleted the farm/2373a045/fix-esm-conditions-capacity branch May 13, 2026 02:18

@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 — straightforward precedence fix with a regression test; the inline note about loadersFromTransformOptions is pre-existing and non-blocking.

Extended reasoning...

Overview

This PR fixes a Zig if-else precedence bug in ESMConditions.init (src/bundler/options.zig). The expression defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len was being parsed with the else-branch consuming 0 + conditions.len, so when allow_addons is true (the default) the user-supplied conditions.len was dropped from the reserved capacity, and subsequent putAssumeCapacity calls could overflow the backing array. The fix hoists the conditional into const addons_count: usize = if (allow_addons) 1 else 0; and uses it in the three ensureTotalCapacity calls — a 4-line, mechanically obvious change. A regression test in test/bundler/bun-build-api.test.ts exercises Bun.build with 0–16 conditions.

Security risks

None. This only adjusts an integer capacity hint passed to ensureTotalCapacity; the new value is strictly >= the old one in all branches, so it can only reserve more (correct) space, not less. No new inputs, no parsing changes, no auth/crypto/permissions involvement.

Level of scrutiny

Low. The diff is tiny, the root cause and fix are both fully explained in the PR description, and the corrected arithmetic is trivially verifiable against the putAssumeCapacity calls immediately below (1 for import/require, conditions.len, defaults.len, optional node-addons, and defaultdefaults.len + 2 + addons_count + conditions.len). The style_condition_map line correctly omits addons_count since node-addons is never inserted there.

Other factors

The one inline finding is explicitly a pre-existing, non-blocking nit about the same precedence pattern in loadersFromTransformOptions, which is harmless there because subsequent inserts use getOrPutValue (auto-grows). It's a reasonable "while you're here" but does not affect this PR's correctness. No CODEOWNERS apply to the touched files, and there are no outstanding reviewer comments.

Comment thread src/bundler/options.zig
Comment on lines +1090 to +1093
const addons_count: usize = if (allow_addons) 1 else 0;
try default_condition_amp.ensureTotalCapacity(defaults.len + 2 + addons_count + conditions.len);
try import_condition_map.ensureTotalCapacity(defaults.len + 2 + addons_count + conditions.len);
try require_condition_map.ensureTotalCapacity(defaults.len + 2 + addons_count + 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.

🟣 Nit (pre-existing, not introduced here): the same if-else precedence trap also lives ~480 lines down in loadersFromTransformOptionsinput_loaders.extensions.len + if (target.isBun()) ... else 0 + if (...) ... else 0 + default_loader_ext.len. That one is harmless because the subsequent inserts use getOrPutValue (which grows), so it just under-reserves and triggers a couple of extra reallocations rather than overflowing. But since it's the identical root cause in the same file, you may want to give it the same const x: usize = if (...) ... else 0; treatment while you're here.

Extended reasoning...

What the issue is

This PR correctly fixes the Zig if-else precedence bug in ESMConditions.init, where if (allow_addons) 1 else 0 + conditions.len parses as if (allow_addons) 1 else (0 + conditions.len). However, the exact same pattern exists further down in the same file, in loadersFromTransformOptions:

var loaders = try stringHashMapFromArrays(
    bun.StringArrayHashMap(Loader),
    allocator,
    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,
    input_loaders.extensions,
    loader_values,
);

In Zig, the else-branch of an if expression greedily consumes a full expression. So a + if (c) x else 0 + y parses as a + (if (c) x else (0 + y)), not (a + (if (c) x else 0)) + y. The chained ifs here therefore do not produce the sum the indentation suggests.

Step-by-step proof

Take target = .bun (so target.isBun() is true and target == .browser is false), with input_loaders.extensions.len = 3:

  1. The inner if (target == .browser) default_loader_ext_browser.len else 0 + default_loader_ext.len parses with the else-branch consuming 0 + default_loader_ext.len. Since the condition is false, it evaluates to 0 + 16 = 16.
  2. The outer if (target.isBun()) default_loader_ext_bun.len else 0 + (16) — condition is true, so it evaluates to default_loader_ext_bun.len = 2. The else-branch (0 + 16) is discarded entirely.
  3. total_capacity = input_loaders.extensions.len + 2 = 5.

The intended value was 3 + 2 + 0 + 16 = 21. So the map is reserved for 5 entries instead of 21.

Why it doesn't crash (unlike the ESMConditions case)

  • stringHashMapFromArrays only does putAssumeCapacity for keys.len items (the user-supplied extensions). Since extensions.len is always a term in the sum regardless of which if-branch is taken, the reserved capacity is always >= extensions.len, so those inserts never overflow.
  • The subsequent default_loader_ext / default_loader_ext_bun / default_loader_ext_browser inserts all use getOrPutValue, which grows the map on demand.
  • Additionally, when input_loaders.extensions.len == 0 (no custom loaders), stringHashMapFromArrays skips ensureTotalCapacity entirely, so the reservation hint is ignored anyway.

So the only impact is a few unnecessary reallocations during build setup when custom loaders are passed — no correctness or safety issue.

Why mention it

It's the identical root cause this PR is fixing (Zig if-else precedence in a capacity calculation), in the same file, and the same fix style applies cleanly:

const bun_count: usize = if (target.isBun()) default_loader_ext_bun.len else 0;
const browser_count: usize = if (target == .browser) default_loader_ext_browser.len else 0;
... input_loaders.extensions.len + bun_count + browser_count + default_loader_ext.len ...

This is pre-existing — the PR doesn't touch, call, or otherwise interact with loadersFromTransformOptions — so it's purely a non-blocking "while you're here" suggestion, not something that should hold up the merge.

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