Add ForeignRef<T> and make FetchHeaders the owned handle
What
bun_opaque gains a generic owned handle for C++-allocated objects that Rust releases through an FFI destructor:
ForeignOwned— "this opaque type has a release extern"ForeignRef<T>—Deref+Drop,#[repr(transparent)]overNonNull<T>, zero costforeign_owned!(T, release_fn)— one line to get both
FetchHeaders is the first user, and the owned handle takes the public name (FetchHeaders : HeadersRef :: Path : PathBuf, with the raw ZST moved to an extern-only sys module). Four such handles were previously hand-rolled and ~27 more C++ types are still released by hand at every call site; those are the follow-up.
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 receivers that only ever needed &self now say so. unsafe { &mut * } goes from 1241 sites to 807.
No allocation, lock, Rc/Arc, or refcount is added anywhere in the diff. (Checked per-round: every added heap::release/RefPtr::new/Box::leak replaces an existing Box::into_raw(Box::new(..)) 1:1.)
Why &self everywhere on FetchHeaders
An opaque_ffi! type is a #[repr(C)] UnsafeCell<[u8;0]> ZST precisely so that &T carries no noalias/readonly and C++ may mutate through it. &mut self on such a type asserts an exclusivity that is never true (C++ owns the object and re-enters through the same pointer) and never needed (every FFI shim already takes &T). All 18 receivers were &mut self; that one wrong choice had grown a DerefMut, two &self -> &mut accessors, a duplicate RAII handle, and forced callers to launder &T into *mut T.
Behavior fix included
Making the receivers honest stopped server.fetch(url, { headers }) from compiling, which surfaced that it was sharing the caller's Headers with the internal Request instead of copying it. Two user-visible consequences: a header the handler set on req.headers showed up on the caller's Headers object, and the two owners disagreed about the object's lifetime. It now copies via clone_this, the same path new Response(_, { headers }) already takes. Regression test added; the aliasing half is asserted deterministically and the whole test also runs under the AddressSanitizer CI job.
Verification
cargo check --workspaceclean;bun bd(debug + ASan) clean- release build + 1,895 tests across
serve/fetch/headers/node-http2/fs/resolve/bunshell: 1,789 pass, 0 real failures - ASan run over
headers/node-http2/serve(642 tests): 0 reports - Miri under
-Zmiri-tree-borrows: 27 crates, 146 tests, 0 UB - The new regression test fails on a build without the fix and passes with it