node:http2: remove unsafe from H2FrameParser - #40240
Jarred-Sumner wants to merge 4 commits into
Conversation
|
Updated 5:49 PM PT - Sep 6th, 2026
❌ @Jarred-Sumner, your commit a292a10 has 5 failures in
🧪 To try this PR locally: bunx bun-pr 40240That installs a local version of the PR into your bun-40240 --bun |
There was a problem hiding this comment.
d8cc46a addresses both concerns from the last round: cork() is now a bounded two-step (one uncork() that may run JS, then displace_cork() which moves the re-corked owner's bytes to its own write_buffer without running JS — so it always terminates and preserves the cork_ref ⇔ CORKED_H2 invariant), and the test's connected() helper now wires error/close to reject, with phase 3 exercising an uncapped B↔C cycle. No further issues found this pass.
Given the scope — a full &mut Stream→Rc<Stream> / hive-pool→RefPtr lifetime rewrite of H2FrameParser, new SelfRoot/new_cyclic primitives in bun_ptr, new AbortSignal::listen_native API, and a cork-slot protocol that took five rounds to converge — plus the stacked base on #40139, a human look is still warranted.
Checked this round: displace_cork() doesn't re-enter JS (only write_buffer.write + register_auto_flush, which early-returns when registered) and releases cork_ref after auto_flush_ref is in place, so it can't drop the last ref; the invariant's four writers (cork/uncork/displace_cork/release_refs_stranded_by_exit) each leave cork_ref and CORKED_H2 consistent; has_nonnative_backpressure is set for BunSocket::None so displaced bytes aren't overtaken.
Extended reasoning...
Overview
This PR eliminates all 54 unsafe blocks from src/runtime/api/bun/h2_frame_parser.rs by: converting *mut Stream map entries to Rc<Stream> with all fields Cell/JsCell; replacing the hive-pool allocator + manual ref_()/deref() with RefPtr::new_cyclic + a SelfRoot token + typed Cell<Option<RefPtr>> slots (cork_ref/auto_flush_ref/writeonly_socket_ref); adding impl Drop; converting BatchSegment::Ext { ptr } to Payload { off, len }; and moving abort-signal listeners to a new AbortSignal::listen_native + AbortListenerRegistration (RAII unregister). Supporting changes: new bun_ptr::SelfRoot/RefPtr::new_cyclic/DanglingOk, a shared FfiSlice.h header, a typed bun_jsc::h2_headers::materialize wrapper, and a HasAutoFlusher impl in webcore.rs. It's stacked on #40139.
Security risks
None new. The change is a memory-safety refactor of existing behavior; the materialize() wrapper adds a bounds assertion the C++ side previously trusted implicitly (now also ASSERTed per-field). No auth/crypto/permission surfaces touched.
Level of scrutiny
High. This is production-critical (every node:http2 session), rewrites the parser's ownership model end to end, and the cork-slot re-entrancy protocol went through five review iterations (c885faf → a8e3f80 → 713e18d → 6b7b22c → d8cc46a), each fixing a distinct leak/livelock/desync in the previous. d8cc46a's two-step approach (one JS-reentrant uncork(), then a non-reentrant displace_cork()) is bounded and preserves the stated invariant, and the new test's phase 3 now exercises the uncapped B↔C cycle that broke 6b7b22c — but the number of iterations to reach a stable design is itself a signal that a human should read the final shape.
Other factors
The PR is stacked on an unmerged base (#40139) and carries commits from #40210, so the effective diff against main is larger than what's shown here. The new bun_ptr primitives (SelfRoot, new_cyclic, the DanglingOk sealed trait gating BackRef::dangling) are foundation-crate additions that other zero-unsafe PRs in the series will depend on. Test coverage for the cork re-entrancy class is now solid (three phases, whole-frame assertions on every wire, uncapped cross-session pings), and the author reports 362/362 on node-http2 + the h2 conformance/grpc suites under debug+ASAN, but I have not independently verified CI is green on d8cc46a.
96ff99b to
0dfd929
Compare
d8cc46a to
bfbdbd0
Compare
There was a problem hiding this comment.
5bc3307 addresses both open findings from the last round: the retirement block at h2_frame_parser.rs:2908-2917 now guards on !pending_header_compression_error, and phase (4) asserts h2AutoFlushRegistered directly so deleting the retirement block breaks it. Beyond the inline nit, nothing new surfaced this pass.
Extended reasoning...
The two unresolved inline comments from the previous review pass (the 🔴 on the retirement block missing the pending_header_compression_error guard, and the 🟡 on phase (4) not exercising the retirement block) are both addressed by the tip commit 5bc3307: the retirement condition now includes && !self.pending_header_compression_error.get(), and the test fixture asserts h2AutoFlushRegistered(nativeOf(c)) before and after the wait via the new bun:internal-for-testing hook. This run's only new finding is a test-quality nit on phase (4)'s wait shape. Not approving — this is a ~1800-line refactor of refcounting, cork-slot re-entrancy, and abort-listener lifetime in node:http2, stacked on #40139, and has surfaced real bugs across six review rounds; a human pass is warranted.
There was a problem hiding this comment.
I reviewed this PR at 0c8ec0d and found no bugs. Given the scope — a ~1000-line refactor of h2_frame_parser.rs touching refcounting, cork-slot handover, stream lifecycle, and new SelfRoot/new_cyclic/AbortListenerRegistration abstractions in bun_ptr — a human look is still warranted before merge.
What was reviewed:
- The retirement block at
on_auto_flush's tail now guards on bothpending_header_compression_errorandtransport_write_fatal(5bc3307, 0c8ec0d), matchingunregister_auto_flush's invariant. cork()'s two-step displacement (displace_cork) preserves wire ordering without running JS; the phase-4 test'sh2AutoFlushRegisteredpoll now fails with the retirement block deleted.release_refs_stranded_by_exit's stranded-keepalive loop holds a scoped ref so it cannot freeselfmid-loop;Dropasserts no attached socket.BatchSegment::Payload { off, len }replaces the rawExt { ptr }— payload slices are resolved against the caller's&[u8]at flush time, no lifetime erasure.
Extended reasoning...
Overview
This PR eliminates all 54 unsafe blocks from src/runtime/api/bun/h2_frame_parser.rs by: (1) making Stream fully interior-mutable and Rc-shared instead of raw *mut Stream map entries; (2) replacing hand-rolled ref/deref pairs with typed RefPtr slots (cork_ref, auto_flush_ref, writeonly_socket_ref) plus a SelfRoot/RefPtr::new_cyclic self-pointer; (3) reworking the thread-local cork slot to a typed BackRef<H2FrameParser, Root> with a two-step cork() that displaces re-entrant foreign owners without running JS; (4) introducing AbortListenerRegistration/NativeAbortListener in bun_jsc::abort_signal; (5) removing the HiveArray pool in favor of plain RefPtr allocation and impl Drop; (6) replacing raw batch-segment pointers with payload offsets. Supporting changes touch bun_ptr (SelfRoot, new_cyclic, OwnedThis, DanglingOk), the FFI slice header, H2HeadersMaterializer.cpp, and add a 220-line multi-phase re-entrancy test plus a bun:internal-for-testing hook.
Security risks
None identified. The change is a memory-safety refactor of an existing subsystem; no new user-facing surface, no auth/crypto/permissions. The HPACK header materializer's per-header bounds check moved from a runtime check to a debug ASSERT, but the Rust wrapper (h2_headers::materialize) now asserts described <= packed.len() before the FFI call, so untrusted-input validation is still on the release path.
Level of scrutiny
High. h2_frame_parser.rs is production-critical native code where refcount imbalances, re-entrancy under user JS callbacks, and cork-slot ownership races have direct UAF/leak consequences. Five prior review rounds on this PR each found a real re-entrancy or lifecycle bug (double-cork assert, unbounded cork loop, displaced auto-flush never retiring, retirement dropping a latched compression/transport error), all now addressed. The new bun_ptr primitives (SelfRoot, new_cyclic, OwnedThis) are also load-bearing abstractions carried from #40210 that a maintainer should sign off on.
Other factors
The PR is stacked on #40139 (base claude/socket-zero-unsafe). Test coverage is substantial (362/362 node-http2, grpc-js, node parallel test-http2-*, ASAN clean, hand-driver leak checks per the description), and the new phase-(4) fixture asserts the retirement block via the internal-for-testing hook so deleting the fix breaks a test. All prior inline findings are resolved. Given the size, the number of subtle interactions already found, and the new cross-cutting abstractions, this warrants human review before merge.
0dfd929 to
72a878a
Compare
0c8ec0d to
0c1fd13
Compare
There was a problem hiding this comment.
Re-reviewed after b29af8a and found nothing new — release_refs_stranded_by_exit now bypasses unregister_auto_flush()'s latched-error guard and drops auto_flush_ref directly, which closes the last open thread. Given the scope (~1500 lines of ref-count/lifetime rework in the h2 parser, new SelfRoot/new_cyclic primitives in bun_ptr, and the FfiSlice FFI signature change), a human pass over the final shape is still worthwhile.
What was reviewed
- b29af8a's inline unregister mirrors
unregister_auto_flush's effect minus thepending_header_compression_errorearly return; that function has no other guards, so nothing else is bypassed. - Re-traced
cork()/uncork()/displace_cork()andon_auto_flush's retirement conditions after the 0c1fd13 rebase — the invariants from the earlier rounds still hold. FfiSlice<T>layout acrossFfiSlice.h,bun_jsc::h2_headers, and theWebSocket.{h,cpp}adoption — field order/repr line up on both sides.
Extended reasoning...
Overview
This PR removes all unsafe from src/runtime/api/bun/h2_frame_parser.rs (54 → 0) by replacing raw-pointer self-references, a hand-rolled HiveArrayFallback pool, and manual ref_()/deref() with typed RefPtr/SelfRoot/BackRef handles from bun_ptr. It introduces RefPtr::new_cyclic + SelfRoot<T> in src/ptr/, an RAII AbortListenerRegistration in src/jsc/AbortSignal.rs, moves Bun__h2__materializeHeaders behind a shared Bun::FfiSlice<T> header (also adopted by WebSocket.{h,cpp}), converts Stream to Cell/JsCell fields under Rc<Stream>, replaces BatchSegment::Ext { ptr, len } with an offset+len pair, and threads cork_ref/auto_flush_ref/keepalive_refs as typed self-ref slots. AutoFlusher call sites in webcore.rs, cron.rs, and socket_body.rs are updated to the new back-ref API, and a test hook is added via internal-for-testing.ts. test/js/node/http2/node-http2.test.js gains ~216 lines exercising the cork/displace/auto-flush re-entry chain.
Security risks
The change is entirely internal to the runtime's HTTP/2 frame parser and its ref-count plumbing; no new user-facing surface, auth, crypto, or permissions paths are touched. The FfiSlice change tightens rather than loosens the FFI boundary (typed ptr+len instead of separate scalar args, with a Rust-side length assertion). The security-relevant risk class here is memory safety — use-after-free or ref-count leaks under re-entrancy — which is exactly what the ten prior review rounds iterated on. No injection, data-exposure, or auth-bypass surfaces are in scope.
Level of scrutiny
High. This is a large refactor of lifetime-critical code: manual ref counting, a per-thread cork slot shared across sessions, deferred-microtask auto-flush registration, abort-signal listener lifecycle, and a process.exit() stranded-ref cleanup path. Over the review history I raised ten distinct re-entrancy/ref-leak findings across the cork()/uncork()/displace_cork()/on_auto_flush/release_refs_stranded_by_exit chain, each of which was fixed in a follow-up commit; b29af8a addresses the last one. The number of iterations alone argues for a maintainer read of the final composed shape rather than automated approval — each fix was locally correct, but the interaction surface between the cork slot, the auto-flush registration, latched session errors, and VM-shutdown finalization is dense enough that a human should confirm the invariants documented at the auto_flush_ref/cork_ref fields match what the code now does end-to-end.
Other factors
The PR description reports full debug+ASAN passes on node-http2.test.js (362/362), the h2 conformance/adversarial/leak suites, and the grpc-js suite, plus a hand driver with LSAN-clean exit under process.exit() from inside callbacks. The new test in node-http2.test.js exercises the multi-session cork displacement path and asserts the auto-flush registration retires via the new h2AutoFlushRegistered hook. It is stacked on #40139 and carries #40210's SelfRoot/new_cyclic commits, so a maintainer will also want to confirm the base has landed before retargeting. No third-party CHANGES_REQUESTED reviews are outstanding in the timeline.
72a878a to
1b2d01c
Compare
b29af8a to
e9c5456
Compare
522dd45 to
7d53924
Compare
…ared/Mut new_cyclic builds a refcounted value that stores its own root pointer as an opaque SelfRoot<T>; the token can only be turned into a ThisPtr through the constructed &T, so it cannot be followed early or late. A Root back-reference can hand out ThisPtrs, so BackRef::dangling() is limited to Shared/Mut.
…ched-in Root back-reference
Squash of the branch's H2FrameParser commits for the rebase over #40516: - node:http2: remove the remaining unsafe from H2FrameParser - hand off the cork slot's ref across the re-entrant write in uncork() - cork() takes the slot in two steps; displace a re-corked owner without writing; retire the auto-flush of a displaced cork owner once its bytes are out - keep a latched compression / transport write error's auto-flush; expose the registration to tests; poll for the auto-flush retirement instead of a fixed sleep - cork/auto-flush/writeonly/keepalive refs release on drop; keepalive refs live in a typed slot - release a latched session error's auto-flush registration when the VM exits before it runs
e9c5456 to
a292a10
Compare
…ter, NodeHTTPResponse, and three JS binding objects (#42682) ### Problem - Some native members exist only for built-in JS to call, and no built-in JS calls them. The largest is `H2FrameParser.setStreamPriority` (`src/runtime/api/bun/h2_frame_parser.rs`). `Http2Stream#priority()` is a no-op since RFC 9113. - Three binding objects (`node:vm`, `node:crypto`, `bun:sql`) carry properties that their only JS consumer never reads. - `packages/bun-usockets` keeps a commented-out `bsd_udp_packet_buffer_ecn` from 2024-04. Nothing reads `completions/spec.yaml`, a 2021 appspec file. ### Fix - Delete each item: 15 files, 535 lines removed. The Notes list every symbol. - Correct because every removed name has zero references in `src/`, `packages/`, `scripts/`, `test/` and `build/debug/codegen/` outside its own definition. No removed member is documented or typed API. User code reaches the h2 parser handle only through the undocumented `Symbol.for("::bunhttp2native::")` key. - The `-D dead-code` lint then reported three more items in `h2_frame_parser.rs`, and two `Stream` fields became write-only. They are removed too. - Verified: `bun bd` and `bun run rust:check-all` (12 targets) pass. The http2, shell, vm, crypto, `node:http`, sql, udp and streams test files pass (list in the Notes). ### Background - A `*.classes.ts` file declares the methods of a native class. The generator emits the C++ wrapper and the Rust glue from it. A `proto` entry that no JS reads is dead, and so is its Rust function. - A binding object is a plain object that native code fills and one built-in module destructures once. A property that the module does not destructure is never read. - 28 other dead-code pull requests are open. This one deletes nothing that they delete. #40240 and #37588 edit the body of `set_stream_priority`. Each conflict resolves by taking the deletion. <details><summary>Notes</summary> Removed, one line each: - `H2FrameParser.setStreamPriority` / `set_stream_priority` (109 lines). No `.setStreamPriority(` in `src/js` or `test/`. - `H2FrameParser.isStreamAborted` / `is_stream_aborted`. Same check. - `H2FrameParser.hasNativeRead` / `has_native_read`. Same check. - `FrameType::HTTP_FRAME_PRIORITY`, `ErrorCode::PROTOCOL_ERROR`, `SignalRef::is_aborted`. Reported by the dead-code lint once the three functions above were gone. Both enums already list only the wire values that the file uses. - `Stream::stream_dependency`, `Stream::exclusive`. Their only reads were in `set_stream_priority`. The declarations, the initializers and the two stores in `request()` go. The locals that `request()` writes to the wire stay, and so does `Stream::weight` (read by `getStreamState`). - `ShellInterpreter.isRunning` / `is_running` and `.started` / `get_started`. `src/js/builtins/shell.ts` only calls `interp.run()`. - `Interpreter::started`. The field was only read by `get_started`. The two stores and the `AtomicBool` import go with it. - `NodeHTTPResponse.dumpRequestBody` / `dump_request_body`. Added in #17093, never called from JS at any commit. - `createNodeVMBinding`: `kUnlinked`, `kLinking`, `kEvaluating`, `kSourceText`, `kSynthetic`. `src/js/node/vm.ts` destructures `kLinked`, `kEvaluated`, `kErrored` and never touches the binding object again. - `createNodeCryptoBinding`: `SecretKeyObject`, `PublicKeyObject`, `PrivateKeyObject`. Not destructured in `src/js/node/crypto.ts`. The classes stay, only the unread properties go. - `bun_sql_jsc::mysql::create_binding`: `MySQLConnection`. `bun_sql_jsc::postgres::create_binding`: `PostgresSQLConnection`. `src/js/internal/sql/{mysql,postgres}.ts` destructure `createConnection`, `createQuery`, `init` only. - `packages/bun-usockets`: the commented-out `bsd_udp_packet_buffer_ecn` (`bsd.c`), its commented-out wrapper `us_udp_packet_buffer_ecn` (`udp.c`), and the two commented-out declarations (`libusockets.h`, `internal/networking/bsd.h`). `git blame`: 589f941, 2024-04-26. The `libusockets.h` lines are context lines of a hunk in #40294. - `completions/spec.yaml`. No hit for `spec.yaml` or `appspec` in the repo. It still lists the removed `bun dev` subcommand. The shell completions are hand-maintained and embedded with `include_bytes!`. Tests run with the debug build: `test/js/node/http2/` (7 files, `node-http2.test.js` 387 pass, `h2-conformance.test.ts` 70 pass), the four `test-http2-*priority*` Node tests, `test/js/bun/shell/` (4 files), `test/js/node/vm/vm.test.ts`, `crypto.key-objects.test.ts`, five `test/js/node/http/` files, five `test/js/sql/` files, `udp_socket.test.ts`, `streams.test.js`. How the candidates were found: - A repo-wide identifier index (definitions with zero other mentions, with and without comments). - A relink of the debug binary with `--gc-sections --print-gc-sections`. Almost every real hit from that pass is already deleted by one of the open pull requests. The rest were inlined functions, `const fn`s used only at compile time, and Windows or macOS paths. - A per-class comparison of every `*.classes.ts` member against `src/js`, `packages/bun-types` and `test/`. The generated thunk for a `proto` entry is `#[no_mangle]` and `#[allow(dead_code)]`, so neither rustc nor the linker can flag these members. - Checks for `.rs` files outside every `mod` tree, headers that nothing includes, C/C++ files outside the build, Cargo features that nothing enables, and long commented-out blocks. All clean apart from the usockets block. Found, not deleted here: - `TCPSocket`/`TLSSocket` `endBuffered` (`$end`): no JS caller, but its removal leaves `write_or_end_buffered::<IS_END>` with one instantiation. That is a refactor, not a deletion. - `*InternalReadableStreamSource.isClosed` getter: no reader, but its removal leaves `NewSource::is_closed` write-only, and the stores are in files that #41088 touches. - `NodeHTTPResponse.onwritable`: no reader in `src/js` today, but #41822 starts to use it. - The HEADERS+PRIORITY emitter and the native `SignalRef` abort path in `H2FrameParser::request()`. `http2.ts` warns with DEP0194 for the priority options and handles `options.signal` itself. Whether these native paths still run needs a separate look. - The `IPV6_RECVTCLASS` / `IP_RECVTOS` `setsockopt` calls in `packages/bun-usockets/src/bsd.c` ("used for getting the ECN"). Nothing reads the ECN now, but their removal changes socket options, so it is not a pure deletion. - `macro(mockedFunction)` and `macro(writer)` in `src/js/builtins/BunBuiltinNames.h` (the builtin-name entries, not the live `mockedFunction` string in `BunCommonStrings.h`): no `mockedFunctionPrivateName` or `writerPrivateName` user, but two open pull requests edit adjacent lines. - `scripts/packer/build-image.pkr.hcl`, `scripts/lldb-inline.sh`, `scripts/lldb-inline-tool.cpp`, `scripts/github-metrics.ts`, `scripts/gamble.ts`: nothing references them, but they are standalone tools that a person can run by hand. Self-reviewed: 12 concerns raised, 10 addressed (the two `Stream` fields, the header comment lines, and the body corrections above). Rejected: a split into three pull requests, because every deletion is verified the same way and a split triples the CI and review rounds. Deferred: a caller lint for `*.classes.ts` `proto` entries, as a follow-up that does not gate this change. </details>
…ter, NodeHTTPResponse, and three JS binding objects (oven-sh#42682) ### Problem - Some native members exist only for built-in JS to call, and no built-in JS calls them. The largest is `H2FrameParser.setStreamPriority` (`src/runtime/api/bun/h2_frame_parser.rs`). `Http2Stream#priority()` is a no-op since RFC 9113. - Three binding objects (`node:vm`, `node:crypto`, `bun:sql`) carry properties that their only JS consumer never reads. - `packages/bun-usockets` keeps a commented-out `bsd_udp_packet_buffer_ecn` from 2024-04. Nothing reads `completions/spec.yaml`, a 2021 appspec file. ### Fix - Delete each item: 15 files, 535 lines removed. The Notes list every symbol. - Correct because every removed name has zero references in `src/`, `packages/`, `scripts/`, `test/` and `build/debug/codegen/` outside its own definition. No removed member is documented or typed API. User code reaches the h2 parser handle only through the undocumented `Symbol.for("::bunhttp2native::")` key. - The `-D dead-code` lint then reported three more items in `h2_frame_parser.rs`, and two `Stream` fields became write-only. They are removed too. - Verified: `bun bd` and `bun run rust:check-all` (12 targets) pass. The http2, shell, vm, crypto, `node:http`, sql, udp and streams test files pass (list in the Notes). ### Background - A `*.classes.ts` file declares the methods of a native class. The generator emits the C++ wrapper and the Rust glue from it. A `proto` entry that no JS reads is dead, and so is its Rust function. - A binding object is a plain object that native code fills and one built-in module destructures once. A property that the module does not destructure is never read. - 28 other dead-code pull requests are open. This one deletes nothing that they delete. oven-sh#40240 and oven-sh#37588 edit the body of `set_stream_priority`. Each conflict resolves by taking the deletion. <details><summary>Notes</summary> Removed, one line each: - `H2FrameParser.setStreamPriority` / `set_stream_priority` (109 lines). No `.setStreamPriority(` in `src/js` or `test/`. - `H2FrameParser.isStreamAborted` / `is_stream_aborted`. Same check. - `H2FrameParser.hasNativeRead` / `has_native_read`. Same check. - `FrameType::HTTP_FRAME_PRIORITY`, `ErrorCode::PROTOCOL_ERROR`, `SignalRef::is_aborted`. Reported by the dead-code lint once the three functions above were gone. Both enums already list only the wire values that the file uses. - `Stream::stream_dependency`, `Stream::exclusive`. Their only reads were in `set_stream_priority`. The declarations, the initializers and the two stores in `request()` go. The locals that `request()` writes to the wire stay, and so does `Stream::weight` (read by `getStreamState`). - `ShellInterpreter.isRunning` / `is_running` and `.started` / `get_started`. `src/js/builtins/shell.ts` only calls `interp.run()`. - `Interpreter::started`. The field was only read by `get_started`. The two stores and the `AtomicBool` import go with it. - `NodeHTTPResponse.dumpRequestBody` / `dump_request_body`. Added in oven-sh#17093, never called from JS at any commit. - `createNodeVMBinding`: `kUnlinked`, `kLinking`, `kEvaluating`, `kSourceText`, `kSynthetic`. `src/js/node/vm.ts` destructures `kLinked`, `kEvaluated`, `kErrored` and never touches the binding object again. - `createNodeCryptoBinding`: `SecretKeyObject`, `PublicKeyObject`, `PrivateKeyObject`. Not destructured in `src/js/node/crypto.ts`. The classes stay, only the unread properties go. - `bun_sql_jsc::mysql::create_binding`: `MySQLConnection`. `bun_sql_jsc::postgres::create_binding`: `PostgresSQLConnection`. `src/js/internal/sql/{mysql,postgres}.ts` destructure `createConnection`, `createQuery`, `init` only. - `packages/bun-usockets`: the commented-out `bsd_udp_packet_buffer_ecn` (`bsd.c`), its commented-out wrapper `us_udp_packet_buffer_ecn` (`udp.c`), and the two commented-out declarations (`libusockets.h`, `internal/networking/bsd.h`). `git blame`: 589f941, 2024-04-26. The `libusockets.h` lines are context lines of a hunk in oven-sh#40294. - `completions/spec.yaml`. No hit for `spec.yaml` or `appspec` in the repo. It still lists the removed `bun dev` subcommand. The shell completions are hand-maintained and embedded with `include_bytes!`. Tests run with the debug build: `test/js/node/http2/` (7 files, `node-http2.test.js` 387 pass, `h2-conformance.test.ts` 70 pass), the four `test-http2-*priority*` Node tests, `test/js/bun/shell/` (4 files), `test/js/node/vm/vm.test.ts`, `crypto.key-objects.test.ts`, five `test/js/node/http/` files, five `test/js/sql/` files, `udp_socket.test.ts`, `streams.test.js`. How the candidates were found: - A repo-wide identifier index (definitions with zero other mentions, with and without comments). - A relink of the debug binary with `--gc-sections --print-gc-sections`. Almost every real hit from that pass is already deleted by one of the open pull requests. The rest were inlined functions, `const fn`s used only at compile time, and Windows or macOS paths. - A per-class comparison of every `*.classes.ts` member against `src/js`, `packages/bun-types` and `test/`. The generated thunk for a `proto` entry is `#[no_mangle]` and `#[allow(dead_code)]`, so neither rustc nor the linker can flag these members. - Checks for `.rs` files outside every `mod` tree, headers that nothing includes, C/C++ files outside the build, Cargo features that nothing enables, and long commented-out blocks. All clean apart from the usockets block. Found, not deleted here: - `TCPSocket`/`TLSSocket` `endBuffered` (`$end`): no JS caller, but its removal leaves `write_or_end_buffered::<IS_END>` with one instantiation. That is a refactor, not a deletion. - `*InternalReadableStreamSource.isClosed` getter: no reader, but its removal leaves `NewSource::is_closed` write-only, and the stores are in files that oven-sh#41088 touches. - `NodeHTTPResponse.onwritable`: no reader in `src/js` today, but oven-sh#41822 starts to use it. - The HEADERS+PRIORITY emitter and the native `SignalRef` abort path in `H2FrameParser::request()`. `http2.ts` warns with DEP0194 for the priority options and handles `options.signal` itself. Whether these native paths still run needs a separate look. - The `IPV6_RECVTCLASS` / `IP_RECVTOS` `setsockopt` calls in `packages/bun-usockets/src/bsd.c` ("used for getting the ECN"). Nothing reads the ECN now, but their removal changes socket options, so it is not a pure deletion. - `macro(mockedFunction)` and `macro(writer)` in `src/js/builtins/BunBuiltinNames.h` (the builtin-name entries, not the live `mockedFunction` string in `BunCommonStrings.h`): no `mockedFunctionPrivateName` or `writerPrivateName` user, but two open pull requests edit adjacent lines. - `scripts/packer/build-image.pkr.hcl`, `scripts/lldb-inline.sh`, `scripts/lldb-inline-tool.cpp`, `scripts/github-metrics.ts`, `scripts/gamble.ts`: nothing references them, but they are standalone tools that a person can run by hand. Self-reviewed: 12 concerns raised, 10 addressed (the two `Stream` fields, the header comment lines, and the body corrections above). Rejected: a split into three pull requests, because every deletion is verified the same way and a split triples the CI and review rounds. Deferred: a caller lint for `*.classes.ts` `proto` entries, as a follow-up that does not gate this change. </details>
What
Same programme as #40055 … #40236. Stacked on #40139 (base branch
claude/socket-zero-unsafe; retarget to main once that lands) and carries #40210's twoSelfRoot/new_cycliccommits.src/runtime/api/bun/h2_frame_parser.rs54 → 0.Stream: all fieldsCell/JsCell, the map holdsRc<Stream>;&mut Stream→&Streameverywhere;queue_frame/flush_queuetake one queue borrow and dispatch outside it (nothing is held across a JS re-entry); batch segments arePayload { off, len }resolved againstsend_data's payload instead of rawExt { ptr }.self_ref: SelfRoot<Self>viaRefPtr::new_cyclic; typed slotscork_ref/auto_flush_ref/writeonly_socket_ref,NativeCallbacks::H2(RefPtr)(attach checkshas_native_callback()first);KeepaliveisScopedRef+ main'snative_keepalivescounter andrelease_refs_stranded_by_exitkeeps main's semantics exactly (aprocess.exit()from inside any h2 JS callback still letsfinalizebring the count to zero). The hive pool is gone;impl Drop(asserts no attached socket); the constructor is split intoconstructor+configurewith an error path that detaches before the final release (the old path could free a still-attached parser).CORKED_H2: Cell<Option<BackRef<H2FrameParser, Root>>>for identity, the owner holdscork_refand clears the slot before releasing;cork()is one-shot as on main;on_auto_flushgoes throughHasAutoFlusherand returnsfalseon the fatal/compression paths (no re-entrant unregister from inside the queue run).SignalRefholds anAbortListenerRegistration(sameAbortSignal::{listen_native, retain}API as fetch: remove unsafe from FetchTasklet and fetch.rs #40202) + the parserRefPtr, dropped insidefree_resources(main's order);on_abortholds a tracked keepalive for the dispatch.FfiSlice.hshared withWebSocket.h;Bun__h2__materializeHeaderstakes two typed slices (bun_jsc::h2_headers::materialize), the per-header bound is a debugASSERT.Testing
Debug+ASAN: node-http2.test.js 362/362 (also under
BUN_JSC_validateExceptionChecks=1), h2-conformance, continuation, invalid-padding, streams-rehash, syscall-fault, upgrade, late-rst, push-refusal; fetch-http2-client/adversarial/leak; grpc-js server/retry/channel-credentials/call-propagation/server-errors/outlier-detection (test-client's 3 failures identical on baseline); node paralleltest-http2-*255/256 (test-http2-pipe flaky only under 16-way parallel; forget-closed-streams ~90 s on debug for both binaries). Hand driver: 100 concurrent 3 MB streams both directions + trailers, ping/settings, rst both sides, AbortSignal, goaway + destroy with open streams, destroy mid-upload, raw socket destroyed with 20 streams, server closed with 5 sessions, 50 short sessions — exit 0, no ASAN output, 120 created / 120 dropped / 120 finalized;process.exit()from inside stream/response/data/write callbacks leaves no h2 frames in LSAN output (same as baseline). clippy clean.