Conversation
WalkthroughThe compiler now recognizes literal ChangesNative bindings support
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
|
Updated 1:05 AM PT - Aug 13th, 2026
❌ @robobun, your commit 5fee0de has 1 failures in
🧪 To try this PR locally: bunx bun-pr 36713That installs a local version of the PR into your bun-36713 --bun |
|
Found 3 issues this PR may fix:
🤖 Generated with Claude Code |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🔴
src/bundler/bundle_v2.rs:6062-6091— The error's suggested remedy "mark the import external" is unactionable: neither--external bindingsnor--external <addon>can suppress this error, because the parser already marked thebindingsrecordIS_UNUSED(skipped at bundle_v2.rs:5884) and theNativeBindingsnot-found branch doescontinue 'outerbefore ever reachingresolve_with_framework(the only place external patterns are consulted). This is also a behavior regression — a project that today passes--external bindingsto defer addon loading to runtime will now fail to build when the.nodefile isn't present on the build machine. Consider checking the resolver's external patterns forbindings/ the addon name before emitting the fatal error, and reword the hint to name the working escape (--external <package-that-calls-bindings>, e.g.--external sqlite3).Extended reasoning...
What the bug is
The new error message reads:
Could not locate native addon "…" for the "bindings" package. Run "bun install" to build it, or mark the import external.
But there is no
--externalvalue the user can pass that suppresses this error for the import in question. The remedy is unactionable as written, and the rewrite additionally regresses the previously-working--external bindingsescape hatch.Code path that triggers it
Step 1 — parser rewrite is unconditional on
--external. Insrc/js_parser/visit/visit_expr.rs:2015-2064, the rewrite fires wheneverp.options.bundleis set and the target isERequireStringwithpath.text == b"bindings". It then:- marks the original
bindingsimport recordIS_UNUSED(line ~2032-2034), and - creates a new
NativeBindings-tagged record whosepath.textis the addon name.
The parser has no access to the resolver's external-pattern list, so
--external bindingscannot influence this step.Step 2 —
--external bindingsnever reaches the resolver. Inbundle_v2.rs:5882-5891, records withIS_UNUSEDare skipped before any resolution runs. Thebindingsrecord therefore never reachesresolver.resolve_with_framework(line 6169), which is whereis_external_patternis consulted (resolver.rs:1209). So passing--external bindingsis a no-op for the immediately-called form.Step 3 —
--external <addon_name>never reaches the resolver either. TheNativeBindingsbranch atbundle_v2.rs:6047runs beforeresolve_with_framework. In theNonearm it setspath.is_disabled = true, logs the error, and doescontinue 'outerat line 6091 — the resolver is never called for this record, sois_external_pattern(addon_name)is never consulted.There is no external-pattern check anywhere between line 5884 and line 6047; only builtin/
bun:*/onResolve-plugin handling sits in between.Concrete walkthrough
Given
node_modules/mypkg/index.jscontainingmodule.exports = require('bindings')('myaddon')with nomyaddon.nodeon disk, and the user runs:- Parser visits the call, sees
bundle=true, matchesERequireString("bindings"), marks record #NIS_UNUSED, creates record #M taggedNativeBindingswithpath.text = "myaddon". - Resolve loop iteration for record #N:
IS_UNUSED→continueat 5890.--external bindingsnever checked. - Resolve loop iteration for record #M: enters
NativeBindingsblock at 6047,resolve_native_bindingsreturnsNone,HANDLES_IMPORT_ERRORSis unset → error logged →continue 'outerat 6091.--external myaddonnever checked. - Build fails with "…or mark the import external", but the user already did.
Trying
--external myaddonor--external myaddon.nodeproduces the identical failure for the same reason (step 3 short-circuits before the resolver).The only working escape is
--external mypkg— externalizing the enclosing package somypkg/index.jsis never parsed at all — which the message does not say and which a user reading "mark the import external" would not naturally infer (the import in question isbindings, notmypkg).Why this matters
- REVIEW.md is explicit: "Error messages are reviewed word-for-word as code … a concrete remedy." A remedy that cannot work as stated blocks merge.
- Behavior regression: before this PR,
bun build --outdir dist --external bindings ./entry.tswould emitrequire('bindings')('myaddon')verbatim, and the bundle would work at runtime providednode_modulesis deployed alongside it. After this PR, the same command fails the build when the.nodefile isn't present on the build machine (common when bundling in CI/Docker without native toolchains and building the addon on the target). The flag the user already relies on is now silently ignored for this shape.
How to fix
Two independent pieces:
- Make the escape hatch work. In the
Nonearm of theNativeBindingsblock (bundle_v2.rs:6062-6091), before emitting the error, consulttranspiler.resolver.is_external_pattern(b"bindings")(and/oris_external_pattern(import_record.path.text)). If either matches, skip the error — either leave the record disabled (so the call becomes a no-op like a try/catch fallthrough) or, better, restore the originalrequire('bindings')(name)shape as an external call so runtime resolution still works. - Reword the remedy to name the escape that actually works today: e.g. "…or add
--external <package>for the package that loads it." — pointing at the enclosing package (sqlite3,mypkg) rather than atbindingsor the addon.
- marks the original
There was a problem hiding this comment.
No further findings from me — the earlier three rounds of feedback (builtin-alias/onResolve interception, --external bindings regression, vacuous test assertion) are all addressed and now covered by tests. Deferring to a human because this is a user-facing bundler feature with design choices (hard-fail on missing .node, which bindings search prefixes to probe, --external semantics) and two open sibling PRs (#35642, #35688) that a maintainer should reconcile.
What was reviewed: the Tag::NativeBindings insertion vs. the ordinal is_internal() check (safe — placed before Runtime, threshold is symbolic); that the rewrite only fires under p.options.bundle with a string-literal arg so transpile-only and non-literal callers are untouched; that the not-found path sets is_disabled and honours HANDLES_IMPORT_ERRORS / ignore_module_resolution_errors / --external bindings; and that the new napi --compile test runs the binary from an empty cwd so it can't accidentally find the source addon.
Extended reasoning...
Overview
The PR adds build-time resolution of require('bindings')('<name>') to the actual .node addon so bun build --compile can embed native modules. It touches four native source files: a new Tag::NativeBindings enum variant in src/ast/import_record.rs; a parser rewrite in src/js_parser/visit/visit_expr.rs that marks the bindings import record IS_UNUSED and emits a tagged require of the addon name; resolve_native_bindings + tag-specific handling in src/bundler/bundle_v2.rs that probes the enclosing package's build/Release-style directories and either rewrites to an absolute .node path or fails the build; and a pub(crate) → pub visibility widening on Resolver::is_external_pattern. Six new itBundled cases and one end-to-end napi --compile test cover the happy path, not-found error, try/catch fall-through, builtin-name collision, --external escape, and non-literal passthrough.
Security risks
None identified. The filesystem probing (bun_sys::exists over a fixed prefix list joined under the enclosing package.json's directory) runs at build time on the developer's machine, not on untrusted input; the addon name is a literal from source the user is already bundling.
Level of scrutiny
High. This is a new user-facing bundler behaviour with a build-failure mode that did not exist before: a project whose .node addon is not yet built (fresh clone, cross-compile host, CI without native deps) now fails bun build unless the call is in try/catch or bindings is externalised. The PR intentionally omits two of the real bindings package's search paths (the ABI-versioned ones) and does not handle the object-argument form require('bindings')({ bindings: 'name' }). Those are reasonable scoping decisions but a maintainer should sign off on them, and on which of the three competing PRs (#35642, #35688, this one) to land.
Other factors
I reviewed this PR three times previously; each finding (builtin-alias / onResolve plugin intercepting the addon name, --external bindings no longer suppressing the error and the error text naming an unreachable remedy, a vacuous .not.toContain assertion, and the misplaced #8697 issue comment) was addressed in commits ae43811, 83ebb0e, and 1757a9f, with new tests compile/NativeBindingsBuiltinNameDoesNotAlias and compile/NativeBindingsExternalSkipsError covering the fixes. This run's bug hunt found nothing new. I spot-checked that inserting NativeBindings mid-enum does not break Tag::is_internal() (it compares against Tag::Runtime as u8 symbolically, and the new variant sits below that threshold). Test coverage looks solid for the shapes handled; the CI failures in the robobun comment (36577.test.ts, bun-upgrade.test.ts) are unrelated to this change.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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 `@src/js_parser/visit/visit_expr.rs`:
- Around line 2010-2055: Update the bindings handling in visit_expr.rs around
the require-expression transformation so the original runtime
require("bindings") call remains available when the derived native addon record
cannot be resolved, rather than marking it unused unconditionally; preserve or
emit an external fallback for that case. In src/bundler/bundle_v2.rs around
lines 6045-6100, add a runtime regression test covering an external bindings
package with a missing addon and verify the generated code still performs the
runtime lookup.
In `@test/bundler/bundler_compile.test.ts`:
- Around line 393-408: Strengthen the itBundled case
“compile/NativeBindingsTryCatchFallsThrough” by inspecting the generated bundle
and asserting it excludes the native-binding failure text “Could not find module
root given file” and the getRoot symbol. Keep the existing fallback runtime
assertion, ensuring the test also detects when the bindings call is not
suppressed and rewritten.
In `@test/napi/napi.test.ts`:
- Around line 207-217: Update the N-API executable test around spawnSync to
create a dedicated fixture temporary directory, set BUN_TMPDIR to that directory
in the child environment, and assert readdirSync on it is empty after execution.
Keep the existing output and success assertions, and do not rely on cwd/runDir
to verify embedded native-addon usage.
🪄 Autofix (Beta)
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: e265731f-ee6f-4d77-b62a-5716109a6a52
📒 Files selected for processing (6)
src/ast/import_record.rssrc/bundler/bundle_v2.rssrc/js_parser/visit/visit_expr.rssrc/resolver/resolver.rstest/bundler/bundler_compile.test.tstest/napi/napi.test.ts
There was a problem hiding this comment.
No new findings this pass — the earlier three rounds of feedback (builtin-alias/onResolve interception, the --external escape hatch and error wording, and the vacuous ExternalSkipsError assertion) have all been addressed. Deferring to a maintainer: this is a user-facing bundler feature with design choices (probe-path set, hard-fail on missing addon, --external bindings semantics) and two open PRs (#35642, #35688) covering the same ground.
What was reviewed:
Tag::NativeBindingsinsertion vs.Tag::is_internal()ordinal check — placed beforeRuntime, so the>= Runtimecomparison is preserved and no other code keys on the discriminant value.- Parser rewrite in
visit_expr.rs: thebindingsrecord is markedIS_UNUSEDonly afteradd_import_record_by_rangereborrowsp.import_records, andimport_records_for_current_partis updated so the new record is linked. resolve_native_bindings: usesbun_sys::exists+ pooled path buffer +enclosing_package_jsonwalk; the resolved path is arena-interned before assignment.
Extended reasoning...
Overview
The PR adds build-time resolution of require('bindings')('<name>') so that bun build --compile can embed the referenced .node addon instead of shipping a binary that fails at runtime with "Could not find module root". It touches four source files: a new Tag::NativeBindings enum variant in src/ast/import_record.rs; a parser rewrite in src/js_parser/visit/visit_expr.rs that detects the call shape, marks the bindings import unused, and emits a tagged require record for the addon name; a filesystem probe + record rewrite in src/bundler/bundle_v2.rs::resolve_import_records (with a new resolve_native_bindings helper); and a pub(crate) → pub visibility widening on Resolver::is_external_pattern. Six new itBundled cases in test/bundler/bundler_compile.test.ts and one end-to-end --compile test in test/napi/napi.test.ts cover the feature.
Security risks
None identified. The probe reads only from directories under the enclosing package.json of the importing file at build time (same trust boundary as the resolver already operates in), and the addon name comes from a source-code string literal, not runtime input. No path components are user-controlled beyond what the bundler already resolves.
Level of scrutiny
High. This is a feature change to the bundler's import-record resolution loop and the parser's visit pass — both hot, correctness-critical paths that affect every bundle. It changes user-visible behaviour: builds that previously succeeded (and produced a broken-at-runtime bundle) now hard-fail when the .node file is absent and the call is not in a try/catch. The chosen probe-directory list, the decision to skip the ABI-versioned compiled/ and lib/binding/ paths, and the --external bindings escape-hatch semantics are all design calls a maintainer should sign off on. There are also two open PRs (#35642, #35688) that github-actions flagged as covering the same ground with the same approach.
Other factors
I've already run three review passes on this PR; each finding was addressed with a follow-up commit and a covering test (NativeBindingsBuiltinNameDoesNotAlias, NativeBindingsExternalSkipsError, and the tightened assertions in 1757a9f). This pass I additionally checked that inserting NativeBindings before Runtime in the #[repr(u8)] enum does not break Tag::is_internal() (it compares >= Tag::Runtime as u8, and grep shows no other ordinal use of this enum), and that the parser reborrows p.import_records after add_import_record_by_range may have grown it, so the earlier record borrow does not dangle. The two CI failures reported by robobun (36577.test.ts on Windows x64 and bun-upgrade.test.ts on Windows aarch64) are unrelated to the changed files. Given the scope and the duplicate-PR situation, this needs a human maintainer's decision rather than a bot approval.
|
CI status for the current revision (5fee0de, build #94182): every build lane and 155 of 156 completed test shards are green, including the bundler and napi suites this PR touches ( Earlier revision (600f61c, before the rework): build #87502All build lanes green; all Linux test lanes (incl. ASAN), macOS, and the Windows shards that got an agent were green. Remaining red was Azure VM-creation throttling, the pre-existing |
… and resolver/fs (#36803) Net: +47 / -1160. ## Removed - **`src/runtime/node/nodejs_error_code.rs`** (1113 lines): a 340-variant `enum Code` mirroring the Node.js `ERR_*` table. The sole reference outside its own module was `node_os.rs:269` doing `<&'static str>::from(ErrorCode::ERR_SYSTEM_ERROR)`, which just produces the string `"ERR_SYSTEM_ERROR"`. Three other call sites in the same file (`node_os.rs:938/1220/1539`) already use `BunString::static_("ERR_SYSTEM_ERROR")` directly, so the remaining one now does the same. The `jsc::ErrorCode` type (backed by `ErrorCode.generated.rs`) is the live `ERR_*` table; this enum was a parallel dead one. - `rg -n 'nodejs_error_code' src/ build/debug/codegen/ src/codegen/` → only the `mod` declaration and two explanatory comments (both updated). - **`dir_iterator::IteratorError`** (11 lines) + **`runtime::Error::DirIterator`** variant (3 lines): the enum is never constructed (`rg 'IteratorError::' src/ build/debug/codegen/` → 0 hits), so the `#[from]` on `Error::DirIterator` can never fire either. - **`VectorArrayBuffer::to_js`** (4 lines): every caller reads `.value` directly; `to_js` was never invoked and is not a trait impl. - **`src/resolver/fs.rs`** commented-out Zig stubs `statBatch/stat/readFile/readDir` (9 lines): never implemented. ## Verification ``` rg -w <symbol> src/ build/debug/codegen/ src/codegen/ bun bd bun run rust:check-all # 10 ok, 0 failed bun bd test test/js/node/os/ test/js/node/fs/fs.test.ts -t readdir bun bd test test/bundler/bundler_loader.test.ts bun bd test test/internal/source-lints/ ``` A source-lint test (`test/internal/source-lints/dead-symbols-nodejs-error-code.test.ts`) asserts none of these reappear. ## Also scanned (nothing removed) `src/http/**` (36 files), `src/install/**`, `src/resolver/**`, `src/ast/**`, `src/semver/**`, `src/valkey/**`, `src/sql/postgres/**`, `src/collections/**`. Several initial candidates turned out to have callers under a different crate or via method-call syntax: `collections::StringMap` (sql_jsc), `semver::string::ArrayHashContext` (install/lockfile), `NewWriter::{int8,f64,bun_string}` (sql_jsc), `Level::{gt,eql}` (js_parser), `SinglyLinkedList::len` (bake/memory_cost), `Target::is_node` (resolve_builtins), `{Parse,Decode}DataURLError::name()` (bundler/transpiler). No overlap with open dead-code PRs #36237, #35775, #36791, #36115, #35437, #35880. <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 1 · 10 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 1 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-nodejs-error-code.test.ts bun test v1.4.0 (6071f67) test/internal/source-lints/dead-symbols-nodejs-error-code.test.ts: 24 | ["src/runtime/error.rs", /\bDirIterator\b/], 25 | ["src/runtime/node/types.rs", /impl VectorArrayBuffer \{\n pub fn to_js\(/], 26 | ["src/resolver/fs.rs", /pub fn statBatch\(fs: \*FileSystemEntry/], 27 | ]; 28 | const resurrected = checks.filter(([file, re]) => re.test(src(file))).map(([file, re]) => `${file}: ${re.source}`); 29 | expect(resurrected).toEqual([]); ^ error: expect(received).toEqual(expected) - [] + [ + "src/runtime/node.rs: \bnodejs_error_code\b", + "src/runtime/node/node_os.rs: crate::node::ErrorCode", + "src/runtime/node/dir_iterator.rs: \benum IteratorError\b", + "src/runtime/error.rs: \bDirIterator\b", + "src/runtime/node/types.rs: impl VectorArrayBuffer \{\n pub fn to_js\(", + "src/resolver/fs.rs: pub fn statBatch\(fs: \*FileSystemEntry", + ] - Expected - 1 + Received + 8 at <ano ... (truncated) release without fix: 1 FAILED bun test v1.4.0-canary.1 (a6ff9d1) test/internal/source-lints/dead-symbols-nodejs-error-code.test.ts: 24 | ["src/runtime/error.rs", /\bDirIterator\b/], 25 | ["src/runtime/node/types.rs", /impl VectorArrayBuffer \{\n pub fn to_js\(/], 26 | ["src/resolver/fs.rs", /pub fn statBatch\(fs: \*FileSystemEntry/], 27 | ]; 28 | const resurrected = checks.filter(([file, re]) => re.test(src(file))).map(([file, re]) => `${file}: ${re.source}`); 29 | expect(resurrected).toEqual([]); ^ error: expect(received).toEqual(expected) - [] + [ + "src/runtime/node.rs: \bnodejs_error_code\b", + "src/runtime/node/node_os.rs: crate::node::ErrorCode", + "src/runtime/node/dir_iterator.rs: \benum IteratorError\b", + "src/runtime/error.rs: \bDirIterator\b", + "src/runtime/node/types.rs: impl VectorArrayBuffer \{\n pub fn to_js\(", + "src/resolver/fs.rs: pub fn statBatch\(fs: \*FileSystemEntry", + ] - Expected - 1 + Received + 8 at <anonymous> (/workspace/bun/test/internal/source-lints/dead-symbols-nodejs-error-code.test.ts:29:23) (fail) dead Rust symbols in runtime/node + resolver do not reappear [0.74ms] 0 pass 1 fail ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console 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/internal/source-lints/dead-symbols-nodejs-error-code.test.ts bun test v1.4.0 (6071f67) test/internal/source-lints/dead-symbols-nodejs-error-code.test.ts: (pass) dead Rust symbols in runtime/node + resolver do not reappear [30.31ms] 1 pass 0 fail 1 expect() calls Ran 1 test across 1 file. [2.02s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 652ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/73] gen ErrorCode+*.h [2/11] gen cpp.rs (cppbind) [3/11] gen BunProcess.lut.h Generating /workspace/bun/build/release/codegen/BunProcess.lut.h from /workspace/bun/src/jsc/bindings/BunProcess.cpp [4/11] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [4/11] 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_resolver v0.0.0 (/workspace/bun/src/resolver) �[1m�[92m Compiling�[0m bun_router v0.0.0 (/workspace/bun/src/router) �[1m�[92m Compiling�[0m bun_bundler v0.0.0 (/workspace/bun/src/bundler) �[1m�[92m Compiling�[0m bun_standalone_graph v0.0.0 (/workspace/bun/src/standalone_graph) �[1m�[92m Compiling�[0m bun_transpiler v0.0.0 (/workspace/bun/src/transpiler) �[1m�[92m Compiling�[0m bun_bunfig v0.0.0 (/workspace/bun/src/bunfig) �[1m�[92m Compiling�[0m bun_instal ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/jsc/ErrorCode.rs | 14 +- src/jsc/lib.rs | 4 +- src/resolver/fs.rs | 9 - src/runtime/error.rs | 3 - src/runtime/node.rs | 4 - src/runtime/node/dir_iterator.rs | 11 - src/runtime/node/node_os.rs | 4 +- src/runtime/node/nodejs_error_code.rs | 1113 -------------------- src/runtime/node/types.rs | 4 - .../dead-symbols-nodejs-error-code.test.ts | 30 + 10 files changed, 35 insertions(+), 1161 deletions(-) ``` </details> **gate history** · 1 passed · 1 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/jsc/ErrorCode.rs 3 3 0 src/jsc/lib.rs 1 1 0 src/resolver/fs.rs 1 1 0 src/runtime/error.rs 1 1 0 src/runtime/node.rs 1 1 0 src/runtime/node/dir_iterator.rs 1 1 0 src/runtime/node/node_os.rs 1 1 0 src/runtime/node/nodejs_error_code.rs 0 0 0 src/runtime/node/types.rs 1 1 0 …nal/source-lints/dead-symbols-nodejs-error-code.test.ts 2 4 0 ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…n't deref a freed NewSource box (#36799) ## What `fetch(url, { signal })` + `resp.body.getReader()` + `reader.cancel()`, drop the Response, then abort the signal: heap-use-after-free on the response body stream's native `Box<NewSource<_>>`. Reproduces 5/5 under ASAN on `main`, clean on `5b7c3cacbe63` (before #36624). ``` READ of size 1 in NewSource<ByteBlobLoader>::cancel (ReadableStream.rs:936) <- ReadableStream::done <- ReadableStream::error <- BodyAbortListener::on_abort (Response.rs:142) <- AbortSignal::runAbortSteps <- Timeout::run <- __bun_fire_timer freed by: JSDestructibleObjectDestroyFunc <- PreciseAllocation::sweep <- sweepPreciseAllocations <- Heap::sweepInFinalize allocated by: Box::new(NewSource<..>) <- Value::to_readable_stream <- Response.body getter ``` Minimal repro (crashes at iteration 0 on debug and release ASAN): ```js const server = Bun.serve({ port: 0, fetch: () => new Response(Buffer.alloc(20000, "x")) }); async function one(signal) { const resp = await fetch(server.url, { signal }); const rd = resp.body.getReader(); await rd.read(); await rd.cancel(); } for (;;) { const ac = new AbortController(); await Promise.all([one(ac.signal), one(ac.signal), one(ac.signal)]); Bun.gc(false); // must NOT be gc(true) await Bun.sleep(5); ac.abort(); await Bun.sleep(5); } ``` ## Cause #36624 changed `readable_stream::Strong` so that `check_body_stream_ref` downgrades `Body.Locked.readable` from a `bun_jsc::Strong` to a raw `weak: JSValue` once the Response wrapper's traced `m_stream` WriteBarrier slot owns the stream. That raw JSValue is never cleared when the stream is collected. After `reader.cancel()` (ByteStream path) or once the body is fully buffered (ByteBlobLoader path), nothing but the Response wrapper's `m_stream` roots the stream. When the user drops the Response, one eden GC: 1. reaps weak handles (so a real `JSC::Weak` would already read `None`), 2. runs `sweepInFinalize` -> `sweepPreciseAllocations`, which sweeps the `JS{Bytes,Blob}InternalReadableStreamSource` cell **synchronously**; its destructor runs `NewSource::finalize` -> `decrement_count` -> `drop(Box<NewSource<_>>)`, 3. leaves the `JSResponse` wrapper's MarkedBlock cell for lazy sweep. `Response::finalize` (the wrapper cell destructor) is what sets `js_ref` to `Finalized` and drops `abort_listener`, so in that window the native `Response` is still alive, `Locked.readable` still holds the stream's raw JSValue, and `BodyAbortListener` is still registered. `AbortSignal.timeout` / `ac.abort()` then fires: * `on_abort` -> `get_body_readable_stream` reaches the dead stream through either `js_ref()` (raw JSValue to the dead-but-unswept wrapper) or `Locked.readable`'s raw JSValue -> `ReadableStreamTag__tagged` -> `m_nativePtr` -> the destructed source cell's `m_ctx` -> the freed `Box<NewSource<_>>`, and * `Value::to_error_instance` reads the same handle via `strong_readable.get()` -> `bytes.on_data()`. `Bun.gc(true)` does not reproduce: a full synchronous sweep runs the wrapper destructor in the same pass, which drops the listener before abort can fire. ## Fix * `readable_stream::Strong::weak` is now `bun_jsc::Weak<()>` instead of a raw `JSValue`. `downgrade()` creates it with the new `WeakRefType::None` (no finalize owner; `JSC::Weak` accepts a null owner). `get()` / `has()` / `is_disturbed()` / `tee()` all see `None` once the stream is reaped, so `Value::to_error_instance` and every other `Locked.readable` reader skip the freed `NewSource` deref. * `BodyAbortListener::on_abort` reads the stream via `Locked.readable` directly instead of `get_body_readable_stream`, whose `js_ref()` path would still read a raw `JsRef::Weak(JSValue)` to the dead-but-unswept wrapper. `Locked.readable.get()` returns the live stream when a reader roots it without the wrapper, and `None` exactly when the stream (and its `NewSource` box) is collected. * `bun_jsc::Weak::create_passive` and a `WeakRefType::None` arm in `Bun__WeakRef__new` support an ownerless `JSC::Weak`. No user-visible behaviour changes: when the stream is collected `readable.error()` was already a no-op (`webStreamControllerError` early-returns on a non-Readable stream; `NewSource::cancel` early-returns on `cancelled`); when the stream is alive `readable.error()` runs exactly as before. ## Verification * `test/js/web/fetch/fetch-abort-after-cancel-gc-fixture.ts` + test: crashes at iteration 0 on `main` under debug+ASAN (`bun bd test`), passes with this PR. * release+ASAN: 5/5 clean on both the fixture (100 iters) and the original fuzzer repro (300 iters); `main` crashes 5/5 and 4/4 respectively. * `test/js/bun/http/serve-http3.test.ts` "client abort during streaming response" passes 3/3 on release+ASAN (regressed on the first commit of this PR; fixed in c5da841). * #36624's leak regression test `test/regression/issue/29267` still passes (the downgrade is preserved; only the storage is now a real Weak). * `test/js/web/fetch/{fetch-abort-stream-body,body,body-stream,body-clone,fetch-abort-queued,fetch-stream-cancel-leak}.test.ts` and `test/js/web/streams/readable-stream-terminal-barrier-release.test.ts` unchanged vs `main`. <!-- robobun:evidence:begin --> --- **no test proof** · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/web/fetch/fetch-abort-stream-body.test.ts <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
… InlineBlob, Ipc.ts (#36791) Net -817 lines (+82 / -899) across 39 files. Every removed symbol was verified via `rg` across `src/` and `build/debug/codegen/` to have zero remaining references; `bun bd` and `bun run rust:check-all` (all CI target triples) pass. ## Whole-file deletions (C++ headers) - `src/jsc/bindings/TextCodecASCIIFastPath.h` (78 lines): never `#include`d anywhere under `src/`, `scripts/`, `cmake/`, or generated code. Defines `PAL::UCharByteFiller` / `PAL::copyASCIIMachineWord`, neither referenced. - `src/jsc/bindings/webcore/Node.h` (108 lines): defines `WebCore::Node` which is never instantiated, subclassed, or referenced as a type. No `WTF_MAKE_TZONE_ALLOCATED_IMPL(Node)` exists so it could never link. Removed the two `#include "Node.h"` lines in `WebCoreOpaqueRoot.h` and `EventTargetHeaders.h` (neither file uses the type). - `src/jsc/bindings/webcore/JSDOMWindow.h`, `JSServiceWorker.h`, `JSWindowProxy.h`: one-line stub headers. Removed their `#include` sites in `JSMessageEvent.cpp`, `JSMessageEventCustom.cpp`, and the commented-out includes in `JSEventTargetCustom.cpp`. ## Dead `extern "C"` exports (lost their Rust caller in #35002) - `highway_strings.cpp`: `highway_char_frequency` + `ScanCharFrequencyImpl` (69-line SIMD body) + `HWY_EXPORT`. Whole-repo `rg -w highway_char_frequency` = 1 hit (its own definition). - `TextCodecWrapper.cpp`: `Bun__isEncodingSupported`, `Bun__getCanonicalEncodingName`. Whole-repo `rg` = 1 hit each. - `StrongRef.cpp` / `.h`: `Bun__StrongRef__get`, `Bun__StrongRef__clear`. `src/jsc/Strong.rs` only declares `new`/`set`/`delete`; reads go through a direct pointer load per the comment at `StrongRef.cpp:19-26`. Also removed `StrongRootBlock::clearValue` which was only called from `Bun__StrongRef__clear`. - `InspectorLifecycleAgent.cpp`: `Bun__LifecycleAgentReportReload` wrapper + `InspectorLifecycleAgent::reportReload()` method + header decl. No Rust caller. - `InspectorBunFrontendDevServerAgent.cpp`: `InspectorBunFrontendDevServerAgent__notifyClientErrorReported` / `notifyGraphUpdate` wrappers + `clientErrorReported()` / `graphUpdate()` class methods + header decls. `inspector_agent.rs` declares the 7 sibling `notify*` wrappers but not these two. - `InspectorBunFrontendDevServerAgent.h`: removed the stale 9-entry `extern "C"` block that declared `BunFrontendDevServerAgent__notify*` (no `Inspector` prefix), which never matched the actual `InspectorBunFrontendDevServerAgent__notify*` definitions or the Rust imports. - `JSS3File.cpp`: `static bool customHasInstance(...)` is a file-scope static function, never referenced in the file. `JSS3File`'s `StructureFlags` is `Base::StructureFlags` with no `ImplementsHasInstance`, so `CREATE_METHOD_TABLE` cannot pick it up. Removed the matching `JSS3File__hasInstance` forward decl, the `has_instance` helper in `S3File.rs`, and its `#[no_mangle]` export whose only caller was this static. ## Commented-out C++ (>6 months stale per `git blame`) - `JSTextEncoder.cpp`: disabled DOMJIT declarations, `DOMJIT::Signature` statics, `HashTableValue` entries, and two `JSC_DEFINE_JIT_OPERATION` bodies (83 lines total). Commented out since 2024-09; the non-DOMJIT `encode`/`encodeInto` entries remain. - `JSURLSearchParams.cpp`, `JSErrorEvent.cpp`, `JSDOMException.cpp`: `#if ENABLE(BINDING_INTEGRITY)` vtable-pointer scaffolding inside and before `toJSNewlyCreated`. Commented out since 2022-03. - `JSPerformance.cpp`, `JSDOMURL.cpp`: `JSDOMWindowBase`-gated `deleteProperty` paths in `finishCreation`/`initializeProperties`. Commented out since 2024. - `JSWorkerOptions.cpp`: `credentials`/`type` dictionary-member parsing. Commented out since 2023-07 / 2025-01. - `JSEventListener.cpp`: `handleBeforeUnloadEventReturnValue` helper and its call site. Commented out since 2022-03. - `PerformanceUserTiming.cpp`: `restrictedMarkFunctions` lookup inside `convertMarkToTimestamp`. Commented out since 2024-01. ## Rust - `Blob.rs`: `pub struct Inline` + `impl Inline` + `impl Default for Inline` (48 lines). `rg 'InlineBlob|blob::Inline'` shows every reference is inside a `//` comment; the `Any` enum at `Blob.rs:6353` has no `Inline` variant. - `Body.rs`, `server/RequestContext.rs`: removed the commented-out `InlineBlob` match arms that referenced the deleted struct. - `S3File.rs`: `has_instance` + `JSS3File__hasInstance` export (only caller was the removed C++ static; see above). - `streams.rs`: `BufferAction::get` (zero callers; all `.get()` calls on `buffer_action` resolve to `JsCell::get`). - `FileReader.rs`: `pub const TAG` (zero references). ## src/js (>6 months stale per `git blame`) - `builtins/Ipc.ts`: 131-line commented-out `handleConversion` map (net.Server/net.Socket/dgram.*). Blame 2025-05-06. - `internal/fs/streams.ts`: commented-out `fastPath._getFd()` block. Blame 2025-01-25. - `node/worker_threads.ts`: two commented-out type imports. Blame 2023-08-07. ## Verification - `bun bd`: builds clean - `bun run rust:check-all`: 10 ok, 0 failed (all target triples) - Smoke tests pass: `text-encoder.test.js` (42 pass), `worker_threads.test.ts` (91 pass), `body.test.ts` (448 pass), `url.test.ts` (18 pass), `performance.test.js` (7 pass) - `test/internal/source-lints/dead-symbols-webcore-inline-extern.test.ts` added: fails on `main` (every check matches), passes after this diff ## Followups (not in this diff) - `BunFrontendDevServerFrontendDispatcher::clientErrorReported`/`graphUpdate` and `LifecycleReporterFrontendDispatcher::reload` in the generated inspector protocol are now unreferenced from C++; trimming those lives in the protocol JSON, out of scope here. Downstream consumers that were already inert before this PR (no Rust producer ever existed): the `LifecycleReporter.reload` listener in `packages/bun-vscode/src/features/diagnostics/diagnostics.ts`, the `ReloadEvent` type in `packages/bun-inspector-protocol/src/protocol/jsc/index.d.ts`, and the two `test.todo` blocks waiting on `clientErrorReported`/`graphUpdate` in `test/cli/inspect/BunFrontendDevServer.test.ts`. Scanned and found clean (no confident dead symbols): `src/install/` (lockfile/npm/migration/yarn/bin/isolated_install), `src/sql/postgres/`, `src/http/` (websocket, h3_client, AsyncHTTP, HTTPThread, etc.), `src/semver/`, `src/resolver/`, `src/ast/`, `src/collections/`, `src/bun_core/`, `src/runtime/api/`, `src/runtime/node/`, `src/dotenv/`, `src/patch/`, `src/glob/`, `src/event_loop/`, `src/threading/`, `src/uws/`, `src/crash_handler/`, `src/valkey/`, `src/runtime/socket/`. <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 2 · 46 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 5 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/dead-symbols-webcore-inline-extern.test.ts bun test v1.4.0 (e434f18) test/internal/source-lints/dead-symbols-webcore-inline-extern.test.ts: 23 | ["src/jsc/bindings/webcore/JSMessageEventCustom.cpp", /#include "JSDOMWindow\.h"/], 24 | ["src/jsc/bindings/webcore/JSMessageEvent.cpp", /#include "JSServiceWorker\.h"|#include "JSWindowProxy\.h"/], 25 | ["src/jsc/bindings/webcore/JSEventTargetCustom.cpp", /"JSDOMWindow\.h"|"JSWindowProxy\.h"/], 26 | ]; 27 | const found = checks.filter(([f, re]) => re.test(src(f))).map(([f, re]) => `${f}: ${re.source}`); 28 | expect(found).toEqual([]); ^ error: expect(received).toEqual(expected) - [] + [ + "src/jsc/bindings/WebCoreOpaqueRoot.h: #include "Node\.h"", + "src/jsc/bindings/webcore/EventTargetHeaders.h: #include "Node\.h"", + "src/jsc/bindings/webcore/JSMessageEventCustom.cpp: #include "JSDOMWindow\.h"", + "src/jsc/bindings/webcore/JSMessageEvent.cpp: #include "JSServiceWorker\.h"|#include "JSWindowProxy\.h"", + "src/jsc ... (truncated) release without fix: 5 FAILED bun test v1.4.0-canary.1 (831f867) test/internal/source-lints/dead-symbols-webcore-inline-extern.test.ts: 23 | ["src/jsc/bindings/webcore/JSMessageEventCustom.cpp", /#include "JSDOMWindow\.h"/], 24 | ["src/jsc/bindings/webcore/JSMessageEvent.cpp", /#include "JSServiceWorker\.h"|#include "JSWindowProxy\.h"/], 25 | ["src/jsc/bindings/webcore/JSEventTargetCustom.cpp", /"JSDOMWindow\.h"|"JSWindowProxy\.h"/], 26 | ]; 27 | const found = checks.filter(([f, re]) => re.test(src(f))).map(([f, re]) => `${f}: ${re.source}`); 28 | expect(found).toEqual([]); ^ error: expect(received).toEqual(expected) - [] + [ + "src/jsc/bindings/WebCoreOpaqueRoot.h: #include "Node\.h"", + "src/jsc/bindings/webcore/EventTargetHeaders.h: #include "Node\.h"", + "src/jsc/bindings/webcore/JSMessageEventCustom.cpp: #include "JSDOMWindow\.h"", + "src/jsc/bindings/webcore/JSMessageEvent.cpp: #include "JSServiceWorker\.h"|#include "JSWindowProxy\.h"", + "src/jsc/bindings/webcore/JSEventTargetCustom.cpp: "JSDOMWindow\.h"|"JSWindowProxy\.h"", + ] - Expected - 1 + Received + 7 at <anonymous> (/workspace/bun/test/internal/source-lints/dead-symbol ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console 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/internal/source-lints/dead-symbols-webcore-inline-extern.test.ts bun test v1.4.0 (e434f18) test/internal/source-lints/dead-symbols-webcore-inline-extern.test.ts: (pass) #includes of deleted webcore/bindings headers do not reappear [16.17ms] (pass) dead extern C wrappers and cascaded methods do not reappear [17.51ms] (pass) commented-out DOMJIT/BINDING_INTEGRITY blocks in webcore do not reappear [17.74ms] (pass) dead InlineBlob struct and S3File hasInstance do not reappear [22.02ms] (pass) commented-out handleConversion/fs-stream blocks in src/js do not reappear [7.89ms] 5 pass 0 fail 5 expect() calls Ran 5 tests across 1 file. [2.09s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 723ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/39] gen cpp.rs (cppbind) [2/39] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [3/39] gen JS modules (bundle-modules) Preprocess modules (8651ms) Bundle modules (39ms) Postprocesss modules (34ms) Bundle Functions (673ms) Generate Code (18ms) [9.43s] Bundled "src/js" for production 2559 kb 193 internal modules 13 native modules 90 internal functions across 19 files [3/29] 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_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/js/builtins/Ipc.ts | 131 --------------------- src/js/internal/fs/streams.ts | 13 -- src/js/node/worker_threads.ts | 2 - src/jsc/bindings/DOMWrapperWorld.cpp | 2 - .../InspectorBunFrontendDevServerAgent.cpp | 28 ----- .../bindings/InspectorBunFrontendDevServerAgent.h | 16 --- src/jsc/bindings/InspectorLifecycleAgent.cpp | 13 -- src/jsc/bindings/InspectorLifecycleAgent.h | 1 - src/jsc/bindings/JSDOMWrapper.cpp | 7 -- src/jsc/bindings/JSS3File.cpp | 26 ---- src/jsc/bindings/JSS3File.h | 2 - src/jsc/bindings/StrongRef.cpp | 10 -- src/jsc/bindings/StrongRef.h | 2 - src/jsc/bindings/StrongRootBlock.h | 15 --- src/jsc/bindings/TextCodecASCIIFastPath.h | 78 ------------ src/jsc/bindings/TextCodecWrapper.cpp | 26 ---- src/jsc/bindings/WebCoreOpaqueRoot.h | 2 - src/jsc/bindings/highway_strings.cpp | 78 ------------ src/jsc/bindings/webcore/EventTargetHeaders.h | 1 - src/jsc/bindings/webcore/JSDOMException.cpp | 30 ----- src/jsc/bindings/webcore/JSDOMPromise.cpp | 2 - src/jsc/bindings/webcore/JSDOMPromiseDeferred.cpp | 4 - src/jsc/bindings/webcore/JSDOMURL.cpp | 12 -- src/jsc/bindings/webcore/JSDOMWindow.h | 1 - src/jsc/bindings/webcore/JSErrorEvent.cpp | 29 ----- src/jsc/bindings/webcore/JSErrorHandler.cpp | 3 - src/jsc/bindings/webcore/JSEventListener.cpp | 27 ----- src/jsc/bindings/webcore/JSEventTargetCustom.cpp | 5 - src/jsc/bindings/webcore/JSMessageEvent.cpp | 2 - src/jsc/bindings/webcore/JSMessageEventCustom.cpp | 3 - src/jsc/bindings/webcore/JSPerformance.cpp | 17 --- src/jsc/bindings/webcore/JSServiceWorker.h ... (truncated) ``` </details> **gate history** · 3 passed · 1 rejected · iteration 2 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/js/builtins/Ipc.ts 1 1 0 src/js/internal/fs/streams.ts 1 1 0 src/js/node/worker_threads.ts 1 1 0 src/jsc/bindings/DOMWrapperWorld.cpp 1 1 0 src/jsc/bindings/InspectorBunFrontendDevServerAgent.cpp 2 2 0 src/jsc/bindings/InspectorBunFrontendDevServerAgent.h 1 1 0 src/jsc/bindings/InspectorLifecycleAgent.cpp 2 2 0 src/jsc/bindings/InspectorLifecycleAgent.h 1 1 0 src/jsc/bindings/JSDOMWrapper.cpp 1 1 0 src/jsc/bindings/JSS3File.cpp 3 3 0 src/jsc/bindings/JSS3File.h 1 1 0 src/jsc/bindings/StrongRef.cpp 1 1 0 src/jsc/bindings/StrongRef.h 1 1 0 src/jsc/bindings/StrongRootBlock.h 2 2 0 src/jsc/bindings/TextCodecASCIIFastPath.h 1 0 0 src/jsc/bindings/TextCodecWrapper.cpp 1 1 0 (+ 30 more files) ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Empirically re-derived which `test/expectations.txt` entries are still needed by removing all 29 and running the full CI matrix ([build 87834](https://buildkite.com/bun/bun/builds/87834)). ## Result: 29 entries → 3 ### Kept (3): still fail on the named lane | entry | lane | observed failure (build 87834) | |---|---|---| | `test/bundler/native-plugin.test.ts` | WINDOWS | MSB8020: ClangCL build tools not found (agent image gap) | | `test/js/node/test/parallel/test-net-pingpong.js` | WINDOWS | named-pipe half-close: count 1000 !== 1001 | | `test/js/node/test/sequential/test-net-listen-shared-ports.js` | LINUX | SO_REUSEPORT shared-listener semantics; passes on macOS/Windows | ### Deleted (6): vendored Node tests that fail deterministically on every lane These can never pass as vendored; removing the files instead of re-quarantining. | file | reason | |---|---| | `test-stream-wrap.js`, `test-stream-wrap-drain.js`, `test-stream-wrap-encoding.js` | require `internal/js_stream_socket` which Bun does not implement | | `test-net-connect-keepalive.js`, `test-net-server-keepalive.js` | assert `_handle.setKeepAlive` receives seconds (libuv convention); Bun's `_handle` is Bun.Socket (ms). End-to-end TCP_KEEPIDLE coverage is in `test/js/bun/net/socket.test.ts` | | `test-set-http-max-http-headers.js` | spawns `test-http-max-http-headers.js` which is not vendored | ### Moved to `no-validate-exceptions.txt` (7): fail only via unchecked-exception assertions These now **run** on ASAN with `validateExceptionChecks` off, instead of being removed from the run entirely. | file | unchecked exception scope | |---|---| | `test/integration/next-pages/test/dev-server-ssr-100.test.ts` | `JSOrderedHashTable::getImpl` → `executeBoundCall` | | `test/integration/next-pages/test/dev-server.test.ts` | same | | `test/integration/next-pages/test/next-build.test.ts` | same | | `test/js/third_party/next-auth/next-auth.test.ts` | same | | `test/napi/napi.test.ts` | `Process_functionDlopen` (BunProcess.cpp:397) | | `test/cli/run/require-cache.test.ts` | `NapiClass::finishCreation` (NapiClass.cpp:120) | | `test/cli/inspect/inspect.test.ts` | `getOwnNonIndexPropertyNames` → `JSObjectInlines::get` (inspector Runtime.evaluate) | Also bumped `esm-fixture-leak-small.mjs` ASAN threshold 400→500 MB (build 87834 measured 407 MB; ASAN quarantine overhead) so `require-cache.test.ts` passes end to end on ASAN. ### Removed (13): now pass on their named lane | entry | was scoped to | now passes on | |---|---|---| | `test/js/node/test/parallel/test-repl-close.js` | WINDOWS-AARCH64 | windows 11 aarch64 | | `test/js/node/test/parallel/test-tls-connect-memleak.js` | LINUX-X64-MUSL | alpine 3.23 x64 + aarch64 | | `test/js/bun/spawn/spawn-maxbuf.test.ts` | (all) | every lane (also fixed for debug in #36782) | | `test/js/bun/spawn/spawn.test.ts` | ASAN | every lane (heap-use-after-free fixed in #36783) | | `test/js/sql/tls-sql.test.ts` | ASAN | debian 13 x64-asan | | `test/js/node/url/pathToFileURL.test.ts` | ASAN | debian 13 x64-asan | | `test/js/node/fs/abort-signal-leak-read-write-file.test.ts` | ASAN | debian 13 x64-asan | | `test/js/web/streams/streams-leak.test.ts` | ASAN | debian 13 x64-asan | | `test/js/node/test/parallel/test-net-server-listen-path.js` | WINDOWS | windows 2019 x64 + 11 aarch64 | | `test/js/node/test/parallel/test-net-pipe-connect-errors.js` | WINDOWS | fixed in #36786 | | `test/js/node/test/parallel/test-net-client-bind-twice.js` | WINDOWS | fixed in #36786 | | `test/js/node/test/parallel/test-net-server-reset.js` | WINDOWS | fixed in #36786 | | `test/js/bun/io/fetch/fetch-abort-slow-connect.test.ts` | DARWIN | darwin 26 aarch64 + 14 x64 | ### Caveats - `test-tls-connect-memleak.js` and `fetch-abort-slow-connect.test.ts` were `FLAKY` quarantines; both passed in probe build 87834 and confirmation builds 87845 / 87860 / 87868. <!-- robobun:evidence:begin --> --- **no test proof** · iteration 4 · docs-only change; test-proof not applicable <!-- robobun:evidence:end -->
### What does this PR do?
Tightens the GitHub Actions workflows against the credential-theft
patterns behind this year's action supply-chain incidents (persisted
checkout tokens, floating tags, `${{ }}` in shell), and deletes
workflows that have been dead for a year or more — one of which
referenced the compromised `actions-cool/issues-helper@v3`.
| Change | Scope |
|---|---|
| `actions/checkout` → v7.0.1 SHA, `persist-credentials: false` | all 30
checkouts; homebrew tap push now passes its token explicitly |
| top-level `permissions: contents: read` | 27 workflows that had none
(job-level grants unchanged) |
| `${{ inputs/steps/env.* }}` moved from `run:` into `env:` | setup-bun
action, release, vscode-release, update-sqlite3, auto-assign-types,
format/clippy/miri |
| third-party actions dropped | `JS-DevTools/npm-publish` → `npm
publish`; `git-auto-commit-action` → plain git; `oven-sh/setup-bun`
v1/v2 → in-repo `setup-bun` |
| triage jobs | `anthropics/claude-code-action/base-action` v1.0.183
(old mirror stopped tagging), model `claude-opus-5` |
| Bun used by CI jobs | 1.3.14 |
| `release.yml` `is-latest` | read boolean `inputs.is-latest`;
`github.event.inputs.is-latest` is the string `"false"` (truthy), so
manual dispatches always published as latest |
| new `dependabot.yml` | github-actions, weekly, 7-day cooldown |
| Deleted | Why |
|---|---|
| `labeled.yml.disabled` | disabled Nov 2025; used
`actions-cool/issues-helper@v3` (compromised May 2026 — never ran here)
|
| `comment-lint.yml.disabled` | disabled Nov 2025; listened for a
workflow that no longer exists |
| `update-root-certs.yml` | disabled in the Actions UI since Aug 2025;
every run was `startup_failure` |
| `test-bump.yml`, `actions/bump`, release `bump` job |
`scripts/bump.ts` was removed in 2024 |
### How did you verify your code works?
`zizmor .github`: 54 → 5 findings; the remainder are three
`pull_request_target` workflows that never check out PR code and a
`GITHUB_PATH` append in setup-bun. All YAML parses. PR-triggered
workflows (lint/format/clippy/bun-types/packages-ci) exercise the
checkout/permissions/setup-bun changes on this PR.
Not exercisable until the next release: the `npm-types` publish step and
the homebrew tap push in `release.yml`.
Upgrades the WebKit fork to upstream WebKit/WebKit@3722912ff800 (2026-08-02) via oven-sh/WebKit#383. oven-sh/WebKit#383 is merged; `WEBKIT_VERSION` points at the merge commit `e6e37cda216c0292ae68c30c84a9dc8601d0fba5`. ## Bun-side changes Upstream `90b2ecf79ae3` keys `m_loadedModules` on `(specifier, ScriptFetchParameters::Type)` and threads the type through `ImportEntry`, `ExportEntry`, `StarExportEntry`, and `ModuleAnalyzer::appendRequestedModule`. Under `bun test --isolate` (the `BunTranspiledModule` path), Bun'''s synthesized record must agree with what JSC'''s own `ModuleAnalyzer` would produce from the printed source; a mismatch fails the BUN_DEBUG record diff in debug and null-derefs `hostResolveImportedModule` in release. - `analyze_transpiled_module` / `js_printer`: every `RecordKind` now carries one trailing `FetchParameters` slot. `add_import_info_*` and `add_export_info_*` accept it; `finalize()` propagates the source import'''s slot through the Local->Indirect conversion. `RequestedModules` dedupes on `(specifier, Type, phase)`. - `analyze_jsc.rs`: decodes the trailing slot to the JSC `Type` enum and passes it to every `addImportEntry*` / `addIndirectExport` / `addNamespaceExport` / `addStarExport`; buffer validation is per-slot so only the trailing slot accepts the wider `FetchParameters` sentinel range. - `BunAnalyzeTranspiledModule.cpp`: all seven `addImportEntry*` / `add*Export` functions take `uint8_t moduleRequestType` and set `.moduleRequestType` explicitly; `dumpRecordInfo()` prints `type(N)` for import/export/star entries; `static_assert` pins the ordinal values `to_script_fetch_parameters_type()` hardcodes. - `RuntimeTranspilerCache` `EXPECTED_VERSION` -> 25 (esm_record layout change). ## WebKit-side changes (oven-sh/WebKit#383) - 17 merge conflicts resolved in JSC/WTF; fork's `USE(BUN_JSC_ADDITIONS)` hunks preserved. - Fork `ffi/` code adapted to upstream's 32-bit removal (`USE_JSVALUE64` macro deleted, `is64Bit()` removed, `payloadFor` -> `lowWordFor`). - `InspectorDebuggerAgent.cpp`: handle `BunTranspiledModule`/`Synthetic` in new `scriptTypeForScript` switch. - Recorded `01aaa3e0be0c` as ancestor via `-s ours` (oven-sh/WebKit#352 was a squash merge). ## How did you verify your code works? - `bun run jsc:build:debug` builds and the `jsc` shell runs (`-e 'print(42)'` -> `42`). - `bun run build:local -p '42'` links and runs against the local merged WebKit. - oven-sh/WebKit#383 preview build green on all 38 platform variants. - `bun bd -p 'process.versions.webkit'` -> `preview-pr-383-b9ea4dc5`. - New test `test/js/bun/jsc/webkit-upgrade-3722912f.test.ts`: 4/4 pass with `bun bd test`, 3/4 fail with `USE_SYSTEM_BUN=1 bun test` (old JSC lacks `Iterator.prototype.includes`, returns `""` for cyclic join, `WebAssembly.Exception.length === 1`). ## JavaScriptCore/WTF/bmalloc changes since 01aaa3e0be0c (Jul 25) Upstream range: WebKit/WebKit@01aaa3e0be0c...3722912ff800 (474 commits total, 110 touching JSC/WTF/bmalloc, Jul 25 → Aug 2 2026). ### Highlights - `29ceb3c03de3` Remove 32-bit JSValues (JSVALUE32_64 and `CPU(NEEDS_ALIGNED_ACCESS)` deleted). - `857bd4334690` Remove ARMv7 JIT support (ARMv7 is CLoop-only now; drops remaining 32-bit/x86 JIT refs). - `232cebabc1f3` Remove big-endian support and platforms without unaligned loads/stores (WTF + JSC). - `6eaa5ac1f65d` Remove 32-bit libpas support. - `bfb1b1183bc2` Remove the B3/Air graph-coloring register allocator (greedy is the only allocator now). - `0d0080ea539d` Enable WebAssembly Memory64 by default (+ Table64/SIMD follow-ups). - `f2f2c2ddf637` Remove `StringRecursionChecker`; cyclic `toString`/`join` now throws RangeError via stack check (spec-correct). - `319f94b3db4a` Enable `Iterator.prototype.includes()` by default. - `90b2ecf79ae3` `hostResolveImportedModule` now honors import-attribute `type` (module loader behavior change). - `f5716f6401ed` Add `preserve_most` calling convention to fastMalloc APIs on ARM64 (perf + ABI of WTF alloc entry points). ### JavaScriptCore **Runtime / builtins** - `f2f2c2ddf637` Remove StringRecursionChecker; rely on stack-overflow checks. - `cd91e7f128dd` Add fast flag for `ToPrimitive(Object)` calls in runtime. - `173a0bd6d937` Use `defaultToPrimitiveFastAndNonObservable` in `JSObject::toString`. - `960adeccefcd` Fast operation for `Array#shift`. - `92c6650c7947` Add `JSArrayIterator::next` C++ helper. - `cb1c48b3e95f` Extend `Array.from()` Set fast path to `set.keys()/.values()`. - `4a2e724d6789` Fix: `Array.from(set.keys()/.values())` fast path ignored `Symbol.iterator` overrides. - `73e4c589c1e6` Extend `StringSplitCache` to RegExp separators. - `656d3c36830f` / `1d355c27ea88` 8-byte SWAR fast paths in `JSON.stringify` string copy (same-type & upconvert). - `9f9370cc729f` Fix JSON.stringify regression around `toJSON` check. - `2eb77e9c9473` BigInt: implement Crandall reduction. - `39b1bb9cfc85` BigInt: deploy Comba multiplication more broadly. - `319f94b3db4a` Enable `Iterator.prototype.includes()`. - `0731b27c1b60` `Iterator.zip`: use null-prototype objects for options/underlying iterator. - `90b2ecf79ae3` `hostResolveImportedModule` respects module request import-attribute `type`. - `4f54300b848a` Collect diagnostics when `getDirect` returns zero JSValue in `llint_slow_path_get_by_id`. **Parser / bytecompiler** - `c2beca7a439f` Lexer: scan integer tokens in a single pass. - `72ea806faa21` Use overflow-safe range when choosing a switch jump table. **LLInt / DFG / FTL / B3** - `29ceb3c03de3` Remove 32-bit JSValues. - `857bd4334690` Remove ARMv7 JIT support. - `bfb1b1183bc2` Remove B3/Air graph-coloring register allocator. - `68cde6ba2ad3` Make IRO (Air register allocation) faster. - `83540481435f` DFG: allocate `BasicBlock::intersectionOfPastValuesAtHead` only for OSR-entry targets. - `1566615170ec` DFG fix: `EnumeratorNextUpdateIndexAndMode` must require original array structure for `InBoundsSaneChain`. - `e759fa9dd063` LLInt: inline hot path of `op_enter`. - `9610c2113b45` offlineasm: emit ARM64 register-offset addressing for BaseIndex operands. - `4ebed2479144` Speed up `addSortedRange` via binary search. - `1c006b0b0f62` Fix regex `setLastIndex` on 32-bit. **WebAssembly** - `0d0080ea539d` Enable Memory64 feature flag. - `15aa6fad53e3` Memory64: SIMD support. - `bf0425598904` Memory64: expand declared memory limits. - `862994e2cc37` Memory64: validate table import address-type match. - `184ee4c654bd` Memory64: Table64 in OMG tier. - `d202bedc5ff6` Memory64: Table64 in BBQ tier. - `bf6512f84f7d` Support `WebAssembly.Exception` `options.traceStack` (+ `stack` getter, ctor length = 2). - `24527bbb9ac8` Optimize Wasm JITCallee publication (lock splitting, icache barrier rework). - `1803d6109d98` Speed up `WebAssembly.Table` construction. - `d434a41411a3` BBQ: optimize `br_table` for consecutive same-target runs. - `51d3dbaa278e` IPInt: add `DEFINE_IPINT_THUNK_FOR_ENTRY`. - `244cd98f7986` Fix `generateWasmOpsHeader.py` under non-UTF-8 locales. **Yarr / RegExp** - `581f1d958329` Start end-anchored fixed-size regexps at the only possible position. - `a458a6c1f0a7` v-mode class-set op loop: stop early when no more output possible. - `7c5dbbcba110` Extend `ParenthesesSubpatternTerminal`. - `e1def8f4e5fd` Don't save sibling/ancestor-sibling frame slots for `ParenContext`. - `1e43057f135a` Extend first-character filter further. - `54916608d7d6` Fix non-BMP advance latch; simplify `tryReadUnicodeCharImpl`. - `46a4b17efbe9` `optimizeBOL`: don't filter contents of negative lookaheads. - `b128ddd863ab` Fix dot-star-wrapped optimization for sticky patterns. - `98d0367d2247` Fix: `^` inside a paren that can match empty does not anchor the pattern. **Intl / Temporal** - `09917ef55b0f` Add missing `U_FAILURE(status)` check in `actualLunisolarMonthLength`. **Inspector** - `3722912ff800` / `7be5445e4a22` / `e8c97076834e` / `f3e34dde7c9d` Canvas: instrument & record WebGPU devices/pipelines. - `301d6b2b21f7` Associate WebAssembly module scripts with the fetching resource. - `28b979b1659b` / `479edb2e4395` Site Isolation: implement `Network.loadResource` / `Network.getSerializedCertificate`. ### WTF - `232cebabc1f3` Remove big-endian support and `CPU(NEEDS_ALIGNED_ACCESS)`. - `e5fa5c604438` Upgrade fast_float to 8.2.10. - `a288a8ec809b` Widen `find16`/`find32` SIMD threshold; faster ASCII case-conversion prefix copy. - `6cb1077d85c8` `makeStringByReplacingAll()` now uses SIMD-accelerated `find()`. - `5590f2e70615` Fix `AdaptiveStringSearcher` good-suffix shift table off-by-one. - `f5716f6401ed` Add `preserve_most` to most fastMalloc APIs on ARM64. - `f7a9d16e1531` Add `removeIf()` to `WeakHashSet` / `WeakListHashSet`. - `e1fc460b8f1d` Add `removeIf()` to `RobinHoodHashTable`. - `ff1f31c83dc7` Treat creating/destroying a `CheckedPtr` as no-delete. - `bf15f00ebe95` Remove 12 unused internal-linkage templates (TypeTraits/HashTable/Vector/etc.). - `5b84cf3719fa` Use `__builtin_trap` instead of inline asm under clang static analyzer. - `158f737725b7` Add helpers for Darwin temp/cache directories. - `04e3d47960f3` Limit URL size at IPC boundary (Chrome/Blink parity). - `5daad377031c` / `a7ea27dd3bb6` Enable `-Wthread-safety` on GTK/WPE and fix findings. - `7eb640d408f8` CMake: merge Mac and iOS ports into "Cocoa". - `cfb222f3c4d1` CMake: run `cleandead` at end of configuration. ### bmalloc - `6eaa5ac1f65d` Remove 32-bit libpas support. - `f5716f6401ed` `preserve_most` on fastMalloc APIs (ARM64). - `8b8b3e5ee16c` Fix inverted `MADV_ZERO` support latch in `VMAllocate.cpp`. - `ead6285911f6` libpas: `pas_thread_local_cache_for_all` clobbered its should-go-again result. - `90cbe5e85528` libpas: fix benign read from a deallocated TLC. - `e388877954d1` PGM allocator: fix uninitialized `free_status` misclassifying OOB as UAF. - `05c83a6550b7` libpas: fix `MTE_overrideEnablementForJavaScriptCore=true` incorrectly disabling MTE. - `f01297663d40` / `86fb5e3a4eef` / `d18773ec666e` libpas test coverage (scavenging / zeroing / paged-out pages). ### Breaking/notable for Bun - **32-bit purge**: `29ceb3c03de3` (JSVALUE32_64 removed), `857bd4334690` (ARMv7 JIT removed), `6eaa5ac1f65d` (32-bit libpas removed), `232cebabc1f3` (big-endian + `CPU(NEEDS_ALIGNED_ACCESS)` removed). Any `#if USE(JSVALUE64)` / `CPU(ADDRESS32)` guards in Bun patches are now dead. - **`VM` layout**: `f2f2c2ddf637` deletes `StringRecursionChecker.{h,cpp}` and the `stringRecursionCheck*` fields from `VM.h`. Cyclic `Array.prototype.join`/`toString` now throws `RangeError` instead of returning `""`. - **Module loader**: `90b2ecf79ae3` changes `hostResolveImportedModule` to propagate the import-attribute `type` — check Bun's module loader hook signatures. - **Register allocator**: `bfb1b1183bc2` removes the B3/Air graph-coloring allocator and its `Options::` toggle. - **fastMalloc ABI (ARM64)**: `f5716f6401ed` adds `__attribute__((preserve_most))` to `fastMalloc`/`fastFree` etc. — affects anything calling these across the WTF boundary on arm64. - **BuiltinNames**: `bf6512f84f7d` registers `stackPrivateName` as private-only; `WebAssembly.Exception` constructor `length` becomes 2 and gains a `stack` prototype getter. - **Feature defaults**: `0d0080ea539d` Wasm Memory64 on by default; `319f94b3db4a` `Iterator.prototype.includes` on by default. - **Wasm threading**: `24527bbb9ac8` reworks icache barrier / callee publication and adds `Thread::barrierInstructionCache()` in WTF. <!-- robobun:evidence:begin --> --- **[decide:webkit]** gate passed · iteration 2 · 8 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: BUILD FAILED (no junit output) $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" "test/js/bun/jsc/webkit-upgrade-3722912f.test.ts" ninja: Entering directory `/workspace/bun/build/debug' [1/182] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/182] gen cpp.rs (cppbind) [3/182] gen JSSink.{cpp,h,lut.h,rs} generated_jssink.rs: 7 sinks, 84 exported symbols Generating /workspace/bun/build/debug/codegen/JSSink.lut.h from /workspace/bun/build/debug/codegen/JSSink.lut.txt [4/182] gen JS modules (bundle-modules) Preprocess modules (8715ms) Bundle modules (63ms) Postprocesss modules (24ms) Bundle Functions (746ms) Generate Code (12ms) [9.57s] Bundled "src/js" for development 2749 kb 193 internal modules 13 native modules 90 internal functions across 19 files [4/181] 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) [177/181] cxx obj/unified/UnifiedSource-src_jsc_bindings-0.cpp.o FAILED: obj/unified/UnifiedSou ... (truncated) release without fix: all passed bun test v1.4.0-canary.1 (385f528) test/js/bun/jsc/webkit-upgrade-3722912f.test.ts: (pass) WebKit 3722912ff800 upgrade > Iterator.prototype.includes is enabled by default (319f94b3db4a) [0.19ms] (pass) WebKit 3722912ff800 upgrade > cyclic Array.prototype.join throws RangeError (f2f2c2ddf637) [2.51ms] (pass) WebKit 3722912ff800 upgrade > WebAssembly.Exception gains options.traceStack and stack getter (bf6512f84f7d) [0.48ms] (pass) WebKit 3722912ff800 upgrade > typed import attributes resolve through BunTranspiledModule (--isolate) (90b2ecf79ae3) [8.49ms] (pass) WebKit 3722912ff800 upgrade > indirect, namespace and star re-exports link on the JSC ModuleAnalyzer path (90b2ecf79ae3) [22.09ms] 5 pass 0 fail 12 expect() calls Ran 5 tests across 1 file. [152.00ms] __F:0:S:0 ``` </details> <details><summary>passes on PR (with fix)</summary> ```console 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/jsc/webkit-upgrade-3722912f.test.ts" bun test v1.4.0 (59b0de0) test/js/bun/jsc/webkit-upgrade-3722912f.test.ts: (pass) WebKit 3722912ff800 upgrade > Iterator.prototype.includes is enabled by default (319f94b3db4a) [13.19ms] (pass) WebKit 3722912ff800 upgrade > cyclic Array.prototype.join throws RangeError (f2f2c2ddf637) [10.75ms] (pass) WebKit 3722912ff800 upgrade > WebAssembly.Exception gains options.traceStack and stack getter (bf6512f84f7d) [3.66ms] (pass) WebKit 3722912ff800 upgrade > typed import attributes resolve through BunTranspiledModule (--isolate) (90b2ecf79ae3) [317.28ms] (pass) WebKit 3722912ff800 upgrade > indirect, namespace and star re-exports link on the JSC ModuleAnalyzer path (90b2ecf79ae3) [1138.48ms] 5 pass 0 fail 12 expect() calls Ran 5 tests across 1 file. [3.17s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 676ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/140] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/140] gen cpp.rs (cppbind) [3/140] gen JSSink.{cpp,h,lut.h,rs} generated_jssink.rs: 7 sinks, 84 exported symbols Generating /workspace/bun/build/release/codegen/JSSink.lut.h from /workspace/bun/build/release/codegen/JSSink.lut.txt [4/140] gen JS modules (bundle-modules) Preprocess modules (8727ms) Bundle modules (51ms) Postprocesss modules (106ms) Bundle Functions (687ms) Generate Code (20ms) [9.61s] Bundled "src/js" for production 2559 kb 193 internal modules 13 native modules 90 internal functions across 19 files [4/139] 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_core v0.0.0 (/workspace/bun/src/bun_core) ^[[1m^[[92m Compiling^[[0m bun_runtime v0.0.0 (/workspace/bun/src/runtime) ^[[1m^[[92m Compilin ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` scripts/build/deps/webkit.ts | 2 +- src/bundler/analyze_transpiled_module.rs | 38 +++-- src/bundler/linker_context/postProcessJSChunk.rs | 8 +- src/bundler_jsc/analyze_jsc.rs | 135 ++++++++++++---- src/js_printer/lib.rs | 191 +++++++++++++++++------ src/jsc/RuntimeTranspilerCache.rs | 5 +- src/jsc/bindings/BunAnalyzeTranspiledModule.cpp | 39 +++-- test/js/bun/jsc/webkit-upgrade-3722912f.test.ts | 106 +++++++++++++ 8 files changed, 416 insertions(+), 108 deletions(-) ``` </details> **gate history** · 5 passed · 1 rejected · iteration 2 <details><summary>evidence per changed file</summary> ``` file reads edits tests scripts/build/deps/webkit.ts 1 1 0 src/bundler/analyze_transpiled_module.rs 3 3 0 src/bundler/linker_context/postProcessJSChunk.rs 1 1 0 src/bundler_jsc/analyze_jsc.rs 13 15 0 src/js_printer/lib.rs 9 16 0 src/jsc/RuntimeTranspilerCache.rs 2 3 0 src/jsc/bindings/BunAnalyzeTranspiledModule.cpp 8 12 0 test/js/bun/jsc/webkit-upgrade-3722912f.test.ts 2 5 0 ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
### What does this PR do?
Removes every function-long `let x = unsafe { &mut *<callback param> };`
reborrow from the Rust codebase (~370 sites under any binding name,
census now 0) and adds a source lint
(`test/internal/source-lints/fn-long-mut-reborrow.test.ts`, runs on
every Rust PR) so the shape cannot come back. The line asserts exclusive
access for the whole function on objects whose callbacks re-enter them
through their own accessors — Stacked/Tree Borrows UB and an LLVM
`noalias` miscompile hazard. Where it was covering real re-entrancy, the
type is fixed instead of the call site:
| Fix shape | Applied to |
|---|---|
| `&self`-only API over `Cell`/`JsCell` state | `SendQueue`,
`RequestContext`, `FileResponseStream`, `MultiPartUpload`,
`UpgradedDuplex`, `SSLWrapper`, `WindowsNamedPipe`, `PathWatcher`,
`Channel`, GC controller |
| Raw `*mut Self` entries (callback may free the object; dispatch runs
with no receiver protector) | `PipeReader`
`read`/`on_poll`/`done`/`on_error`/`register_poll`/`stop_for_max_buffer`,
`DuplexUpgradeContext` `on_*`, `ProxyTunnel::shutdown` |
| Allocation-root pointer (`root: Cell<*mut Self>`) so pool/heap release
never deallocates through a `&self`-derived tag | `SendQueue`,
`Channel`, `WindowsNamedPipe`, `RequestContext` (request-pool `put`) |
| Move ownership out before dispatch instead of aliasing across
re-entrant JS | `MultiPartUpload` full-buffer part transfer |
Supporting changes: `BackRef`/`ParentRef` gain a `Shared`/`Mut`
write-provenance marker (writing through a `&T`-born backref is now a
type error); `bun_event_loop::AnyTask` (`*mut c_void` + fn pointer) is
deleted for typed `Taskable` tags; the `ErasedJsError` re-export alias
is deleted; the cron register/remove job scaffolding is deduplicated via
`CronJobBase` defaults.
Deliberate behavior changes: IPC decode loops no longer hold a buffer
borrow across user JS; subprocess buffered output is no longer
re-delivered after a streaming drain (fixes an output-doubling bug this
PR introduced mid-flight and CI caught); the test coordinator no longer
drops a `Channel`/`Process` from inside that object's own callback.
Out of scope, tracked follow-ups: `uv::StreamReader`'s `&mut Self`
methods, `PipeWriter`'s `LaunderedSelf` impls, the remaining `&mut self`
wrappers on `PosixBufferedReader` (`close`/`finish`/`start`/`watch`),
and the test coordinator's `ChannelOwner::on_channel_frame` chain
(`on_frame`/`assign_work` under a protected `&mut Worker`; `bail_out`'s
comment documents the reachability) — all pre-existing parent-chain
receivers, documented at the sites.
### How did you verify your code works?
`bun run rust:check-all` (10/10 targets incl. Windows), `bun run
rust:clippy` (clean), `bun run rust:miri -p bun_ptr` (18/18), the new
source lint at census 0. Debug build driven end to end: IPC round trips
(20k messages + disconnect), `Bun.serve`
bodies/`Bun.file`/streaming/abort/burst (104-test serve-file suite),
TLS-over-duplex (62 tests incl. close-UAF), S3 multipart, subprocess
`maxBuffer` overflow, cron register/validate, server
finalize-during-shutdown.
Benchmarked CI builds of this head vs its `main` merge-base (same
WebKit/mimalloc) on a 64-core Linux box: HTTP hello/echo, IPC, spawn, fs
streams, startup all within the measured ±1% noise floor;
anonymous/private-dirty RSS identical (3,444 kB both arms).
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Split from #36801 (1/5). ## What When the threadsafe function was created with `func == NULL`, pass `js_callback = NULL` to `call_js_cb` instead of an encoded `undefined`. ## Why (fixes #13771, closes #30543) Node's `DispatchOne` ([src/node_api.cc#L452-L462](https://github.com/nodejs/node/blob/v26.x/src/node_api.cc#L452-L462)): ```cpp napi_value js_callback = nullptr; if (!ref.IsEmpty()) { v8::Local<v8::Function> js_cb = v8::Local<v8::Function>::New(env->isolate, ref); js_callback = v8impl::JsValueFromV8LocalValue(js_cb); } env->CallbackIntoModule<false>( [&](napi_env env) { call_js_cb(env, js_callback, context, data); }); ``` When `ref` is empty (the caller passed `func = NULL` to `napi_create_threadsafe_function`), `js_callback` stays `nullptr`. Bun was passing `napi_value::create(env, cb_js.get().unwrap_or(JSValue::UNDEFINED))`, which is the encoded `undefined` value (a non-zero pointer). Addons such as napi-rs and lightningcss test `if (js_callback != NULL)` and, seeing a non-null value, try to `napi_call_function` it: ``` thread '<unnamed>' panicked at napi/src/threadsafe_function.rs:235:57: called `Result::unwrap()` on an `Err` value: Error { status: InvalidArg, reason: "expect Function, got: Undefined" } ``` ## Verification New `checkSameOutput` test fails under the released Bun: ``` $ USE_SYSTEM_BUN=1 bun test test/napi/napi.test.ts -t "passes NULL js_callback" - "js_callback == NULL: 1 + "js_callback == NULL: 0 ``` The `kMaxIterationCount` cap that was in the first revision of this PR has been dropped: no issue has been filed about the unbounded dispatch loop, and the cap as written did not yield to setImmediate/timers anyway (`enqueue_task_concurrent` is drained inside the same `tick()`). <!-- robobun:evidence:begin --> --- **no test proof** · iteration 3 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/napi/napi.test.ts <!-- robobun:evidence:end -->
### What does this PR do? Fixes #36832. `Bun.build({ splitting: true })` on a tree with chained `sideEffects: false` barrel packages (e.g. `@sentry/node-core` re-exporting `@sentry/core`) produced non-deterministic output, and in the unlucky parse order silently dropped modules: the build reported `success: true` while the emitted chunks referenced symbols no chunk declares, failing at boot with ``` SyntaxError: Exported binding 'tK' needs to refer to a top-level declared variable. ``` ### Cause Barrel optimization defers a barrel's unused re-export records at parse time and un-defers them later as requests for the names arrive (`scheduleBarrelDeferredImports`). Two of those request paths depended on parse-completion order: 1. A namespace request (`import * as ns from 'pkg-a'`) arriving after the barrel was parsed only un-deferred the barrel's own records. The names the barrel re-exports from an inner barrel (`export { beta } from 'pkg-b'`) were never requested from `pkg-b` when `pkg-b` had already been parsed with a partial request set. `pkg-b`'s records stayed deferred, the module bodies were dropped from the output, and the namespace object still referenced their symbols. This is the dropped-module case in the issue: the same 31 `@sentry/core` modules missing, 18 orphaned minified bindings. 2. `export * from` targets are meant to be exempt from deferral (the `IS_EXPORT_STAR_TARGET` check in `applyBarrelOptimization`), but the flag only lands if the star exporter parses before the target. This is what flapped the chunk graph (25 vs 32 chunks for identical input). ### Fix In `src/bundler/barrel_imports.rs`: - The BFS star branch now resolves un-deferred records inline and propagates the request onward: each re-exported name is requested from the module it comes from (by its original alias); namespace re-exports and `export *` targets are requested as full-namespace. - Star items mark a barrel as fully requested at most once, so the new propagation terminates on `export *` cycles. - `export * from` targets are recorded as fully requested when the star exporter is processed, making the deferral decision independent of parse order (same outcome the `IS_EXPORT_STAR_TARGET` flag produces when the exporter happens to parse first). ### Verification On the reporter's repro (https://github.com/Karavil/bun-splitting-orphaned-exports), the unfixed build produced three distinct outputs across runs (25-chunk good, 32-chunk variant, 25-chunk bad with 12 unlinkable chunks, ~2-4% of runs on a 16-core box). With the fix, output is byte-identical across every run, all chunks link when re-bundled individually, and the bundle boots. New test `barrel/NamespaceImportUndefersChainedBarrels` reconstructs the bad parse order deterministically: the file holding `import * as ns` is large enough that the small barrel files always parse (and defer) first. It fails on the unfixed build every time (module body missing from output, boot fails) and passes with the fix. Existing barrel, splitting, and edgecase bundler suites pass. <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 1 · 2 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: BUILD FAILED (no junit output) $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/bundler_barrel.test.ts ninja: Entering directory `/workspace/bun/build/debug' [1/8] gen cpp.rs (cppbind) [2/8] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/8] 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) [3/8] cxx obj/unified/UnifiedSource-src_jsc_bindings-0.cpp.o FAILED: obj/unified/UnifiedSource-src_jsc_bindings-0.cpp.o /usr/bin/ccache /usr/lib/llvm-21/bin/clang++ -march=nehalem -O0 -g3 -gz=zstd -glldb -fsanitize=address -fno-exceptions -fno-c++-static-destructors -fno-rtti -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -fvisibility=hidden -fvisibility-inlines-hidden -fno-unwind-tables -fno-asynchronous-unwind-tables -Wno-c23-extensions -ffunction-sections -fdata-sections -faddrsig -fno-semantic-interposition -fno-delete-null-pointer-checks -fdiagnostics-color=always -ferror-limit=100 -std=gnu++23 ... (truncated) release without fix: all passed bun test v1.4.0-canary.1 (5c9b728) test/bundler/bundler_barrel.test.ts: (pass) bundler > barrel/SkipUnusedWithOptimizeImports [13.95ms] (pass) bundler > barrel/AllExportsNeeded [3.87ms] (pass) bundler > barrel/SkipUnusedWithSideEffectsFalse [4.67ms] (pass) bundler > barrel/NoOptimizationWithoutSideEffects [3.30ms] (pass) bundler > barrel/ExportStarLoadsAll [2.62ms] (pass) bundler > barrel/NonBarrelWithLocalExports [2.60ms] (pass) bundler > barrel/NamespaceImportLoadsAll [2.71ms] (pass) bundler > barrel/OutputEquivalence [3.66ms] (pass) bundler > barrel/DefaultReExport [3.58ms] (pass) bundler > barrel/ImportThenExport [3.44ms] (pass) bundler > barrel/ReExportChain [3.52ms] (pass) bundler > barrel/StarWithNamedFromSameSource [2.46ms] (pass) bundler > barrel/SideEffectOnlyImport [3.32ms] (pass) bundler > barrel/MultipleImporters [3.47ms] (pass) bundler > barrel/CircularExports [11.45ms] (pass) bundler > barrel/CircularStarExports [12.35ms] (pass) bundler > barrel/NamespaceReExportCycleThroughStarTarget [13.21ms] (pass) bundler > barrel/SelfReExport [6.58ms] (pass) bundler > barrel/DynamicImportInSubmodule [4.67ms] (pass) bundler > barrel/DynamicImportWithStaticImpor ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console 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/bundler/bundler_barrel.test.ts bun test v1.4.0 (2d34dde) test/bundler/bundler_barrel.test.ts: (pass) bundler > barrel/SkipUnusedWithOptimizeImports [602.47ms] (pass) bundler > barrel/AllExportsNeeded [118.09ms] (pass) bundler > barrel/SkipUnusedWithSideEffectsFalse [91.17ms] (pass) bundler > barrel/NoOptimizationWithoutSideEffects [99.23ms] (pass) bundler > barrel/ExportStarLoadsAll [84.00ms] (pass) bundler > barrel/NonBarrelWithLocalExports [89.63ms] (pass) bundler > barrel/NamespaceImportLoadsAll [87.82ms] (pass) bundler > barrel/OutputEquivalence [109.01ms] (pass) bundler > barrel/DefaultReExport [116.58ms] (pass) bundler > barrel/ImportThenExport [111.44ms] (pass) bundler > barrel/ReExportChain [94.02ms] (pass) bundler > barrel/StarWithNamedFromSameSource [86.77ms] (pass) bundler > barrel/SideEffectOnlyImport [111.60ms] (pass) bundler > barrel/MultipleImporters [99.50ms] (pass) bundler > barrel/CircularExports [372.92ms] (pass) bundler > barrel/CircularStarExports [401.76ms] (pass) bundler > barrel/NamespaceReExportCycleThr ... (truncated) release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 681ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/9] gen cpp.rs (cppbind) [2/9] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/9] 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_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp) �[1m�[92m ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` src/bundler/barrel_imports.rs | 162 ++++++++++++++++++++++++++++++------ test/bundler/bundler_barrel.test.ts | 112 +++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 24 deletions(-) ``` </details> **gate history** · 4 passed · 0 rejected · iteration 1 <details><summary>evidence per changed file</summary> ``` file reads edits tests src/bundler/barrel_imports.rs 10 34 0 test/bundler/bundler_barrel.test.ts 2 4 0 ``` </details> **root cause** · written by the author bot The bundler's barrel import deferral failed to propagate namespace requests through chained barrel re-exports, and `export * from` targets were only marked fully requested when the exporter parsed before the target, making module inclusion dependent on the non-deterministic parallel parse order. When an unfavorable order occurred, deferred modules were never scheduled and were silently dropped from the output while other chunks still referenced their minified exports. The fix propagates full-namespace and aliased requests through both named and star re-exports during the BFS and seeds expor… <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…#36833) Net -285 LOC (349 deletions, 64 insertions including the source-lint test). Each symbol was verified with `rg -w <symbol> src/ build/debug/codegen/` to have zero references outside its own definition, then confirmed by a full `bun bd` build and `bun run rust:check-all` across all 10 targets. ### `src/platform/darwin.rs` (whole file, 210 LOC) The entire file duplicates `bun_sys::darwin`: the `OSLog`/`Signpost`/`Interval` API and the `nocancel` extern block have zero callers. `bun_perf` (the only signpost consumer) imports `bun_sys::darwin::OSLog` and `bun_sys::darwin::os_log::signpost::*`. `bun_platform` is force-linked only for `linux.rs`'s `#[no_mangle]` export. Also drops the now-unused `bun_opaque`/`strum` deps from `bun_platform/Cargo.toml` and the stale doc comment in `src/sys/lib.rs` that pointed here. ### webcrypto C++ - `CryptoKeyHMAC::create` + const-ref ctor, `CryptoKeyAES::create` + const-ref ctor: never called; `generate`/`importRaw`/`importJwk` all construct via the rvalue ctor directly. - `JSCryptoKey::fromJS`: declared, never defined, never called. - `JSSubtleCrypto::toWrapped`: only reachable via `convert<IDLInterface<SubtleCrypto>>`; no such call exists. - `OpenSSLCryptoUniquePtr.h`: `X509Ptr`/`BIOPtr` aliases (zero refs) and the `OPENSSL_VERSION_NUMBER >= 0x30000000L` block (BoringSSL defines `0x1010107f`, so it never compiles, and no code references `OsslParamBldPtr`/`OsslParamPtr`/`EVPKDFCtxPtr`/`EVPKDFPtr`). - `CommonCryptoDERUtilities.h`: `extraBytesNeededForEncodedLength` is only called from the same TU; header decl removed, made `static` in the .cpp. - `ScriptExecutionContext.h`: `wrapCryptoKey`/`unwrapCryptoKey` stubs plus the commented-out virtual decls; leftover from the earlier `SerializedCryptoKeyWrap` removal. ### sqlite / NodeVM C++ - `lazy_sqlite3.h`: `sqlite3_column_int` / `sqlite3_memory_used` / `sqlite3_prepare16_v3` typedef+var+define+dlsym lines (code uses `sqlite3_column_int64`, tracks memory via `sqlite_malloc_amount`, uses `sqlite3_prepare_v3`). - `SQLiteSingleton::schema_versions`: never read, never appended to. - `JSStatementSync::allowBareNamedParams`/`allowUnknownNamedParams`/`rowStructure` getters: members are accessed directly. - `DOMIsoSubspaces`/`DOMClientIsoSubspaces::m_*subspaceForJSSQLStatementConstructor`: `JSSQLStatementConstructor` lives in `JSFunction`'s subspace per the `static_assert` in `JSSQLStatement.h`. - `NodeVMGlobalObject::sigintReceived`: not virtual, never called; `SigintWatcher::signalAll` invokes `vm().notifyNeedTermination()` directly. - `NodeVMModuleRequest::specifier`/`importAttributes` getters: `toJS` reads `m_specifier`/`m_importAttributes` directly. ### Rust - `bun_ast::PartTag::{JsxImport, CjsImports, ReactFastRefresh}`: never assigned or compared. - `bun_ast::flags::JSXElement::HasAnyDynamic`: never inserted or tested. - `bun_ast::import_record::Tag::Tailwind`: last variant, never constructed or matched. - `bun_ast::BindingNodeList` type alias + its unused re-export in `bun_js_parser::parser`. - `bun_ast::StoreAstAllocHeap::reset`: callers invoke the free fn `store_ast_alloc_heap::reset()` directly. - `bun_jsc::JSPromise::reject_task`: sibling `resolve_task` has 4 callers, `reject_task` has zero. - `bun_jsc::JSRuntimeType::UNDEFINED`: only `NOTHING` is referenced. ### src/js - `readline.js`: unused `ObjectSetPrototypeOf` primordial destructure. - `repl.js`: unused `ArrayPrototypeSlice` primordial destructure. - `zlib.ts`: collapse redundant `ArrayBufferIsView` intermediate alias. ### Verification - `bun bd` builds clean - `bun run rust:check-all` passes all 10 targets (incl. `aarch64-apple-darwin` for the `bun_platform` change) - Smoke tests pass: `web-crypto.test.ts`, `sqlite.test.js`, `node-sqlite.test.ts`, `zlib.test.js`, `transpiler.test.js` - `test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts` fails on main, passes on this branch ### Followups (not deleted; left for review) - `src/runtime/api/bun/h2/connection.rs:1731-1941` outbound-stream API (`begin_header_block`/`encode_header`/`send_header_block`/`send_data`/`send_push_promise` + transitively `SendWindow::{available,consume}`, `Coder::{take_pending_size_update,encode}`, `write_table_size_update`, ~215 LOC): only called from `#[cfg(test)]`. File carries `#![allow(dead_code)]` and was authored in #31584; likely intentional WIP scaffolding for migrating `h2_frame_parser`'s outbound path. - `src/jsc/bindings/webcrypto/*.idl` (29 files, ~1055 LOC): not processed by any build step (`scripts/glob-sources.ts` globs only `*.cpp`), but #34838 edited them recently so they may be maintained as documentation. - `src/jsc/ErrorCode.rs`: `Zig_ErrorCodeJSErrorObject` `#[no_mangle]` static with zero refs in any `.cpp`/`.h` (sibling `Zig_ErrorCodeParserError` is declared in `headers-handwritten.h`; this one is not). <!-- robobun:evidence:begin --> --- **[review]** gate passed · iteration 0 · 34 files touched <details><summary>fails on main (without fix)</summary> ```console 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/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts bun test v1.4.0 (e2a9bd9) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: 19 | function src(p: string): string { 20 | return readFileSync(path.join(repoRoot, p), "utf8"); 21 | } 22 | 23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => { 24 | expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/); ^ error: expect(received).not.toMatch(expected) Expected substring or pattern: not /pub mod darwin;/ Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", t ... (truncated) release without fix: 3 FAILED bun test v1.4.0-canary.1 (1498d7b) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: 19 | function src(p: string): string { 20 | return readFileSync(path.join(repoRoot, p), "utf8"); 21 | } 22 | 23 | test("bun_platform no longer declares a darwin module (duplicated bun_sys::darwin)", () => { 24 | expect(src("src/platform/lib.rs")).not.toMatch(/pub mod darwin;/); ^ error: expect(received).not.toMatch(expected) Expected substring or pattern: not /pub mod darwin;/ Received: "#![allow(non_snake_case, non_camel_case_types, non_upper_case_globals)]\n#![warn(unused_must_use)]\n//! Per-OS APIs that don't fit in `bun_sys` (signposts, the `sys_epoll_pwait2` export).\n\n// Android is listed alongside Linux so the `#[no_mangle]` C exports\n// (`sys_epoll_pwait2`, …) reach the linker on the `*-linux-android` targets.\n#[cfg(target_os = \"macos\")]\npub mod darwin;\n#[cfg(any(target_os = \"linux\", target_os = \"android\"))]\npub(crate) mod linux;\n" at <anonymous> (/workspace/bun/test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts:24:42) (fail) bun_platform no long ... (truncated) ``` </details> <details><summary>passes on PR (with fix)</summary> ```console 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/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts bun test v1.4.0 (e2a9bd9) test/internal/source-lints/dead-symbols-platform-webcrypto-sqlite-vm.test.ts: (pass) bun_platform no longer declares a darwin module (duplicated bun_sys::darwin) [10.05ms] (pass) dead C++ symbols in webcrypto/sqlite/NodeVM do not reappear [23.34ms] (pass) dead Rust symbols in ast/jsc do not reappear [14.33ms] 3 pass 0 fail 3 expect() calls Ran 3 tests across 1 file. [2.09s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 733ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [1/139] gen generated_host_exports.rs generated_host_exports.rs: 94 exports (host=3, lazy=10, generic=81, rust=0); 238 extern-C blocks audited [2/139] gen cpp.rs (cppbind) [3/139] gen JS modules (bundle-modules) Preprocess modules (9279ms) Bundle modules (59ms) Postprocesss modules (259ms) Bundle Functions (943ms) Generate Code (32ms) [10.59s] Bundled "src/js" for production 2558 kb 193 internal modules 13 native modules 90 internal functions across 19 files [3/138] 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_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1 ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` Cargo.lock | 2 - src/ast/import_record.rs | 2 - src/ast/lib.rs | 7 +- src/ast/nodes.rs | 6 +- src/js/node/readline.js | 1 - src/js/node/repl.js | 1 - src/js/node/zlib.ts | 3 +- src/js_parser/parser.rs | 4 +- src/jsc/JSPromise.rs | 12 -- src/jsc/JSRuntimeType.rs | 1 - src/jsc/bindings/NodeVM.cpp | 5 - src/jsc/bindings/NodeVM.h | 1 - src/jsc/bindings/NodeVMModule.h | 3 - src/jsc/bindings/ScriptExecutionContext.h | 16 -- src/jsc/bindings/sqlite/JSSQLStatement.cpp | 2 - src/jsc/bindings/sqlite/NodeSqlite.h | 3 - src/jsc/bindings/sqlite/lazy_sqlite3.h | 18 -- src/jsc/bindings/webcore/DOMClientIsoSubspaces.h | 1 - src/jsc/bindings/webcore/DOMIsoSubspaces.h | 1 - .../webcrypto/CommonCryptoDERUtilities.cpp | 2 +- .../bindings/webcrypto/CommonCryptoDERUtilities.h | 1 - src/jsc/bindings/webcrypto/CryptoKeyAES.cpp | 7 - src/jsc/bindings/webcrypto/CryptoKeyAES.h | 5 - src/jsc/bindings/webcrypto/CryptoKeyHMAC.cpp | 7 - src/jsc/bindings/webcrypto/CryptoKeyHMAC.h | 6 - src/jsc/bindings/webcrypto/JSCryptoKey.h | 2 - src/jsc/bindings/webcrypto/JSSubtleCrypto.cpp | 7 - src/jsc/bindings/webcrypto/JSSubtleCrypto.h | 1 - .../bindings/webcrypto/OpenSSLCryptoUniquePtr.h | 13 -- src/platform/Cargo.toml | 2 - src/platform/darwin.rs | 210 --------------------- src/platform/lib.rs | 4 +- src/sys/lib.rs ... (truncated) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests Cargo.lock 0 0 0 src/ast/import_record.rs 1 1 0 src/ast/lib.rs 3 3 0 src/ast/nodes.rs 2 2 0 src/js/node/readline.js 1 1 0 src/js/node/repl.js 1 1 0 src/js/node/zlib.ts 1 1 0 src/js_parser/parser.rs 1 1 0 src/jsc/JSPromise.rs 1 1 0 src/jsc/JSRuntimeType.rs 1 1 0 src/jsc/bindings/NodeVM.cpp 1 1 0 src/jsc/bindings/NodeVM.h 1 1 0 src/jsc/bindings/NodeVMModule.h 1 1 0 src/jsc/bindings/ScriptExecutionContext.h 2 1 0 src/jsc/bindings/sqlite/JSSQLStatement.cpp 1 1 0 src/jsc/bindings/sqlite/NodeSqlite.h 2 2 0 (+ 18 more files) ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
…ntly (#36821) ## What `test/cli/run/workspaces.test.ts` has three tests that each spawn an independent `bun run --workspaces` subprocess in its own temp dir. This tightens what each test asserts and wraps them in `describe.concurrent` since they share no state. ## Assertion changes 10 `expect()` calls, up from 8: - Check `stderr`/`stdout` before `exitCode` so failures print useful output. - Assert `stderr` is empty on both success paths (previously captured but never checked). - Happy path: compare the full stdout as a sorted line set instead of two substring checks, so an unexpected extra line or format change fails. This also subsumes the old `not.toContain("root test")` check. - `--if-present`: also assert nothing is emitted for the package that lacks the script. - Error path: also assert `stdout` is empty and include the script name in the expected error text. - `await using proc = Bun.spawn(...)` for cleanup. No tests removed or skipped. ## Timing Local `bun bd test` (debug+ASAN), 5 runs each: median 2.74s before, 2.22s after. debian-13 x64-asan CI lane: **11.99s** (build 88024) vs **11.06s** (build 88091, this PR). The three subprocesses are CPU-bound under ASAN so running them concurrently on the CI runner wins less than I initially expected; the assertion tightening is the main value here. <!-- robobun:evidence:begin --> --- **[stamp-90s]** gate passed · iteration 0 · 1 files touched <details><summary>passes on PR (with fix)</summary> ```console Test-only change. Debug/ASAN (expected pass): $ bun bd test 'test/cli/run/workspaces.test.ts' $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/cli/run/workspaces.test.ts bun test v1.4.0 (c8d83dd) test/cli/run/workspaces.test.ts: (pass) bun run --workspaces > runs script in all workspace packages [184.40ms] (pass) bun run --workspaces > fails when no packages have the script [126.21ms] (pass) bun run --workspaces > --if-present succeeds when script is missing [154.23ms] 3 pass 0 fail 10 expect() calls Ran 3 tests across 1 file. [2.22s] Exit: 0 ``` </details> <details><summary>diff hotspot</summary> ``` test/cli/run/workspaces.test.ts | 186 +++++++++++++++++++++------------------- 1 file changed, 98 insertions(+), 88 deletions(-) ``` </details> **gate history** · 1 passed · 0 rejected · iteration 0 <details><summary>evidence per changed file</summary> ``` file reads edits tests test/cli/run/workspaces.test.ts 4 4 0 ``` </details> <!-- robobun:evidence:end -->
|
Note on the review threads and the Files tab: after The 120 comment-cop threads from the latest run were generated from that stale file list; all but one are on lines that belong to |
…austed (#39183) ### Problem - The `Comment Cop` check is red on every claude-labeled PR whenever the repo's GraphQL quota is used up. The step dies before it scans anything: ``` GraphqlResponseError: Request failed due to following response errors: - API rate limit already exceeded for site ID installation. ##[error]Unhandled error: GraphqlResponseError: ... ``` with `x-ratelimit-resource: graphql`, `x-ratelimit-limit: 10000`, `x-ratelimit-remaining: 0` (run [31898901053](https://github.com/oven-sh/bun/actions/runs/31898901053) on #39139). All 33 comment-cop failures on Aug 15 are this error (197 runs succeeded, 171 were skipped); they hit unrelated PRs at the same time because the quota is shared by every workflow run in the repo, and they come back whenever PR volume is high. - Cause, `.github/workflows/comment-cop.yml:124` on main: the step reads the PR's existing review threads with `github.graphql()`, outside any `try`, and dedup and posting both sit behind that call. The REST call just before it (`pulls.listFiles`) had succeeded in each failing run, so the step had the diff; it only lacked the dedup data, which it was reading from the exhausted quota. - The same thread data also fed an auto-resolve of stale threads. That part does not work: under the Actions `GITHUB_TOKEN` every `resolveReviewThread` mutation fails with `Resource not accessible by integration`, after which the step still logs `Resolved N stale comment-cop thread(s).` Two successful runs from today: [31900717034](https://github.com/oven-sh/bun/actions/runs/31900717034) (20 attempted, 20 failed, "Resolved 20") and [31900354924](https://github.com/oven-sh/bun/actions/runs/31900354924) (4 attempted, 4 failed, "Resolved 4"). #36959 documents the same thing and is the PR that moves resolution to a token that can do it. So the GraphQL query was paying for one thing REST can provide and one thing that does not happen. ### Fix - Dedup reads the PR's review comments with `pulls.listReviewComments` (REST) and collects the `<!-- comment-cop:KEY -->` markers from them. Every comment the step posts starts with that marker, so the review comments carry the same keys the thread roots did; REST is the quota the step already needs for `listFiles` and `createReviewComment`, and it was available in every failing run. - The GraphQL query and the resolve loop are removed, so the step makes no GraphQL request at all; the failure in the Problem section cannot happen, rather than being caught. Nothing observable is lost: the mutations the loop issued all fail today, and the only other thing it did was log `Resolved N stale comment-cop thread(s).` after they had failed. Resolving stale threads stays with #36959, which will also need to move its thread lookup into its token step, since this PR removes the `GITHUB_TOKEN` lookup it currently reuses (noted there). - What the step posts is unchanged: the script on main and the REST dedup were dry-run (real reads, writes recorded) against 9 PRs carrying existing comment-cop threads (#35988, #36956, #36713, #35635, #33632, #35596, #39139, #30609, #36959) and chose the same comments to post on every one of them; the canned scenarios below show the same thing with GraphQL working and with it exhausted. - Test: `test/internal/source-lints/comment-cop.test.ts` extracts the script from the workflow and runs it the way `actions/github-script` does, against a fake `github` whose GraphQL requests all fail with the rate limit error above. It checks that groups not yet flagged are posted with the right line ranges, that a group already flagged (and a stale marker) are left alone, that a second run recognizes the comments the first run posted and posts nothing, and that no GraphQL request is made. Both tests fail against the workflow on main (the script throws the error above) and pass with this change. `source-lints.yml` now also triggers on changes to `comment-cop.yml`, so the test runs whenever the script is edited; it is excluded from the Buildkite shards like the rest of that directory. - Also ran: `bun bd test test/internal/source-lints/comment-cop.test.ts`, `bun test test/internal/source-lints/` (whole directory green), prettier on the three files. The comment-cop run on this PR itself still executes the script from main (`pull_request_target`), so the `Source lints` job is the one that exercises the change here. - Does not overlap with #37948 (which groups are flagged) or #38127 (where the file list comes from); both touch other parts of the script. #36959 rewrites the block this PR deletes and will need a rebase either way. ### Background - Comment Cop (`.github/workflows/comment-cop.yml`): on each push to a claude-labeled PR it reads the PR diff, finds multi-line comments added under `src/`, and posts one review comment per comment block. Each bot comment starts with `<!-- comment-cop:KEY -->`, KEY being the file path plus a hash of the block's text; a block whose KEY is already on the PR is not posted again. The check is advisory (not required for merge). - GitHub API quotas: REST and GraphQL requests count against separate hourly quotas (`x-ratelimit-resource` is `core` for REST and `graphql` for GraphQL). For the `GITHUB_TOKEN` Actions hands out, each quota is per repository, so every workflow run in oven-sh/bun draws on the same two pools, and GraphQL-heavy automation (the `gh pr` / `gh issue` / `gh search` commands used by other workflows go through GraphQL) empties the GraphQL pool for everything else when PR volume is high. - Review threads vs review comments: a review thread is GraphQL's grouping of a line comment with its replies, and is the only place a thread's id (what `resolveReviewThread` takes) and its resolved flag exist. `GET /repos/{owner}/{repo}/pulls/{n}/comments` returns every review comment on the PR, including each thread's root comment, so the markers are reachable from REST; only resolving needs GraphQL, and under `GITHUB_TOKEN` GitHub refuses that mutation regardless of the `pull-requests: write` permission. <details> <summary>Script on main vs this branch against a fake github (the fake's resolve mutation fails the way GITHUB_TOKEN's does)</summary> ``` main | quota ok, stale threads present | exit 0 | posts ["src/foo.ts:5-6"] | resolve attempts ["T_STALE_OPEN"] | graphql ["query","query","mutation"] | warnings 1 main | graphql quota exhausted, stale threads present | step fails (unhandled rate limit error) | posts [] | resolve attempts [] | graphql ["query"] | warnings 0 main | graphql quota exhausted, nothing stale | step fails (unhandled rate limit error) | posts [] | resolve attempts [] | graphql ["query"] | warnings 0 main | graphql quota exhausted, PR has no review comments yet | step fails (unhandled rate limit error) | posts [] | resolve attempts [] | graphql ["query"] | warnings 0 fixed | quota ok, stale threads present | exit 0 | posts ["src/foo.ts:5-6"] | resolve attempts [] | graphql [] | warnings 0 fixed | graphql quota exhausted, stale threads present | exit 0 | posts ["src/foo.ts:5-6"] | resolve attempts [] | graphql [] | warnings 0 fixed | graphql quota exhausted, nothing stale | exit 0 | posts ["src/foo.ts:5-6"] | resolve attempts [] | graphql [] | warnings 0 fixed | graphql quota exhausted, PR has no review comments yet | exit 0 | posts ["src/foo.ts:2-3","src/foo.ts:5-6"] | resolve attempts [] | graphql [] | warnings 0 ``` </details> <details> <summary>Dry-run against live PRs: comments the script on main would post vs the REST dedup (real reads through a user token, writes recorded)</summary> ``` PR #35988: same posts (0 vs 0) 245 threads on the PR are stale; main attempts to resolve them, this branch does not PR #36956: same posts (0 vs 0) PR #36713: same posts (1832 vs 1832) the stale cached file list that #38127 fixes; identical on both PR #35635: same posts (0 vs 0) PR #33632: same posts (0 vs 0) PR #35596: same posts (0 vs 0) PR #39139: same posts (1 vs 1) the comment the failing run above did not get to post PR #30609: same posts (0 vs 0) PR #36959: same posts (0 vs 0) ``` </details> <details> <summary>Headers from the failing run</summary> ``` errors: [ { type: 'RATE_LIMIT', code: 'graphql_rate_limit', message: 'API rate limit already exceeded for site ID installation.' } ] variables: { owner: 'oven-sh', repo: 'bun', pr: 39139, after: null } 'x-ratelimit-limit': '10000' 'x-ratelimit-remaining': '0' 'x-ratelimit-resource': 'graphql' 'x-ratelimit-used': '10000' ``` </details>
Problem
Packages such as
sqlite3,better-sqlite3,serialportorzeromqload their native addon withrequire("bindings")("name"). Thebindingspackage locates the.nodefile at runtime by walking up from the caller's__filenameto the package root and probingbuild/Release/name.nodeand a few sibling directories. Once the caller is bundled that walk starts at the bundle, and in abun build --compileexecutable it fails immediately (bun --compileerror.error: Could not find module root given file.#10964, Bun build does not work with the sqlite3 library #14301, better-sqlite3 .node exe is not bundled #8895):Even when the walk could succeed, the
.nodefile is never part of the build, because the bundler only ever seesrequire("bindings"), not the addon.Fix
src/js_parser/p.rs,maybe_rewrite_bindings_require): whenFeatures::rewrite_bindings_requireis set and a call has the shaperequire("bindings")("name")orrequire("bindings")({ bindings: "name" }), the import record thatrequire("bindings")produced is retargeted atname.node(the extension is added likebindingsdoes) and taggedNativeBindings, and the call is replaced by arequireof that record. Any other argument (further options, a computed name) changes whatbindingswould look for and is left alone.require()orimport(), the barrel optimization un-defers every unused record of that module and resolves it, so thebindingspackage ended up bundled after all (the earlier revision of this PR, and bundler: resolve require('bindings')(name) to the built .node file #35642 and bundler: resolve require("bindings")(name) to a static .node import #35688, had this gap; their tests used amodule.exports = require(...)module that the bundler collapses before linking, which hid it).ParseTaskenables the feature only for server-side targets, outside the dev server, and whenbindingsis not external (Resolver::is_package_external, which covers--external bindingsand--packages external). Browser builds, dev-server builds and builds that externalizebindingsare byte-for-byte what they were; the last one keeps the runtime lookup the user asked for.src/bundler/bundle_v2.rs,resolve_import_records): aNativeBindingsrecord is looked up withResolver::resolve_bindings_addonbefore anything else. The module root is found the waybindings'getRoot()finds it (nearest directory with apackage.json, named or not, or anode_modulesdirectory) and the build directories are probed inbindings1.5.0's order, minus the two entries whose names encode the build host's Node version. On success the record now holds the addon's absolute path and continues through normal resolution, so plugins,--outdirand--compiletreat it exactly like a hand-writtenrequire("/abs/addon.node")and the existing napi loader copies or embeds the file. On failure the record goes through the same path as any unresolvable import: a build error naming the addon; inside atryblock, arequirethat throws at runtime, so packages with a JS fallback behave as they would withbindings; underignoreModuleResolutionErrors, the same stub every other unresolvable require gets.bindingswould use at runtime, and it reproducesbindings' own search; everythingbindingswould do differently (other options, a dynamic name) is left to run at runtime. The missing-addon behaviour mirrors whatbindingsdoes at runtime (throw), moved to build time where it can be.test/bundler/bundler_loader.test.ts(describe("require('bindings')"), 16 cases: both targets, both argument forms, other options and computed names left alone, abuild/Debugaddon withbindingsnot installed at all, both module-root rules, an addon namedzlib, the missing-addon error, thetry/catchfallback at runtime,external: ["bindings"],packages: "external", the browser target and the dev-server format; 10 fail on the released binary, the 6 left-alone cases pass on both and pin the gating) andtest/napi/napi.test.ts, which compiles an app whose dependency loads the realnapitests.nodethroughrequire('bindings')and runs the executable from an unrelated directory (esm and cjs).bundler_loader,bundler_edgecase,bundler_cjs,bundler_barrel,bundler_npmandesbuild/packagejsonpass.Consolidation
#35642 and #35688 were alternative fixes for the same issue. Folded in from #35642: appending
.nodein the parser, not touching browser builds, thebuild/Debugcase withoutbindingsinstalled, and the docs note; from #35688: the{ bindings: "name" }form and the negative test for other options. Not adopted: #35642 left a missing addon as a runtimerequire("name.node")and #35688 turned it into an empty stub; both hid the problem until runtime, and #35688's stub also defeatedtry/catchfallbacks. Thebindingspackage itself is not vendored as a fixture (a throwing stub proves the same thing).Background
require("x")and the bundler resolves them after parsing. Expressions refer to records by index.HANDLES_IMPORT_ERRORS/WAS_UNRESOLVED: record flags meaning "this require is inside atry" and "resolution failed"; the printer turns a record with both into a throwing expression and one with only the latter into an empty stub, which is how unresolvable requires behave everywhere else in the bundler.require()orimport()of a module requests all of it, which un-defers every unused record in it..nodefiles, which copies the file next to the bundle or embeds it into a compiled executable and rewrites therequireto point at it.Fixes #10964
Fixes #14301
Fixes #8895
no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/bundler/bundler_compile.test.ts test/napi/napi.test.ts