Skip to content

Handle exceptions from toStringOrNull in Bun.plugin target parsing#28369

Closed
robobun wants to merge 1 commit into
mainfrom
farm/8f0ba90e/fix-plugin-toprimitive-exception
Closed

Handle exceptions from toStringOrNull in Bun.plugin target parsing#28369
robobun wants to merge 1 commit into
mainfrom
farm/8f0ba90e/fix-plugin-toprimitive-exception

Conversation

@robobun

@robobun robobun commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator

When Bun.plugin() receives an object whose target property has a Symbol.toPrimitive that returns a non-primitive value, toStringOrNull throws a JS exception and returns nullptr. The code correctly skipped the inner if block, but then continued execution with the pending exception still active, eventually hitting assertNoException() in constructEmptyObject.

This adds RETURN_IF_EXCEPTION checks after both toStringOrNull() (which can throw when Symbol.toPrimitive returns a non-primitive) and value() (which can also throw) to properly propagate the exception back to JavaScript.

Repro:

const obj = { setup() {} };
Object.defineProperty(obj, "target", {
  get() { return { [Symbol.toPrimitive]() { return {}; } }; }
});
Bun.plugin(obj); // was: assertion failure / crash; now: throws TypeError

robobun verification (iter 40): Diff is exactly two RETURN_IF_EXCEPTION(throwScope, {}) guards in BunPlugin.cpp (lines 304 and 310) matching the established pattern at lines 290/293/300 of the same function. On main, execution falls through to constructEmptyObject with a pending exception, hitting assertNoException(). Regression test (15 lines, in-process) exercises the exact crash path via Symbol.toPrimitive returning non-primitive {}; asserts .toThrow("toPrimitive") — would crash/SIGABRT on main, not pass. Lint JS passed on both a960e7d and cbbbd50. No TODO/FIXME/HACK. No CHANGES_REQUESTED reviews. No unrelated changes in final diff. Buildkite #41233 pending at time of approval.

@robobun

robobun commented Mar 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 11:34 PM PT - Mar 25th, 2026

@robobun, your commit 6751096 has 3 failures in Build #42156 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 28369

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

bun-28369 --bun

@coderabbitai

coderabbitai Bot commented Mar 21, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Added explicit exception checks in the Bun.plugin native setup to return early if a pending JS exception exists while extracting/processing the plugin target. Added a spawned-process regression test for a target whose Symbol.toPrimitive returns a non-primitive. Also swapped import order in an unrelated s3 test file.

Changes

Cohort / File(s) Summary
Plugin binding update
src/bun.js/bindings/BunPlugin.cpp
Inserted two RETURN_IF_EXCEPTION(throwScope, {}) checks: one after extracting the target string/value and one after the if (targetValue) { ... } handling block to abort early on pending JS exceptions.
Regression test (plugin)
test/js/bun/plugin/plugin-toprimitive-crash.test.ts
Added a spawned Bun subprocess test that calls Bun.plugin() with an object whose target getter’s [Symbol.toPrimitive] returns a non-primitive; child process prints exception name and message and exits; parent asserts expected output and exit code.
Test import order tweak
test/js/bun/s3/s3-fd-validation.test.ts
Swapped named imports from bun:test ({ test, expect }{ expect, test }) with no behavioral changes.
🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: adding exception handling for toStringOrNull in Bun.plugin target parsing.
Description check ✅ Passed The PR description clearly explains the bug, provides a reproducible case, and details the fix with proper verification.

✏️ 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

No related open issues found.

This PR addresses a specific and previously unreported crash scenario involving Bun.plugin() when target properties use Symbol.toPrimitive returning non-primitive values. The fix adds proper exception handling in the plugin validation code path.

🤖 Generated with Claude Code

@claude claude Bot left a comment

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.

LGTM — the two RETURN_IF_EXCEPTION additions correctly fix the crash by propagating the pending JSC exception rather than continuing into constructEmptyObject.

Extended reasoning...

Overview

This PR touches two files: src/bun.js/bindings/BunPlugin.cpp (2-line logic change) and a new test file. The C++ change adds RETURN_IF_EXCEPTION after JSString::value(globalObject) (which can throw) and after the entire toStringOrNull conditional block (to catch the case where toStringOrNull returned null because it threw, but the inner if body was skipped).

Security risks

None. This is a crash fix that converts an assertion failure into a proper JS TypeError. No auth, crypto, or permission code is involved.

Level of scrutiny

Low-to-medium. The pattern of adding RETURN_IF_EXCEPTION after JSC operations that can throw is mechanical and well-established throughout this file and the broader codebase. Both placement sites are correct: the first guards value() (which resolves a JSString and can trigger getters/toPrimitive), and the second guards the toStringOrNull path where the returned null means an exception is already pending.

Other factors

The bug hunter flagged two nits on the test: a missing expect(stderr).toBe("") (CLAUDE.md convention for spawned-process tests) and a hardcoded JSC-internal error string that could break on WebKit upgrades. These are valid test-quality observations already posted as inline comments, but they don't affect the correctness of the C++ fix. The fix itself is minimal, targeted, and clearly correct.

Comment thread test/js/bun/plugin/plugin-toprimitive-crash.test.ts Outdated

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@test/js/bun/plugin/plugin-toprimitive-crash.test.ts`:
- Line 35: Replace the brittle negative stderr check
(expect(stderr).not.toContain("error:")) with a positive assertion that verifies
the specific expected failure behavior: capture the process result used in this
test (stdout/stderr or the thrown exception from the test harness) and assert
the exact error type or message you expect (e.g., match the expected error
name/message from stdout or assert the thrown exception via the test runner).
Update the assertion around the variables stdout/stderr or the function that
runs the plugin to check for the precise expected output/exception instead of
asserting absence of "error:".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 68795341-bfeb-47f2-8979-77d808b95c75

📥 Commits

Reviewing files that changed from the base of the PR and between 56c07c0870b8d4fa9c9742c54a952d7940d754ae and 64c33e97e51b32be26d981e0d273860e9e54ee55.

📒 Files selected for processing (1)
  • test/js/bun/plugin/plugin-toprimitive-crash.test.ts

Comment thread test/js/bun/plugin/plugin-toprimitive-crash.test.ts Outdated
Comment thread test/js/bun/plugin/plugin-toprimitive-crash.test.ts Outdated

@claude claude Bot left a comment

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.

LGTM — all prior review feedback has been addressed.

Extended reasoning...

Overview

This PR touches two files: src/bun.js/bindings/BunPlugin.cpp (2 lines added) and a new regression test test/js/bun/plugin/plugin-toprimitive-crash.test.ts (34 lines). The C++ change adds two RETURN_IF_EXCEPTION(throwScope, {}) guards in setupBunPlugin after the toStringOrNull call and after the value() call, matching the identical pattern already used at lines 290/293/300 in the same function.

Security risks

No security risks. The change only adds early-exit guards for an existing exception path; it does not alter any auth, crypto, permissions, or data-exposure logic.

Level of scrutiny

Low. The fix is mechanical: two one-liner macro invocations following an established pattern already present in the same function block. The regression test is subprocess-based (survives assertion crashes), uses bunEnv, wraps Bun.plugin() in try/catch, and now asserts expect(stderr).toBe("") per test/CLAUDE.md convention. Both the positive stdout assertions ("TypeError", "toPrimitive") and the empty-stderr invariant are present in the final state.

Other factors

The pre-existing bug noted in the bug report (validated target string is never actually used to select per-target plugin functions) predates this PR and is not introduced or worsened by it. All reviewer feedback across multiple iterations has been resolved in commit 380e035.

Comment on lines 301 to 311
if (targetValue) {
if (auto* targetJSString = targetValue.toStringOrNull(globalObject)) {
String targetString = targetJSString->value(globalObject);
RETURN_IF_EXCEPTION(throwScope, {});
if (!(targetString == "node"_s || targetString == "bun"_s || targetString == "browser"_s)) {
JSC::throwTypeError(globalObject, throwScope, "plugin target must be one of 'node', 'bun' or 'browser'"_s);
return {};
}
}
RETURN_IF_EXCEPTION(throwScope, {});
}

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.

🟣 Pre-existing issue: Bun.plugin({ target: "node", ... }) silently registers as a bun-target plugin because the validated target string is never used. After the validation block, the builder object is hardcoded with target: "bun" and always wires jsFunctionAppendOnLoadPluginBun / jsFunctionAppendOnResolvePluginBun, regardless of what the caller specified. This is unrelated to the PR's exception-propagation fix, but the PR directly touches this code block.

Extended reasoning...

What the bug is

In setupBunPlugin (BunPlugin.cpp lines 301–311), the function reads and validates the JS-level target property against "node", "bun", and "browser", throwing a TypeError for any other value. However, the validated targetString variable is never used after the validation check. The function always constructs the builder object with target: "bun" hardcoded and always wires jsFunctionAppendOnLoadPluginBun / jsFunctionAppendOnResolvePluginBun as the onLoad/onResolve handlers.

The specific code path

After the validation block (line ~311), the code does:

builderObject->putDirect(vm, Identifier::fromString(vm, "target"_s), jsString(vm, String("bun"_s)), 0);
// ...
builderObject->putDirectNativeFunction(vm, globalObject, ..., jsFunctionAppendOnLoadPluginBun, ...);
builderObject->putDirectNativeFunction(vm, globalObject, ..., jsFunctionAppendOnResolvePluginBun, ...);

This is unconditional. The BunPluginTarget target parameter accepted by setupBunPlugin is never referenced inside the function body, and the only call site (jsFunctionBunPlugin) always passes BunPluginTargetBun anyway.

Why existing code does not prevent it

The validation block correctly rejects invalid target strings, giving the impression that per-target semantics are implemented. The infrastructure for per-target registration does exist — jsFunctionAppendOnLoadPluginNode, jsFunctionAppendOnLoadPluginBrowser, etc. are defined — but setupBunPlugin never selects between them based on the validated target.

Step-by-step proof

  1. User calls Bun.plugin({ target: "node", setup(b) { b.onLoad({ filter: /\.txt$/ }, handler) } }).
  2. setupBunPlugin reads and validates the target property — "node" passes validation.
  3. targetString holds "node" but is never consulted again.
  4. The builder object is constructed with target: "bun" and onLoad wired to jsFunctionAppendOnLoadPluginBun.
  5. When setup(b) calls b.onLoad(...), the handler is registered in globalObject->onLoadPlugins as a bun-target plugin, not a node-target plugin.
  6. The user receives silent misbehavior — no error, but wrong semantics.

Impact

Any code that depends on target-specific plugin registration (e.g., plugins that should only apply during bundling for a specific target) will silently apply as a bun-runtime plugin instead. This is a correctness issue but pre-dates this PR.

How to fix

After validation, capture the resolved BunPluginTarget enum value and use it to select the correct onLoad/onResolve native functions. A switch on the validated targetString ("node" → BunPluginTargetNode, "browser" → BunPluginTargetBrowser, else BunPluginTargetBun) would allow the existing per-target functions to be wired correctly.

Pre-existing status

This bug predates the PR. The PR only adds RETURN_IF_EXCEPTION(throwScope, {}) guards at lines 304 and 310 to handle the crash when Symbol.toPrimitive returns a non-primitive. It does not introduce or worsen the target-ignored behavior, but it directly touches the affected block, making this an appropriate finding.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@robobun worth fixing?

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.

Yes, worth fixing, but as a separate PR. The pre-existing bug is real: BunPluginTarget target is accepted as a parameter by setupBunPlugin and jsFunctionBunPlugin always passes BunPluginTargetBun, while the validated targetString is never used to select jsFunctionAppendOnLoadPluginNode/jsFunctionAppendOnLoadPluginBrowser (which do exist). This means Bun.plugin({ target: "node" }) silently registers as a bun-target plugin.

That said, this PR is specifically about the exception-propagation crash fix and the target-selection logic is orthogonal. I would recommend merging this PR as-is and opening a follow-up to wire the target enum through setupBunPlugin properly.

Comment thread test/js/bun/plugin/plugin-toprimitive-crash.test.ts Outdated
@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch 3 times, most recently from 193d9ba to 6764546 Compare March 21, 2026 16:10

@claude claude Bot left a comment

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.

LGTM — all prior feedback has been addressed in the final commit.

Extended reasoning...

Overview

This PR adds two RETURN_IF_EXCEPTION(throwScope, {}) guards in setupBunPlugin (BunPlugin.cpp) after toStringOrNull() and after the if (targetValue) block. It adds a subprocess-based regression test and an import-order tweak in an unrelated test file.

Security risks

None. The change purely adds exception propagation guards in JS exception handling paths; no auth, crypto, or permissions code is touched.

Level of scrutiny

Low. The two added lines follow the identical pattern already present at lines 290, 293, and 300 in the same function. The fix is mechanical and the intent is clear: propagate a pending JS exception rather than continue into constructEmptyObject which asserts no exception is pending.

Other factors

All inline feedback from prior review rounds has been incorporated in the final diff: stderr: "pipe" is present in spawn options, proc.stderr.text() is included in the Promise.all destructuring, expect(stderr).toBe("") is asserted, and the stdout assertions use .toContain rather than a hardcoded JSC-internal message string. No bugs were found by the automated bug hunting system.

@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch from ec3cfe3 to 491e1c7 Compare March 21, 2026 18:05

@claude claude Bot left a comment

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.

LGTM — the C++ fix is minimal and correct, and the regression test now properly captures stderr with toBe("") as required.

Extended reasoning...

Overview

This PR touches two files: src/bun.js/bindings/BunPlugin.cpp (2 lines added) and a new regression test test/js/bun/plugin/plugin-toprimitive-crash.test.ts. The C++ change adds two RETURN_IF_EXCEPTION(throwScope, {}) guards in setupBunPlugin — one after targetJSString->value(globalObject) and one after the entire if (targetValue) block — to prevent execution from continuing with a pending JS exception into constructEmptyObject, which would hit assertNoException().

Security risks

None. The change only adds early-return guards on exception paths; it does not alter any auth, permissions, crypto, or data-exposure logic.

Level of scrutiny

Low. The fix is mechanical and follows the identical pattern of the existing guards at lines 290, 293, and 300 in the same function. The pattern (RETURN_IF_EXCEPTION(throwScope, {})) is well-established JSC idiom throughout the codebase. The regression test is subprocess-based (survives assertion crashes), asserts exitCode === 0, stdout contains TypeError and toPrimitive, and stderr is empty — covering the full crash scenario.

Prior review concerns

My earlier comments flagged (1) missing stderr: "pipe", (2) missing proc.stderr.text() in destructuring, and (3) missing expect(stderr).toBe(""). The final diff addresses all three: stderr: "pipe" is present in spawn options, all three values are destructured from Promise.all, and expect(stderr).toBe("") appears before expect(exitCode).toBe(0). All prior inline threads are marked resolved.

Other factors

The pre-existing issue I noted (target string being validated but then hardcoded as "bun") predates this PR and is out of scope. The unrelated bundler_compile.test.ts CI failures appear to be infrastructure flakiness not caused by this PR.

@claude claude Bot left a comment

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.

LGTM — all previously flagged test issues have been addressed.

Extended reasoning...

Overview

This PR touches src/bun.js/bindings/BunPlugin.cpp (2 lines added), adds a new regression test test/js/bun/plugin/plugin-toprimitive-crash.test.ts, and makes a trivial import-order tweak in test/js/bun/s3/s3-fd-validation.test.ts.

Security risks

No security-sensitive code is touched. The change adds exception propagation guards in a plugin registration path; there is no auth, crypto, or permission-sensitive logic involved.

Level of scrutiny

Low. The C++ change is two lines of RETURN_IF_EXCEPTION(throwScope, {}), following the identical macro pattern already present at lines 290, 293, and 300 of the same function. The fix is mechanical and follows established JSC exception-handling conventions. The regression test spawns a subprocess and now correctly captures and asserts on stderr (expect(stderr).toBe("")) — the pattern required by test/CLAUDE.md.

Other factors

All prior inline comment threads from previous review runs are marked resolved. The s3-fd-validation.test.ts change is a no-op import reorder (autofix.ci output). No bugs were found by the automated bug hunting system. The fix is self-contained and the scope matches the PR description exactly.

@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch from 5c5c413 to bc43f78 Compare March 21, 2026 20:06
Comment thread test/js/bun/plugin/plugin-toprimitive-crash.test.ts Outdated
@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch from 95d7100 to cbd0792 Compare March 21, 2026 22:05

@claude claude Bot left a comment

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.

LGTM — the two RETURN_IF_EXCEPTION guards are correct and match the existing pattern in the same function.

Extended reasoning...

Overview

This PR touches src/bun.js/bindings/BunPlugin.cpp (two-line guard addition), a new regression test, and a trivial import-order tweak in an unrelated test. Scope is minimal.

Security risks

None. The change only adds early-return guards on an existing exception scope; it does not affect auth, crypto, permissions, or any sensitive code path.

Level of scrutiny

Low. The fix is mechanical: RETURN_IF_EXCEPTION(throwScope, {}) is a well-understood JSC macro used many times elsewhere in the same file. The two new calls are placed exactly where toStringOrNull() and value() can leave a pending exception, which is precisely the documented root cause of the crash.

Other factors

The void stderr test style nit has been raised and resolved through prior inline comment rounds. Robobun verification explicitly calls it a non-blocking style gap. The regression test's exitCode + stdout assertions are sufficient to detect the original crash. CI failures in the timeline are in bundler_compile.test.ts across multiple platforms, unrelated to this diff.

@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch from cbd0792 to 3356005 Compare March 22, 2026 01:45
Comment thread test/js/bun/plugin/plugin-toprimitive-crash.test.ts Outdated
@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch from 04b84b2 to b729322 Compare March 22, 2026 04:07
@robobun

robobun commented Mar 22, 2026

Copy link
Copy Markdown
Collaborator Author

Verification (robobun): C++ fix adds two standard RETURN_IF_EXCEPTION guards matching the existing pattern in setupBunPlugin (lines 290/293/300). Regression test exercises the exact crash path (Symbol.toPrimitive returning non-primitive) — would SIGABRT on main, throws TypeError with the fix. CI Build #40978 pending; previous Build #40953 failures are unrelated (bundler_compile timeouts, worker_threads segfault). No TODO/FIXME/HACK in diff. No blocking review concerns.

@claude claude Bot left a comment

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.

LGTM — the two RETURN_IF_EXCEPTION guards are correct and follow the established pattern in the same function.

Extended reasoning...

Overview

The PR touches three files: BunPlugin.cpp (C++ fix), a new regression test plugin-toprimitive-crash.test.ts, and a cosmetic import-order change in s3-fd-validation.test.ts.

Security risks

No security-sensitive code paths are affected. The change adds exception propagation guards in plugin target parsing — no auth, crypto, or permissions code is involved.

Level of scrutiny

Low: the C++ change is two lines of RETURN_IF_EXCEPTION(throwScope, {}) matching the identical pattern already used at lines 290/293/300 in the same function. This is a mechanical safety fix for a crash path, not a logic change. The regression test is a clean in-process test that directly exercises the crash path and confirms it now throws instead of asserting.

Other factors

The test was heavily iterated based on prior review feedback (subprocess approach → simpler in-process approach). The final 15-line version is clean and correct. The one remaining nit — using .toThrow("toPrimitive") instead of .toThrow(TypeError) — is filed as an inline comment but does not block approval: the test still catches the regression (crash vs. throw) and the ECMAScript spec mandates TypeError for this path, so a different error type from JSC is not a realistic concern.

};
},
});
expect(() => Bun.plugin(obj)).toThrow("toPrimitive");

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.

🟡 The test uses .toThrow("toPrimitive") which only verifies the error message contains the substring, but does not assert the thrown error is a TypeError. The PR explicitly states the fix causes "throws TypeError" and the C++ code calls JSC::throwTypeError(...); a future regression throwing a different error type (e.g. RangeError) with "toPrimitive" in its message would silently pass this check. Fix: use .toThrow(TypeError) to also assert the error type.

Extended reasoning...

What the bug is and how it manifests

Line 14 of the new test uses expect(() => Bun.plugin(obj)).toThrow("toPrimitive"). Jest/Bun’s .toThrow(string) overload only checks that the thrown error’s message contains the given substring. It does not check the error class at all. This means the test only validates two things: (1) the function throws something, and (2) whatever is thrown has a message containing "toPrimitive".

The specific code path that triggers it

The C++ fix in BunPlugin.cpp inserts RETURN_IF_EXCEPTION(throwScope, {}) guards after toStringOrNull() and after value(). When Symbol.toPrimitive returns a non-primitive, JSC internally calls JSC::throwTypeError(...) (from within toPrimitive()), setting a pending TypeError in the throw scope. The RETURN_IF_EXCEPTION guard then surfaces that pending exception to JavaScript. The PR description explicitly documents this: "was: assertion failure / crash; now: throws TypeError".

Why existing code does not prevent it

The .toThrow("toPrimitive") matcher is satisfied by any thrown value whose .message contains "toPrimitive", regardless of the error’s constructor or prototype chain. If a future regression changed the error type—say, a refactor accidentally used throwRangeError or a generic throwException—while preserving a message containing "toPrimitive", the test would continue to pass, providing false confidence that the TypeError contract is maintained.

Impact assessment

This is a test-precision gap, not a bug in the production fix itself. The C++ fix is correct and the primary regression goal (crash → throw) is properly tested. However, the stated contract of the fix (specifically TypeError) is not enforced by the test. The PR description and the inline C++ semantics create an expectation that a TypeError is thrown; the test does not verify this.

Addressing the refutation

The refutation correctly notes that the ECMAScript spec (sec-toprimitive) mandates a TypeError when Symbol.toPrimitive returns a non-primitive, and JSC follows the spec. There is no plausible scenario within spec-compliant JSC where a different error type with "toPrimitive" in its message would be thrown from this path. This analysis is accepted—the gap is theoretical rather than a realistic regression risk—which is why this is filed at nit severity, not normal.

Step-by-step proof

  1. The test at line 14 calls expect(() => Bun.plugin(obj)).toThrow("toPrimitive").
  2. Internally, Bun’s test runner resolves .toThrow(string) as a substring message check.
  3. Suppose a future engineer refactors setupBunPlugin to call a custom error constructor that extends Error (not TypeError) but uses the same JSC message string.
  4. The thrown error’s message still contains "toPrimitive"; .toThrow("toPrimitive") passes.
  5. But the thrown error is not a TypeError; .toThrow(TypeError) would have failed, catching the regression.

How to fix

Replace line 14 with:

expect(() => Bun.plugin(obj)).toThrow(TypeError);

Or, to assert both type and message:

expect(() => Bun.plugin(obj)).toThrow(new TypeError());
// and separately:
try { Bun.plugin(obj); } catch (e) {
  expect(e).toBeInstanceOf(TypeError);
  expect((e as TypeError).message).toContain("toPrimitive");
}

The single .toThrow(TypeError) form is idiomatic and sufficient.

@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch 2 times, most recently from 8f21762 to 543c4fb Compare March 22, 2026 12:07

@robobun robobun left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The target-ignored behavior predates this PR - the validated target string has never been wired to select between the per-target onLoad/onResolve functions (jsFunctionAppendOnLoadPluginNode, etc.). It's always hardcoded to bun. Happy to fix it in a follow-up if you'd like, but keeping this PR scoped to the crash fix.

Comment thread src/bun.js/bindings/BunPlugin.cpp

@claude claude Bot left a comment

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.

LGTM — the two RETURN_IF_EXCEPTION guards are correct and minimal, matching the existing pattern in the same function, and the regression test directly exercises the crash path.

Extended reasoning...

Overview

The PR touches src/bun.js/bindings/BunPlugin.cpp (two RETURN_IF_EXCEPTION(throwScope, {}) guards added at lines 304 and 310 of setupBunPlugin), a new in-process regression test test/js/bun/plugin/plugin-toprimitive-crash.test.ts, and a trivial import-order swap in an unrelated S3 test.

Security risks

None. The change only adds early-return exception propagation guards; it does not alter any auth, permission, crypto, or sandbox boundary.

Level of scrutiny

Low. This is a minimal, surgical bug fix following a well-established JSC pattern already used at three other sites in the same function (lines 290, 293, 300). The crash path (pending JS exception reaching assertNoException() in constructEmptyObject) is straightforward and the fix is the canonical remedy.

Other factors

The regression test constructs the exact repro from the PR description in-process — it would SIGABRT on unpatched code and now throws a TypeError. The only nit (.toThrow("toPrimitive") vs .toThrow(TypeError)) is already documented in an inline comment; the ECMAScript spec mandates TypeError for this case so the assertion is functionally sound. No bugs were found by the automated hunting system. CI is pending but unrelated failures (bundler timeouts, worker_threads) have been identified as pre-existing.

@claude claude Bot left a comment

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.

LGTM — the two RETURN_IF_EXCEPTION guards are minimal, correct, and match the established pattern; the in-process regression test covers the exact crash path.

Extended reasoning...

Overview

This PR touches src/bun.js/bindings/BunPlugin.cpp (two one-line exception guards), a new 15-line in-process regression test, and a cosmetic import-order swap in an unrelated test file.

Security risks

None. The change is purely defensive exception handling — it propagates an already-pending JSC exception rather than allowing execution to continue with a dirty throw scope into assertNoException().

Level of scrutiny

Low. Both RETURN_IF_EXCEPTION(throwScope, {}) calls follow the identical macro pattern used at multiple other sites in the same function (lines 290, 293, 300 of the context). The fix does not introduce new logic, change semantics for valid inputs, or affect any code paths other than the crash path.

Other factors

The PR went through many review iterations focused on spawned-subprocess test patterns. The author resolved all those concerns by switching to a clean in-process test, which is simpler and more reliable. The remaining open nit (.toThrow("toPrimitive") vs .toThrow(TypeError)) is theoretical — ECMAScript sec-toprimitive mandates TypeError for this case and JSC follows the spec. The bug hunting system found no bugs. CI passed on 6 platforms with the remaining failures attributed to infra issues unrelated to this change.

@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch from 0457145 to be0c025 Compare March 22, 2026 18:43

@claude claude Bot left a comment

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.

LGTM — the two RETURN_IF_EXCEPTION guards are correct, follow the established pattern, and the regression test covers the crash path.

Extended reasoning...

Overview

Two files changed: BunPlugin.cpp gets two RETURN_IF_EXCEPTION(throwScope, {}) guards in setupBunPlugin, and a new 15-line in-process regression test is added at test/js/bun/plugin/plugin-toprimitive-crash.test.ts.

C++ fix

The guards are inserted at exactly the right points: after targetJSString->value(globalObject) (which can throw if the string value conversion fails) and after the entire if (targetValue) block (to catch the case where toStringOrNull returned nullptr due to a pending exception from Symbol.toPrimitive). Both placements follow the identical pattern used throughout the same function and file. The fix is minimal and correct.

Regression test

The test directly exercises the crash path: a Symbol.toPrimitive returning a non-primitive object. The expect(() => Bun.plugin(obj)).toThrow("toPrimitive") assertion confirms the crash is converted to a proper thrown exception. A prior inline comment I filed noted .toThrow(TypeError) would be marginally stricter, but .toThrow("toPrimitive") is sufficient to confirm the crash → throw conversion, and the ECMAScript spec mandates TypeError here so there is no realistic false-negative risk.

Security risks

None. This only affects Bun.plugin() target validation and adds exception propagation — no auth, crypto, or permission code paths involved.

Level of scrutiny

Low. This is a two-line targeted exception-propagation fix in a well-understood function, with a direct regression test. The pattern is identical to existing guards in the same function (lines 290/293/300). CI passed on all reported platforms.

@claude claude Bot left a comment

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.

LGTM — the two RETURN_IF_EXCEPTION guards are correct and the in-process regression test cleanly covers the crash path.

Extended reasoning...

Overview

Two files changed: BunPlugin.cpp receives two RETURN_IF_EXCEPTION(throwScope, {}) guards inside setupBunPlugin, and a new 15-line in-process test exercises the exact crash path (Symbol.toPrimitive returning a non-primitive object).

Security risks

None. This is pure exception-propagation hygiene — converting a native SIGABRT into a proper JS TypeError. No auth, crypto, or permissions code is touched.

Level of scrutiny

Low. The diff is minimal and mechanical: the RETURN_IF_EXCEPTION macro is the standard JSC pattern used dozens of times in the same file and throughout the codebase. The test is synchronous and self-contained. CI passed on 6 platforms.

Other factors

The PR went through many iterations focused on the regression test format (spawned subprocess vs in-process). The final in-process approach is simpler and makes all the previous stderr/Promise.all concerns moot. My outstanding inline comment about .toThrow(TypeError) vs .toThrow("toPrimitive") is a theoretical nit — ECMAScript mandates TypeError for this case — and is already noted on the PR.

@claude claude Bot left a comment

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.

LGTM — the C++ fix is minimal and correct, and the in-process regression test covers the exact crash path.

Extended reasoning...

Overview

Two files changed: BunPlugin.cpp adds two RETURN_IF_EXCEPTION(throwScope, {}) guards in setupBunPlugin (lines 304 and 309), and a new 15-line in-process test exercises the exact crash path (Symbol.toPrimitive returning a non-primitive {}).

Security risks

No security-sensitive code touched. The change is purely exception-propagation hygiene in the plugin setup path.

Level of scrutiny

Low — the fix pattern is identical to the existing guards at lines 290, 293, and 300 of the same function. The root cause (pending exception surviving into constructEmptyObjectassertNoException() → SIGABRT) is well-understood and the two guards directly close both exit paths from toStringOrNull and value() that can throw.

Other factors

The CI failure (webview.test.ts timeout on macOS x64) is unrelated to this PR. The in-process test in the final revision resolves all previous subprocess/stderr concerns. The pre-existing BunPluginTarget-ignored bug I noted is orthogonal and tracked for a separate PR. The outstanding nit about .toThrow(TypeError) vs .toThrow("toPrimitive") is minor — the test still exercises the crash path correctly.

@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch from cbbbd50 to aef07b1 Compare March 23, 2026 02:30
@robobun

robobun commented Mar 23, 2026

Copy link
Copy Markdown
Collaborator Author

Verification (robobun): CI Lint JS pass, buildkite build 41268 in progress (not red). Diff is 2 lines in BunPlugin.cpp adding standard RETURN_IF_EXCEPTION guards matching existing pattern at lines 290/293/300. 15-line in-process regression test exercises the exact crash path (Symbol.toPrimitive returning non-primitive - would SIGABRT on main, now throws TypeError). No TODO/FIXME/HACK. All reviews LGTM.

@robobun robobun force-pushed the farm/8f0ba90e/fix-plugin-toprimitive-exception branch from aef07b1 to 6751096 Compare March 25, 2026 16:47

@claude claude Bot left a comment

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.

LGTM — the C++ fix is correct and the final in-process test is clean.

Extended reasoning...

Overview

This PR touches two files: src/bun.js/bindings/BunPlugin.cpp (two RETURN_IF_EXCEPTION guard insertions) and test/js/bun/plugin/plugin-toprimitive-crash.test.ts (a new in-process regression test). The scope is narrow and self-contained.

Security risks

None. The change adds early-return exception guards to existing validation logic; it does not affect authentication, permissions, or data handling.

Level of scrutiny

This is a minimal crash-fix: two guard lines inserted at the exact points where a pending JSC exception could be left active, following the identical RETURN_IF_EXCEPTION(throwScope, {}) pattern already present at lines 290, 293, and 300 of the same function. The fix is mechanical and correct.

Other factors

The long review thread was driven by iterating on a spawned-subprocess version of the test, all of which is no longer relevant — the final committed test is a simple in-process 15-line test that directly exercises the crash path. The .toThrow("toPrimitive") assertion is sufficient to catch any regression (the ECMAScript spec mandates a TypeError when Symbol.toPrimitive returns a non-primitive, so the message substring check is reliable). CI lint passed per robobun verification.

@robobun

robobun commented May 23, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #31286, which applies the same RETURN_IF_EXCEPTION fix on the current source layout (src/jsc/bindings/BunPlugin.cpp) with a regression test in test/js/bun/plugin/plugins.test.ts. Closing to keep a single PR for this fix.

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.

2 participants