-
Notifications
You must be signed in to change notification settings - Fork 5k
Fix crash in Bun.build() with many conditions
#30557
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1087,10 +1087,11 @@ | |||||||||||
| var require_condition_map = ConditionsMap.init(allocator); | ||||||||||||
| var style_condition_map = ConditionsMap.init(allocator); | ||||||||||||
|
|
||||||||||||
| try default_condition_amp.ensureTotalCapacity(defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len); | ||||||||||||
| try import_condition_map.ensureTotalCapacity(defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len); | ||||||||||||
| try require_condition_map.ensureTotalCapacity(defaults.len + 2 + if (allow_addons) 1 else 0 + conditions.len); | ||||||||||||
| const addon_count: usize = if (allow_addons) 1 else 0; | ||||||||||||
|
Check warning on line 1090 in src/bundler/options.zig
|
||||||||||||
| try default_condition_amp.ensureTotalCapacity(defaults.len + 2 + addon_count + conditions.len); | ||||||||||||
| try import_condition_map.ensureTotalCapacity(defaults.len + 2 + addon_count + conditions.len); | ||||||||||||
| try require_condition_map.ensureTotalCapacity(defaults.len + 2 + addon_count + conditions.len); | ||||||||||||
| try style_condition_map.ensureTotalCapacity(defaults.len + 2 + conditions.len); | ||||||||||||
|
Check notice on line 1094 in src/bundler/options.zig
|
||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟣 Pre-existing, not introduced here, but since you're touching this exact block: Extended reasoning...What the inconsistency is
try style_condition_map.ensureTotalCapacity(defaults.len + 2 + conditions.len);but the loop that actually inserts those conditions skips for (conditions) |condition| {
import_condition_map.putAssumeCapacity(condition, {});
require_condition_map.putAssumeCapacity(condition, {});
default_condition_amp.putAssumeCapacity(condition, {});
}So Why it's a behavioral inconsistency, not just over-reservation
pub fn appendSlice(self: *ESMConditions, conditions: []const string) bun.OOM!void {
try self.default.ensureUnusedCapacity(conditions.len);
try self.import.ensureUnusedCapacity(conditions.len);
try self.require.ensureUnusedCapacity(conditions.len);
try self.style.ensureUnusedCapacity(conditions.len);
for (conditions) |condition| {
...
self.style.putAssumeCapacity(condition, {});
}
}The resolver consults
Step-by-step example
Why nothing prevents itThere's no fallback — ImpactUser-supplied FixAdd the missing line to the loop: for (conditions) |condition| {
import_condition_map.putAssumeCapacity(condition, {});
require_condition_map.putAssumeCapacity(condition, {});
default_condition_amp.putAssumeCapacity(condition, {});
style_condition_map.putAssumeCapacity(condition, {});
}(Capacity is already reserved on line 1094, so Relationship to this PRNot introduced here — the PR only touches lines 1090-1093. But line 1094 is literally the next line, in the same function, operating on the same |
||||||||||||
|
|
||||||||||||
| import_condition_map.putAssumeCapacity("import", {}); | ||||||||||||
| require_condition_map.putAssumeCapacity("require", {}); | ||||||||||||
|
|
||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 nit (pre-existing): the same
if (cond) a else 0 + ...precedence pattern this PR fixes also exists inloadersFromTransformOptions(~line 1575):input_loaders.extensions.len + if (target.isBun()) default_loader_ext_bun.len else 0 + if (target == .browser) ... else 0 + default_loader_ext.len. It doesn't crash there because the subsequent inserts usegetOrPutValue(which grows) rather thanputAssumeCapacity, so the only effect is an unnecessary reallocation — but since this PR is specifically about this precedence trap, it might be worth giving that callsite the sameconst x: usize = if (...) n else 0;treatment for consistency.Extended reasoning...
What
loadersFromTransformOptionsin the same file computes its hashmap capacity as:Per Zig grammar, an
IfExpr'selsebranch consumes a full expression, so this parses as:This is the exact precedence pitfall this PR fixes in
ESMConditions.init.Step-by-step
With
target = .bunand a user-suppliedloadersmap of 1 entry:target.isBun()→ true, so the wholeifevaluates todefault_loader_ext_bun.len= 2.+ if (target == .browser) ... + default_loader_ext.lenis part of the else branch and is never evaluated.1 + 2= 3.1 + 2 + 0 + 18= 21.stringHashMapFromArrayscallsensureTotalCapacity(3)thenputAssumeCapacityexactlyinput_loaders.extensions.len= 1 time → fits.default_loader_extentries and 2default_loader_ext_bunentries are inserted viagetOrPutValue, which grows the map → no crash, just a redundant rehash/realloc.(Note also that
stringHashMapFromArraysonly callsensureTotalCapacityat all wheninput_loaders.extensions.len > 0, i.e. only when the user passes custom loaders.)Why it doesn't crash here but did in ESMConditions
ESMConditions.initfollows the under-reservedensureTotalCapacitywithputAssumeCapacityfor all inserts, so under-allocation overflows the map and trips the assertion.loadersFromTransformOptionsonlyputAssumeCapacitys the firstinput_loaders.extensions.lenitems — and that term is the unconditional first addend, always counted regardless of how theifparses — so the assumed-capacity inserts always fit. Everything after that usesgetOrPutValue.Addressing the objection
One verifier argued this shouldn't be flagged because there's no incorrect behavior — just a sub-optimal capacity hint. That's accurate: this is not a correctness bug and would not justify a standalone report. It's flagged here only because the PR's stated purpose is fixing this specific precedence trap in this file, and an identical instance sits ~500 lines away. Filing it as a nit / pre-existing so the author can decide whether to roll it into the same change; it should not block the PR.
Suggested fix
Same shape as the PR's fix: