Skip to content

fix(plugin): propagate exception from Bun.plugin target string coercion#31286

Closed
robobun wants to merge 4 commits into
mainfrom
farm/c98a9c70/bun-plugin-target-exception
Closed

fix(plugin): propagate exception from Bun.plugin target string coercion#31286
robobun wants to merge 4 commits into
mainfrom
farm/c98a9c70/bun-plugin-target-exception

Conversation

@robobun

@robobun robobun commented May 23, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

Fixes a crash (debug assertion ExceptionScope::assertNoException) found by fuzzing in Bun.plugin().

When the target option is an object whose toString() throws, setupBunPlugin called targetValue.toStringOrNull(globalObject), got back nullptr, and silently fell through with the exception still pending. It then continued building the builder object and invoked the user's setup() function while an exception was pending, which trips ExceptionScope::assertNoException() in debug builds and leaves the VM in an inconsistent exception state in release builds.

Bun.plugin({
  setup() {},
  target: { toString() { throw new Error("boom"); } },
});

Now the exception from the string coercion of target is checked and propagated to the caller as a normal JS error.

How did you verify your code works?

  • Reproducer above aborts with ASSERTION FAILED: Unexpected exception observed before the fix and throws a catchable Error: boom after the fix (debug/ASAN build).
  • Added a regression test in test/js/bun/plugin/plugins.test.ts ("handles a 'target' whose toString throws"); bun bd test test/js/bun/plugin/plugins.test.ts passes (30 pass, 1 todo, 0 fail).

When the `target` option passed to Bun.plugin() is an object whose
toString() throws, the pending exception was ignored and execution
continued into the setup() call, tripping ExceptionScope::assertNoException
in debug builds. Check for the exception after coercing `target` to a
string and propagate it.
@coderabbitai

coderabbitai Bot commented May 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: adb3d4ee-6e31-4e04-8ec0-e400ccdb28bc

📥 Commits

Reviewing files that changed from the base of the PR and between f161e03 and 6aa3152.

📒 Files selected for processing (2)
  • src/jsc/bindings/BunPlugin.cpp
  • test/js/bun/plugin/plugins.test.ts

Walkthrough

The PR refactors how the Bun plugin validates the target option to improve error handling. The C++ binding replaces a conditional toStringOrNull check with explicit toString() conversion and exception handling, ensuring errors during conversion are propagated. Test coverage is added to verify this behavior: when target.toString() throws, the error is rethrown and the setup function is not invoked.

Changes

Plugin target option error handling

Layer / File(s) Summary
Target option error handling implementation and test
src/jsc/bindings/BunPlugin.cpp, test/js/bun/plugin/plugins.test.ts
The target option validation refactors from toStringOrNull with conditional validation to explicit toString() with exception checks, ensuring conversion errors are propagated. A new test case verifies that when target.toString() throws, the error is rethrown and the setup function is not invoked.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: fixing exception propagation from Bun.plugin target string coercion, which matches the core fix in both the C++ implementation and test additions.
Description check ✅ Passed The description is comprehensive and well-structured, covering both required sections: 'What does this PR do?' provides detailed context about the crash fix with code example, and 'How did you verify your code works?' includes reproducer verification and test details.
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.

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Infer (1.2.0)
src/jsc/bindings/BunPlugin.cpp

In file included from src/jsc/bindings/BunPlugin.cpp:1:
In file included from src/jsc/bindings/BunPlugin.h:3:
src/jsc/bindings/root.h:42:10: fatal error: 'cmakeconfig.h' file not found
42 | #include "cmakeconfig.h"
| ^~~~~~~~~~~~~~~
1 error generated.
Aborting translation of method 'Zig::jsFunctionAppendOnLoadPluginBody' in file 'src/jsc/bindings/BunPlugin.cpp': "Assert_failure src/clang/cAst_utils.ml:249:53"
Uncaught Internal Error: "Assert_failure src/clang/cAst_utils.ml:249:53"
Error backtrace:
Raised at ClangFrontend__CAst_utils.get_decl_from_typ_ptr in file "src/clang/cAst_utils.ml", line 249, characters 53-65
Called from ClangFrontend__CTrans.CTrans_funct.get_destructor_decl_ref in file "src/clang/cTrans.ml", line 658, characters 12-59
Called from ClangFrontend__CTrans.CTrans_funct.destructor_calls.(fun) in file "src/clang/cTrans.ml", line 2048, characters 12-69
Called from Base__List.rev_filter_map.loop in file "src/list.ml", line 944, characters 13-17
Called f

... [truncated 2200 characters] ...

"src/clang/cFrontend_decl.ml" (inlined), line 54, characters 4-52
Called from ClangFrontend__CFrontend_decl.CFrontend_decl_funct.function_decl in file "src/clang/cFrontend_decl.ml", line 90, characters 12-151
Called from ClangFrontend__CFrontend_decl.CFrontend_decl_funct.translate_one_declaration in file "src/clang/cFrontend_decl.ml", line 453, characters 10-56
Called from ClangFrontend__CFrontend_decl.CFrontend_decl_funct.translate_one_declaration.translate in file "src/clang/cFrontend_decl.ml" (inlined), line 448, characters 24-96
Called from Stdlib__List.iter in file "list.ml", line 110, characters 12-15
Called from Stdlib__List.iter in file "list.ml", line 110, characters 12-15
Called from Stdlib__List.iter in file "list.ml" (inlined), line 110, characters 17-25
Called from Base__List


Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun

robobun commented May 23, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:05 AM PT - Jun 7th, 2026

@robobun, your commit 6aa3152 has 3 failures in Build #61272 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31286

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

bun-31286 --bun

@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 — straightforward addition of RETURN_IF_EXCEPTION checks after toStringOrNull/value, with a regression test.

Extended reasoning...

Overview

This PR adds two RETURN_IF_EXCEPTION(throwScope, {}) checks in setupBunPlugin (src/jsc/bindings/BunPlugin.cpp) after targetValue.toStringOrNull(globalObject) and targetJSString->value(globalObject), so that a JS exception thrown during string coercion of the target option is propagated instead of leaving the VM with a pending exception while continuing to call setup(). A regression test is added in test/js/bun/plugin/plugins.test.ts alongside the existing "handles invalid 'target'" test.

Security risks

None. This is purely defensive exception propagation in argument validation for Bun.plugin(). It does not touch auth, crypto, filesystem, or networking, and does not change any success-path behavior.

Level of scrutiny

Low. The change is a 4-line mechanical application of the standard JSC RETURN_IF_EXCEPTION idiom, identical to the pattern already used on the surrounding lines (e.g., after getIfPropertyExists). The refactor from if (auto* x = ...) to a hoisted assignment + check + if (x) is semantically equivalent when no exception is thrown, and strictly more correct when one is. The added check after ->value(globalObject) is also correct since rope resolution can throw.

Other factors

  • The bug-hunting system found no issues.
  • A targeted regression test is included and mirrors the existing neighboring test's structure.
  • No CODEOWNERS entry covers this path.
  • No outstanding human review comments on the PR.

@github-actions

Copy link
Copy Markdown
Contributor

This PR may be a duplicate of:

  1. Handle exceptions from toStringOrNull in Bun.plugin target parsing #28369 - Also adds exception handling for target string coercion in setupBunPlugin (BunPlugin.cpp)
  2. Fix unchecked exception when Bun.plugin target coercion throws #31017 - Also fixes unchecked exception when Bun.plugin target toString() throws

🤖 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 — straightforward RETURN_IF_EXCEPTION fix with a regression test.

Extended reasoning...

Overview

This PR adds two RETURN_IF_EXCEPTION(throwScope, {}) checks in setupBunPlugin (src/jsc/bindings/BunPlugin.cpp) after targetValue.toStringOrNull(globalObject) and targetJSString->value(globalObject), so an exception thrown from a user-supplied target.toString() is propagated instead of leaving a pending exception on the VM. A regression test is added in test/js/bun/plugin/plugins.test.ts that mirrors the existing "handles invalid 'target'" test.

Security risks

None. This is purely defensive exception handling on an edge-case input shape; it does not change auth, permissions, parsing of untrusted data, or any security-sensitive surface.

Level of scrutiny

Low. The change is 4 lines of C++ that hoist a call out of an if condition and insert the standard JSC RETURN_IF_EXCEPTION macro — the exact same pattern already used a few lines above for getIfPropertyExists and throughout the rest of this file (e.g., after toWTFString in jsFunctionAppendOnLoadPluginBody). There are no new control-flow branches beyond the early return, no behavior change for valid inputs, and the test follows the adjacent test's structure.

Other factors

The bug-hunting system found no issues. The duplicate-PR bot flagged two other PRs targeting the same fix, which is a maintainer triage concern rather than a correctness concern for this diff. CI shows failures on an earlier commit (a9c2360) with a retrigger commit on top; given the change is additive exception checks plus a self-contained test, any CI failures are very likely unrelated flakes, but maintainers should confirm green CI before merge as usual.

@robobun

robobun commented May 23, 2026

Copy link
Copy Markdown
Collaborator Author

CI status

  • Build #57344 (commit a9c2360): every test job that ran passed (74 checks green). Only red check: darwin-14-aarch64-test-bun expired waiting for an agent and never executed.
  • Build #57355 (retrigger, 778670d): 2 failed jobs, both unrelated — ubuntu 25.04 x64: test/js/bun/util/v8-heap-snapshot.test.ts SIGKILLed (OOM, no core found); windows 11 aarch64: test/cli/install/bun-install-security-provider.test.ts "Large payload via ipc pipe" hit its 120s timeout. All other files in both shards: 0 failures.
  • Build #57410 (commit 993e93a, applying the review suggestion): 284 jobs passed; the single failed job is darwin 14 aarch64 - test-bun, where the only failing file is test/cli/install/migration/complex-workspace.test.ts — the bun install subprocess in its setup exited non-zero ("Failed to install"), cascading into the dependent assertions. In that same job, test/js/bun/plugin/plugins.test.ts (the file this PR touches) passed: 30 pass, 0 fail.

Every failure across the three builds is infrastructure/flake in files untouched by this PR; the plugin test suite passes on every platform, including with the toString() change from review. Not retriggering further — ready for maintainer review/merge.


Update for build #61272 (retriggered on 6aa3152): every test-bun job fails on the same single test, test/cli/install/bunx.test.ts > should handle package that requires node 24. Root cause is external: the test runs bunx @angular/cli@latest, and @angular/cli@22.0.0 (published 2026-06-03) requires node ^22.22.3 || ^24.15.0 || >=26.0.0, while Bun self-reports Node 24.3.0, which fails ^24.15.0, so the CLI exits 3. This will affect any branch (including main) on agents without a warm bunx cache until the reported Node version is bumped or the test is pinned. The other two failures are a 0-byte Intel SDE download in verify-baseline and binary-size comparing this (now 3-week-old) base against today's canary. None are related to this PR's change.

Comment thread src/jsc/bindings/BunPlugin.cpp Outdated
RETURN_IF_EXCEPTION(throwScope, {});
if (targetValue) {
if (auto* targetJSString = targetValue.toStringOrNull(globalObject)) {
auto* targetJSString = targetValue.toStringOrNull(globalObject);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
auto* targetJSString = targetValue.toStringOrNull(globalObject);
auto* targetJSString = targetValue.toString(globalObject);

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.

Applied in 993e93a — switched to toString() and also dropped the null check since toString() returns the empty string (never null) on exception; the RETURN_IF_EXCEPTION right after it handles the error path. Repro and test/js/bun/plugin/plugins.test.ts still pass with the debug build.

toString() returns the empty string instead of null on exception, so the
null check is unnecessary; keep the exception checks.

@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 — straightforward exception-propagation fix with a regression test, and Jarred's review suggestion has been applied.

Extended reasoning...

Overview

This PR fixes a missing exception check in setupBunPlugin (src/jsc/bindings/BunPlugin.cpp): when coercing the target option to a string, the old code used toStringOrNull and silently fell through on a pending exception. The fix replaces it with toString() followed by RETURN_IF_EXCEPTION (and another after ->value()), which is the same pattern already used elsewhere in this file (e.g. in JSMock__jsModuleMock). A regression test is added to test/js/bun/plugin/plugins.test.ts.

Security risks

None. This is purely error-path handling for a user-supplied option object; no auth, crypto, filesystem, or network surface is touched.

Level of scrutiny

Low. The diff is ~7 lines of logic that mechanically applies the canonical JSC exception-check idiom, plus a 15-line test. The only behavioral change is that an exception thrown from target[Symbol.toPrimitive]/toString now surfaces to the caller instead of tripping a debug assertion / leaving a pending exception — strictly a correctness improvement on an edge case found by fuzzing.

Other factors

  • A maintainer (Jarred-Sumner) already reviewed and left a concrete suggestion (use toString() instead of toStringOrNull()), which was applied in 993e93a and acknowledged.
  • CI: the plugin test suite passed on every platform where it ran; the only failures were unrelated infra flakes (OOM SIGKILL, agent expiration, IPC timeout).
  • No CODEOWNERS entry covers these files.
  • The bug-hunting system found no issues.

@robobun

robobun commented Jun 7, 2026

Copy link
Copy Markdown
Collaborator Author

Strengthened the regression test in 6aa3152: it now also asserts setup is never invoked when the target coercion throws. That is the observable bug on release builds (the pending exception used to propagate only after setup() had already run), so the test now fails on an unfixed release build too, not just via the debug assertion. Verified it fails with USE_SYSTEM_BUN=1 (setup called once) and passes with the debug build (30 pass, 1 todo, 0 fail).

@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 — straightforward exception-propagation fix following the standard JSC pattern, with the maintainer's suggestion already applied and a regression test added.

Extended reasoning...

Overview

This PR fixes a missed exception check in setupBunPlugin (src/jsc/bindings/BunPlugin.cpp). Previously, targetValue.toStringOrNull() could throw and return nullptr, after which the code silently fell through the if and proceeded to call the user's setup() with a pending exception — tripping a debug assertion and leaving the VM in an inconsistent state in release builds. The fix replaces it with toString() followed by RETURN_IF_EXCEPTION, plus a second RETURN_IF_EXCEPTION after ->value(). A regression test is added in test/js/bun/plugin/plugins.test.ts asserting both that the user's error propagates and that setup is never invoked.

Security risks

None. This is pure error-path handling for an obscure input shape (an object with a throwing toString passed as target). No new data flows, no auth/crypto/permissions.

Level of scrutiny

Low. The diff is ~7 lines of mechanical change that exactly matches the idiom used throughout this file and the rest of the JSC bindings (toStringRETURN_IF_EXCEPTIONvalueRETURN_IF_EXCEPTION). The only semantic change is that a previously-swallowed pending exception now returns early — which is strictly more correct. The validation logic itself (node/bun/browser check) is unchanged.

Other factors

Jarred-Sumner already reviewed and left a single suggestion (use toString() instead of toStringOrNull()), which was applied in 993e93a and acknowledged. CI is green across platforms for the touched test file; the only failures across three builds were unrelated infra flakes. The bug-hunting system found no issues. The latest commit (6aa3152) only strengthens the test to also fail on an unfixed release build — a test-only improvement.

@robobun

robobun commented Jun 11, 2026

Copy link
Copy Markdown
Collaborator Author

Closing in favor of #32106, which is the same fix rebased on current main. This branch's base is from May and its last build (#61272) failed across every platform, so it would need a full rebase anyway. #32106 carries over this PR's test coverage: it asserts both that the coercion error propagates and that setup() is never invoked, for a throwing toString and for a Symbol.toPrimitive that returns an object (the shape the latest fuzzer report hit).

@robobun robobun closed this Jun 11, 2026
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