-
Notifications
You must be signed in to change notification settings - Fork 5k
Fix crash in Bun.build with custom conditions #30528
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 |
|---|---|---|
|
|
@@ -649,6 +649,36 @@ | |
| expect(await html?.text()).toContain("<meta name='injected-by-plugin' content='true'>"); | ||
| }, | ||
| ); | ||
|
|
||
| test("does not crash with many custom conditions", async () => { | ||
|
Check warning on line 653 in test/bundler/bun-build-api.test.ts
|
||
|
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. 🟡 nit: Per Extended reasoning...What
The new test Why this applies hereThe most directly comparable test in the same describe block is
That test uses Safety checkThere is nothing preventing this test from running concurrently:
So the "unless it's very difficult" escape hatch in the guideline does not apply. ImpactPurely a style/convention nit. The test is functionally correct as written; using plain Fix- test("does not crash with many custom conditions", async () => {
+ test.concurrent("does not crash with many custom conditions", async () => { |
||
| // ESMConditions.init under-reserved capacity when allow_addons was true | ||
| // (the default) due to `if`-expression precedence, so passing several | ||
| // custom conditions overflowed putAssumeCapacity and crashed the bundler | ||
| // thread. Run in a subprocess since the crash aborts the whole process. | ||
| const dir = tempDirWithFiles("bun-build-api-conditions", { | ||
| "entry.ts": "export const x = 1;", | ||
| }); | ||
| await using proc = Bun.spawn({ | ||
| cmd: [ | ||
| bunExe(), | ||
| "-e", | ||
| ` | ||
| const result = await Bun.build({ | ||
| entrypoints: [${JSON.stringify(join(dir, "entry.ts"))}], | ||
| conditions: ["a", "b", "c", "d", "e", "f", "g", "h"], | ||
| }); | ||
| console.log("success:" + result.success); | ||
| `, | ||
| ], | ||
| env: bunEnv, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); | ||
| expect(stderr).toBe(""); | ||
| expect(stdout.trim()).toBe("success:true"); | ||
| expect(exitCode).toBe(0); | ||
| }); | ||
| }); | ||
|
|
||
| test.concurrent("macro with nested object", async () => { | ||
|
|
||
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.
🟣 Heads up: the same
if-expression precedence footgun also exists inloadersFromTransformOptionsin this file (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 inserts usegetOrPutValuerather thanputAssumeCapacity, so it just under-reserves and rehashes — but since this PR is specifically about this parsing gotcha, it might be worth fixing that occurrence too while you're here. (Pre-existing, not introduced by this PR.)Extended reasoning...
What
This PR fixes a Zig
if-expression precedence bug inESMConditions.initwheredefaults.len + 2 + if (allow_addons) 1 else 0 + conditions.lenparsed theelsebranch as(0 + conditions.len), droppingconditions.lenfrom the reservation whenallow_addonswas true. The exact same pattern exists elsewhere in the same file, inloadersFromTransformOptions:Why this is the same bug
Per the Zig grammar (and as the PR description itself explains), the
elsebranch of anifexpression consumes the full remaining expression. So this parses as:Step-by-step example
Take
target = .bunwith no custom loaders (input_loaders.extensions.len == 0):target.isBun()is true, so the firstifevaluates todefault_loader_ext_bun.len == 2.ifand+ default_loader_ext.len) is part of the untakenelsebranch and is skipped.stringHashMapFromArrays=0 + 2 = 2.0 + 2 + 0 + 18 = 20(sincedefault_loader_ext.len == 18).Similarly with
target = .browser: the firstifis false →0 + (second if); the secondifis true →default_loader_ext_browser.len == 1;default_loader_ext.lenis again dropped. Total =extensions.len + 1instead ofextensions.len + 1 + 18.Why it doesn't crash here
Unlike
ESMConditions.init, this occurrence is harmless in practice:stringHashMapFromArraysonly callsensureTotalCapacitywhenkeys.len > 0, and the onlyputAssumeCapacitycalls it makes are forinput_loaders.extensions— andtotal_capacity >= input_loaders.extensions.lenholds in every branch of the mis-parsed expression.default_loader_ext,default_loader_ext_bun, anddefault_loader_ext_browserall usegetOrPutValue(withtry), which grows the map on demand rather than assuming capacity.So the only effect is a missed pre-reservation and an extra rehash/realloc when custom loaders are passed with a bun/browser target — a minor perf inefficiency, not a correctness issue.
Suggested fix
Same approach as the PR's fix — hoist the conditionals into locals so the arithmetic is unambiguous:
Severity
Pre-existing — the PR doesn't touch
loadersFromTransformOptions, and there's no functional/correctness impact. But it's the identical footgun, in the same file, that this PR is explicitly about, so it seemed worth flagging as a "while you're here".