-
Notifications
You must be signed in to change notification settings - Fork 4.7k
bake: validate app.bundlerOptions and its server/client/ssr fields are objects #30638
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 |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
|
|
||
| describe("Bun.serve app.bundlerOptions validation", () => { | ||
| test("throws when bundlerOptions is not an object", () => { | ||
| expect(() => | ||
| Bun.serve({ | ||
| // @ts-expect-error | ||
| app: { bundlerOptions: 42 }, | ||
| }), | ||
| ).toThrow("'app.bundlerOptions' must be an object"); | ||
| }); | ||
|
Comment on lines
+4
to
+11
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. Assert error shape ( Right now these tests only check message text, so they won’t catch regressions where the thrown error loses the expected type/code. Suggested test hardening describe("Bun.serve app.bundlerOptions validation", () => {
+ function expectInvalidArgType(fn: () => void, expectedMessage: string) {
+ try {
+ fn();
+ throw new Error("Expected Bun.serve() to throw");
+ } catch (err: any) {
+ expect(err).toBeInstanceOf(TypeError);
+ expect(err?.code).toBe("ERR_INVALID_ARG_TYPE");
+ expect(err?.message).toBe(expectedMessage);
+ }
+ }
+
test("throws when bundlerOptions is not an object", () => {
- expect(() =>
+ expectInvalidArgType(() =>
Bun.serve({
// `@ts-expect-error`
app: { bundlerOptions: 42 },
}),
- ).toThrow("'app.bundlerOptions' must be an object");
+ "'app.bundlerOptions' must be an object");
});
describe.each(["server", "client", "ssr"] as const)("bundlerOptions.%s", key => {
test.each([1073741824, "foo", true])("throws when value is %p", value => {
- expect(() =>
+ expectInvalidArgType(() =>
Bun.serve({
// `@ts-expect-error`
app: { bundlerOptions: { [key]: value } },
}),
- ).toThrow(`'app.bundlerOptions.${key}' must be an object`);
+ `'app.bundlerOptions.${key}' must be an object`);
});
});
});Also applies to: 15-21 🤖 Prompt for AI Agents
Comment on lines
+4
to
+11
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. 🟡 This test calls Extended reasoning...What the bug isThe new test file calls pub fn bake() bool {
return env.is_canary or env.isDebug or
bun.getRuntimeFeatureFlag(.BUN_FEATURE_FLAG_EXPERIMENTAL_BAKE);
}When this returns The code path that triggers it
Why existing code doesn't prevent it
Step-by-step proof
ImpactThis won't show up on this PR's own CI because PR builds default to canary ( How to fixMatch the pattern used by |
||
|
|
||
| describe.each(["server", "client", "ssr"] as const)("bundlerOptions.%s", key => { | ||
| test.each([1073741824, "foo", true])("throws when value is %p", value => { | ||
| expect(() => | ||
| Bun.serve({ | ||
| // @ts-expect-error | ||
| app: { bundlerOptions: { [key]: value } }, | ||
| }), | ||
| ).toThrow(`'app.bundlerOptions.${key}' must be an object`); | ||
| }); | ||
| }); | ||
| }); | ||
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.
🔴 The new
isObject()guard fixes the crash forserver/client/ssr, but the samedebugAssert(target.isObject())crash still exists ~15 lines below for theminifyfield. Withminify: false(or any non-boolean non-object likeminify: 5), theisBoolean() and asBoolean()guard evaluates to false and falls through tominify_options.getBooleanLoose(global, "whitespace"), which calls.get()on a non-object and trips the same assertion. Since this PR's goal is eliminating this crash class inBuildConfigSubset.fromJS, theminifypath should get the same treatment —break :brkon any boolean (not justtrue) and add anisObject()check before thegetBooleanLoosecalls.Extended reasoning...
What the bug is
BuildConfigSubset.fromJSnow correctly rejects non-object values forjs_optionsitself (the new check at bake.zig:211), but the nestedminifyfield at lines 224–240 still passes a non-object JSValue into a property getter, triggering the exact samebun.debugAssert(target.isObject())panic this PR is fixing.Code path
getBooleanLoose(JSValue.zig:1866) immediately callsthis.get(global, property_name), andget()hasbun.debugAssert(target.isObject())at JSValue.zig:1534 — the same assertion in the stack trace from the PR description.Why the existing guard doesn't help
The guard is
minify_options.isBoolean() and minify_options.asBoolean(). It only takes the earlybreak :brkwhen the value is boolean AND true. Anything else falls through to the property reads:minify: false→isBoolean() and asBoolean()=true and false=false→ falls throughminify: 5→isBoolean()=false→ falls throughminify: "yes"→isBoolean()=false→ falls throughgetOptionalonly filtersundefined/null, so all of these reachminify_optionsas non-object JSValues and then hitgetBooleanLoose→get→debugAssert(target.isObject())→ panic.Step-by-step proof
With this input (a debug build of Bun):
UserOptions.fromJSreadsbundlerOptions(an object — passes the new check at line 71).server(an object — passes the new check at line 211 insideBuildConfigSubset.fromJS).js_options.getOptional(global, "minify", JSValue)returns the JS booleanfalse(not filtered, sincegetOptionalonly dropsundefined/null).minify_options.isBoolean() and minify_options.asBoolean()→true and false→false. Does notbreak :brk.minify_options.getBooleanLoose(global, "whitespace")callsthis.get(...)withthis = false.bun.debugAssert(target.isObject())fails →panic: reached unreachable code.This is the same crash signature as the one in the PR description, just one frame deeper.
Impact
minify: falseis a very natural thing for a user to write to explicitly disable minification for one graph. In debug/assert builds it panics; in release builds it callsget()on a primitive, which is undefined behavior per the function's contract. The same Fuzzilli setup that produced fingerprint40903585822636ecwill immediately rediscover this once the outer check lands.Relationship to this PR
The lines themselves (224–240) are pre-existing, but they are inside
BuildConfigSubset.fromJS— the function this PR modifies specifically to eliminate this crash class. Leaving an identical crash ~15 lines below the new guard makes the fix incomplete for its stated purpose, so it's worth addressing here rather than in a follow-up.Suggested fix
Mirror what
JSBundler.zigdoes for the same option (JSBundler.zig:693–713):And add
minify: false/minify: 5cases totest/bake/app-options-validation.test.ts.