Skip to content

Bun.plugin: fix segfault when an object loader result's exports getter throws - #37026

Merged
Jarred-Sumner merged 3 commits into
mainfrom
farm/3b284c22/fix-object-loader-exports-getter-crash
Aug 7, 2026
Merged

Jarred-Sumner merged 3 commits into
mainfrom
farm/3b284c22/fix-object-loader-exports-getter-crash

Conversation

@robobun

@robobun robobun commented Aug 6, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

Bun.plugin({
  name: "virt",
  setup(build) {
    build.module("virt-mod", () => {
      const result = { loader: "object" };
      Object.defineProperty(result, "exports", {
        get() { throw new Error("exports getter threw"); },
      });
      return result;
    });
  },
});
await import("virt-mod");
panic(main thread): Segmentation fault at address 0x0

Deterministic on 1.4.0 and main. The same crash happens through build.onLoad returning a loader: "object" result, and through require() of the virtual module (there the fault address is the getIfPropertyExists call on the null object). UBSan on a debug build reports src/jsc/modules/ObjectModule.cpp:20:17: runtime error: member call on null pointer of type 'JSC::JSCell'.

Cause

handleOnLoadObjectResult in src/jsc/bindings/ModuleLoader.cpp sets result.type = OnLoadResultTypeObject before reading the exports property off the plugin's result object. When that property read throws (a user-defined getter, or a Proxy trap), the exception branch stored the exception in result.value.error but returned with the type still OnLoadResultTypeObject. handleVirtualModuleResult then took the Object case, value.object.getObject() on the stored exception cell returned nullptr, and the null object was dereferenced: immediately via getIfPropertyExists on the require() path, or inside generateObjectModuleSourceCode's source generator on the import() path.

Fix

Set result.type = OnLoadResultTypeError in the exception branch, matching every other error path in the function. The import now rejects (and require() throws) with the getter's error.

Verification

USE_SYSTEM_BUN=1 bun test test/js/bun/plugin/plugins.test.ts -t "throwing exports getter"
  -> 3 fail (child process segfaults)

bun bd test test/js/bun/plugin/plugins.test.ts
  -> 38 pass, 0 fail

New tests cover all three faces: build.module + import(), build.module + require(), and build.onLoad + import(). Each spawns a subprocess and asserts the process exits 0 after printing the getter's error message.


[review] gate passed · iteration 0 · 2 files touched

fails on main (without fix)
ASAN without fix: 3 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/plugin/plugins.test.ts
bun test v1.4.0 (113ff8b97)

test/js/bun/plugin/plugins.test.ts:
If bundling, conditions should include development or production. If not bundling, conditions or NODE_ENV should include development or production. See https://www.npmjs.com/package/esm-env for tips on setting conditions in popular bundlers and runtimes.
(pass) require > SSRs `<h1>Hello world!</h1>` with Svelte [1142.00ms]
(pass) require > beep:boop returns 42 [9.21ms]
(pass) require > object module works [8.22ms]
(pass) module > throws with require() [13.05ms]
(pass) module > async module works with async import [27.21ms]
(pass) module > sync module module works with require() [4.35ms]
(pass) module > sync module module works with require.resolve() [2.91ms]
(pass) module > sync module module works with import [6.30ms]
(pass) module > modules are overridable [88.62ms]
(pass) dynamic import > SSRs `<h1>Hello world!</h1>` with Svelte [5.71ms]
(pass) dynamic import > beep:boop returns 42 [4.12ms]
(pass) dynamic import > async:onLoad returns
... (truncated)

release without fix: 3 FAILED
bun test v1.4.0-canary.1 (b58cd4685)

test/js/bun/plugin/plugins.test.ts:
If bundling, conditions should include development or production. If not bundling, conditions or NODE_ENV should include development or production. See https://www.npmjs.com/package/esm-env for tips on setting conditions in popular bundlers and runtimes.
(pass) require > SSRs `<h1>Hello world!</h1>` with Svelte [30.13ms]
(pass) require > beep:boop returns 42 [0.18ms]
(pass) require > object module works [0.12ms]
(pass) module > throws with require() [0.15ms]
(pass) module > async module works with async import [1.27ms]
(pass) module > sync module module works with require() [0.09ms]
(pass) module > sync module module works with require.resolve() [0.05ms]
(pass) module > sync module module works with import [0.07ms]
(pass) module > modules are overridable [0.19ms]
(pass) dynamic import > SSRs `<h1>Hello world!</h1>` with Svelte [0.08ms]
(pass) dynamic import > beep:boop returns 42 [0.05ms]
(pass) dynamic import > async:onLoad returns 42 [1.37ms]
(pass) dynamic import > async object loader returns 42 [1.29ms]
(pass) import statement > SSRs `<h1>Hello world!</h1>` with Svelte [1.64ms]
(pass) erro
... (truncated)
passes on PR (with fix)
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/js/bun/plugin/plugins.test.ts
bun test v1.4.0 (113ff8b97)

test/js/bun/plugin/plugins.test.ts:
If bundling, conditions should include development or production. If not bundling, conditions or NODE_ENV should include development or production. See https://www.npmjs.com/package/esm-env for tips on setting conditions in popular bundlers and runtimes.
(pass) require > SSRs `<h1>Hello world!</h1>` with Svelte [1124.06ms]
(pass) require > beep:boop returns 42 [9.79ms]
(pass) require > object module works [8.10ms]
(pass) module > throws with require() [13.06ms]
(pass) module > async module works with async import [24.45ms]
(pass) module > sync module module works with require() [5.10ms]
(pass) module > sync module module works with require.resolve() [2.96ms]
(pass) module > sync module module works with import [6.19ms]
(pass) module > modules are overridable [24.83ms]
(pass) dynamic import > SSRs `<h1>Hello world!</h1>` with Svelte [84.32ms]
(pass) dynamic import > beep:boop returns 42 [4.81ms]
(pass) dynamic import > async:onLoad return
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 624ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/22] gen cpp.rs (cppbind)
[2/22] gen JS modules (bundle-modules)
Preprocess modules (8842ms)
Bundle modules (48ms)
Postprocesss modules (32ms)
Bundle Functions (611ms)
Generate Code (17ms)

[9.57s] Bundled "src/js" for production
  2573 kb
  193 internal modules
  13 native modules
  84 internal functions across 17 files
[2/7] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19)

�[1m�[92m   Compiling�[0m bun_bin v0.0.0 (/workspace/bun/src/bun_bin)
�[1m�[92m    Finished�[0m `release` profile [optimized + debuginfo] target(s) in 2m 56s
[3/7] cxx obj/unified/UnifiedSource-src_jsc_bindings-2.cpp.o
[4/7] link bun-profile
[6/7] strip bun
[6/7] bun-profile --revision
1.4.0-canary.1+113ff8b97
[build] done
bun test v1.4.0-canary.1 (113ff8b97)

test/js/bun/plugin/plugins.test.ts:
If bundling, conditions should include development or production. If not bundling, conditions or NODE_ENV should includ
... (truncated)
diff hotspot
src/jsc/bindings/ModuleLoader.cpp  |  1 +
 test/js/bun/plugin/plugins.test.ts | 83 +++++++++++++++++++++++++++++++++++++-
 2 files changed, 83 insertions(+), 1 deletion(-)

gate history · 1 passed · 0 rejected · iteration 0

evidence per changed file
file                                reads  edits  tests
src/jsc/bindings/ModuleLoader.cpp       3      1      0
test/js/bun/plugin/plugins.test.ts      4      3      0

…s getter throws

handleOnLoadObjectResult set result.type to OnLoadResultTypeObject before
reading the exports property. When that read threw, the exception branch
stored the error but left the type as Object, so the caller treated the
exception cell as the exports object, got nullptr from getObject(), and
dereferenced it. Flip the type to OnLoadResultTypeError in that branch so
the import rejects (or require throws) with the getter's error.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Object Loader Errors

Layer / File(s) Summary
Classify exports access errors
src/jsc/bindings/ModuleLoader.cpp
handleOnLoadObjectResult marks exceptions from reading the loader object's exports property as OnLoadResultTypeError.
Verify error propagation
test/js/bun/plugin/plugins.test.ts
Subprocess tests cover throwing exports getters for dynamic import(), require(), and onLoad-provided modules.

Suggested reviewers: jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title clearly identifies the Bun.plugin segmentation fault and the object loader exports getter fix.
Description check ✅ Passed The description explains the reproduction, cause, fix, affected paths, and verification results, despite using different headings than the template.

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

@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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@test/js/bun/plugin/plugins.test.ts`:
- Around line 564-573: Update expectCleanFailure to assert that the captured
stderr is empty after awaiting the child process streams, then retain the
existing stdout assertion and exitCode check in their current order.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 7281d303-4575-4ce9-aca1-5d1efb79231f

📥 Commits

Reviewing files that changed from the base of the PR and between f4f3844 and 1e6c203.

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

Comment thread test/js/bun/plugin/plugins.test.ts
Comment thread test/js/bun/plugin/plugins.test.ts

@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 — one-line fix that tags the exception branch as OnLoadResultTypeError, matching the other error path in the same function.

What was reviewed:

  • Confirmed OnLoadResult.value is a union and the caller's OnLoadResultTypeObject case dereferences value.object.getObject(), so the mislabeled type explains the null deref exactly.
  • Checked the sibling error branch (non-object exports) already sets OnLoadResultTypeError; this change just brings the getter-throw branch to parity.
  • Tests: subprocess-isolated crash repros for build.module+import(), build.module+require(), and build.onLoad+import(); stdout/stderr/exitCode asserted in order, it.concurrent, prior review comments (stderr assertion, duplicate harness import) all resolved on HEAD.
Extended reasoning...

Overview

The PR fixes a deterministic segfault in Bun.plugin when an object-loader result's exports property throws on read (via a user getter or Proxy trap). The native change is a single line in src/jsc/bindings/ModuleLoader.cpp: inside handleOnLoadObjectResult, the exception branch after getIfPropertyExists(..., exportsPublicName()) now sets result.type = OnLoadResultTypeError before storing the exception and returning. Previously the type was left at OnLoadResultTypeObject (set at the top of the function), so handleVirtualModuleResult took the Object case, called .getObject() on the stored exception cell, got nullptr, and dereferenced it. The other error branch in the same function (when exports exists but isn't an object) already sets OnLoadResultTypeError, so this is a straightforward consistency fix.

Three subprocess tests are added to test/js/bun/plugin/plugins.test.ts covering build.module + import(), build.module + require(), and build.onLoad + import(). Each spawns bun -e with bunEnv, catches the getter's error in-process, and the test asserts exact stdout, empty stderr, and exit code 0 (in that order).

Security risks

None. This turns a null-pointer crash into a properly propagated JS error. No new input surface, no privilege-relevant code.

Level of scrutiny

Low-to-medium. The native change is one line in an error path, mechanically mirrors the adjacent error branch, and the root-cause analysis in the PR description traces cleanly to the code. The test additions follow house conventions (it.concurrent, subprocess isolation for a crash repro, pipes drained concurrently, exit code asserted last).

Other factors

All prior review feedback is resolved: the CodeRabbit request for a stderr assertion was applied in 113ff8b, and my earlier note about a duplicate harness import was already handled by the autofix commit 295f468 (the mid-file import now pulls only tempDir). No outstanding threads. The bug-hunting system found no issues this run.

@Jarred-Sumner
Jarred-Sumner merged commit 5a5f1b8 into main Aug 7, 2026
54 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/3b284c22/fix-object-loader-exports-getter-crash branch August 7, 2026 01:24
Jarred-Sumner pushed a commit that referenced this pull request Aug 22, 2026
)

### Problem
- #39804 (commit 025570f) changed `generateObjectModuleSourceCode`
(`src/jsc/modules/ObjectModule.cpp:25`). A throwing getter on the
exports object of a `loader: "object"` result now fails the import.
Before, the loader exported `undefined` for it.
- #39804 added tests for the `mock.module()` entry point only. Its
description lists this change under "no repro", but a plugin reaches it:
bun `1.4.0-canary.1+6e906e468` (before #39804) prints `boom=undefined`
for the cases below.
- This replaces #33793. Its code change landed through #39804. Its
plugin test did not.

### Fix
- Test only. Adds `describe("object loader with a throwing getter on an
export")` to `test/js/bun/plugin/plugins.test.ts`, next to the #37026
block for a throwing getter on the result's `exports` property.
- Three cases: `import()` and `require()` of a `build.module()` result,
and `import()` of a `build.onLoad()` result. Each checks that the caught
error is the object the getter threw. The getter sits between two plain
exports.
- Verified: all three fail with `USE_SYSTEM_BUN=1`
(`1.4.0-canary.1+6e906e468`) and pass with a debug build of main at
40ef811. The full file passes there (45 tests).

### Background
- A `loader: "object"` result becomes a synthetic module.
`ModuleLoader.cpp:442` passes its `exports` object to
`generateObjectModuleSourceCode`, which reads each own enumerable
property once into the namespace. `require()` takes the same path after
the `__esModule` check at `ModuleLoader.cpp:422`.
- `mock.module()` of a module that is not loaded yet uses the same
generator. That is the entry point #39804 tests.
- #37026 covers the layer above: a getter on the `exports` property of
the result object (`ModuleLoader.cpp:155`).

<details><summary>Notes</summary>

- Output on the old binary: `imported boom=undefined` for the two
`import()` cases and `required boom=undefined` for the `require()` case.
- #33793's test ran `import()` and `require()` in one subprocess. This
version mirrors the shape of the #37026 block: one subprocess per entry
point, exact `stdout`, empty `stderr`, exit code 0.
- #33793's `mock.module()` test is not carried over. #39804 added two
tests for that entry point in
`test/js/bun/test/mock/mock-module.test.ts` (the import failure and the
untouched namespace). Both of #33793's test cases pass on a debug build
of main with no `src/` change. That is the basis for closing #33793.
- Commands: `bun bd test test/js/bun/plugin/plugins.test.ts` (45 pass)
and `USE_SYSTEM_BUN=1 bun test test/js/bun/plugin/plugins.test.ts -t
"throwing getter on an export"` (3 fail).
</details>
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