Skip to content

Bun.plugin.clearAll(): clear namespaces and groups together - #32261

Merged
Jarred-Sumner merged 5 commits into
mainfrom
farm/1a485378/fix-plugin-clearall
Aug 17, 2026
Merged

Jarred-Sumner merged 5 commits into
mainfrom
farm/1a485378/fix-plugin-clearall

Conversation

@robobun

@robobun robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator

Repro

function register() {
  Bun.plugin({
    name: "p",
    setup(b) {
      b.onResolve({ filter: /.*/, namespace: "myns" }, ({ path }) => ({ path, namespace: "myns" }));
      b.onLoad({ filter: /.*/, namespace: "myns" }, () => ({ contents: "export default 1;", loader: "js" }));
    },
  });
}
register();
Bun.plugin.clearAll();
register();
await import("myns:hello"); // SIGABRT

Cause

jsFunctionBunPluginClear cleared onLoadPlugins.groups but not onLoadPlugins.namespaces, and onResolvePlugins.namespaces but not onResolvePlugins.groups. Base::namespaces and Base::groups are parallel-indexed: Base::group() looks up namespaces[i] and returns &groups[i].

After clearAll():

  • onLoad: namespaces.size() == N, groups.size() == 0. Re-registering the same namespace hits the group() lookup branch in Base::append, which returns &groups[i] past the end of an empty vector and trips the WTF::Vector bounds assert (SIGABRT, exit 134).
  • onResolve: namespaces.size() == 0, groups.size() == N. Re-registering appends a fresh entry to both, but groups[0] is still the stale pre-clearAll() group, so the next resolve for that namespace runs the old callback.

Fix

Add Base::clear() that resets fileNamespace, namespaces, and groups together, and call it for both onLoadPlugins and onResolvePlugins.

Verification

Two new subprocess tests in test/js/bun/plugin/plugins.test.ts cover both paths. Without the fix the onLoad test aborts with exit 134 and the onResolve test fails with stale onResolve callback ran; with the fix both pass. Full plugins.test.ts suite: 33 pass, 1 pre-existing todo.


[review] gate passed · iteration 10 · 3 files touched

fails on main (without fix)
ASAN without fix: 4 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 (8326d1bd3)

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 [1254.41ms]
(pass) require > beep:boop returns 42 [10.45ms]
(pass) require > object module works [8.11ms]
(pass) module > throws with require() [12.78ms]
(pass) module > async module works with async import [26.54ms]
(pass) module > sync module module works with require() [4.48ms]
(pass) module > sync module module works with require.resolve() [3.08ms]
(pass) module > sync module module works with import [4.88ms]
(pass) module > modules are overridable [90.66ms]
(pass) dynamic import > SSRs `<h1>Hello world!</h1>` with Svelte [6.21ms]
(pass) dynamic import > beep:boop returns 42 [4.69ms]
(pass) dynamic import > async:onLoad return
... (truncated)

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

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 [21.71ms]
(pass) require > beep:boop returns 42 [0.18ms]
(pass) require > object module works [0.10ms]
(pass) module > throws with require() [0.19ms]
(pass) module > async module works with async import [1.29ms]
(pass) module > sync module module works with require() [0.07ms]
(pass) module > sync module module works with require.resolve() [0.06ms]
(pass) module > sync module module works with import [0.08ms]
(pass) module > modules are overridable [0.22ms]
(pass) dynamic import > SSRs `<h1>Hello world!</h1>` with Svelte [0.12ms]
(pass) dynamic import > beep:boop returns 42 [0.06ms]
(pass) dynamic import > async:onLoad returns 42 [1.37ms]
(pass) dynamic import > async object loader returns 42 [1.25ms]
(pass) import statement > SSRs `<h1>Hello world!</h1>` with Svelte [1.82ms]
(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 (8326d1bd3)

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 [1252.23ms]
(pass) require > beep:boop returns 42 [10.17ms]
(pass) require > object module works [8.00ms]
(pass) module > throws with require() [12.88ms]
(pass) module > async module works with async import [26.29ms]
(pass) module > sync module module works with require() [4.47ms]
(pass) module > sync module module works with require.resolve() [3.15ms]
(pass) module > sync module module works with import [5.02ms]
(pass) module > modules are overridable [86.03ms]
(pass) dynamic import > SSRs `<h1>Hello world!</h1>` with Svelte [6.29ms]
(pass) dynamic import > beep:boop returns 42 [3.85ms]
(pass) dynamic import > async:onLoad return
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 618ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/124] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 241 extern-C blocks audited
[2/124] gen cpp.rs (cppbind)
[2/124] 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_runtime v0.0.0 (/workspace/bun/src/runtime)
�[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 4m 19s
[120/124] cxx obj/codegen/GeneratedFakeTimersConfig.cpp.o
[121/124] link bun-profile
[123/124] strip bun
[123/124] bun-profile --revision
1.4.0-canary.1+ce16ca657
[build] done
bun test v1.4.0-canary.1 (ce16ca657)

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-e
... (truncated)
diff hotspot
src/jsc/bindings/BunPlugin.cpp     |   7 +-
 src/jsc/bindings/BunPlugin.h       |   7 ++
 test/js/bun/plugin/plugins.test.ts | 133 +++++++++++++++++++++++++++++++++++++
 3 files changed, 143 insertions(+), 4 deletions(-)

gate history · 1 passed · 0 rejected · iteration 10

evidence per changed file
file                                reads  edits  tests
src/jsc/bindings/BunPlugin.cpp          4      2     11
src/jsc/bindings/BunPlugin.h            1      1     11
test/js/bun/plugin/plugins.test.ts      3      8     11

root cause · written by the author bot

Bun.plugin.clearAll() cleared only one of the two parallel-indexed vectors in each plugin registry, emptying onLoadPlugins.groups while leaving namespaces populated and doing the reverse for onResolvePlugins, so a later registration in a previously used namespace made Base::group() index past the end of groups and append Strong handles into unowned memory. The fix adds a single BunPlugin::Base::clear() that resets namespaces, groups, and the remaining per-registry state together and calls it for both the onLoad and onResolve registries, with the host function also removing v…

@robobun

robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 7:08 PM PT - Aug 16th, 2026

@robobun, your commit ce16ca6 is building: #99734

@coderabbitai

coderabbitai Bot commented Jun 15, 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: 35717feb-3614-464c-afdb-8d40b082efba

📥 Commits

Reviewing files that changed from the base of the PR and between c3995e4 and ce16ca6.

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

Included review availability: Your plan includes up to 5 reviews per rolling hour; 0 remain after this review.


Walkthrough

Adds centralized plugin registry clearing. Bun.plugin.clearAll() now resets plugin handlers, virtual modules, and relative lookup state. New subprocess tests cover repeated registration, stale callbacks, namespace isolation, and virtual-module lookup.

Changes

Plugin clear state reset and validation

Layer / File(s) Summary
Plugin registry clear implementation
src/jsc/bindings/BunPlugin.h, src/jsc/bindings/BunPlugin.cpp
Adds BunPlugin::Base::clear() and uses it for both plugin registries. The host function also removes virtual modules and resets mustDoExpensiveRelativeLookup.
clearAll() behavior tests
test/js/bun/plugin/plugins.test.ts
Adds subprocess tests for repeated clearing, stale callback removal, namespace isolation, and virtual-module lookup reset.

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 main fix: clearing plugin namespaces and groups together in Bun.plugin.clearAll().
Description check ✅ Passed The description explains the bug, root cause, fix, and verification results, although it uses different headings from the repository template.

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

@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 — minimal, obviously-correct fix for the parallel-vector desync in clearAll(), with regression tests for both failure modes.

Extended reasoning...

Overview

This PR fixes Bun.plugin.clearAll(), which previously cleared onLoadPlugins.groups without onLoadPlugins.namespaces and onResolvePlugins.namespaces without onResolvePlugins.groups. Since Base::namespaces and Base::groups are parallel-indexed (used together in Base::group() / Base::append()), this desync caused a WTF::Vector OOB assert (SIGABRT) on onLoad re-registration and stale-callback execution on onResolve re-registration. The fix adds a Base::clear() that resets fileNamespace, namespaces, and groups atomically, and calls it for both plugin sets. Net code change: +7 lines in the header, -4/+2 in the .cpp. Two subprocess regression tests cover both paths.

Security risks

None. This is internal bookkeeping for the runtime plugin registry — clearing three Vectors instead of two. No parsing, no auth, no untrusted input handling, no new surface area.

Level of scrutiny

Low. The root-cause analysis in the PR description matches the code exactly: Base::group() indexes groups[i] by the position found in namespaces, so clearing one without the other is plainly wrong. The fix is the obvious one-liner refactor (clear all three together) and is strictly more correct than what it replaces. Tests are well-isolated in subprocesses to avoid disturbing the rest of the file's plugin state.

Other factors

The one inline finding is an explicitly pre-existing, perf-only nit (mustDoExpensiveRelativeLookup not reset) that this PR neither introduces nor worsens — fine to address separately. No CODEOWNERS coverage for these paths, no outstanding reviewer comments, and the change is self-contained with no callers of the new Base::clear() other than jsFunctionBunPluginClear.

Comment thread src/jsc/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 — straightforward fix keeping the parallel namespaces/groups vectors in sync on clearAll(), and the follow-up reset of mustDoExpensiveRelativeLookup looks good.

Extended reasoning...

Overview

Adds BunPlugin::Base::clear() (clears fileNamespace, namespaces, groups together) and rewrites jsFunctionBunPluginClear to call it for both onLoadPlugins and onResolvePlugins, plus resets mustDoExpensiveRelativeLookup. Two subprocess regression tests added to plugins.test.ts.

Security risks

None. This is internal plugin-registry bookkeeping; no auth, crypto, parsing of untrusted input, or permission surface touched.

Level of scrutiny

Low. The bug and fix are mechanical: namespaces and groups are parallel-indexed (see Base::group() / Base::append()), and the previous clearAll() cleared them asymmetrically, causing an OOB index on re-register (onLoad) or stale callbacks (onResolve). The new code strictly clears a superset of what the old code cleared, so it cannot regress existing behavior — it only restores the invariant that the two vectors stay the same size.

Other factors

  • The follow-up commit addressed my earlier nit (resetting mustDoExpensiveRelativeLookup), and that thread is resolved.
  • Tests are run in subprocesses so they don't wipe the file-level plugins the rest of the suite depends on; the assertions check exit code 0 and exact stdout, which would catch both the SIGABRT and the stale-callback failure modes described in the PR.
  • No CODEOWNERS entry covers these paths.
  • Net C++ change is ~10 lines with no new control flow or allocation patterns.

@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 parallel-array desync fix is correct and the nit was addressed; CI failures are unrelated infra (musl LTO / asan flakes).

Extended reasoning...

Overview

This PR fixes Bun.plugin.clearAll() so that the parallel-indexed namespaces and groups vectors on BunPlugin::Base are cleared together. The old code cleared onLoadPlugins.groups without onLoadPlugins.namespaces (→ OOB index / SIGABRT on re-register) and onResolvePlugins.namespaces without onResolvePlugins.groups (→ stale callbacks). The fix adds a small Base::clear() helper and calls it for both plugin maps, plus resets mustDoExpensiveRelativeLookup (my prior nit, now addressed in dc82119). Two subprocess regression tests cover both failure modes.

Security risks

None. This is internal plugin-registry bookkeeping — clearing WTF::Vectors of Strong<> handles. No new inputs are parsed, no auth/crypto/permissions touched, and the change strictly makes clearAll() more thorough than before.

Level of scrutiny

Low-to-moderate. The C++ change is ~10 lines and mechanically obvious once you see that Base::group() indexes groups[i] by the position found in namespaces — clearing them together is the only correct option. clear() is non-virtual, but call sites use the concrete OnLoad/OnResolve members directly so no dispatch surprise. The test additions follow the existing bunExe()/bunEnv subprocess pattern in the same file and are appropriately isolated (so clearAll() doesn't nuke the file's other plugin fixtures).

Other factors

  • My prior inline nit (reset mustDoExpensiveRelativeLookup) was applied and the thread is resolved.
  • Bug hunter found no issues.
  • The Buildkite failures reported by robobun are unrelated to this diff: three are musl LTO linker errors ("Linking two modules of different data layouts") and the rest are pre-existing asan flakes in test/cli/install/GHSA-pfwx-36v6-832x.test.ts / package.json — none touch the plugin code or test file changed here.

@robobun
robobun force-pushed the farm/1a485378/fix-plugin-clearall branch from a597b87 to b7332d1 Compare June 15, 2026 07:12

@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 — small, well-tested fix for the parallel-array desync; the inline UAF note is a contrived self-inflicted edge case and non-blocking.

Extended reasoning...

Overview

This PR fixes Bun.plugin.clearAll(), which previously cleared onLoadPlugins.groups but not .namespaces, and onResolvePlugins.namespaces but not .groups. Since namespaces and groups are parallel-indexed in BunPlugin::Base, this desync caused a SIGABRT (out-of-bounds WTF::Vector access) on re-registration of an onLoad namespace, and stale-callback execution for onResolve. The fix adds a 6-line Base::clear() that resets all three fields together and calls it for both plugin maps. A follow-up commit (dc82119) addressed my earlier nit by also resetting mustDoExpensiveRelativeLookup. Two new subprocess tests in plugins.test.ts exercise both failure paths.

Security risks

None. This is plugin-registry bookkeeping inside a single process; no auth/crypto/permissions/external-input parsing is touched. The UAF noted inline requires the user's own plugin code to call clearAll() from inside its own namespaced onResolve callback — self-inflicted, same-process, and a variant of a pre-existing groups.append() realloc hazard rather than a new attack surface.

Level of scrutiny

Low-to-moderate. The C++ change is ~10 net lines and the correctness argument is trivial: two parallel Vectors must be cleared together, and the new Base::clear() does exactly that plus the existing fileNamespace.clear(). The previous code was plainly wrong (asymmetric field clearing); the new code is the obvious fix. None of the touched files fall under CODEOWNERS.

Other factors

  • My prior review nit (mustDoExpensiveRelativeLookup) was addressed and resolved.
  • Tests are subprocess-isolated so they don't disturb the rest of plugins.test.ts, and the PR description reports the full suite still passes (33 pass / 1 pre-existing todo).
  • The one new finding from this run is explicitly a non-blocking nit covering a contrived reentrancy case; it doesn't affect the normal register → clearAll → re-register flow this PR targets.

Given the small scope, clear root cause, targeted tests, and resolved prior feedback, this is safe to approve without further human review.

Comment thread src/jsc/bindings/BunPlugin.h
@robobun

robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

(superseded — see the verification comment below for current state on cf2499f5.)

@robobun
robobun force-pushed the farm/1a485378/fix-plugin-clearall branch from b7332d1 to cf2499f Compare July 6, 2026 02:48
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main (was 301 commits behind) and added two tests.

An independent fuzz report hit the same root cause from a different angle, and it's worth recording because the symptom is silent rather than a crash. The existing tests re-register the same namespace after clearAll(). When a different namespace is registered instead, it takes the cleared namespace's index:

Bun.plugin({ name: "old", setup: b => b.onResolve({ filter: /.*/, namespace: "aa" }, oldResolve) });
Bun.plugin.clearAll();
Bun.plugin({ name: "new", setup: b => b.onResolve({ filter: /.*/, namespace: "bb" }, newResolve) });
await import("bb:hello");
// oldResolve runs for the "bb" namespace; newResolve never runs.

onResolvePlugins.namespaces was cleared but groups was not, so re-registering appends "bb" at index 0 of namespaces while groups[0] is still the cleared plugin's group. Base::group("bb") returns it, and the cleared callback's returned namespace then feeds the new plugin's onLoad. No crash, no error, just the wrong callbacks.

Added:

  • a fresh namespace does not inherit a cleared plugin's callbacks — asserts call counts, so the cleared resolver running is caught even when the import still happens to succeed.
  • resets the virtual module lookup mode — covers the mustDoExpensiveRelativeLookup reset, which had no test. Without it, mock.module() with a file: specifier followed by clearAll() aborts on the next resolve (ASSERTION FAILED: !globalObject->onLoadPlugins.mustDoExpensiveRelativeLookup, ZigGlobalObject.cpp:3392, exit 134).

All four Bun.plugin.clearAll() tests fail on main's src/ and pass with this diff. Full test/js/bun/plugin/ + test/js/bun/test/mock/: 56 pass, 2 pre-existing todo, 0 fail.

The previous build's only non-flaky failure was test/js/bun/io/fetch/fetch-abort-slow-connect.test.ts on darwin-26-aarch64 (expect(e.name).toBe("TimeoutError") got "Error"), which doesn't touch the plugin registry.

@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 — both earlier review threads addressed; the parallel-vector invariant fix is minimal and well-tested.

Extended reasoning...

Overview

The PR fixes Bun.plugin.clearAll() (jsFunctionBunPluginClear), which previously cleared mismatched halves of the parallel-indexed namespaces/groups vectors on onLoadPlugins and onResolvePlugins, leaving them out of sync and causing a SIGABRT (WTF::Vector bounds assert) or stale-callback execution on re-registration. The fix adds BunPlugin::Base::clear() that resets fileNamespace, namespaces, and groups together, calls it for both plugin maps, and additionally resets mustDoExpensiveRelativeLookup alongside the virtualModules delete. Four subprocess tests are added to test/js/bun/plugin/plugins.test.ts.

Security risks

None. This is internal bookkeeping for runtime plugin registration; no untrusted input parsing, no auth/crypto/permissions surface. The change strictly restores an invariant (namespaces.size() == groups.size()) that the surrounding code already assumes.

Level of scrutiny

Low-to-medium. It touches C++ JSC bindings, but the actual change is a 7-line helper plus a 5-line call-site rewrite that is trivially verifiable against the data structure's own group()/append() methods — those index groups[i] by the position of namespaces[i], so clearing both together is the only correct behavior. No allocation, exception-scope, or GC-rooting changes.

Other factors

Both of my prior inline threads are resolved: the mustDoExpensiveRelativeLookup reset was applied in dc5bef6, and the reentrancy concern I raised about OnResolve::run() is moot on current main — that function already snapshots matched callbacks into a local MarkedArgumentBuffer before invoking any user JS, so groups.clear() during a callback cannot dangle the iteration. The new tests follow harness conventions (bunExe/bunEnv, tempDir, concurrent subprocess spawn with piped stdout/stderr/exit asserted together) and directly exercise both failure modes described in the PR body plus the cross-namespace mis-routing and virtual-module lookup-mode reset. No open reviewer comments remain and the bug-hunting pass found nothing.

@robobun

robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: rebased onto latest main, ready to merge

Now at 319877f3 (base bdc097dc). The rebase changed no content — the diff is still the same three files:

 src/jsc/bindings/BunPlugin.cpp     |   7 +-
 src/jsc/bindings/BunPlugin.h       |   7 ++
 test/js/bun/plugin/plugins.test.ts | 133 +++++++++++++++

Verification

Debug + ASAN, test/js/bun/plugin/plugins.test.ts:

  • src/ reverted to the merge-base — all 4 clearAll() tests fail. The virtual-module one trips a real debug assertion, so the mustDoExpensiveRelativeLookup reset is load-bearing, not just a perf nicety:
    ASSERTION FAILED: !globalObject->onLoadPlugins.mustDoExpensiveRelativeLookup
    src/jsc/bindings/ZigGlobalObject.cpp : Zig::GlobalObject::moduleLoaderResolve
    
  • with the fix — 36 pass, 0 fail, 1 pre-existing todo.

Re-confirmed against the new base that both preconditions still hold: the assertion is present, and Base's parallel namespaces/groups vectors with group() returning &groups[i] are unchanged.

CI

Previous runs went red only on infrastructure, never on this diff:

  • darwin aarch64 - test-bun: buildkite-agent artifact download timed out after 120s — the shard never ran a test.
  • hot.test.ts, bun-install-registry.test.ts (Windows aarch64): the "flaky" annotation bucket, auto-retried.

plugins.test.ts passes on every lane that ran it, and binary size delta is 0 across all targets. A fresh run is in flight on the rebased head.

@robobun
robobun force-pushed the farm/1a485378/fix-plugin-clearall branch from cf2499f to 319877f Compare July 6, 2026 06:32
@robobun

robobun commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto main to clear the conflict with #33409.

The collision was confined to plugins.test.ts: both branches appended tests after recursion throws stack overflow at entry point and both added tempDir to the same harness import. src/ didn't overlap (#33409 touches VirtualMachine.rs, this touches BunPlugin.cpp/.h). Both test sets are kept.

Re-verified on the rebased tree: the four Bun.plugin.clearAll() tests fail against main's src/ and pass with this diff, while #33409's two onResolve tests pass either way, as expected since they don't depend on this fix. Full plugins.test.ts: 38 pass, 1 pre-existing 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 — both prior nits are addressed and the added tests cover the cross-namespace and lookup-mode-reset cases.

Extended reasoning...

Overview

Fixes Bun.plugin.clearAll() so the parallel-indexed namespaces and groups vectors on BunPlugin::Base are cleared together via a new Base::clear(), and resets OnLoad::mustDoExpensiveRelativeLookup alongside the virtualModules delete. Native change is ~10 lines across BunPlugin.{h,cpp}; the rest is four subprocess tests in test/js/bun/plugin/plugins.test.ts.

Security risks

None. Bun.plugin is user-controlled runtime configuration, not a trust boundary; the change only affects how registered callbacks are torn down. No parsing of untrusted input, no auth/crypto/permissions.

Level of scrutiny

Low-to-medium. The bug and fix are mechanical: the old code cleared two fields from each of onLoadPlugins/onResolvePlugins asymmetrically, leaving namespaces and groups desynchronized (SIGABRT on one side, stale callbacks on the other). The fix clears all three fields on both sides through a single helper, which is the obviously-correct shape. No CODEOWNERS on this path.

Other factors

I reviewed this twice previously. The mustDoExpensiveRelativeLookup nit was addressed in dc82119, and the reentrant-clearAll() iterator-invalidation concern I flagged is moot after the rebase — OnResolve::run() on current main (via #33072) already snapshots matching callbacks into a MarkedArgumentBuffer before invoking user JS, so groups.clear() mid-callback no longer dangles. Since then the only additions are two more tests (cross-namespace mis-routing and the debug-assertion for the stale lookup flag), both verified to fail on the PR base and pass with the fix. CI failures on the last build were an artifact-download timeout and known Windows flakes, none touching the plugin registry.

@robobun

robobun commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator Author

Unblocked: rebased onto a green base

The earlier CI red here was a main breakage (stale cookie-map.test.ts Expires expectations left behind by #32926), not this diff. That is now fixed on main by #33425, and this branch has been rebased onto it (head 7816f861, base c3995e43). Same four commits, same three-file diff; a fresh build is running.

This PR on its own merits

  • src/ reverted to the merge-base: all 4 clearAll() tests fail, one of them on a real debug assertion:
    ASSERTION FAILED: !globalObject->onLoadPlugins.mustDoExpensiveRelativeLookup
    
  • with the fix: plugins.test.ts is 36 pass / 0 fail (1 pre-existing todo).

Zero unresolved review threads. If the new build shows red, the one lane to double-check first is darwin aarch64 - test-bun, which has been dying on a 120s artifact-download timeout before running any tests on several recent builds across unrelated PRs.

@robobun

robobun commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Verified this change also fixes a crash reproducible on current main and on 1.4.0: Bun.plugin.clearAll() followed by registering and importing into a namespace aborts with SIGABRT and no output. The out-of-sync namespaces/groups vectors make Base::group() index past the end of groups (WTF::CrashOnOverflow::overflowed() via OnLoad::run and Bun__runVirtualModule), so the abort can happen at import time, not only at registration time as in the existing tests.

Bun.plugin({ name: "before", setup(b) {
  b.onLoad({ filter: /.*/, namespace: "cr" }, () => ({ contents: "export default 'before'", loader: "js" }));
}});
Bun.plugin.clearAll();
Bun.plugin({ name: "after", setup(b) {
  b.onResolve({ filter: /.*/, namespace: "cr" }, (a) => ({ path: a.path, namespace: "cr" }));
}});
await import("cr:x"); // exit 134 without this fix, clean rejection with it

On this branch the repro exits 0 with a clean rejection, and the four clearAll tests here pass under a debug ASAN build. I also checked the adjacent cases: a namespace whose loader was cleared no longer gets served by a freshly registered namespace's loader, and a namespace registered only after clearAll() loads normally instead of aborting.

One more data point for merging: the cookie-map failures that made build 68774 red were fixed on main by #33425 (48ff9eb), which landed a few hours after this branch's last push. A re-run against current main should come back green.

jsFunctionBunPluginClear cleared onLoadPlugins.groups but not
onLoadPlugins.namespaces, and onResolvePlugins.namespaces but not
onResolvePlugins.groups. These vectors are parallel-indexed, so after
clearAll() re-registering the same namespace would return &groups[i]
past the end of an empty vector (onLoad, SIGABRT on the WTF::Vector
bounds assert) or hit a stale group entry holding the old callback
(onResolve).

Add Base::clear() that resets fileNamespace, namespaces, and groups
together, and call it for both onLoad and onResolve.
The flag is only ever written true by mock.module() for relative
specifiers and gates an extra path-join in resolveVirtualModule().
After clearAll() deletes virtualModules it should go back to false so
later non-relative virtual modules skip the unnecessary branch.
… mode reset

The existing tests re-register the same namespace after clearAll(). Add the
case where a different namespace is registered, which takes the cleared
namespace's index and so inherits its callbacks, and a test for the
mustDoExpensiveRelativeLookup reset.
@Jarred-Sumner
Jarred-Sumner force-pushed the farm/1a485378/fix-plugin-clearall branch from 319877f to 7816f86 Compare August 17, 2026 01:57

@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 inline nit on the self-referential stderr field is worth tightening but non-blocking; the load-bearing stdout/exitCode assertions catch both the SIGABRT and stale-callback regressions.

Extended reasoning...

Overview

Adds Base::clear() in BunPlugin.h that resets fileNamespace, namespaces, and groups together, and rewires jsFunctionBunPluginClear to call it for both onLoadPlugins and onResolvePlugins instead of clearing a mismatched subset of each. Also resets mustDoExpensiveRelativeLookup alongside the virtualModules delete (my earlier nit, now addressed). Four subprocess tests in plugins.test.ts cover the onLoad OOB abort, the onResolve stale-callback path, cross-namespace index reuse, and the virtual-module lookup-mode assertion.

Security risks

None. clearAll() is a same-process teardown of user-registered plugin callbacks; no untrusted-input parsing, no auth/crypto, no new external surface.

Level of scrutiny

Medium — native C++ in JSC bindings, but the change is a 7-line inline method plus a 5-line call-site rewrite whose correctness follows directly from Base::group() indexing groups[i] by namespaces[i]. The parallel-vector invariant is visible ~15 lines above the new clear(). No allocation, no exception paths, no GC interaction.

Other factors

  • Both of my prior inline comments are resolved: the mustDoExpensiveRelativeLookup reset landed in this diff, and the OnResolve::run reentrancy hazard was deferred to a follow-up (and the snapshot-before-iterate fix has since landed on main, so groups.clear() during a callback no longer dangles the iteration).
  • Tests were verified to fail against merge-base src/ (exit 134 / "stale onResolve callback ran" / ASSERTION FAILED: !mustDoExpensiveRelativeLookup) and pass with the fix, per the PR thread.
  • The one open nit (self-referential stderr in the expected object) weakens only the stderr check; stdout is asserted exactly and exitCode: 0 catches the abort, so the tests still fail for the right reason without the fix.
  • CI red on the last build was an unrelated cookie-map.test.ts breakage on the base commit, since fixed on main.

Comment thread test/js/bun/plugin/plugins.test.ts Outdated
The expected objects passed the captured stderr back as its own expected
value, so that field could never fail. bunEnv silences debug logging and
all four subprocesses are clean, so assert the empty string.

@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 stderr assertion nit from the last pass is addressed in ce16ca6, and all prior feedback is resolved.

Extended reasoning...

Overview

This PR fixes a parallel-array desync bug in Bun.plugin.clearAll(). The old jsFunctionBunPluginClear cleared onLoadPlugins.groups but not .namespaces, and onResolvePlugins.namespaces but not .groups, breaking the invariant that Base::group() relies on (namespaces[i] ↔ groups[i]). The fix adds Base::clear() that resets all three fields together, calls it for both plugin maps, and resets mustDoExpensiveRelativeLookup alongside the virtualModules delete. Net native change is ~7 lines; the rest is four subprocess tests.

Security risks

None. This is a state-reset fix in the runtime plugin registry; no untrusted input parsing, auth, or crypto is touched.

Level of scrutiny

Medium — native C++ in the JSC bindings, but the change is mechanical (clear three parallel vectors together instead of two of four). The root cause is well-explained and the fix is the obvious one. I raised three concerns across earlier passes and all were addressed: the mustDoExpensiveRelativeLookup reset (37d7090), the reentrant clearAll()-from-callback UAF (moot now that OnResolve::run() snapshots callbacks into a MarkedArgumentBuffer before invoking user JS), and the self-referential stderr in the test assertions (ce16ca6).

Other factors

The four new tests follow harness conventions (subprocess isolation via bunEnv/bunExe, concurrent pipe draining, combined-object assertions with stderr: ""), and the author has repeatedly verified fail-before/pass-after against merge-base src/. All review threads are resolved and no bugs were found this run.

@Jarred-Sumner
Jarred-Sumner merged commit 1de3d37 into main Aug 17, 2026
6 of 7 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the farm/1a485378/fix-plugin-clearall branch August 17, 2026 02:21
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