Repository navigation
Conversation
|
Warning Review limit reached
On-demand reviews are free for the next 13 days. After that, they cost $0.25 per reviewed file. Or wait 13 minutes for your next included review. View limit detailsLimit details: You’ve used all 10 included reviews currently available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Essentials Run ID: 📒 Files selected for processing (4)
Comment |
|
Updated 8:05 PM PT - Sep 7th, 2026
❌ @robobun, your commit 33c5eb0 has 2 failures in
🧪 To try this PR locally: bunx bun-pr 31983That installs a local version of the PR into your bun-31983 --bun |
|
Found 3 issues this PR may fix:
🤖 Generated with Claude Code |
|
Not adding the suggested "Fixes" lines: this PR should not auto-close those issues. The unsound signature fixed here was a latent API hazard, not a live runtime bug. The only caller of the conversion, The three issues also sit on different paths: #31467 crashes in uWS C++ |
There was a problem hiding this comment.
I didn't find any issues — the ownership-transfer rework looks correct and well-tested — but since this changes unsafe raw-pointer ownership semantics on the node:http server write path and adds a crate to the Miri lane, I'd like a human to confirm before merging.
Extended reasoning...
Overview
This PR fixes a soundness hole in us_socket_stream_buffer_t: the old to_stream_buffer(&self) rebuilt an owning Vec<u8> via Vec::from_raw_parts without clearing the stored raw parts, so two safe calls could produce two owners of one allocation (double free). The fix renames it to take_stream_buffer(&mut self) and nulls list_ptr/list_len/list_cap/cursor after the rebuild, routes destroy and update through it (making destroy idempotent and update leak-free), updates the sole caller in src/runtime/socket/uws_jsc.rs, adds six Rust unit tests, wires bun_uws_sys into MIRI_CRATES + the workflow path filter, and adds a source-text lint under test/internal/.
Security risks
None introduced. The change strictly tightens memory safety: it removes a double-free reachable from safe Rust and a silent leak in update. No new attack surface, no auth/crypto/permissions involved.
Level of scrutiny
High. The code under change is unsafe Rust managing Vec raw-part ownership across the C++ FFI boundary, and us_socket_buffered_js_write is on the node:http server response write hot path. I traced the single in-tree caller and confirmed the take→work→update sequence is behaviorally unchanged (the new drop(self.take_stream_buffer()) at the top of update is a no-op there because the prior take already nulled the pointer). destroy now reconstructs with list_len instead of 0, which is equivalent for Vec<u8> drop. The 8-test count matches 6 new stream-buffer tests + 2 pre-existing quic/Header.rs classify tests, all FFI-free, so the Miri addition should be safe.
Other factors
No CODEOWNERS cover these paths. No prior reviewer comments to address. The bug-hunting pass found nothing. The reasoning and test coverage (including Miri mutation verification) are thorough. My only reason for not auto-approving is the criticality of the path: unsafe ownership changes at an FFI boundary in production HTTP server code warrant a human sign-off even when they look right.
|
CI status after the rebase (builds 112310, 112371, 112396, 112419): every hard failure is a break on main, present on every concurrent PR build in the same window.
Both test failures are reported to main-break triage. The lanes that exercise this diff are green: cargo miri test (with |
|
Stale PR review: keep open, rework. The fix for #31971 is still wanted. On main, the safe The current diff cannot merge as it is:
The wanted shape:
Two open PRs touch the same line. #35018 also says that it fixes #31971, because it removes |
us_socket_stream_buffer_t::to_stream_buffer took &self and rebuilt an owning Vec<u8> from raw parts without clearing them, so safe Rust could mint two owners of the same allocation (double free). Replace it with take_stream_buffer(&mut self) which nulls the raw parts, route destroy through it so teardown is idempotent, and make update drop any still-owned buffer before overwriting the parts. Add crate unit tests and run bun_uws_sys under the cargo-miri CI lane. Fixes #31971
301c914 to
217797a
Compare
…ints The lint reads only src/uws_sys/us_socket_t.rs and never runs the built binary, so it belongs in test/internal/source-lints/ where the GitHub Actions lane runs it against a released bun. Its header now cites the miri job in rust-lints.yml instead of the removed miri.yml.
|
Rebased onto main (ae3c3ad) and resolved three conflicts. Details are in the Notes block of the PR description. Also addressed the review comments: the comments in the diff are one line each, the source lint moved to |
Fixes #31971
Problem
us_socket_stream_buffer_t::to_stream_buffer(&self)rebuilt an owningVec<u8>withVec::from_raw_partsand leftlist_ptr/list_len/list_capin place. Two calls, or one call followed byus_socket_free_stream_buffer, gave two owners of one allocation. Safe Rust could reach it:free(): double free detected in tcache 2, or under Miriconstructing invalid value of type &mut [u8]: encountered a dangling reference (use-after-free).&selfsignature insrc/uws_sys/us_socket_t.rs. The one caller,us_socket_buffered_js_writeinsrc/runtime/socket/uws_jsc.rs, avoided the hazard by call-site discipline only.Fix
to_stream_buffer(&self)becomestake_stream_buffer(&mut self). It rebuilds theVec, then nulls the four raw-part fields. A second take returns an empty buffer.total_bytes_writtenis socket state and survives.destroyandupdateroute through the take.destroyis idempotent.updatedrops a buffer it still owns before it overwrites the parts (that was a silent leak).update. Only the signature enforces the contract now.cargo miri test -p bun_uws_sys(6 new tests, 8 total) andtest/internal/source-lints/uws-stream-buffer-ownership.test.ts. Alsobun bd teston the node:http backpressure, nested-cork, ondata-reregister and main suites.Background
us_socket_stream_buffer_tis a#[repr(C)]struct that C++ (JSNodeHTTPServerSocket) embeds by value. It holds the raw parts of aVec<u8>so C++ can read them. Only Rust rebuilds theVec, intake_stream_buffer.bun_uws_sysnow runs in thecargo miri testCI job (MIRI_CRATESinscripts/rust-miri.ts). Miri interprets the tests and reports a double free or a leak as a hard error.&mut selfsignature and the singleVec::from_raw_partssite. It lives intest/internal/source-lints/, which runs on GitHub Actions without a build.Notes
Mutation checks under Miri: remove the four field resets in
take_stream_bufferanddestroy_after_take_is_a_noopfails with the dangling-reference error above. Remove thedrop(self.take_stream_buffer())inupdateandupdate_drops_the_previously_owned_bufferfails withmemory leaked.Rebase onto main (ae3c3ad) resolved three conflicts.
.github/workflows/miri.ymlno longer exists: main folded the miri job intorust-lints.yml, whose path filter already coverssrc/**/*.rs, so this PR only adds the crate toMIRI_CRATES.destroyon main had changed to&mut selfand already nulledlist_ptr/list_cap; this PR keeps that signature and routes it through the take, which also clearslist_lenandcursor. TheDefaultimpl the tests used was deleted on main as dead code (#39795), so the test module has a privateempty()constructor that mirrors the C++ zero-initialization.The unsound path was never reachable from JS because the one caller ordered around it. The three crashes the issue-finder bot linked (#31467, #27929, #31004) are on
Bun.serveand uWS teardown paths that do not use this buffer, so this PR does not close them.[human-review] gate passed · iteration 6 · 4 files touched
fails on main (without fix)
passes on PR (with fix)
diff hotspot
gate history · 1 passed · 0 rejected · iteration 6
evidence per changed file
root cause · written by the author bot
The safe method
us_socket_stream_buffer_t::to_stream_buffer(&self)rebuilt aVecwithVec::from_raw_partsfrom pointer, length, and capacity fields it never cleared, so calling it twice produced two owning vectors over the same allocation and a double free through safe Rust. The fix replaces the&selfconversion with a consumingtakethat moves the raw parts out and resets them to an empty state, making the ownership transfer one-shot, and updates the caller inuws_jsc.rsaccordingly. A source lint intest/internal/source-lints/guards against reintroducing an owning conversio…