Skip to content

uws: make stream-buffer conversion a one-shot ownership transfer - #31983

Open
robobun wants to merge 3 commits into
mainfrom
farm/42b32e6a/uws-stream-buffer-take
Open

uws: make stream-buffer conversion a one-shot ownership transfer#31983
robobun wants to merge 3 commits into
mainfrom
farm/42b32e6a/uws-stream-buffer-take

Conversation

@robobun

@robobun robobun commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Fixes #31971

Bug

us_socket_stream_buffer_t::to_stream_buffer was a safe method taking &self that rebuilt an owning Vec<u8> via Vec::from_raw_parts without clearing list_ptr/list_len/list_cap. Calling it twice, or once followed by us_socket_free_stream_buffer, produced two owners of the same allocation: a double free reachable from safe Rust. Both types are re-exported from the crate's public surface.

let mut raw = us_socket_stream_buffer_t::default();
raw.update(StreamBuffer { list: vec![1, 2, 3], cursor: 0 });
let first = raw.to_stream_buffer();
let second = raw.to_stream_buffer();
drop(first);
drop(second);
// free(): double free detected in tcache 2
// ASAN: attempting double-free on 0x... in thread T0

Fix

In src/uws_sys/us_socket_t.rs:

  • to_stream_buffer(&self) is now take_stream_buffer(&mut self): it rebuilds the Vec and nulls list_ptr/list_len/list_cap/cursor, so the returned buffer is the allocation's sole owner and a second take yields an empty buffer. The four field resets are the load-bearing lines. total_bytes_written is cumulative socket state, not buffer contents, and survives the take.
  • destroy routes through the take, so teardown is idempotent instead of relying on a "not called more than once" doc contract.
  • update drops any still-owned buffer before overwriting the raw parts (previously a silent leak, same lifecycle class). Every method now maintains: non-null list_ptr means the struct owns exactly one decomposed Vec.

The only in-tree caller, us_socket_buffered_js_write (src/runtime/socket/uws_jsc.rs), already ordered JS coercion before materializing the buffer and handed ownership back via update, so node:http server write behavior is unchanged; the one-shot contract is now enforced by the signature rather than by call-site discipline. Its comments are updated for the new semantics.

Tests

  • stream_buffer_tests in src/uws_sys/us_socket_t.rs: one-shot take, destroy-after-take, idempotent destroy, update-over-update, total_bytes_written preservation, empty-capacity round trip. They are pure Rust (no FFI at test runtime), so bun_uws_sys is added to MIRI_CRATES (scripts/rust-miri.ts plus the path filter in .github/workflows/miri.yml): the Miri CI lane now runs them and deterministically flags a regression. Verified by mutation: reverting the field resets fails under Miri with "constructing invalid value of type &mut [u8]: encountered a dangling reference (use-after-free)"; removing the drop in update fails with "memory leaked".
  • test/internal/uws-stream-buffer-ownership.test.ts: source-text lint pinning the take_stream_buffer(&mut self) signature and the single Vec::from_raw_parts site, same pattern and placement as ban-words.test.ts. The unsound path is not reachable from JS (the one caller ordered around it), so there is no behavioral JS repro; this is the bun bd test artifact and it fails on the unfixed source.

Verification: cargo test -p bun_uws_sys (8 pass) and the full bun run rust:miri set pass. bun bd test passes on node-http-backpressure.test.ts, node-http-backpressure-max.test.ts, node-http-nested-cork.test.ts, node-http-ondata-reregister-leak.test.ts, and node-http.test.ts (one pre-existing proxy-test failure there reproduces identically on the unmodified release build: environment ECONNREFUSED).

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
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

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

More reviews will be available in 4 minutes and 30 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 079d0314-3912-48bc-802e-f0a3d88f2261

📥 Commits

Reviewing files that changed from the base of the PR and between 09703da and 301c914.

📒 Files selected for processing (5)
  • .github/workflows/miri.yml
  • scripts/rust-miri.ts
  • src/runtime/socket/uws_jsc.rs
  • src/uws_sys/us_socket_t.rs
  • test/internal/uws-stream-buffer-ownership.test.ts

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

@robobun

robobun commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:05 AM PT - Jun 10th, 2026

@robobun, your commit 301c914 has 1 failures in Build #61705 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 31983

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

bun-31983 --bun

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Segfault in uWS::HttpContext<false>::onClose during socket teardown (Bun 1.3.14, Linux, standalone executable) #31467 - Segfault in uWS::HttpContext::onClose during socket teardown on a long-running HTTP server. Heap corruption from the double-free could cause crashes during socket close.
  2. Heap corruption (malloc free list) during long-running server with sharp + mongodb #27929 - Explicitly reports "malloc: double free for ptr" and heap corruption on a long-running Bun.serve() server — exact symptom of this bug.
  3. Recurring segfault after POST to a Hono route mounting @hono/mcp StreamableHTTPTransport (Bun 1.3.13 and 1.3.14) #31004 - Recurring segfault after HTTP POST response on Hono + Bun.serve at null-pointer-offset addresses, consistent with use-after-free in write buffer.

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

Fixes #31467
Fixes #27929
Fixes #31004

🤖 Generated with Claude Code

@robobun

robobun commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

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, us_socket_buffered_js_write (the node:http server socket write path), already coerced JS values before taking the buffer and handed ownership back via update() before returning, with no JS executing in between, so the double free was not reachable from any shipped code path. This PR changes no runtime behavior; it makes the one-shot contract structural.

The three issues also sit on different paths: #31467 crashes in uWS C++ HttpContext::onClose teardown, and #27929 / #31004 are Bun.serve() fetch-handler servers (#27929 additionally loads the sharp native addon), none of which go through JSNodeHTTPServerSocket buffered writes. Closing them on merge would leave real bugs marked fixed.

@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.

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.

@robobun

robobun commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

CI status across three full runs (61359, 61666, 61705 after a maintainer branch update + one retrigger): every hard failure is reproduced on unrelated sibling branches, none touch this diff.

  • 61359: bunx.test.ts @angular/cli@latest registry breakage (since skipped on main in Skip bunx.test.ts in CI until the Node.js version bump #32042), init.test.ts snapshot drift and a mysql2 docker ECONNREFUSED on darwin (both also on builds 61354/61358).
  • 61666: zero failed jobs; 17 jobs expired waiting for agents, cascading 151 waiting_failed.
  • 61705: test/js/web/streams/streams-leak.test.ts on debian-13 x64, which also hard-fails on unrelated build 61704; remaining items are retried-flaky (windows update-interactive, serve-body-leak on asan passed on retry).

The diff's own lanes are green everywhere: cargo miri test (including the new bun_uws_sys lane), clippy, format, all platform builds, the linux-asan test lane, and the node:http suites. Binary size unchanged. Ready for review.

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.

[Unsoundness] us_socket_stream_buffer_t::to_stream_buffer can duplicate raw Vec ownership

1 participant