Skip to content

jsc: free owned FFI allocations when the post-call trap check returns Err - #34577

Open
robobun wants to merge 4 commits into
mainfrom
farm/5cf7e7f5/trap-window-ffi-leaks
Open

jsc: free owned FFI allocations when the post-call trap check returns Err#34577
robobun wants to merge 4 commits into
mainfrom
farm/5cf7e7f5/trap-window-ffi-leaks

Conversation

@robobun

@robobun robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator

What

call_check_slow / from_js_host_call_generic run the FFI closure, then check for a pending exception (including VMTraps). If a worker's terminate() sets the NeedTermination trap in the window between the C++ function's own RETURN_IF_EXCEPTION and the Rust post-call check, the wrapper returns Err and the closure's return value is dropped. When that value is a raw heap pointer (or a +1'd BunString, which is Copy with no Drop), the drop is a no-op and the allocation leaks.

Same class of bug #34574 fixed for JSPropertyIterator, without the Ref<VM> cascade: here the leak is just the one allocation per occurrence, so ~VM still runs.

Cause

A sweep of every call_check_slow / from_js_host_call_generic call site turned up six whose closure returns an owned allocation from C++:

wrapper returns C++ alloc destructor
URL::from_js *mut URL new WTF::URL (BunString.cpp) URL__deinit
URL::href_from_js BunString +1 toStringRef (BunString.cpp) String::deref
FetchHeaders::create_from_js *mut FetchHeaders new WebCore::FetchHeaders (bindings.cpp:1813) WebCore__FetchHeaders__deref
FetchHeaders::clone_this *mut FetchHeaders new WebCore::FetchHeaders (bindings.cpp:1860) WebCore__FetchHeaders__deref
JSBigInt::to_string BunString +1 toStringRef (JSBigIntBinding.cpp) String::deref
JSValue::serialize SerializedScriptValue* leakRef() (Serialization.cpp) Bun__SerializedScriptSlice__free

For FetchHeaders::create_from_js the window is wider than just the return instruction: the final C++ guard is a bare throwScope.exception() (no trap handling), so a trap set during headers->fill() is not observed until the Rust side checks, and fill() iterates every header entry.

Fix

Adds call_check_slow_owned(global, f, free) in TopExceptionScope.rs (with a from_js_host_call_owned alias in host_fn.rs): same shape as call_check_slow, but free runs on the closure's return before Err is propagated. The hazard is documented once on the helper and pointed at from call_check_slow_at's doc comment. All six sites become a single call with the matching destructor as the free closure.

Verification

New ASAN-only test terminate() during an allocating FFI wrapper does not leak the allocation in worker_threads.test.ts: a worker tight-loops new Response("", {headers: {200 entries}}) (drives FetchHeaders::create_from_js) while the parent terminate()s it, under Malloc=1 detect_leaks=1. 4 processes x 42 workers each; the parent fixture prints done once every chain finishes and the test asserts on that plus a null signalCode.

LSan fail-before stack (3/3 runs):

Direct leak of 336 byte(s) in 3 object(s) allocated from:
    #10 in WebCore::FetchHeaders::operator new(unsigned long) .../FetchHeaders.h:44:5
    #11 in WebCore__FetchHeaders__createFromJS .../bindings.cpp:1813:21
    #16 in <bun_jsc::fetch_headers::FetchHeaders>::create_from_js .../FetchHeaders.rs:147:9
    #17 in <bun_runtime::webcore::response::HeadersRef>::create_from_js .../Response.rs:90:12
    #18 in <bun_runtime::webcore::response::Init>::init .../Response.rs:1412:34
    #19 in <bun_runtime::webcore::response::Response>::constructor .../Response.rs:1239:28

Pass-after 3/3. worker_threads.test.ts 92 pass, headers.test.ts 99 pass, response.test.ts 23 pass.

Malloc=1 also surfaces pre-existing per-thread singletons (WebCore::eventNames, Bun.main's AtomString) that are normally hidden by bmalloc, so the test assertion is scoped to the six C++ function names above rather than a blanket LSan check.

A couple of sites use an &mut BunString out-param instead of a closure return (JSValue::get_name, jsonStringify); those have the same hazard at the caller and are left for a follow-up since the fix shape is different.


no test proof · iteration 0 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/worker_threads/worker_threads.test.ts

… Err

call_check_slow / from_js_host_call_generic run the FFI closure then check
for a pending exception (including VMTraps). When a worker's terminate()
sets the NeedTermination trap between the C++ function's own
RETURN_IF_EXCEPTION and the Rust post-call check, the wrapper returns Err
and the closure's return value is dropped. For closures that return an
owned heap pointer (or a +1'd BunString, which is Copy with no Drop), that
drop is a no-op and the allocation leaks.

Same shape #34574 fixed for JSPropertyIterator. A sweep of every call site
found six with an owning return:

  URL::from_js               (*mut WTF::URL)
  URL::href_from_js          (BunString +1)
  FetchHeaders::create_from_js  (*mut WebCore::FetchHeaders)
  FetchHeaders::clone_this   (*mut WebCore::FetchHeaders)
  JSBigInt::to_string        (BunString +1)
  JSValue::serialize         (SerializedScriptValue* via leakRef)

Each now hoists the return into a local before the check and frees it on
the Err branch.

The test drives FetchHeaders::create_from_js in a worker while the parent
terminate()s it. createFromJS's final guard is a bare throwScope.exception()
(no trap handling), so the trap window spans the entire headers->fill()
loop; 200 header entries make that wide enough for the LSan report to hit
reliably (3/3 fail-before, 3/3 pass-after). Malloc=1 so LSan can see the
WebCore allocation.
@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 6 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ffd23ed4-caf5-48d1-ba52-ae3e8c49a33f

📥 Commits

Reviewing files that changed from the base of the PR and between 6843b4c and 090ff8e.

📒 Files selected for processing (10)
  • src/jsc/FetchHeaders.rs
  • src/jsc/JSBigInt.rs
  • src/jsc/JSValue.rs
  • src/jsc/TopExceptionScope.rs
  • src/jsc/URL.rs
  • src/jsc/host_fn.rs
  • src/jsc/lib.rs
  • test/js/node/worker_threads/worker-terminate-ffi-alloc-parent-fixture.js
  • test/js/node/worker_threads/worker-terminate-ffi-alloc-worker-fixture.js
  • test/js/node/worker_threads/worker_threads.test.ts

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

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:36 AM PT - Jul 18th, 2026

@robobun, your commit 090ff8e has 1 failures in Build #75287 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34577

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

bun-34577 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 1 issue this PR may fix:

  1. macOS Apple Silicon: memory invisible to RSS — bmalloc slabs, worker cleanup gaps, GC safety bugs #28318 - Umbrella issue tracking worker cleanup gaps and memory invisible to RSS; this PR fixes 6 FFI call sites that leak C++ heap allocations (FetchHeaders, URL, BunString, SerializedScriptValue) when worker termination fires mid-call

If this is helpful, copy the block below into the PR description to auto-close this issue on merge.

Fixes #28318

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Beyond the inline nit, I checked that String::deref() is a no-op for the Dead tag (src/bun_core/string/mod.rs:539), so out.deref() in href_from_js/JSBigInt::to_string is safe when C++ threw before returning a +1 impl; and that each of the six destructor pairings (WebCore__FetchHeaders__deref, URL__deinit, Bun__SerializedScriptSlice__free, String::deref) matches the allocation named in the PR table, with null/DEAD guards on every Err path so nothing double-frees when C++ returned no allocation.

Extended reasoning...

Verified the Err-path cleanup at each of the six sites cannot free an allocation that was never made: the raw-pointer sites guard on non-null / NonNull::new, and the two BunString sites rely on String::deref() gating on tag == WTFStringImpl (checked in bun_core), so DEAD is a no-op. Success paths are byte-for-byte behavior-preserving. Leaving unapproved because this is FFI memory-ownership code across six JSC wrappers — worth a maintainer glance even though the pattern is mechanical.

Comment thread test/js/node/worker_threads/worker_threads.test.ts
robobun and others added 2 commits July 18, 2026 09:26
…to test

Addresses review feedback:

- Six open-coded hoist/check/free-on-Err blocks replaced with one named
  helper, call_check_slow_owned (and its from_js_host_call_owned alias),
  next to the existing per-shape wrappers in TopExceptionScope.rs. The
  trap-window hazard is documented once on the helper and pointed at from
  call_check_slow_at.
- Test fixture now prints 'done' to stdout once every chain finishes, and
  the test asserts on that plus a null signalCode. Without a positive
  marker a missing fixture or crash-before-LSan satisfied the purely
  negative assertions. exitCode cannot be asserted because Malloc=1
  surfaces pre-existing per-thread singletons that make LSan exit 1.
If the worker fixture is missing, 'error' fires before 'exit' and the
chain previously still advanced to 'done'; the test's stderr scan only
matches the six target C++ symbols, so a MODULE_NOT_FOUND passed the
assertions with zero workers ever entering create_from_js.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found after the two fixture fixes — the failed gate in 090ff8e closes the last vacuous-pass path. Deferring to a human because this adds a new ownership primitive (call_check_slow_owned) to the exception-scope machinery and rewires six FFI sites' error-path destructors; worth a maintainer's eyes on the free-closure pairings.

What was reviewed:

  • call_check_slow_owned: closure runs exactly once on both cfg paths in call_check_slow_at, so slot.expect(..) is sound; free(r) only on Err.
  • Each of the six free closures null-guards before calling the destructor; BunString::deref() is a no-op on Dead/Empty tags, so the two string sites are safe when C++ returned a Dead-tagged value.
  • Test fixture now gates done on !failed, so a missing worker fixture or worker error fails the stdout: "done" assertion.
Extended reasoning...

Overview

Adds call_check_slow_owned (and its from_js_host_call_owned alias) to TopExceptionScope.rs / host_fn.rs: same shape as call_check_slow, but takes a free: impl FnOnce(R) that runs on the closure's return before propagating Err. Six call sites whose C++ callee returns an owned heap allocation (URL::from_js, URL::href_from_js, FetchHeaders::create_from_js, FetchHeaders::clone_this, JSBigInt::to_string, JSValue::serialize) are converted to the new helper with matching destructor closures. An ASAN-only LSan test in worker_threads.test.ts drives FetchHeaders::create_from_js in a tight loop while the parent terminate()s the worker, and asserts none of the six symbol names appear in the leak report.

Security risks

None identified. This is a leak fix on the worker-termination path; no user-controlled data reaches a new sink, no auth/crypto/permissions code is touched.

Level of scrutiny

High. This is native memory-safety code at the Rust/C++ FFI boundary — the repo's most-blocked review category. The change introduces a new primitive that every future "owned-return FFI wrapper" will use, and each of the six converted sites pairs a C++ allocator with a specific destructor. A wrong pairing or a free on an already-freed value would be a use-after-free reachable from worker termination. I checked each pairing:

  • FetchHeaders (both sites): NonNull::newif let Some(mut p) { p.as_mut().deref() } — matches WebCore__FetchHeaders__deref; null-guarded.
  • URL::from_js: raw *mutif !p.is_null() { URL__deinit(p) } — null-guarded.
  • URL::href_from_js / JSBigInt::to_string: BunStrings.deref(). Verified bun_core::String::deref() only acts on Tag::WTFStringImpl (src/bun_core/string/mod.rs:539), so a Dead-tagged return (which URL__getHrefFromJS produces on failure per its doc comment) is a no-op — no double-free.
  • JSValue::serialize: SerializedScriptValueExternalif !ext.handle.is_null() { Bun__SerializedScriptSlice__free(ext.handle) } — null-guarded, matches the existing Drop for SerializedScriptValue.

The helper itself smuggles R out via Option<R> and matches on call_check_slow_at's Result<()>; both cfg branches of call_check_slow_at run f() exactly once before the exception check, so the .expect(..) cannot fail.

Other factors

Two prior inline concerns from me (vacuous-pass test, then the residual missing-fixture gap) were both addressed — the parent fixture now prints done gated on !failed, and the test asserts {stdout: "done", signalCode: null}. The PR description documents a demonstrated 3/3 fail-before / 3/3 pass-after with the LSan stack. The test is ASAN-only and probabilistic (race-window based), which is unavoidable for this bug class but means non-ASAN CI won't exercise it.

I'm not approving because this adds a reusable ownership primitive to the JSC exception-scope layer and touches six FFI destructor pairings — a maintainer should sign off on the helper's shape and confirm the sweep didn't miss sibling sites (the PR description notes &mut BunString out-param sites are deferred to a follow-up).

@robobun

robobun commented Jul 18, 2026

Copy link
Copy Markdown
Collaborator Author

CI builds #75210 and #75287 both show only pre-existing or flaky-retry failures (no-orphans.test.ts on macOS, various install/websocket/terminal flakes). None touch worker_threads.test.ts or any of the files in this diff; the new LSan test passed on the asan lane.

The diff is green. Ready for review.

Jarred-Sumner added a commit that referenced this pull request Aug 17, 2026
…ll_jsc, node-fallbacks, and misc crates (#39420)

Net -4,402 lines (172 files, +509 / -4,911). Everything removed has zero
references across `src/`, `scripts/`, `test/`, `packages/` and the
regenerated `build/debug/codegen/` output, and the removal builds:
removing a Rust or C++ definition that still had a caller fails to
compile or link, so a green build is the reference check for those; JS,
codegen and manifest removals were additionally grepped by name
(including `bun:internal-for-testing` consumers under `test/`).

Most of these were first identified by the sweeps that were closed
yesterday for merge conflicts (#37272, #37062, #38439, #37089, #38703).
This PR re-applies the subset that still applies on current main, minus
anything an open PR already deletes and minus small hunks in files that
change daily (see "Left out" below), plus a few new finds.

### react_compiler (-1,166)

- `validate_no_derived_computations_in_effects_exp` and its ~22
exclusive helpers/types (~1.1k lines in
`validation/validate_no_derived_computations_in_effects.rs`). The
pipeline only ever calls the non-`_exp` validation; the `_exp`
env-config flag was parsed from fixture pragmas and read by nothing.
`react-compiler-fixtures.test.ts` now lists the two pragmas as ignored
instead of handled; the fixture suite still passes.
- `SymbolHost` (back-compat alias of `Host`; `DESIGN.md` updated),
`HirBox`, `is_use_state_type`, `default_true`.

### C++ bindings (-1,350 across 64 files)

- `JSDOMConvertBufferSource.h`: the IDL typed-array specializations and
`toPossiblyShared*Array` helpers for every element type no binding
converts (only the Uint8Array/ArrayBuffer views are used).
- `JSDOMPromiseDeferred.h/.cpp`: `resolveWithJSValue`,
`resolveWithNewlyCreated`, `resolveCallbackValueWithNewlyCreated` and
friends.
- `IDLTypes.h`: the `IDLUnsupportedType` family, the IDB / WebGL /
`ScheduledAction` wrappers and stale forward declarations;
`JSDOMConvertDate.h/.cpp` (the only `IDLDate` converter) deleted along
with its two includes. The 8-line `IDLDate` struct itself stays for now
(see "Left out"). `JSDOMConvertScheduledAction.h`, orphaned in the same
way, is already deleted by #35775, so it is not touched here.
- `NetworkLoadMetrics.h`: WebKit networking-stack fields/accessors bun
never reads.
- Deleted files: `JSWorkerOptions.h/.cpp` (`JSWorker.cpp` builds
`WorkerOptions` by hand), `JSMIMEBindings.h/.cpp` (`createMIMEBinding`
had no callers; include dropped from `ZigGlobalObject.cpp`),
`JSDOMIterator.cpp` (`addValueIterableMethods`).
- `ncrypto.h/.cpp`: `peekError`, `BignumPointer::isZero`,
`EVPKeyCtxPointer::sign`, unused copy/move `operator=` overloads and an
`AsymmetricKeyEncodingConfig` constructor.
- Smaller: `JSDOMOperationReturningPromise.h`
(`call*ReturningOwnPromise`), `JSDOMAttribute.h`
(`setPassingPropertyName`, `setStatic`), `JSDOMGuardedObject`
(`DoNotRegisterWithGlobalObjectTag`), `Event`/`EventTarget`
(`resetBeforeDispatch`, default-handled flags, `isNode()`),
`EventEmitter::{eventTypes,eventListeners}`,
`HTTPHeaderIdentifiers::identifierFor`, `JSURLPatternResult`
`convertDictionary` stubs,
`PerformanceTiming::monotonicTimeToIntegerMilliseconds`,
`ContextDestructionObserver::protectedScriptExecutionContext`,
`expectedEnumerationValues<CryptoKeyUsage>`,
`BufferSource::mutableData`/`toBufferSource`, `BunString__toInt32`,
`BunString::utf8ByteLength`, the `Ref<StringImpl>` overload of
`toCrossThreadShareable`, `normalWorld()`, `TextEncoding(const
String&)`, `JSBuffer` `createBuffer`/`constructFromEncoding` overloads,
the `JSX509Certificate` `m_infoAccess` lazy property and the non-legacy
branch of `computeInfoAccess` (the prototype getter reads the view
directly; x509 tests pass),
`BakeAdditionsToGlobalObject::wrapComponent`,
`jsFunction_lsanDoLeakCheck`, and the dead `BunObject+exports.h` macro
entries (together with the Rust `BunObject_callback_nanoseconds` export
the `nanoseconds` entry declared; `Bun.nanoseconds` is
`functionBunNanoseconds` in `BunObject.cpp`).

### install_jsc / install_types / bun:internal-for-testing (-344)

- `install_jsc/dependency_jsc.rs` and `update_request_jsc.rs` deleted:
their only consumers were the `npa` / `npmTag` exports of
`bun:internal-for-testing`, which no test imports. The
`dispatch_js2native.rs` re-exports, the `generate-js2native.ts` file-map
entry and the `lsanDoLeakCheck` export (tests use `isASANEnabled`) go
with them.
- `install_types/lib.rs`: the `ExternalString` / `SlicedString` /
`SemverString` re-export modules; nothing names those paths (everything
imports the types from `bun_semver`). New in this PR.

### node-fallbacks (-400) and codegen (-392)

- `util.js`: a ~270 line commented-out `util.types` block.
- `package.json` / `bun.lock` / `tsconfig.json`: dependencies and path
mappings nothing imports (`esbuild`, `buffer`, `events`, `util`, `url`,
`process`, `path-browserify`, `os-browserify`, `timers-browserify`,
`tty-browserify`, `vm-browserify`, ...). `build-fallbacks.ts` marks
every builtin name external, and the remaining sources only import the
packages still listed; `bun install --frozen-lockfile` in the directory
is a no-op and `bundler_browser.test.ts` passes.
- `src/codegen/generate-unified-source-bundles.rb`: WebKit's Ruby
generator, superseded by `scripts/build/unified.ts`; nothing invokes it.

### Rust crates (-1,200 across bun_jsc, bun_runtime, css, uws_sys and
leaf crates)

- bun_jsc: `BuiltinName::get` + `BUILTIN_NAME_MAP`,
`MarkedArrayBuffer::to_js` (the `ArrayBuffer::alloc` Uint8Array arm and
its `Bun__allocUint8ArrayForCopy` binding are kept, so `alloc` keeps
mirroring `create`), the `__dangerouslySetPtr` wrapper
`js_class_module!` emitted into every class, `ErrorBuilder::new`,
`TopExceptionScope::new`, `Task::new`, `JSCell::to_js`,
`JSGlobalObject::{to_js,ref_,ctx}`, `job::{on_js_thread,off_thread}`,
`AbortReason` impl, `JSPromise` settle helpers, `UUID::ZERO`,
`TagPayload::get`.
- bun_runtime: the `target_os = "wasi"` directory-iterator backend in
`dir_iterator.rs` (no shipped target is wasi and the `bun_sys::wasi`
module it imports does not exist), the `Display` impls in
`assert/myers_diff.rs`, the non-unix stub and not-macos escapes in
`fs_events.rs` (the file is only compiled on macOS),
`ArrayBufferSink::to_js`, unused re-exports in `node.rs` /
`api/bun/spawn.rs` / `ffi/mod.rs` (with `abi_type` formatters narrowed
to `pub(crate)`), `Error::UnableToDecode`,
`MyersDiff::Error::OutOfMemory`. The first three are new in this PR.
- css: the inherent `eql` / `to_css` / `parse` forwarders whose callers
all go through the `CssEql` / `ToCss` / `Parse` trait impls
(`values/calc.rs` and friends), `generics::{implement_eql,parse}`,
`TokenList::parse_with_options`, `CssString::parse`.
- uws_sys: the `uws_loop_defer`, `uws_res_clear_corked_socket`,
`uws_ws_iterate_topics` and `uws_h3_req_get_parameter` C shims plus
their Rust declarations and wrappers
(`Loop::{uncork,wake,next_tick,run}`), `AnyResponse::init`,
`SocketGroup::is_empty`, `socket.rs` `group()` accessors and the
`SocketTcp`/`SocketTls` aliases, `Opcode::Close`, `WindowsLoop`.
- leaf crates: `windows_sys` constants and their `bun_sys::windows`
re-exports, `zlib`/`zlib_sys` declarations (`deflateInit_`,
`inflateInit_`, the `gz*` file API, legacy type aliases), `sha_hmac`
deprecated-API hashers (`SHA512` raw, `RIPEMD160`, `MD5_SHA1`, `Blake2`
evp), `wyhash` `HashInt` impls for u16/u64, `libarchive` commented-out
Zig-era callbacks, `bun_alloc` (`AllocError::name`, `usable_size`,
`BSSList::init`), `clap::Error::WriteFailed`, `csrf` error variants,
`pe::Error::{InputIsSigned,InsufficientSpace}`, `md` `Setextheader`,
`opaque_mut_nn`, `cares_sys` `AddrInfo_hints::is_empty`, `boringssl_sys`
constants, `errno` `Mode` re-exports, `bounded_array::get`,
`string::write::Result`, `SplitIterator::rest`, `OutOfRangeValue` impls,
`symbol::Map::init`, `sql_jsc` re-exports.

### src/js (-32)

- Unused REPL primordials entries in
`internal/repl/node-primordials.js`; with that gone `SafeWeakSet` had no
importer, so `internal/primordials.js` stops exporting it (new in this
PR). Unused export-object entries in `internal/fs/watch.ts` and
`internal/readline/interface.js`.

### scripts (-9)

- `glob-sources.ts` `src/*.c` pattern (matched nothing since
`asan-config.c` was deleted), the write-only `BUN_DEP_*` defines in
`depVersionsHeader.ts`, the unread `kqueue` config field.

### Verification

- `bun bd` (full debug build) passes.
- `bun run rust:check-all`: all 11 target triples ok (covers the
windows/darwin-only removals in `windows_sys`, `sys/windows`,
`zlib_sys/win32.rs`, `fs_events.rs`, `windows-shim`).
- `cargo fmt --check`, `cargo clippy --workspace`, clang-format on every
touched C++ file, prettier, and `bun run lint` are clean.
- All of `test/internal/source-lints/` (including the new
`dead-symbols-react-compiler-webcore-idl-misc.test.ts` that guards these
symbols, and `dead-code-escapes` against the updated
`dead-code-escape-limits.json`), `react-compiler-fixtures.test.ts`,
`bundler_browser.test.ts`, css, cryptohasher/hash, node:assert, zlib,
url, events, websocket, inspect and x509 tests pass. `serve.test.ts` has
the same 4 failures as the unmodified release build in this container
(IPv6 / root port range), nothing else.

### Left out on purpose (follow-up candidates)

- The pre-engine inbound path in `h2_frame_parser.rs` (~2.2k lines,
still dead, #37272's diff still applies cleanly): the file has had 15
commits in the last two weeks, so it is better landed on its own.
- Deletions already owned by open PRs: simdutf wrappers (#38958),
`getStackTraceForThrownValue` (#37450), `validateOneOf` (#38401), the
redis error variants (#34829), `URL::from_js` (#33889 / #34577),
`schema::api` re-exports (#37095), `FsPath` (#39327), `NodeJSFS` `Null`
impl (#38065), the deprecated selector `to_css` (#33332).
- Small hunks in high-churn files (`bindings.cpp`,
`ZigGlobalObject.cpp`, `Blob.rs`, `streams.rs`, `BunProcess.cpp`, the
`ManifestLoad::LoadFromMemory` parameter across `src/install`, the
watcher `loader` parameter), and the `#[no_mangle]` statics
(`Zig_ErrorCode*`, `Bun__versions_*`) nothing on the C++ side reads.
- `VM::has_termination_request` and its `JSC__VM__hasTerminationRequest`
shim in `bindings.cpp`: a dead pair, kept intact here because
`bindings.cpp` is the most actively edited file in the tree; both halves
go together in a follow-up.
- `IDLDate` in `IDLTypes.h`: its only converter is deleted here, but the
struct itself is left in place so this PR's file deletions stay
independent of the header edits; removing the struct is a one-hunk
follow-up once the converter files are gone.
- Found but not removed here: the windows shim's `ReadWithoutLaunch`
mode (~110 lines, overlaps #36200), `node_quic_binding.rs` constants JS
never destructures (~35 lines), the native `NodeJSFS.unwatchFile` /
`FSWatcher.hasRef` / `QuicSession.silentClose` / `QuicEndpoint.ref`
bindings JS never calls, never-constructed `bun_install::Error`
variants, and ~160 lines of `$`-declarations in `src/js/builtins.d.ts`
with no users.

<!-- robobun:evidence:begin -->

---

**[review]** gate passed · iteration 1 · 172 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 10 failed, 320 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/react-compiler-fixtures.test.ts test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts
bun test v1.4.0 (8326d1b)

test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts:
88 |     // Arena box alias with zero uses (HirVec is the one HIR actually uses).
89 |     ["src/react_compiler/hir/mod.rs", /\bHirBox\b/],
90 |     // Type predicate whose only callers were in the removed _exp validation.
91 |     ["src/react_compiler/hir/mod.rs", /\bis_use_state_type\b/],
92 |   ];
93 |   expect(resurrected(checks)).toEqual([]);
                                   ^
error: expect(received).toEqual(expected)

- []
+ [
+   "src/react_compiler/validation/validate_no_derived_computations_in_effects.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/hir/environment_config.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/program.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/program.rs: \bSymbolHo
... (truncated)

release without fix: 10 failed, 1146 skipped
bun test v1.4.0-canary.1 (21a4206)

test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts:
88 |     // Arena box alias with zero uses (HirVec is the one HIR actually uses).
89 |     ["src/react_compiler/hir/mod.rs", /\bHirBox\b/],
90 |     // Type predicate whose only callers were in the removed _exp validation.
91 |     ["src/react_compiler/hir/mod.rs", /\bis_use_state_type\b/],
92 |   ];
93 |   expect(resurrected(checks)).toEqual([]);
                                   ^
error: expect(received).toEqual(expected)

- []
+ [
+   "src/react_compiler/validation/validate_no_derived_computations_in_effects.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/hir/environment_config.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/program.rs: validate_no_derived_computations_in_effects_exp",
+   "src/react_compiler/program.rs: \bSymbolHost\b",
+   "src/react_compiler/lib.rs: \bSymbolHost\b",
+   "src/react_compiler/hir/mod.rs: \bHirBox\b",
+   "src/react_compiler/hir/mod.rs: \bis_use_state_type\b",
+ ]

- Expected  - 1
+ Received  + 9

      at <anonymous> (/workspace/bun/test/internal/source
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: 320 skipped
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/bundler/transpiler/react-compiler-fixtures.test.ts test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts
bun test v1.4.0 (8326d1b)

test/internal/source-lints/dead-symbols-react-compiler-webcore-idl-misc.test.ts:
(pass) dead react_compiler symbols do not reappear [21.23ms]
(pass) dead exe_format symbols do not reappear [2.63ms]
(pass) dead bun_core / bun_alloc / bun_ast / bun_ptr items do not reappear [18.66ms]
(pass) dead bun_css items do not reappear [21.91ms]
(pass) dead bun_jsc items do not reappear [20.67ms]
(pass) dead FFI-crate items do not reappear [30.81ms]
(pass) dead Rust symbols (install, webcore, jsc, leaf crates) do not reappear [18.03ms]
(pass) unused re-export names do not reappear [12.35ms]
(pass) stale build-script entries do not reappear [6.80ms]
(pass) dead Rust FFI wrappers and trait methods do not reappear [22.18ms]
(pass) dead C++ binding helpers do not reappear [99.82ms]
(pass) dead WebCore / IDL binding code does not reappear [114.33ms]
(pass) dead code in install_jsc, install_t
... (truncated)

release with fix: 1146 skipped
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 641ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/130] install /workspace/bun/src/node-fallbacks
bun install v1.4.0-canary.1 (21a4206)

Checked 111 installs across 104 packages (no changes) [3.00ms]
[2/130] gen node-fallbacks/react-refresh.js
Bundled 1 module in 5ms

  react-refresh.js  4.81 KB  (entry point)

[3/130] gen JSBuffer.lut.h
Generating /workspace/bun/build/release/codegen/JSBuffer.lut.h from /workspace/bun/src/jsc/bindings/JSBuffer.cpp
[4/130] gen generated_host_exports.rs
generated_host_exports.rs: 92 exports (host=3, lazy=10, generic=79, rust=0); 241 extern-C blocks audited
[5/130] gen node-fallbacks/*.js
[6/130] gen cpp.rs (cppbind)
[7/130] gen JS modules (bundle-modules)
Preprocess modules (7887ms)
Bundle modules (42ms)
Postprocesss modules (259ms)
Bundle Functions (695ms)
Generate Code (32ms)

[8.94s] Bundled "src/js" for production
  2630 kb
  197 internal modules
  13 native modules
  91 internal functions across 17 files
[7/129] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu)

  nightly-2026-07-20-x86_64-unknow
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
scripts/build/config.ts                            |    4 -
 scripts/build/depVersionsHeader.ts                 |    3 -
 scripts/build/source.ts                            |    2 +-
 scripts/glob-sources.ts                            |    1 -
 src/ast/symbol.rs                                  |    8 -
 src/boringssl_sys/boringssl.rs                     |    8 -
 src/bun_alloc/lib.rs                               |   29 +-
 src/bun_core/bounded_array.rs                      |    8 -
 src/bun_core/fmt.rs                                |   17 -
 src/bun_core/string/immutable.rs                   |    7 -
 src/bun_core/string/mod.rs                         |   11 -
 src/bun_core/string/write.rs                       |    3 -
 src/bun_core/windows_sys.rs                        |    2 +-
 src/cares_sys/c_ares.rs                            |    6 -
 src/clap/error.rs                                  |    9 -
 src/clap/lib.rs                                    |    5 -
 src/codegen/generate-js2native.ts                  |    1 -
 src/codegen/generate-unified-source-bundles.rb     |  392 -------
 src/csrf/lib.rs                                    |    3 -
 src/css/css_parser.rs                              |   19 +-
 src/css/generics.rs                                |   10 -
 src/css/lib.rs                                     |    2 +-
 src/css/properties/custom.rs                       |    8 +-
 src/css/rules/mod.rs                               |    7 +-
 src/css/values/angle.rs                            |    4 -
 src/css/values/calc.rs                             |  132 +--
 src/css/values/css_string.rs                       |    6 -
 src/css/values/time.rs                             |    7 -
 src/css_derive/lib.rs                              |    4 +-
 src/css_jsc/css_internals.rs                       |   11 -
 src/errno/darwin_errno.rs                          |    1 -
 src/errno/freebsd_errno.rs                         |    1 -
 src/errno/linux_
... (truncated)
```

</details>

**gate history** · 4 passed · 1 rejected · iteration 1

<details><summary>evidence per changed file</summary>

```
file                                reads  edits  tests
scripts/build/config.ts                 0      0      0
scripts/build/depVersionsHeader.ts      0      0      0
scripts/build/source.ts                 0      0      0
scripts/glob-sources.ts                 0      0      0
src/ast/symbol.rs                       0      0      0
src/boringssl_sys/boringssl.rs          0      0      0
src/bun_alloc/lib.rs                    0      0      0
src/bun_core/bounded_array.rs           0      0      0
src/bun_core/fmt.rs                     0      0      0
src/bun_core/string/immutable.rs        0      0      0
src/bun_core/string/mod.rs              0      0      0
src/bun_core/string/write.rs            0      0      0
src/bun_core/windows_sys.rs             0      0      0
src/cares_sys/c_ares.rs                 0      0      0
src/clap/error.rs                       0      0      0
src/clap/lib.rs                         0      0      0
(+ 156 more files)
```

</details>

<!-- robobun:evidence:end -->

---------

Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant