Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions src/bake/bake.zig
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,17 @@ pub const UserOptions = struct {
}

if (try config.getOptional(global, "bundlerOptions", JSValue)) |js_options| {
if (!js_options.isObject()) {
return global.throwInvalidArguments("'" ++ api_name ++ ".bundlerOptions' must be an object", .{});
}
if (try js_options.getOptional(global, "server", JSValue)) |server_options| {
bundler_options.server = try BuildConfigSubset.fromJS(global, server_options);
bundler_options.server = try BuildConfigSubset.fromJS(global, "server", server_options);
}
if (try js_options.getOptional(global, "client", JSValue)) |client_options| {
bundler_options.client = try BuildConfigSubset.fromJS(global, client_options);
bundler_options.client = try BuildConfigSubset.fromJS(global, "client", client_options);
}
if (try js_options.getOptional(global, "ssr", JSValue)) |ssr_options| {
bundler_options.ssr = try BuildConfigSubset.fromJS(global, ssr_options);
bundler_options.ssr = try BuildConfigSubset.fromJS(global, "ssr", ssr_options);
}
}

Expand Down Expand Up @@ -202,7 +205,11 @@ const BuildConfigSubset = struct {
minify_identifiers: ?bool = null,
minify_whitespace: ?bool = null,

pub fn fromJS(global: *jsc.JSGlobalObject, js_options: JSValue) bun.JSError!BuildConfigSubset {
pub fn fromJS(global: *jsc.JSGlobalObject, comptime name: []const u8, js_options: JSValue) bun.JSError!BuildConfigSubset {
if (!js_options.isObject()) {
return global.throwInvalidArguments("'" ++ api_name ++ ".bundlerOptions." ++ name ++ "' must be an object", .{});
}
Comment on lines +208 to +211

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.

🔴 This fix is incomplete — the nested minify option just below has the exact same bug. Bun.serve({ app: { bundlerOptions: { client: { minify: false } } } }) (or minify: 128n) still falls through to minify_options.getBooleanLoose(...) on a non-object and trips the same debugAssert(target.isObject()) this PR is fixing. The boolean guard should handle false (and non-objects should throw) before calling getBooleanLoose.

Extended reasoning...

What the bug is

This PR adds an isObject() guard at the top of BuildConfigSubset.fromJS (src/bake/bake.zig:209-211) to prevent debugAssert(target.isObject()) from firing when a non-object is passed for bundlerOptions.{server,client,ssr}. However, immediately below in the same function, the nested minify sub-option is read via getOptional(global, "minify", JSValue) and — if it isn't the boolean true — passed straight to getBooleanLoose, which has the exact same precondition.

Code path

In BuildConfigSubset.fromJS:

if (try js_options.getOptional(global, "minify", JSValue)) |minify_options| brk: {
    if (minify_options.isBoolean() and minify_options.asBoolean()) {
        // ... handle 'true'
        break :brk;
    }
    if (try minify_options.getBooleanLoose(global, "whitespace")) |value| { ... }

getBooleanLoose (src/jsc/JSValue.zig:1866-1868) calls this.get(global, property_name), and get() begins with bun.debugAssert(target.isObject()) (src/jsc/JSValue.zig:1534). So calling getBooleanLoose on a non-object primitive trips the same assertion this PR was opened to fix.

Why the existing guard doesn't help

The new isObject() check at line 209 validates js_options (the client/server/ssr object), but minify_options is fetched from inside it via getOptional(..., JSValue), which returns any non-null/undefined value untyped. The only guard on minify_options is isBoolean() and asBoolean(), which only short-circuits for true.

Step-by-step proof

With Bun.serve({ app: { bundlerOptions: { client: { minify: false } } } }):

  1. js_options = { minify: false } → passes the new isObject() check at line 209.
  2. getOptional(global, "minify", JSValue) returns the JS boolean false.
  3. Guard: minify_options.isBoolean() = true, minify_options.asBoolean() = falsetrue and false = false. Does not break :brk.
  4. Falls through to minify_options.getBooleanLoose(global, "whitespace").
  5. getBooleanLoose calls this.get(...)bun.debugAssert(false.isObject())panic in debug builds.

The same happens with minify: 128n / 5 / "str" / Symbol() (step 3: isBoolean() is false, so the AND is false, fall through). The minify: false case is particularly notable because it's a perfectly reasonable thing for a user to write.

Impact

Same fingerprint class (debugAssert target.isObject() in JSValue.get) as the crash this PR is fixing, in the very function this PR modified. The fuzzer will hit it next.

Fix

Either change the boolean guard to handle both values (if (minify_options.isBoolean()) { const b = minify_options.asBoolean(); options.minify_* = b; break :brk; }), or add an if (!minify_options.isObject()) return global.throwInvalidArguments(...) after the boolean check, mirroring what was done for js_options. The new tests should also cover { [key]: { minify: value } } for the same primitive set.


var options = BuildConfigSubset{};

if (try js_options.getOptional(global, "sourcemap", JSValue)) |val| brk: {
Expand Down
20 changes: 20 additions & 0 deletions test/js/bun/http/bun-serve-args.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -670,3 +670,23 @@ describe("Bun.serve unix socket validation", () => {
}
});
});

describe("app.bundlerOptions validation", () => {
const primitives = [128n, 5, "str", true, Symbol()];

test.each(primitives)("throws when bundlerOptions is %p", value => {
expect(() => {
// @ts-expect-error - Testing invalid input
serve({ port: 0, app: { bundlerOptions: value } });
}).toThrow("'app.bundlerOptions' must be an object");
});

describe.each(["server", "client", "ssr"])("bundlerOptions.%s", key => {
test.each(primitives)("throws when value is %p", value => {
expect(() => {
// @ts-expect-error - Testing invalid input
serve({ port: 0, app: { bundlerOptions: { [key]: value } } });
}).toThrow(`'app.bundlerOptions.${key}' must be an object`);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
});
Loading