Skip to content

DO NOT MERGE: bisect s3.test.ts regression (commit 2/4) - #33908

Draft
Jarred-Sumner wants to merge 2 commits into
mainfrom
claude/s3-bisect-c2
Draft

DO NOT MERGE: bisect s3.test.ts regression (commit 2/4)#33908
Jarred-Sumner wants to merge 2 commits into
mainfrom
claude/s3-bisect-c2

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Temporary, will be closed. Bisecting an s3.test.ts crash that only reproduces in CI (needs R2 credentials).

Known: main is green, and the stack's first commit is green (#33820). The crash enters somewhere in commits 2-4.

Crash is panic: called Option::unwrap()on aNone value at Blob.rs Wrapper::resolve -> JSPromiseStrong::get(), reached via on_response -> error_with_body -> Callback::fail: the upload callback settles an already-cleared promise.

This PR is that stack's commit 2 in isolation. Only the s3.test.ts result matters.

A C++-allocated object that Rust releases through an FFI destructor needs two
types: the borrowed opaque (the `opaque_ffi!` ZST) and an owner whose Drop
gives the ref back. Four such owners were hand-rolled and 27 more C++ types
release by hand at every call site. Add the generic pair to `bun_opaque`:

  - `ForeignOwned`: this opaque type has a release extern
  - `ForeignRef<T>`: `Deref` + `Drop`, `repr(transparent)` over `NonNull<T>`
  - `foreign_owned!(T, release_fn)` emits the impl

`FetchHeaders` is the first user, and the owned handle takes the public name;
the raw ZST moves into an extern-only `sys` module.

  - Every receiver is `&self`. The type is `UnsafeCell`-backed precisely so
    that `&T` carries no `noalias`, and C++ mutates the header storage through
    the same pointer, so `&mut self` asserted an exclusivity that was never
    true and never needed.
  - Constructors return `Self`; C++ always hands back a fresh +1.
  - `cast()` returns `ManuallyDrop<FetchHeaders>`: it borrows the ref that the
    JS `Headers` wrapper owns, so releasing it must not be expressible.
  - The four `void*`-taking externs become `unsafe fn`. C++ dereferences the
    pointer, and safe Rust can forge a `*mut c_void`, so `safe fn` was
    unsound on all four. This matches the rule already written above the
    extern block.
  - `create()` and `copy_to()` take slices instead of `*mut StringPointer`
    plus a separate length.
  - `FetchHeaders::from` and `create_value` had no callers; deleted. The
    `_`-suffixed raw variants had no external callers; now private.

Making `cast()` a `ManuallyDrop` stopped `server.fetch(url, { headers })`
from compiling, which is how it turned out to be a use-after-free. That path
adopted the ref owned by the JS `Headers` wrapper: `cast_` never bumps the
refcount, and the wrapper's `Ref<>` derefs on finalize, so the internal
`Request` and the wrapper each released the same single ref. ASan confirms
the heap-use-after-free, and the aliasing half is deterministic without it: a
header the handler sets on `req.headers` shows up on the caller's `Headers`
object. Copy with `clone_this` instead, which is the path
`new Response(_, { headers })` already takes. All six `cast`/`cast_` callers
were audited; this was the only one that adopted the borrow. Regression test
added.

The rest of the diff converts the `unsafe { &mut *ptr }` pattern this grew
out of: raw-pointer fields become the in-tree owners (`JsCell`, `Cell`,
`ParentRef`, `BackRef`, `Box<Self>` receivers) across the runtime, and the
receivers that only ever needed `&self` say so. No allocation, lock, `Rc`,
`Arc`, or refcount is added anywhere in the diff.
Extends the ForeignRef<T> pattern from FetchHeaders to 16 more opaque FFI
types. Each moves its opaque_ffi! ZST into a `sys` module and exposes a
release function, replacing hand-rolled owners (OwnedSslCtx, OwnedDecompressor)
and bare raw pointers.

Also flips 314 `&mut self` receivers to `&self` on opaque ZSTs. Those types are
UnsafeCell-backed and !Freeze, so `&T` carries no noalias and C mutates through
it; `&mut self` asserted an exclusivity that was never true and never needed.

The receiver flip is only sound when a method hands back nothing aliasing real
memory. Four do, and keep `&mut self`: ConnectingSocket::ext, Timer::ext,
ListenSocket::ext, and JSUint8Array::slice all return `&mut T` into storage the
ZST receiver does not cover.

Two fixes fall out of the rename:

- cppbind mapped JSC::SourceProvider to the owning handle, so the generated
  safe wrapper passed the address of a Rust stack slot to C++ `->deref()`.
  It now names the sys:: ZST, matching the WebCore::EventLoopTask entry.
- Flipping Response::upgrade to `&self` moved method resolution to
  ResponseLike::upgrade, which boxes its argument a second time. The DevServer
  call site now names the inherent method explicitly.

Restores the debug-only corrupted-HandleSlot assert that Strong::destroy
carried before it became a ForeignRef; a bad slot otherwise faults inside
JSC with no Rust frame.

Adds a verify skill capturing the build-and-drive recipe.
@robobun

robobun commented Jul 10, 2026

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

@Jarred-Sumner, your commit 4cd86af has 3 failures in Build #71447 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 33908

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

bun-33908 --bun

@github-actions

Copy link
Copy Markdown
Contributor

Found 5 issues this PR may fix:

  1. Bun.Image: Blob read dispatch failure leaks the read chain and pins the wrapper #32125 - Blob read dispatch failure leaks the read chain; PR refactors ownership/RAII in Blob.rs and the S3 read path with ForeignRef<T> Drop-backed wrappers
  2. [Unsoundness] Reference-receiver to_js methods rely on prose heap-allocation contracts #31985 - Unsoundness in reference-receiver to_js methods (Blob.rs, Response.rs, Request.rs); PR converts these types to Drop-backed Rust ownership with ForeignRef<T>
  3. [Unsoundness] libarchive RAII owners expose safe explicit-free methods #31972 - RAII owners expose safe explicit-free methods allowing double-free; PR introduces ForeignRef<T> as a proper RAII wrapper for C/C++ FFI handles
  4. [Unsoundness] us_socket_stream_buffer_t::to_stream_buffer can duplicate raw Vec ownership #31971 - Duplicated Vec ownership in socket stream buffer; PR's ownership refactoring addresses the same class of duplicated-ownership unsoundness across FFI boundaries
  5. Memory leak when downloading file (both @google-cloud/storage and Bun's S3) #20487 - Memory leak downloading via S3; PR changes S3Client.rs, S3File.rs, Blob.rs, and fetch.rs ownership to Drop-backed RAII which addresses promise/Strong ref cleanup on download completion

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

Fixes #32125
Fixes #31985
Fixes #31972
Fixes #31971
Fixes #20487

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants