Skip to content

bake: validate app.bundlerOptions values are objects#30468

Closed
robobun wants to merge 2 commits into
mainfrom
farm/b6a80391/bake-bundler-options-validation
Closed

bake: validate app.bundlerOptions values are objects#30468
robobun wants to merge 2 commits into
mainfrom
farm/b6a80391/bake-bundler-options-validation

Conversation

@robobun

@robobun robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator

Fuzzer found a debug assertion crash in Bun.serve({ app: { bundlerOptions: ... } }).

JSValue.get asserts that its target is an object, but bake.UserOptions.fromJS and BuildConfigSubset.fromJS were calling .getOptional() on user-provided values without validating them first. This crashed when:

  • app.bundlerOptions was a primitive (number, string, boolean, symbol, bigint)
  • app.bundlerOptions.{server,client,ssr} was a primitive
  • app.bundlerOptions.*.minify was false or any non-boolean primitive (the previous check was isBoolean() and asBoolean(), so false fell through to the object path)

These now throw InvalidArguments errors instead.

Fingerprint: 86855f8bedcf582b

Previously, passing a non-object value for `app.bundlerOptions`,
`app.bundlerOptions.{server,client,ssr}`, or a non-boolean non-object
value for `minify` would hit a debug assertion in `JSValue.get` instead
of throwing a proper error. Additionally, `minify: false` would fall
through to the object path and crash.

Now these cases throw `InvalidArguments` errors.
@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 5:11 PM PT - May 10th, 2026

@autofix-ci[bot], your commit fb38f0b has 1 failures in Build #53202 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 30468

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

bun-30468 --bun

@coderabbitai

coderabbitai Bot commented May 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

Pull request was closed or merged during review

Walkthrough

Adds context-aware validation for nested bundlerOptions subsets by passing a comptime name into BuildConfigSubset.fromJS, supports boolean or object minify values, and adds parameterized tests covering server/client/ssr and minify cases.

Changes

Bundler Options Validation

Layer / File(s) Summary
Entry Point Routing
src/bake/bake.zig
UserOptions.fromJS now routes bundlerOptions.server, bundlerOptions.client, and bundlerOptions.ssr subset parsing through BuildConfigSubset.fromJS with corresponding name arguments for context-specific validation.
Subset Object Validation
src/bake/bake.zig
BuildConfigSubset.fromJS signature updated to accept comptime name: []const u8; validates each subset is an object and includes the bundlerOptions.<name> path in error messages.
Minify Type and Expansion
src/bake/bake.zig
Minify field now accepts boolean (expanding to minify_syntax, minify_identifiers, minify_whitespace) or object; error messages reference bundlerOptions.<name>.minify.
Test Suite Structure
test/bake/bundler-options-validation.test.ts
Adds a parameterized test suite for Bun.serve app.bundlerOptions validation covering server, client, and ssr subsets.
Type Validation Tests
test/bake/bundler-options-validation.test.ts
Parameterized tests ensure app.bundlerOptions and each subset must be objects; non-object values throw path-specific errors.
Minify Field Tests
test/bake/bundler-options-validation.test.ts
Tests verify minify accepts boolean or object types with context-specific error paths, and confirm boolean minify values do not trigger validation crashes.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides context about the bug (fuzzer-found assertion crash) and specific examples of invalid inputs, but lacks verification details and implementation overview. Add a 'How did you verify your code works?' section describing test coverage (e.g., the new test suite in bundler-options-validation.test.ts) to complete the template.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: adding validation for app.bundlerOptions values to ensure they are objects.
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. bake: validate bundlerOptions values are objects before property access #30125 - Also validates bundlerOptions values are objects in bake.zig and fixes the minify: false fallthrough bug
  2. Validate app.bundlerOptions types in Bun.serve #30402 - Also validates app.bundlerOptions types in BuildConfigSubset.fromJS/UserOptions.fromJS and fixes the minify: false fallthrough bug

🤖 Generated with Claude Code

@robobun

robobun commented May 11, 2026

Copy link
Copy Markdown
Collaborator Author

Duplicate of #30125 (and #30402). Closing in favor of the earlier PR.

@robobun robobun closed this May 11, 2026
@robobun robobun deleted the farm/b6a80391/bake-bundler-options-validation branch May 11, 2026 00:11
Comment thread src/bake/bake.zig
Comment on lines +71 to +73
if (!js_options.isObject()) {
return global.throwInvalidArguments("'" ++ api_name ++ ".bundlerOptions' must be an object", .{});
}

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.

🟣 Heads-up (pre-existing, not introduced by this PR): the same crash class still exists for framework.fileSystemRouterTypes — each fsr_opts array element goes straight into getOptionalString()target.get() without an isObject() check, so Bun.serve({ app: { framework: { fileSystemRouterTypes: [42] } } }) still trips bun.debugAssert(target.isObject()). The adjacent builtInModules loop already guards this; might be worth adding the same if (!fsr_opts.isObject()) return global.throwInvalidArguments(...) while you're in this file.

Extended reasoning...

What the bug is

This PR fixes the debug-assert crash for app.bundlerOptions by adding isObject() guards before calling .get()/.getOptional() on user-supplied values. However, the identical crash pattern remains in the same file for framework.fileSystemRouterTypes array elements.

In Framework.fromJS (src/bake/bake.zig), the fileSystemRouterTypes loop iterates each element as fsr_opts and immediately passes it to getOptionalString(fsr_opts, global, "root", ...). getOptionalString() calls target.get(global, property) on its first line, and JSValue.get() (src/jsc/JSValue.zig:1534) contains bun.debugAssert(target.isObject()). There is no isObject() check on fsr_opts before this call.

Code path

while (try it.next()) |fsr_opts| : (i += 1) {
    const root = try getOptionalString(fsr_opts, global, "root", refs, arena) orelse { ... };
    // ^ no isObject() guard — crashes if fsr_opts is a primitive

Compare with the adjacent builtInModules loop in the same function, which does guard:

while (try it.next()) |file| : (i += 1) {
    if (!file.isObject()) {
        return global.throwInvalidArguments("'builtInModules[{d}]' is not an object", .{i});
    }

The asymmetry confirms this is an oversight rather than intentional.

Why existing code doesn't prevent it

opts.getArray(global, "fileSystemRouterTypes") only validates that the property itself is an array; it does not validate the element types. The iterator yields whatever JSValues are in the array, including primitives.

Step-by-step proof

  1. User calls Bun.serve({ app: { framework: { fileSystemRouterTypes: [42] } } }).
  2. UserOptions.fromJS sees config is an object, skips the bundlerOptions branch (not present), and calls Framework.fromJS with the framework object.
  3. Framework.fromJS sees opts is an object, falls through the string/object checks, processes reactFastRefresh/serverComponents/builtInModules (all absent → defaults).
  4. Reads fileSystemRouterTypes via getArray — it's an array of length 1, passes.
  5. Iterator yields fsr_opts = 42 (a JS number, not an object).
  6. Calls getOptionalString(fsr_opts, global, "root", ...)target.get(global, "root")bun.debugAssert(target.isObject()) fires on the number → crash in debug builds.

Impact

Same as the bug this PR fixes (fingerprint 86855f8bedcf582b): a debug-assert crash on malformed user input instead of a clean InvalidArguments throw. The fuzzer that found the bundlerOptions case will likely re-discover this via the framework path.

Fix

Add the same guard at the top of the loop:

while (try it.next()) |fsr_opts| : (i += 1) {
    if (!fsr_opts.isObject()) {
        return global.throwInvalidArguments("'fileSystemRouterTypes[{d}]' is not an object", .{i});
    }
    const root = try getOptionalString(fsr_opts, global, "root", refs, arena) orelse { ... };

This is pre-existing — the PR does not touch these lines — so it's non-blocking, but worth doing while the author is in this file fixing the same root cause.

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