From ec572ec5cc4c528282a7e6f3ca853f618430d081 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 25 Aug 2026 04:23:40 +0000 Subject: [PATCH 1/8] webcore: take Response/Request/Body/ReadableStream/FileReader/Sink/streams to zero unsafe Body is now `JsCell` reached through closure-scoped borrows; BodyMixin getters split into a borrow-scoped front half and a script half (`LockedRead`) that runs once the borrow is released. Response is CellRefCounted with its weak-pointer finalize protocol in bun_ptr (`HasWeakPtrData`, `finalize_owner`, `destroy_weakly_held`); `to_js`/`into_js`/`to_js_retained` hand a typed `RefPtr` to the wrapper and to native holders (FetchTasklet, HTMLRewriter). The abort listener is a `NativeAbortListener` registration. Request finalizes through the same weak-pointer protocol and owns its pooled body as a `BodyHiveHandle`. `NewSource` is CellRefCounted with a `SelfRoot`; `SourceRef` is the counted native reference (FileReader's in-flight read ref and the reader's re-entrancy refs are typed slots). FileReader's BufferedReader parent is the enclosing Source (`impl_buffered_reader_parent!` borrow = this mode with a nested reader path; `read_from`/`close_from`/ `start_from`/`unpause_from` dispatch without a live reader borrow). JSSink host functions resolve `this` in the generated thunks (`${name}__getThis`); `__finalize`/`__controllerFinalize` dispatch on `JsSinkType::FINALIZE` (ThisPtr / &mut / Box); the SinkID map and `Bun__NativeTransformSink__writeBytes` / `Bun__onSinkDestroyed` are generated. `assign_to_stream` installs the controller through a caller closure. HTTPServerWritable's teardown is its `Drop`, `abort` returns the source for the caller to close, the byte-list pool slot is a `PoolGuard`, and RequestContext owns the sink as `OwnedThis`. BakeResponse / wasm_streaming / Crypto exports are HOST_EXPORTs. Lower layers: `RefPtr::from_box`, `OwnedThis`, `JsCell::into_inner`, `ObjectPool::try_get`, `String::into_wtf` / `From`, `ExternalShared::into_raw`, `StoreRef::set_mime_type`, `JSValue::as_direct` checks `is_cell`, generated `take_ptr` for class wrappers, `boxed_taskable!`. --- src/bun_core/external_shared.rs | 5 + src/bun_core/string/mod.rs | 18 + src/codegen/generate-classes.ts | 32 + src/codegen/generate-jssink.ts | 165 ++- src/collections/pool.rs | 9 + src/event_loop/ConcurrentTask.rs | 27 + src/io/PipeReader.rs | 124 ++ src/io/lib.rs | 134 ++- src/jsc/AbortSignal.rs | 54 + src/jsc/FetchHeaders.rs | 111 ++ src/jsc/JSValue.rs | 17 +- src/jsc/lib.rs | 6 +- src/jsc/webcore_types.rs | 10 + src/ptr/js_cell.rs | 11 +- src/ptr/lib.rs | 51 + src/ptr/ref_count.rs | 10 + src/ptr/weak_ptr.rs | 157 ++- src/runtime/api/bun/spawn/stdio.rs | 8 +- src/runtime/api/html_rewriter.rs | 239 ++-- src/runtime/api/streams.classes.ts | 5 - src/runtime/dispatch.rs | 6 +- src/runtime/image/codecs.rs | 7 + src/runtime/jsc_hooks.rs | 18 + src/runtime/node/types.rs | 18 +- src/runtime/server/FileRoute.rs | 20 +- src/runtime/server/RequestContext.rs | 333 +++--- src/runtime/server/StaticRoute.rs | 38 +- src/runtime/server/mod.rs | 7 +- src/runtime/server/server_body.rs | 2 +- src/runtime/shell/Builtin.rs | 7 +- src/runtime/shell/states/Cmd.rs | 14 +- src/runtime/webcore/ArrayBufferSink.rs | 25 +- src/runtime/webcore/BakeResponse.rs | 124 +- src/runtime/webcore/Blob.rs | 122 +- src/runtime/webcore/Body.rs | 1049 ++++++++--------- src/runtime/webcore/ByteBlobLoader.rs | 162 +-- src/runtime/webcore/ByteStream.rs | 150 +-- src/runtime/webcore/Crypto.rs | 6 +- src/runtime/webcore/FileReader.rs | 536 ++++----- src/runtime/webcore/FileSink.rs | 6 +- src/runtime/webcore/ObjectURLRegistry.rs | 5 +- src/runtime/webcore/ReadableStream.rs | 611 +++++----- src/runtime/webcore/Request.rs | 612 ++++------ src/runtime/webcore/Response.rs | 676 ++++------- src/runtime/webcore/Sink.rs | 275 ++--- src/runtime/webcore/fetch.rs | 83 +- .../webcore/fetch/FetchRequestBodySink.rs | 21 +- src/runtime/webcore/fetch/FetchTasklet.rs | 122 +- src/runtime/webcore/s3/client.rs | 35 +- src/runtime/webcore/streams.rs | 259 ++-- src/runtime/webcore/wasm_streaming.rs | 67 +- 51 files changed, 3332 insertions(+), 3277 deletions(-) diff --git a/src/bun_core/external_shared.rs b/src/bun_core/external_shared.rs index 0951f44e7396..c79b2164bda6 100644 --- a/src/bun_core/external_shared.rs +++ b/src/bun_core/external_shared.rs @@ -39,6 +39,11 @@ impl ExternalShared { self.ptr.as_ptr() } + /// Give up the handle without releasing its ref; the caller now owns that +1. + pub fn into_raw(self) -> *mut T { + core::mem::ManuallyDrop::new(self).ptr.as_ptr() + } + /// # Safety /// `raw` must be a valid pointer managed by the external refcount. pub unsafe fn clone_from_raw(raw: *mut T) -> Self { diff --git a/src/bun_core/string/mod.rs b/src/bun_core/string/mod.rs index f9fbc7d42d79..c8c68e899604 100644 --- a/src/bun_core/string/mod.rs +++ b/src/bun_core/string/mod.rs @@ -564,6 +564,17 @@ impl String { core::ptr::null_mut() } } + /// Move the owned `WTF::StringImpl` ref out as a [`WTFString`](crate::WTFString); + /// `None` (and `self` dropped) for the non-WTF tags. + #[inline] + pub fn into_wtf(self) -> Option { + if self.tag != Tag::WTFStringImpl { + return None; + } + // SAFETY: tag checked — a live, non-null `StringImpl` whose +1 we own + // and hand over. + Some(unsafe { crate::WTFString::adopt(self.leak_wtf_impl()) }) + } /// An isolated copy of a WTF-backed impl (+1, `clone()` for other tags), /// for handing the value to one other thread; not for sharing one impl /// between VMs. @@ -1055,6 +1066,13 @@ impl Drop for String { self.deref(); } } +impl From for String { + /// Re-wrap an owned `WTF::StringImpl` ref (no count change). + #[inline] + fn from(wtf: crate::WTFString) -> Self { + Self::adopt_wtf_impl(wtf.into_raw()) + } +} impl Clone for String { /// +1 on the same `WTF::StringImpl` (bitwise for the non-WTF tags). #[inline] diff --git a/src/codegen/generate-classes.ts b/src/codegen/generate-classes.ts index c9c4cb421db1..e764d76f001b 100644 --- a/src/codegen/generate-classes.ts +++ b/src/codegen/generate-classes.ts @@ -2214,6 +2214,38 @@ ${cachedExterns} let ok = ${symbolName(typeName, "dangerouslySetPtr")}(value, core::ptr::null_mut()); debug_assert!(ok); } + ${ + refCounted + ? `/// Take the wrapper's \`m_ctx\` back as the reference it held (what + /// its finalizer would otherwise release), leaving the wrapper detached. + /// \`None\` if \`value\` is not a live \`${typeName}\` wrapper. + #[inline] pub fn take_ref(value: JSValue) -> Option<::bun_ptr::RefPtr<${typeName}>> { + let ptr = ${symbolName(typeName, "fromJS")}(value); + if ptr.is_null() { + return None; + } + let ok = ${symbolName(typeName, "dangerouslySetPtr")}(value, core::ptr::null_mut()); + debug_assert!(ok); + // SAFETY: \`m_ctx\` carried the wrapper's ref; it was just cleared, so + // that ref is now ours. + Some(unsafe { ::bun_ptr::RefPtr::from_raw(ptr) }) + }` + : `/// Take the wrapper's \`m_ctx\` back as the \`Box\` its constructor handed + /// over (what its finalizer would otherwise receive), leaving the wrapper + /// detached. \`None\` if \`value\` is not a live \`${typeName}\` wrapper. + #[inline] pub fn take_ptr(value: JSValue) -> Option<::std::boxed::Box<${typeName}>> { + let ptr = ${symbolName(typeName, "fromJS")}(value); + if ptr.is_null() { + return None; + } + let ok = ${symbolName(typeName, "dangerouslySetPtr")}(value, core::ptr::null_mut()); + debug_assert!(ok); + // SAFETY: \`m_ctx\` is the \`heap::into_raw\` allocation the wrapper owned + // (its finalizer reclaims it the same way); it was just cleared, so this + // is the only owner. + Some(unsafe { ::std::boxed::Box::from_raw(ptr) }) + }` + } ${gcAccessors} }`; diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index eacf4d7598ba..283a90e3f597 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -1124,9 +1124,45 @@ use bun_jsc::{self, host_fn, CallFrame, JSGlobalObject, JSValue}; #[allow(dead_code, unreachable_pub, unused)] pub use ${rustPath} as ${name}; +/// The live \`m_sinkPtr\` of the \`JS${name}\` wrapper \`value\` encodes, or \`None\` +/// if it is not one or is detached. Frame-scoped like \`JSValue::as_class_this_ptr\`: +/// \`value\` keeps the payload alive while it is on the stack. +#[allow(dead_code, unreachable_pub, unused)] +pub fn ${name}__fromJSThis(value: JSValue) -> Option> { + // SAFETY: \`from_js\` returns the wrapper's live, non-null payload (\`JSSink\` + // is \`repr(transparent)\` over \`T\`). + ${JSSinkT}::from_js(value).map(|p| unsafe { bun_ptr::ThisPtr::new(p.cast::<${name}>()) }) +} + `; - const hostFns = ["construct", "write", "end", "flush", "start"] as const; + // `callframe.this()` → the wrapper's `m_sinkPtr` as `&mut JSSink`, or the + // detached / wrong-type error. Unbounded `'a`: the sink lives in its own heap + // allocation behind the wrapper; host fns are single-threaded and synchronous, + // so this is the only `&mut` for the body of the call. + templ += `#[allow(dead_code, unreachable_pub, unused, non_snake_case)] +fn ${name}__getThis<'a>( + global: &JSGlobalObject, + callframe: &CallFrame, +) -> bun_jsc::JsResult<&'a mut ${JSSinkT}> { + let ptr = ${JSSinkT}::this_ptr_from_frame(global, callframe)?; + // SAFETY: \`${name}__fromJS\` returned the wrapper's live, non-null \`m_sinkPtr\`. + Ok(unsafe { &mut *ptr.as_ptr() }) +} + +`; + + symbols.push(`${name}__construct`); + templ += `bun_jsc::jsc_host_abi! { + #[allow(dead_code, unreachable_pub, unused)] + #[unsafe(no_mangle)] + pub unsafe fn ${name}__construct(global: &JSGlobalObject, callframe: &CallFrame) -> JSValue { + host_fn::host_fn_static(global, callframe, ${JSSinkT}::js_construct) + } +} + +`; + const hostFns = ["write", "end", "flush", "start"] as const; for (const fn of hostFns) { const sym = `${name}__${fn}`; symbols.push(sym); @@ -1135,7 +1171,7 @@ pub use ${rustPath} as ${name}; #[allow(dead_code, unreachable_pub, unused)] #[unsafe(no_mangle)] pub unsafe fn ${sym}(global: &JSGlobalObject, callframe: &CallFrame) -> JSValue { - host_fn::host_fn_static(global, callframe, ${JSSinkT}::js_${fn}) + host_fn::host_fn_static(global, callframe, |g, c| ${JSSinkT}::js_${fn}(g, c, ${name}__getThis)) } } @@ -1172,8 +1208,15 @@ pub extern "C" fn ${name}__memoryCost(this: &${name}) -> usize { templ += `#[allow(dead_code, unreachable_pub, unused)] #[unsafe(no_mangle)] pub unsafe extern "C" fn ${name}__finalize(this: *mut ${name}) { - // SAFETY: C++ hands over its live \`m_sinkPtr\` once and never uses it again. - ${JSSinkT}::js_finalize(unsafe { bun_ptr::ThisPtr::new(this) }) + use crate::webcore::sink::{FinalizeReceiver, JsSinkType}; + match <${name} as JsSinkType>::FINALIZE { + // SAFETY: C++ hands over its live \`m_sinkPtr\` once and never uses it again. + FinalizeReceiver::ThisPtr => ${JSSinkT}::js_finalize(unsafe { bun_ptr::ThisPtr::new(this) }), + // SAFETY: as above; the sink's owner keeps it alive across the call. + FinalizeReceiver::Mut => <${name} as JsSinkType>::finalize_mut(unsafe { &mut *this }), + // SAFETY: the wrapper's \`m_sinkPtr\` is the Box \`construct\` leaked for it. + FinalizeReceiver::Box => <${name} as JsSinkType>::finalize_boxed(unsafe { Box::from_raw(this) }), + } } `; @@ -1184,8 +1227,15 @@ pub unsafe extern "C" fn ${name}__finalize(this: *mut ${name}) { templ += `#[allow(dead_code, unreachable_pub, unused)] #[unsafe(no_mangle)] pub unsafe extern "C" fn ${name}__controllerFinalize(this: *mut ${name}) { - // SAFETY: as \`__finalize\`: the controller's live \`m_sinkPtr\`, handed over once. - ${JSSinkT}::js_controller_finalize(unsafe { bun_ptr::ThisPtr::new(this) }) + use crate::webcore::sink::{FinalizeReceiver, JsSinkType}; + match <${name} as JsSinkType>::FINALIZE { + // SAFETY: as \`__finalize\`: the controller's live \`m_sinkPtr\`, handed over once. + FinalizeReceiver::ThisPtr => ${JSSinkT}::js_controller_finalize(unsafe { bun_ptr::ThisPtr::new(this) }), + // SAFETY: as \`__finalize\`. + FinalizeReceiver::Mut => <${name} as JsSinkType>::finalize_mut(unsafe { &mut *this }), + // SAFETY: as \`__finalize\`. + FinalizeReceiver::Box => <${name} as JsSinkType>::finalize_boxed(unsafe { Box::from_raw(this) }), + } } `; @@ -1241,6 +1291,109 @@ pub extern "C" fn ${name}__updateRef(this: &mut ${name}, value: bool) { `; } + // ── Bun__NativeTransformSink__writeBytes ────────────────────────────────── + // Route a borrowed byte chunk from a native transform (`JSTransformStream` + // with `m_nativeSinkPtr` attached) into the concrete sink via + // `SinkHandle::write`. `sink_id` is `WebCore::SinkID` (src/jsc/bindings/Sink.h); + // `sink_ptr` is the wrapper's `m_sinkPtr` (`*mut JSSink`, which is + // `repr(transparent)` over `T`). + const sinkIds: Record = { + ArrayBufferSink: 0, + FileSink: 2, + HTMLRewriterSink: 3, + HTTPResponseSink: 4, + HTTPSResponseSink: 5, + NetworkSink: 6, + FetchRequestBodySink: 7, + }; + // `SinkHandle` variant + `BackRef` constructor per sink. + const sinkHandles: Record = { + ArrayBufferSink: ["ArrayBuffer", "from_raw_mut"], + FileSink: ["FileSink", "from_raw"], + HTMLRewriterSink: ["HTMLRewriter", "from_raw"], + HTTPResponseSink: ["HttpResponse", "from_raw_mut"], + HTTPSResponseSink: ["HttpsResponse", "from_raw_mut"], + NetworkSink: ["S3Upload", "from_raw_mut"], + FetchRequestBodySink: ["FetchRequestBody", "from_raw_mut"], + }; + symbols.push("Bun__NativeTransformSink__writeBytes"); + templ += `/// Map a C++ \`WebCore::SinkID\` + erased \`m_sinkPtr\` to a \`SinkHandle\`. +/// +/// # Safety +/// \`ptr\` must be a live, properly-aligned pointer to the concrete sink type +/// that \`id\` names (the same pointer the per-sink thunks receive), valid for +/// the lifetime of the returned handle. +#[allow(dead_code, unreachable_pub, unused)] +pub unsafe fn sink_handle_from_id( + id: u8, + ptr: ::core::ptr::NonNull<::core::ffi::c_void>, +) -> crate::webcore::SinkHandle { + use crate::webcore::SinkHandle; + let raw = ptr.as_ptr(); + // SAFETY: caller contract. + unsafe { + match id { +${classes + .map( + name => + ` ${sinkIds[name]} => SinkHandle::${sinkHandles[name][0]}(bun_ptr::BackRef::${sinkHandles[name][1]}(raw.cast::<${name}>())),`, + ) + .join("\n")} + // 1 (TextSink) and any unknown id → no native sink. + _ => SinkHandle::None, + } + } +} + +/// See \`crate::webcore::sink::native_transform_sink_write\`. +#[allow(dead_code, unreachable_pub, unused)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Bun__NativeTransformSink__writeBytes( + sink_id: u8, + sink_ptr: *mut ::core::ffi::c_void, + global: &JSGlobalObject, + ptr: *const u8, + len: usize, +) -> JSValue { + let Some(sink_ptr) = ::core::ptr::NonNull::new(sink_ptr) else { + return JSValue::js_number(0.0); + }; + // SAFETY: C++ passes the live \`m_sinkPtr\` of the type \`sink_id\` names, valid + // for the duration of this synchronous call. + let handle = unsafe { sink_handle_from_id(sink_id, sink_ptr) }; + if ptr.is_null() { + return JSValue::js_number(0.0); + } + // SAFETY: C++ passes \`len\` live readable bytes at non-null \`ptr\` (a GC-kept + // \`JSArrayBufferView\` or a caller-owned scratch buffer). + let bytes = unsafe { ::bun_core::ffi::slice(ptr, len) }; + crate::webcore::sink::native_transform_sink_write(handle, global, bytes) +} + +`; + + // ── Bun__onSinkDestroyed ────────────────────────────────────────────────── + // The wrapper/controller's \`onDestroy\` tagged pointer (see + // \`crate::webcore::sink::DestructorPtr\`) decoded back to its pointee. + symbols.push("Bun__onSinkDestroyed"); + templ += `#[allow(dead_code, unreachable_pub, unused)] +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Bun__onSinkDestroyed( + ptr_value: *mut ::core::ffi::c_void, + _sink_ptr: *mut ::core::ffi::c_void, +) { + use crate::webcore::sink::{DestructorPtr, Destructor}; + match Destructor::decode(DestructorPtr::from(Some(ptr_value))) { + Destructor::None | Destructor::Detached => {} + // SAFETY: C++ round-trips the \`onDestroy\` value \`destructor_ptr_subprocess\` + // encoded from a live \`Subprocess\` that outlives its stdin sink wrapper. + Destructor::Subprocess(subprocess) => unsafe { &mut *subprocess }.on_stdin_destroyed(), + Destructor::Unknown => ::bun_core::debug_warn!("Unknown sink type"), + } +} + +`; + templ += `// sinks: ${classes.length}, exported symbols: ${symbols.length}\n`; return { src: templ, symbols }; } diff --git a/src/collections/pool.rs b/src/collections/pool.rs index 1fe739052dc4..7ea482245d56 100644 --- a/src/collections/pool.rs +++ b/src/collections/pool.rs @@ -346,6 +346,15 @@ where } } + /// [`get`](Self::get) that only reuses a pooled node (never allocates). + pub fn try_get() -> Option> { + Some(PoolGuard { + node: Self::get_if_exists()?, + release: Self::release, + _marker: PhantomData, + }) + } + /// Return a node to the pool's free list (or free it if the pool is full). /// /// Takes a raw `*mut Node`, not `&mut Node`: when the pool is already diff --git a/src/event_loop/ConcurrentTask.rs b/src/event_loop/ConcurrentTask.rs index 245c9500ea3c..d87603433eb4 100644 --- a/src/event_loop/ConcurrentTask.rs +++ b/src/event_loop/ConcurrentTask.rs @@ -164,6 +164,33 @@ pub trait Taskable { unsafe fn release_unrun(this: *mut Self); } +/// [`Taskable`] for a type that is only ever queued as a leaked `Box` +/// (`heap::into_raw` / `Box::into_raw` at every post site, `Box::from_raw` in +/// its `bun_runtime::dispatch` arm). Released unrun by reclaiming the box and +/// handing it to `|this| $release` (default: drop it). +/// +/// ```ignore +/// bun_event_loop::boxed_taskable!(ShellGlobTask, ShellGlobTask, |this| this.task.unref_unrun()); +/// ``` +#[macro_export] +macro_rules! boxed_taskable { + ($ty:ty, $tag:ident) => { + $crate::boxed_taskable!($ty, $tag, |this| ()); + }; + ($ty:ty, $tag:ident, |$this:ident| $release:expr) => { + impl $crate::Taskable for $ty { + const TAG: $crate::TaskTag = $crate::task_tag::$tag; + unsafe fn release_unrun(this: *mut Self) { + // SAFETY: `release_unrun` contract — `this` is the queued + // `Task::ptr` under `TAG`, which for this type is a leaked `Box`. + #[allow(unused_mut)] + let mut $this: ::std::boxed::Box = unsafe { ::bun_core::heap::take(this) }; + $release; + } + } + }; +} + impl TaskTag { /// The tag's identifier, for diagnostics. pub fn name(self) -> &'static str { diff --git a/src/io/PipeReader.rs b/src/io/PipeReader.rs index 60e18aa01ea2..1302da397e8a 100644 --- a/src/io/PipeReader.rs +++ b/src/io/PipeReader.rs @@ -91,6 +91,130 @@ pub trait BufferedReaderParent { } } +/// A [`BufferedReaderParent`] that holds its reader in a by-value field, so a +/// live parent is a live reader. Implemented by `impl_buffered_reader_parent!` +/// (`reader = ;`); lets the parent drive the raw-pointer entry points +/// ([`BufferedReader::read`], [`BufferedReader::on_error`]) from a +/// [`ThisPtr`](bun_ptr::ThisPtr) without materialising a reference that the +/// re-entrant dispatch could invalidate. +/// +/// # Safety +/// `reader` must be a place projection to a field inside `*this` (same +/// allocation), performing no reads. +pub unsafe trait BufferedReaderOwner: Sized { + fn reader(this: *mut Self) -> *mut BufferedReader; +} + +/// The field types `impl_buffered_reader_parent!`'s `reader = ;` accepts. +pub trait ReaderSlot { + fn raw(slot: *mut Self) -> *mut BufferedReader; +} +impl ReaderSlot for BufferedReader { + #[inline(always)] + fn raw(slot: *mut Self) -> *mut BufferedReader { + slot + } +} +impl ReaderSlot for bun_ptr::JsCell { + #[inline(always)] + fn raw(slot: *mut Self) -> *mut BufferedReader { + // `JsCell` is `repr(transparent)` over `UnsafeCell`. + slot.cast() + } +} + +/// Place projection for `impl_buffered_reader_parent!`'s `reader = ;`: +/// `this + offset`, typed by the (never called) field accessor. +#[doc(hidden)] +#[inline(always)] +pub fn reader_slot_ptr( + this: *mut T, + offset: usize, + _field: fn(&T) -> &S, +) -> *mut BufferedReader { + S::raw(this.wrapping_byte_add(offset).cast::()) +} + +impl BufferedReader { + /// The reader embedded in `parent`. The callbacks these entries dispatch + /// go to the registered parent (`set_parent`), which must be `parent` once + /// set. + #[inline] + fn embedded_in(parent: bun_ptr::ThisPtr

) -> *mut Self { + let reader = P::reader(parent.as_ptr()); + #[cfg(debug_assertions)] + { + // SAFETY: `parent` is live (`ThisPtr` invariant), so its embedded reader is. + let registered = unsafe { (*reader).vtable.parent }; + assert!( + registered.is_null() || registered == parent.as_ptr().cast::(), + "BufferedReader dispatched through a parent it was not registered with" + ); + } + reader + } + + /// [`read`](Self::read) on the reader embedded in `parent`. + #[inline] + pub fn read_from(parent: bun_ptr::ThisPtr

) { + // SAFETY: `parent` is live (`ThisPtr` invariant), so its embedded + // reader is; no reference to either is held across the dispatch. + unsafe { Self::read(Self::embedded_in(parent)) } + } + + /// [`on_error`](Self::on_error) on the reader embedded in `parent`. + #[inline] + pub fn on_error_from(parent: bun_ptr::ThisPtr

, err: sys::Error) { + // SAFETY: as `read_from`. + unsafe { Self::on_error(Self::embedded_in(parent), err) } + } + + /// [`read_into`](Self::read_into) on the reader embedded in `parent`. + #[inline] + pub fn read_into_from( + parent: bun_ptr::ThisPtr

, + dst: &mut [u8], + ) -> (usize, ReadState) { + // SAFETY: as `read_from`. + unsafe { Self::read_into(Self::embedded_in(parent), dst) } + } + + /// [`close`](Self::close) on the reader embedded in `parent`. The done + /// callback it dispatches re-enters `parent`, so no borrow of the reader + /// may be live in the caller. + #[inline] + pub fn close_from(parent: bun_ptr::ThisPtr

) { + // SAFETY: as `read_from`. + unsafe { (*Self::embedded_in(parent)).close() } + } + + /// [`start`](Self::start) / [`start_file_offset`](Self::start_file_offset) + /// on the reader embedded in `parent`; a registration error re-enters + /// `parent` through `on_reader_error`. + #[inline] + pub fn start_from( + parent: bun_ptr::ThisPtr

, + fd: Fd, + is_pollable: bool, + offset: Option, + ) -> sys::Result<()> { + // SAFETY: as `read_from`. + let reader = unsafe { &mut *Self::embedded_in(parent) }; + match offset { + Some(offset) => reader.start_file_offset(fd, is_pollable, offset), + None => reader.start(fd, is_pollable), + } + } + + /// [`unpause`](Self::unpause) on the reader embedded in `parent` (on + /// Windows an exhausted limit reports EOF to `parent` from inside). + #[inline] + pub fn unpause_from(parent: bun_ptr::ThisPtr

) { + // SAFETY: as `read_from`. + unsafe { (*Self::embedded_in(parent)).unpause() } + } +} + impl BufferedReaderVTable { fn init() -> BufferedReaderVTable { BufferedReaderVTable { diff --git a/src/io/lib.rs b/src/io/lib.rs index a41b2f6ade24..4a2b1c5ffad1 100644 --- a/src/io/lib.rs +++ b/src/io/lib.rs @@ -545,23 +545,145 @@ bun_dispatch::link_interface! { /// case where the inherent takes `&self`/`&mut self`; sites whose inherent must /// stay raw-pointer (e.g. `Arc::from_raw` keepalive in shell `PipeReader`) /// forward as `::method(this)` instead. +/// +/// With a leading `borrow = this;` the bodies instead get `this` bound as a +/// [`bun_ptr::ThisPtr`] and are pasted as-is (no `unsafe` block) — for +/// intrusively refcounted parents whose handlers are safe fns taking +/// `ThisPtr` and may drop their last ref mid-callback. An optional +/// `reader = ;` (or a nested `a.b` path) names the by-value field (a +/// `BufferedReader` or a `JsCell`) holding the reader; it implements +/// [`pipe_reader::BufferedReaderOwner`] so the parent can drive the reader +/// through [`BufferedReader::read_from`] / [`BufferedReader::on_error_from`]. #[macro_export] macro_rules! impl_buffered_reader_parent { // Single-lifetime generic: trait impl over `<'lt>`, link registered at `'static`. ( $variant:ident for $T:ident<$lt:lifetime>; - $($rest:tt)* + borrow = this; + $( reader = $($reader:ident).+; )? + has_on_read_chunk = $($rest:tt)* + ) => { + $crate::buffered_reader_parent_link!($variant for $T<'static>); + $( $crate::__impl_buffered_reader_owner! { [$lt] [$T<$lt>] $($reader).+ } )? + $crate::__impl_buffered_reader_parent_body_this! { [$lt] [$T<$lt>] $variant; has_on_read_chunk = $($rest)* } + }; + ( + $variant:ident for $T:ident<$lt:lifetime>; + $( reader = $($reader:ident).+; )? + has_on_read_chunk = $($rest:tt)* ) => { $crate::buffered_reader_parent_link!($variant for $T<'static>); - $crate::__impl_buffered_reader_parent_body! { [$lt] [$T<$lt>] $variant; $($rest)* } + $( $crate::__impl_buffered_reader_owner! { [$lt] [$T<$lt>] $($reader).+ } )? + $crate::__impl_buffered_reader_parent_body! { [$lt] [$T<$lt>] $variant; has_on_read_chunk = $($rest)* } }; // Non-generic. ( $variant:ident for $T:ty; - $($rest:tt)* + borrow = this; + $( reader = $($reader:ident).+; )? + has_on_read_chunk = $($rest:tt)* ) => { $crate::buffered_reader_parent_link!($variant for $T); - $crate::__impl_buffered_reader_parent_body! { [] [$T] $variant; $($rest)* } + $( $crate::__impl_buffered_reader_owner! { [] [$T] $($reader).+ } )? + $crate::__impl_buffered_reader_parent_body_this! { [] [$T] $variant; has_on_read_chunk = $($rest)* } + }; + ( + $variant:ident for $T:ty; + $( reader = $($reader:ident).+; )? + has_on_read_chunk = $($rest:tt)* + ) => { + $crate::buffered_reader_parent_link!($variant for $T); + $( $crate::__impl_buffered_reader_owner! { [] [$T] $($reader).+ } )? + $crate::__impl_buffered_reader_parent_body! { [] [$T] $variant; has_on_read_chunk = $($rest)* } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __impl_buffered_reader_owner { + ([$($lt:lifetime)?] [$T:ty] $($reader:ident).+) => { + // SAFETY: `$reader` is a by-value field (path) of `$T`, so the + // projection stays inside `*this`'s allocation. + unsafe impl $(<$lt>)? $crate::pipe_reader::BufferedReaderOwner for $T { + #[inline] + fn reader(this: *mut Self) -> *mut $crate::BufferedReader { + $crate::pipe_reader::reader_slot_ptr( + this, + ::core::mem::offset_of!(Self, $($reader).+), + |p: &Self| &p.$($reader).+, + ) + } + } + }; +} + +#[doc(hidden)] +#[macro_export] +macro_rules! __impl_buffered_reader_parent_body_this { + ( + [$($lt:lifetime)?] [$T:ty] $variant:ident; + has_on_read_chunk = $has:expr; + $( on_read_chunk = |$rc_this:ident, $rc_chunk:ident, $rc_more:ident| $rc:expr; )? + on_reader_done = |$rd_this:ident| $rd:expr; + on_reader_error = |$re_this:ident, $re_err:ident| $re:expr; + loop_ = |$l_this:ident| $lp:expr; + event_loop = |$e_this:ident| $ev:expr; + $( ref_ = |$rf_this:ident| $rf:expr; )? + $( deref = |$dr_this:ident| $dr:expr; )? + ) => { + // SAFETY (all generated methods): `this` is the `*mut Self` registered + // via `set_parent` — the live parent's root pointer for as long as the + // reader it embeds is being called, which is `ThisPtr::new`'s contract. + impl $(<$lt>)? $crate::pipe_reader::BufferedReaderParent for $T { + const KIND: $crate::BufferedReaderParentLinkKind = + $crate::BufferedReaderParentLinkKind::$variant; + const HAS_ON_READ_CHUNK: bool = $has; + $( + unsafe fn on_read_chunk( + this: *mut Self, + $rc_chunk: $crate::Chunk<'_>, + $rc_more: $crate::ReadState, + ) -> bool { + // SAFETY: see impl-level note. + let $rc_this = unsafe { $crate::__bun_ptr::ThisPtr::::new(this) }; + $rc + } + )? + unsafe fn on_reader_done(this: *mut Self) { + // SAFETY: see impl-level note. + let $rd_this = unsafe { $crate::__bun_ptr::ThisPtr::::new(this) }; + $rd + } + unsafe fn on_reader_error(this: *mut Self, $re_err: $crate::__bun_sys::Error) { + // SAFETY: see impl-level note. + let $re_this = unsafe { $crate::__bun_ptr::ThisPtr::::new(this) }; + $re + } + unsafe fn loop_(this: *mut Self) -> *mut $crate::pipe_reader::Loop { + // SAFETY: see impl-level note. + let $l_this = unsafe { $crate::__bun_ptr::ThisPtr::::new(this) }; + $lp + } + unsafe fn event_loop(this: *mut Self) -> $crate::EventLoopHandle { + // SAFETY: see impl-level note. + let $e_this = unsafe { $crate::__bun_ptr::ThisPtr::::new(this) }; + $ev + } + $( + unsafe fn ref_(this: *mut Self) { + // SAFETY: see impl-level note. + let $rf_this = unsafe { $crate::__bun_ptr::ThisPtr::::new(this) }; + $rf + } + )? + $( + unsafe fn deref(this: *mut Self) { + // SAFETY: see impl-level note. + let $dr_this = unsafe { $crate::__bun_ptr::ThisPtr::::new(this) }; + $dr + } + )? + } }; } @@ -628,6 +750,8 @@ macro_rules! __impl_buffered_reader_parent_body { }; } +#[doc(hidden)] +pub use bun_ptr as __bun_ptr; #[doc(hidden)] pub use bun_sys as __bun_sys; @@ -666,7 +790,7 @@ pub use source::Source; // Stub for never-constructed-on-POSIX `Source` so cross-platform sigs // (`Option`) typecheck. -pub use pipe_reader::{BufferedReader, BufferedReaderParent, PosixFlags}; +pub use pipe_reader::{BufferedReader, BufferedReaderOwner, BufferedReaderParent, PosixFlags}; pub use open_for_writing_mod::{open_for_writing, open_for_writing_impl}; diff --git a/src/jsc/AbortSignal.rs b/src/jsc/AbortSignal.rs index 5e5e64bb4e05..16684d6a8f65 100644 --- a/src/jsc/AbortSignal.rs +++ b/src/jsc/AbortSignal.rs @@ -73,6 +73,34 @@ pub trait AbortListener { fn on_abort(&mut self, reason: JSValue); } +/// A native `abort` listener registered with [`AbortSignal::listen_native`]; +/// called on the signal's JS thread. +pub trait NativeAbortListener: Sized { + fn on_abort(this: bun_ptr::ThisPtr, reason: JSValue); +} + +/// A native listener's registration on an [`AbortSignal`]: holds a reference +/// on the signal and one unit of its pending activity, and removes the +/// listener when dropped. The listener keeps this for as long as it lives at +/// the address it registered. +pub struct AbortListenerRegistration { + signal: AbortSignalRef, + ctx: *mut c_void, +} + +impl AbortListenerRegistration { + pub fn signal(&self) -> &AbortSignal { + &self.signal + } +} + +impl Drop for AbortListenerRegistration { + fn drop(&mut self) { + self.signal.clean_native_bindings(self.ctx); + self.signal.pending_activity_unref(); + } +} + impl AbortSignal { pub fn listen(&self, ctx: *mut C) -> &AbortSignal { extern "C" fn callback(ptr: *mut c_void, reason: JSValue) { @@ -84,6 +112,25 @@ impl AbortSignal { self.add_listener(ctx.cast::(), callback::) } + /// Call `C::on_abort` on `listener`'s pointee when the signal aborts, for + /// as long as the returned registration is held. + pub fn listen_native( + &self, + listener: bun_ptr::BackRef, + ) -> AbortListenerRegistration { + extern "C" fn callback(ptr: *mut c_void, reason: JSValue) { + // SAFETY: `ptr` is the root pointer `listen_native` registered; its + // pointee holds the registration, which unregisters this callback + // before the pointee goes away. + C::on_abort(unsafe { bun_ptr::ThisPtr::new(ptr.cast::()) }, reason); + } + let ctx = listener.this_ptr().as_ptr().cast::(); + let signal = self.retain(); + self.pending_activity_ref(); + self.add_listener(ctx, callback::); + AbortListenerRegistration { signal, ctx } + } + pub fn add_listener( &self, ctx: *mut c_void, @@ -164,6 +211,13 @@ impl AbortSignal { unsafe { AbortSignalRef::adopt(WebCore__AbortSignal__ref(self)) } } + /// Take a counted reference on this signal. + pub fn retain(&self) -> AbortSignalRef { + // SAFETY: `&AbortSignal` only exists for a live C++ `WebCore::AbortSignal` + // (opaque FFI handle); `ref_()` returns it with the count bumped. + unsafe { AbortSignalRef::adopt(self.ref_()) } + } + pub fn unref(&self) { WebCore__AbortSignal__unref(self) } diff --git a/src/jsc/FetchHeaders.rs b/src/jsc/FetchHeaders.rs index f2d4eb6fb7c6..5a0c71b05529 100644 --- a/src/jsc/FetchHeaders.rs +++ b/src/jsc/FetchHeaders.rs @@ -308,3 +308,114 @@ impl FetchHeaders { // `WebCore__FetchHeaders__put` extern decl above and the `fast_*` methods take // it by value, so the re-export is ABI-transparent. pub use bun_http_types::Method::HeaderName as HTTPHeaderName; + +/// RAII handle to a C++-owned `WebCore::FetchHeaders`. +/// +/// Holds exactly one ref on the C++ intrusive refcount; `Drop` releases it via +/// `WebCore__FetchHeaders__deref`. NOT a `std::rc::Rc` (the payload lives on +/// the C++ heap and is opaque here). +/// +/// Intentionally not `Clone`: the only "share" operation the surface +/// exposes is `clone_this()`, which deep-copies a fresh `FetchHeaders` on the +/// C++ side. Transferring ownership is by-move. +#[repr(transparent)] +pub struct HeadersRef(NonNull); + +impl HeadersRef { + /// Adopt a freshly-created `FetchHeaders*` (refcount already 1). + /// + /// # Safety + /// `ptr` must be a valid `WebCore::FetchHeaders*` and the caller must + /// transfer ownership of one ref. + #[inline] + pub unsafe fn adopt(ptr: NonNull) -> Self { + Self(ptr) + } + + #[inline] + pub fn as_ptr(&self) -> *mut FetchHeaders { + self.0.as_ptr() + } + + /// `FetchHeaders.createEmpty()` — fresh C++ allocation, refcount 1. + #[inline] + pub fn create_empty() -> Self { + // SAFETY: C++ allocates a new FetchHeaders with refcount 1; never null. + unsafe { Self::adopt(FetchHeaders::create_empty()) } + } + + /// `FetchHeaders.createFromUWS(req)` — fresh C++ allocation, refcount 1. + #[inline] + pub fn create_from_uws(uws_request: *mut core::ffi::c_void) -> Self { + // SAFETY: C++ allocates a new FetchHeaders with refcount 1; never null. + unsafe { Self::adopt(FetchHeaders::create_from_uws(uws_request)) } + } + + /// `FetchHeaders.createFromPicoHeaders(list)` — fresh C++ allocation, refcount 1. + #[inline] + pub fn create_from_pico_headers(pico_headers_list: &[bun_http::picohttp::Header]) -> Self { + // SAFETY: C++ allocates a new FetchHeaders with refcount 1; never null. + unsafe { Self::adopt(FetchHeaders::create_from_pico_headers(pico_headers_list)) } + } + + /// `FetchHeaders.createFromJS(global, value)` — may throw, may return null. + #[inline] + pub fn create_from_js(global: &JSGlobalObject, value: JSValue) -> JsResult> { + // SAFETY: C++ returns a +1 ref or null. + Ok(FetchHeaders::create_from_js(global, value)?.map(|p| unsafe { Self::adopt(p) })) + } + + /// `FetchHeaders.cloneThis(global)` — deep copy on the C++ side. + #[inline] + pub fn clone_this(&self, global: &JSGlobalObject) -> JsResult> { + Self::clone_from(bun_opaque::opaque_deref_mut(self.0.as_ptr()), global) + } + + /// Deep-copy `headers` (e.g. a JS `Headers` object's) into a fresh handle. + #[inline] + pub fn clone_from( + headers: &mut FetchHeaders, + global: &JSGlobalObject, + ) -> JsResult> { + // SAFETY: C++ returns a +1 ref or null. + Ok(headers + .clone_this(global)? + .map(|p| unsafe { Self::adopt(p) })) + } + + /// The C++ object, whose accessors take `&mut` (opaque ZST handle, S008). + #[inline] + #[allow(clippy::mut_from_ref)] + pub fn headers(&self) -> &mut FetchHeaders { + bun_opaque::opaque_deref_mut(self.0.as_ptr()) + } +} + +impl core::ops::Deref for HeadersRef { + type Target = FetchHeaders; + #[inline] + fn deref(&self) -> &FetchHeaders { + // `FetchHeaders` is an opaque ZST FFI handle (S008); `self.0` is live + // for the lifetime of `self` — safe `*const → &` via `opaque_deref`. + bun_opaque::opaque_deref(self.0.as_ptr()) + } +} + +impl core::ops::DerefMut for HeadersRef { + #[inline] + fn deref_mut(&mut self) -> &mut FetchHeaders { + // `FetchHeaders` is an opaque ZST FFI handle (S008); `self.0` is live + // for the lifetime of `self` — safe `*mut → &mut` via `opaque_deref_mut`. + bun_opaque::opaque_deref_mut(self.0.as_ptr()) + } +} + +impl Drop for HeadersRef { + #[inline] + fn drop(&mut self) { + // `self.0` is live; releasing our +1 ref via WebCore__FetchHeaders__deref. + // Explicit UFCS to avoid `core::ops::Deref::deref` resolution ambiguity. + // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. + FetchHeaders::deref(bun_opaque::opaque_deref_mut(self.0.as_ptr())); + } +} diff --git a/src/jsc/JSValue.rs b/src/jsc/JSValue.rs index e9852689e0a6..853fb9fbf104 100644 --- a/src/jsc/JSValue.rs +++ b/src/jsc/JSValue.rs @@ -945,13 +945,14 @@ impl JSValue { } T::from_js(self) } - /// `JSValue.asDirect(T)` — unchecked-prototype downcast. - /// Caller must have already verified `is_cell()`; dispatches via + /// `JSValue.asDirect(T)` — unchecked-prototype downcast: dispatches via /// [`JsClass::from_js_direct`] (skips the prototype-chain walk that `as_` - /// performs, so subclasses are *not* matched). + /// performs, so subclasses are *not* matched). `None` for non-cells. #[inline] pub fn as_direct(self) -> Option<*mut T> { - debug_assert!(self.is_cell()); + if !self.is_cell() { + return None; + } T::from_js_direct(self) } /// Safe shared-borrow downcast — `as_()` followed by `&*ptr`. @@ -985,6 +986,14 @@ impl JSValue { self.as_::().map(|p| unsafe { &*p }) } + /// [`as_class_ref`](Self::as_class_ref) without the prototype-chain walk + /// ([`as_direct`](Self::as_direct)): subclasses are not matched. + #[inline] + pub fn as_direct_class_ref(self) -> Option<&'static T> { + // SAFETY: as for `as_class_ref`. + self.as_direct::().map(|p| unsafe { &*p }) + } + /// [`as_class_ref`](Self::as_class_ref) as a [`ThisPtr`](bun_ptr::ThisPtr), /// for `m_ctx` payloads that are intrusively refcounted: lets the caller /// take its own ref (`RefPtr::from_this`) or dispatch into a diff --git a/src/jsc/lib.rs b/src/jsc/lib.rs index 27696ef5e79e..a09de2e03049 100644 --- a/src/jsc/lib.rs +++ b/src/jsc/lib.rs @@ -765,7 +765,9 @@ pub use self::dom_form_data::DOMFormData; pub use self::url::{URL, URLJsc}; pub use self::zig_stack_frame::ZigStackFrame; pub use self::zig_stack_trace::ZigStackTrace; -pub use abort_signal::{AbortSignal, AbortSignalRef}; +pub use abort_signal::{ + AbortListenerRegistration, AbortSignal, AbortSignalRef, NativeAbortListener, +}; // `VM` / `JSGlobalObject` — opaque FFI handles to C++-owned objects. Defined // once in their dedicated port files (`VM.rs` / `JSGlobalObject.rs`) and @@ -895,7 +897,7 @@ pub use self::resolved_source_tag::ResolvedSourceTag; // ────────────────────────────────────────────────────────────────────────── #[path = "FetchHeaders.rs"] pub mod fetch_headers; -pub use self::fetch_headers::{FetchHeaders, HTTPHeaderName}; +pub use self::fetch_headers::{FetchHeaders, HTTPHeaderName, HeadersRef}; /// `BuiltinName` — fast-path property keys preallocated as `JSC::Identifier`s /// in C++ (`BunBuiltinNames.h`). Passed to `JSValue::fast_get` as a `u8` index diff --git a/src/jsc/webcore_types.rs b/src/jsc/webcore_types.rs index 474eb0e864c5..4b91e84ccc56 100644 --- a/src/jsc/webcore_types.rs +++ b/src/jsc/webcore_types.rs @@ -1128,6 +1128,16 @@ pub mod store { // live; `as_ptr` carries the allocation's provenance. unsafe { &mut (*this.as_ptr()).data } } + + /// Overwrite `mime_type` through the shared handle (same discipline + /// as [`data_mut`](Self::data_mut)). + #[inline] + pub fn set_mime_type(this: &RefPtr, mime_type: MimeType) { + // SAFETY: single-threaded JS event-loop discipline — no `&`/`&mut` + // to `mime_type` is held across this write; `as_ptr` carries the + // allocation's provenance. + unsafe { (*this.as_ptr()).mime_type = mime_type }; + } } // SAFETY: `Store`'s refcount is atomic and its payload is either diff --git a/src/ptr/js_cell.rs b/src/ptr/js_cell.rs index 48833384558f..c241346b4c3b 100644 --- a/src/ptr/js_cell.rs +++ b/src/ptr/js_cell.rs @@ -104,9 +104,8 @@ impl JsCell { /// Replace the contained value, returning the old one. #[inline(always)] pub fn replace(&self, value: T) -> T { - // Route through the single audited `with_mut` site; the `&mut T` is - // closure-scoped so no aliasing obligation leaks to this fn. - self.with_mut(|slot| core::mem::replace(slot, value)) + // SAFETY: as `set`. + unsafe { core::mem::replace(&mut *self.0.get(), value) } } /// Raw pointer to the inner `T` — for FFI / `addr_of!` paths that must @@ -115,6 +114,12 @@ impl JsCell { pub const fn as_ptr(&self) -> *mut T { self.0.get() } + + /// Unwrap the value. + #[inline(always)] + pub fn into_inner(self) -> T { + self.0.into_inner() + } } impl JsCell { diff --git a/src/ptr/lib.rs b/src/ptr/lib.rs index c0b1e9623639..13b76d8a735b 100644 --- a/src/ptr/lib.rs +++ b/src/ptr/lib.rs @@ -660,6 +660,14 @@ impl ThisPtr { self.0.as_ptr() } + /// Record this pointer as a write-capable back-reference (for handle enums + /// whose dispatcher forms the `&mut`). The holder takes on the `BackRef` + /// invariant. + #[inline] + pub fn backref_mut(self) -> BackRef { + BackRef(self.0, core::marker::PhantomData) + } + /// Fresh shared borrow of the pointee. /// /// Sound under the [`new`](Self::new) invariant: the pointee is live and @@ -690,6 +698,49 @@ impl core::ops::Deref for ThisPtr { } } +// ───────────────────────────────────────────────────────────────────────────── +// OwnedThis — single-owner heap allocation that hands out `ThisPtr`s. +// +// `Box` asserts unique access on every touch, which is wrong for a callback +// hub whose address is also held by C / JS / a task queue and re-entered while +// a method on it is running. `OwnedThis` keeps the ownership (drop frees) but +// only ever lends the pointee as `ThisPtr` / `&T`. +// ───────────────────────────────────────────────────────────────────────────── + +/// The unique owner of a heap-allocated `T` that is otherwise reached through +/// [`ThisPtr`] copies. Dropping it drops and frees the `T`; every `ThisPtr` +/// lent from it must be dead by then (the usual back-reference obligation). +pub struct OwnedThis(core::ptr::NonNull); + +impl OwnedThis { + #[inline] + pub fn new(value: T) -> Self { + OwnedThis(core::ptr::NonNull::from(Box::leak(Box::new(value)))) + } + + /// A dispatch handle to the pointee (root provenance). + #[inline] + pub fn this_ptr(&self) -> ThisPtr { + ThisPtr(self.0) + } +} + +impl core::ops::Deref for OwnedThis { + type Target = T; + #[inline] + fn deref(&self) -> &T { + // SAFETY: we own the live allocation. + unsafe { self.0.as_ref() } + } +} + +impl Drop for OwnedThis { + fn drop(&mut self) { + // SAFETY: `new` leaked exactly this `Box`; we are its unique owner. + drop(unsafe { Box::from_raw(self.0.as_ptr()) }); + } +} + // SAFETY: `BackRef` is morally `&T` (Deref/get) with, for `P = Mut`, an // unsafe `get_mut` escape hatch whose exclusivity is the caller's per-site // obligation. Match `&T` auto-trait bounds: `&T: Send ⇔ T: Sync`, diff --git a/src/ptr/ref_count.rs b/src/ptr/ref_count.rs index 7c530fc3500c..6513c33fbaaf 100644 --- a/src/ptr/ref_count.rs +++ b/src/ptr/ref_count.rs @@ -541,6 +541,16 @@ impl RefPtr { Self(ptr) } + /// Adopt a freshly boxed `T` (its embedded count is the initial 1) as its + /// first `RefPtr`. + #[inline] + pub fn from_box(boxed: Box) -> Self { + let ptr = bun_core::heap::into_raw_nn(boxed); + // SAFETY: sole owner of a live heap `T`. + debug_assert!(unsafe { T::rc_has_one_ref(ptr.as_ptr()) }); + Self(ptr) + } + /// [`new`](Self::new) for a `T` that stores its own root pointer (to hand /// out [`ThisPtr`](crate::ThisPtr)s from `&self` entry points). `init` /// receives a [`SelfRoot`](crate::SelfRoot) to store in the value; the diff --git a/src/ptr/weak_ptr.rs b/src/ptr/weak_ptr.rs index 1f013acfeecd..7c3814b7b128 100644 --- a/src/ptr/weak_ptr.rs +++ b/src/ptr/weak_ptr.rs @@ -1,63 +1,99 @@ +use core::cell::Cell; use core::ptr::NonNull; /// Bit layout: /// bits 0..=30 → reference_count /// bit 31 → finalized #[repr(transparent)] -#[derive(Copy, Clone, Eq, PartialEq)] -pub struct WeakPtrData(u32); +pub struct WeakPtrData(Cell); impl WeakPtrData { - pub const EMPTY: Self = Self(0); // reference_count = 0, finalized = false + pub const EMPTY: Self = Self(Cell::new(0)); // reference_count = 0, finalized = false const REF_MASK: u32 = 0x7FFF_FFFF; // low 31 bits const FINALIZED_BIT: u32 = 0x8000_0000; // bit 31 #[inline] - pub(crate) fn reference_count(self) -> u32 { - self.0 & Self::REF_MASK + pub(crate) fn reference_count(&self) -> u32 { + self.0.get() & Self::REF_MASK } #[inline] - pub(crate) fn set_reference_count(&mut self, n: u32) { + pub(crate) fn set_reference_count(&self, n: u32) { debug_assert!(n <= Self::REF_MASK); - self.0 = (self.0 & Self::FINALIZED_BIT) | (n & Self::REF_MASK); + self.0 + .set((self.0.get() & Self::FINALIZED_BIT) | (n & Self::REF_MASK)); } #[inline] - pub(crate) fn finalized(self) -> bool { - (self.0 & Self::FINALIZED_BIT) != 0 + pub(crate) fn finalized(&self) -> bool { + (self.0.get() & Self::FINALIZED_BIT) != 0 } #[inline] - pub(crate) fn set_finalized(&mut self, v: bool) { + pub(crate) fn set_finalized(&self, v: bool) { if v { - self.0 |= Self::FINALIZED_BIT; + self.0.set(self.0.get() | Self::FINALIZED_BIT); } else { - self.0 &= !Self::FINALIZED_BIT; + self.0.set(self.0.get() & !Self::FINALIZED_BIT); } } - pub fn on_finalize(&mut self) -> bool { + pub fn on_finalize(&self) -> bool { debug_assert!(!self.finalized()); self.set_finalized(true); self.reference_count() == 0 } } +impl Default for WeakPtrData { + fn default() -> Self { + Self::EMPTY + } +} + /// Implemented by types that embed a `WeakPtrData` field and can be weakly /// referenced via `WeakPtr`. /// -/// The field projection is a trait method (typically implemented via -/// `core::mem::offset_of!`). +/// The owner of such a value gives it up through [`finalize_owner`] (or, for an +/// intrusively refcounted value, [`destroy_weakly_held`] as its +/// `CellRefCounted::destroy`), which frees the allocation only once no +/// `WeakPtr` holds it. pub trait HasWeakPtrData { - /// Return a pointer to the embedded `WeakPtrData` field on `this`. - /// - /// # Safety - /// `this` must point to a live allocation of `Self` (the inner contents - /// may already be finalized, but the allocation itself must not yet be - /// freed). - unsafe fn weak_ptr_data(this: *mut Self) -> *mut WeakPtrData; + /// The embedded `WeakPtrData` field. + fn weak_ptr_data(&self) -> &WeakPtrData; + + /// The owner is done with the value but a `WeakPtr` still holds its + /// allocation: release what the value owns, leaving it valid (its later + /// drop must be a no-op release) until the last `WeakPtr` frees it. + fn finalize_contents(&self); +} + +/// The owner's release of a weakly-referenceable value. Frees `owner` now if +/// no [`WeakPtr`] holds it; otherwise marks it finalized, runs +/// [`HasWeakPtrData::finalize_contents`], and leaves the allocation to the last +/// `WeakPtr` (whose [`get`](WeakPtr::get) reads `None` from here on). +pub fn finalize_owner(owner: Box) { + let this = Box::into_raw(owner); + // SAFETY: `this` was just leaked from the `Box` we own; live and non-null. + let value = unsafe { &*this }; + if value.weak_ptr_data().on_finalize() { + // SAFETY: no `WeakPtr` holds the allocation, so we are its sole owner. + drop(unsafe { Box::from_raw(this) }); + } else { + value.finalize_contents(); + } +} + +/// [`finalize_owner`] for an intrusively refcounted `T` whose count reached +/// zero — the shape `#[ref_count(destroy = …)]` takes. +/// +/// # Safety +/// `this` is the sole live owner of a `Box`-allocated `T` (the +/// `CellRefCounted::destroy` contract). +pub unsafe fn destroy_weakly_held(this: *mut T) { + // SAFETY: fn contract. + finalize_owner(unsafe { Box::from_raw(this) }); } /// Allow a type to be weakly referenced. This keeps a reference count of how @@ -86,16 +122,16 @@ impl WeakPtr { /// Take a weak reference to `this`, incrementing its weak count. /// /// # Safety - /// `this` must be non-null and point to a live, not-yet-finalized `T`. - /// It must carry the provenance of the whole allocation (as produced by + /// `this` must be non-null and point to a live, not-yet-finalized, + /// `Box`-allocated `T` whose owner releases it only through + /// [`finalize_owner`] / [`destroy_weakly_held`]. It must carry the + /// provenance of the whole allocation (as produced by /// `bun_core::heap::into_raw` / `Box::into_raw`), **not** a reborrow of a /// `&mut T` — see the [type-level note](WeakPtr#provenance). pub unsafe fn init_ref(this: *mut T) -> Self { debug_assert!(!this.is_null()); - // SAFETY: caller contract — `this` points to a live `T`. Projecting - // straight to the embedded field means no whole-struct `&mut T` is - // formed, so `this`'s provenance reaches the stored pointer intact. - let d = unsafe { &mut *T::weak_ptr_data(this) }; + // SAFETY: caller contract — `this` points to a live `T`. + let d = unsafe { &*this }.weak_ptr_data(); debug_assert!(!d.finalized()); d.set_reference_count(d.reference_count() + 1); Self { @@ -114,7 +150,7 @@ impl WeakPtr { if let Some(value) = self.raw_ptr { // SAFETY: allocation is live while any WeakPtr holds it (see above). unsafe { - if !(*T::weak_ptr_data(value.as_ptr())).finalized() { + if !value.as_ref().weak_ptr_data().finalized() { return Some(&mut *value.as_ptr()); } self.deref_internal(value); @@ -128,17 +164,18 @@ impl WeakPtr { /// allocation whose embedded `WeakPtrData` has `reference_count > 0`. unsafe fn deref_internal(&mut self, value: NonNull) { self.raw_ptr = None; - // SAFETY: caller guarantees `value` points to a live allocation; - // projecting to the embedded `WeakPtrData` field. - let weak_data = unsafe { &mut *T::weak_ptr_data(value.as_ptr()) }; - let count = weak_data.reference_count() - 1; - weak_data.set_reference_count(count); - let finalized = weak_data.finalized(); + let (count, finalized) = { + // SAFETY: caller guarantees `value` points to a live allocation. + let weak_data = unsafe { value.as_ref() }.weak_ptr_data(); + let count = weak_data.reference_count() - 1; + weak_data.set_reference_count(count); + (count, weak_data.finalized()) + }; if finalized && count == 0 { // The allocation came from `heap::alloc` (via `Box::new`). // SAFETY: this is the last reference and the owner has finalized, - // so we hold the only pointer to a `Box`-allocated `T`. `weak_data` - // is dead here, so freeing through `value` disturbs no live borrow. + // so we hold the only pointer to a `Box`-allocated `T`. No borrow of + // it is live here, so freeing through `value` disturbs nothing. drop(unsafe { bun_core::heap::take(value.as_ptr()) }); } } @@ -167,6 +204,7 @@ mod tests { use std::sync::{Mutex, MutexGuard, PoisonError}; static DROPS: AtomicUsize = AtomicUsize::new(0); + static FINALIZED_CONTENTS: AtomicUsize = AtomicUsize::new(0); /// `DROPS` is process-wide but libtest runs `#[test]`s on parallel threads, /// so every test asserting on it holds this for its duration. @@ -184,7 +222,7 @@ mod tests { weak: WeakPtrData, /// Inline (not behind a `Box`) so writing it is a write into the /// `Owner` allocation itself — the access a stale handle trips on. - payload: u32, + payload: Cell, /// Proves the allocation is actually freed rather than merely leaked. _heap: Box, } @@ -196,23 +234,25 @@ mod tests { } impl HasWeakPtrData for Owner { - unsafe fn weak_ptr_data(this: *mut Self) -> *mut WeakPtrData { - // SAFETY: caller contract — pure field projection, no read. - unsafe { &raw mut (*this).weak } + fn weak_ptr_data(&self) -> &WeakPtrData { + &self.weak + } + fn finalize_contents(&self) { + FINALIZED_CONTENTS.fetch_add(1, Ordering::SeqCst); } } fn new_owner(payload: u32) -> *mut Owner { bun_core::heap::into_raw(Box::new(Owner { weak: WeakPtrData::EMPTY, - payload, + payload: Cell::new(payload), _heap: Box::new(payload), })) } #[test] fn bit_layout() { - let mut d = WeakPtrData::EMPTY; + let d = WeakPtrData::EMPTY; assert_eq!(d.reference_count(), 0); assert!(!d.finalized()); @@ -234,10 +274,10 @@ mod tests { #[test] fn on_finalize_reports_last_ref() { - let mut d = WeakPtrData::EMPTY; + let d = WeakPtrData::EMPTY; assert!(d.on_finalize()); - let mut d = WeakPtrData::EMPTY; + let d = WeakPtrData::EMPTY; d.set_reference_count(1); assert!(!d.on_finalize()); } @@ -248,15 +288,20 @@ mod tests { fn weak_ptr_outlives_finalize_then_frees() { let _serial = serial(); let before = drops(); + let finalized_before = FINALIZED_CONTENTS.load(Ordering::SeqCst); let raw = new_owner(4); // SAFETY: `raw` is a freshly leaked Box; live and not finalized. let mut weak = unsafe { WeakPtr::init_ref(raw) }; - assert_eq!(weak.get().map(|o| o.payload), Some(4)); + assert_eq!(weak.get().map(|o| o.payload.get()), Some(4)); // Owner finalizes its contents: not the last ref, so the allocation stays. - // SAFETY: `raw` is live. - assert!(!unsafe { (*Owner::weak_ptr_data(raw)).on_finalize() }); + // SAFETY: `raw` is the live Box we leaked above. + finalize_owner(unsafe { Box::from_raw(raw) }); assert_eq!(drops(), before); + assert_eq!( + FINALIZED_CONTENTS.load(Ordering::SeqCst), + finalized_before + 1 + ); // `get` on a finalized owner releases the ref and reports `None`, which // is the last ref, so `deref_internal` frees the allocation. @@ -278,7 +323,7 @@ mod tests { drop(weak); assert_eq!(drops(), before); // SAFETY: no weak refs remain; the owner frees its own allocation. - drop(unsafe { bun_core::heap::take(raw) }); + finalize_owner(unsafe { Box::from_raw(raw) }); assert_eq!(drops(), before + 1); } @@ -297,8 +342,8 @@ mod tests { for i in 2..5u32 { // SAFETY: `raw` is live; the owner writes through its own pointer. // This is a foreign write for any handle built from a reborrow. - unsafe { (*raw).payload = i }; - assert_eq!(weak.get().map(|o| o.payload), Some(i)); + unsafe { (*raw).payload.set(i) }; + assert_eq!(weak.get().map(|o| o.payload.get()), Some(i)); } drop(weak); @@ -320,12 +365,12 @@ mod tests { // SAFETY: see above. let mut b = unsafe { WeakPtr::init_ref(raw) }; // SAFETY: `raw` is live. - assert_eq!(unsafe { (*Owner::weak_ptr_data(raw)).reference_count() }, 2); - assert_eq!(a.get().map(|o| o.payload), Some(2)); - assert_eq!(b.get().map(|o| o.payload), Some(2)); + assert_eq!(unsafe { &*raw }.weak_ptr_data().reference_count(), 2); + assert_eq!(a.get().map(|o| o.payload.get()), Some(2)); + assert_eq!(b.get().map(|o| o.payload.get()), Some(2)); - // SAFETY: `raw` is live. - assert!(!unsafe { (*Owner::weak_ptr_data(raw)).on_finalize() }); + // SAFETY: `raw` is the live Box leaked above. + finalize_owner(unsafe { Box::from_raw(raw) }); drop(a); assert_eq!(drops(), before); drop(b); diff --git a/src/runtime/api/bun/spawn/stdio.rs b/src/runtime/api/bun/spawn/stdio.rs index 787aec56fc7b..0447a6886267 100644 --- a/src/runtime/api/bun/spawn/stdio.rs +++ b/src/runtime/api/bun/spawn/stdio.rs @@ -504,9 +504,13 @@ impl Stdio { // `value` is on the stack. `dupe()` only bumps the store refcount. return out_stdio.extract_blob(global, webcore::blob::Any::Blob(blob.dupe()), i); } else if let Some(req) = value.as_class_ref::() { - return Self::extract_body_value(out_stdio, global, i, req.get_body_value(), is_sync); + return req + .body_value() + .with_mut(|body| Self::extract_body_value(out_stdio, global, i, body, is_sync)); } else if let Some(res) = value.as_class_ref::() { - return Self::extract_body_value(out_stdio, global, i, res.get_body_value(), is_sync); + return res + .body_value() + .with_mut(|body| Self::extract_body_value(out_stdio, global, i, body, is_sync)); } if let Some(stream_) = webcore::ReadableStream::from_js(value, global)? { diff --git a/src/runtime/api/html_rewriter.rs b/src/runtime/api/html_rewriter.rs index 77b135f44cb3..9ce7c38c885b 100644 --- a/src/runtime/api/html_rewriter.rs +++ b/src/runtime/api/html_rewriter.rs @@ -421,11 +421,16 @@ impl HTMLRewriter { // reason, connection error) instead of a generic "body already used" // — the error is the useful bit, and `wire_input` would otherwise // treat `Value::Error` as an empty blob and emit an empty document. - let body_value = response.get_body_value(); - if let webcore::body::Value::Error(err) = body_value { - return Err(global.throw_value(err.to_js(global))); + if let Some(err_js) = response + .body_value() + .with_mut(|body_value| match body_value { + webcore::body::Value::Error(err) => Some(err.to_js(global)), + _ => None, + }) + { + return Err(global.throw_value(err_js)); } - if matches!(*body_value, webcore::body::Value::Used) { + if matches!(response.body_value().get(), webcore::body::Value::Used) { return Err( global.throw_invalid_arguments(format_args!("Response body already used")) ); @@ -453,7 +458,7 @@ impl HTMLRewriter { }; if kind != ResponseKind::Other { - let body_value = webcore::body::extract(global, response_value)?; + let body_value = webcore::Body::new(webcore::body::extract(global, response_value)?); let resp = RefPtr::new(Response::init( webcore::response::Init { status_code: 200, @@ -486,15 +491,15 @@ impl HTMLRewriter { // handler that would suspend fail the rewrite instead, and `init` // rethrows that as the synchronous TypeError above. let mut blob = out_response - .get_body_value() - .use_as_any_blob_allow_non_utf8_string(); + .body_value() + .with_mut(|v| v.use_as_any_blob_allow_non_utf8_string()); - // Null out the JS wrapper's `m_ctx` so its GC finalize is a no-op, - // then release the wrapper's +1 ourselves. The pipe still holds its - // own (`RewriterPipe.response`). - js_Response::detach_ptr(out_response_value); - // SAFETY: releases the wrapper's ref that `detach_ptr` orphaned. - unsafe { Response::deref(out_response.as_const_ptr().cast_mut()) }; + // Take the JS wrapper's reference back (its GC finalize becomes a + // no-op) and release it now. The pipe still holds its own + // (`RewriterPipe.response`), which keeps the allocation until the + // Transform cell is collected. + let _ = out_response; + drop(js_Response::take_ref(out_response_value)); return match kind { ResponseKind::String => blob.to_string(global, webcore::Lifetime::Transfer), @@ -704,10 +709,10 @@ pub struct RewriterPipe { /// `Bun.write`, … via [`Self::on_start_buffering`]): the output is /// observed and never backpressured. buffered_consumer: Cell, - /// The pipe's ref on the output Response, so the body stays reachable - /// (`fail()`, the abandon-suspension path) after the Response JS wrapper - /// has been swept alongside the Transform cell. - response: JsCell>>, + /// Output Response. The pipe holds a native `+1` (released in `Drop`) so + /// the body stays reachable on the abandon-suspension path after the + /// Response JS wrapper has been swept alongside the Transform cell. + response: JsCell>>, // ── suspension (from #33243) ───────────────────────────────────────── phase: Cell, @@ -1022,7 +1027,7 @@ impl RewriterPipe { // A consumer reading `.body` creates the ByteStream lazily; until then // the sink buffers into `output_buffer`, and `on_start_streaming` // hands that over as `DrainResult::Owned`. - let result = bun_core::heap::alloc_nn(Response::init( + let result = Box::new(Response::init( webcore::response::Init { status_code: 200, ..Default::default() @@ -1039,21 +1044,24 @@ impl RewriterPipe { BunString::EMPTY, false, )); - let result_ref = BackRef::from(result); - // SAFETY: `result` is the live Response just allocated above. - this.response - .set(Some(unsafe { RefPtr::init_ref(result.as_ptr()) })); - result_ref.set_init( + result.set_init( original.get_method(), original.get_init_status_code(), original.get_init_status_text().clone(), ); // https://github.com/oven-sh/bun/issues/3334 - result_ref.set_init_headers(original.clone_init_headers(global)?); + result.set_init_headers(original.clone_init_headers(global)?); - let response_js_value = result_ref.to_js(&this.global); + // Pipe owns a `+1` on the Response native so `fail()` can still reach + // the body after the Response JS wrapper has been swept (the + // abandon-suspension path runs from a deferred task after the + // Transform cell and Response wrapper were collected together); + // released in `Drop for RewriterPipe`. + let (response_js_value, pipe_ref) = result.to_js_retained(&this.global); + let result_ref = BackRef::::new(&*pipe_ref); + this.response.set(Some(pipe_ref)); // Hand ownership of `pipe` to its `JSHTMLRewriterTransform` wrapper cell. // The cell's WriteBarrier slots root the Response and (later) the @@ -1072,10 +1080,9 @@ impl RewriterPipe { result_ref.set_url(original.url().clone()); // ── wire input ────────────────────────────────────────────────────── - let value = original.get_body_value(); let owned_readable_stream = original.get_body_readable_stream(); - Self::wire_input(this, global, value, owned_readable_stream); + Self::wire_input(this, global, original.body_value(), owned_readable_stream); // A handler that failed synchronously (the input was materialized, so // the whole rewrite ran inline above) surfaces as a synchronous throw @@ -1096,74 +1103,95 @@ impl RewriterPipe { fn wire_input( pipe: bun_ptr::BackRef, global: &JSGlobalObject, - value: &mut webcore::body::Value, + body: &JsCell, stream: Option, ) { + #[allow(clippy::large_enum_variant)] + enum Input { + Failed, + Buffered(webcore::blob::Any), + Stream(ReadableStream), + } // `pipe` is the `heap::alloc_nn` allocation from `init()`; every field // is `Cell`/`JsCell`, so the shared `BackRef` borrow is sound across // the re-entrant lol-html calls below. let this = pipe; - // A Locked body with no realised stream (fresh `fetch()` Response), or - // a file/S3-backed Blob, must be turned into a ReadableStream first so - // the ByteStream/FileReader wiring below can drive it. - let mut stream = stream; - if stream.is_none() { - let needs_stream = match value { - webcore::body::Value::Locked(_) => true, - webcore::body::Value::Blob(b) => b.needs_to_read_file() || b.is_s3(), - _ => false, - }; - if needs_stream { - match value - .to_readable_stream(global) - .and_then(|v| ReadableStream::from_js(v, global)) - { - Ok(s) => stream = s, - Err(e) => { - let err = global.take_exception(e); - this.set_handler_error(err); - return; + // Decide inside the body borrow; the handlers `feed` runs and the JS + // pump can both reach the input Response again. + let input = body.with_mut(|value| { + // A Locked body with no realised stream (fresh `fetch()` Response), or + // a file/S3-backed Blob, must be turned into a ReadableStream first so + // the ByteStream/FileReader wiring below can drive it. + let mut stream = stream; + if stream.is_none() { + let needs_stream = match value { + webcore::body::Value::Locked(_) => true, + webcore::body::Value::Blob(b) => b.needs_to_read_file() || b.is_s3(), + _ => false, + }; + if needs_stream { + match value + .to_readable_stream(global) + .and_then(|v| ReadableStream::from_js(v, global)) + { + Ok(s) => stream = s, + Err(e) => { + let err = global.take_exception(e); + this.set_handler_error(err); + return Input::Failed; + } } } } - } - // Materialized-body fast path: feed synchronously, end, return. No - // stream wiring; this covers InternalBlob/WTFStringImpl/Empty/Used and - // Blob-with-bytes (the `sync_only_noun` path always lands here). - let Some(stream) = stream else { - // lol-html consumes UTF-8; `use_as_any_blob()` encodes a non-ASCII - // WTFStringImpl into an InternalBlob so `.slice()` is always UTF-8. - let mut any_blob = value.use_as_any_blob(); - let bytes = any_blob.slice(); - // Mark EOF first so a handler that suspends mid-feed resumes into - // `end_rewrite` once its promise settles. - this.input_ended.set(true); - if this.feed(bytes) { - this.end_rewrite(); + // Materialized-body fast path: feed synchronously, end, return. No + // stream wiring; this covers InternalBlob/WTFStringImpl/Empty/Used and + // Blob-with-bytes (the `sync_only_noun` path always lands here). + let Some(stream) = stream else { + // lol-html consumes UTF-8; `use_as_any_blob()` encodes a non-ASCII + // WTFStringImpl into an InternalBlob so `.slice()` is always UTF-8. + return Input::Buffered(value.use_as_any_blob()); + }; + + if stream.is_locked(global) || stream.is_disturbed(global) { + let err = system_error( + "ERR_STREAM_ALREADY_FINISHED", + "Stream already used, please create a new one", + ); + this.set_handler_error(err.to_error_instance(global)); + return Input::Failed; } - // `blob::Any` has no `Drop`; release the WTFStringImpl/Blob `+1` - // transferred by `use_as_any_blob`. A suspended lol-html has - // already copied the unconsumed tail into its arena. - any_blob.detach(); - return; - }; - if stream.is_locked(global) || stream.is_disturbed(global) { - let err = system_error( - "ERR_STREAM_ALREADY_FINISHED", - "Stream already used, please create a new one", + // Root the stream on the pipe and mark the input body consumed, so a + // second `transform()` / `.text()` on the same input throws "Body + // already used" instead of quietly yielding an empty document. + js_HTMLRewriterTransform::input_stream_set_cached( + this.cell.get(), + global, + stream.value, ); - this.set_handler_error(err.to_error_instance(global)); - return; - } - - // Root the stream on the pipe and mark the input body consumed, so a - // second `transform()` / `.text()` on the same input throws "Body - // already used" instead of quietly yielding an empty document. - js_HTMLRewriterTransform::input_stream_set_cached(this.cell.get(), global, stream.value); - *value = webcore::body::Value::Used; + *value = webcore::body::Value::Used; + Input::Stream(stream) + }); + let stream = match input { + Input::Failed => return, + Input::Buffered(mut any_blob) => { + let bytes = any_blob.slice(); + // Mark EOF first so a handler that suspends mid-feed resumes into + // `end_rewrite` once its promise settles. + this.input_ended.set(true); + if this.feed(bytes) { + this.end_rewrite(); + } + // Release the WTFStringImpl/Blob `+1` transferred by + // `use_as_any_blob` now: a suspended lol-html has already + // copied the unconsumed tail into its arena. + any_blob.detach(); + return; + } + Input::Stream(stream) => stream, + }; let sink_handle = SinkHandle::HTMLRewriter(this); @@ -1188,7 +1216,9 @@ impl RewriterPipe { this.pump_controller_attached.set(true); this.ref_(); let assignment_result = - JSSink::::assign_to_stream(global, stream.value, pipe.into()); + JSSink::::assign_to_stream(global, stream.value, pipe.into(), |s| { + this.input_source.set(s) + }); assignment_result.ensure_still_alive(); if let Some(err) = assignment_result.to_error() { @@ -1556,16 +1586,18 @@ impl RewriterPipe { }; // For a waiting `.blob()`'s content type. let headers = response.get_fetch_headers().map(NonNull::from); - let body_value = response.get_body_value(); let bytes = self.output_buffer.replace(Vec::new()); - let mut prev_value = core::mem::replace( - body_value, - webcore::body::Value::InternalBlob(webcore::InternalBlob { - bytes, - was_string: false, - }), - ); - let _ = webcore::body::Value::resolve(&mut prev_value, body_value, &self.global, headers); + response.body_value().with_mut(|body_value| { + let mut prev_value = core::mem::replace( + body_value, + webcore::body::Value::InternalBlob(webcore::InternalBlob { + bytes, + was_string: false, + }), + ); + let _ = + webcore::body::Value::resolve(&mut prev_value, body_value, &self.global, headers); + }); } /// Feed the accumulated `pending_input` once unblocked, then maybe end, @@ -1734,18 +1766,19 @@ impl RewriterPipe { out.on_data(StreamResult::Err(err.to_stream_error(&self.global))); self.detach_output(); } else if let Some(response) = self.response.get().as_deref() { - let body_value = response.get_body_value(); - let has_readable = match body_value { - webcore::body::Value::Locked(l) => l.readable.has(), - _ => false, - }; - if !has_readable - && matches!(body_value, webcore::body::Value::Locked(l) - if l.promise.is_none() && l.on_receive_value.is_none()) - { - *body_value = webcore::body::Value::Empty; - } - let _ = body_value.to_error_instance(err, &self.global); + response.body_value().with_mut(|body_value| { + let has_readable = match body_value { + webcore::body::Value::Locked(l) => l.readable.has(), + _ => false, + }; + if !has_readable + && matches!(body_value, webcore::body::Value::Locked(l) + if l.promise.is_none() && l.on_receive_value.is_none()) + { + *body_value = webcore::body::Value::Empty; + } + let _ = body_value.to_error_instance(err, &self.global); + }); } self.release_input_roots(src); } diff --git a/src/runtime/api/streams.classes.ts b/src/runtime/api/streams.classes.ts index 5a0a5b6a7537..53a6e79d3f9a 100644 --- a/src/runtime/api/streams.classes.ts +++ b/src/runtime/api/streams.classes.ts @@ -10,11 +10,6 @@ function source(name) { return define({ name: name + "InternalReadableStreamSource", rustPath: rustPaths[name], - // R-2 Phase 3 opt-out: the codegen-facing wrapper `NewSource` impl in - // ReadableStream.rs still has `&mut self` host-fns (the embedded context - // types — ByteStream/FileReader/ByteBlobLoader — are Cell-migrated, but - // the generic wrapper is not yet). Remove once `NewSource` is migrated. - sharedThis: false, construct: false, noConstructor: true, finalize: true, diff --git a/src/runtime/dispatch.rs b/src/runtime/dispatch.rs index b3f99bc2e599..5228fe6bd9cf 100644 --- a/src/runtime/dispatch.rs +++ b/src/runtime/dispatch.rs @@ -477,9 +477,11 @@ pub(crate) fn run_task( // `flush_task_ref` until this call (which may free it). FileSink::run_flush_task(unsafe { bun_ptr::ThisPtr::new(cast_ptr!(FileSink)) }); } - // `cast_ptr!` yields the heap-allocated task; sole owner. task_tag::StreamPending => { - StreamPending::run_from_js_thread(cast_ptr!(StreamPending)); + // SAFETY: `Pending::run_on_next_tick` boxed it; the arm consumes the box. + StreamPending::run_from_js_thread(unsafe { + bun_core::heap::take(cast_ptr!(StreamPending)) + }); } _ => { diff --git a/src/runtime/image/codecs.rs b/src/runtime/image/codecs.rs index b1c7f87cae74..f39e50590931 100644 --- a/src/runtime/image/codecs.rs +++ b/src/runtime/image/codecs.rs @@ -524,6 +524,13 @@ macro_rules! encoded_wrap_free { } impl Encoded { + /// The encoded bytes (owned by `self` until drop). + #[inline] + pub(crate) fn as_slice(&self) -> &[u8] { + // SAFETY: `bytes` is the live codec allocation `self` owns and frees on drop. + unsafe { self.bytes.as_ref() } + } + #[allow(dead_code)] pub(crate) fn from_owned(bytes: Vec) -> Encoded { let mut bytes = core::mem::ManuallyDrop::new(bytes); diff --git a/src/runtime/jsc_hooks.rs b/src/runtime/jsc_hooks.rs index 5eb8bfa11816..421c9f198731 100644 --- a/src/runtime/jsc_hooks.rs +++ b/src/runtime/jsc_hooks.rs @@ -206,6 +206,24 @@ pub(crate) fn cron_jobs_mut() -> Option<&'static mut Vec crate::webcore::body::BodyHiveHandle { + let state = runtime_state(); + debug_assert!( + !state.is_null(), + "body_hive_alloc before init_runtime_state" + ); + // SAFETY: `state` is this thread's live boxed `RuntimeState`. The pool box + // is freed only in `deinit_runtime_state`, after the VM — and with it every + // `Request` / `RequestContext` that holds a slot handle — is torn down. + unsafe { + let pool = &raw const **(*state).body_value_pool; + crate::webcore::body::BodyHiveHandle::new(value, pool) + } +} + #[inline] pub(crate) fn active_handles() -> Option<&'static mut ActiveHandles> { let state = runtime_state(); diff --git a/src/runtime/node/types.rs b/src/runtime/node/types.rs index aedf9cf262d7..b6fd7eb82c42 100644 --- a/src/runtime/node/types.rs +++ b/src/runtime/node/types.rs @@ -187,10 +187,11 @@ impl BlobOrStringOrBuffer { } if allow_request_response { if let Some(request) = value.as_class_ref::() { - let body_value = request.get_body_value(); - body_value.to_blob_if_possible(); - - if let Some(mut any_blob) = body_value.try_use_as_any_blob() { + let any_blob = request.body_value().with_mut(|body_value| { + body_value.to_blob_if_possible(); + body_value.try_use_as_any_blob() + }); + if let Some(mut any_blob) = any_blob { let blob = any_blob.to_blob(global); any_blob.detach(); return Ok(Some(Self::Blob(Box::new(blob)))); @@ -202,10 +203,11 @@ impl BlobOrStringOrBuffer { } if let Some(response) = value.as_class_ref::() { - let body_value = response.get_body_value(); - body_value.to_blob_if_possible(); - - if let Some(mut any_blob) = body_value.try_use_as_any_blob() { + let any_blob = response.body_value().with_mut(|body_value| { + body_value.to_blob_if_possible(); + body_value.try_use_as_any_blob() + }); + if let Some(mut any_blob) = any_blob { let blob = any_blob.to_blob(global); any_blob.detach(); return Ok(Some(Self::Blob(Box::new(blob)))); diff --git a/src/runtime/server/FileRoute.rs b/src/runtime/server/FileRoute.rs index 126b57911eee..6fbbad1947c4 100644 --- a/src/runtime/server/FileRoute.rs +++ b/src/runtime/server/FileRoute.rs @@ -134,13 +134,16 @@ impl FileRoute { argument: JSValue, ) -> JsResult>> { // `as_class_ref` is the safe shared-borrow downcast (one audited - // unsafe in `JSValue`); `get_body_value`/`get_init_headers`/ + // unsafe in `JSValue`); `body_value`/`get_init_headers`/ // `status_code` all take `&self`. if let Some(response) = argument.as_class_ref::() { - let body_value = response.get_body_value(); - body_value.to_blob_if_possible(); - let needs_read = matches!(body_value, BodyValue::Blob(b) if b.needs_to_read_file()); - if needs_read { + let route = response.body_value().with_mut(|body_value| { + body_value.to_blob_if_possible(); + let needs_read = + matches!(body_value, BodyValue::Blob(b) if b.needs_to_read_file()); + if !needs_read { + return Ok(None); + } // `needs_to_read_file()` ⇒ `store` is Some and `data` is `File`. let is_fd = matches!( body_value, @@ -168,12 +171,15 @@ impl FileRoute { let headers = headers_from(response.get_init_headers(), &blob); let status_code = response.status_code(); - return Ok(Some(RefPtr::new(FileRoute::new( + Ok(Some(RefPtr::new(FileRoute::new( blob, headers, None, status_code, - )))); + )))) + })?; + if route.is_some() { + return Ok(route); } } if let Some(blob) = argument.as_class_ref::() { diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index 8579440c76bb..dbe014510c75 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -166,7 +166,7 @@ pub struct RequestContext< /// chunked / H3 bodies consumed as a stream are capped against this. pub(crate) request_body_streamed_len: Cell, - pub sink: Cell>>>, + pub sink: JsCell>>>, pub(crate) byte_stream: Cell>>, /// This keeps the Response body's ReadableStream alive. pub(crate) response_body_readable_stream_ref: JsCell, @@ -364,10 +364,11 @@ fn release_body_stream(response: &mut Response, global_this: &JSGlobalObject) { stream.done(); } // Read after the stream calls: the check observes the post-detach state. - let body_value = response.get_body_value(); - if matches!(body_value, Body::Value::Locked(_)) { - *body_value = Body::Value::Used; - } + response.body_value().with_mut(|body_value| { + if matches!(body_value, Body::Value::Locked(_)) { + *body_value = Body::Value::Used; + } + }); } // ─── sibling-subtree shims ─────────────────────────────────────────────────── @@ -598,19 +599,18 @@ where self.request_body .get() .as_ref() - .map(|h| unsafe { &mut (*h.as_ptr()).value }) + .map(|h| unsafe { h.value.get_mut() }) } /// Exclusive borrow of the heap [`ResponseStreamJSSink`] this context owns. /// /// Returns an unbounded `&'r mut` because the sink is a separate heap - /// allocation (`heap::alloc` in [`do_render_stream`]), **not** a sub-field + /// allocation (`OwnedThis::new` in [`do_render_stream`]), **not** a sub-field /// of `*self` (same pattern as [`request_body_mut`]). /// /// # Safety (encapsulated) - /// While `Some`, `sink` points to the JSSink allocated by - /// `do_render_stream`; this `RequestContext` is its sole owner until - /// [`destroy_sink`] consumes it. Single-threaded — no other `&mut` alias. + /// While `Some`, `sink` owns the JSSink `do_render_stream` created until + /// it is taken out and dropped. Single-threaded — no other `&mut` alias. #[inline] #[allow( clippy::mut_from_ref, @@ -619,7 +619,10 @@ where fn sink_mut(&self) -> Option<&mut ResponseStreamJSSink> { // SAFETY: see fn doc — heap JSSink owned by this ctx, sole live // mutable view, single-threaded. - self.sink.get().map(|p| unsafe { &mut *p.as_ptr() }) + self.sink + .get() + .as_ref() + .map(|p| unsafe { &mut *p.this_ptr().as_ptr() }) } /// @@ -730,7 +733,7 @@ where // Not `cancel()`: it skips a stream with no reader, which an unattached body is. crate::dispatch::fold(stream.cancel_with_reason(global_this, JSValue::UNDEFINED)); } - *response.get_body_value() = Body::Value::Used; + response.body_value().set(Body::Value::Used); } /// [`Self::cancel_unread_body`] for a rooted handler result: a `Response` or a settled promise of one. @@ -932,7 +935,7 @@ where } // check if the body is Locked (streaming) if let Some(body) = self.request_body.get() { - if matches!(&**body, Body::Value::Locked(_)) { + if matches!(body.value.get(), Body::Value::Locked(_)) { return false; } } @@ -961,15 +964,17 @@ where // whose reactions consume the sink (`handleResolveStream` / `handleRejectStream`), // so a client abort in that state reaches deinit with the sink still owned here. // This is the owner's last exit: release it exactly like the settle paths do. - if let Some(wrapper_ptr) = self.sink.take() { - // SAFETY: deinit runs once, after `detach_response()` removed the uWS callbacks; - // the context is the sink's sole owner (see the `sink` field's doc comment). - let wrapper = unsafe { &mut *wrapper_ptr.as_ptr() }; + if let Some(wrapper) = self.sink_mut() { + // deinit runs once, after `detach_response()` removed the uWS callbacks. + let owned_sink = self + .sink + .take() + .expect("infallible: sink_mut returned Some"); wrapper.sink.finalize(); if let Some(sink_global) = wrapper.sink.global_this { ResponseStreamJSSink::::detach(&mut wrapper.sink.source, &sink_global); } - Self::destroy_sink(wrapper_ptr); + drop(owned_sink); } self.request_body_buf.set(Vec::new()); @@ -1442,7 +1447,7 @@ where request_body_buf: JsCell::new(Vec::new()), request_body_content_len: Cell::new(0), request_body_streamed_len: Cell::new(0), - sink: Cell::new(None), + sink: JsCell::new(None), byte_stream: Cell::new(None), response_body_readable_stream_ref: JsCell::new(readable_stream::Strong::default()), pathname: JsCell::new(BunString::EMPTY), @@ -1517,17 +1522,13 @@ where } // if have sink, call onAborted on sink - if let Some(sink_ptr) = this.sink.get() { + if let Some(wrapper) = this.sink_mut() { // The sink abort runs the stream's JS onClose through its signal. any_js_calls.set(true); - // SAFETY: `sink_ptr` is the live JSSink allocated by do_render_stream - // (repr(transparent) over the sink). `abort` takes the raw pointer - // because the teardown it can re-enter frees the sink. - unsafe { - ResponseStream::::abort( - sink_ptr.as_ptr().cast::>(), - ); - } + // The borrow ends before the source close, whose re-entrant + // teardown frees the sink. + let mut source = wrapper.sink.abort(); + source.close(None); // End request streaming here, not in deinit: a `Used` body // (textStream) can only be rejected through // request_body_readable_stream_ref, and finalize_without_deinit @@ -2060,14 +2061,10 @@ where Self::handle_first_stream_write(unsafe { &*ctx.cast::() }); } - /// Tear down a heap `ResponseStreamJSSink` allocated by `do_render_stream`. - /// JSSink is `repr(transparent)` so the inner-ptr free matches the - /// outer allocation. - fn destroy_sink(ptr: NonNull>) { - // `ptr` was `heap::alloc`'d in do_render_stream and is being consumed - // exactly once here. `JSSink` is repr(transparent), so the inner - // `HTTPServerWritable` shares the allocation Layout. - ResponseStream::::destroy(ptr.as_ptr().cast::>()); + /// Drop the heap `ResponseStreamJSSink` allocated by `do_render_stream` + /// (its `Drop` settles what is parked on it). + fn destroy_sink(&self) { + drop(self.sink.take()); } /// `on_abort` ran from inside the user code `do_render_stream` invoked @@ -2085,16 +2082,22 @@ where let mut readable_ref = self .response_body_readable_stream_ref .replace(readable_stream::Strong::default()); - if let Some(wrapper_ptr) = self.sink.take() { - // SAFETY: this context is the sink's sole owner until `destroy_sink` - // below (see the `sink` field); `on_abort` leaves it allocated. - let wrapper = unsafe { &mut *wrapper_ptr.as_ptr() }; - ResponseStreamJSSink::::detach(&mut wrapper.sink.source, global_this); + // `on_abort` leaves the sink allocated. + if let Some(wrapper) = self.sink_mut() { + // Taken out now so the re-entrant `cancel` below sees no sink; dropped last. + let owned_sink = self + .sink + .take() + .expect("infallible: sink_mut returned Some"); + ResponseStreamJSSink::::detach( + &mut wrapper.sink.source, + global_this, + ); crate::dispatch::fold(stream.cancel(global_this)); wrapper.sink.mark_done(); wrapper.sink.on_first_write = None; wrapper.sink.finalize(); - Self::destroy_sink(wrapper_ptr); + drop(owned_sink); } readable_ref.deinit(); } @@ -2121,19 +2124,17 @@ where stream.value.ensure_still_alive(); - let response_stream_box = Box::new(ResponseStreamJSSink:: { - sink: ResponseStream:: { - res: Some(resp), - buffer: Vec::::default(), - on_first_write: Some(Self::handle_first_stream_write_thunk), - ctx: Some(this.as_ctx_ptr().cast::()), - global_this: Some(bun_ptr::BackRef::new(global_this)), - ..Default::default() - }, - }); - let response_stream_ptr = bun_core::heap::into_raw_nn(response_stream_box); - this.sink.set(Some(response_stream_ptr)); - // SAFETY: just allocated; sole live mutable view (this.sink only stores the ptr). + let mut sink = ResponseStream::::default(); + sink.res = Some(resp); + sink.on_first_write = Some(Self::handle_first_stream_write_thunk); + sink.ctx = Some(this.as_ctx_ptr().cast::()); + sink.global_this = Some(bun_ptr::BackRef::new(global_this)); + let response_stream_owned = + bun_ptr::OwnedThis::new(ResponseStreamJSSink:: { sink }); + let response_stream_ptr: NonNull> = + response_stream_owned.this_ptr().into(); + this.sink.set(Some(response_stream_owned)); + // SAFETY: just allocated; sole live mutable view (this.sink only owns the allocation). let response_stream = unsafe { &mut *response_stream_ptr.as_ptr() }; // `JSSink` is `repr(transparent)` over `T`: same address, root provenance. response_stream.sink.root.set(Some( @@ -2152,11 +2153,13 @@ where ); // We are already corked! - let assignment_result: JSValue = ResponseStreamJSSink::::assign_to_stream( - global_this, - stream.value, - NonNull::from(&mut response_stream.sink), - ); + let assignment_result: JSValue = + ResponseStreamJSSink::::assign_to_stream( + global_this, + stream.value, + NonNull::from(&mut response_stream.sink), + |s| response_stream.sink.source = s, + ); assignment_result.ensure_still_alive(); @@ -2182,8 +2185,7 @@ where &mut response_stream.sink.source, global_this, ); - this.sink.set(None); - Self::destroy_sink(response_stream_ptr); + this.destroy_sink(); return this.handle_reject(err_value); } @@ -2193,8 +2195,7 @@ where &mut response_stream.sink.source, global_this, ); - this.sink.set(None); - Self::destroy_sink(response_stream_ptr); + this.destroy_sink(); stream.done(); this.response_body_readable_stream_ref .with_mut(|s| s.deinit()); @@ -2250,12 +2251,14 @@ where } // TODO: should this timeout? - let body_value = this.response_mut().unwrap().get_body_value(); - *body_value = Body::Value::Locked(Body::PendingValue { - readable: readable_stream::Strong::init(*stream, global_this), - global: std::ptr::from_ref(global_this), - ..Default::default() - }); + this.response_mut() + .unwrap() + .body_value() + .set(Body::Value::Locked(Body::PendingValue { + readable: readable_stream::Strong::init(*stream, global_this), + global: std::ptr::from_ref(global_this), + ..Default::default() + })); let cell = this.create_promise_cell(global_this); effective_result.then_with_value( global_this, @@ -2303,8 +2306,7 @@ where &mut response_stream.sink.source, global_this, ); - this.sink.set(None); - Self::destroy_sink(response_stream_ptr); + this.destroy_sink(); return this.handle_reject(effective_result); } } @@ -2329,8 +2331,7 @@ where ); response_stream.sink.mark_done(); response_stream.sink.finalize(); - this.sink.set(None); - Self::destroy_sink(response_stream_ptr); + this.destroy_sink(); readable_ref.deinit(); this.render_missing(); return; @@ -2345,8 +2346,7 @@ where crate::dispatch::fold(stream.cancel(global_this)); response_stream.sink.mark_done(); response_stream.sink.finalize(); - this.sink.set(None); - Self::destroy_sink(response_stream_ptr); + this.destroy_sink(); readable_ref.deinit(); this.render_missing(); } @@ -2565,14 +2565,13 @@ where // GET strips the handler's Content-Length / Transfer-Encoding and frames // from the body, so HEAD must too (RFC 9110 §9.3.2). Only a bodiless // Response leaves those headers as what GET would have sent (#15355). - let body_decides_framing = { - let body_value = response.get_body_value(); + let body_decides_framing = response.body_value().with_mut(|body_value| { body_value.to_blob_if_possible(); !matches!( body_value, Body::Value::Used | Body::Value::Null | Body::Value::Empty | Body::Value::Error(_) ) - }; + }); // `fast_get`/`fast_has` take `&mut self` (FFI shim), so use the `_mut` // accessor — `get_fetch_headers()` and `get_init_headers()` alias the // same `init.headers` field. @@ -2611,73 +2610,84 @@ where } // the body decides the framing (or there is neither a body nor a // handler-supplied Content-Length / Transfer-Encoding header) - let body_value = response.get_body_value(); - match body_value { - Body::Value::InternalBlob(_) | Body::Value::WTFStringImpl(_) => { - let mut blob = body_value.use_as_any_blob_allow_non_utf8_string(); - let size = blob.size(); + enum HeadFraming { + Length(crate::webcore::blob::SizeType), + S3Stat, + Chunked, + Zero, + } + // Decide inside a short borrow; `render_metadata()` re-fetches the + // Response from `response_weakref`, so no borrow of its body may be + // live across it. Nothing is written to the socket in between, so the + // wire output is unchanged. + let framing = response + .body_value() + .with_mut(|body_value| match body_value { + Body::Value::InternalBlob(_) | Body::Value::WTFStringImpl(_) => { + let mut blob = body_value.use_as_any_blob_allow_non_utf8_string(); + let size = blob.size(); + blob.detach(); + HeadFraming::Length(size) + } + Body::Value::Blob(blob) => { + if shim::blob_is_s3(blob) { + return HeadFraming::S3Stat; + } + blob.resolve_size(); + HeadFraming::Length(blob.size.get()) + } + Body::Value::Locked(_) => HeadFraming::Chunked, + Body::Value::Used + | Body::Value::Null + | Body::Value::Empty + | Body::Value::Error(_) => HeadFraming::Zero, + }); + match framing { + HeadFraming::Length(size) => { this.render_metadata(); - if size == crate::webcore::blob::MAX_SIZE { resp.write_header_int(b"content-length", 0); } else { resp.write_header_int(b"content-length", size as u64); } this.end_without_body(this.should_close_connection()); - blob.detach(); } - - Body::Value::Blob(blob) => { - if shim::blob_is_s3(blob) { - // we need to read the size asynchronously - // in this case should always be a redirect so should not hit this path, but in case we change it in the future lets handle it - // Ref for the S3 stat; adopted and released by - // `on_s3_size_resolved_thunk`. - this.ref_(); - - let crate::webcore::blob::store::Data::S3(s3) = - &blob.store.get().as_ref().unwrap().data - else { - unreachable!() - }; - let credentials = s3.get_credentials(); - let path = s3.path(); - // `Transpiler::env_mut` is the safe accessor for the - // process-singleton dotenv loader (set during init). - let proxy_url = global_this - .bun_vm() - .as_mut() - .transpiler - .env_mut() - .get_http_proxy(true, None, None) - .map(|proxy| proxy.href); - - let _ = S3::client::stat( - credentials, - path, - Self::on_s3_size_resolved_thunk, - this.as_ctx_ptr().cast::(), - proxy_url, - s3.request_payer, - ); // TODO: properly propagate exception upwards - return; - } - // Size the blob *before* `render_metadata()`: it re-fetches the - // Response from `response_weakref`, so no borrow of the Response - // (here, `blob`) may still be live across it. Nothing is written - // to the socket in between, so the wire output is unchanged. - blob.resolve_size(); - let blob_size = blob.size.get(); - this.render_metadata(); - - if blob_size == crate::webcore::blob::MAX_SIZE { - resp.write_header_int(b"content-length", 0); - } else { - resp.write_header_int(b"content-length", blob_size as u64); - } - this.end_without_body(this.should_close_connection()); + HeadFraming::S3Stat => { + // we need to read the size asynchronously + // in this case should always be a redirect so should not hit this path, but in case we change it in the future lets handle it + // Ref for the S3 stat; adopted and released by + // `on_s3_size_resolved_thunk`. + this.ref_(); + let Body::Value::Blob(blob) = response.body_value().get() else { + unreachable!() + }; + let crate::webcore::blob::store::Data::S3(s3) = + &blob.store.get().as_ref().unwrap().data + else { + unreachable!() + }; + let credentials = s3.get_credentials(); + let path = s3.path(); + // `Transpiler::env_mut` is the safe accessor for the + // process-singleton dotenv loader (set during init). + let proxy_url = global_this + .bun_vm() + .as_mut() + .transpiler + .env_mut() + .get_http_proxy(true, None, None) + .map(|proxy| proxy.href); + + let _ = S3::client::stat( + credentials, + path, + Self::on_s3_size_resolved_thunk, + this.as_ctx_ptr().cast::(), + proxy_url, + s3.request_payer, + ); // TODO: properly propagate exception upwards } - Body::Value::Locked(_) => { + HeadFraming::Chunked => { this.render_metadata(); if !MUX { // SAFETY: FFI handle @@ -2689,7 +2699,7 @@ where } this.end_without_body(this.should_close_connection()); } - Body::Value::Used | Body::Value::Null | Body::Value::Empty | Body::Value::Error(_) => { + HeadFraming::Zero => { this.render_metadata(); // SAFETY: FFI handle resp.write_header_int(b"content-length", 0); @@ -2896,7 +2906,8 @@ where let mut wrote_anything = false; let mut ended_response = false; if let Some(wrapper) = self.sink_mut() { - let wrapper_ptr = self + // Taken out now so re-entrant JS below sees no sink; dropped last. + let owned_sink = self .sink .take() .expect("infallible: sink_mut returned Some"); @@ -2917,8 +2928,11 @@ where .sink .global_this .expect("sink.global_this set in do_render_stream"); - ResponseStreamJSSink::::detach(&mut wrapper.sink.source, &sink_global); - Self::destroy_sink(wrapper_ptr); + ResponseStreamJSSink::::detach( + &mut wrapper.sink.source, + &sink_global, + ); + drop(owned_sink); } debug_assert!(self.server.get().is_some()); @@ -2933,7 +2947,7 @@ where stream.done(); } - *resp.get_body_value() = Body::Value::Used; + resp.body_value().set(Body::Value::Used); } if self.is_aborted_or_ended() { @@ -2999,7 +3013,8 @@ where let mut ended_response = false; if let Some(wrapper) = self.sink_mut() { - let wrapper_ptr = self + // Taken out now so re-entrant JS below sees no sink; dropped last. + let owned_sink = self .sink .take() .expect("infallible: sink_mut returned Some"); @@ -3025,8 +3040,11 @@ where .sink .global_this .expect("sink.global_this set in do_render_stream"); - ResponseStreamJSSink::::detach(&mut wrapper.sink.source, &sink_global); - Self::destroy_sink(wrapper_ptr); + ResponseStreamJSSink::::detach( + &mut wrapper.sink.source, + &sink_global, + ); + drop(owned_sink); } if let Some(resp) = self.response_mut() { @@ -3500,7 +3518,7 @@ where // A null-body status never transmits the body. if let Some(server) = this.server.get() && let Some(response) = this.response_mut() - && matches!(response.get_body_value(), Body::Value::Locked(_)) + && matches!(response.body_value().get(), Body::Value::Locked(_)) { Self::cancel_unread_body(response, server.global_this()); } @@ -3535,10 +3553,7 @@ where let (value, owned_readable) = { let response: &mut Response = self.response_mut().unwrap(); let owned_readable = response.get_body_readable_stream(); - ( - std::ptr::from_mut(response.get_body_value()), - owned_readable, - ) + (response.body_value().as_ptr(), owned_readable) }; self.do_render_with_body(value, owned_readable); } @@ -4036,13 +4051,14 @@ where unsafe fn protect_for_body_and_render(&self, response_value: JSValue, response: *mut Response) { // SAFETY: caller contract: `response` is live. This is the only borrow // of its body, and it ends before `render` reborrows the cell. - let body_value = unsafe { (*response).get_body_value() }; - body_value.to_blob_if_possible(); - let sent_after_return = match body_value { - Body::Value::Blob(blob) => shim::blob_needs_to_read_file(blob), - Body::Value::Locked(_) => true, - _ => false, - }; + let sent_after_return = unsafe { &*response }.body_value().with_mut(|body_value| { + body_value.to_blob_if_possible(); + match body_value { + Body::Value::Blob(blob) => shim::blob_needs_to_read_file(blob), + Body::Value::Locked(_) => true, + _ => false, + } + }); if sent_after_return { response_value.protect(); self.flags.set_response_protected(true); @@ -4311,8 +4327,7 @@ where #[inline] fn live_resp(&self) -> Option { if let Some(sink) = self.sink.get() { - // SAFETY: `sink` is owned by this context and freed in `handle_resolve_stream`/`deinit`. - if unsafe { (*sink.as_ptr()).sink.ended_response } { + if sink.sink.ended_response { return None; } } diff --git a/src/runtime/server/StaticRoute.rs b/src/runtime/server/StaticRoute.rs index 66f52755e5e5..5a94b114814e 100644 --- a/src/runtime/server/StaticRoute.rs +++ b/src/runtime/server/StaticRoute.rs @@ -158,24 +158,18 @@ impl StaticRoute { // The user may want to pass in the same Response object multiple endpoints // Let's let them do that. - let body_value = response.get_body_value(); - let was_string = body_value.was_string(); - body_value.to_blob_if_possible(); - - let blob: AnyBlob = 'brk: { + let was_string = response.body_value().get().was_string(); + let blob: AnyBlob = response.body_value().with_mut(|body_value| { + body_value.to_blob_if_possible(); match body_value { - BodyValue::Used => { - return Err(global_this.throw_invalid_arguments(format_args!( - "Response body has already been used" - ))); - } + BodyValue::Used => Err(global_this.throw_invalid_arguments(format_args!( + "Response body has already been used" + ))), - BodyValue::Null | BodyValue::Empty => { - break 'brk AnyBlob::InternalBlob(InternalBlob { - bytes: Vec::::new(), - was_string: false, - }); - } + BodyValue::Null | BodyValue::Empty => Ok(AnyBlob::InternalBlob(InternalBlob { + bytes: Vec::::new(), + was_string: false, + })), BodyValue::Blob(_) | BodyValue::InternalBlob(_) @@ -195,16 +189,14 @@ impl StaticRoute { ); *body_value = BodyValue::Blob(blob.dupe()); - break 'brk AnyBlob::Blob(blob); + Ok(AnyBlob::Blob(blob)) } - _ => { - return Err(global_this.throw_invalid_arguments(format_args!( - "Body must be fully buffered before it can be used in a static route. Consider calling new Response(await response.blob()) to buffer the body." - ))); - } + _ => Err(global_this.throw_invalid_arguments(format_args!( + "Body must be fully buffered before it can be used in a static route. Consider calling new Response(await response.blob()) to buffer the body." + ))), } - }; + })?; if let Some(h) = response.get_init_headers_mut() { h.fast_remove(HTTPHeaderName::TransferEncoding); diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index d29af0bf5146..eeffaa9b3f42 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -795,10 +795,9 @@ impl NewServer { // Allocate the pooled body slot (ref_count = 1). let body_hive = crate::webcore::body::hive_alloc(crate::webcore::body::Value::Null); - // Raw payload pointer for the deferred Locked write below. - // SAFETY: slot stays live — both `ctx.request_body` and `Request.body` hold a +1. - let body_value: *mut crate::webcore::body::Value = - unsafe { core::ptr::addr_of_mut!((*body_hive.as_ptr()).value) }; + // Raw payload pointer for the deferred Locked write below; the slot + // stays live — both `ctx.request_body` and `Request.body` hold a +1. + let body_value: *mut crate::webcore::body::Value = body_hive.value.as_ptr(); // Bump once so the ctx and JS Request each // own a +1 on the same slot (streamed bytes buffered into the ctx // surface on `request.body`/`request.json()`). diff --git a/src/runtime/server/server_body.rs b/src/runtime/server/server_body.rs index 5a3f6ff0a1e0..a6ef227ec70a 100644 --- a/src/runtime/server/server_body.rs +++ b/src/runtime/server/server_body.rs @@ -204,7 +204,7 @@ where self.request_body .get() .as_ref() - .map(|h| unsafe { &mut (*h.as_ptr()).value }) + .map(|h| unsafe { h.value.get_mut() }) } #[inline] fn set_signal(&self, sig: *mut AbortSignal) { diff --git a/src/runtime/shell/Builtin.rs b/src/runtime/shell/Builtin.rs index 3504a9a802ea..6910e3cd9c0e 100644 --- a/src/runtime/shell/Builtin.rs +++ b/src/runtime/shell/Builtin.rs @@ -724,10 +724,7 @@ impl Builtin { } else if let Some(body) = crate::webcore::body::Value::from_request_or_response(jsval) { - // SAFETY: returned a live JSC-owned `*mut Value` borrowed - // from a Response/Request wrapper. - let body = unsafe { &mut *body }; - let is_file_blob = matches!(body, crate::webcore::body::Value::Blob(b) + let is_file_blob = matches!(body.value.get(), crate::webcore::body::Value::Blob(b) if !b.needs_to_read_file()); if (redirect.stdout() || redirect.stderr()) && !is_file_blob { let _ = global.throw(format_args!( @@ -735,7 +732,7 @@ impl Builtin { )); return Some(Yield::failed()); } - let original_blob = body.use_(); + let original_blob = body.value.with_mut(|v| v.use_()); if !redirect.stdin() && !redirect.stdout() && !redirect.stderr() { drop(original_blob); return None; diff --git a/src/runtime/shell/states/Cmd.rs b/src/runtime/shell/states/Cmd.rs index 893313e8f953..9ea131d957ef 100644 --- a/src/runtime/shell/states/Cmd.rs +++ b/src/runtime/shell/states/Cmd.rs @@ -769,21 +769,19 @@ impl Cmd { } } else if crate::webcore::ReadableStream::from_js(jsval, global)?.is_some() { panic!("TODO SHELL READABLE STREAM"); - } else if let Some(req) = jsval.as_::() { - // SAFETY: `as_` returns a live JSC-owned `*mut Response`; - // `get_body_value` is `&self`. - let req = unsafe { &*req }; - req.get_body_value().to_blob_if_possible(); + } else if let Some(req) = jsval.as_class_ref::() { + let body = req.body_value(); + body.with_mut(|v| v.to_blob_if_possible()); if flags.stdin() { - let b = req.get_body_value().use_as_any_blob(); + let b = body.with_mut(|v| v.use_as_any_blob()); stdio[STDIN_NO].extract_blob(global, b, STDIN_NO as i32)?; } if flags.stdout() { - let b = req.get_body_value().use_as_any_blob(); + let b = body.with_mut(|v| v.use_as_any_blob()); stdio[STDOUT_NO].extract_blob(global, b, STDOUT_NO as i32)?; } if flags.stderr() { - let b = req.get_body_value().use_as_any_blob(); + let b = body.with_mut(|v| v.use_as_any_blob()); stdio[STDERR_NO].extract_blob(global, b, STDERR_NO as i32)?; } } else { diff --git a/src/runtime/webcore/ArrayBufferSink.rs b/src/runtime/webcore/ArrayBufferSink.rs index acfd0b0dcf6d..dbc118f28185 100644 --- a/src/runtime/webcore/ArrayBufferSink.rs +++ b/src/runtime/webcore/ArrayBufferSink.rs @@ -66,8 +66,7 @@ impl ArrayBufferSink { Ok(JSValue::js_number(0.0)) } - /// The sink a new `JSArrayBufferSink` wrapper owns; its `finalize` - /// [`destroy`](Self::destroy)s it. + /// The sink a new `JSArrayBufferSink` wrapper owns; its `finalize` drops it. pub(crate) fn construct() -> core::ptr::NonNull { bun_core::heap::into_raw_nn(Box::new(ArrayBufferSink { bytes: Vec::::default(), @@ -115,15 +114,6 @@ impl ArrayBufferSink { Ok(()) } - /// # Safety - /// `this` is the allocation `js_construct` leaked into the JS wrapper, whose - /// `__finalize` (the sole caller) frees it exactly once, here. - pub(crate) unsafe fn destroy(this: *mut Self) { - // SAFETY: reclaiming ownership drops `bytes` (Vec impls Drop) and - // frees the box. - drop(unsafe { bun_core::heap::take(this) }); - } - pub(crate) fn end_from_js( &mut self, _global_this: &JSGlobalObject, @@ -165,13 +155,18 @@ impl crate::webcore::sink::JsSinkType for ArrayBufferSink { const HAS_CONSTRUCT: bool = true; const HAS_FLUSH_FROM_JS: bool = true; const START_TAG: Option = Some(streams::StartTag::ArrayBufferSink); + const FINALIZE: crate::webcore::sink::FinalizeReceiver = + crate::webcore::sink::FinalizeReceiver::Box; crate::impl_js_sink_forwarders!(); - fn finalize(this: bun_ptr::ThisPtr) { - // SAFETY: trait contract — `this` is the wrapper's live sink, which - // `construct` allocated for it alone; nothing uses it afterwards. - unsafe { Self::destroy(this.as_ptr()) }; + fn finalize(_this: bun_ptr::ThisPtr) { + unreachable!("ArrayBufferSink is released through finalize_boxed"); + } + /// The allocation `construct` leaked into the JS wrapper; dropping it + /// frees `bytes` and the box. + fn finalize_boxed(self: Box) { + drop(self); } fn construct() -> core::ptr::NonNull { Self::construct() diff --git a/src/runtime/webcore/BakeResponse.rs b/src/runtime/webcore/BakeResponse.rs index 6f9c6d23fce7..5a4b827a4018 100644 --- a/src/runtime/webcore/BakeResponse.rs +++ b/src/runtime/webcore/BakeResponse.rs @@ -7,8 +7,8 @@ use bun_jsc::{CallFrame, HTTPHeaderName, JSGlobalObject, JSValue, JsError, JsRes pub(crate) fn fix_dead_code_elimination() { bun_core::keep_symbols!( - BakeResponseClass__constructForSSR, - BakeResponseClass__constructRender + crate::generated_host_exports::BakeResponseClass__constructForSSR, + crate::generated_host_exports::BakeResponseClass__constructRender ); } @@ -18,8 +18,7 @@ bun_jsc::jsc_abi_extern! { #[allow(improper_ctypes)] // `&JSGlobalObject` discharges the only deref'd-param precondition; // `this` is stored opaquely in the JS wrapper (module-private — sole - // caller is `to_js_for_ssr`, whose own signature carries the - // ownership-transfer contract). + // caller is `to_js_for_ssr`, which hands over a fresh heap allocation). safe fn BakeResponse__createForSSR( global_object: &JSGlobalObject, this: *mut Response, @@ -38,47 +37,31 @@ pub enum SSRKind { } /// Create the JS `BakeResponse` wrapper for `this`. The C++ wrapper **adopts** -/// the `*mut Response` allocation (freed in `BakeResponseClass__finalize`), so -/// callers must hand over a heap pointer they no longer own — typically via -/// `heap::alloc`. -/// -/// # Safety -/// `this` must be a valid heap-allocated `Response` whose ownership is being -/// transferred to the JS GC. After this call the caller must not free or -/// dereference `this`. -unsafe fn to_js_for_ssr( - this: *mut Response, - global_object: &JSGlobalObject, - kind: SSRKind, -) -> JSValue { - // SAFETY: caller contract — `this` is a valid exclusive heap allocation. - unsafe { &mut *this }.calculate_estimated_byte_size(); - BakeResponse__createForSSR(global_object, this, kind as u8) +/// the allocation (its reference is released in `BakeResponseClass__finalize`). +fn to_js_for_ssr(this: Box, global_object: &JSGlobalObject, kind: SSRKind) -> JSValue { + this.calculate_estimated_byte_size(); + BakeResponse__createForSSR(global_object, bun_core::heap::into_raw(this), kind as u8) } -// C++ side declares `extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES` (SYSV_ABI on win-x64). -bun_jsc::jsc_host_abi! { - #[unsafe(no_mangle)] - pub(crate) unsafe fn BakeResponseClass__constructForSSR( - global_object: &JSGlobalObject, - call_frame: &CallFrame, - bake_ssr_has_jsx: *mut c_int, - js_this: JSValue, - ) -> *mut c_void { - // SAFETY: caller (C++) guarantees `bake_ssr_has_jsx` is a valid, exclusive out-pointer for the call. - let bake_ssr_has_jsx = unsafe { &mut *bake_ssr_has_jsx }; - match constructor(global_object, call_frame, bake_ssr_has_jsx, js_this) { - Ok(response) => response.cast::(), - Err(JsError::Thrown) => core::ptr::null_mut(), - Err(JsError::Terminated) => { - // A constructor runs beneath script: rethrow so the caller keeps unwinding. - let _ = bun_jsc::Stopped.throw(global_object); - core::ptr::null_mut() - } - Err(JsError::OutOfMemory) => { - let _ = global_object.throw_out_of_memory(); - core::ptr::null_mut() - } +/// C++ side declares `extern JSC_CALLCONV void* JSC_HOST_CALL_ATTRIBUTES` (SYSV_ABI on win-x64). +// HOST_EXPORT(BakeResponseClass__constructForSSR, jsc) +pub fn construct_for_ssr( + global_object: &JSGlobalObject, + call_frame: &CallFrame, + bake_ssr_has_jsx: &mut c_int, + js_this: JSValue, +) -> *mut c_void { + match constructor(global_object, call_frame, bake_ssr_has_jsx, js_this) { + Ok(response) => bun_core::heap::into_raw(response).cast::(), + Err(JsError::Thrown) => core::ptr::null_mut(), + Err(JsError::Terminated) => { + // A constructor runs beneath script: rethrow so the caller keeps unwinding. + let _ = bun_jsc::Stopped.throw(global_object); + core::ptr::null_mut() + } + Err(JsError::OutOfMemory) => { + let _ = global_object.throw_out_of_memory(); + core::ptr::null_mut() } } } @@ -88,7 +71,7 @@ fn constructor( callframe: &CallFrame, bake_ssr_has_jsx: &mut c_int, js_this: JSValue, -) -> JsResult<*mut Response> { +) -> JsResult> { let arguments: [JSValue; 2] = callframe.arguments_as_array::<2>(); // Allow `return new Response( ... , { ... }` @@ -111,54 +94,29 @@ fn constructor( Response::constructor(global_this, callframe, js_this) } -// Raw JSHostFn shim that #[bun_jsc::host_fn] would emit for `construct_redirect`; -// TODO(refactor): replace this hand-written export with the macro. -// C++ side declares `extern "C" SYSV_ABI ... JSC_HOST_CALL_ATTRIBUTES`. -bun_jsc::jsc_host_abi! { - #[unsafe(no_mangle)] - pub(crate) unsafe fn BakeResponseClass__constructRedirect( - global_object: &JSGlobalObject, - call_frame: &CallFrame, - ) -> JSValue { - bun_jsc::to_js_host_call(global_object, || construct_redirect(global_object, call_frame)) - } -} - -fn construct_redirect(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +// HOST_EXPORT(BakeResponseClass__constructRedirect) +pub fn construct_redirect( + global_this: &JSGlobalObject, + callframe: &CallFrame, +) -> JsResult { let response = Response::construct_redirect_impl(global_this, callframe)?; - let response = Box::new(response); let vm = global_this.bun_vm().as_mut(); // Check if dev_server_async_local_storage is set (indicating we're in Bun dev server) if let Some(async_local_storage) = vm.get_dev_server_async_local_storage()? { assert_streaming_disabled(global_this, async_local_storage, b"Response.redirect")?; // Ownership of the allocation transfers to the JS wrapper. - let ptr = bun_core::heap::into_raw(response); - // SAFETY: `ptr` is a fresh heap allocation; JS wrapper adopts it. - return Ok(unsafe { to_js_for_ssr(ptr, global_this, SSRKind::Redirect) }); + return Ok(to_js_for_ssr(response, global_this, SSRKind::Redirect)); } // Ownership of the allocation transfers to the JS wrapper (freed in // `ResponseClass__finalize`). - let ptr = bun_core::heap::into_raw(response); - // SAFETY: `ptr` is a fresh heap allocation; `Response::to_js` hands it to - // the C++ wrapper which owns it thereafter. - Ok(unsafe { &mut *ptr }.to_js(global_this)) -} - -// C++ side declares `extern "C" SYSV_ABI ... JSC_HOST_CALL_ATTRIBUTES`. -bun_jsc::jsc_host_abi! { - #[unsafe(no_mangle)] - pub(crate) unsafe fn BakeResponseClass__constructRender( - global_object: &JSGlobalObject, - call_frame: &CallFrame, - ) -> JSValue { - bun_jsc::to_js_host_call(global_object, || construct_render(global_object, call_frame)) - } + Ok(response.into_js(global_this)) } /// This function is only available on JSBakeResponse -fn construct_render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { +// HOST_EXPORT(BakeResponseClass__constructRender) +pub fn construct_render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments: [JSValue; 2] = callframe.arguments_as_array::<2>(); let vm = global_this.bun_vm().as_mut(); @@ -194,8 +152,10 @@ fn construct_render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsRe Init { status_code: 200, headers: { - let mut headers = HeadersRef::create_empty(); - headers.put(HTTPHeaderName::Location, &path_str, global_this)?; + let headers = HeadersRef::create_empty(); + headers + .headers() + .put(HTTPHeaderName::Location, &path_str, global_this)?; Some(headers) }, ..Default::default() @@ -206,9 +166,7 @@ fn construct_render(global_this: &JSGlobalObject, callframe: &CallFrame) -> JsRe )); // Ownership of the allocation transfers to the JS wrapper. - let ptr = bun_core::heap::into_raw(response); - // SAFETY: `ptr` is a fresh heap allocation; JS wrapper adopts it. - let response_js = unsafe { to_js_for_ssr(ptr, global_this, SSRKind::Render) }; + let response_js = to_js_for_ssr(response, global_this, SSRKind::Render); response_js.ensure_still_alive(); Ok(response_js) diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index 6abf47a766b1..f7cc710c98c9 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -1307,6 +1307,7 @@ impl BlobExt for Blob { global_this, readable_stream.value, file_sink.this_ptr().into(), + |s| file_sink.source.set(s), ); assignment_result.ensure_still_alive(); @@ -4223,7 +4224,7 @@ pub(crate) fn write_file_internal( // TODO: implement a writev() fast path let source_blob: Blob = 'brk: { - // `Response` and `Request` both expose `get_body_value()` / + // `Response` and `Request` both expose `body_value()` / // `get_body_readable_stream()` (`BodyMixin`). Every body borrow below // is re-derived and scoped so none spans the JS-running calls in the // arms. @@ -4243,7 +4244,7 @@ pub(crate) fn write_file_internal( // A stream someone holds a reader on, or has read from, is theirs. let existing = body.get_body_readable_stream() - .or_else(|| match body.get_body_value() { + .or_else(|| match body.body_value().get() { BodyValue::Locked(locked) => locked.readable.get(), _ => None, }); @@ -4254,8 +4255,8 @@ pub(crate) fn write_file_internal( } } // A body that is all here (also behind an untouched `.body` stream) is written as a blob. - body.get_body_value().to_blob_if_possible(); - let tag = match body.get_body_value() { + body.body_value().with_mut(|v| v.to_blob_if_possible()); + let tag = match body.body_value().get() { BodyValue::Error(_) => BodyTag::Error, BodyValue::Locked(_) => BodyTag::Locked, BodyValue::Used => { @@ -4267,17 +4268,19 @@ pub(crate) fn write_file_internal( match tag { BodyTag::Use => { // `use_()` runs no JS. - Ok(ControlFlow::Continue(body.get_body_value().use_())) + Ok(ControlFlow::Continue( + body.body_value().with_mut(|v| v.use_()), + )) } BodyTag::Error => { - let err_js = { - let BodyValue::Error(err_ref) = body.get_body_value() else { + let err_js = body.body_value().with_mut(|v| { + let BodyValue::Error(err_ref) = v else { unreachable!() }; err_ref.to_js(global_this) - }; + }); destination_blob.detach(); - let _ = body.get_body_value().use_(); + let _ = body.body_value().with_mut(|v| v.use_()); Ok(ControlFlow::Break( JSPromise::rejected_promise(global_this, err_js).to_js(), )) @@ -4292,9 +4295,11 @@ pub(crate) fn write_file_internal( let aws_options = s3.get_credentials_with_options(options.extra_options, global_this)?; // May run JS. - let _ = body.get_body_value().to_readable_stream(global_this)?; + let _ = body + .body_value() + .with_mut(|v| v.to_readable_stream(global_this))?; let readable_opt = body.get_body_readable_stream().or_else(|| { - let BodyValue::Locked(locked) = body.get_body_value() else { + let BodyValue::Locked(locked) = body.body_value().get() else { return None; }; locked.readable.get() @@ -4337,24 +4342,27 @@ pub(crate) fn write_file_internal( // A body that is a stream, or that its producer can stream (fetch, the // server, HTMLRewriter): pipe it into the file instead of collecting it in // memory first. The stream also outlives the Response it came from. - let streamable = body.get_body_readable_stream().is_some() || { - let BodyValue::Locked(locked) = body.get_body_value() else { - unreachable!() - }; - locked.readable.has() || locked.on_start_streaming.is_some() - }; + let streamable = body.get_body_readable_stream().is_some() + || body.body_value().with_mut(|v| { + let BodyValue::Locked(locked) = v else { + unreachable!() + }; + locked.readable.has() || locked.on_start_streaming.is_some() + }); if streamable { // May run JS. - let _ = body.get_body_value().to_readable_stream(global_this)?; + let _ = body + .body_value() + .with_mut(|v| v.to_readable_stream(global_this))?; let readable = body.get_body_readable_stream().or_else(|| { - let BodyValue::Locked(locked) = body.get_body_value() else { + let BodyValue::Locked(locked) = body.body_value().get() else { return None; }; locked.readable.get() }); // `to_readable_stream` may have replaced the value. if let (Some(readable), BodyValue::Locked(_)) = - (readable, body.get_body_value()) + (readable, body.body_value().get()) { let promise = destination_blob.pipe_readable_stream_to_blob( global_this, @@ -4368,22 +4376,35 @@ pub(crate) fn write_file_internal( }); if !failed { // The stream now belongs to the sink. - *body.get_body_value() = BodyValue::Used; + body.body_value().set(BodyValue::Used); } return Ok(ControlFlow::Break(promise)); } // The producer settled the body while the stream was being made. - match body.get_body_value() { - BodyValue::Locked(_) => {} - BodyValue::Error(err) => { - let err_js = err.to_js(global_this); + enum Settled { + No, + Error(JSValue), + Value, + } + let settled = body.body_value().with_mut(|v| match v { + BodyValue::Locked(_) => Settled::No, + BodyValue::Error(err) => Settled::Error(err.to_js(global_this)), + _ => Settled::Value, + }); + match settled { + Settled::No => {} + Settled::Error(err_js) => { destination_blob.detach(); - let _ = body.get_body_value().use_(); + let _ = body.body_value().with_mut(|v| v.use_()); return Ok(ControlFlow::Break( JSPromise::rejected_promise(global_this, err_js).to_js(), )); } - _ => return Ok(ControlFlow::Continue(body.get_body_value().use_())), + Settled::Value => { + return Ok(ControlFlow::Continue( + body.body_value().with_mut(|v| v.use_()), + )); + } } } let task = Box::new(WriteFileWaitFromLockedValueTask { @@ -4397,11 +4418,15 @@ pub(crate) fn write_file_internal( mkdirp_if_not_exists: options.mkdirp_if_not_exists.unwrap_or(true), }); let promise = task.promise.value(); - let BodyValue::Locked(locked) = body.get_body_value() else { - unreachable!() - }; - let producer_hook = locked.on_start_buffering.take().zip(locked.task); - locked.on_receive_value = Some(webcore::body::ReceiveValue::WriteFile(task)); + let producer_hook = body.body_value().with_mut(|v| { + let BodyValue::Locked(locked) = v else { + unreachable!() + }; + let producer_hook = locked.on_start_buffering.take().zip(locked.task); + locked.on_receive_value = + Some(webcore::body::ReceiveValue::WriteFile(task)); + producer_hook + }); // Signalled last (see `PendingValue::on_start_buffering`): // the task may run and the body value be replaced inside. if let Some((on_start_buffering, producer_task)) = producer_hook { @@ -5268,7 +5293,7 @@ impl read_file::ReadFileToJs for ToFormDataWithBytesFn { pub enum Any { Blob(Blob), InternalBlob(Internal), - WTFStringImpl(bun_core::WTFStringImpl), + WTFStringImpl(bun_core::WTFString), } impl Any { @@ -5300,8 +5325,7 @@ impl Any { pub(crate) fn memory_cost(&self) -> usize { match self { Any::Blob(blob) => blob.store().map(|s| s.memory_cost()).unwrap_or(0), - Any::WTFStringImpl(str) => { - let s = super::body::wtf_impl(str); + Any::WTFStringImpl(s) => { if s.ref_count() == 1 { s.memory_cost() } else { @@ -5323,7 +5347,7 @@ impl Any { pub(crate) fn fast_size(&self) -> SizeType { match self { Any::Blob(b) => b.size.get(), - Any::WTFStringImpl(s) => super::body::wtf_impl(s).byte_length() as SizeType, + Any::WTFStringImpl(s) => s.byte_length() as SizeType, Any::InternalBlob(_) => self.slice().len() as SizeType, } } @@ -5332,7 +5356,7 @@ impl Any { pub(crate) fn size(&self) -> SizeType { match self { Any::Blob(b) => b.size.get(), - Any::WTFStringImpl(s) => super::body::wtf_impl(s).utf8_byte_length() as SizeType, + Any::WTFStringImpl(s) => s.utf8_byte_length() as SizeType, _ => self.slice().len() as SizeType, } } @@ -5450,8 +5474,9 @@ impl Any { str } Any::WTFStringImpl(impl_) => { - let str = - BunString::adopt_wtf_impl(core::mem::replace(impl_, core::ptr::null_mut())); + // Copy the handle out (a ref) and drop `self`'s (a deref) + // rather than moving the whole `Any`. + let str = BunString::from(impl_.clone()); *self = Any::Blob(Blob::default()); if str.length() == 0 { return Ok(JSValue::NULL); @@ -5522,8 +5547,9 @@ impl Any { Ok(owned) } Any::WTFStringImpl(impl_) => { - let str = - BunString::adopt_wtf_impl(core::mem::replace(impl_, core::ptr::null_mut())); + // Copy the handle out (a ref) and drop `self`'s (a deref) + // rather than moving the whole `Any`. + let str = BunString::from(impl_.clone()); *self = Any::Blob(Blob::default()); str.into_js(global) } @@ -5560,8 +5586,9 @@ impl Any { jsc::ArrayBuffer::from_default_allocator(global, TYPED_ARRAY_VIEW, bytes) } Any::WTFStringImpl(impl_) => { - let str = - BunString::adopt_wtf_impl(core::mem::replace(impl_, core::ptr::null_mut())); + // Copy the handle out (a ref) and drop `self`'s (a deref) + // rather than moving the whole `Any`. + let str = BunString::from(impl_.clone()); *self = Any::Blob(Blob::default()); let out_bytes = str.to_utf8(); @@ -5582,7 +5609,7 @@ impl Any { match self { Any::Blob(blob) => blob.is_detached(), Any::InternalBlob(ib) => ib.bytes.is_empty(), - Any::WTFStringImpl(s) => super::body::wtf_impl(s).length() == 0, + Any::WTFStringImpl(s) => s.length() == 0, } } } @@ -5617,7 +5644,7 @@ impl Any { pub(crate) fn slice(&self) -> &[u8] { match self { Any::Blob(b) => b.shared_view(), - Any::WTFStringImpl(s) => super::body::wtf_impl(s).utf8_slice(), + Any::WTFStringImpl(s) => s.utf8_slice(), Any::InternalBlob(ib) => ib.slice_const(), } } @@ -5647,9 +5674,8 @@ impl Any { ib.bytes.shrink_to_fit(); *self = Any::Blob(Blob::default()); } - Any::WTFStringImpl(s) => { - // `Any` owns one ref on the WTFStringImpl pointee. - super::body::wtf_impl(s).deref(); + Any::WTFStringImpl(_) => { + // Dropping the handle releases `Any`'s ref on the WTFStringImpl. *self = Any::Blob(Blob::default()); } } diff --git a/src/runtime/webcore/Body.rs b/src/runtime/webcore/Body.rs index 493b63960056..21f3188e3ba5 100644 --- a/src/runtime/webcore/Body.rs +++ b/src/runtime/webcore/Body.rs @@ -19,81 +19,27 @@ use crate::jsc::HTTPHeaderName; pub use crate::webcore::InternalBlob; use crate::webcore::form_data::AsyncFormDataExt as _; use bun_core::String as BunString; -use bun_core::{Utf8Bytes, WTFStringImpl, WTFStringImplExt as _, WTFStringImplStruct}; -use bun_jsc::JsCell; +use bun_core::{Utf8Bytes, WTFString, WTFStringImplExt as _}; use bun_jsc::StringJsc as _; -use bun_jsc::bun_string_jsc; - -/// Deref the `Value::WTFStringImpl` / `AnyBlob::WTFStringImpl` payload. -/// Centralises the per-site `(**s)` raw deref at the dozen `match` arms below -/// (and in `Blob::Any`, `Response::construct_json`). -/// -/// # Safety (encapsulated) -/// `Value::WTFStringImpl` always stores a non-null `*mut WTF::StringImpl` -/// (constructed via `String::leak_wtf_impl()` / `r#ref()`); the body holds a -/// +1 intrusive ref for as long as the variant is active, so the pointee is -/// live for any borrow tied to `&s`. All `WTFStringImplStruct` methods take -/// `&self` (refcount lives in a `Cell`), so a shared borrow suffices even for -/// `r#ref()` / `deref()`. -#[inline(always)] -pub(super) fn wtf_impl(s: &WTFStringImpl) -> &WTFStringImplStruct { - // SAFETY: see fn doc — non-null, intrusive-refcounted, live while held. - unsafe { &**s } -} - -/// Mutable view of a [`Blob`]'s backing `Store` through its -/// `JsCell>>` field. Centralises the per-site raw -/// `(*blob.store.get()…as_ptr()).mime_type = …` deref under the same -/// invariant `Store::data_mut` already documents: -/// shared-mutable interior, single-threaded JS event-loop, no concurrent -/// `&Store` outstanding for the borrow's duration. -#[inline] -#[allow(clippy::mut_from_ref)] -fn blob_store_mut(blob: &Blob) -> Option<&mut blob::Store> { - blob.store - .get() - .as_ref() - // SAFETY: `RefPtr` invariant — pointee is a live heap `Store` while - // any `RefPtr` exists; single-threaded JS event-loop discipline - // guarantees no other `&`/`&mut Store` is live for this borrow. - .map(|s| unsafe { &mut *s.as_ptr() }) -} +use bun_jsc::{JsCell, bun_string_jsc}; fn set_blob_content_type(blob: &Blob, mime_type: MimeType) { blob.content_type_was_set.set(true); - if let Some(store) = blob_store_mut(blob) { - store.mime_type = mime_type.clone(); + if let Some(store) = blob.store.get().as_ref() { + blob::Store::set_mime_type(store, mime_type.clone()); } blob.content_type .set(blob::BlobContentType::from(mime_type)); } -// ──────────────────────────────────────────────────────────────────────────── -// Local shims for upstream-gated `JsClass` impls / `AnyPromise` methods. -// These adapt call sites in this file without editing `bun_jsc` (orphan rule). -// ──────────────────────────────────────────────────────────────────────────── - -#[inline] -fn as_dom_form_data(value: JSValue) -> Option<*mut DOMFormData> { - // `DOMFormData` is an opaque C++ type without a `#[bun_jsc::JsClass]` derive; - // route through the hand-written `from_js` (`DOMFormData.rs`) instead of - // `value.as_::()`. - DOMFormData::from_js(value).map(std::ptr::from_mut::) -} -#[inline] -fn as_url_search_params(value: JSValue) -> Option<*mut URLSearchParams> { - // See `as_dom_form_data` — opaque C++ type, hand-written `from_js`. - URLSearchParams::from_js(value).map(|p| p.as_ptr()) -} - bun_core::declare_scope!(BodyValue, visible); bun_core::declare_scope!(BodyMixin, visible); // R-2 (host-fn re-entrancy): `Body` is embedded inline in JS-exposed -// `Response` (and aliased via `HiveRef` in `Request`). Every BodyMixin host -// fn takes `&self` and projects `&mut Value` through this `JsCell`; the -// `UnsafeCell` inside suppresses LLVM `noalias` on `&Body` so a re-entrant -// host call cannot stack two `&mut` to the same field. +// `Response` (and pooled in a `HiveRef` for `Request`). Every BodyMixin host +// fn takes `&self` and reaches the `Value` through this `JsCell` in +// closure-scoped borrows, so a re-entrant host call cannot stack two `&mut` +// to the same field. #[repr(C)] pub struct Body { pub value: JsCell, // = Value::Empty, @@ -115,21 +61,13 @@ impl Body { } } - /// R-2 interior-mutability projection: `&self` → `&mut Value`. - /// Single-JS-thread invariant (see `JsCell`) makes this sound; keep the - /// returned borrow short and do not hold it across a call that re-enters - /// JS and may touch this same body. #[inline] - #[allow(clippy::mut_from_ref)] - pub(crate) fn value_mut(&self) -> &mut Value { - // SAFETY: single-JS-thread invariant — `Body` lives inside a - // `Request`/`Response` JSC heap cell; concurrent access is impossible - // and re-entrant host fns each form a fresh short-lived borrow. - unsafe { self.value.get_mut() } + pub(crate) fn into_value(self) -> Value { + self.value.into_inner() } pub(crate) fn len(&self) -> blob::SizeType { - self.value_mut().size() + self.value.with_mut(|v| v.size()) } } @@ -157,7 +95,7 @@ impl Body { ) .map_err(|_| core::fmt::Error)?; - match self.value_mut() { + match self.value.get() { Value::Blob(blob) => { formatter.print_comma::(writer)?; writer.write_str("\n")?; @@ -170,7 +108,7 @@ impl Body { // global. Compute the size from the matched payload directly. let size = match v { Value::InternalBlob(b) => b.slice_const().len(), - Value::WTFStringImpl(s) => wtf_impl(s).utf8_byte_length(), + Value::WTFStringImpl(s) => s.utf8_byte_length(), _ => unreachable!(), }; formatter.print_comma::(writer)?; @@ -204,7 +142,7 @@ impl Body { // renamed to `reset()` since it cannot take `self` by value (in-place state transition). impl Body { pub fn reset(&self) { - self.value_mut().reset(); + self.value.with_mut(Value::reset); } } @@ -326,8 +264,6 @@ impl PendingValue { /// If the size is unknown will be 0 fn size_hint(&self) -> blob::SizeType { if let Some(readable) = self.readable.get() { - // BACKREF: see `Source::bytes()` — payload live while the - // ReadableStream JS wrapper (rooted via `self.readable`) is alive. if let Some(bytes) = readable.ptr.bytes() { return bytes.size_hint.get(); } @@ -389,12 +325,17 @@ impl PendingValue { None } + /// Commit this pending body to `action`. The script half of the read — + /// draining an already-realised stream, or telling the producer to start + /// buffering (which may settle and so replace the `Value` this lives in) + /// — is returned for the caller to [`run`](LockedRead::run) once its + /// borrow of the body is released. fn set_promise( &mut self, global_this: &JSGlobalObject, action: Action, owned_readable: Option, - ) -> JsResult { + ) -> JsResult { self.action = action; if let Some(readable) = owned_readable.or_else(|| self.readable.get()) { match &mut self.action { @@ -404,15 +345,13 @@ impl PendingValue { | Action::GetBlob | Action::GetArrayBuffer | Action::GetBytes => { - let promise = match &mut self.action { - Action::GetJSON => global_this.readable_stream_to_json(readable.value), - Action::GetArrayBuffer => { - global_this.readable_stream_to_array_buffer(readable.value) - } - Action::GetBytes => global_this.readable_stream_to_bytes(readable.value), - Action::GetText => global_this.readable_stream_to_text(readable.value), - Action::GetBlob => global_this.readable_stream_to_blob(readable.value), - Action::GetFormData(form_data) => 'brk: { + let (kind, encoding) = match &mut self.action { + Action::GetJSON => (StreamRead::JSON, JSValue::UNDEFINED), + Action::GetArrayBuffer => (StreamRead::ArrayBuffer, JSValue::UNDEFINED), + Action::GetBytes => (StreamRead::Bytes, JSValue::UNDEFINED), + Action::GetText => (StreamRead::Text, JSValue::UNDEFINED), + Action::GetBlob => (StreamRead::Blob, JSValue::UNDEFINED), + Action::GetFormData(form_data) => { let fd = form_data.take().unwrap(); let encoding_js = match &fd.encoding { bun_core::form_data::Encoding::Multipart(multipart) => { @@ -421,8 +360,7 @@ impl PendingValue { bun_core::form_data::Encoding::URLEncoded => JSValue::UNDEFINED, }; // fd dropped at end of scope (Box -> Drop) - break 'brk global_this - .readable_stream_to_form_data(readable.value, encoding_js); + (StreamRead::FormData, encoding_js) } _ => unreachable!(), }; @@ -430,7 +368,11 @@ impl PendingValue { // The ReadableStream within is expected to keep this Promise alive. // If you try to protect() this, it will leak memory because the other end of the ReadableStream won't call it. // See https://github.com/oven-sh/bun/issues/13678 - return promise; + return Ok(LockedRead::Stream { + readable, + kind, + encoding, + }); } Action::None => {} } @@ -442,13 +384,74 @@ impl PendingValue { self.promise = Some(promise_value); promise_value.protect(); - if let Some(on_start_buffering) = self.on_start_buffering.take() { - // Last use of `self`: the producer may settle the body (and so - // replace `*self`) before this returns. - let task = self.task.unwrap(); - on_start_buffering(task); + // Last use of `self`: the producer may settle the body (and so + // replace `*self`) from inside the hook. + let start = self + .on_start_buffering + .take() + .map(|hook| (hook, self.task.unwrap())); + Ok(LockedRead::Buffer { + promise: promise_value, + start, + }) + } + } +} + +/// Which `readableStreamTo*` builtin drains the stream. +pub enum StreamRead { + Text, + JSON, + ArrayBuffer, + Bytes, + Blob, + FormData, +} + +/// The script half of a [`PendingValue::set_promise`] read; run it with no +/// borrow of the body live (a JS-backed stream's `pull`, or the producer's +/// start hook, can reach the same body again). +#[must_use] +pub enum LockedRead { + Stream { + readable: ReadableStream, + kind: StreamRead, + encoding: JSValue, + }, + Buffer { + promise: JSValue, + start: Option<(fn(ctx: NonNull), NonNull)>, + }, +} + +impl LockedRead { + pub fn run(self, global_this: &JSGlobalObject) -> JsResult { + match self { + LockedRead::Stream { + readable, + kind, + encoding, + } => { + let _keep = jsc::EnsureStillAlive(readable.value); + match kind { + StreamRead::JSON => global_this.readable_stream_to_json(readable.value), + StreamRead::ArrayBuffer => { + global_this.readable_stream_to_array_buffer(readable.value) + } + StreamRead::Bytes => global_this.readable_stream_to_bytes(readable.value), + StreamRead::Text => global_this.readable_stream_to_text(readable.value), + StreamRead::Blob => global_this.readable_stream_to_blob(readable.value), + StreamRead::FormData => { + global_this.readable_stream_to_form_data(readable.value, encoding) + } + } + } + LockedRead::Buffer { promise, start } => { + if let Some((on_start_buffering, task)) = start { + on_start_buffering(task); + } + Ok(promise) } - Ok(promise_value) } } } @@ -499,6 +502,10 @@ pub(crate) trait BodyOwnerJs { // Pooled inline in `HiveRef` slots; boxing `Blob` would change // construction/match sites across many files and defeat the pool. #[allow(clippy::large_enum_variant)] +// Plain tag + union rather than niche-filled through `WTFString`'s non-null +// pointer: measurably cheaper moves/matches on the Request/Response +// constructor and body-reader paths. +#[repr(u8)] pub enum Value { Blob(Blob), @@ -534,11 +541,9 @@ pub enum Value { /// ``` /// /// This works for .json(), too. - // `bun_core::WTFStringImpl` = `*mut WTFStringImplStruct` — a Copy raw - // pointer to an *intrusively* refcounted WTF::StringImpl. We hold the +1 directly - // (no Arc) and ref/deref explicitly at fixed points (from_js / clone / - // use_ / to_blob_if_possible / reset / use_as_any_blob*). - WTFStringImpl(WTFStringImpl), + // The body's own +1 on the *intrusively* refcounted `WTF::StringImpl`; + // released when the handle is dropped or moved out. + WTFStringImpl(WTFString), /// Single-use Blob /// Avoids a heap allocation. InternalBlob(InternalBlob), @@ -554,20 +559,14 @@ const POOL_SIZE: usize = if bun_alloc::heap_breakdown::ENABLED { } else { 256 }; -pub(crate) type HiveRef = bun_collections::HiveRef; +pub(crate) type HiveRef = bun_collections::HiveRef; pub(crate) type HiveAllocator = bun_collections::hive_array::Fallback; -pub(crate) type BodyHiveHandle = bun_collections::HiveRefHandle; +pub(crate) type BodyHiveHandle = bun_collections::HiveRefHandle; /// Moves `value` into a pooled `HiveRef` slot and returns an owning handle /// (ref_count = 1). pub(crate) fn hive_alloc(value: Value) -> BodyHiveHandle { - let state = crate::jsc_hooks::runtime_state(); - debug_assert!(!state.is_null(), "hive_alloc before init_runtime_state"); - // SAFETY: `state` is the live boxed RuntimeState; `body_value_pool` is a - // heap-stable `Box` for the VM lifetime. - let pool = unsafe { &raw const **(*state).body_value_pool }; - // SAFETY: `pool` outlives every handle (process lifetime). - unsafe { BodyHiveHandle::new(value, pool) } + crate::jsc_hooks::body_hive_alloc(Body::new(value)) } #[derive(Clone, Copy, PartialEq, Eq, strum::IntoStaticStr)] @@ -665,7 +664,7 @@ impl ValueError { impl From for Value { /// Each arm moves its payload as is: a `WTFStringImpl`'s `+1` travels with - /// the pointer and is released by `Value::drop`, so nothing is ref'd here. + /// the handle, so nothing is ref'd here. fn from(blob: AnyBlob) -> Value { match blob { AnyBlob::Blob(b) => Value::Blob(b), @@ -676,25 +675,21 @@ impl From for Value { } impl Value { - /// Downcast a `JSValue` to the `Body.Value` it owns, if any. + /// The `Body` a `Request`/`Response` wrapper `value` owns, if it is one. /// /// `Body.Value` is not itself a JS class — it lives inside a `Request` or /// `Response` wrapper — so the generic `JSValue::as_::()` path - /// cannot be used. Instead, try both wrapper classes and return the inner - /// body pointer. - /// - /// Returns a raw pointer; the storage is owned - /// by the JSC heap cell and outlives the call only as long as `value` is - /// kept alive by the caller. - pub(crate) fn from_request_or_response(value: JSValue) -> Option<*mut Value> { + /// cannot be used. The storage is owned by the JSC heap cell and outlives + /// the call only as long as `value` is kept alive by the caller. + pub(crate) fn from_request_or_response(value: JSValue) -> Option<&'static Body> { if value.is_empty_or_undefined_or_null() { return None; } if let Some(req) = value.as_class_ref::() { - return Some(std::ptr::from_mut::(req.get_body_value())); + return Some(req.body()); } if let Some(res) = value.as_class_ref::() { - return Some(std::ptr::from_mut::(res.get_body_value())); + return Some(res.body()); } None } @@ -710,11 +705,11 @@ impl Value { impl Value { pub(crate) fn to_blob_if_possible(&mut self) { - if let Value::WTFStringImpl(str) = *self { - if let Utf8Bytes::Owned(bytes) = wtf_impl(&str).to_utf8() { + if let Value::WTFStringImpl(str) = &*self { + if let Utf8Bytes::Owned(bytes) = str.to_utf8() { // The UTF-8 buffer is already heap-owned by the slice wrapper; - // transfer it (no copy). The deref is handled by `Value::drop` on the - // overwritten `WTFStringImpl` variant — do NOT deref explicitly here. + // transfer it (no copy). The overwritten `WTFStringImpl` handle + // releases its ref on assignment. *self = Value::InternalBlob(InternalBlob { bytes, was_string: true, @@ -735,7 +730,7 @@ impl Value { match self { Value::Blob(b) => b.get_size_for_bindings() as blob::SizeType, Value::InternalBlob(b) => b.slice_const().len() as blob::SizeType, - Value::WTFStringImpl(s) => wtf_impl(s).utf8_byte_length() as blob::SizeType, + Value::WTFStringImpl(s) => s.utf8_byte_length() as blob::SizeType, Value::Locked(l) => l.size_hint(), _ => 0, } @@ -744,7 +739,7 @@ impl Value { pub(crate) fn memory_cost(&self) -> usize { match self { Value::InternalBlob(b) => b.memory_cost(), - Value::WTFStringImpl(s) => wtf_impl(s).memory_cost(), + Value::WTFStringImpl(s) => s.memory_cost(), // Not `size_hint()`: a Locked body owns no bytes (they live in the // ByteStream buffer, separately accounted), so reporting the // content-length here mis-trains JSC's GC live-size estimate. @@ -756,7 +751,7 @@ impl Value { pub(crate) fn estimated_size(&self) -> usize { match self { Value::InternalBlob(b) => b.slice_const().len(), - Value::WTFStringImpl(s) => wtf_impl(s).byte_slice().len(), + Value::WTFStringImpl(s) => s.byte_slice().len(), // See memory_cost(): size_hint is anticipated, not allocated. Value::Locked(_) => 0, _ => 0, @@ -888,31 +883,19 @@ impl Value { return ReadableStream::empty(global_this); } - // `new_mut` centralises the post-allocation deref; ownership of the - // heap `NewSource` transfers to the JS wrapper's `m_ctx` in - // `to_readable_stream()` below (freed by the GC finalizer). - let reader = webcore::readable_stream::NewSource::::new_mut( - webcore::readable_stream::NewSource { - // `ByteStream::default()` is the post-setup state. - context: ByteStream::default(), - global_this: Some(bun_ptr::BackRef::new(global_this)), - ..Default::default() - }, - ); - + // The JS wrapper made by `to_readable_stream()` below owns the source + // from here (freed by the GC finalizer). + let reader = + webcore::readable_stream::NewSource::new(ByteStream::new(drain_result), global_this); reader.producer.set(locked.producer); - reader.context.setup(); - reader.context.apply_drain_result(drain_result); - - let context_ptr: *mut ByteStream = &raw mut reader.context; let stream_value = if text_mode { reader.to_text_readable_stream(global_this)? } else { reader.to_readable_stream(global_this)? }; let readable = ReadableStream { - ptr: webcore::readable_stream::Source::Bytes(context_ptr), + ptr: webcore::readable_stream::Source::Bytes(reader.as_context_ptr()), value: stream_value, }; locked.readable = webcore::readable_stream::Strong::init(readable, global_this); @@ -949,8 +932,8 @@ impl Value { debug_assert!(str.tag() == bun_core::Tag::WTFStringImpl); - // `leak_wtf_impl()` transfers the +1 ref out of the bun_core::String wrapper. - return Ok(Value::WTFStringImpl(str.leak_wtf_impl())); + // `into_wtf()` moves the +1 ref out of the bun_core::String wrapper. + return Ok(Value::WTFStringImpl(str.into_wtf().unwrap())); } if js_type.is_typed_array_or_array_buffer() { @@ -970,25 +953,22 @@ impl Value { } } - if let Some(form_data) = as_dom_form_data(value) { - // SAFETY: shim returns a live JSC heap cell. - return Ok(Value::Blob(Blob::from_dom_form_data(global_this, unsafe { - &mut *form_data - }))); + if let Some(form_data) = DOMFormData::from_js(value) { + return Ok(Value::Blob(Blob::from_dom_form_data( + global_this, + form_data, + ))); } - if let Some(search_params) = as_url_search_params(value) { - // SAFETY: shim returns a live JSC heap cell. + if let Some(search_params) = URLSearchParams::from_js(value) { + // S008: `URLSearchParams` is an `opaque_ffi!` ZST handle — safe deref. return Ok(Value::Blob(Blob::from_url_search_params( global_this, - unsafe { &mut *search_params }, + bun_opaque::opaque_deref_mut(search_params.as_ptr()), ))); } if js_type == jsc::JSType::DOMWrapper { - // `as_class_ref` is the safe shared-borrow downcast (one audited - // unsafe in `JSValue`); `dupe_with_content_type` / `encode_for_body` - // both take `&self`. if let Some(blob) = value.as_class_ref::() { return Ok(Value::Blob( // We must preserve "type" so that DOMFormData and the "type" field are preserved. @@ -1004,10 +984,9 @@ impl Value { // Blob.Store frees via an Allocator, so dupe out of the // codec's allocator here. The hot path (`.bytes()`) hands the // codec buffer to JS without this copy. - // SAFETY: `encoded.bytes` is the codec-owned slice; copy then drop frees it. - let owned: Box<[u8]> = Box::from(unsafe { encoded.bytes.as_ref() }); + let owned: Vec = encoded.as_slice().to_vec(); drop(encoded); - let blob = Blob::init(owned.into_vec(), global_this); + let blob = Blob::init(owned, global_this); blob.content_type .set(blob::BlobContentType::Static(mime.as_bytes())); blob.content_type_was_set.set(true); @@ -1025,16 +1004,12 @@ impl Value { ))); } - match readable.ptr { - webcore::readable_stream::Source::Blob(blob) => { - // SAFETY: `Source::Blob` holds a live *mut ByteBlobLoader for the - // lifetime of the ReadableStream JS wrapper. - let result = unsafe { (*blob).to_any_blob(global_this) } - .map_or(Value::Empty, Value::from); - readable.force_detach(global_this); - return Ok(result); - } - _ => {} + if let Some(blob) = readable.ptr.blob() { + let result = blob + .to_any_blob(global_this) + .map_or(Value::Empty, Value::from); + readable.force_detach(global_this); + return Ok(result); } return Ok(Value::from_readable_stream_without_lock_check( @@ -1074,8 +1049,6 @@ impl Value { .then(|| readable.ptr.bytes()) .flatten() .map(|bytes| { - // BACKREF: `Source::bytes()` payload is live for the - // ReadableStream JS wrapper's lifetime. let mut blob = new.use_as_any_blob_allow_non_utf8_string(); bytes.on_data(streams::Result::TemporaryAndDone(bun_ptr::RawSlice::new( blob.slice(), @@ -1146,8 +1119,6 @@ impl Value { r?; break 'inner; }; - // `webcore::form_data::AsyncFormData` re-exports `bun_core::form_data::AsyncFormData`; - // `to_js` is provided via the `AsyncFormDataExt` extension trait. let result = async_form_data.to_js(global, blob.slice(), promise); blob.detach(); // async_form_data dropped (Box -> Drop replaces deinit) @@ -1186,16 +1157,12 @@ impl Value { match self { Value::Blob(b) => { - // `Value` has `Drop`, so we cannot move the `Blob` out by - // value (E0509). `mem::take` leaves a default `Blob` whose `deinit()` - // (run by `Value::drop` on the assignment below) is a no-op. let new_blob = core::mem::take(b); *self = Value::Used; debug_assert!(!new_blob.is_heap_allocated()); // owned by Body new_blob } Value::InternalBlob(ib) => { - // SAFETY: VirtualMachine::get() returns the live per-thread VM. let global = VirtualMachine::get().global(); let new_blob = Blob::init( ib.to_owned_slice(), @@ -1208,19 +1175,13 @@ impl Value { new_blob } Value::WTFStringImpl(wtf) => { - let wtf = *wtf; - // Transfer the body's +1 to local `wtf`; suppress `Value::drop` (which - // would deref) so the StringImpl stays alive across `to_utf8` and is - // released exactly once below. - let _ = core::mem::ManuallyDrop::new(core::mem::replace(self, Value::Used)); - let wtf_ref = wtf_impl(&wtf); - // SAFETY: VirtualMachine::get() returns the live per-thread VM. let global = VirtualMachine::get().global(); - let new_blob = Blob::init(wtf_ref.to_utf8().into_vec(), global); - // Release the +1 the body held. - wtf_ref.deref(); + let new_blob = Blob::init(wtf.to_utf8().into_vec(), global); + // Releases the body's ref on the string. + *self = Value::Used; new_blob } + // Leave the non-payload variants (`Locked`/`Error`/`Null`/…) in place. // `Blob::default()` leaves `global_this` null which matches the // don't-care contract here. _ => Blob::default(), @@ -1232,11 +1193,9 @@ impl Value { Value::Blob(b) => AnyBlob::Blob(core::mem::take(b)), Value::InternalBlob(b) => AnyBlob::InternalBlob(core::mem::take(b)), Value::WTFStringImpl(str) => { - if wtf_impl(str).can_use_as_utf8() { - // Transfer the body's +1 to AnyBlob; suppress `Value::drop` so the - // assignment below does not deref the StringImpl we just handed out. - let s = *str; - let _ = core::mem::ManuallyDrop::new(core::mem::replace(self, Value::Used)); + if str.can_use_as_utf8() { + let s = str.clone(); + *self = Value::Used; return Some(AnyBlob::WTFStringImpl(s)); } else { return None; @@ -1254,29 +1213,22 @@ impl Value { pub(crate) fn use_as_any_blob(&mut self) -> AnyBlob { let was_null = matches!(self, Value::Null); - // `Value` has `Drop`, so we cannot `mem::replace` then - // destructure by value (E0509). Match by `&mut` and `mem::take` the - // payload; the trailing `*self = Used/Null` runs `Value::drop` on the - // emptied/residual variant (no-op for taken Blob/InternalBlob, releases - // the +1 for the UTF-8-converted WTFStringImpl arm, deinit for Locked). let any_blob: AnyBlob = match self { Value::Blob(b) => AnyBlob::Blob(core::mem::take(b)), Value::InternalBlob(b) => AnyBlob::InternalBlob(core::mem::take(b)), - Value::WTFStringImpl(str) => 'brk: { - let str = *str; - let wtf_ref = wtf_impl(&str); - if let Utf8Bytes::Owned(utf8) = wtf_ref.to_utf8() { - // The deref is handled by `Value::drop` on the - // assignment below (the variant is still `WTFStringImpl(str)`). - break 'brk AnyBlob::InternalBlob(InternalBlob { + Value::WTFStringImpl(str) => { + if let Utf8Bytes::Owned(utf8) = str.to_utf8() { + // The handle's ref is released by the assignment below (the + // variant is still `WTFStringImpl(str)`). + AnyBlob::InternalBlob(InternalBlob { // Transfer ownership of the heap-allocated UTF-8 buffer (no copy). bytes: utf8, was_string: true, - }); + }) } else { - // Transfer the body's +1 into AnyBlob; suppress `Value::drop`. - let _ = core::mem::ManuallyDrop::new(core::mem::replace(self, Value::Used)); - break 'brk AnyBlob::WTFStringImpl(str); + let s = str.clone(); + *self = Value::Used; + AnyBlob::WTFStringImpl(s) } } Value::Locked(l) => l @@ -1291,14 +1243,14 @@ impl Value { pub(crate) fn use_as_any_blob_allow_non_utf8_string(&mut self) -> AnyBlob { let was_null = matches!(self, Value::Null); - // see `use_as_any_blob` — match by `&mut` to avoid E0509. let any_blob: AnyBlob = match self { Value::Blob(b) => AnyBlob::Blob(core::mem::take(b)), Value::InternalBlob(b) => AnyBlob::InternalBlob(core::mem::take(b)), Value::WTFStringImpl(s) => { - let s = *s; - // Transfer the body's +1 into AnyBlob; suppress `Value::drop`. - let _ = core::mem::ManuallyDrop::new(core::mem::replace(self, Value::Used)); + // Copy the handle out (a ref) and drop the body's (a deref) + // rather than moving the whole `Value`. + let s = s.clone(); + *self = Value::Used; AnyBlob::WTFStringImpl(s) } Value::Locked(l) => l @@ -1317,9 +1269,8 @@ impl Value { global: &JSGlobalObject, ) -> jsc::JsResult<()> { if let Value::Locked(_) = self { - // reshaped for borrowck + E0509 (`Value` has `Drop`) — `mem::take` - // the `PendingValue` out (leaves `Locked(default)`, whose Drop is a no-op on - // an empty readable), then overwrite with `Error`. + // Take the `PendingValue` out (leaves `Locked(default)`, which owns + // nothing), then overwrite with `Error`. let mut locked = match self { Value::Locked(l) => core::mem::take(l), _ => unreachable!(), @@ -1354,8 +1305,6 @@ impl Value { // The Promise version goes before the ReadableStream version incase the Promise version is used too. // Avoid creating unnecessary duplicate JSValue. if let Some(readable) = strong_readable.get() { - // BACKREF: see `Source::bytes()` — payload live for the - // lifetime of the ReadableStream JS wrapper. if let Some(bytes) = readable.ptr.bytes() { bytes.on_data(streams::Result::Err(err_ref.to_stream_error(global))); } else { @@ -1380,14 +1329,15 @@ impl Value { } // mutates self to Null and is called explicitly at specific protocol points. - // Renamed from `deinit` per PORTING.md (never expose `pub fn deinit(&mut self)`). Now - // delegates the actual resource release to `Drop` (below) via assignment, so a later - // `HiveArray::put()` → `drop_in_place` on the resulting `Null` is a guaranteed no-op - // (idempotent — no double-free). + // Renamed from `deinit` per PORTING.md (never expose `pub fn deinit(&mut self)`). + // Every variant's payload releases what it owns when dropped (the Blob's + // store, the `WTF::StringImpl` ref, the InternalBlob's buffer, the Locked + // stream root, the Error's Strong), so the assignment below — like a + // `HiveRef` slot being recycled — is the whole release. pub fn reset(&mut self) { if let Value::Locked(locked) = self { // Locked stays Locked (callers may still inspect the variant after - // reset()); flip the `deinit` latch so Drop is a no-op afterwards. + // reset()). if !locked.deinit { locked.deinit = true; locked.readable.deinit(); @@ -1395,39 +1345,10 @@ impl Value { } return; } - // Assignment runs `Drop` on the old variant: deref WTFStringImpl, deinit - // Blob, free InternalBlob's Vec, reset Error. Null/Used/Empty are no-ops. *self = Value::Null; } } -/// Runs when a `HiveRef` slot is recycled -/// (`HiveArray::Fallback::put` → `drop_in_place`; see -/// `bun_collections::HiveRef::unref`). Without this impl `Request`/`Response` -/// GC finalization leaked `WTFStringImpl` refs / `Blob` stores / -/// `InternalBlob` buffers (H3 elysia rss). -/// -/// Unlike `reset()` this never reassigns `*self` (it's already being torn -/// down), so calling `reset()` first then dropping (or dropping a `Null` -/// produced by `reset()`) is a no-op second pass — no double-free. -impl Drop for Value { - fn drop(&mut self) { - match self { - Value::Locked(locked) => { - if !locked.deinit { - locked.deinit = true; - locked.readable.deinit(); - } - } - Value::WTFStringImpl(s) => wtf_impl(s).deref(), - Value::Blob(b) => b.deinit(), - Value::Error(e) => e.reset(), - // `InternalBlob`'s `Vec` is freed by the compiler's drop glue. - Value::InternalBlob(_) | Value::Used | Value::Empty | Value::Null => {} - } - } -} - impl Value { pub(crate) fn tee( &mut self, @@ -1485,19 +1406,10 @@ impl Value { return Ok(Value::Null); } - // `new_mut` centralises the post-allocation deref; ownership of the - // heap `NewSource` transfers to the JS wrapper's `m_ctx` in - // `to_readable_stream()` below (freed by the GC finalizer). - let reader = webcore::readable_stream::NewSource::::new_mut( - webcore::readable_stream::NewSource { - context: ByteStream::default(), - global_this: Some(bun_ptr::BackRef::new(global_this)), - ..Default::default() - }, - ); - - reader.context.setup(); - reader.context.apply_drain_result(drain_result); + // The JS wrapper made by `to_readable_stream()` below owns the source + // from here (freed by the GC finalizer). + let reader = + webcore::readable_stream::NewSource::new(ByteStream::new(drain_result), global_this); // reshaped for borrowck — re-borrow locked after the early *self = Null path above. let Value::Locked(locked) = self else { @@ -1506,10 +1418,9 @@ impl Value { reader.producer.set(locked.producer); - let context_ptr: *mut ByteStream = &raw mut reader.context; locked.readable = webcore::readable_stream::Strong::init( ReadableStream { - ptr: webcore::readable_stream::Source::Bytes(context_ptr), + ptr: webcore::readable_stream::Source::Bytes(reader.as_context_ptr()), value: reader.to_readable_stream(global_this)?, }, global_this, @@ -1562,9 +1473,8 @@ impl Value { return Ok(Value::Blob(b.dupe_with_content_type(false))); } - if let Value::WTFStringImpl(s) = *self { - wtf_impl(&s).r#ref(); - return Ok(Value::WTFStringImpl(s)); + if let Value::WTFStringImpl(s) = self { + return Ok(Value::WTFStringImpl(s.clone())); } if matches!(self, Value::Null) { @@ -1587,12 +1497,12 @@ impl Value { // ──────────────────────────────────────────────────────────────────────────── // https://github.com/WebKit/webkit/blob/main/Source/WebCore/Modules/fetch/FetchBody.cpp#L45 -pub(crate) fn extract(global_this: &JSGlobalObject, value: JSValue) -> JsResult { +pub(crate) fn extract(global_this: &JSGlobalObject, value: JSValue) -> JsResult { let body_value = Value::from_js(global_this, value)?; if let Value::Blob(b) = &body_value { debug_assert!(!b.is_heap_allocated()); // owned by Body } - Ok(Body::new(body_value)) + Ok(body_value) } // ──────────────────────────────────────────────────────────────────────────── @@ -1600,19 +1510,13 @@ pub(crate) fn extract(global_this: &JSGlobalObject, value: JSValue) -> JsResult< // ──────────────────────────────────────────────────────────────────────────── /// Mixin trait with provided methods. -/// Implementers supply `get_body_value`, `get_fetch_headers`, `get_form_data_encoding`, -/// and optionally override `get_body_readable_stream`. +/// Implementers supply `body`, `get_fetch_headers`, `get_form_data_encoding`. /// -/// R-2 (host-fn re-entrancy): every JS-exposed method takes `&self`. The -/// codegen shim still emits `this: &mut T` — `&mut T` -/// auto-derefs to `&T` so the impls below compile against either. +/// R-2 (host-fn re-entrancy): every JS-exposed method takes `&self` and +/// reaches the body's `Value` in closure-scoped borrows of its `JsCell`. pub(crate) trait BodyMixin: BodyOwnerJs + Sized { - /// R-2 interior-mutability boundary: implementors project `&mut Value` - /// from `&self` via `JsCell` (Response) or a raw `NonNull` deref (Request); - /// see [`Body::value_mut`]. Single-JS-thread invariant — keep the borrow - /// short and do not hold it across a call that re-enters JS. - #[allow(clippy::mut_from_ref)] - fn get_body_value(&self) -> &mut Value; + /// The owner's `Body` (inline in `Response`, pooled for `Request`). + fn body(&self) -> &Body; /// `FetchHeaders` is an /// opaque, intrusively-refcounted C++ handle whose accessors take `&mut self` /// (FFI signature is `*mut`). Returning `NonNull` instead of `&FetchHeaders` @@ -1620,6 +1524,12 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { fn get_fetch_headers(&self) -> Option>; fn get_form_data_encoding(&self) -> JsResult>>; + /// The body's `Value` slot. + #[inline] + fn body_value(&self) -> &JsCell { + &self.body().value + } + // ──────────────────────────────────────────────────────────────────── // Twin methods (identical for Request/Response). These were previously // open-coded in both files against `js_gen::*` / `js::*` directly; the @@ -1630,13 +1540,20 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { /// JS-side `js.gc.stream` cache is the /// source of truth; fall back to the native `Locked.readable` slot. fn get_body_readable_stream(&self) -> Option { + self.readable_stream_of(self.body_value().get()) + } + + /// [`get_body_readable_stream`](Self::get_body_readable_stream) against an + /// already-borrowed `value`. + #[inline(always)] + fn readable_stream_of(&self, value: &Value) -> Option { if let Some(js_ref) = self.js_ref() { if let Some(stream) = Self::stream_get_cached(js_ref) { // JS is always source of truth for the stream return ReadableStream::from_js_direct(stream); } } - if let Value::Locked(locked) = self.get_body_value() { + if let Value::Locked(locked) = value { return locked.readable.get(); } None @@ -1648,10 +1565,12 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { if let Some(js_ref) = self.js_ref() { Self::stream_set_cached(js_ref, global_object, JSValue::ZERO); } - if let Value::Locked(locked) = self.get_body_value() { - // `mem::take` swaps in `Default` and drops the old value. - let _ = core::mem::take(&mut locked.readable); - } + self.body_value().with_mut(|value| { + if let Value::Locked(locked) = value { + // `mem::take` swaps in `Default` and drops the old value. + let _ = core::mem::take(&mut locked.readable); + } + }); } /// Migrate any `Locked.readable` strong ref @@ -1659,13 +1578,15 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { /// wrapper owns the stream; native side must not hold it strongly). fn check_body_stream_ref(&self, global_object: &JSGlobalObject) { if let Some(js_value) = self.js_ref() { - if let Value::Locked(locked) = self.get_body_value() { - if let Some(stream) = locked.readable.get() { - stream.value.ensure_still_alive(); - Self::stream_set_cached(js_value, global_object, stream.value); - locked.readable.downgrade(global_object); + self.body_value().with_mut(|value| { + if let Value::Locked(locked) = value { + if let Some(stream) = locked.readable.get() { + stream.value.ensure_still_alive(); + Self::stream_set_cached(js_value, global_object, stream.value); + locked.readable.downgrade(global_object); + } } - } + }); } } @@ -1684,7 +1605,7 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { Self::body_set_cached(js_wrapper, global_this, cloned_stream); } } - if let Value::Locked(locked) = self.get_body_value() { + if let Value::Locked(locked) = self.body_value().get() { if let Some(readable) = locked.readable.get() { Self::body_set_cached(this_value, global_this, readable.value); } @@ -1696,21 +1617,19 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { /// JS-side cached stream when present, then repoint this owner's /// `body`/`stream` cache slots at the fresh branch in `locked.readable`. fn clone_body_value_via_cached_stream(&self, global_this: &JSGlobalObject) -> JsResult { - let cloned = 'brk: { + let cloned = self.body_value().with_mut(|value| { if let Some(js_ref) = self.js_ref() { if let Some(stream) = Self::stream_get_cached(js_ref) { let mut readable = ReadableStream::from_js_direct(stream); if let Some(r) = readable.as_mut() { - break 'brk self - .get_body_value() - .clone_with_readable_stream(global_this, Some(r))?; + return value.clone_with_readable_stream(global_this, Some(r)); } } } - self.get_body_value().clone(global_this)? - }; + value.clone(global_this) + })?; if let Some(js_ref) = self.js_ref() { - if let Value::Locked(locked) = self.get_body_value() { + if let Value::Locked(locked) = self.body_value().get() { if let Some(readable) = locked.readable.get() { Self::body_set_cached(js_ref, global_this, readable.value); } @@ -1721,54 +1640,40 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { } fn get_text(&self, global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let value = self.get_body_value(); - if matches!(value, Value::Used) { - return Ok(handle_body_already_used(global_object)); - } - if let Some(rejected) = handle_body_error(value, global_object) { - return Ok(rejected); - } - - if matches!(value, Value::Locked(_)) { - if let Some(readable) = self.get_body_readable_stream() { - if readable.is_disturbed(global_object) { - return Ok(handle_body_already_used(global_object)); - } - let value = self.get_body_value(); - if let Value::Locked(locked) = value { - return locked.set_promise(global_object, Action::GetText, Some(readable)); - } - } - let value = self.get_body_value(); - if let Value::Locked(locked) = value { - if !locked.action.is_none() - || locked.is_disturbed::(global_object, callframe.this()) + self.body_value() + .with_mut(|value| { + if let Some(early) = + self.pending_body_read(value, global_object, callframe.this(), false, || { + Action::GetText + })? { - return Ok(handle_body_already_used(global_object)); + return Ok(early); } - return locked.set_promise(global_object, Action::GetText, None); - } - } - - let value = self.get_body_value(); - let mut blob = value.use_as_any_blob_allow_non_utf8_string(); - let result = JSPromise::wrap(global_object, |g| blob.to_string(g, Lifetime::Transfer)); - blob.detach(); - result + let mut blob = value.use_as_any_blob_allow_non_utf8_string(); + let result = + JSPromise::wrap(global_object, |g| blob.to_string(g, Lifetime::Transfer)); + blob.detach(); + result.map(BodyRead::Settled) + })? + .finish(global_object) } fn get_body(&self, global_this: &JSGlobalObject) -> JsResult { - let body = self.get_body_value(); - - if matches!(body, Value::Used) { - return ReadableStream::used(global_this); - } - if matches!(body, Value::Locked(_)) { - if let Some(readable) = self.get_body_readable_stream() { - return Ok(readable.value); + let stream = self.body_value().with_mut(|body| { + if matches!(body, Value::Used) { + return ReadableStream::used(global_this).map(Err); } - } - let stream = self.get_body_value().to_readable_stream(global_this)?; + if matches!(body, Value::Locked(_)) { + if let Some(readable) = self.readable_stream_of(body) { + return Ok(Err(readable.value)); + } + } + body.to_readable_stream(global_this).map(Ok) + })?; + let stream = match stream { + Ok(created) => created, + Err(existing) => return Ok(existing), + }; // The wrapper's traced `m_stream` slot owns the stream from here; // release the `Strong` `to_readable_stream` parked in `Locked.readable`. self.check_body_stream_ref(global_this); @@ -1787,18 +1692,20 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { // A `Locked` body whose stream is already materialized (user-provided // ReadableStream, or `.body` was accessed first) is decoded via a reader // on that existing stream. - if matches!(self.get_body_value(), Value::Locked(_)) { + if matches!(self.body_value().get(), Value::Locked(_)) { if let Some(readable) = self.get_body_readable_stream() { let text = ReadableStream::text_decode_from(global_this, readable.value)?; self.detach_readable_stream(global_this); - *self.get_body_value() = Value::Used; + self.body_value().set(Value::Used); return Ok(text); } } // Step 2: null body → a new empty closed ReadableStream. // Steps 3-6: decode directly from the body's backing bytes. - let stream = self.get_body_value().to_text_readable_stream(global_this)?; + let stream = self + .body_value() + .with_mut(|value| value.to_text_readable_stream(global_this))?; if stream.is_null() { return ReadableStream::empty(global_this); } @@ -1813,19 +1720,15 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { global_object: &JSGlobalObject, check: fn(&ReadableStream, &JSGlobalObject) -> bool, ) -> bool { - // reshaped for borrowck — `get_body_readable_stream` needs `&self`, - // so we can't hold a `match` borrow on `get_body_value()` across it. - match self.get_body_value() { + match self.body_value().get() { Value::Used => true, Value::Locked(pending) if !pending.action.is_none() => true, - Value::Locked(_) => 'brk: { - if let Some(readable) = self.get_body_readable_stream() { - break 'brk check(&readable, global_object); + value @ Value::Locked(pending) => { + if let Some(readable) = self.readable_stream_of(value) { + return check(&readable, global_object); } - if let Value::Locked(pending) = self.get_body_value() { - if let Some(stream) = pending.readable.get() { - break 'brk check(&stream, global_object); - } + if let Some(stream) = pending.readable.get() { + return check(&stream, global_object); } false } @@ -1854,48 +1757,81 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { Ok(()) } - fn get_json(&self, global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { - let value = self.get_body_value(); + /// Front half of `text()` / `json()` / `arrayBuffer()` / `bytes()`, + /// run inside the body borrow: reject a used or failed body, or commit a + /// still-streaming one to `action` (its script half comes back as + /// [`BodyRead::Locked`]). `None`: the body is buffered — read it out. + #[inline(always)] + fn pending_body_read( + &self, + value: &mut Value, + global_object: &JSGlobalObject, + this_value: JSValue, + to_blob_if_possible: bool, + action: fn() -> Action, + ) -> JsResult> { if matches!(value, Value::Used) { - return Ok(handle_body_already_used(global_object)); + return Ok(Some(BodyRead::Settled(handle_body_already_used( + global_object, + )))); } if let Some(rejected) = handle_body_error(value, global_object) { - return Ok(rejected); + return Ok(Some(BodyRead::Settled(rejected))); } if matches!(value, Value::Locked(_)) { - if let Some(readable) = self.get_body_readable_stream() { + if let Some(readable) = self.readable_stream_of(value) { if readable.is_disturbed(global_object) { - return Ok(handle_body_already_used(global_object)); + return Ok(Some(BodyRead::Settled(handle_body_already_used( + global_object, + )))); + } + if to_blob_if_possible { + value.to_blob_if_possible(); } - let value = self.get_body_value(); - value.to_blob_if_possible(); if let Value::Locked(locked) = value { - return locked.set_promise(global_object, Action::GetJSON, Some(readable)); + return locked + .set_promise(global_object, action(), Some(readable)) + .map(|r| Some(BodyRead::Locked(r))); } } - let value = self.get_body_value(); if let Value::Locked(locked) = value { if !locked.action.is_none() - || locked.is_disturbed::(global_object, callframe.this()) + || locked.is_disturbed::(global_object, this_value) { - return Ok(handle_body_already_used(global_object)); + return Ok(Some(BodyRead::Settled(handle_body_already_used( + global_object, + )))); + } + if to_blob_if_possible { + value.to_blob_if_possible(); } - // reshaped for borrowck - let _ = locked; - let value = self.get_body_value(); - value.to_blob_if_possible(); if let Value::Locked(locked) = value { - return locked.set_promise(global_object, Action::GetJSON, None); + return locked + .set_promise(global_object, action(), None) + .map(|r| Some(BodyRead::Locked(r))); } } } + Ok(None) + } - let value = self.get_body_value(); - let mut blob = value.use_as_any_blob_allow_non_utf8_string(); - let result = JSPromise::wrap(global_object, |g| blob.to_json(g, Lifetime::Share)); - blob.detach(); - result + fn get_json(&self, global_object: &JSGlobalObject, callframe: &CallFrame) -> JsResult { + self.body_value() + .with_mut(|value| { + if let Some(early) = + self.pending_body_read(value, global_object, callframe.this(), true, || { + Action::GetJSON + })? + { + return Ok(early); + } + let mut blob = value.use_as_any_blob_allow_non_utf8_string(); + let result = JSPromise::wrap(global_object, |g| blob.to_json(g, Lifetime::Share)); + blob.detach(); + result.map(BodyRead::Settled) + })? + .finish(global_object) } fn get_array_buffer( @@ -1904,54 +1840,24 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { callframe: &CallFrame, ) -> JsResult { bun_core::scoped_log!(BodyMixin, "getArrayBuffer"); - let value = self.get_body_value(); - - if matches!(value, Value::Used) { - return Ok(handle_body_already_used(global_object)); - } - if let Some(rejected) = handle_body_error(value, global_object) { - return Ok(rejected); - } - - if matches!(value, Value::Locked(_)) { - if let Some(readable) = self.get_body_readable_stream() { - if readable.is_disturbed(global_object) { - return Ok(handle_body_already_used(global_object)); - } - let value = self.get_body_value(); - value.to_blob_if_possible(); - if let Value::Locked(locked) = value { - return locked.set_promise( - global_object, - Action::GetArrayBuffer, - Some(readable), - ); - } - } - let value = self.get_body_value(); - if let Value::Locked(locked) = value { - if !locked.action.is_none() - || locked.is_disturbed::(global_object, callframe.this()) + self.body_value() + .with_mut(|value| { + if let Some(early) = + self.pending_body_read(value, global_object, callframe.this(), true, || { + Action::GetArrayBuffer + })? { - return Ok(handle_body_already_used(global_object)); + return Ok(early); } - let _ = locked; - let value = self.get_body_value(); - value.to_blob_if_possible(); - if let Value::Locked(locked) = value { - return locked.set_promise(global_object, Action::GetArrayBuffer, None); - } - } - } - - // toArrayBuffer in AnyBlob checks for non-UTF8 strings - let value = self.get_body_value(); - let mut blob: AnyBlob = value.use_as_any_blob_allow_non_utf8_string(); - let result = JSPromise::wrap(global_object, |g| { - blob.to_array_buffer(g, Lifetime::Transfer) - }); - blob.detach(); - result + // toArrayBuffer in AnyBlob checks for non-UTF8 strings + let mut blob: AnyBlob = value.use_as_any_blob_allow_non_utf8_string(); + let result = JSPromise::wrap(global_object, |g| { + blob.to_array_buffer(g, Lifetime::Transfer) + }); + blob.detach(); + result.map(BodyRead::Settled) + })? + .finish(global_object) } fn get_bytes( @@ -1959,50 +1865,24 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { global_object: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let value = self.get_body_value(); - - if matches!(value, Value::Used) { - return Ok(handle_body_already_used(global_object)); - } - if let Some(rejected) = handle_body_error(value, global_object) { - return Ok(rejected); - } - - if matches!(value, Value::Locked(_)) { - if let Some(readable) = self.get_body_readable_stream() { - if readable.is_disturbed(global_object) { - return Ok(handle_body_already_used(global_object)); - } - let value = self.get_body_value(); - value.to_blob_if_possible(); - if let Value::Locked(locked) = value { - return locked.set_promise(global_object, Action::GetBytes, Some(readable)); - } - } - let value = self.get_body_value(); - if let Value::Locked(locked) = value { - if !locked.action.is_none() - || locked.is_disturbed::(global_object, callframe.this()) + self.body_value() + .with_mut(|value| { + if let Some(early) = + self.pending_body_read(value, global_object, callframe.this(), true, || { + Action::GetBytes + })? { - return Ok(handle_body_already_used(global_object)); + return Ok(early); } - let _ = locked; - let value = self.get_body_value(); - value.to_blob_if_possible(); - if let Value::Locked(locked) = value { - return locked.set_promise(global_object, Action::GetBytes, None); - } - } - } - - // toArrayBuffer in AnyBlob checks for non-UTF8 strings - let value = self.get_body_value(); - let mut blob: AnyBlob = value.use_as_any_blob_allow_non_utf8_string(); - let result = JSPromise::wrap(global_object, |g| { - blob.to_uint8_array(g, Lifetime::Transfer) - }); - blob.detach(); - result + // toArrayBuffer in AnyBlob checks for non-UTF8 strings + let mut blob: AnyBlob = value.use_as_any_blob_allow_non_utf8_string(); + let result = JSPromise::wrap(global_object, |g| { + blob.to_uint8_array(g, Lifetime::Transfer) + }); + blob.detach(); + result.map(BodyRead::Settled) + })? + .finish(global_object) } fn get_form_data( @@ -2010,35 +1890,34 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { global_object: &JSGlobalObject, callframe: &CallFrame, ) -> JsResult { - let value = self.get_body_value(); - - if matches!(value, Value::Used) { - return Ok(handle_body_already_used(global_object)); - } - if let Some(rejected) = handle_body_error(value, global_object) { - return Ok(rejected); - } + if let Some(settled) = self.body_value().with_mut(|value| { + if matches!(value, Value::Used) { + return Some(handle_body_already_used(global_object)); + } + if let Some(rejected) = handle_body_error(value, global_object) { + return Some(rejected); + } - if matches!(value, Value::Locked(_)) { - if let Some(readable) = self.get_body_readable_stream() { - if readable.is_disturbed(global_object) { - return Ok(handle_body_already_used(global_object)); + if matches!(value, Value::Locked(_)) { + if let Some(readable) = self.readable_stream_of(value) { + if readable.is_disturbed(global_object) { + return Some(handle_body_already_used(global_object)); + } + value.to_blob_if_possible(); + let _ = readable; // not consumed in this branch } - let value = self.get_body_value(); - value.to_blob_if_possible(); - let _ = readable; // not consumed in this branch - } - let value = self.get_body_value(); - if let Value::Locked(locked) = value { - if !locked.action.is_none() - || locked.is_disturbed::(global_object, callframe.this()) - { - return Ok(handle_body_already_used(global_object)); + if let Value::Locked(locked) = value { + if !locked.action.is_none() + || locked.is_disturbed::(global_object, callframe.this()) + { + return Some(handle_body_already_used(global_object)); + } + value.to_blob_if_possible(); } - let _ = locked; - let value = self.get_body_value(); - value.to_blob_if_possible(); } + None + }) { + return Ok(settled); } let Some(encoder) = self.get_form_data_encoding()? else { @@ -2053,24 +1932,25 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { .reject()); }; - let value = self.get_body_value(); - if let Value::Locked(_locked) = value { - let owned_readable = self.get_body_readable_stream(); - // reshaped for borrowck — re-borrow after self method call. - let value = self.get_body_value(); - let Value::Locked(locked) = value else { - unreachable!() - }; - return locked.set_promise( - global_object, - Action::GetFormData(Some(encoder)), - owned_readable, - ); - } - - let mut blob: AnyBlob = value.use_as_any_blob(); - // `encoder.encoding` is `bun_core::form_data::Encoding`; convert - // to the `webcore::form_data::Encoding` shape FormData::to_js expects. + let (mut blob, encoder) = match self.body_value().with_mut(|value| { + if let Value::Locked(_) = value { + let owned_readable = self.readable_stream_of(value); + let Value::Locked(locked) = value else { + unreachable!() + }; + return locked + .set_promise( + global_object, + Action::GetFormData(Some(encoder)), + owned_readable, + ) + .map(Ok); + } + Ok(Err((value.use_as_any_blob(), encoder))) + })? { + Ok(read) => return read.run(global_object), + Err(buffered) => buffered, + }; let encoding = match encoder.encoding { bun_core::form_data::Encoding::URLEncoded => webcore::form_data::Encoding::URLEncoded, bun_core::form_data::Encoding::Multipart(b) => { @@ -2106,72 +1986,77 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { global_object: &JSGlobalObject, this_value: JSValue, ) -> JsResult { - let value = self.get_body_value(); - - if matches!(value, Value::Used) { - return Ok(handle_body_already_used(global_object)); - } - if let Some(rejected) = handle_body_error(value, global_object) { - return Ok(rejected); - } - - if matches!(value, Value::Locked(_)) { - if let Some(readable) = self.get_body_readable_stream() { - let value = self.get_body_value(); - let Value::Locked(locked) = value else { - unreachable!() - }; - if !locked.action.is_none() - || ((!this_value.is_empty() && readable.is_disturbed(global_object)) - || (this_value.is_empty() && readable.is_disturbed(global_object))) - { - return Ok(handle_body_already_used(global_object)); - } - value.to_blob_if_possible(); - if let Value::Locked(locked) = value { - return locked.set_promise(global_object, Action::GetBlob, Some(readable)); + self.body_value() + .with_mut(|value| { + if matches!(value, Value::Used) { + return Ok(BodyRead::Settled(handle_body_already_used(global_object))); } - } - let value = self.get_body_value(); - if let Value::Locked(locked) = value { - if !locked.action.is_none() - || ((!this_value.is_empty() - && locked.is_disturbed::(global_object, this_value)) - || (this_value.is_empty() && locked.readable.is_disturbed(global_object))) - { - return Ok(handle_body_already_used(global_object)); + if let Some(rejected) = handle_body_error(value, global_object) { + return Ok(BodyRead::Settled(rejected)); } - let _ = locked; - let value = self.get_body_value(); - value.to_blob_if_possible(); - if let Value::Locked(locked) = value { - return locked.set_promise(global_object, Action::GetBlob, None); + + if matches!(value, Value::Locked(_)) { + if let Some(readable) = self.readable_stream_of(value) { + let Value::Locked(locked) = &*value else { + unreachable!() + }; + if !locked.action.is_none() + || ((!this_value.is_empty() && readable.is_disturbed(global_object)) + || (this_value.is_empty() && readable.is_disturbed(global_object))) + { + return Ok(BodyRead::Settled(handle_body_already_used(global_object))); + } + value.to_blob_if_possible(); + if let Value::Locked(locked) = value { + return locked + .set_promise(global_object, Action::GetBlob, Some(readable)) + .map(BodyRead::Locked); + } + } + if let Value::Locked(locked) = &*value { + if !locked.action.is_none() + || ((!this_value.is_empty() + && locked.is_disturbed::(global_object, this_value)) + || (this_value.is_empty() + && locked.readable.is_disturbed(global_object))) + { + return Ok(BodyRead::Settled(handle_body_already_used(global_object))); + } + value.to_blob_if_possible(); + if let Value::Locked(locked) = value { + return locked + .set_promise(global_object, Action::GetBlob, None) + .map(BodyRead::Locked); + } + } } - } - } - let value = self.get_body_value(); - let blob_owned = value.use_(); - let blob = &blob_owned; - if blob.content_type().is_empty() { - if let Some(fetch_headers) = BodyMixin::get_fetch_headers(self) { - // `fetch_headers` is a live C++ FetchHeaders handle; - // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. - let fetch_headers = bun_opaque::opaque_deref_mut(fetch_headers.as_ptr()); - if let Some(content_type) = fetch_headers.fast_get(HTTPHeaderName::ContentType) { - let content_slice = content_type.to_utf8(); - let mime_type = MimeType::init(content_slice.slice(), true, None); - set_blob_content_type(blob, mime_type); + let blob_owned = value.use_(); + let blob = &blob_owned; + if blob.content_type().is_empty() { + if let Some(fetch_headers) = BodyMixin::get_fetch_headers(self) { + // `fetch_headers` is a live C++ FetchHeaders handle; + // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. + let fetch_headers = bun_opaque::opaque_deref_mut(fetch_headers.as_ptr()); + if let Some(content_type) = + fetch_headers.fast_get(HTTPHeaderName::ContentType) + { + let content_slice = content_type.to_utf8(); + let mime_type = MimeType::init(content_slice.slice(), true, None); + set_blob_content_type(blob, mime_type); + // content_slice dropped (replaces defer content_slice.deinit()) + } + } + if !blob.content_type_was_set.get() && blob.store.get().is_some() { + set_blob_content_type(blob, bun_http_types::MimeType::TEXT); + } } - } - if !blob.content_type_was_set.get() && blob.store.get().is_some() { - set_blob_content_type(blob, bun_http_types::MimeType::TEXT); - } - } - Ok(JSPromise::resolved_promise_value( - global_object, - blob_owned.to_js(global_object), - )) + Ok(BodyRead::Settled(JSPromise::resolved_promise_value( + global_object, + blob_owned.to_js(global_object), + ))) + })? + .finish(global_object) } fn get_blob_without_call_frame(&self, global_object: &JSGlobalObject) -> JsResult { @@ -2179,6 +2064,24 @@ pub(crate) trait BodyMixin: BodyOwnerJs + Sized { } } +/// Outcome of the borrow-scoped half of a `BodyMixin` read. +pub(crate) enum BodyRead { + /// Settled inside the borrow (used / errored / disturbed / buffered): the promise. + Settled(JSValue), + /// Still streaming: run this once the borrow is released. + Locked(LockedRead), +} + +impl BodyRead { + #[inline(always)] + fn finish(self, global_object: &JSGlobalObject) -> JsResult { + match self { + BodyRead::Settled(value) => Ok(value), + BodyRead::Locked(read) => read.run(global_object), + } + } +} + fn handle_body_already_used(global_object: &JSGlobalObject) -> JSValue { global_object .err( diff --git a/src/runtime/webcore/ByteBlobLoader.rs b/src/runtime/webcore/ByteBlobLoader.rs index 259fbd00423c..e56b9bb57235 100644 --- a/src/runtime/webcore/ByteBlobLoader.rs +++ b/src/runtime/webcore/ByteBlobLoader.rs @@ -1,35 +1,40 @@ +use core::cell::Cell; + use bun_collections::VecExt; -use bun_jsc::{JSGlobalObject, JSValue, JsResult}; -use bun_ptr::RefPtr; +use bun_jsc::{JSGlobalObject, JSValue, JsCell, JsResult}; -use crate::webcore::blob::store::StoreExt as _; +use crate::webcore::blob::store::{BytesExt as _, Data}; use crate::webcore::blob::{self, Blob, BlobExt as _, Store}; use crate::webcore::readable_stream; use crate::webcore::streams; +use bun_ptr::RefPtr; +// R-2: every method reachable from JS or a producer takes `&self`; mutable +// state lives in `Cell`/`JsCell`. Both are `#[repr(transparent)]`, so the +// embedded layout (offset 0 of `NewSource`) is unchanged. pub struct ByteBlobLoader { - pub offset: blob::SizeType, + pub offset: Cell, // LIFETIMES.tsv: SHARED — ref() on setup, deref() in clearData - pub(crate) store: Option>, + pub(crate) store: JsCell>>, pub(crate) chunk_size: blob::SizeType, - pub(crate) remain: blob::SizeType, - pub(crate) done: bool, + pub(crate) remain: Cell, + pub(crate) done: Cell, /// https://github.com/oven-sh/bun/issues/14988 /// Necessary for converting a ByteBlobLoader from a Blob -> back into a Blob /// Especially for DOMFormData, where the specific content-type might've been serialized into the data. - pub(crate) content_type: blob::BlobContentType, + pub(crate) content_type: JsCell, } impl Default for ByteBlobLoader { fn default() -> Self { Self { - offset: 0, - store: None, + offset: Cell::new(0), + store: JsCell::new(None), chunk_size: 1024 * 1024 * 2, - remain: 1024 * 1024 * 2, - done: false, - content_type: blob::BlobContentType::default(), + remain: Cell::new(1024 * 1024 * 2), + done: Cell::new(false), + content_type: JsCell::new(blob::BlobContentType::default()), } } } @@ -43,26 +48,26 @@ impl readable_stream::SourceContext for ByteBlobLoader { const SUPPORTS_REF: bool = false; crate::source_context_codegen!(js_BlobInternalReadableStreamSource); - fn on_start(&mut self) -> streams::Start { + fn on_start(&self) -> streams::Start { Self::on_start(self) } - fn on_pull(&mut self, buf: &mut [u8], view: JSValue) -> streams::Result { + fn on_pull(&self, buf: &mut [u8], view: JSValue) -> streams::Result { Self::on_pull(self, buf, view) } - fn on_cancel(&mut self) { + fn on_cancel(&self) { Self::on_cancel(self); } - fn deinit_fn(&mut self) { + fn deinit_fn(&self) { Self::deinit(self) } - fn drain_internal_buffer(&mut self) -> Vec { + fn drain_internal_buffer(&self) -> Vec { Self::drain(self) } fn memory_cost_fn(&self) -> usize { Self::memory_cost(self) } fn to_buffered_value( - &mut self, + &self, global: &JSGlobalObject, action: streams::BufferActionTag, ) -> Option> { @@ -70,11 +75,11 @@ impl readable_stream::SourceContext for ByteBlobLoader { } } -bun_core::impl_field_parent! { ByteBlobLoader => Source.context; fn parent; } +// Every `ByteBlobLoader` is the `context` field of a heap-allocated `Source`. +bun_core::impl_field_parent! { ByteBlobLoader => Source.context; pub fn shared parent; } impl ByteBlobLoader { - pub(crate) fn setup(&mut self, blob: &Blob, user_chunk_size: blob::SizeType) { - // In-place init — `self` is a pre-allocated slot inside `Source`. + pub(crate) fn new(blob: &Blob, user_chunk_size: blob::SizeType) -> Self { let store = blob.store.get().as_ref().unwrap().clone(); // `Blob` is not `Clone`, so use the non-mutating `resolved_size()` helper. let (offset, size) = blob.resolved_size(); @@ -83,54 +88,55 @@ impl ByteBlobLoader { } else { blob::BlobContentType::default() }; - *self = ByteBlobLoader { - offset, - store: Some(store), + ByteBlobLoader { + offset: Cell::new(offset), + store: JsCell::new(Some(store)), chunk_size: (if user_chunk_size > 0 { user_chunk_size.min(size) } else { size }) .min(1024 * 1024 * 2), - remain: size, - done: false, - content_type, - }; + remain: Cell::new(size), + done: Cell::new(false), + content_type: JsCell::new(content_type), + } } - pub(crate) fn on_start(&mut self) -> streams::Start { - // `streams::BlobSizeType` and `blob::SizeType` are both u64 in the Rust port. + pub(crate) fn on_start(&self) -> streams::Start { streams::Start::ChunkSize(self.chunk_size) } - pub(crate) fn on_pull(&mut self, buffer: &mut [u8], array: JSValue) -> streams::Result { + pub(crate) fn on_pull(&self, buffer: &mut [u8], array: JSValue) -> streams::Result { array.ensure_still_alive(); let _keep = bun_jsc::EnsureStillAlive(array); - let Some(store) = self.store.clone() else { + let Some(store) = self.store.get().clone() else { return streams::Result::Done; }; - if self.done { + if self.done.get() { return streams::Result::Done; } let temporary = store.shared_view(); - let temporary = &temporary[(self.offset as usize).min(temporary.len())..]; + let temporary = &temporary[(self.offset.get() as usize).min(temporary.len())..]; - let take = buffer.len().min(temporary.len().min(self.remain as usize)); + let take = buffer + .len() + .min(temporary.len().min(self.remain.get() as usize)); let temporary = &temporary[..take]; if temporary.is_empty() { self.clear_data(); - self.done = true; + self.done.set(true); return streams::Result::Done; } let copied = blob::SizeType::try_from(temporary.len()).expect("int cast"); - self.remain = self.remain.saturating_sub(copied); - self.offset = self.offset.saturating_add(copied); + self.remain.set(self.remain.get().saturating_sub(copied)); + self.offset.set(self.offset.get().saturating_add(copied)); debug_assert!(buffer.as_ptr() != temporary.as_ptr()); buffer[..temporary.len()].copy_from_slice(temporary); - if self.remain == 0 { + if self.remain.get() == 0 { return streams::Result::IntoArrayAndDone(streams::IntoArray { value: array, len: copied, @@ -143,26 +149,32 @@ impl ByteBlobLoader { }) } - pub(crate) fn to_any_blob(&mut self, global: &JSGlobalObject) -> Option { + pub(crate) fn to_any_blob(&self, global: &JSGlobalObject) -> Option { // Take ownership via detach_store() up front. let store = self.detach_store()?; - if self.offset == 0 && self.remain == store.size() && self.content_type.is_empty() { - // SAFETY: `RefPtr` deref is `&Store`; `to_any_blob` needs `&mut` to move bytes out. - // We hold the only outstanding ref (just detached) so exclusive access is sound. - if let Some(blob) = unsafe { (*store.as_ptr()).to_any_blob() } { - drop(store); - return Some(blob); + if self.offset.get() == 0 + && self.remain.get() == store.size() + && self.content_type.get().is_empty() + { + // We hold the only outstanding ref (just detached), so the store's + // bytes can move out. + if store.has_one_ref() { + if let Data::Bytes(bytes) = Store::data_mut(&store) { + let blob = blob::Any::InternalBlob(bytes.to_internal_blob()); + drop(store); + return Some(blob); + } } } let blob = Blob::init_with_store(store, global); - blob.offset.set(self.offset); - blob.size.set(self.remain); + blob.offset.set(self.offset.get()); + blob.size.set(self.remain.get()); // Make sure to preserve the content-type. // https://github.com/oven-sh/bun/issues/14988 - if !self.content_type.is_empty() { - let ct = core::mem::take(&mut self.content_type); + if !self.content_type.get().is_empty() { + let ct = self.content_type.take(); blob.content_type_was_set.set(!ct.is_empty()); blob.content_type.set(ct); } @@ -171,53 +183,57 @@ impl ByteBlobLoader { Some(blob::Any::Blob(blob)) } - pub(crate) fn detach_store(&mut self) -> Option> { + pub(crate) fn detach_store(&self) -> Option> { if let Some(store) = self.store.take() { - self.done = true; + self.done.set(true); return Some(store); } None } - pub(crate) fn on_cancel(&mut self) { + pub(crate) fn on_cancel(&self) { self.clear_data(); } - // Kept as inherent method (not `Drop`) — invoked via `SourceContext::deinit_fn`. - // Only side-effect teardown lives here; the enclosing `Box` is freed by - // the caller (`NewSource::decrement_count`) *after* this returns. Freeing the - // parent here would deallocate the storage backing `&mut self` (dangling UAF). - pub(crate) fn deinit(&mut self) { + // Kept as inherent method (not `Drop`) — invoked via `SourceContext::deinit_fn` + // from the enclosing `Source`'s `Drop`, before the owned fields drop. + pub(crate) fn deinit(&self) { self.clear_data(); } - fn clear_data(&mut self) { - self.content_type = blob::BlobContentType::default(); - - if let Some(store) = self.store.take() { - drop(store); // store.deref() - } + fn clear_data(&self) { + self.content_type.set(blob::BlobContentType::default()); + // RefPtr::Drop derefs. + self.store.set(None); } - pub(crate) fn drain(&mut self) -> Vec { - let Some(store) = self.store.clone() else { + pub(crate) fn drain(&self) -> Vec { + let Some(store) = self.store.get().clone() else { return Vec::new(); }; let temporary = store.shared_view(); - let temporary = &temporary[self.offset as usize..]; - let take = 16384usize.min(temporary.len().min(self.remain as usize)); + let temporary = &temporary[self.offset.get() as usize..]; + let take = 16384usize.min(temporary.len().min(self.remain.get() as usize)); let temporary = &temporary[..take]; // A single owning copy (avoids a `ManuallyDrop` borrow dance). let cloned = Vec::::from_slice(temporary); - self.offset = self.offset.saturating_add(cloned.len() as blob::SizeType); - self.remain = self.remain.saturating_sub(cloned.len() as blob::SizeType); + self.offset.set( + self.offset + .get() + .saturating_add(cloned.len() as blob::SizeType), + ); + self.remain.set( + self.remain + .get() + .saturating_sub(cloned.len() as blob::SizeType), + ); cloned } pub(crate) fn to_buffered_value( - &mut self, + &self, global: &JSGlobalObject, action: streams::BufferActionTag, ) -> JsResult { @@ -238,7 +254,7 @@ impl ByteBlobLoader { pub(crate) fn memory_cost(&self) -> usize { // ReadableStreamSource covers @sizeOf(FileReader) - if let Some(store) = &self.store { + if let Some(store) = self.store.get() { return store.memory_cost(); } 0 diff --git a/src/runtime/webcore/ByteStream.rs b/src/runtime/webcore/ByteStream.rs index baf415acf24d..0ecf1e057c62 100644 --- a/src/runtime/webcore/ByteStream.rs +++ b/src/runtime/webcore/ByteStream.rs @@ -5,6 +5,7 @@ use bun_jsc::strong::Optional as StrongOptional; use bun_jsc::{self as jsc, JSGlobalObject, JSValue, JsCell}; use bun_sys::Error as SysError; +use crate::webcore::readable_stream::SourceRef; use crate::webcore::streams::{self, BufferAction, IntoArray}; use crate::webcore::{DrainResult, SinkHandle, blob, readable_stream}; @@ -17,18 +18,13 @@ bun_output::declare_scope!(ByteStream, visible); /// `size_hint` are written only at init time (before the JS wrapper exists) /// and stay bare. /// -/// The `SourceContext` trait still spells its callbacks `&mut self` (shared -/// across `ByteBlobLoader` / `FileReader`); the trait impl below auto-derefs -/// to the `&self` inherent bodies. pub struct ByteStream { pub(crate) buffer: JsCell>, pub(crate) has_received_last_chunk: Cell, pub(crate) pending: JsCell, pub(crate) done: Cell, - /// Borrowed view into a JS `Uint8Array` passed from `on_pull`; kept alive by `pending_value`. - // Raw fat slice ptr because the backing store is JS-heap-owned and rooted via - // `pending_value: Strong`. Never freed by Rust. - pub(crate) pending_buffer: Cell<*mut [u8]>, + /// Roots the JS `Uint8Array` passed from `on_pull`; `on_data` re-derives the + /// destination bytes from it. pub(crate) pending_value: JsCell, // jsc.Strong.Optional pub offset: Cell, pub(crate) high_water_mark: blob::SizeType, @@ -50,7 +46,6 @@ impl Default for ByteStream { ..Default::default() }), done: Cell::new(false), - pending_buffer: Cell::new(Self::empty_pending_buffer()), pending_value: JsCell::new(StrongOptional::empty()), offset: Cell::new(0), high_water_mark: 0, @@ -72,7 +67,7 @@ pub type Source = readable_stream::NewSource; /// can be collected (`SourceHandle::consumer_collected`). #[derive(Default)] pub struct ProducerHold { - source: Cell>>, + source: JsCell>>, parked: Cell, } @@ -88,18 +83,17 @@ pub enum AfterDelivery { } impl ProducerHold { - /// Take the producer ref on the stream's source (JS thread). - /// - /// # Safety - /// `bytes` is the live ByteStream of a stream the caller holds. - pub unsafe fn hold(&self, bytes: *mut ByteStream) { + /// Take the producer ref on `readable`'s source, if it is a byte stream (JS thread; the + /// caller holds the stream). + pub fn hold(&self, readable: &readable_stream::ReadableStream) { self.release(); - // SAFETY: fn contract; the ref keeps the Source alive past this call. - unsafe { - let source = Source::from_context_ptr(bytes); - (*source).increment_count(); - self.source.set(core::ptr::NonNull::new(source)); - } + self.source.set(SourceRef::byte_stream(readable)); + } + + /// [`hold`](Self::hold) for a source the caller has in hand. + pub fn hold_source(&self, source: &Source) { + self.release(); + self.source.set(Some(source.retain())); } pub fn is_held(&self) -> bool { @@ -109,22 +103,16 @@ impl ProducerHold { /// The held stream, pinned for the guard's life: a consumer inside `on_data` can cancel the /// producer (which drops the hold), and while parked the wrapper is not rooted. pub fn bytes(&self) -> Option { - let source = self.source.get()?; - // SAFETY: live through our ref; no borrow of the source exists yet. - unsafe { (*source.as_ptr()).increment_count() }; - Some(PinnedBytes(source)) + self.source.get().clone().map(PinnedBytes) } /// Stop being the producer. The source stays pinned by the returned guard, so the caller can /// still deliver a terminal chunk. Touches no JS cell. pub fn take(&self) -> Option { - let source = self.source.take()?; + let source = self.source.replace(None)?; self.parked.set(false); - // SAFETY: still pinned by our ref, which the guard now owns. - unsafe { - (*source.as_ptr()).producer.set(streams::SourceHandle::None); - (*source.as_ptr()).wrapper_unrooted.set(false); - } + source.producer.set(streams::SourceHandle::None); + source.wrapper_unrooted.set(false); Some(PinnedBytes(source)) } @@ -151,9 +139,7 @@ impl ProducerHold { return false; } if let Some(source) = self.source.get() { - // SAFETY: live through our ref. The caller may hold the `&ByteStream` of this very - // source (the chunk it just delivered), which is why this is not a method call. - unsafe { Source::unroot_wrapper(source.as_ptr()) }; + source.unroot_wrapper(); } true } @@ -165,8 +151,7 @@ impl ProducerHold { return false; } if let Some(source) = self.source.get() { - // SAFETY: as in `park`. - unsafe { Source::root_wrapper(source.as_ptr()) }; + source.root_wrapper(); } true } @@ -179,20 +164,12 @@ impl Drop for ProducerHold { } /// A counted ref on a stream's `Source` for the guard's life; derefs to its ByteStream. -pub struct PinnedBytes(core::ptr::NonNull); +pub struct PinnedBytes(SourceRef); impl core::ops::Deref for PinnedBytes { type Target = ByteStream; fn deref(&self) -> &ByteStream { - // SAFETY: pinned by this guard's ref; ByteStream is `&self`-only. - unsafe { &(*self.0.as_ptr()).context } - } -} - -impl Drop for PinnedBytes { - fn drop(&mut self) { - // SAFETY: balances the ref this guard owns. Can free the source. - unsafe { Source::decrement_count(self.0.as_ptr()) }; + &self.0.context } } @@ -202,32 +179,29 @@ impl readable_stream::SourceContext for ByteStream { const SUPPORTS_REF: bool = false; crate::source_context_codegen!(js_BytesInternalReadableStreamSource); - // R-2: trait sigs are fixed at `&mut self` (shared with the other - // `SourceContext` impls); `&mut T` auto-derefs to `&T` so each body - // forwards to the `&self` inherent method below. - fn on_start(&mut self) -> streams::Start { + fn on_start(&self) -> streams::Start { Self::on_start(self) } - fn on_pull(&mut self, buf: &mut [u8], view: JSValue) -> streams::Result { + fn on_pull(&self, buf: &mut [u8], view: JSValue) -> streams::Result { Self::on_pull(self, buf, view) } - fn on_cancel(&mut self) { + fn on_cancel(&self) { Self::on_cancel(self) } - fn deinit_fn(&mut self) { + fn deinit_fn(&self) { Self::finalize(self) } - fn wrapper_finalized(&mut self) { + fn wrapper_finalized(&self) { self.parent_const().producer.get().consumer_collected(); } - fn drain_internal_buffer(&mut self) -> Vec { + fn drain_internal_buffer(&self) -> Vec { Self::drain(self) } fn memory_cost_fn(&self) -> usize { Self::memory_cost(self) } fn to_buffered_value( - &mut self, + &self, global: &JSGlobalObject, action: streams::BufferActionTag, ) -> Option> { @@ -235,39 +209,26 @@ impl readable_stream::SourceContext for ByteStream { } } -// SAFETY: `ByteStream` is always the `context` field of a `Source` -// (ReadableStream.NewSource); never constructed standalone. Everything it -// touches on the `Source` is a `Cell`, so the `&Source` arm suffices. -bun_core::impl_field_parent! { ByteStream => Source.context; pub fn shared parent_const; } +// Every `ByteStream` is the `context` field of a heap-allocated `Source` +// (ReadableStream.NewSource); never constructed standalone. +bun_core::impl_field_parent! { ByteStream => Source.context; pub fn shared parent; pub fn shared parent_const; } impl ByteStream { - #[inline] - const fn empty_pending_buffer() -> *mut [u8] { - core::ptr::slice_from_raw_parts_mut(core::ptr::NonNull::::dangling().as_ptr(), 0) - } - - /// Init-time reset. Runs before the JS - /// wrapper exists, so `&mut self` is sound here (R-2 exemption). - pub(crate) fn setup(&mut self) { - // Called immediately after `ByteStream::default()` construction; - // the old value owns nothing the new one - // reuses, so dropping it is the intended reset. - drop(core::mem::take(self)); - } - - /// Seeds the stream from the drain result; init-time like [`Self::setup`]. - pub(crate) fn apply_drain_result(&mut self, drain_result: DrainResult) { + /// A stream seeded from what the producer had already buffered. + pub(crate) fn new(drain_result: DrainResult) -> Self { + let mut this = Self::default(); match drain_result { DrainResult::EstimatedSize(estimated_size) => { - self.high_water_mark = estimated_size as blob::SizeType; - self.size_hint.set(estimated_size as blob::SizeType); + this.high_water_mark = estimated_size as blob::SizeType; + this.size_hint.set(estimated_size as blob::SizeType); } DrainResult::Owned { list, size_hint } => { - self.buffer.set(list); - self.size_hint.set(size_hint as blob::SizeType); + this.buffer.set(list); + this.size_hint.set(size_hint as blob::SizeType); } DrainResult::Aborted => {} } + this } fn on_start(&self) -> streams::Start { @@ -574,10 +535,9 @@ impl ByteStream { if self.pending.get().state == streams::PendingState::Pending { debug_assert!(self.buffer.get().is_empty()); - // Re-derive the destination from the GC-rooted view instead of trusting the - // raw pointer captured at pull time: JS can detach or transfer the backing - // ArrayBuffer between the pull and the data arriving, leaving - // `pending_buffer` dangling. A detached view re-derives to an empty slice. + // Derive the destination from the GC-rooted view: JS can detach or + // transfer the backing ArrayBuffer between the pull and the data + // arriving. A detached view re-derives to an empty slice. let global = self.parent_const().global_this(); let mut pending_view = self .pending_value @@ -591,7 +551,6 @@ impl ByteStream { debug_assert!(pending_buf.as_ptr() != chunk.as_ptr()); pending_buf[..to_copy_len].copy_from_slice(&chunk[..to_copy_len]); let has_remaining = chunk.len() > to_copy_len; - self.pending_buffer.set(Self::empty_pending_buffer()); let is_really_done = self.has_received_last_chunk.get() && to_copy_len <= pending_buffer_len; @@ -774,13 +733,10 @@ impl ByteStream { return streams::Result::Done; } - // Raw borrow of a JS-owned buffer; rooted by `set_value`. - self.pending_buffer.set(std::ptr::from_mut::<[u8]>(buffer)); + // The JS-owned buffer is rooted by `set_value`; `on_data` re-derives it. self.set_value(view); - // R-2: `JsCell::as_ptr` yields the stable `*mut Pending` that the - // returned `streams::Result::Pending` raw-backref needs. - streams::Result::Pending(self.pending.as_ptr()) + streams::Result::Pending(bun_ptr::BackRef::new(&self.pending)) } pub(crate) fn on_cancel(&self) { @@ -805,7 +761,6 @@ impl ByteStream { } if !view.is_empty() { - self.pending_buffer.set(Self::empty_pending_buffer()); self.pending.with_mut(|p| { p.result.release(); p.result = streams::Result::Done; @@ -829,14 +784,9 @@ impl ByteStream { } /// NOTE: not `impl Drop` — `ByteStream` is the `context` payload of a `.classes.ts` - /// `ReadableStreamSource`; teardown is driven by the GC finalizer via `Source::finalize`, - /// which calls this. Per §JSC, `.classes.ts` payloads use `finalize`, not `deinit`/`Drop`. - /// - /// R-2: stays `&mut self` — this is the destructor path (called once from - /// `SourceContext::deinit_fn(&mut self)` after the ref-count hits zero), so - /// no JS re-entry can alias `self`; and `parent().deinit()` needs unique - /// `Box` provenance. - fn finalize(&mut self) { + /// `ReadableStreamSource`; teardown is driven by the enclosing `Source`'s `Drop` + /// (`SourceContext::deinit_fn`) once its last reference is released. + fn finalize(&self) { bun_jsc::mark_binding!(); if self.buffer.get().capacity() > 0 { self.buffer.with_mut(|b| { @@ -849,7 +799,6 @@ impl ByteStream { if !self.done.get() { self.done.set(true); - self.pending_buffer.set(Self::empty_pending_buffer()); let is_promise = self.pending.with_mut(|p| { p.result.release(); p.result = streams::Result::Done; @@ -870,9 +819,6 @@ impl ByteStream { // each variant's JSPromiseStrong payload. drop(action); } - // Enclosing `Box>` is freed by the caller - // (`NewSource::decrement_count`) after this returns; freeing it here would - // deallocate the storage backing `&mut self` (dangling UAF). } pub(crate) fn drain(&self) -> Vec { diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 3b813f548486..ea42e9531b15 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -129,7 +129,7 @@ fn random_data(global: &JSGlobalObject, slice: &mut [u8]) { } // The #[bun_jsc::host_fn] attribute macro emits the `extern "C"` shim with the -// correct calling convention and `#[unsafe(no_mangle)]` under the exported name. +// correct calling convention under the exported name. #[bun_jsc::host_fn(export = "Bun__randomUUIDv7")] fn bun_random_uuid_v7(global: &JSGlobalObject, callframe: &CallFrame) -> JsResult { let arguments = callframe.arguments_undef::<2>(); @@ -360,8 +360,8 @@ fn bun_random_uuid_v5(global: &JSGlobalObject, callframe: &CallFrame) -> JsResul encoding.encode_with_max_size(global, 32, &uuid.bytes) } -#[unsafe(no_mangle)] -extern "C" fn CryptoObject__create(global: &JSGlobalObject) -> JSValue { +// HOST_EXPORT(CryptoObject__create, c) +pub fn crypto_object_create(global: &JSGlobalObject) -> JSValue { bun_jsc::mark_binding!(); // Box::new aborts on OOM, so an out-of-memory throw arm is unreachable. diff --git a/src/runtime/webcore/FileReader.rs b/src/runtime/webcore/FileReader.rs index 13ee76ec5b8f..d480295f7f25 100644 --- a/src/runtime/webcore/FileReader.rs +++ b/src/runtime/webcore/FileReader.rs @@ -1,4 +1,4 @@ -use core::cell::{Cell, UnsafeCell}; +use core::cell::Cell; use core::mem; use bun_collections::VecExt; @@ -8,7 +8,7 @@ use bun_io as aio; use bun_io::FileType; use bun_io::{BufferedReader, Chunk, ReadState}; use bun_jsc::JsCell; -use bun_ptr::{AsCtxPtr, RefPtr}; +use bun_ptr::{RefPtr, ThisPtr}; use bun_sys::{self as sys, Fd, FdExt}; use crate::webcore::SinkHandle; @@ -16,43 +16,38 @@ use crate::webcore::blob; use crate::webcore::jsc::{self as jsc, EventLoopHandle, JSValue}; use crate::webcore::jsc::{EnsureStillAlive, strong::Optional as Strong}; use crate::webcore::node_types::PathOrFileDescriptor; -use crate::webcore::readable_stream; +use crate::webcore::readable_stream::{self, SourceRef}; use crate::webcore::streams; bun_core::declare_scope!(FileReader, visible); // R-2 (host-fn re-entrancy): every JS-exposed / vtable-reachable method takes -// `&self`; per-field interior mutability via `Cell` (Copy) / `JsCell` (non- -// Copy). The `SourceContext` trait and `BufferedReaderParent` shims still -// hand in `&mut Self` / `*mut Self` until those layers are migrated — `&mut T` -// auto-derefs to `&T` so the impls below compile against either. `Cell` and -// `JsCell` are both `#[repr(transparent)]`, so the embedded layout (offset -// 0 of `NewSource`) is unchanged. +// `&self` (or the enclosing source's `ThisPtr`); per-field interior mutability +// via `Cell` (Copy) / `JsCell` (non-Copy). `Cell` and `JsCell` are both +// `#[repr(transparent)]`, so the embedded layout (offset 0 of +// `NewSource`) is unchanged. pub struct FileReader { - /// Wrapped in `UnsafeCell` so that the back-ref `*mut FileReader` (vtable - /// `parent`) and the reader's own `&mut self` both derive from a - /// SharedReadWrite root — see `BufferedReaderParent` aliasing contract - /// (PipeReader.rs). The vtable callbacks fire while a `&mut BufferedReader` - /// is live on the caller's stack and re-enter `self.reader` (close/buffer/ - /// is_done); without `UnsafeCell` materializing `&mut FileReader` there is - /// Stacked-Borrows UB. Matches sibling `IOReader` (shell) port. - pub(crate) reader: UnsafeCell, + /// The `BufferedReader` re-enters this `FileReader` (through the enclosing + /// `Source`, its registered parent) from inside its own methods, so every + /// access is a short closure-scoped borrow. + pub(crate) reader: JsCell, pub(crate) done: Cell, pub(crate) pending: JsCell, pub(crate) pending_value: JsCell, // Strong.Optional - // TODO(refactor): `&'static mut [u8]` forge — borrows a JS typed-array buffer - // that GC can move/collect, and `&'static mut` asserts uniqueness the GC - // does not honour. `bun_ptr::Interned` is read-only by construction so - // does NOT cover this; tracked under the sibling `static-widen-mut` - // pattern (field should become `*mut [u8]` / `RawSliceMut`). - pub(crate) pending_view: JsCell<&'static mut [u8]>, pub(crate) fd: Cell, /// Read-only after construction (set via struct literal in `from_blob_*`). pub(crate) start_offset: Option, /// Length of the slice window at `start_offset`; the reader is limited to it when it is started and ends the stream there. Read-only after init. pub(crate) max_size: Option, pub(crate) started: Cell, - pub(crate) waiting_for_on_reader_done: Cell, + /// The reference held across an in-flight read (from `start()` until + /// `on_reader_done` / `on_reader_error`): it keeps the source — and, through + /// [`readable_stream::NewSource::retain`], the JS wrapper — alive so an + /// event-loop callback with no JS on the stack never lands on a freed source. + pub(crate) read_ref: Cell>>, + /// References the embedded reader holds on the source while one of its + /// entry points runs (`BufferedReaderParent::ref_` / `deref`, LIFO). + pub(crate) reader_refs: JsCell>>, pub(crate) event_loop: Cell, pub(crate) lazy: JsCell, pub(crate) buffered: JsCell>, @@ -71,16 +66,16 @@ pub struct FileReader { impl Default for FileReader { fn default() -> Self { Self { - reader: UnsafeCell::new(IOReader::init::()), + reader: JsCell::new(IOReader::init::()), done: Cell::new(false), pending: JsCell::new(streams::Pending::default()), pending_value: JsCell::new(Strong::empty()), - pending_view: JsCell::new(&mut []), fd: Cell::new(Fd::INVALID), start_offset: None, max_size: None, started: Cell::new(false), - waiting_for_on_reader_done: Cell::new(false), + read_ref: Cell::new(None), + reader_refs: JsCell::new(Vec::new()), // Sentinel only; never dispatched (callers must overwrite before use). event_loop: Cell::new(EventLoopHandle::init(core::ptr::null_mut())), lazy: JsCell::new(Lazy::None), @@ -254,59 +249,44 @@ impl Lazy { // BufferedReader vtable parent: wires the // `onReadChunk`/`onReaderDone`/`onReaderError`/`loop`/`eventLoop` callbacks. -// -// R-2: every mutated field on `FileReader` is `Cell`/`JsCell`/`UnsafeCell`- -// backed, so materializing `&FileReader` via `(&*this)` does not assert Unique -// over any byte the caller may have borrowed (SharedReadWrite root); the -// inherent impls re-derive any reader access through `reader()` -// (`UnsafeCell::get`). +// The registered parent is the enclosing `Source` (root pointer), so every +// handler receives its `ThisPtr` and reaches the `FileReader` at `.context`. bun_io::impl_buffered_reader_parent! { - FileReader for FileReader; + FileReader for Source; + borrow = this; + reader = context.reader; has_on_read_chunk = true; - on_read_chunk = |this, chunk, state| (&*this).on_read_chunk(chunk, state); - on_reader_done = |this| (&*this).on_reader_done(); - on_reader_error = |this, err| (&*this).on_reader_error(err); + on_read_chunk = |this, chunk, state| FileReader::on_read_chunk(this, chunk, state); + on_reader_done = |this| FileReader::on_reader_done(this); + on_reader_error = |this, err| FileReader::on_reader_error(this, err); loop_ = |this| { - let ev = (&*this).event_loop.get(); + let ev = this.context.event_loop.get(); // The event loop is a libuv // `uv_loop_t*` on Windows. `.cast()` reconciles the impl-declared // `bun_uws_sys::Loop` nominal with `bun_io::Loop` (= `uv::Loop`). #[cfg(windows)] { ev.uv_loop().cast() } #[cfg(not(windows))] { ev.r#loop() } }; - event_loop = |this| (&*this).event_loop.get().as_event_loop_ctx(); + event_loop = |this| this.context.event_loop.get().as_event_loop_ctx(); // A read delivers to `on_read_chunk` consumers (JS, or a native sink such // as HTMLRewriter) that can drop this stream's last GC root and allocate // before the read loop's frames unwind, so the reader pins its parent — - // and, through `increment_count`, the JS wrapper — for the duration. - ref_ = |this| (*(&*this).parent()).increment_count(); - deref = |this| { let _ = Source::decrement_count((&*this).parent()); }; + // and, through `retain`, the JS wrapper — for the duration. + ref_ = |this| { let retained = this.retain(); this.context.reader_refs.with_mut(|refs| refs.push(retained)); }; + deref = |this| { let released = this.context.reader_refs.with_mut(|refs| refs.pop()); drop(released); }; } impl FileReader { - /// SharedReadWrite accessor for the embedded `BufferedReader`. See the - /// `UnsafeCell` note on the field declaration — this is the single point - /// through which all `self.reader` access flows so vtable-callback - /// re-entrancy and outer `&mut FileReader` borrows both root at the cell. - /// SAFETY: single-threaded (JS event loop); the cell is the sole - /// SharedReadWrite root — see the unsafe block below. + /// This reader as the enclosing source's dispatch handle. #[inline] - #[allow(clippy::mut_from_ref)] - pub(crate) fn reader(&self) -> &mut IOReader { - // SAFETY: `FileReader` is single-threaded (JS event loop) and every - // `self.reader` access flows through this accessor, so the `UnsafeCell` - // is the sole SharedReadWrite root — no `&mut IOReader` is held live - // across a vtable-callback re-entry point (see field doc comment). - unsafe { &mut *self.reader.get() } + pub(crate) fn this_ptr(&self) -> ThisPtr { + self.parent().this_ptr() } - // In-place init — `self` is the `context` field of an already-allocated - // `Source`; `event_loop` is set to its real value right after the reset. - // R-2: kept `&mut self` — init-time constructor that runs before any - // host-fn could re-enter; `*self =` requires unique access. - pub(crate) fn on_start(&self) -> streams::Start { - self.reader().set_parent(self.as_ctx_ptr().cast()); + let source = self.this_ptr(); + self.reader + .with_mut(|r| r.set_parent(source.as_ptr().cast())); let was_lazy = !matches!(self.lazy.get(), Lazy::None); let mut pollable = false; #[cfg(unix)] @@ -340,18 +320,18 @@ impl FileReader { #[cfg(unix)] { use bun_io::pipe_reader::PosixFlags; - self.reader() - .flags - .set(PosixFlags::NONBLOCKING, opened.nonblocking); - self.reader().flags.set(PosixFlags::POLLABLE, pollable); + self.reader.with_mut(|r| { + r.flags.set(PosixFlags::NONBLOCKING, opened.nonblocking); + r.flags.set(PosixFlags::POLLABLE, pollable); + }); } #[cfg(windows)] { use bun_io::pipe_reader::WindowsFlags; - self.reader() - .flags - .set(WindowsFlags::NONBLOCKING, opened.nonblocking); - self.reader().flags.set(WindowsFlags::POLLABLE, pollable); + self.reader.with_mut(|r| { + r.flags.set(WindowsFlags::NONBLOCKING, opened.nonblocking); + r.flags.set(WindowsFlags::POLLABLE, pollable); + }); } } } @@ -360,16 +340,14 @@ impl FileReader { } { - let reader_fd = self.reader().get_fd(); + let reader_fd = self.reader.get().get_fd(); if reader_fd != Fd::INVALID && self.fd.get() == Fd::INVALID { self.fd.set(reader_fd); } } - // `bun_vm()` returns a raw `*mut VirtualMachine` (never null for a Bun - // global); deref to call `event_loop()`. { - let global = self.parent_global(); + let global = self.parent().global_this(); // `bun_vm()` is the live thread-local VM; `event_loop()` is its // per-thread `jsc::EventLoop`. self.event_loop.set(EventLoopHandle::init( @@ -378,10 +356,10 @@ impl FileReader { } if was_lazy { - // The across-read ref roots the JS wrapper (`increment_count` - // upgrades `this_jsvalue` to Strong) so an event-loop callback - // firing with no JS on the stack never lands on a freed box. For a - // POSIX non-pollable regular file every read is synchronous + // The across-read ref roots the JS wrapper (`retain` upgrades + // `this_jsvalue` to Strong) so an event-loop callback firing with + // no JS on the stack never lands on a freed source. For a POSIX + // non-pollable regular file every read is synchronous // (`read_file` → `sys::pread`), so there is no such callback — // holding the Strong there would root an abandoned reader forever // and leak its fd. Windows file reads are async via libuv even for @@ -391,23 +369,15 @@ impl FileReader { #[cfg(windows)] let need_io_ref = true; if need_io_ref { - // SAFETY: see `parent()`. - unsafe { (*self.parent()).increment_count() }; - self.waiting_for_on_reader_done.set(true); + self.read_ref.set(Some(source.retain())); } - self.reader().set_limit(self.max_size); - let start_result = if let Some(offset) = self.start_offset { - self.reader() - .start_file_offset(self.fd.get(), pollable, offset) - } else { - self.reader().start(self.fd.get(), pollable) - }; + self.reader.with_mut(|r| r.set_limit(self.max_size)); + let start_result = + IOReader::start_from(source, self.fd.get(), pollable, self.start_offset); if let Err(e) = start_result { if need_io_ref { - self.waiting_for_on_reader_done.set(false); - let parent = self.parent(); - // SAFETY: see `parent()`; JS finalizer still holds a ref so this cannot free it. - let _ = unsafe { Source::decrement_count(parent) }; + // The JS wrapper still holds its reference, so this cannot free the source. + drop(self.read_ref.take()); } return streams::Start::Err(e); } @@ -416,13 +386,11 @@ impl FileReader { { use bun_io::pipe_reader::PosixFlags; if !self.started.get() - && !self.waiting_for_on_reader_done.get() - && self.reader().flags.contains(PosixFlags::POLLABLE) - && !self.reader().is_done() + && !self.has_read_ref() + && self.reader.get().flags.contains(PosixFlags::POLLABLE) + && !self.reader.get().is_done() { - self.waiting_for_on_reader_done.set(true); - // SAFETY: see `parent()`. - unsafe { (*self.parent()).increment_count() }; + self.read_ref.set(Some(source.retain())); } } #[cfg(windows)] @@ -431,13 +399,11 @@ impl FileReader { // ref across the pending uv_read_start so the source is not // finalized while IOCP has a read queued on it. if !self.started.get() - && !self.waiting_for_on_reader_done.get() - && self.reader().source.is_some() - && !self.reader().is_done() + && !self.has_read_ref() + && self.reader.get().source.is_some() + && !self.reader.get().is_done() { - self.waiting_for_on_reader_done.set(true); - // SAFETY: see `parent()`. - unsafe { (*self.parent()).increment_count() }; + self.read_ref.set(Some(source.retain())); } } } @@ -446,10 +412,10 @@ impl FileReader { { use bun_io::pipe_reader::PosixFlags; if file_type == FileType::Socket { - self.reader().flags.insert(PosixFlags::SOCKET); + self.reader.with_mut(|r| r.flags.insert(PosixFlags::SOCKET)); } - let r = self.reader(); + let r = self.reader.get(); if let Some(poll) = r.handle.get_poll() { // `bun_io::FilePoll` is an opaque vtable wrapper; flag // mutation goes through `set_flag(FilePollFlag)`. @@ -469,7 +435,7 @@ impl FileReader { self.started.set(true); - if self.reader().is_done() { + if self.reader.get().is_done() { self.consume_reader_buffer(); if !self.buffered.get().is_empty() { return streams::Start::OwnedAndDone(Vec::::move_from_list( @@ -480,13 +446,12 @@ impl FileReader { #[cfg(unix)] { use bun_io::pipe_reader::PosixFlags; - if !was_lazy && self.reader().flags.contains(PosixFlags::POLLABLE) { + if !was_lazy && self.reader.get().flags.contains(PosixFlags::POLLABLE) { // A from_pipe() reader may arrive with IS_PAUSED set (lazy // subprocess stdio); clear it so read() does not no-op. - self.reader().unpause(); - // SAFETY: the reader cell is live for `self`'s lifetime; `read` is - // the raw re-entrancy-safe entry (its dispatch runs user JS). - unsafe { IOReader::read(self.reader.get()) }; + IOReader::unpause_from(source); + // `read_from` is the re-entrancy-safe entry (its dispatch runs user JS). + IOReader::read_from(source); } } } @@ -494,17 +459,12 @@ impl FileReader { streams::Start::Ready } - /// Safe accessor for the parent `NewSource.global_this` back-reference. - /// - /// One unsafe (`from_field_ptr` raw-place projection of a `Copy` field — - /// no `&Source` is materialized so no aliasing with `&self`); callers - /// then `Deref` the returned `BackRef` with no unsafe. #[inline] - fn parent_global(&self) -> bun_ptr::BackRef { - // SAFETY: see `parent()` — `self` is the `context` field of a live - // heap-allocated `Source`. Reading the `Copy` `global_this` via - // `(*ptr).field` is a raw-place read, not a `&Source` borrow. - unsafe { (*self.parent()).global_this }.expect("NewSource.global_this set before use") + fn has_read_ref(&self) -> bool { + let r = self.read_ref.take(); + let held = r.is_some(); + self.read_ref.set(r); + held } /// Lazily start the reader for a native-sink hookup. Bun's file-backed @@ -516,9 +476,7 @@ impl FileReader { if self.started.get() { return None; } - // SAFETY: see `parent()` — `self` is the `context` field of a live - // heap-allocated `Source`; single-threaded JS, no aliasing `&mut`. - unsafe { (*self.parent()).global_this = Some(bun_ptr::BackRef::new(global)) }; + self.parent().set_global_this(global); match self.on_start() { streams::Start::Ready | streams::Start::Empty | streams::Start::ChunkSize(_) => None, other => Some(other), @@ -533,16 +491,17 @@ impl FileReader { } /// Sink's drain ack: unpause, push any buffered bytes, then resume reading. - pub(crate) fn pull_into_sink(&self) { - if !self.sink_paused.replace(false) { + pub(crate) fn pull_into_sink(this: ThisPtr) { + let self_ = &this.context; + if !self_.sink_paused.replace(false) { return; } - let sink = *self.sink.get(); + let sink = *self_.sink.get(); if sink.is_none() { return; } - let reader_done = self.reader_finished(); - let buffered = self.drain(); + let reader_done = self_.reader_finished(); + let buffered = self_.drain(); if !buffered.is_empty() { let chunk = if reader_done { streams::Result::OwnedAndDone(buffered) @@ -551,38 +510,38 @@ impl FileReader { }; match sink.write(&chunk) { streams::Writable::Backpressure(_) => { - self.sink_paused.set(true); - self.reader().pause(); + self_.sink_paused.set(true); + self_.reader.with_mut(|r| r.pause()); return; } streams::Writable::Err(e) => { - self.sink.set(SinkHandle::None); + self_.sink.set(SinkHandle::None); sink.end(Some(streams::StreamError::Error(e))); return; } streams::Writable::Done => { - self.sink.set(SinkHandle::None); + self_.sink.set(SinkHandle::None); sink.end(None); return; } _ => {} } } - if reader_done || self.done.get() { - self.sink.set(SinkHandle::None); + if reader_done || self_.done.get() { + self_.sink.set(SinkHandle::None); // A read error from before the sink was attached ends it here. sink.end( - self.read_error + self_ + .read_error .replace(None) .map(streams::StreamError::Error), ); return; } - if !self.reader().has_pending_read() { - self.reader().unpause(); - // SAFETY: the reader cell is live for `self`'s lifetime; `read` is - // the raw re-entrancy-safe entry (its dispatch runs user JS). - unsafe { IOReader::read(self.reader.get()) }; + if !self_.reader.get().has_pending_read() { + IOReader::unpause_from(this); + // `read_from` is the re-entrancy-safe entry (its dispatch runs user JS). + IOReader::read_from(this); } } @@ -592,37 +551,35 @@ impl FileReader { return; } self.done.set(true); - self.reader().update_ref(false); - if !self.reader().is_done() { - self.reader().close(); + self.reader.with_mut(|r| r.update_ref(false)); + if !self.reader.get().is_done() { + // Its done callback re-enters `on_reader_done`. + IOReader::close_from(self.this_ptr()); } } // NOTE: not `impl Drop` — FileReader is embedded as `Source.context` and this is - // invoked from the Source's JS finalizer path via `SourceContext::deinit_fn`. - // Not `pub`: reached only via the `SourceContext` trait impl below. - // - // Only side-effect teardown lives here. Owned fields (buffered: Vec, reader: - // BufferedReader, pending_value: Strong, lazy: Arc) drop when the caller - // (`NewSource::decrement_count`) reclaims the `Box` *after* this - // returns. Freeing the parent here would - // deallocate the storage backing `&self` while the borrow is still live - // — a dangling-reference UAF — so ownership release stays with the caller. + // invoked from the Source's `Drop` via `SourceContext::deinit_fn`, before the + // owned fields (buffered, reader, pending_value, lazy) drop. fn deinit(&self) { - self.reader().update_ref(false); + self.reader.with_mut(|r| r.update_ref(false)); } - fn finalize_detach(&self) -> bool { - debug_assert!(!(self.done.get() && self.waiting_for_on_reader_done.get())); - if self.done.get() || !self.waiting_for_on_reader_done.get() { - return false; + /// The JS wrapper is being finalized: if a read is still in flight, give up + /// its reference (the wrapper it was rooting is gone) and mark the reader done. + fn finalize_detach(&self) { + let read_ref = self.read_ref.take(); + debug_assert!(!(self.done.get() && read_ref.is_some())); + if self.done.get() || read_ref.is_none() { + self.read_ref.set(read_ref); + return; } - self.waiting_for_on_reader_done.set(false); self.done.set(true); - true + drop(read_ref); } - pub(crate) fn on_read_chunk(&self, chunk: Chunk<'_>, state: ReadState) -> bool { + pub(crate) fn on_read_chunk(this: ThisPtr, chunk: Chunk<'_>, state: ReadState) -> bool { + let self_ = &this.context; bun_core::scoped_log!( FileReader, "onReadChunk() = {} ({})", @@ -630,35 +587,35 @@ impl FileReader { read_state_tag(state) ); - if self.done.get() { - self.reader().close(); + if self_.done.get() { + IOReader::close_from(this); return false; } let has_more = state != ReadState::Eof; - let sink = *self.sink.get(); + let sink = *self_.sink.get(); if sink.is_some() { - self.write_chunk_to_sink(sink, &chunk, has_more) - } else if self.pending.get().state == streams::PendingState::Pending { + self_.write_chunk_to_sink(sink, &chunk, has_more) + } else if self_.pending.get().state == streams::PendingState::Pending { // Pipes may return 0-byte reads short of EOF; keep reading. if chunk.is_empty() && state == ReadState::Drained { true } else { - self.resolve_pending_read(chunk, has_more) + Self::resolve_pending_read(this, chunk, has_more) } } else { - if self.buffered.get().is_empty() && chunk.is_owned() { - self.buffered.set(chunk.take()); + if self_.buffered.get().is_empty() && chunk.is_owned() { + self_.buffered.set(chunk.take()); } else { - self.buffered.with_mut(|b| b.extend_from_slice(&chunk)); + self_.buffered.with_mut(|b| b.extend_from_slice(&chunk)); } // No JS read is waiting; stop at the highwater mark and let onPull restart. `started` gates it: a non-lazy `Bun.spawn` pipe is already reading before any consumer attaches, and throttling then deadlocks a child alternating stdout/stderr writes. - let keep_going = !self.started.get() - || (self.flowing.get() && self.buffered.get().len() < self.highwater_mark); + let keep_going = !self_.started.get() + || (self_.flowing.get() && self_.buffered.get().len() < self_.highwater_mark); // A completion-driven reader keeps issuing reads unless stopped; `on_pull` restarts it. #[cfg(windows)] if !keep_going { - self.reader().pause(); + self_.reader.with_mut(|r| r.pause()); } keep_going } @@ -676,7 +633,7 @@ impl FileReader { streams::Writable::Backpressure(_) => { // Returning `false` ends a synchronous read loop; an event-driven reader (Windows, pollable fds) has to be paused or its next completion piles into the sink. `pull_into_sink` unpauses. self.sink_paused.set(true); - self.reader().pause(); + self.reader.with_mut(|r| r.pause()); return false; } streams::Writable::Err(e) => { @@ -700,36 +657,37 @@ impl FileReader { } /// Settles the parked JS read with `chunk` (invariant: a parked read means `buffered` was already drained into it). - fn resolve_pending_read(&self, chunk: Chunk<'_>, has_more: bool) -> bool { - let was_done = self.reader().is_done(); - let global = self.parent_global(); - let mut pending_array_buffer = self + fn resolve_pending_read(this: ThisPtr, chunk: Chunk<'_>, has_more: bool) -> bool { + let self_ = &this.context; + let was_done = self_.reader.get().is_done(); + let global = this.global_this(); + let mut pending_array_buffer = self_ .pending_value .get() .get() - .and_then(|view| view.as_array_buffer(&global)) + .and_then(|view| view.as_array_buffer(global)) .unwrap_or_default(); let pending_buf = pending_array_buffer.slice_mut(); let ret = if chunk.is_empty() { - let buffered = self.buffered.replace(Vec::new()); + let buffered = self_.buffered.replace(Vec::new()); let result = if buffered.is_empty() { streams::Result::Done } else if pending_buf.len() >= buffered.len() { pending_buf[..buffered.len()].copy_from_slice(&buffered); streams::Result::IntoArrayAndDone(streams::IntoArray { - value: self.pending_value.get().get().unwrap_or_default(), + value: self_.pending_value.get().get().unwrap_or_default(), len: buffered.len() as u64, }) } else { streams::Result::OwnedAndDone(buffered) }; - self.pending.with_mut(|p| p.result = result); + self_.pending.with_mut(|p| p.result = result); false } else { let result = if pending_buf.len() >= chunk.len() { pending_buf[..chunk.len()].copy_from_slice(&chunk); let into = streams::IntoArray { - value: self.pending_value.get().get().unwrap_or_default(), + value: self_.pending_value.get().get().unwrap_or_default(), len: chunk.len() as u64, }; if was_done { @@ -748,21 +706,20 @@ impl FileReader { // Copied into a fresh Uint8Array by `run()` below, before this returns. streams::Result::Temporary(bun_ptr::RawSlice::new(&chunk)) }; - self.pending.with_mut(|p| p.result = result); + self_.pending.with_mut(|p| p.result = result); !was_done }; - self.pending_value + self_ + .pending_value .with_mut(|p| p.clear_without_deallocation()); - self.pending_view.set(&mut []); - // A re-entrant cancel() inside `run()` reaches on_reader_done, which drops the across-read ref and lets a GC free this box while the io caller still holds `&mut` into it. - // SAFETY: see `parent()`. - let _pin = unsafe { SourcePin::new(self.parent()) }; - self.pending.with_mut(|p| p.run()); + // Pin across `run()`: a re-entrant cancel() reaches on_reader_done, which drops the across-read ref and lets a GC free this source while the io caller is still inside it. + let _pin = this.retain(); + self_.pending.with_mut(|p| p.run()); // Re-entrant cancel or a nested pull that read to EOF closed the reader; tell the io caller to stop so it does not re-read the captured fd. - ret && !self.done.get() && !self.reader().is_done() + ret && !self_.done.get() && !self_.reader.get().is_done() } - pub(crate) fn on_pull(&self, buffer: &'static mut [u8], array: JSValue) -> streams::Result { + pub(crate) fn on_pull(&self, buffer: &mut [u8], array: JSValue) -> streams::Result { // `buffer` borrows a JS typed array kept alive by `array`. array.ensure_still_alive(); let _keep = EnsureStillAlive(array); @@ -773,7 +730,6 @@ impl FileReader { self.pending_value .with_mut(|p| p.clear_without_deallocation()); - self.pending_view.set(&mut []); if buffer.len() >= drained.len() as usize { let drained_len = drained.len(); @@ -804,13 +760,13 @@ impl FileReader { } // A stored error also ends a reader that never started (`from_bytes_then_error`). - if self.reader().is_done() || self.read_error.get().is_some() { + if self.reader.get().is_done() || self.read_error.get().is_some() { return self.end_of_reader(); } - if !self.reader().has_pending_read() && self.flowing.get() { - // SAFETY: the reader cell is live for `self`'s lifetime; `read_into` is the raw re-entrancy-safe entry (EOF/error dispatch runs user JS). - let (amount_read, state) = unsafe { IOReader::read_into(self.reader.get(), buffer) }; + if !self.reader.get().has_pending_read() && self.flowing.get() { + // `read_into_from` is the re-entrancy-safe entry (EOF/error dispatch runs user JS). + let (amount_read, state) = IOReader::read_into_from(self.this_ptr(), buffer); bun_core::scoped_log!(FileReader, "onPull({}) = {}", buffer.len(), amount_read); let done = state == ReadState::Eof || self.reader_finished(); if amount_read > 0 { @@ -833,134 +789,126 @@ impl FileReader { streams::Result::Owned(drained) }; } - if done || self.reader().is_done() { + if done || self.reader.get().is_done() { return self.end_of_reader(); } } let buffer_len = buffer.len(); - let global = self.parent_global(); - self.pending_value.with_mut(|p| p.set(&global, array)); - self.pending_view.set(buffer); + let global = self.parent().global_this(); + self.pending_value.with_mut(|p| p.set(global, array)); #[cfg(windows)] if self.flowing.get() { - self.reader().unpause(); + IOReader::unpause_from(self.this_ptr()); } bun_core::scoped_log!(FileReader, "onPull({}) = pending", buffer_len); - streams::Result::Pending(self.pending.as_ptr()) + streams::Result::Pending(bun_ptr::BackRef::new(&self.pending)) } pub(crate) fn drain(&self) -> Vec { if !self.buffered.get().is_empty() { let out = Vec::::move_from_list(self.buffered.replace(Vec::new())); - debug_assert!(self.reader().buffer().as_ptr() != out.as_ptr()); + debug_assert!(self.reader.with_mut(|r| r.buffer().as_ptr()) != out.as_ptr()); return out; } - if self.reader().has_pending_read() { + if self.reader.get().has_pending_read() { return Vec::::default(); } - Vec::::move_from_list(mem::take(self.reader().buffer())) + Vec::::move_from_list(self.reader.with_mut(|r| mem::take(r.buffer()))) } pub(crate) fn set_ref_or_unref(&self, enable: bool) { if self.done.get() { return; } - self.reader().update_ref(enable); + self.reader.with_mut(|r| r.update_ref(enable)); } fn consume_reader_buffer(&self) { if self.buffered.get().capacity() == 0 { - self.buffered.set(mem::take(self.reader().buffer())); + self.buffered + .set(self.reader.with_mut(|r| mem::take(r.buffer()))); } } - pub(crate) fn on_reader_done(&self) { + pub(crate) fn on_reader_done(this: ThisPtr) { + let self_ = &this.context; bun_core::scoped_log!(FileReader, "onReaderDone()"); - // `p.run()` and `on_close()` can run user JS, and the `self.buffered` / - // `waiting_for_on_reader_done` reads below must not land on a freed box. - let parent = self.parent(); - // SAFETY: see `parent()`. - let _pin = unsafe { SourcePin::new(parent) }; - let sink = *self.sink.get(); + // Pin across `p.run()` and `on_close()`: both can run user JS, and the + // `buffered` / `read_ref` accesses below must not land on a freed + // source. Same bracket as on_read_chunk / on_reader_error. + let _pin = this.retain(); + let sink = *self_.sink.get(); if sink.is_some() { - self.consume_reader_buffer(); - if !self.sink_paused.get() { - self.sink.set(SinkHandle::None); - let buffered = self.buffered.replace(Vec::new()); + self_.consume_reader_buffer(); + if !self_.sink_paused.get() { + self_.sink.set(SinkHandle::None); + let buffered = self_.buffered.replace(Vec::new()); if !buffered.is_empty() { let _ = sink.write(&streams::Result::OwnedAndDone(buffered)); } sink.end(None); } } else { - self.consume_reader_buffer(); - if self.pending.get().state == streams::PendingState::Pending { - if !self.buffered.get().is_empty() { - let buffered = self.buffered.replace(Vec::new()); - self.pending.with_mut(|p| { + self_.consume_reader_buffer(); + if self_.pending.get().state == streams::PendingState::Pending { + if !self_.buffered.get().is_empty() { + let buffered = self_.buffered.replace(Vec::new()); + self_.pending.with_mut(|p| { p.result = streams::Result::OwnedAndDone(Vec::::move_from_list(buffered)) }); } else { - self.pending.with_mut(|p| p.result = streams::Result::Done); + self_.pending.with_mut(|p| p.result = streams::Result::Done); } - self.buffered.set(Vec::new()); - self.pending.with_mut(|p| p.run()); + self_.buffered.set(Vec::new()); + self_.pending.with_mut(|p| p.run()); } // Don't handle buffered data here - it will be returned on the next onPull // This ensures proper ordering of chunks } // Only close the stream if there's no buffered data left to deliver - if self.buffered.get().is_empty() { - // SAFETY: see `parent()`; the pin keeps the count > 0. - unsafe { (*parent).on_close() }; - } - if self.waiting_for_on_reader_done.get() { - self.waiting_for_on_reader_done.set(false); - // SAFETY: see `parent()`; `_pin` keeps the count > 0. - let _ = unsafe { Source::decrement_count(parent) }; + if self_.buffered.get().is_empty() { + this.on_close(); } + drop(self_.read_ref.take()); } - pub(crate) fn on_reader_error(&self, err: sys::Error) { - self.consume_reader_buffer(); - if self.buffered.get().capacity() > 0 && self.buffered.get().is_empty() { - self.buffered.set(Vec::new()); + pub(crate) fn on_reader_error(this: ThisPtr, err: sys::Error) { + let self_ = &this.context; + self_.consume_reader_buffer(); + if self_.buffered.get().capacity() > 0 && self_.buffered.get().is_empty() { + self_.buffered.set(Vec::new()); } // `sink.end()` and `p.run()` run user JS, which can reach on_reader_done - // and drop the across-read ref before the read of it below. - let parent = self.parent(); - // SAFETY: see `parent()`. - let _pin = unsafe { SourcePin::new(parent) }; + // and drop the across-read ref before the `read_ref` access below. + let _pin = this.retain(); - let sink = *self.sink.get(); + let sink = *self_.sink.get(); if sink.is_some() { - self.sink.set(SinkHandle::None); - self.sink_paused.set(false); + self_.sink.set(SinkHandle::None); + self_.sink_paused.set(false); sink.end(Some(streams::StreamError::Error(err))); - } else if self.pending.get().state == streams::PendingState::Pending { - self.pending.with_mut(|p| { + } else if self_.pending.get().state == streams::PendingState::Pending { + self_.pending.with_mut(|p| { p.result = streams::Result::Err(streams::StreamError::Error(err)); }); - self.pending.with_mut(|p| p.run()); + self_.pending.with_mut(|p| p.run()); } else { // `p.run()` would no-op and the pull promise would never settle. - self.read_error.set(Some(err)); + self_.read_error.set(Some(err)); } - if self.waiting_for_on_reader_done.get() && !self.done.get() { - self.waiting_for_on_reader_done.set(false); - // SAFETY: see `parent()`; `_pin` keeps the count > 0. - let _ = unsafe { Source::decrement_count(parent) }; + if !self_.done.get() { + drop(self_.read_ref.take()); } - self.close_after_error(); + self_.close_after_error(); } /// An errored stream is never cancelled, so release the poll and the fd here. @@ -969,13 +917,15 @@ impl FileReader { return; } self.done.set(true); - self.reader().update_ref(false); - self.reader().deinit(); + self.reader.with_mut(|r| { + r.update_ref(false); + r.deinit(); + }); } /// Done, with no stored read error left for one more pull to return. fn reader_finished(&self) -> bool { - self.reader().is_done() && self.read_error.get().is_none() + self.reader.get().is_done() && self.read_error.get().is_none() } /// The stored read error, or a clean end. @@ -996,7 +946,7 @@ impl FileReader { } #[cfg(windows)] { - self.reader().set_raw_mode(_flag) + self.reader.with_mut(|r| r.set_raw_mode(_flag)) } } @@ -1015,95 +965,59 @@ impl FileReader { self.flowing.set(flag); if flag { - self.reader().unpause(); - if !self.reader().is_done() && !self.reader().has_pending_read() { + IOReader::unpause_from(self.this_ptr()); + if !self.reader.get().is_done() && !self.reader.get().has_pending_read() { // Kick off a new read if needed - // SAFETY: the reader cell is live for `self`'s lifetime; `read` is - // the raw re-entrancy-safe entry (its dispatch runs user JS). - unsafe { IOReader::read(self.reader.get()) }; + IOReader::read_from(self.this_ptr()); } } else { - self.reader().pause(); + self.reader.with_mut(|r| r.pause()); } } pub(crate) fn memory_cost(&self) -> usize { // ReadableStreamSource covers @sizeOf(FileReader) - self.reader().memory_cost() + self.buffered.get().capacity() + self.reader.get().memory_cost() + self.buffered.get().capacity() } } pub type Source = readable_stream::NewSource; -/// Holds a ref on the `Source` that embeds a `FileReader` while a dispatch runs -/// user JS. Dropping it releases the ref and can free the source, so a pin must -/// outlive every use of the reader it protects. -struct SourcePin(*mut Source); - -impl SourcePin { - /// # Safety - /// `parent` is the live `Source` that embeds the caller. - unsafe fn new(parent: *mut Source) -> Self { - // SAFETY: fn contract. - unsafe { (*parent).increment_count() }; - Self(parent) - } -} - -impl Drop for SourcePin { - fn drop(&mut self) { - // SAFETY: balances the ref taken in `new`. - let _ = unsafe { Source::decrement_count(self.0) }; - } -} - -// SAFETY: `FileReader` is always the `context` field of a heap-allocated -// `Source`. `parent` is the `raw` arm because the ref-count pin -// (`increment_count`/`decrement_count`) and `global_this` are plain `Source` -// fields; callers deref in a tight `unsafe { (*ptr).method() }` scope and never -// hold `&mut Source` across other `self.*` accesses. -bun_core::impl_field_parent! { FileReader => Source.context; pub fn raw parent; pub fn shared parent_const; } +// Every `FileReader` is the `context` field of a heap-allocated `Source`. +bun_core::impl_field_parent! { FileReader => Source.context; pub fn shared parent; pub fn shared parent_const; } impl readable_stream::SourceContext for FileReader { const NAME: &'static str = "File"; const SUPPORTS_REF: bool = true; crate::source_context_codegen!(js_FileInternalReadableStreamSource); - // R-2: trait sigs are still `&mut self` (shared with ByteBlobLoader/ - // ByteStream — separate migration); the inherent impls take `&self`, so - // these forward via auto-deref. The `&mut` here is what the codegen shim - // currently emits; once `NewSource` is celled the trait flips to `&self` - // and these become straight `Self::*(self, ..)` calls. - fn on_start(&mut self) -> streams::Start { + fn on_start(&self) -> streams::Start { Self::on_start(self) } - fn on_pull(&mut self, buf: &mut [u8], arr: JSValue) -> streams::Result { - // SAFETY: lifetime laundering — `buf` borrows a JS typed array kept alive - // by `arr` (see the lifetime note at the top of the file). - let buf = unsafe { &mut *std::ptr::from_mut::<[u8]>(buf) }; + fn on_pull(&self, buf: &mut [u8], arr: JSValue) -> streams::Result { Self::on_pull(self, buf, arr) } - fn on_cancel(&mut self) { + fn on_cancel(&self) { Self::on_cancel(self); } - fn deinit_fn(&mut self) { + fn deinit_fn(&self) { Self::deinit(self) } - fn finalize_detach(&mut self) -> bool { + fn finalize_detach(&self) { Self::finalize_detach(self) } - fn set_ref_unref(&mut self, e: bool) { + fn set_ref_unref(&self, e: bool) { Self::set_ref_or_unref(self, e) } - fn drain_internal_buffer(&mut self) -> Vec { + fn drain_internal_buffer(&self) -> Vec { Self::drain(self) } fn memory_cost_fn(&self) -> usize { Self::memory_cost(self) } - fn set_raw_mode(&mut self, flag: bool) -> Option> { + fn set_raw_mode(&self, flag: bool) -> Option> { Some(Self::set_raw_mode(self, flag)) } - fn set_flowing(&mut self, flag: bool) { + fn set_flowing(&self, flag: bool) { Self::set_flowing(self, flag) } // toBufferedValue: null diff --git a/src/runtime/webcore/FileSink.rs b/src/runtime/webcore/FileSink.rs index 0dd27162ea43..09f12f149c27 100644 --- a/src/runtime/webcore/FileSink.rs +++ b/src/runtime/webcore/FileSink.rs @@ -1326,7 +1326,7 @@ impl FileSink { p.consumed += accepted; p.result = streams::Writable::Owned(p.consumed); }); - streams::Writable::Pending(self.pending.as_ptr()) + streams::Writable::Pending(bun_ptr::BackRef::new(&self.pending)) } } } @@ -1542,7 +1542,9 @@ impl FileSink { // before GC, so its destructor only reaches `controller_finalize` at // heap teardown, where it stands in for the pump's reaction. let promise_result = - JSSink::assign_to_stream(global_this, stream.value, NonNull::from(this)); + JSSink::assign_to_stream(global_this, stream.value, NonNull::from(this), |s| { + this.source.set(s) + }); if let Some(err) = promise_result.to_error() { this.readable_stream.set(readable_stream::Strong::default()); diff --git a/src/runtime/webcore/ObjectURLRegistry.rs b/src/runtime/webcore/ObjectURLRegistry.rs index 905f70cfe3ff..ae851bbf0752 100644 --- a/src/runtime/webcore/ObjectURLRegistry.rs +++ b/src/runtime/webcore/ObjectURLRegistry.rs @@ -33,6 +33,8 @@ pub struct Entry { blob: Blob, } +// `Entry` is auto-`Send`: its sole field is `Blob`, which already asserts +// `Send + Sync` (see `webcore_types::Blob`). const _: fn() = || { fn assert_send() {} assert_send::(); @@ -130,8 +132,7 @@ fn bun_create_object_url( .throw_invalid_arguments(format_args!("createObjectURL expects a Blob object"))); }; let registry = ObjectURLRegistry::singleton(); - // SAFETY: `bun_vm_ptr()` returns the live VM pointer for `global_object`. - let uuid = registry.register(unsafe { &mut *global_object.bun_vm_ptr() }, blob); + let uuid = registry.register(global_object.bun_vm().as_mut(), blob); bun_core::String::create_format(format_args!("blob:{}", uuid)).into_js(global_object) } diff --git a/src/runtime/webcore/ReadableStream.rs b/src/runtime/webcore/ReadableStream.rs index 86f0bc7d40a9..6396ae6ed88c 100644 --- a/src/runtime/webcore/ReadableStream.rs +++ b/src/runtime/webcore/ReadableStream.rs @@ -7,7 +7,7 @@ use crate::webcore::jsc::{self as jsc, CallFrame, JSGlobalObject, JSValue, JsRes // `bun_jsc` not yet a dep; alias to local shim so `bun_jsc::Strong` etc. resolve. use crate::webcore::jsc as bun_jsc; use bun_collections::VecExt; -use bun_sys as syscall; +use bun_ptr::RefPtr; use crate::webcore::streams; use crate::webcore::{self, Blob, ByteBlobLoader, ByteStream, FileReader}; @@ -43,7 +43,7 @@ pub enum Strong { Held(bun_jsc::Strong), /// After [`Self::downgrade`]: the owning wrapper's `m_stream` `WriteBarrier` /// roots the stream; observed through a real `JSC::Weak` so readers see - /// `None` once the stream (and its `Box>`) is collected. + /// `None` once the stream (and its `NewSource<_>`) is collected. Weak(bun_jsc::Weak<()>), } @@ -114,8 +114,7 @@ impl Strong { unsafe extern "C" { /// C++ writes the two teed-stream JSValues into the out-params; reference - /// params encode the non-null/aligned precondition so callers need no - /// `unsafe` block. + /// params encode the non-null/aligned precondition. safe fn ReadableStream__tee( stream: JSValue, global_this: &JSGlobalObject, @@ -196,17 +195,14 @@ impl ReadableStream { self.reload_tag(); match self.ptr { - Source::Blob(blobby) => { - // SAFETY: ptr came from ReadableStreamTag__tagged; valid while stream alive. - let blobby = unsafe { &mut *blobby }; + Source::Blob(_) => { + let blobby = self.ptr.blob().expect("matched Blob"); if let Some(blob) = blobby.to_any_blob(global_this) { self.done(); return Some(blob); } } Source::File(_) => { - // BACKREF: see `Source::file()` — payload valid while stream alive. - // R-2: `lazy`/`started` are `JsCell`/`Cell`; shared borrow suffices. let blobby = self.ptr.file().expect("matched File"); if let webcore::file_reader::Lazy::Blob(store) = blobby.lazy.get() { let blob = Blob::init_with_store(store.clone(), global_this); @@ -224,7 +220,6 @@ impl ReadableStream { } } Source::Bytes(_) => { - // BACKREF: see `Source::bytes()` — payload valid while stream alive. let bytes = self.ptr.bytes().expect("matched Bytes"); // If we've received the complete body by the time this function is called // we can avoid streaming it and convert it to a Blob @@ -245,12 +240,9 @@ impl ReadableStream { // cancel actually mark the stream source as done // this will resolve any pending promises to done: true match self.ptr { - // SAFETY: ptrs came from ReadableStreamTag__tagged; valid while stream alive. - Source::Blob(source) => unsafe { (*NewSource::from_context_ptr(source)).cancel() }, - // SAFETY: ptr came from ReadableStreamTag__tagged; valid while stream alive. - Source::File(source) => unsafe { (*NewSource::from_context_ptr(source)).cancel() }, - // SAFETY: ptr came from ReadableStreamTag__tagged; valid while stream alive. - Source::Bytes(source) => unsafe { (*NewSource::from_context_ptr(source)).cancel() }, + Source::Blob(_) => self.ptr.blob().expect("matched Blob").parent().cancel(), + Source::File(_) => self.ptr.file().expect("matched File").parent().cancel(), + Source::Bytes(_) => self.ptr.bytes().expect("matched Bytes").parent().cancel(), _ => {} } } @@ -291,7 +283,6 @@ impl ReadableStream { } pub(crate) fn force_detach(&self, global_object: &JSGlobalObject) { - // SAFETY: FFI call; value is a valid ReadableStream JSValue. ReadableStream__detach(self.value, global_object); } @@ -322,7 +313,7 @@ impl ReadableStream { if let Some(byte_stream) = self.ptr.bytes() { if byte_stream.sink.get().is_none() { set_source(SourceHandle::ByteStream(byte_stream)); - byte_stream.parent_const().set_sink_owner(owner_cell); + byte_stream.parent().set_sink_owner(owner_cell); byte_stream.sink.set(sink); byte_stream.sink_paused.set(false); self.lock_native(global); @@ -381,11 +372,11 @@ impl ReadableStream { Some(_) | None => {} } set_source(SourceHandle::FileReader(file_reader)); - file_reader.parent_const().set_sink_owner(owner_cell); + file_reader.parent().set_sink_owner(owner_cell); file_reader.sink.set(sink); file_reader.sink_paused.set(true); self.lock_native(global); - file_reader.pull_into_sink(); + FileReader::pull_into_sink(file_reader.this_ptr()); return NativeWireResult::Wired; } } @@ -398,7 +389,6 @@ impl ReadableStream { } pub fn is_locked(&self, global_object: &JSGlobalObject) -> bool { - // SAFETY: FFI call; value is a valid ReadableStream JSValue. ReadableStream__isLocked(self.value, global_object) } @@ -421,19 +411,19 @@ impl ReadableStream { value, ptr: Source::JavaScript, }), + // tag == Blob ⇒ ptr is the non-null `NewSource` `m_ctx` from C++. Tag::Blob => Some(ReadableStream { value, - // SAFETY: tag == Blob ⇒ ptr is a non-null *ByteBlobLoader from C++. ptr: Source::Blob(ptr.cast::()), }), + // tag == File ⇒ ptr is the non-null `NewSource` `m_ctx` from C++. Tag::File => Some(ReadableStream { value, - // SAFETY: tag == File ⇒ ptr is a non-null *FileReader from C++. ptr: Source::File(ptr.cast::()), }), + // tag == Bytes ⇒ ptr is the non-null `NewSource` `m_ctx` from C++. Tag::Bytes => Some(ReadableStream { value, - // SAFETY: tag == Bytes ⇒ ptr is a non-null *ByteStream from C++. ptr: Source::Bytes(ptr.cast::()), }), _ => None, @@ -512,18 +502,16 @@ impl ReadableStream { }; match &store.data { webcore::blob::store::Data::Bytes(_) => { - let reader = NewSource::::new_mut(NewSource { - global_this: Some(bun_ptr::BackRef::new(global_this)), - context: ByteBlobLoader::default(), - ..Default::default() - }); - reader.context.setup(blob, recommended_chunk_size); + // The JS wrapper made by `to_readable_stream()` owns the source. + let reader = NewSource::new( + ByteBlobLoader::new(blob, recommended_chunk_size), + global_this, + ); reader.to_readable_stream(global_this) } webcore::blob::store::Data::File(_) => { - let reader = NewSource::::new_mut(NewSource { - global_this: Some(bun_ptr::BackRef::new(global_this)), - context: FileReader { + let reader = NewSource::new( + FileReader { event_loop: core::cell::Cell::new(jsc::EventLoopHandle::init( global_this.bun_vm().as_mut().event_loop().cast(), )), @@ -536,8 +524,8 @@ impl ReadableStream { lazy: bun_jsc::JsCell::new(webcore::file_reader::Lazy::Blob(store.clone())), ..Default::default() }, - ..Default::default() - }); + global_this, + ); reader.to_readable_stream(global_this) } webcore::blob::store::Data::S3(s3) => { @@ -575,30 +563,31 @@ impl ReadableStream { _parent: P, buffered_reader: &mut bun_io::BufferedReader, ) -> JsResult { - let source = NewSource::::new_mut(NewSource { - global_this: Some(bun_ptr::BackRef::new(global_this)), - context: FileReader { + // The JS wrapper made by `to_readable_stream()` owns the source. + let source = NewSource::new( + FileReader { event_loop: core::cell::Cell::new(jsc::EventLoopHandle::init( global_this.bun_vm().as_mut().event_loop().cast(), )), ..Default::default() }, - ..Default::default() - }); - let ctx_ptr: *mut FileReader = &raw mut source.context; + global_this, + ); + // The reader's parent is the source (see `FileReader`'s + // `impl_buffered_reader_parent!`). + let parent = source.this_ptr().as_ptr().cast::(); source .context - .reader() - .from(buffered_reader, ctx_ptr.cast::()); + .reader + .with_mut(|r| r.from(buffered_reader, parent)); let stream = source.to_readable_stream(global_this)?; - // The transferred poll's owner now points into this box; root the - // wrapper before JS can GC it. `on_start` skips a second ref via the - // same `waiting_for_on_reader_done` flag; `on_reader_done` releases. - if !source.context.reader().is_done() { - source.context.waiting_for_on_reader_done.set(true); - source.increment_count(); + // The transferred poll's owner now points into this source; hold a + // reference (which roots the wrapper) until `on_reader_done` releases it. + // `on_start` sees the held reference and does not take a second one. + if !source.context.reader.get().is_done() { + source.context.read_ref.set(Some(source.retain())); } Ok(stream) @@ -628,31 +617,21 @@ impl ReadableStream { } pub fn empty(global_this: &JSGlobalObject) -> JsResult { - bun_jsc::from_js_host_call(global_this, || { - // SAFETY: FFI call into JSC bindings; global_this is a valid &JSGlobalObject. - ReadableStream__empty(global_this) - }) + bun_jsc::from_js_host_call(global_this, || ReadableStream__empty(global_this)) } pub fn used(global_this: &JSGlobalObject) -> JsResult { - bun_jsc::from_js_host_call(global_this, || { - // SAFETY: FFI call into JSC bindings; global_this is a valid &JSGlobalObject. - ReadableStream__used(global_this) - }) + bun_jsc::from_js_host_call(global_this, || ReadableStream__used(global_this)) } /// A stream already in the `errored` state, so every read rejects with /// `reason` instead of closing cleanly. pub fn errored(global_this: &JSGlobalObject, reason: JSValue) -> JsResult { - bun_jsc::from_js_host_call(global_this, || { - // SAFETY: FFI call into JSC bindings; global_this is a valid &JSGlobalObject. - ReadableStream__errored(global_this, reason) - }) + bun_jsc::from_js_host_call(global_this, || ReadableStream__errored(global_this, reason)) } } pub(crate) fn is_disturbed_value(value: JSValue, global_object: &JSGlobalObject) -> bool { - // SAFETY: FFI call; value may be any JSValue (C++ side checks). ReadableStream__isDisturbed(value, global_object) } @@ -698,7 +677,9 @@ bun_core::assert_ffi_discr!( ); // Clone/Copy: bitwise OK — variant pointers are non-owning handles to -// JSC-managed loader objects (lifetime governed by the stream/JS heap). +// JSC-managed loader objects (lifetime governed by the stream/JS heap). They +// are the JS wrapper's `m_ctx` (`*mut NewSource`, whose `context` sits at +// offset 0). #[derive(Copy, Clone)] pub enum Source { Invalid, @@ -724,9 +705,6 @@ impl Source { /// BACKREF outlives-holder invariant. R-2: every `ByteStream` field touched /// through this borrow is `Cell`/`JsCell`-backed, so re-entrant JS that /// re-derives a fresh `&ByteStream` from `m_ctx` aliases shared-only. - /// - /// Centralises the per-site raw-pointer deref so call sites are - /// unsafe-free; the one audited deref lives in [`bun_ptr::BackRef::get`]. #[inline] pub fn bytes(self) -> Option> { match self { @@ -737,13 +715,7 @@ impl Source { } } - /// Shared borrow of the `File` payload as a [`BackRef`](bun_ptr::BackRef). - /// - /// Same invariant as [`bytes`](Self::bytes): the pointer is the JS - /// wrapper's `m_ctx` heap allocation, non-null and live while the owning - /// `ReadableStream` JSValue is rooted. R-2: every `FileReader` field - /// touched through this borrow is `Cell`/`JsCell`-backed, so re-entrant JS - /// that re-derives a fresh `&FileReader` from `m_ctx` aliases shared-only. + /// Shared borrow of the `File` payload; same invariant as [`bytes`](Self::bytes). #[inline] pub fn file(self) -> Option> { match self { @@ -753,6 +725,17 @@ impl Source { _ => None, } } + + /// Shared borrow of the `Blob` payload; same invariant as [`bytes`](Self::bytes). + #[inline] + pub fn blob(self) -> Option> { + match self { + Source::Blob(p) => Some(bun_ptr::BackRef::from( + NonNull::new(p).expect("Source::Blob payload is non-null"), + )), + _ => None, + } + } } // ─── NewSource ─────────────────────────────────────────────────────────────── @@ -761,6 +744,10 @@ impl Source { // the generic struct over it. /// Per-context configuration and callbacks for `NewSource`. +/// +/// R-2: every callback takes `&self`; contexts keep their mutable state in +/// `Cell`/`JsCell` so a re-entrant JS call that re-derives `&Self` from the +/// wrapper's `m_ctx` aliases shared-only. pub trait SourceContext: Sized { /// `name_` — used to look up `jsc.Codegen.JS{NAME}InternalReadableStreamSource`. const NAME: &'static str; @@ -787,29 +774,26 @@ pub trait SourceContext: Sized { /// `js_${NAME}InternalReadableStreamSource::sink_owner_set_cached` fn js_sink_owner_set_cached(this: JSValue, global: &JSGlobalObject, value: JSValue); - fn on_start(&mut self) -> streams::Start; - fn on_pull(&mut self, buf: &mut [u8], view: JSValue) -> streams::Result; - fn on_cancel(&mut self); + fn on_start(&self) -> streams::Start; + fn on_pull(&self, buf: &mut [u8], view: JSValue) -> streams::Result; + fn on_cancel(&self); /// Per-context teardown side-effects (unref pollers, flush pending callbacks, - /// release handles). **Must NOT free the enclosing `NewSource` allocation** — - /// that is done by the caller ([`NewSource::decrement_count`]) *after* this - /// returns, via `Box::from_raw`, which then runs `Drop` on every field. Freeing - /// here would deallocate the storage backing the live `&mut self` borrow (UAF). - fn deinit_fn(&mut self); + /// release handles), run from `NewSource`'s `Drop` before the fields drop. + fn deinit_fn(&self); - fn finalize_detach(&mut self) -> bool { - false - } + /// The JS wrapper is being finalized: release any reference the context + /// itself still holds on the source (an in-flight read's). + fn finalize_detach(&self) {} /// The JS wrapper was collected while native refs remain. Runs inside a GC /// sweep: no JS. `ByteStream` tells a parked producer nobody can read it now. - fn wrapper_finalized(&mut self) {} + fn wrapper_finalized(&self) {} /// `setRefUnrefFn` — default no-op. - fn set_ref_unref(&mut self, _enable: bool) {} + fn set_ref_unref(&self, _enable: bool) {} /// `drainInternalBuffer` — default returns empty. - fn drain_internal_buffer(&mut self) -> Vec { + fn drain_internal_buffer(&self) -> Vec { Vec::::default() } @@ -820,7 +804,7 @@ pub trait SourceContext: Sized { /// `toBufferedValue` — `None` ⇒ "not implemented" (caller throws TODO). fn to_buffered_value( - &mut self, + &self, _global_this: &JSGlobalObject, _action: streams::BufferActionTag, ) -> Option> { @@ -830,12 +814,12 @@ pub trait SourceContext: Sized { /// Returns `None` if the context type does not support raw mode. /// The `None` default is only reachable if codegen wires `setRawMode` for /// a context that does not implement it (see `set_raw_mode_from_js`). - fn set_raw_mode(&mut self, _flag: bool) -> Option> { + fn set_raw_mode(&self, _flag: bool) -> Option> { None } /// Default no-op. - fn set_flowing(&mut self, _flag: bool) {} + fn set_flowing(&self, _flag: bool) {} } // Hand-wired JSC class (the `#[bun_jsc::JsClass]` derive cannot be used on a @@ -848,33 +832,36 @@ pub trait SourceContext: Sized { // [`ReadableStream::from_js`] casts that straight to `*mut C`. // With Rust's default repr the field is reordered and the cast reads // adjacent fields as the loader, returning empty bodies. +// +// Refcounted: the JS wrapper owns the initial reference (released in +// [`NewSource::finalize`]); producers and in-flight reads hold [`SourceRef`]s. +// When the count reaches zero the context's `deinit_fn` runs and the +// allocation is freed. #[repr(C)] +#[derive(bun_ptr::CellRefCounted)] pub struct NewSource { pub context: C, - pub cancelled: bool, - pub ref_count: u32, - pub pending_err: Option, - pub close_handler: Option)>, - /// Borrowed opaque context for native `close_handler`s (never - /// owned/freed here). The JS path stores - /// `on_js_close` and leaves this `None` — see [`Self::on_close`]. - pub close_ctx: Option>, + self_root: bun_ptr::SelfRoot, + pub cancelled: Cell, + pub ref_count: Cell, + /// `set_on_close_from_js` ran since the last `on_close`: the next close + /// fires the JS `onclose` callback. + js_close_armed: Cell, /// Upstream producer to notify on cancel/drain/consumer-attach. Replaces /// the per-signal fn-ptr + ctx-ptr pairs with one typed handle. pub producer: Cell, - // JSC_BORROW: process-lifetime VM global. Heap m_ctx field reassigned in - // `start()` from a fresh `&JSGlobalObject`; `BackRef` gives a safe `Deref` - // projection without propagating a lifetime parameter into FFI codegen. - pub global_this: Option>, + // JSC_BORROW: process-lifetime VM global. Reassigned in `start()` from a + // fresh `&JSGlobalObject`; `BackRef` gives a safe `Deref` projection + // without propagating a lifetime parameter into FFI codegen. + global_this: jsc::JsCell>, /// Back-reference to the owning `JS{Blob,Bytes,File}InternalReadableStreamSource` /// wrapper. Starts `Weak` (set in [`Self::to_readable_stream`]), is - /// [`JsRef::upgrade`]d to `Strong` in [`Self::increment_count`] while a - /// native I/O ref is held (FileReader `waiting_for_on_reader_done`), and - /// [`JsRef::downgrade`]d back to `Weak` in [`Self::decrement_count`] when - /// only the wrapper's own ref remains. [`Self::finalize`] flips it to - /// `Finalized` so [`Self::on_js_close`] reads `None` instead of a - /// dead-but-unswept cell. - pub this_jsvalue: jsc::JsRef, + /// [`JsRef::upgrade`]d to `Strong` in [`Self::retain`] while a native + /// reference is held (FileReader's in-flight read), and + /// [`JsRef::downgrade`]d back to `Weak` when only the wrapper's own ref + /// remains. [`Self::finalize`] flips it to `Finalized` so + /// [`Self::on_js_close`] reads `None` instead of a dead-but-unswept cell. + pub this_jsvalue: jsc::JsCell, /// The producer holding a native ref has parked ([`Self::unroot_wrapper`]): /// its ref keeps this allocation, not the wrapper, so an unread stream can /// be collected. Cleared by [`Self::root_wrapper`]. @@ -885,21 +872,42 @@ pub struct NewSource { pub is_closed: Cell, } -impl Default for NewSource { - fn default() -> Self { - Self { - context: C::default(), - cancelled: false, - ref_count: 1, - pending_err: None, - close_handler: None, - close_ctx: None, - producer: Cell::new(streams::SourceHandle::None), - global_this: None, - this_jsvalue: jsc::JsRef::empty(), - wrapper_unrooted: Cell::new(false), - is_closed: Cell::new(false), - } +/// A counted reference to a [`NewSource`]: keeps the allocation alive and, +/// unless the producer parked it ([`NewSource::unroot_wrapper`]), the JS +/// wrapper rooted. Released on drop. +pub struct SourceRef(bun_ptr::BackRef, bun_ptr::Root>); + +impl SourceRef { + /// A reference on the `ByteStream` source behind `stream`, if that is what it is. + pub fn byte_stream(stream: &ReadableStream) -> Option { + stream.ptr.bytes().map(|bytes| bytes.parent().retain()) + } +} + +impl Clone for SourceRef { + fn clone(&self) -> Self { + self.0.retain() + } +} + +impl core::ops::Deref for SourceRef { + type Target = NewSource; + #[inline] + fn deref(&self) -> &NewSource { + self.0.get() + } +} + +impl Drop for SourceRef { + fn drop(&mut self) { + self.0.will_release_ref(); + as bun_ptr::CellRefCounted>::deref_nn(self.0.this_ptr().into()); + } +} + +impl Drop for NewSource { + fn drop(&mut self) { + self.context.deinit_fn(); } } @@ -909,7 +917,6 @@ impl Default for NewSource { // The `.classes.ts` → `.rs` generator (when re-run with Rust output) is expected // to emit those `const JS_*` bindings directly. pub(crate) trait NewSourceCodegen { - fn to_js(&mut self, global_this: &JSGlobalObject) -> JSValue; fn pending_promise_set_cached(this: JSValue, global: &JSGlobalObject, value: JSValue); fn on_drain_callback_set_cached(this: JSValue, global: &JSGlobalObject, value: JSValue); fn on_drain_callback_get_cached(this: JSValue) -> Option; @@ -991,15 +998,6 @@ macro_rules! source_context_codegen { } impl NewSourceCodegen for NewSource { - fn to_js(&mut self, global_this: &JSGlobalObject) -> JSValue { - // `self` is a heap-allocated `NewSource` produced by [`NewSource::new`] - // (`heap::alloc`); ownership transfers to the JS wrapper as `m_ctx`. C++ side - // stores it as `void*` and the GC finalizer drives `decrement_count` → `deinit`. - C::js_create( - std::ptr::from_mut::(self).cast::(), - global_this, - ) - } fn pending_promise_set_cached(this: JSValue, global: &JSGlobalObject, value: JSValue) { C::js_pending_promise_set_cached(this, global, value) } @@ -1029,11 +1027,42 @@ const _: () = assert!(core::mem::offset_of!(NewSource, context) == 0 const _: () = assert!(core::mem::offset_of!(NewSource, context) == 0); impl NewSource { + /// Heap-allocate a source around `context`. The returned reference is the + /// one the JS wrapper takes over in [`Self::to_readable_stream`] (and + /// releases in [`Self::finalize`]); until then the caller holds it. + pub fn new(context: C, global_this: &JSGlobalObject) -> RefPtr { + RefPtr::new_cyclic(|self_root| NewSource { + context, + self_root, + cancelled: Cell::new(false), + ref_count: Cell::new(1), + js_close_armed: Cell::new(false), + producer: Cell::new(streams::SourceHandle::None), + global_this: jsc::JsCell::new(bun_ptr::BackRef::new(global_this)), + this_jsvalue: jsc::JsCell::new(jsc::JsRef::empty()), + wrapper_unrooted: Cell::new(false), + is_closed: Cell::new(false), + }) + } + + /// This source as a dispatch handle (root provenance). + #[inline] + pub fn this_ptr(&self) -> bun_ptr::ThisPtr { + self.self_root.this_ptr(self) + } + + /// The `context` pointer C++ and [`Source`] identify this source by + /// (`context` is at offset 0, so it is the allocation root). + #[inline] + pub fn as_context_ptr(&self) -> *mut C { + self.this_ptr().as_ptr().cast::() + } + /// Point the `owner` slot at the GC cell of the peer producing into this /// source (its `producer` backref), so rooting the source roots the /// producer. `JSValue::UNDEFINED` clears; no-op without a JS wrapper. pub fn set_owner(&self, value: JSValue) { - if let Some(this) = self.this_jsvalue.try_get() { + if let Some(this) = self.this_jsvalue.get().try_get() { ::owner_set_cached(this, self.global_this(), value); } } @@ -1041,116 +1070,67 @@ impl NewSource { /// Same as [`Self::set_owner`] for the `sinkOwner` slot: roots the peer /// this source pipes into (its `sink` backref). pub fn set_sink_owner(&self, value: JSValue) { - if let Some(this) = self.this_jsvalue.try_get() { + if let Some(this) = self.this_jsvalue.get().try_get() { ::sink_owner_set_cached(this, self.global_this(), value); } } - /// Safe `&JSGlobalObject` accessor for the JSC_BORROW `global_this` - /// back-pointer. `global_this` is stored from a live `&JSGlobalObject` at - /// construction (or reassigned in `start()` from a fresh live one); the - /// VM-owned global outlives every `NewSource` it owns. + /// The JSC_BORROW `global_this` back-pointer (set at construction, + /// reassigned in `start()`); the VM-owned global outlives every source. #[inline] pub fn global_this(&self) -> &JSGlobalObject { - self.global_this - .as_ref() - .expect("NewSource.global_this used before init") - .get() + self.global_this.get().get() } - /// Heap-allocate and hand back the raw pointer. - /// - /// Ownership is **not** retained by Rust: the returned pointer is intended to - /// be installed as the JS wrapper's `m_ctx` via [`Self::to_readable_stream`] - /// (or [`NewSourceCodegen::to_js`]), after which the GC finalizer drives - /// teardown through [`Self::decrement_count`] → context `deinit_fn` → - /// [`Self::deinit`]. Dropping a `Box` here would free the allocation while - /// the JS cell still points at it (UAF), so this returns `*mut Self`. - pub fn new(init: Self) -> *mut Self { - bun_core::heap::into_raw(Box::new(init)) - } - - /// Inverse of the `*mut Self as *mut C` cast [`ReadableStream::from_js`] - /// performs: `ctx` must be that pointer (whole-allocation provenance), not - /// one derived from a `&C`/`&mut C` — use the context's - /// `impl_field_parent!` accessors for those. #[inline] - pub unsafe fn from_context_ptr(ctx: *mut C) -> *mut Self { - // SAFETY: caller contract. - unsafe { bun_core::from_field_ptr!(Self, context, ctx) } + pub fn set_global_this(&self, global: &JSGlobalObject) { + self.global_this.set(bun_ptr::BackRef::new(global)); } - /// [`Self::new`] returning the leaked allocation as an unbounded `&mut`. - /// - /// Every call site of `new()` immediately did `unsafe { &mut *p }` to set - /// up the context and then handed ownership to the JS wrapper via - /// [`Self::to_readable_stream`]. Centralising that deref here (one - /// audited `unsafe`, N safe callers) — the allocation is fresh, non-null, - /// uniquely owned, and outlives the returned borrow because the JS GC - /// finalizer (not Rust `Drop`) reclaims it via [`Self::decrement_count`]. - #[inline] - pub fn new_mut<'a>(init: Self) -> &'a mut Self { - // SAFETY: `heap::into_raw(Box::new(..))` is non-null, aligned, and the - // sole pointer to a fresh allocation; forming `&mut` is unique. - // Ownership transfers to the JS wrapper's `m_ctx`, so the unbounded - // lifetime is correct (no Rust owner will drop underneath the borrow). - unsafe { &mut *Self::new(init) } - } - - pub fn set_ref(&mut self, value: bool) { + pub fn set_ref(&self, value: bool) { if C::SUPPORTS_REF { self.context.set_ref_unref(value); } } - pub fn on_pull_from_js(&mut self, buf: &mut [u8], view: JSValue) -> streams::Result { + pub fn on_pull_from_js(&self, buf: &mut [u8], view: JSValue) -> streams::Result { self.context.on_pull(buf, view) } - pub fn on_start_from_js(&mut self) -> streams::Start { + pub fn on_start_from_js(&self) -> streams::Start { self.context.on_start() } - pub fn cancel(&mut self) { - if self.cancelled { + pub fn cancel(&self) { + if self.cancelled.get() { return; } - self.cancelled = true; + self.cancelled.set(true); self.context.on_cancel(); let mut p = self.producer.replace(streams::SourceHandle::None); p.close(None); } - pub fn on_close(&mut self) { - if self.cancelled { + pub fn on_close(&self) { + if self.cancelled.get() { return; } - if let Some(close) = self.close_handler.take() { - // Identity check against the *exact* fn pointer stored by `set_on_close_from_js`, so the - // JS path receives `self` (not `close_ctx`, which is unset on that path). - if close as usize == Self::on_js_close as fn(Option<*mut c_void>) as usize { - Self::on_js_close(Some(std::ptr::from_mut(self).cast::())); - } else { - close(self.close_ctx.map(|p| p.as_ptr())); - } + if self.js_close_armed.replace(false) { + self.on_js_close(); } } - /// `JSReadableStreamSource.onClose` — invoked via `close_handler` when the - /// JS side registered an `onclose` callback. Stored *directly* in - /// `close_handler` by [`Self::set_on_close_from_js`] so the fn-pointer - /// identity check above matches. - fn on_js_close(ptr: Option<*mut c_void>) { - // SAFETY: ptr was set to `self as *mut NewSource` in on_close()/set_on_close_from_js. - let this = unsafe { &mut *(ptr.unwrap().cast::>()) }; + /// `JSReadableStreamSource.onClose` — invoked from `on_close` when the JS + /// side registered an `onclose` callback ([`Self::set_on_close_from_js`]). + fn on_js_close(&self) { // Reached from `FileReader::on_reader_done` off the event loop. While - // the across-read ref is held (`increment_count` upgraded to Strong), - // the wrapper is rooted and `try_get()` is `Some`. If the wrapper was + // the across-read ref is held (`retain` upgraded to Strong), the + // wrapper is rooted and `try_get()` is `Some`. If the wrapper was // already finalized, `try_get()` is `None` and there is no callback. - let Some(this_jsvalue) = this.this_jsvalue.try_get() else { + let Some(this_jsvalue) = self.this_jsvalue.get().try_get() else { return; }; - let global_this = this.global_this(); + let global_this = self.global_this(); if let Some(cb) = ::on_close_callback_get_cached(this_jsvalue) { if !cb.is_undefined() { global_this.queue_microtask(cb, &[]); @@ -1163,138 +1143,90 @@ impl NewSource { ); } - pub fn increment_count(&mut self) { - self.ref_count += 1; - // A ref beyond the JS wrapper's own is held (in practice a FileReader - // `waiting_for_on_reader_done` I/O ref). Root the wrapper so - // `on_js_close`, reached from `on_reader_done` off the event loop with - // no JS frame on the stack, never reads a dead-but-unswept cell. + /// Take a counted reference. A ref beyond the JS wrapper's own is now + /// held (in practice a FileReader's in-flight read): root the wrapper so + /// `on_js_close`, reached from `on_reader_done` off the event loop with no + /// JS frame on the stack, never reads a dead-but-unswept cell. + pub fn retain(&self) -> SourceRef { + self.ref_(); if !self.wrapper_unrooted.get() { - // SAFETY: `self` is live for the call. - unsafe { Self::upgrade_wrapper(self) }; + self.upgrade_wrapper(); } + SourceRef(self.self_root.backref(self)) } - /// # Safety - /// `this` points at a live `NewSource`. - unsafe fn upgrade_wrapper(this: *mut Self) { - // SAFETY: fn contract; field places only, see `unroot_wrapper`. - unsafe { - if let Some(global) = (*this).global_this.as_deref() { - if (*this).this_jsvalue.is_not_empty() { - (*this).this_jsvalue.upgrade(global); - } + fn upgrade_wrapper(&self) { + let global = self.global_this(); + self.this_jsvalue.with_mut(|this_jsvalue| { + if this_jsvalue.is_not_empty() { + this_jsvalue.upgrade(global); } - } + }); } /// The producer keeps its native ref but stops rooting the wrapper: nothing /// is reading, so the stream should be collectable. [`SourceContext::wrapper_finalized`] /// tells the producer if that happens. - /// - /// Takes a raw pointer: the producer reaches this while it holds a `&C` into - /// `this` (the chunk it is delivering to), so only the fields written here - /// are touched, never a `&mut Self` that would cover the context too. - /// - /// # Safety - /// `this` points at a live `NewSource`. - pub unsafe fn unroot_wrapper(this: *mut Self) { - // SAFETY: fn contract. - unsafe { - (*this).wrapper_unrooted.set(true); - (*this).this_jsvalue.downgrade(); - } + pub fn unroot_wrapper(&self) { + self.wrapper_unrooted.set(true); + self.this_jsvalue.with_mut(jsc::JsRef::downgrade); } /// Undo [`Self::unroot_wrapper`]: a consumer is reading again. - /// - /// # Safety - /// As [`Self::unroot_wrapper`]. - pub unsafe fn root_wrapper(this: *mut Self) { - // SAFETY: fn contract. - unsafe { - (*this).wrapper_unrooted.set(false); - if (*this).ref_count > 1 { - Self::upgrade_wrapper(this); - } + pub fn root_wrapper(&self) { + self.wrapper_unrooted.set(false); + if self.ref_count.get() > 1 { + self.upgrade_wrapper(); } } - /// Release one reference. If the count hits zero, runs context teardown and - /// **frees the allocation**. - /// - /// Takes a raw pointer (not `&mut self`) because the zero-refcount path - /// deallocates `*this`; holding a live `&mut Self` across that drop would be - /// a dangling-reference UAF (Stacked Borrows: protected tag on freed memory). - /// - /// SAFETY: `this` must point at a live `NewSource` produced by - /// [`Self::new`] (i.e. `Box::into_raw`). Caller must not dereference `this` - /// — nor any interior pointer such as `&mut context` — after this returns. - pub unsafe fn decrement_count(this: *mut Self) -> u32 { - // SAFETY: caller contract — `this` is live for the duration of this block. - let remaining = unsafe { - let r = &mut (*this).ref_count; - #[cfg(debug_assertions)] - if *r == 0 { - panic!("Attempted to decrement ref count below zero"); - } - *r -= 1; - *r - }; - if remaining == 1 { - // Only the JS wrapper's own ref remains: drop the Strong root so - // the wrapper becomes collectable again. - // SAFETY: caller contract — `this` is live while remaining > 0. - unsafe { (*this).this_jsvalue.downgrade() }; - } - if remaining == 0 { - // SAFETY: still live; run side-effect teardown while fields are valid. - unsafe { - (*this).context.deinit_fn(); - } - // SAFETY: `this` originated from `Box::into_raw` in `Self::new`. No - // `&mut` borrow of `*this` is live at this point — reclaim and drop, - // which runs `Drop` on `context` and all other fields, then frees. - drop(unsafe { bun_core::heap::take(this) }); - return 0; + /// Bookkeeping ahead of releasing one reference: once only the JS wrapper's + /// own ref will remain, drop the Strong root so the wrapper becomes + /// collectable again. + fn will_release_ref(&self) { + let rc = self.ref_count.get(); + debug_assert!(rc > 0, "Attempted to decrement ref count below zero"); + if rc == 2 { + self.this_jsvalue.with_mut(jsc::JsRef::downgrade); } - remaining } - pub fn drain(&mut self) -> Vec { + pub fn drain(&self) -> Vec { self.context.drain_internal_buffer() } fn to_readable_stream_with( - &mut self, + &self, global_this: &JSGlobalObject, from_native: fn(&JSGlobalObject, JSValue) -> JsResult, ) -> JsResult { - let out_value = if let Some(v) = self.this_jsvalue.try_get() { + let out_value = if let Some(v) = self.this_jsvalue.get().try_get() { v } else { - ::to_js(self, global_this) + // The wrapper's `m_ctx` takes over the initial reference; the GC + // finalizer drives teardown through `finalize`. + C::js_create(self.this_ptr().as_ptr().cast::(), global_this) }; out_value.ensure_still_alive(); - if self.this_jsvalue.is_empty() { - self.this_jsvalue = jsc::JsRef::init_weak(out_value); + if self.this_jsvalue.get().is_empty() { + self.this_jsvalue.set(jsc::JsRef::init_weak(out_value)); } from_native(global_this, out_value) } - pub(crate) fn to_readable_stream(&mut self, global_this: &JSGlobalObject) -> JsResult { + pub(crate) fn to_readable_stream(&self, global_this: &JSGlobalObject) -> JsResult { self.to_readable_stream_with(global_this, ReadableStream::from_native) } pub(crate) fn to_text_readable_stream( - &mut self, + &self, global_this: &JSGlobalObject, ) -> JsResult { self.to_readable_stream_with(global_this, ReadableStream::from_native_text) } pub fn set_raw_mode_from_js( - this: &mut Self, + this: &Self, global: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { @@ -1308,7 +1240,7 @@ impl NewSource { } pub fn set_flowing_from_js( - this: &mut Self, + this: &Self, _global: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { @@ -1321,11 +1253,6 @@ impl NewSource { pub fn memory_cost(&self) -> usize { self.context.memory_cost_fn() + core::mem::size_of::() } - - // Teardown is folded into [`Self::decrement_count`]'s - // zero-refcount path. A `&mut self` deinit here would free the storage - // backing the live `self` borrow (dangling UAF), so the drop is performed - // there via raw `*mut Self` instead. } // ─── codegen-facing inherent methods ───────────────────────────────────────── @@ -1333,7 +1260,7 @@ impl NewSource { // `NewSource` (aliased as `{Blob,Bytes,File}InternalReadableStreamSource`). impl NewSource { pub fn pull_from_js( - &mut self, + &self, global_this: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { @@ -1348,11 +1275,11 @@ impl NewSource { } pub fn start_from_js( - &mut self, + &self, global_this: &JSGlobalObject, _call_frame: &CallFrame, ) -> JsResult { - self.global_this = Some(bun_ptr::BackRef::new(global_this)); + self.set_global_this(global_this); match self.on_start_from_js() { streams::Start::Empty => Ok(JSValue::js_number(0.0)), streams::Start::Ready => Ok(JSValue::js_number(16384.0)), @@ -1362,7 +1289,7 @@ impl NewSource { } } - pub fn get_is_closed_from_js(&mut self, _global_object: &JSGlobalObject) -> JSValue { + pub fn get_is_closed_from_js(&self, _global_object: &JSGlobalObject) -> JSValue { JSValue::from(self.is_closed.get()) } @@ -1398,7 +1325,7 @@ impl NewSource { } pub fn cancel_from_js( - &mut self, + &self, _global_object: &JSGlobalObject, _call_frame: &CallFrame, ) -> JsResult { @@ -1407,18 +1334,15 @@ impl NewSource { } pub fn set_on_close_from_js( - &mut self, + &self, global_object: &JSGlobalObject, value: JSValue, ) -> JsResult<()> { - // Store the handler by *identity* — `NewSource::on_close` compares the - // stored fn pointer against `on_js_close` to decide whether to pass - // `self` (JS path) or `close_ctx` (native path). - self.close_handler = Some(Self::on_js_close); - self.global_this = Some(bun_ptr::BackRef::new(global_object)); + self.js_close_armed.set(true); + self.set_global_this(global_object); if value.is_undefined() { - if let Some(this_jsvalue) = self.this_jsvalue.try_get() { + if let Some(this_jsvalue) = self.this_jsvalue.get().try_get() { ::on_close_callback_set_cached( this_jsvalue, global_object, @@ -1436,7 +1360,7 @@ impl NewSource { )); } let cb = value.with_async_context_if_needed(global_object); - if let Some(this_jsvalue) = self.this_jsvalue.try_get() { + if let Some(this_jsvalue) = self.this_jsvalue.get().try_get() { ::on_close_callback_set_cached( this_jsvalue, global_object, @@ -1447,13 +1371,13 @@ impl NewSource { } pub fn set_on_drain_from_js( - &mut self, + &self, global_object: &JSGlobalObject, value: JSValue, ) -> JsResult<()> { - self.global_this = Some(bun_ptr::BackRef::new(global_object)); + self.set_global_this(global_object); - let Some(this_jsvalue) = self.this_jsvalue.try_get() else { + let Some(this_jsvalue) = self.this_jsvalue.get().try_get() else { return Ok(()); }; @@ -1478,8 +1402,8 @@ impl NewSource { Ok(()) } - pub fn get_on_close_from_js(&mut self, _global_object: &JSGlobalObject) -> JSValue { - if let Some(this_jsvalue) = self.this_jsvalue.try_get() { + pub fn get_on_close_from_js(&self, _global_object: &JSGlobalObject) -> JSValue { + if let Some(this_jsvalue) = self.this_jsvalue.get().try_get() { if let Some(val) = ::on_close_callback_get_cached(this_jsvalue) { @@ -1489,15 +1413,16 @@ impl NewSource { JSValue::UNDEFINED } - pub fn get_on_drain_from_js(&mut self, _global_object: &JSGlobalObject) -> JSValue { + pub fn get_on_drain_from_js(&self, _global_object: &JSGlobalObject) -> JSValue { self.this_jsvalue + .get() .try_get() .and_then(::on_drain_callback_get_cached) .unwrap_or(JSValue::UNDEFINED) } pub fn update_ref_from_js( - &mut self, + &self, _global_object: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { @@ -1506,31 +1431,23 @@ impl NewSource { Ok(JSValue::UNDEFINED) } + /// The JS wrapper's finalizer: release its reference. Producer / in-flight + /// read references may keep the allocation past this call. pub fn finalize(self: Box) { - // Refcounted: `decrement_count` releases the JS wrapper's +1; allocation - // may outlive this call if other refs remain, so hand ownership back to - // the raw refcount via a raw pointer (the call may free `*this`). - let this = Box::into_raw(self); - // SAFETY: `this` is live — just unwrapped from `Box`. - unsafe { (*this).this_jsvalue.finalize() }; - // SAFETY: `this` is live; the JS-wrapper +1 (released last) keeps ref_count > 0 - // across whatever ref the producer drops in response. - unsafe { - if (*this).ref_count > 1 { - (*this).context.wrapper_finalized(); - } - } - // SAFETY: `this` is live; the JS-wrapper ref below still pins the count. - if unsafe { (*this).context.finalize_detach() } { - // SAFETY: `this` is live; the JS-wrapper +1 (released below) keeps ref_count > 0. - let _ = unsafe { Self::decrement_count(this) }; + let this: &Self = Box::leak(self); + let wrapper_ref = SourceRef(this.self_root.backref(this)); + this.this_jsvalue.with_mut(jsc::JsRef::finalize); + // The wrapper's reference (released last) keeps ref_count > 0 across + // whatever ref the producer or context drops in response. + if this.ref_count.get() > 1 { + this.context.wrapper_finalized(); } - // SAFETY: `this` came from `Box::into_raw`; not accessed after. - let _ = unsafe { Self::decrement_count(this) }; + this.context.finalize_detach(); + drop(wrapper_ref); } pub fn drain_from_js( - &mut self, + &self, global_this: &JSGlobalObject, _call_frame: &CallFrame, ) -> JsResult { @@ -1550,7 +1467,7 @@ impl NewSource { // text/arrayBuffer/blob/bytes/json all share the same body modulo // `BufferActionTag`. Collapsed into one helper to avoid 5× drift. fn to_buffered_value_from_js( - &mut self, + &self, global_this: &JSGlobalObject, _call_frame: &CallFrame, action: streams::BufferActionTag, @@ -1562,7 +1479,7 @@ impl NewSource { } pub fn text_from_js( - &mut self, + &self, global_this: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { @@ -1570,7 +1487,7 @@ impl NewSource { } pub fn array_buffer_from_js( - &mut self, + &self, global_this: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { @@ -1582,7 +1499,7 @@ impl NewSource { } pub fn blob_from_js( - &mut self, + &self, global_this: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { @@ -1590,7 +1507,7 @@ impl NewSource { } pub fn bytes_from_js( - &mut self, + &self, global_this: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { @@ -1598,7 +1515,7 @@ impl NewSource { } pub fn json_from_js( - &mut self, + &self, global_this: &JSGlobalObject, call_frame: &CallFrame, ) -> JsResult { diff --git a/src/runtime/webcore/Request.rs b/src/runtime/webcore/Request.rs index 8ceaadd119dd..3d53d6b121ae 100644 --- a/src/runtime/webcore/Request.rs +++ b/src/runtime/webcore/Request.rs @@ -1,7 +1,6 @@ //! https://developer.mozilla.org/en-US/docs/Web/API/Request use core::cell::Cell; -use core::ptr::NonNull; use std::borrow::Cow; use bun_jsc::JsCell; @@ -10,7 +9,7 @@ use enumset::EnumSet; use super::response::HeadersRef; use crate::api::AnyRequestContext; use crate::webcore::BlobExt as _; -use crate::webcore::body::{self, BodyHiveHandle, BodyMixin, Value as BodyValue}; +use crate::webcore::body::{self, Body, BodyHiveHandle, BodyMixin, Value as BodyValue}; use crate::webcore::jsc::{ CallFrame, HTTPHeaderName, JSGlobalObject, JSValue, JsError, JsRef, JsResult, }; @@ -31,12 +30,19 @@ use bun_jsc::EncodedSliceJsc as _; use bun_jsc::StringJsc as _; use bun_jsc::generated::JSRequest as js_gen; use bun_ptr::weak_ptr::WeakPtrData; -use core::mem::ManuallyDrop; impl bun_ptr::weak_ptr::HasWeakPtrData for Request { - unsafe fn weak_ptr_data(this: *mut Self) -> *mut WeakPtrData { - // SAFETY: caller guarantees `this` points to a live (possibly-finalized) allocation. - unsafe { core::ptr::addr_of_mut!((*this).weak_ptr_data) } + fn weak_ptr_data(&self) -> &WeakPtrData { + &self.weak_ptr_data + } + + /// The JS wrapper is gone but a `WeakRef` (RequestContext.request_weakref) + /// still holds the allocation: release inner resources now so they aren't + /// pinned until the last `WeakPtr` drops. + fn finalize_contents(&self) { + drop(self.body.take()); + self.js_ref.with_mut(|r| r.finalize()); + self.finalize_without_deinit(); } } pub(crate) type WeakRef = bun_ptr::WeakPtr; @@ -60,11 +66,7 @@ const _: () = { // `js_ref = .init_weak(...)`, and `check_body_stream_ref` — // otherwise the wrapper reports size 0 and any Locked-body // ReadableStream is never migrated into the GC slot. - let ptr = bun_core::heap::into_raw(Box::new(self)); - // SAFETY: `ptr` is a freshly-leaked heap allocation; the inherent - // `to_js` hands it to the C++ wrapper which takes ownership (freed - // via `RequestClass__finalize`). Same pattern as `do_clone`. - unsafe { Request::to_js(&*ptr, global) } + Request::to_js_boxed(Box::new(self), global) } fn get_constructor(global: &bun_jsc::JSGlobalObject) -> bun_jsc::JSValue { js::get_constructor(global) @@ -76,9 +78,8 @@ const _: () = { /// `&mut Request`) so re-entrant JS calls cannot stack two `&mut` to the same /// instance. Fields mutated by host-fns are wrapped in `Cell` (Copy scalars) /// or `JsCell` (Drop types). Both are `#[repr(transparent)]`, so `#[repr(C)]` -/// field layout is unchanged. `method`/`flags`/`request_context`/`body`/ -/// `weak_ptr_data` are only written during construction or via raw-ptr -/// `finalize`, so stay plain. +/// field layout is unchanged. `method`/`flags`/`request_context`/ +/// `weak_ptr_data` are only written during construction, so stay plain. #[repr(C)] pub struct Request { pub(crate) url: JsCell, @@ -91,10 +92,9 @@ pub struct Request { signal: JsCell>, /// Owning `+1` handle into the per-VM `Body::Value` hive pool. The /// `Request` and (when served by `Bun.serve`) the `RequestContext` each - /// hold their own `+1` on the same slot. `ManuallyDrop` because - /// `finalize()` decouples from `Box` and must release this handle exactly - /// once before `Box::from_raw().drop()` (which would otherwise re-run it). - body: ManuallyDrop, + /// hold their own `+1` on the same slot. `None` only once the JS wrapper + /// has been finalized ([`HasWeakPtrData::finalize_contents`]). + body: JsCell>, js_ref: JsCell, pub method: Method, pub(crate) flags: Flags, @@ -166,8 +166,8 @@ impl crate::webcore::body::BodyOwnerJs for Request { impl BodyMixin for Request { #[inline] - fn get_body_value(&self) -> &mut BodyValue { - Request::get_body_value(self) + fn body(&self) -> &Body { + Request::body(self) } #[inline] fn get_fetch_headers(&self) -> Option> { @@ -190,38 +190,28 @@ impl BodyMixin for Request { // ─── header accessors & simple getters ────────────────────────────────────── impl Request { - /// Inherent shim; `impl BodyMixin for Request` supplies the real trait method. - #[inline] - #[allow(clippy::mut_from_ref)] - pub(crate) fn get_body_value(&self) -> &mut BodyValue { - self.body_value_mut() + /// The pooled `Body` this request (and, under `Bun.serve`, its + /// `RequestContext`) share. + #[inline(always)] + pub(crate) fn body(&self) -> &Body { + self.body + .get() + .as_deref() + .expect("Request body used after finalize") } - /// Immutable view of the body value. + /// The pooled body's `Value` slot. #[inline] - fn body_value(&self) -> &BodyValue { - &self.body + pub(crate) fn body_value(&self) -> &JsCell { + &self.body().value } - /// R-2: `&self` → `&mut` through the slot's raw pointer. The slot is shared - /// with `RequestContext.request_body` but never `&mut`-borrowed concurrently - /// (single-threaded event-loop sequencing). Keep the borrow short. + /// The cached headers as the `&mut` the C++ accessors take (opaque ZST + /// handle — see [`HeadersRef::headers`]). #[inline] #[allow(clippy::mut_from_ref)] - fn body_value_mut(&self) -> &mut BodyValue { - // SAFETY: see R-2 invariant above. - unsafe { &mut (*self.body.as_ptr()).value } - } - - /// R-2: short-hand for `unsafe { self.headers.get_mut() }`. The - /// single-JS-thread invariant (see `JsCell` docs) means no other - /// `&mut Option` is live for the duration of the borrow. - #[inline] - #[allow(clippy::mut_from_ref)] - fn headers_mut(&self) -> &mut Option { - // SAFETY: single-JS-thread; callers below keep the borrow short and do - // not re-enter a path that touches `self.headers`. - unsafe { self.headers.get_mut() } + fn fetch_headers_mut(&self) -> Option<&mut FetchHeaders> { + self.headers.get().as_ref().map(HeadersRef::headers) } // Returns if the request has headers already cached/set. @@ -243,10 +233,10 @@ impl Request { pub(crate) fn ensure_fetch_headers( &self, global_this: &JSGlobalObject, - ) -> JsResult<&mut HeadersRef> { + ) -> JsResult<&mut FetchHeaders> { if self.headers.get().is_some() { // headers is already set - return Ok(self.headers_mut().as_mut().unwrap()); + return Ok(self.fetch_headers_mut().unwrap()); } if let Some(req) = self.request_context.get_request() { @@ -257,21 +247,16 @@ impl Request { } else { // we don't have a request context, so we need to create an empty headers object self.headers.set(Some(HeadersRef::create_empty())); - // Snapshot the pointer first; it stays valid across the field borrow. - let content_type: Option<*const [u8]> = match self.body_value() { - BodyValue::Blob(blob) => { - Some(std::ptr::from_ref::<[u8]>(blob.content_type_slice())) - } + let blob_source; + let content_type: Option<&[u8]> = match self.body_value().get() { + BodyValue::Blob(blob) => Some(blob.content_type_slice()), BodyValue::Locked(locked) => match locked.readable.get() { - Some(readable) => match readable.ptr { - crate::webcore::readable_stream::Source::Blob(blob) => { - // SAFETY: `Source::Blob` holds a live `*mut ByteBlobLoader` - // for as long as the readable stream exists; we only read - // its `content_type` slice and immediately copy below. - let ct: &[u8] = unsafe { (*blob).content_type.as_slice() }; - Some(std::ptr::from_ref::<[u8]>(ct)) + Some(readable) => match readable.ptr.blob() { + Some(blob) => { + blob_source = blob; + Some(blob_source.content_type.get().as_slice()) } - _ => None, + None => None, }, None => None, }, @@ -279,11 +264,8 @@ impl Request { }; if let Some(content_type_) = content_type { - // SAFETY: the sources above are live for the duration of this - // call; the bytes are copied into the header map below. - let content_type_ = unsafe { &*content_type_ }; if !content_type_.is_empty() { - self.headers_mut().as_mut().unwrap().put( + self.fetch_headers_mut().unwrap().put( HTTPHeaderName::ContentType, &BunString::ascii(content_type_), global_this, @@ -292,11 +274,10 @@ impl Request { } } - Ok(self.headers_mut().as_mut().unwrap()) + Ok(self.fetch_headers_mut().unwrap()) } - #[allow(clippy::mut_from_ref)] - pub(crate) fn get_fetch_headers_unless_empty(&self) -> Option<&mut HeadersRef> { + pub(crate) fn get_fetch_headers_unless_empty(&self) -> Option<&HeadersRef> { if self.headers.get().is_none() { if let Some(req) = self.request_context.get_request() { // we have a request context, so we can get the headers from it @@ -306,8 +287,8 @@ impl Request { } } - let headers = self.headers_mut().as_mut()?; - if headers.is_empty() { + let headers = self.headers.get().as_ref()?; + if headers.headers().is_empty() { return None; } Some(headers) @@ -330,8 +311,8 @@ impl Request { } } - if let Some(head) = self.headers_mut().as_mut() { - if head.is_empty() { + if let Some(head) = self.headers.get().as_ref() { + if head.headers().is_empty() { return Ok(None); } @@ -350,13 +331,13 @@ impl Request { } } - if let Some(headers) = self.headers_mut().as_mut() { + if let Some(headers) = self.fetch_headers_mut() { if let Some(value) = headers.fast_get(HTTPHeaderName::ContentType) { return Ok(Some(value.to_utf8())); } } - if let BodyValue::Blob(blob) = self.body_value() { + if let BodyValue::Blob(blob) = self.body_value().get() { let ct = blob.content_type_slice(); if !ct.is_empty() { return Ok(Some(bun_core::Utf8Bytes::Borrowed(ct))); @@ -372,7 +353,7 @@ impl Request { core::mem::size_of::() + self.request_context.memory_cost() + self.url.get().byte_slice().len() - + self.body_value().memory_cost() + + self.body_value().get().memory_cost() } #[bun_uws::uws_callback(export = "Request__setCookiesOnRequestContext")] @@ -424,7 +405,7 @@ impl Request { url: JsCell::new(url), headers: JsCell::new(headers), signal: JsCell::new(None), - body: ManuallyDrop::new(body), + body: JsCell::new(Some(body)), js_ref: JsCell::new(JsRef::empty()), method, flags: Flags::default(), @@ -456,7 +437,7 @@ impl Request { #[bun_uws::uws_callback(export = "Bun__JSRequest__calculateEstimatedByteSize")] pub fn calculate_estimated_byte_size(&self) { self.reported_estimated_size.set( - self.body_value().estimated_size() + self.body_value().get().estimated_size() + self.size_of_url() + core::mem::size_of::(), ); @@ -483,6 +464,12 @@ impl Request { self.check_body_stream_ref(global_object); js_value } + + /// Hand a heap `Request` to a new JS wrapper, which owns it from here + /// (freed via `RequestClass__finalize`). + pub fn to_js_boxed(this: Box, global_object: &JSGlobalObject) -> JSValue { + Box::leak(this).to_js(global_object) + } } // Request is opaque on the C++ side; see note on the JsClass extern block above. @@ -544,7 +531,7 @@ impl Request { writer, "{} ({}) {{", class_label, - bun_fmt::size(self.body_value_mut().size() as usize, Default::default()) + bun_fmt::size(self.body().len() as usize, Default::default()) )?; { // RAII guard restores indent on every exit incl. `?` error paths. @@ -616,7 +603,7 @@ impl Request { ) .map_err(js_err)?; - match self.body_value_mut() { + match self.body_value().get() { BodyValue::Blob(blob) => { writer.write_str("\n")?; formatter.write_indent(writer)?; @@ -625,7 +612,7 @@ impl Request { BodyValue::InternalBlob(_) | BodyValue::WTFStringImpl(_) => { writer.write_str("\n")?; formatter.write_indent(writer)?; - let size = self.body_value_mut().size(); + let size = self.body().len(); if size == 0 { let empty = Blob::init_empty(formatter.global_this()); empty.write_format::(&mut formatter, writer)?; @@ -705,7 +692,7 @@ impl Request { fetch_request_mode_to_js(self.flags.mode, global_this) } - pub(crate) fn finalize_without_deinit(&mut self) { + pub(crate) fn finalize_without_deinit(&self) { // headers.deref() → HeadersRef::Drop when set to None self.headers.set(None); @@ -716,30 +703,11 @@ impl Request { } pub fn finalize(self: Box) { - // weak_ptr_data may have outstanding refs aliasing this allocation; - // hand ownership back to the raw pointer FIRST so a panic in the work - // below leaks instead of Box-drop UAF-ing those weak holders. - let this = bun_core::heap::release(self); - // Release the request's `+1` on the body slot. `ManuallyDrop` so the - // hot-path `Box::from_raw().drop()` below cannot re-run this. - // SAFETY: `this` is live and this is the sole release point for `body`. - unsafe { ManuallyDrop::drop(&mut this.body) }; - if this.weak_ptr_data.on_finalize() { - // Hot path: no outstanding weak refs. Reclaim and drop the whole - // allocation in one shot — `Box::from_raw`'s drop runs - // `drop_in_place` over every field (headers / url / signal / - // js_ref) once, without the `Cell::set` - // read-write-drop round-trips the old `finalize_without_deinit()` - // call performed here before re-dropping the (now-empty) fields. - // SAFETY: `this` is the live Box-allocated payload. - drop(unsafe { Box::from_raw(this) }); - } else { - // Cold path: weak_ptr_data still has outstanding refs — keep the - // allocation alive, but release inner resources now so they aren't - // pinned until the last `WeakPtr` drops. - this.js_ref.with_mut(|r| r.finalize()); - this.finalize_without_deinit(); - } + // No outstanding weak refs: the whole allocation (headers / url / + // signal / body / js_ref) drops in one shot. Otherwise + // `finalize_contents` releases them now and the last `WeakPtr` frees + // the allocation. + bun_ptr::weak_ptr::finalize_owner(self); } pub(crate) fn get_redirect(&self, global_this: &JSGlobalObject) -> JSValue { @@ -747,7 +715,7 @@ impl Request { } pub(crate) fn get_referrer(&self, global_object: &JSGlobalObject) -> JSValue { - if let Some(headers_ref) = self.headers_mut().as_mut() { + if let Some(headers_ref) = self.fetch_headers_mut() { if let Some(referrer) = headers_ref.get(b"referrer", global_object) { return referrer.to_js(global_object); } @@ -973,22 +941,18 @@ impl Request { ::check_body_stream_ref(self, global_object) } + /// Every early return drops `req`, whose fields release what they hold + /// (headers / url / signal / the pooled body slot). pub(crate) fn construct_into( global_this: &JSGlobalObject, arguments: &[JSValue], this_value: JSValue, ) -> JsResult { - let mut success = false; - // SAFETY: bun_vm() yields the live per-thread VM singleton. - let body = body::hive_alloc(BodyValue::Null); - // Snapshot the seed slot pointer for the repoint check below; `body` - // (the +1) is moved into `req.body` next. - let body_seed_ptr = body.as_ptr(); let mut req = Request { url: JsCell::new(BunString::EMPTY), headers: JsCell::new(None), signal: JsCell::new(None), - body: ManuallyDrop::new(body), + body: JsCell::new(Some(body::hive_alloc(BodyValue::Null))), js_ref: JsCell::new(JsRef::init_weak(this_value)), method: Method::GET, flags: Flags::default(), @@ -996,42 +960,17 @@ impl Request { weak_ptr_data: WeakPtrData::EMPTY, reported_estimated_size: Cell::new(0), }; - // A scopeguard cannot capture `&mut req` while the - // fn body also uses it. Cleanup is invoked at each early-return site via `bail!`. - let cleanup = |req: &mut Request, - body_seed_ptr: *mut crate::webcore::body::HiveRef, - success: bool| { - // Snapshot before the `!success` drop — reading a `ManuallyDrop` - // after `ManuallyDrop::drop()` is documented use-after-drop. - let req_body_ptr = req.body.as_ptr(); - if !success { - req.finalize_without_deinit(); - // SAFETY: `req.body` is live; this is the sole release on this path. - unsafe { ManuallyDrop::drop(&mut req.body) }; - } - if req_body_ptr != body_seed_ptr { - // `clone_into` `ptr::write`-overwrote `req.body`, orphaning the - // seed slot's +1. Recover and drop it. - // SAFETY: `body_seed_ptr` is a live +1 leaked by the ptr::write. - drop(unsafe { BodyHiveHandle::from_raw(body_seed_ptr) }); - } - }; - - macro_rules! bail { - ($e:expr) => {{ - cleanup(&mut req, body_seed_ptr, success); - return $e; - }}; - } + // The pooled body slot is fresh (nothing else references it yet), so + // the body producers below write straight into it. if arguments.is_empty() { - bail!(Err(global_this.throw(format_args!( + return Err(global_this.throw(format_args!( "Failed to construct 'Request': 1 argument required, but only 0 present." - )))); + ))); } else if arguments[0].is_empty_or_undefined_or_null() || !arguments[0].is_cell() { - bail!(Err(global_this.throw(format_args!( + return Err(global_this.throw(format_args!( "Failed to construct 'Request': expected non-empty string or object, got undefined" - )))); + ))); } let url_or_object = arguments[0]; @@ -1045,19 +984,16 @@ impl Request { bun_jsc::DOMURL::cast_(url_or_object, global_this.vm()).is_some(); if is_first_argument_a_url { - let str = match BunString::from_js(arguments[0], global_this) { - Ok(s) => s, - Err(e) => bail!(Err(e)), - }; + let str = BunString::from_js(arguments[0], global_this)?; req.url.set(str); if !req.url.get().is_empty() { fields.insert(Fields::Url); } } else if !url_or_object_type.is_object() { - bail!(Err(global_this.throw(format_args!( + return Err(global_this.throw(format_args!( "Failed to construct 'Request': expected non-empty string or object" - )))); + ))); } let values_to_try_: [JSValue; 2] = [ @@ -1083,21 +1019,12 @@ impl Request { && value_type == bun_jsc::JSType::FinalObject && values_to_try[1].js_type() == bun_jsc::JSType::DOMWrapper; if value_type == bun_jsc::JSType::DOMWrapper { - if let Some(request) = value.as_direct::() { - // SAFETY: as_direct returns a live *mut Request payload (m_ctx) - let request = unsafe { &*request }; + if let Some(request) = value.as_direct_class_ref::() { if values_to_try.len() == 1 { - match Request::clone_into( - request, - &mut req, - global_this, - fields.contains(Fields::Url), - ) { - Ok(()) => {} - Err(e) => bail!(Err(e)), - } - success = true; - cleanup(&mut req, body_seed_ptr, success); + let preserved_url = fields.contains(Fields::Url).then(|| req.url.take()); + // The seed `req` (and its pooled body slot) is dropped; + // the clone starts from a fresh `js_ref`. + req = request.clone_with_url(global_this, preserved_url)?; return Ok(req); } @@ -1122,53 +1049,40 @@ impl Request { } if !fields.contains(Fields::Headers) { - match request.clone_headers(global_this) { - Ok(Some(headers)) => { - req.headers.set(Some(headers)); - fields.insert(Fields::Headers); - } - Ok(None) => {} - Err(e) => bail!(Err(e)), + if let Some(headers) = request.clone_headers(global_this)? { + req.headers.set(Some(headers)); + fields.insert(Fields::Headers); } } if !fields.contains(Fields::Body) { - match request.body_value() { + match request.body_value().get() { BodyValue::Null | BodyValue::Empty | BodyValue::Used => {} _ => { - match request.clone_body_value_via_cached_stream(global_this) { - Ok(v) => { - *req.body_value_mut() = v; - } - Err(e) => bail!(Err(e)), - } + req.body().value.with_mut(|slot| -> JsResult<()> { + *slot = + request.clone_body_value_via_cached_stream(global_this)?; + Ok(()) + })?; fields.insert(Fields::Body); } } } } - if let Some(response) = value.as_direct::() { - // SAFETY: `as_direct` returned a live `*mut Response` owned by the JS wrapper. - let response = unsafe { &mut *response }; + if let Some(response) = value.as_direct_class_ref::() { if !fields.contains(Fields::Method) { req.method = response.get_method(); fields.insert(Fields::Method); } if !fields.contains(Fields::Headers) { - if let Some(headers) = response.get_init_headers_mut() { + if response.get_init_headers().is_some() { // The flag is set unconditionally once `getInitHeaders()` yielded a // value, even if `cloneThis` returns null — so a later arg can't // repopulate headers from a different source. - match headers.clone_this(global_this) { - Ok(h) => { - // SAFETY: clone_this returns a +1 ref FetchHeaders. - req.headers.set(h.map(|p| unsafe { HeadersRef::adopt(p) })); - fields.insert(Fields::Headers); - } - Err(e) => bail!(Err(e)), - } + req.headers.set(response.clone_init_headers(global_this)?); + fields.insert(Fields::Headers); } } @@ -1181,15 +1095,14 @@ impl Request { } if !fields.contains(Fields::Body) { - match response.get_body_value() { + match response.body_value().get() { BodyValue::Null | BodyValue::Empty | BodyValue::Used => {} _ => { - match response.clone_body_value_via_cached_stream(global_this) { - Ok(v) => { - *req.body_value_mut() = v; - } - Err(e) => bail!(Err(e)), - } + req.body().value.with_mut(|slot| -> JsResult<()> { + *slot = + response.clone_body_value_via_cached_stream(global_this)?; + Ok(()) + })?; fields.insert(Fields::Body); } } @@ -1198,53 +1111,40 @@ impl Request { } if !fields.contains(Fields::Body) { - match value.fast_get(global_this, bun_jsc::BuiltinName::Body) { - Ok(Some(body_)) => { - fields.insert(Fields::Body); - // fetch spec Request(init): `keepalive: true` with a ReadableStream - // body throws before body extraction (Node's message is "keepalive"). - if crate::webcore::ReadableStream::is_readable_stream(body_) { - match value.get(global_this, "keepalive") { - Ok(Some(keepalive)) if keepalive.to_boolean() => { - bail!(Err( - global_this.throw_type_error(format_args!("keepalive")) - )); - } - Ok(_) => {} - Err(e) => bail!(Err(e)), - } - } - match BodyValue::from_js(global_this, body_) { - Ok(v) => { - *req.body_value_mut() = v; + if let Some(body_) = value.fast_get(global_this, bun_jsc::BuiltinName::Body)? { + fields.insert(Fields::Body); + // fetch spec Request(init): `keepalive: true` with a ReadableStream + // body throws before body extraction (Node's message is "keepalive"). + if crate::webcore::ReadableStream::is_readable_stream(body_) { + if let Some(keepalive) = value.get(global_this, "keepalive")? { + if keepalive.to_boolean() { + return Err(global_this.throw_type_error(format_args!("keepalive"))); } - Err(e) => bail!(Err(e)), } } - Ok(None) => {} - Err(e) => bail!(Err(e)), + req.body().value.with_mut(|slot| -> JsResult<()> { + *slot = BodyValue::from_js(global_this, body_)?; + Ok(()) + })?; } // BodyValue::from_js() throws without returning Err; see Blob::from_dom_form_data if global_this.has_exception() { - bail!(Err(JsError::Thrown)); + return Err(JsError::Thrown); } } if !fields.contains(Fields::Url) { - match value.fast_get(global_this, bun_jsc::BuiltinName::Url) { - Ok(Some(url)) => { - match BunString::from_js(url, global_this) { - Ok(s) => req.url.set(s), - Err(e) => bail!(Err(e)), - } + match value.fast_get(global_this, bun_jsc::BuiltinName::Url)? { + Some(url) => { + req.url.set(BunString::from_js(url, global_this)?); if !req.url.get().is_empty() { fields.insert(Fields::Url); } // first value } - Ok(None) => { + None => { // Short-circuit ordering: only probe // `implementsToString` (which performs JS property // lookup with observable side effects) when the first @@ -1252,15 +1152,8 @@ impl Request { if value == values_to_try[values_to_try.len() - 1] && !is_first_argument_a_url { - let implements = match value.implements_to_string(global_this) { - Ok(b) => b, - Err(e) => bail!(Err(e)), - }; - if implements { - let str = match BunString::from_js(value, global_this) { - Ok(s) => s, - Err(e) => bail!(Err(e)), - }; + if value.implements_to_string(global_this)? { + let str = BunString::from_js(value, global_this)?; req.url.set(str); if !req.url.get().is_empty() { fields.insert(Fields::Url); @@ -1268,7 +1161,6 @@ impl Request { } } } - Err(e) => bail!(Err(e)), } } @@ -1276,107 +1168,92 @@ impl Request { // WebIDL `AbortSignal?`: present iff the member is not undefined. // `fast_get` maps absent/undefined → None; `null` is Some(null) and // means "present, detach" (no fallback to the input Request's signal). - match value.fast_get(global_this, bun_jsc::BuiltinName::signal) { - Ok(Some(signal_)) => { - fields.insert(Fields::Signal); - if signal_.is_null() { - // explicit detach; leave `req.signal` as None - } else if let Some(signal) = AbortSignal::ref_from_js(signal_) { - // Keep it alive - signal_.ensure_still_alive(); - // `ref_from_js` already ref'd. - req.signal.set(Some(signal)); - } else { - bail!(Err(global_this.throw_type_error(format_args!( - "Failed to construct 'Request': signal is not of type AbortSignal." - )))); - } + if let Some(signal_) = value.fast_get(global_this, bun_jsc::BuiltinName::signal)? { + fields.insert(Fields::Signal); + if signal_.is_null() { + // explicit detach; leave `req.signal` as None + } else if let Some(signal) = AbortSignal::ref_from_js(signal_) { + // Keep it alive + signal_.ensure_still_alive(); + // `ref_from_js` already ref'd. + req.signal.set(Some(signal)); + } else { + return Err(global_this.throw_type_error(format_args!( + "Failed to construct 'Request': signal is not of type AbortSignal." + ))); } - Ok(None) => {} - Err(e) => bail!(Err(e)), } } if !fields.contains(Fields::Method) || !fields.contains(Fields::Headers) { - match crate::webcore::response::Init::init(global_this, value) { - Ok(Some(response_init)) => { - let header_check = !explicit_check - || (explicit_check - && match value.fast_get(global_this, bun_jsc::BuiltinName::Headers) - { - Ok(v) => v.is_some(), - Err(e) => bail!(Err(e)), - }); - if header_check { - if let Some(headers) = response_init.headers { - if !fields.contains(Fields::Headers) { - req.headers.set(Some(headers)); - fields.insert(Fields::Headers); - } else { - drop(headers); // headers.deref() - } + if let Some(response_init) = + crate::webcore::response::Init::init(global_this, value)? + { + let header_check = !explicit_check + || (explicit_check + && value + .fast_get(global_this, bun_jsc::BuiltinName::Headers)? + .is_some()); + if header_check { + if let Some(headers) = response_init.headers { + if !fields.contains(Fields::Headers) { + req.headers.set(Some(headers)); + fields.insert(Fields::Headers); + } else { + drop(headers); // headers.deref() } } + } - let method_check = !explicit_check - || (explicit_check - && match value.fast_get(global_this, bun_jsc::BuiltinName::Method) { - Ok(v) => v.is_some(), - Err(e) => bail!(Err(e)), - }); - if method_check { - if !fields.contains(Fields::Method) { - req.method = response_init.method; - fields.insert(Fields::Method); - } + let method_check = !explicit_check + || (explicit_check + && value + .fast_get(global_this, bun_jsc::BuiltinName::Method)? + .is_some()); + if method_check { + if !fields.contains(Fields::Method) { + req.method = response_init.method; + fields.insert(Fields::Method); } } - Ok(None) => {} - Err(e) => bail!(Err(e)), } } // Extract redirect option if !fields.contains(Fields::Redirect) { - match value.get_optional_enum::(global_this, "redirect") { - Ok(Some(redirect_value)) => { - req.flags.redirect = redirect_value; - fields.insert(Fields::Redirect); - } - Ok(None) => {} - Err(e) => bail!(Err(e)), + if let Some(redirect_value) = + value.get_optional_enum::(global_this, "redirect")? + { + req.flags.redirect = redirect_value; + fields.insert(Fields::Redirect); } } // Extract cache option if !fields.contains(Fields::Cache) { - match value.get_optional_enum::(global_this, "cache") { - Ok(Some(cache_value)) => { - req.flags.cache = cache_value; - fields.insert(Fields::Cache); - } - Ok(None) => {} - Err(e) => bail!(Err(e)), + if let Some(cache_value) = + value.get_optional_enum::(global_this, "cache")? + { + req.flags.cache = cache_value; + fields.insert(Fields::Cache); } } // Extract mode option if !fields.contains(Fields::Mode) { - match value.get_optional_enum::(global_this, "mode") { - Ok(Some(mode_value)) => { - req.flags.mode = mode_value; - fields.insert(Fields::Mode); - } - Ok(None) => {} - Err(e) => bail!(Err(e)), + if let Some(mode_value) = + value.get_optional_enum::(global_this, "mode")? + { + req.flags.mode = mode_value; + fields.insert(Fields::Mode); } } } if req.url.get().is_empty() { - bail!(Err(global_this.throw(format_args!( + return Err(global_this.throw(format_args!( "Failed to construct 'Request': url is required." - )))); + ))); } let href = bun_url::href_from_string(req.url.get()); @@ -1387,7 +1264,7 @@ impl Request { "Failed to construct 'Request': Invalid URL \"{}\"", req.url.get() )); - bail!(Err(global_this.throw_value(err))); + return Err(global_this.throw_value(err)); } // hrefFromString increments the reference count if they end up being @@ -1398,36 +1275,21 @@ impl Request { req.url.set(href); - if matches!(req.body_value(), BodyValue::Blob(_)) && req.headers.get().is_some() { - if let BodyValue::Blob(blob) = req.body_value() { + if let BodyValue::Blob(blob) = req.body_value().get() { + if let Some(headers) = req.fetch_headers_mut() { let ct: &[u8] = blob.content_type_slice(); - if !ct.is_empty() - && !req - .headers_mut() - .as_mut() - .unwrap() - .fast_has(HTTPHeaderName::ContentType) - { - // Reshaped for borrowck — split borrow of req.body and req.headers - let ct_ptr: *const [u8] = ct; - match req.headers_mut().as_mut().unwrap().put( + if !ct.is_empty() && !headers.fast_has(HTTPHeaderName::ContentType) { + headers.put( HTTPHeaderName::ContentType, - // SAFETY: ct_ptr borrows req.body which is not mutated here. - &BunString::ascii(unsafe { &*ct_ptr }), + &BunString::ascii(ct), global_this, - ) { - Ok(()) => {} - Err(e) => bail!(Err(e)), - } + )?; } } } req.calculate_estimated_byte_size(); req.check_body_stream_ref(global_this); - success = true; - - cleanup(&mut req, body_seed_ptr, success); Ok(req) } @@ -1451,93 +1313,51 @@ impl Request { let this_value = callframe.this(); let cloned = self.clone(global_this)?; - let cloned_ptr = bun_core::heap::into_raw(cloned); - // SAFETY: cloned_ptr was just created via heap::alloc above; toJS adopts ownership. - let js_wrapper = unsafe { (*cloned_ptr).to_js(global_this) }; + let js_wrapper = Request::to_js_boxed(cloned, global_this); self.sync_cloned_body_stream_caches(this_value, js_wrapper, global_this); Ok(js_wrapper) } - pub(crate) fn clone_into( + /// A clone of this request (fetch spec `Request.clone` step 2) with a + /// fresh pooled body; `url` overrides the cloned URL when given. + fn clone_with_url( &self, - req: &mut Request, global_this: &JSGlobalObject, - preserve_url: bool, - ) -> JsResult<()> { - // allocator param dropped (global mimalloc) + url: Option, + ) -> JsResult { let _ = self.ensure_url(); let body_ = self.clone_body_value_via_cached_stream(global_this)?; - // BodyValue's Drop frees `body_` on the `?` error path let body = body::hive_alloc(body_); - // Last fallible call; an early return here leaves `req.url` untouched. - // `body` (a `BodyHiveHandle`) drops on the `?` error path, releasing its +1. + // Last fallible call; `body` (a `BodyHiveHandle`) drops on the `?` + // error path, releasing its +1. let headers = self.clone_headers(global_this)?; - let url = if preserve_url { - req.url.take() - } else { - self.url.get().clone() + let url = match url { + Some(url) => url, + None => self.url.get().clone(), }; - // `ptr::write` is a raw bit-overwrite — no destructors run on the old - // `*req`, so the Drop impl on `JsRef` doesn't fire on the caller's - // sentinel. - // The old `req.body` hive ref is intentionally NOT unref'd here: - // `clone()` seeds it with a dangling sentinel, and `construct_into` - // releases its seed via the ptr-equality arm of its `cleanup`. - // `url` was taken above (preserve_url) or is the empty - // sentinel; remaining incoming fields are None/weak/Copy by contract. - // SAFETY: `req` is a valid &mut, fully initialized by the caller; - // nothing between here and the write can panic. - unsafe { - core::ptr::write( - req, - Request { - url: JsCell::new(url), - headers: JsCell::new(headers), - signal: JsCell::new(None), - body: ManuallyDrop::new(body), - js_ref: JsCell::new(JsRef::empty()), - method: self.method, - flags: self.flags, - request_context: AnyRequestContext::NULL, - weak_ptr_data: WeakPtrData::EMPTY, - reported_estimated_size: Cell::new(0), - }, - ); - } + let req = Request { + url: JsCell::new(url), + headers: JsCell::new(headers), + signal: JsCell::new(None), + body: JsCell::new(Some(body)), + js_ref: JsCell::new(JsRef::empty()), + method: self.method, + flags: self.flags, + request_context: AnyRequestContext::NULL, + weak_ptr_data: WeakPtrData::EMPTY, + reported_estimated_size: Cell::new(0), + }; if let Some(signal) = self.signal.get() { // `AbortSignalRef::clone` → C++ `ref()`. req.signal.set(Some(signal.clone())); } - Ok(()) + Ok(req) } pub(crate) fn clone(&self, global_this: &JSGlobalObject) -> JsResult> { - // allocator param dropped (global mimalloc) - // `clone_into` `ptr::write`s the new fields over the seed - // without reading or dropping it. - let mut req = Box::new(Request { - url: JsCell::new(BunString::EMPTY), - headers: JsCell::new(None), - signal: JsCell::new(None), - // `clone_into` `ptr::write`s the whole struct without dropping the - // sentinel; seed with a non-deref'd dangling handle. `ManuallyDrop` - // suppresses drop, so the `?` error path won't unref the dangling ptr. - // SAFETY: never deref'd or dropped — overwritten by `clone_into`. - body: ManuallyDrop::new(unsafe { - BodyHiveHandle::from_raw(NonNull::dangling().as_ptr()) - }), - js_ref: JsCell::new(JsRef::empty()), - method: Method::GET, - flags: Flags::default(), - request_context: AnyRequestContext::NULL, - weak_ptr_data: WeakPtrData::EMPTY, - reported_estimated_size: Cell::new(0), - }); - // Box drops on the error path automatically - self.clone_into(&mut req, global_this, false)?; - Ok(req) + Ok(Box::new(self.clone_with_url(global_this, None)?)) } } @@ -1553,7 +1373,7 @@ impl Request { url: JsCell::new(BunString::EMPTY), headers: JsCell::new(None), signal: JsCell::new(signal), - body: ManuallyDrop::new(body), + body: JsCell::new(Some(body)), js_ref: JsCell::new(JsRef::empty()), method, flags: Flags { diff --git a/src/runtime/webcore/Response.rs b/src/runtime/webcore/Response.rs index 79b0121bddab..b08ab94f6ccc 100644 --- a/src/runtime/webcore/Response.rs +++ b/src/runtime/webcore/Response.rs @@ -1,10 +1,8 @@ use core::cell::Cell; -use core::ffi::c_void; use core::mem; -use core::ptr::NonNull; use bun_jsc::JsCell; -use bun_jsc::{AbortSignal, AbortSignalRef, GlobalRef}; +use bun_jsc::{AbortSignal, GlobalRef}; use bun_ptr::RefPtr; use crate::webcore::jsc::{ @@ -22,129 +20,36 @@ use super::{FetchHeaders, ReadableStream, Request}; // `crate::webcore::response` because the `.classes.ts` source path is // `bun.jsc.WebCore.response.Blob`. Keep this `pub use` so that resolves. pub use super::blob::Blob; -use bun_ptr::weak_ptr::WeakPtrData; - -/// RAII handle to a C++-owned `WebCore::FetchHeaders`. -/// -/// Holds exactly one ref on the C++ intrusive refcount; `Drop` releases it via -/// `WebCore__FetchHeaders__deref`. NOT a `std::rc::Rc` (the payload lives on -/// the C++ heap and is opaque here). -/// -/// Intentionally not `Clone`: the only "share" operation the surface -/// exposes is `clone_this()`, which deep-copies a fresh `FetchHeaders` on the -/// C++ side. Transferring ownership is by-move. -#[repr(transparent)] -pub struct HeadersRef(NonNull); - -impl HeadersRef { - /// Adopt a freshly-created `FetchHeaders*` (refcount already 1). - /// - /// # Safety - /// `ptr` must be a valid `WebCore::FetchHeaders*` and the caller must - /// transfer ownership of one ref. - #[inline] - pub(crate) unsafe fn adopt(ptr: NonNull) -> Self { - Self(ptr) - } - - #[inline] - pub(crate) fn as_ptr(&self) -> *mut FetchHeaders { - self.0.as_ptr() - } - - /// `FetchHeaders.createEmpty()` — fresh C++ allocation, refcount 1. - #[inline] - pub(crate) fn create_empty() -> Self { - // SAFETY: C++ allocates a new FetchHeaders with refcount 1; never null. - unsafe { Self::adopt(FetchHeaders::create_empty()) } - } - - /// `FetchHeaders.createFromUWS(req)` — fresh C++ allocation, refcount 1. - #[inline] - pub(crate) fn create_from_uws(uws_request: *mut core::ffi::c_void) -> Self { - // SAFETY: C++ allocates a new FetchHeaders with refcount 1; never null. - unsafe { Self::adopt(FetchHeaders::create_from_uws(uws_request)) } - } - - /// `FetchHeaders.createFromJS(global, value)` — may throw, may return null. - #[inline] - pub(crate) fn create_from_js( - global: &JSGlobalObject, - value: JSValue, - ) -> JsResult> { - // SAFETY: C++ returns a +1 ref or null. - Ok(FetchHeaders::create_from_js(global, value)?.map(|p| unsafe { Self::adopt(p) })) - } - - /// `FetchHeaders.cloneThis(global)` — deep copy on the C++ side. - #[inline] - pub(crate) fn clone_this(&self, global: &JSGlobalObject) -> JsResult> { - // SAFETY: C++ returns a +1 ref or null. - Ok(bun_opaque::opaque_deref_mut(self.0.as_ptr()) - .clone_this(global)? - .map(|p| unsafe { Self::adopt(p) })) - } -} - -impl core::ops::Deref for HeadersRef { - type Target = FetchHeaders; - #[inline] - fn deref(&self) -> &FetchHeaders { - // `FetchHeaders` is an opaque ZST FFI handle (S008); `self.0` is live - // for the lifetime of `self` — safe `*const → &` via `opaque_deref`. - bun_opaque::opaque_deref(self.0.as_ptr()) - } -} - -impl core::ops::DerefMut for HeadersRef { - #[inline] - fn deref_mut(&mut self) -> &mut FetchHeaders { - // `FetchHeaders` is an opaque ZST FFI handle (S008); `self.0` is live - // for the lifetime of `self` — safe `*mut → &mut` via `opaque_deref_mut`. - bun_opaque::opaque_deref_mut(self.0.as_ptr()) - } -} - -impl Drop for HeadersRef { - #[inline] - fn drop(&mut self) { - // `self.0` is live; releasing our +1 ref via WebCore__FetchHeaders__deref. - // Explicit UFCS to avoid `core::ops::Deref::deref` resolution ambiguity. - // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. - FetchHeaders::deref(bun_opaque::opaque_deref_mut(self.0.as_ptr())); - } -} +pub use bun_jsc::HeadersRef; +use bun_ptr::weak_ptr::{HasWeakPtrData, WeakPtrData}; /// Errors the owning fetch `Response`'s body on abort (Fetch spec "abort a fetch" step 4). pub(crate) struct BodyAbortListener { - signal: AbortSignalRef, - /// `Response` owns `Box`, so a ref-counted pointer here would cycle. - response: bun_ptr::ParentRef, + /// Our listener on the signal (and reference on it); dropped with the Response. + registration: Cell>, + /// `Response` owns this, so a ref-counted pointer here would cycle. + response: bun_ptr::BackRef, global: GlobalRef, } -impl BodyAbortListener { - unsafe extern "C" fn on_abort(ctx: *mut c_void, reason: JSValue) { +impl bun_jsc::NativeAbortListener for BodyAbortListener { + fn on_abort(this: bun_ptr::ThisPtr, reason: JSValue) { reason.ensure_still_alive(); - // SAFETY: `ctx` is the `Box` registered in - // `attach_abort_signal`; `clean_native_bindings` removes it before the - // box is dropped, so it is live here. Copy out up front: erroring a - // still-streaming body can re-enter `Response::unref` via - // `FetchTasklet::abandon_response_body` and destroy this box. - let (response, global) = - unsafe { ((*ctx.cast::()).response, (*ctx.cast::()).global) }; - // SAFETY: `response` is live (see above). - let _keepalive = unsafe { RefPtr::init_ref(response.as_mut_ptr()) }; + // Copy out up front: erroring a still-streaming body can re-enter and + // drop the response's last other ref via + // `FetchTasklet::abandon_response_body`, destroying this listener. + let (response, global) = (this.response, this.global); + let _keepalive = bun_ptr::RefPtr::from_this(response.this_ptr()); if !matches!( - response.get_body_value(), + response.body_value().get(), BodyValue::Used | BodyValue::Error(_) | BodyValue::Null | BodyValue::Empty ) { // Not `get_body_readable_stream`: its `js_ref()` path reads a raw // JSValue to a wrapper that may be unmarked but not yet swept, - // reaching a `NewSource` box the source cell's (PreciseAllocation) + // reaching a `NewSource` the source cell's (PreciseAllocation) // destructor already freed. `Locked.readable` is a real `JSC::Weak` - // on the stream and reads `None` exactly when the box is gone. - if let BodyValue::Locked(locked) = response.get_body_value() { + // on the stream and reads `None` exactly when it is gone. + if let BodyValue::Locked(locked) = response.body_value().get() { if let Some(readable) = locked.readable.get() { readable.value.ensure_still_alive(); crate::dispatch::fold(readable.error(&global, reason)); @@ -152,28 +57,21 @@ impl BodyAbortListener { } let err = BodyValueError::JSValue(bun_jsc::strong::Optional::create(reason, &global)); // R-2: re-derive after `error()` ran JS. - let _ = response.get_body_value().to_error_instance(err, &global); + let _ = response + .body_value() + .with_mut(|value| value.to_error_instance(err, &global)); } } } -impl Drop for BodyAbortListener { - fn drop(&mut self) { - let ctx = core::ptr::from_mut(self).cast::(); - self.signal.clean_native_bindings(ctx); - self.signal.pending_activity_unref(); - } -} - // `jsc.Codegen.JSResponse` — the real bindings, emitted by // `js_class_module!` in `bun_jsc::generated`. pub mod js { pub use bun_jsc::generated::JSResponse::*; } -// NOTE: toJS is overridden below. -// Typed re-exports. The `js::` module erases the payload to `*mut ()` -// (Response is defined above the `bun_jsc` crate, so `js_class_module!` -// can't name it); cast at this boundary. +/// Typed `from_js`. The `js::` module erases the payload to `*mut ()` +/// (Response is defined above the `bun_jsc` crate, so `js_class_module!` +/// can't name it). #[inline] pub fn from_js(value: JSValue) -> Option<*mut Response> { js::from_js(value).map(<*mut ()>::cast::) @@ -187,34 +85,43 @@ pub fn from_js_ref(value: JSValue) -> Option> { .map(bun_ptr::ParentRef::from) } -// `JsClass` impl delegates to `bun_jsc::generated::JSResponse` — the -// `js_class_module!` expansion already declares the -// `Response__{fromJS,fromJSDirect,create,getConstructor}` externs with the -// correct `JSC_CALLCONV`. Payload is type-erased to `*mut ()` at the `bun_jsc` -// tier (Response lives in a higher crate); the macro casts at the boundary. -bun_jsc::impl_js_class_via_generated!(Response => bun_jsc::generated::JSResponse); +// Routes through the codegen'd `JSResponse` wrappers; `to_js` goes through the +// inherent [`Response::to_js`] so generic `::to_js` callers also run +// `calculate_estimated_byte_size`, seed `js_ref`, and migrate a Locked-body +// stream into the GC slot. +impl bun_jsc::JsClass for Response { + fn from_js(value: JSValue) -> Option<*mut Self> { + from_js(value) + } + fn from_js_direct(value: JSValue) -> Option<*mut Self> { + js::from_js_direct(value).map(<*mut ()>::cast::) + } + fn to_js(self, global: &JSGlobalObject) -> JSValue { + Response::to_js(self, global) + } + fn get_constructor(global: &JSGlobalObject) -> JSValue { + js::get_constructor(global) + } +} /// R-2 (`sharedThis`): every JS-facing host-fn takes `&Response` (not /// `&mut Response`) so re-entrant JS calls cannot stack two `&mut` to the same /// instance. Fields mutated by host-fns are therefore wrapped in `Cell` (Copy -/// scalars) or `JsCell` (non-Copy `body`/`init`/`url`/`js_ref`). Both are -/// `#[repr(transparent)]`, so `#[repr(C)]` field layout is unchanged. +/// scalars) or `JsCell` (non-Copy `init`/`url`/`js_ref`); `body` is itself a +/// `JsCell` wrapper. Both are `#[repr(transparent)]`, so `#[repr(C)]` field +/// layout is unchanged. /// -/// Exception: the `BodyMixin` trait family (`get_text`/`get_json`/...) still -/// takes `&mut self` — the trait is shared with `Request` (not yet migrated). -/// Those methods reach mutable state exclusively through the `JsCell`-wrapped -/// `body`, so the `UnsafeCell` indirection still suppresses field-level -/// `noalias` caching across re-entry. +/// The allocation is refcounted: the JS wrapper owns one reference (released in +/// [`Response::finalize`]); fetch and HTMLRewriter hold `RefPtr`s so a +/// discarded JS Response can still have its body resolved. #[repr(C)] #[derive(bun_ptr::CellRefCounted)] -#[ref_count(destroy = Response::destroy)] +#[ref_count(destroy = bun_ptr::weak_ptr::destroy_weakly_held)] pub struct Response { - body: JsCell, + body: Body, init: JsCell, url: JsCell, redirected: Cell, - /// The JS wrapper, fetch (so a discarded JS Response can still resolve - /// its body) and HTMLRewriter each hold a ref. ref_count: Cell, /// Bun.serve's RequestContext holds a weak reference so `onAbort` / /// `handleResolveStream` / `handleRejectStream` can safely observe that the @@ -227,29 +134,54 @@ pub struct Response { reported_estimated_size: Cell, /// Fetch's `AbortSignal` listener; survives `FetchTasklet` teardown so a fully-buffered body is still errored. - abort_listener: JsCell>>, + abort_listener: JsCell>>, } impl Default for Response { fn default() -> Self { + Self::new( + Init::default(), + BodyValue::Null, + BunString::EMPTY, + false, + JsRef::empty(), + ) + } +} + +impl Response { + /// Every field spelled out (no `..Default::default()` temporary to move + /// the `Cell` fields out of), so construction lowers to member stores. + #[inline] + fn new(init: Init, body: BodyValue, url: BunString, redirected: bool, js_ref: JsRef) -> Self { Self { - body: JsCell::new(Body::default()), - init: JsCell::new(Init::default()), - url: JsCell::new(BunString::EMPTY), - redirected: Cell::new(false), + body: Body::new(body), + init: JsCell::new(init), + url: JsCell::new(url), + redirected: Cell::new(redirected), ref_count: Cell::new(1), weak_ptr_data: WeakPtrData::EMPTY, - js_ref: JsCell::new(JsRef::empty()), + js_ref: JsCell::new(js_ref), reported_estimated_size: Cell::new(0), abort_listener: JsCell::new(None), } } } -impl bun_ptr::weak_ptr::HasWeakPtrData for Response { - unsafe fn weak_ptr_data(this: *mut Self) -> *mut WeakPtrData { - // SAFETY: caller guarantees `this` points to a live (possibly-finalized) allocation. - unsafe { core::ptr::addr_of_mut!((*this).weak_ptr_data) } +impl HasWeakPtrData for Response { + fn weak_ptr_data(&self) -> &WeakPtrData { + &self.weak_ptr_data + } + + /// The last reference is gone but a `WeakRef` (RequestContext.response_weakref) + /// still holds the allocation: release what the fields own, leaving them + /// empty. `WeakRef::get()` returns null from here on. + fn finalize_contents(&self) { + self.init.set(Init::default()); + self.body.reset(); + self.url.set(BunString::EMPTY); + self.js_ref.set(JsRef::empty()); + self.abort_listener.set(None); } } pub(crate) type WeakRef = bun_ptr::WeakPtr; @@ -281,13 +213,13 @@ impl crate::webcore::body::BodyOwnerJs for Response { // BodyMixin is a trait with default methods providing getText/ // getBody/getBytes/getBodyUsed/getJSON/getArrayBuffer/getBlob/getBlobWithoutCallFrame/ -// getFormData over any type exposing getBodyValue()/getFormDataEncoding()/etc. +// getFormData over any type exposing body()/getFormDataEncoding()/etc. // Response implements it. impl BodyMixin for Response { #[inline] - fn get_body_value(&self) -> &mut BodyValue { - Response::get_body_value(self) + fn body(&self) -> &Body { + &self.body } #[inline] fn get_fetch_headers(&self) -> Option> { @@ -315,13 +247,13 @@ impl Response { url: BunString, redirected: bool, ) -> Response { - Response { - init: JsCell::new(response_init), - body: JsCell::new(body), - url: JsCell::new(url), - redirected: Cell::new(redirected), - ..Default::default() - } + Self::new( + response_init, + body.into_value(), + url, + redirected, + JsRef::empty(), + ) } #[inline] @@ -366,38 +298,22 @@ impl Response { self.init.get().headers.as_deref() } - /// R-2 `JsCell` escape hatch — single-JS-thread invariant. Centralises the - /// `unsafe { self.init.get_mut() }` deref so the four call sites - /// ([`get_init_headers_mut`], [`header`], [`get_or_create_headers`], - /// [`get_content_type`]) read it as a plain `&mut Init`. - /// - /// # Safety (encapsulated) - /// `Response` is JS-thread-affine (`!Sync`) and `init` is never reborrowed - /// across re-entrant JS; the returned `&mut Init` is held only for FFI - /// out-param writes (`FetchHeaders::fast_get`/`put`) that do not call back - /// into Response host-fns, so no overlapping `&mut Init` is live. - #[inline] - #[allow(clippy::mut_from_ref)] - fn init_mut(&self) -> &mut Init { - // SAFETY: see fn doc — single-JS-thread, no overlapping `&mut Init`. - unsafe { self.init.get_mut() } - } - + /// The init headers as the `&mut` the C++ accessors take (opaque ZST + /// handle — see [`HeadersRef::headers`]). #[inline] #[allow(clippy::mut_from_ref)] pub(crate) fn get_init_headers_mut(&self) -> Option<&mut FetchHeaders> { - self.init_mut().headers.as_deref_mut() + self.init.get().headers.as_ref().map(HeadersRef::headers) } /// Deep-copy this response's init headers (if any) into a fresh - /// `HeadersRef`. Centralises the `FetchHeaders::clone_this` + - /// `HeadersRef::adopt` pair so callers stay `unsafe`-free. + /// `HeadersRef`. #[inline] pub(crate) fn clone_init_headers( &self, global: &JSGlobalObject, ) -> JsResult> { - match self.init_mut().headers.as_ref() { + match self.init.get().headers.as_ref() { Some(headers) => headers.clone_this(global), None => Ok(None), } @@ -416,24 +332,18 @@ impl Response { pub(crate) fn estimated_size(this: &Response) -> usize { this.reported_estimated_size.get() } - - /// R-2: returns `&mut BodyValue` from `&self` via the `JsCell` escape - /// hatch. Callers must keep the borrow short and not hold it across calls - /// that may re-enter a `Response` host-fn (which could project a second - /// `&mut` to the same `body`). - #[inline] - #[allow(clippy::mut_from_ref)] - pub(crate) fn get_body_value(&self) -> &mut BodyValue { - // R-2: both `Response.body` and `Body.value` are `JsCell` — - // single-JS-thread interior-mutability boundary. See `Body::value_mut`. - self.body.get().value_mut() - } } impl Response { #[inline] pub(crate) fn get_body_len(&self) -> usize { - self.body.get().len() as usize + self.body.len() as usize + } + + /// The body's `Value` slot. + #[inline] + pub(crate) fn body_value(&self) -> &JsCell { + &self.body.value } pub(crate) fn get_form_data_encoding( @@ -451,7 +361,7 @@ impl Response { pub(crate) fn calculate_estimated_byte_size(&self) { self.reported_estimated_size.set( - self.body.get().value.get().estimated_size() + self.body.value.get().estimated_size() + self.url.get().byte_slice().len() + self.init.get().status_text.byte_slice().len() + mem::size_of::(), @@ -463,20 +373,41 @@ impl Response { ::check_body_stream_ref(self, global_object) } - pub fn to_js(&self, global_object: &JSGlobalObject) -> JSValue { - self.calculate_estimated_byte_size(); + /// Move to the heap and create the JS wrapper, which owns the initial + /// reference (released in [`Response::finalize`]). + /// Hand a heap `Response` to a new JS wrapper, which holds that reference. + #[inline] + pub fn into_js(self: Box, global_object: &JSGlobalObject) -> JSValue { + Self::create_js(RefPtr::from_box(self), global_object) + } + + #[inline] + pub fn to_js(self, global_object: &JSGlobalObject) -> JSValue { + Box::new(self).into_js(global_object) + } + + /// [`to_js`](Self::to_js) that also hands back a second, native reference. + #[inline] + pub(crate) fn to_js_retained( + self: Box, + global_object: &JSGlobalObject, + ) -> (JSValue, RefPtr) { + let this = RefPtr::from_box(self); + let native = this.clone(); + (Self::create_js(this, global_object), native) + } + + /// Hand `this` (the wrapper's reference) to a new `JSResponse`. + fn create_js(this: RefPtr, global_object: &JSGlobalObject) -> JSValue { + this.calculate_estimated_byte_size(); // `bun_jsc::generated::JSResponse::to_js` ⇒ `Response__create` (C++ // shim). Payload type is erased (`*mut ()`) at the bun_jsc tier. - // R-2: cast through `*const` then `.cast_mut()` so the payload pointer - // (which the C++ side stores into `m_ctx`) is derived from `&self` - // without forging a `&mut`. - let js_value = js::to_js( - core::ptr::from_ref::(self).cast_mut().cast::<()>(), - global_object, - ); - self.js_ref.set(JsRef::init_weak(js_value)); + let js_value = js::to_js(this.as_ptr().cast::<()>(), global_object); + this.js_ref.set(JsRef::init_weak(js_value)); - self.check_body_stream_ref(global_object); + this.check_body_stream_ref(global_object); + // The wrapper's `m_ctx` now holds this reference. + let _ = this.into_raw(); js_value } @@ -491,41 +422,34 @@ impl Response { } /// Install a [`BodyAbortListener`] so abort reaches this body after `FetchTasklet` has detached. - /// - /// SAFETY: `this` must be a live heap `Response` (stored as the listener's [`ParentRef`]). - pub(crate) unsafe fn attach_abort_signal( - this: *mut Response, + pub(crate) fn attach_abort_signal( + this: bun_ptr::ThisPtr, global: &JSGlobalObject, signal: &AbortSignal, ) { - let signal_ref = signal.ref_(); - signal.pending_activity_ref(); - let mut listener = Box::new(BodyAbortListener { - signal: signal_ref, - // SAFETY: caller contract; `this` is live and owns the box. - response: unsafe { bun_ptr::ParentRef::from_raw_mut(this) }, + let listener = bun_ptr::OwnedThis::new(BodyAbortListener { + registration: Cell::new(None), + response: this.into(), global: GlobalRef::new(global), }); - signal.add_listener( - core::ptr::from_mut(&mut *listener).cast::(), - BodyAbortListener::on_abort, - ); - // SAFETY: caller contract; `this` is live. - unsafe { (*this).abort_listener.set(Some(listener)) }; + listener + .registration + .set(Some(signal.listen_native(listener.this_ptr().into()))); + this.abort_listener.set(Some(listener)); } #[inline] pub(crate) fn set_size_hint(&self, size_hint: super::blob::SizeType) { - if let BodyValue::Locked(locked) = self.body.get().value_mut() { - locked.size_hint = size_hint; - if let Some(readable) = locked.readable.get() { - // BACKREF: see `Source::bytes()` — back-pointer owned by the - // ReadableStream; `size_hint` is `Cell<_>` so shared deref + `.set()`. - if let Some(bytes) = readable.ptr.bytes() { - bytes.size_hint.set(size_hint); + self.body.value.with_mut(|value| { + if let BodyValue::Locked(locked) = value { + locked.size_hint = size_hint; + if let Some(readable) = locked.readable.get() { + if let Some(bytes) = readable.ptr.bytes() { + bytes.size_hint.set(size_hint); + } } } - } + }); } } @@ -588,18 +512,15 @@ impl Response { pub(crate) fn get_or_create_headers( &self, global_this: &JSGlobalObject, - ) -> JsResult<&mut HeadersRef> { - // R-2 escape hatch via `init_mut()` — the returned `&mut HeadersRef` - // borrows `self.init`; callers (`get_headers`, `construct_*`) do not - // hold the borrow across calls that re-enter Response host-fns. - let init = self.init_mut(); - if init.headers.is_none() { - init.headers = Some(HeadersRef::create_empty()); - - if let BodyValue::Blob(blob) = self.body.get().value.get() { + ) -> JsResult<&mut FetchHeaders> { + if self.init.get().headers.is_none() { + self.init + .with_mut(|init| init.headers = Some(HeadersRef::create_empty())); + + if let BodyValue::Blob(blob) = self.body.value.get() { let content_type = blob.content_type_slice(); if !content_type.is_empty() { - init.headers.as_mut().unwrap().put( + self.get_init_headers_mut().unwrap().put( HTTPHeaderName::ContentType, &BunString::ascii(content_type), global_this, @@ -608,7 +529,7 @@ impl Response { } } - Ok(init.headers.as_mut().unwrap()) + Ok(self.get_init_headers_mut().unwrap()) } pub(crate) fn get_headers(this: &Self, global_this: &JSGlobalObject) -> JsResult { @@ -616,15 +537,14 @@ impl Response { } pub(crate) fn get_content_type(&self) -> JsResult>> { - // R-2 escape hatch via `init_mut()` — `fast_get` (FFI out-param write) - // does not re-enter JS. - if let Some(headers) = self.init_mut().headers.as_mut() { + // `fast_get` (FFI out-param write) does not re-enter JS. + if let Some(headers) = self.get_init_headers_mut() { if let Some(value) = headers.fast_get(HTTPHeaderName::ContentType) { return Ok(Some(value.to_utf8())); } } - if let BodyValue::Blob(blob) = self.body.get().value.get() { + if let BodyValue::Blob(blob) = self.body.value.get() { let content_type = blob.content_type_slice(); if !content_type.is_empty() { return Ok(Some(Utf8Bytes::Borrowed(content_type))); @@ -756,9 +676,7 @@ impl Response { writer.write_str("\n")?; formatter.reset_line(); - // SAFETY: R-2 `JsCell` escape hatch — `Body::write_format` takes - // `&mut self`; single-JS-thread invariant. - unsafe { self.body.get_mut() } + self.body .write_format::(&mut *formatter, writer)?; } writer.write_str("\n")?; @@ -775,81 +693,32 @@ impl Response { ) -> JsResult { this.throw_if_body_unusable(global_this)?; let this_value = callframe.this(); - let cloned = this.clone(global_this)?; - - // SAFETY: `cloned` is a freshly-boxed Response from `clone()`. - let js_wrapper = Response::make_maybe_pooled(global_this, cloned); + let js_wrapper = Box::new(this.clone_value(global_this)?).into_js(global_this); this.sync_cloned_body_stream_caches(this_value, js_wrapper, global_this); Ok(js_wrapper) } - /// # Safety - /// `ptr` must point to a live `Response` allocation (e.g. freshly boxed via - /// [`Response::clone`]); ownership of the +1 ref transfers to the returned - /// JS wrapper. - // Safety contract is documented above; callers pass freshly-boxed pointers. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn make_maybe_pooled(global_object: &JSGlobalObject, ptr: *mut Response) -> JSValue { - // SAFETY: caller contract — `ptr` is live and uniquely owned. - unsafe { (*ptr).to_js(global_object) } + /// Move a fetch/S3-built `Response` to the heap and hand it to a new JS wrapper. + #[inline] + pub(crate) fn make_maybe_pooled(global_object: &JSGlobalObject, response: Response) -> JSValue { + response.to_js(global_object) } pub(crate) fn clone_value(&self, global_this: &JSGlobalObject) -> JsResult { - let body = Body::new(self.clone_body_value_via_cached_stream(global_this)?); - // `Body` has NO `Drop`; arm a guard so the - // `?` below releases the cloned body payload. - let body = scopeguard::guard(body, |b| b.reset()); + let body = self.clone_body_value_via_cached_stream(global_this)?; let init = self.init.get().clone(global_this)?; - Ok(Response { - body: JsCell::new(scopeguard::ScopeGuard::into_inner(body)), - init: JsCell::new(init), - url: JsCell::new(self.url.get().clone()), - redirected: Cell::new(self.redirected.get()), - ..Default::default() - }) - } - - pub(crate) fn clone(&self, global_this: &JSGlobalObject) -> JsResult<*mut Response> { - Ok(bun_core::heap::into_raw(Box::new( - self.clone_value(global_this)?, - ))) - } - - fn destroy(this: *mut Response) { - // SAFETY: ref_count hit 0; this is the unique owner - unsafe { - // We assign safe-empty values rather than `drop_in_place` so the - // struct stays in a valid (all-empty) state if `on_finalize()` - // returns false and the allocation outlives this call until the - // last WeakRef releases it. - // - // - `Init` field drop glue releases `headers` (HeadersRef::Drop → - // C++ deref) and `status_text` (WTF deref). - // - `Body` has NO `Drop`; `reset()` is the explicit cleanup API - // (Body.rs renames `deinit` → `reset`). `drop_in_place` here - // would leak refcounted payloads (WTFStringImpl, Blob store). - // - `url` — assignment drops the old value (WTF deref). - // - `JsRef` — assignment drops the `Strong` arm (block slot released). - (*this).init.set(Init::default()); - (*this).body.get_mut().reset(); - (*this).url.set(BunString::EMPTY); - (*this).js_ref.set(JsRef::empty()); - (*this).abort_listener.set(None); - - // Contents are gone; the allocation itself stays until any outstanding - // WeakRef derefs (RequestContext.response_weakref). WeakRef.get() returns - // null from here on. - if (*this).weak_ptr_data.on_finalize() { - // Do NOT use heap::take — that would re-run field drop glue - // on init/url/js_ref. They are now safe-empty so the second drop - // would be a no-op, but it is still wasted work and fragile under - // future field additions; free the allocation with a raw dealloc. - let layout = std::alloc::Layout::new::(); - std::alloc::dealloc(this.cast::(), layout); - } - } - } - + Ok(Response::new( + init, + body, + self.url.get().clone(), + self.redirected.get(), + JsRef::empty(), + )) + } + + /// The JS wrapper is being collected; its reference is released after + /// this (fetch / HTMLRewriter refs and any outstanding `WeakRef` may keep + /// the allocation past that). pub fn finalize(&self) { self.js_ref.with_mut(JsRef::finalize); } @@ -859,25 +728,19 @@ impl Response { callframe: &CallFrame, ) -> JsResult { // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 - // SAFETY: `bun_vm()` returns a raw `*mut VirtualMachine` (PORTING.md - // §raw-ptr) — borrow it for the duration of args parsing. let mut args = bun_jsc::ArgumentsSlice::init(global_this.bun_vm(), callframe.arguments()); - // `Init`'s field drop glue releases its refs on `?`. `Body` has NO `Drop` and its - // `WTFStringImpl` arm is a raw `*mut` (no drop glue), so wrap the - // stack value in a scopeguard that calls `body.reset()` - // on early return; disarmed before `heap::alloc`. - let response = scopeguard::guard( - Response { - body: JsCell::new(Body::new(BodyValue::Empty)), - init: JsCell::new(Init { - status_code: 200, - ..Default::default() - }), + // `Init`'s and `Body`'s field drop glue releases their refs on `?`. + let response = Box::new(Response::new( + Init { + status_code: 200, ..Default::default() }, - |r| r.body.get().reset(), - ); + BodyValue::Empty, + BunString::EMPTY, + false, + JsRef::empty(), + )); let json_value = args.next_eat().unwrap_or_default(); if !json_value.is_empty() { @@ -906,9 +769,10 @@ impl Response { if !str.is_empty() { debug_assert!(str.tag() == bun_core::Tag::WTFStringImpl); - let value = &response.body.get().value; - value.set(BodyValue::WTFStringImpl(str.leak_wtf_impl())); - value.with_mut(|v| v.to_blob_if_possible()); + response.body.value.with_mut(|v| { + *v = BodyValue::WTFStringImpl(str.into_wtf().unwrap()); + v.to_blob_if_possible(); + }); } } @@ -934,12 +798,7 @@ impl Response { &BunString::ascii(json_mime.value.as_ref()), global_this, )?; - // Disarm the body-reset guard: all fallible ops have succeeded. - let response = scopeguard::ScopeGuard::into_inner(response); - // Ownership transfers to the JSC wrapper (freed via `finalize`). - let ptr = bun_core::heap::into_raw(Box::new(response)); - // SAFETY: `ptr` is freshly boxed and uniquely owned here. - Ok(unsafe { (*ptr).to_js(global_this) }) + Ok(response.into_js(global_this)) } fn validate_redirect_status_code( @@ -962,30 +821,28 @@ impl Response { callframe: &CallFrame, ) -> JsResult { let response = Self::construct_redirect_impl(global_this, callframe)?; - // Ownership transfers to the JSC wrapper (freed via `finalize`). - let ptr = bun_core::heap::into_raw(Box::new(response)); - // SAFETY: `ptr` is freshly boxed and uniquely owned here. - Ok(unsafe { (*ptr).to_js(global_this) }) + Ok(response.into_js(global_this)) } pub(crate) fn construct_redirect_impl( global_this: &JSGlobalObject, callframe: &CallFrame, - ) -> JsResult { + ) -> JsResult> { // https://github.com/remix-run/remix/blob/db2c31f64affb2095e4286b91306b96435967969/packages/remix-server-runtime/responses.ts#L4 - // SAFETY: see `construct_json`. let mut args = bun_jsc::ArgumentsSlice::init(global_this.bun_vm(), callframe.arguments()); let url_string: BunString; - let response: Response = 'brk: { - let response = Response { - init: JsCell::new(Init { + let response: Box = 'brk: { + let response = Box::new(Response::new( + Init { status_code: 302, ..Default::default() - }), - body: JsCell::new(Body::new(BodyValue::Empty)), - ..Default::default() - }; + }, + BodyValue::Empty, + BunString::EMPTY, + false, + JsRef::empty(), + )); let url_string_value = args.next_eat().unwrap_or_default(); url_string = if url_string_value.is_empty() { @@ -1033,36 +890,31 @@ impl Response { global_this: &JSGlobalObject, _callframe: &CallFrame, ) -> JsResult { - // Ownership transfers to the JSC wrapper (freed via `finalize`). - let response = bun_core::heap::into_raw(Box::new(Response { - init: JsCell::new(Init { + let response = Box::new(Response::new( + Init { status_code: 0, ..Default::default() - }), - body: JsCell::new(Body::new(BodyValue::Empty)), - ..Default::default() - })); + }, + BodyValue::Empty, + BunString::EMPTY, + false, + JsRef::empty(), + )); - // SAFETY: `response` is freshly boxed and uniquely owned here. - let js_value = unsafe { (*response).to_js(global_this) }; - // SAFETY: `to_js` does not free the payload; still uniquely owned. - unsafe { (*response).js_ref.set(JsRef::init_weak(js_value)) }; - Ok(js_value) + Ok(response.into_js(global_this)) } // Hand-written: the constructor signature includes js_this (the pre-allocated - // JS wrapper), which `#[bun_jsc::host_fn]` has no variant for. + // JS wrapper), which `#[bun_jsc::host_fn]` has no variant for. The returned + // box is the wrapper's reference. pub(crate) fn constructor( global_this: &JSGlobalObject, callframe: &CallFrame, js_this: JSValue, - ) -> JsResult<*mut Response> { + ) -> JsResult> { let arguments = callframe.arguments_as_array::<2>(); if !arguments[0].is_undefined_or_null() && arguments[0].is_object() { - // `as_class_ref` is the safe shared-borrow downcast (one audited - // unsafe in `JSValue`); only `&self` accessors (`is_s3`, `store`) - // are touched on this path. if let Some(blob) = arguments[0].as_class_ref::() { if blob.is_s3() { if !arguments[1].is_empty_or_undefined_or_null() { @@ -1070,15 +922,16 @@ impl Response { "new Response(s3File) do not support ResponseInit options", ))); } - let response = Response { - init: JsCell::new(Init { + let response = Response::new( + Init { status_code: 302, ..Default::default() - }), - body: JsCell::new(Body::new(BodyValue::Empty)), - js_ref: JsCell::new(JsRef::init_weak(js_this)), - ..Default::default() - }; + }, + BodyValue::Empty, + BunString::EMPTY, + false, + JsRef::init_weak(js_this), + ); let s3 = blob.store.get().as_ref().unwrap().data.as_s3(); let credentials = s3.get_credentials(); @@ -1115,11 +968,11 @@ impl Response { &BunString::ascii(&result.url), global_this, )?; - return Ok(bun_core::heap::into_raw(Box::new(response))); + return Ok(Box::new(response)); } } } - let mut init: Init = 'brk: { + let init: Init = 'brk: { if arguments[1].is_undefined_or_null() { break 'brk Init { status_code: 200, @@ -1135,16 +988,12 @@ impl Response { ))); }; - let body: Body = 'brk: { + let body: BodyValue = 'brk: { if arguments[0].is_undefined_or_null() { - break 'brk Body::new(BodyValue::Null); + break 'brk BodyValue::Null; } - // `Body::extract` is a free fn re-exported as `body::extract`. super::body::extract(global_this, arguments[0])? }; - // `Body` has NO `Drop`; arm a guard so the - // error returns below release the extracted body payload. - let body = scopeguard::guard(body, |b| b.reset()); // extract() throws without returning Err; see Blob::from_dom_form_data if global_this.has_exception() { @@ -1152,10 +1001,10 @@ impl Response { } // Perform the only remaining fallible op BEFORE heap-allocating: - // doing it on stack locals lets `?` trigger the scopeguard and - // `init`'s drop glue and avoids leaking the heap allocation entirely. - if let BodyValue::Blob(blob) = body.value.get() { - if let Some(headers) = init.headers.as_deref_mut() { + // doing it on stack locals lets `?` run `body`'s and `init`'s drop glue + // and avoids leaking the heap allocation entirely. + if let BodyValue::Blob(blob) = &body { + if let Some(headers) = init.headers.as_ref().map(HeadersRef::headers) { let content_type = blob.content_type_slice(); if !content_type.is_empty() && !headers.fast_has(HTTPHeaderName::ContentType) { headers.put( @@ -1167,23 +1016,18 @@ impl Response { } } - // Disarm: all fallible ops have succeeded. - let body = scopeguard::ScopeGuard::into_inner(body); // Ownership transfers to the JSC wrapper (freed via `finalize`). The - // codegen constructor thunk receives this `*mut Response` and binds it - // to `js_this`. - let response = bun_core::heap::into_raw(Box::new(Response { - body: JsCell::new(body), - init: JsCell::new(init), - js_ref: JsCell::new(JsRef::init_weak(js_this)), - ..Default::default() - })); - // SAFETY: `response` is freshly boxed and uniquely owned by this fn - // until returned; reborrow for the trailing (infallible) setup. - let resp_ref = unsafe { &*response }; - - resp_ref.calculate_estimated_byte_size(); - resp_ref.check_body_stream_ref(global_this); + // codegen constructor thunk binds this box to `js_this`. + let response = Box::new(Response::new( + init, + body, + BunString::EMPTY, + false, + JsRef::init_weak(js_this), + )); + + response.calculate_estimated_byte_size(); + response.check_body_stream_ref(global_this); Ok(response) } } @@ -1249,12 +1093,7 @@ impl Init { if js_type == JSType::DOMWrapper { // fast path: it's a Request object or a Response object // we can skip calling JS getters - if let Some(req) = response_init.as_direct::() { - // SAFETY: `as_direct` returned a live `*mut Request` owned by the - // JS wrapper cell; the wrapper is rooted by `response_init` for - // the duration of this call, so no GC can finalize it here. - // Everything touched is `&self`. - let req = unsafe { &*req }; + if let Some(req) = response_init.as_direct_class_ref::() { if let Some(headers) = req.get_fetch_headers_unless_empty() { result.headers = headers.clone_this(global_this)?; } @@ -1263,10 +1102,7 @@ impl Init { return Ok(Some(result)); } - if let Some(resp) = response_init.as_direct::() { - // SAFETY: `as_direct` returned a live `*mut Response` owned by the - // JS wrapper cell; rooted by `response_init` for this call. - let resp = unsafe { &*resp }; + if let Some(resp) = response_init.as_direct_class_ref::() { return Ok(Some(resp.init.get().clone(global_this)?)); } } @@ -1280,11 +1116,7 @@ impl Init { // `FetchHeaders` is an opaque ZST FFI handle (S008) — safe deref. let orig = bun_opaque::opaque_deref_mut(orig.as_ptr()); if !orig.is_empty() { - result.headers = orig.clone_this(global_this)?.map(|p| { - // SAFETY: `clone_this` returns a fresh +1-ref'd `FetchHeaders*`; - // ownership of that ref is transferred into the `HeadersRef`. - unsafe { HeadersRef::adopt(p) } - }); + result.headers = HeadersRef::clone_from(orig, global_this)?; } } else { result.headers = HeadersRef::create_from_js(global_this, headers)?; diff --git a/src/runtime/webcore/Sink.rs b/src/runtime/webcore/Sink.rs index ae174c51748a..7cbcc9ac727c 100644 --- a/src/runtime/webcore/Sink.rs +++ b/src/runtime/webcore/Sink.rs @@ -218,26 +218,22 @@ impl JSSink { } /// Pump `stream` into the sink through a new `JSReadable*SinkController`, - /// kept as the sink's `source()`. It is installed before the pump starts - /// because the pump drains whatever the stream already holds (user code - /// included) before returning, and a sink failing in there detaches its - /// `source()`. + /// handed to `set_source` (the sink keeps it as its `source()`). It is + /// installed before the pump starts because the pump drains whatever the + /// stream already holds (user code included) before returning, and a sink + /// failing in there detaches its `source()`. `ptr` becomes the + /// controller's `m_sinkPtr`. pub fn assign_to_stream( global: &crate::webcore::jsc::JSGlobalObject, stream: crate::webcore::jsc::JSValue, - mut ptr: NonNull, + ptr: NonNull, + set_source: impl FnOnce(SourceHandle), ) -> crate::webcore::jsc::JSValue where T: JsSinkType, { - // SAFETY: `ptr` is a live sink owned by the caller for this synchronous - // call; the pointer is only stashed in C++ `m_sinkPtr`. - let ptr = unsafe { ptr.as_mut() }; - let controller = - T::create_controller_extern(global, std::ptr::from_mut::(ptr).cast::()); - if let Some(src) = ptr.source() { - *src = streams::SourceHandle::JSController(controller); - } + let controller = T::create_controller_extern(global, ptr.as_ptr().cast::()); + set_source(streams::SourceHandle::JSController(controller)); let result = streams::controller_abi::assign_to_stream(global, stream, controller); // Setup threw (e.g. a direct stream's `pull` getter): nothing will ever // end()/close() the controller, and its destructor would otherwise run @@ -275,6 +271,14 @@ impl JSSink { } } +/// See [`JsSinkType::FINALIZE`]. +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum FinalizeReceiver { + ThisPtr, + Mut, + Box, +} + /// Trait collecting every method `JSSink` may call on the wrapped `SinkType`. /// Most of these are optional, modeled with default method bodies and /// associated `const` gates. @@ -294,13 +298,29 @@ pub trait JsSinkType: Sized + JsSinkAbi { /// `Start::from_js_with_tag` branch in `JSSink::js_start`. const START_TAG: Option = None; + /// How `${abi}__finalize` / `${abi}__controllerFinalize` hand `m_sinkPtr` + /// over: which of [`finalize`](Self::finalize) / + /// [`finalize_mut`](Self::finalize_mut) / [`finalize_boxed`](Self::finalize_boxed) + /// the codegen'd thunk calls. + const FINALIZE: FinalizeReceiver = FinalizeReceiver::ThisPtr; + fn memory_cost(&self) -> usize; /// `${abi}__finalize`: the JS wrapper cell holding `this` as `m_sinkPtr` /// is giving up its claim on the sink, and never uses it again. `ThisPtr`, - /// not `&mut self`: for `ArrayBufferSink`, `FileSink` and - /// `FetchRequestBodySink` that releases the allocation, and freeing under - /// a live reference argument is UB. + /// not `&mut self`: for `FileSink` and `FetchRequestBodySink` that + /// releases the allocation, and freeing under a live reference argument + /// is UB. fn finalize(this: bun_ptr::ThisPtr); + /// [`FinalizeReceiver::Mut`]: the sink outlives the call (someone else + /// frees it), so the wrapper's pointer is a plain exclusive borrow. + fn finalize_mut(&mut self) { + unreachable!("JsSinkType::finalize_mut on {}", Self::NAME); + } + /// [`FinalizeReceiver::Box`]: the JS wrapper was the sink's sole owner + /// (`construct` leaked a `Box` into `m_sinkPtr`) and hands it back. + fn finalize_boxed(self: Box) { + unreachable!("JsSinkType::finalize_boxed on {}", Self::NAME); + } /// `${abi}__controllerFinalize`: a `JSReadable*Controller` died still /// attached to the sink (heap teardown; a live controller detaches first). /// A sink whose controller path holds a different claim than its wrapper @@ -389,19 +409,20 @@ pub trait JsSinkType: Sized + JsSinkAbi { // no lut entry and no C++ caller. // ────────────────────────────────────────────────────────────────────────── +/// Resolves a host call's `this` to the wrapper's sink (the codegen'd +/// `${name}__getThis`, which owns the `m_sinkPtr` deref). +pub type GetThis<'a, T> = fn( + &crate::webcore::jsc::JSGlobalObject, + &crate::webcore::jsc::CallFrame, +) -> crate::webcore::jsc::JsResult<&'a mut JSSink>; + impl JSSink { - /// `JSSink.getThis` — recover `&mut JSSink` from `callframe.this()` or - /// throw the appropriate detached/cast-failed error. - /// - /// Returns an unbounded `&'a mut`: the sink lives in its own heap - /// allocation behind the JS wrapper cell (allocated by `construct`, freed - /// by codegen `finalize`), so its lifetime is independent of `global`/`frame`. Host - /// fns are single-threaded and synchronous — only one `&mut JSSink` per - /// `this` is live for the body of each host call. - fn get_this<'a>( + /// `JSSink.getThis` — the `m_sinkPtr` of `callframe.this()`, or the + /// appropriate detached/cast-failed error. + pub fn this_ptr_from_frame( global: &crate::webcore::jsc::JSGlobalObject, frame: &crate::webcore::jsc::CallFrame, - ) -> crate::webcore::jsc::JsResult<&'a mut JSSink> { + ) -> crate::webcore::jsc::JsResult>> { let raw = T::from_js_extern(frame.this()); match raw { from_js_result::DETACHED => Err(global.throw(format_args!( @@ -410,9 +431,7 @@ impl JSSink { ))), from_js_result::CAST_FAILED => Err(bun_jsc::ErrorCode::INVALID_THIS .throw(global, format_args!("Expected {}", T::NAME))), - // SAFETY: codegen returns a non-null `*mut JSSink` for live - // wrappers; see fn doc for the `'a` justification. - ptr => Ok(unsafe { &mut *(ptr as *mut JSSink) }), + ptr => Ok(NonNull::new(ptr as *mut JSSink).expect("non-sentinel m_sinkPtr")), } } @@ -431,14 +450,17 @@ impl JSSink { } /// `${abi_name}__write` host-fn body. - pub(crate) fn js_write( + pub(crate) fn js_write<'a>( global: &crate::webcore::jsc::JSGlobalObject, frame: &crate::webcore::jsc::CallFrame, - ) -> crate::webcore::jsc::JsResult { + get_this: GetThis<'a, T>, + ) -> crate::webcore::jsc::JsResult + where + T: 'a, + { use crate::webcore::jsc::JSValue; bun_core::mark_binding!(); - // SAFETY: get_this returns a live ThisSink* on Ok. - let this = Self::get_this(global, frame)?; + let this = get_this(global, frame)?; if let Some(err) = this.sink.get_pending_error() { return Err(global.throw_value(err)); @@ -505,15 +527,19 @@ impl JSSink { } /// `${abi_name}__flush` host-fn body. - pub(crate) fn js_flush( + pub(crate) fn js_flush<'a>( global: &crate::webcore::jsc::JSGlobalObject, frame: &crate::webcore::jsc::CallFrame, - ) -> crate::webcore::jsc::JsResult { + get_this: GetThis<'a, T>, + ) -> crate::webcore::jsc::JsResult + where + T: 'a, + { use crate::webcore::jsc::JSValue; use bun_sys_jsc::ErrorJsc; bun_core::mark_binding!(); - let this = Self::get_this(global, frame)?; + let this = get_this(global, frame)?; if let Some(err) = this.sink.get_pending_error() { return Err(global.throw_value(err)); @@ -536,10 +562,14 @@ impl JSSink { } /// `${abi_name}__start` host-fn body. - pub(crate) fn js_start( + pub(crate) fn js_start<'a>( global: &crate::webcore::jsc::JSGlobalObject, frame: &crate::webcore::jsc::CallFrame, - ) -> crate::webcore::jsc::JsResult { + get_this: GetThis<'a, T>, + ) -> crate::webcore::jsc::JsResult + where + T: 'a, + { use crate::webcore::jsc::JSValue; use bun_sys_jsc::ErrorJsc; bun_core::mark_binding!(); @@ -557,7 +587,7 @@ impl JSSink { streams::Start::Empty }; - let this = Self::get_this(global, frame)?; + let this = get_this(global, frame)?; if let Some(err) = this.sink.get_pending_error() { return Err(global.throw_value(err)); @@ -570,15 +600,18 @@ impl JSSink { } /// `${abi_name}__end` host-fn body. - pub(crate) fn js_end( + pub(crate) fn js_end<'a>( global: &crate::webcore::jsc::JSGlobalObject, frame: &crate::webcore::jsc::CallFrame, - ) -> crate::webcore::jsc::JsResult { + get_this: GetThis<'a, T>, + ) -> crate::webcore::jsc::JsResult + where + T: 'a, + { use bun_sys_jsc::ErrorJsc; bun_core::mark_binding!(); - // SAFETY: get_this returns a live ThisSink* on Ok. - let this = Self::get_this(global, frame)?; + let this = get_this(global, frame)?; if let Some(err) = this.sink.get_pending_error() { return Err(global.throw_value(err)); @@ -744,104 +777,32 @@ impl JSSink { // routes through `SinkHandle::write`. // ────────────────────────────────────────────────────────────────────────── -/// Map a C++ `WebCore::SinkID` + erased `m_sinkPtr` to a [`SinkHandle`]. -/// -/// `ptr` is the `m_sinkPtr` stored on the JS wrapper (a `*mut JSSink` for -/// the `T` selected by `id`); `JSSink` is `#[repr(transparent)]` over `T`, -/// so the cast to `*mut T` is an address-preserving no-op. -/// -/// # Safety -/// `ptr` must be a live, properly-aligned pointer to the concrete sink type -/// that `id` names (the same pointer the generated `${name}__*` thunks -/// receive), valid for the lifetime of the returned handle. -pub(crate) unsafe fn sink_handle_from_id( - id: u8, - ptr: NonNull, -) -> crate::webcore::SinkHandle { - use crate::webcore::SinkHandle; - // Mirrors `enum SinkID` in src/jsc/bindings/Sink.h. - const ARRAY_BUFFER_SINK: u8 = 0; - const FILE_SINK: u8 = 2; - const HTML_REWRITER_SINK: u8 = 3; - const HTTP_RESPONSE_SINK: u8 = 4; - const HTTPS_RESPONSE_SINK: u8 = 5; - const NETWORK_SINK: u8 = 6; - const FETCH_REQUEST_BODY_SINK: u8 = 7; - - let raw = ptr.as_ptr(); - match id { - // SAFETY: caller contract — `raw` is a live `*mut ArrayBufferSink`. - ARRAY_BUFFER_SINK => SinkHandle::ArrayBuffer(unsafe { - bun_ptr::BackRef::from_raw_mut(raw.cast::()) - }), - // SAFETY: caller contract — `raw` is a live `*mut FileSink`. - FILE_SINK => SinkHandle::FileSink(unsafe { - bun_ptr::BackRef::from_raw(raw.cast::()) - }), - // SAFETY: caller contract — `raw` is a live `*mut RewriterPipe`. - HTML_REWRITER_SINK => SinkHandle::HTMLRewriter(unsafe { - bun_ptr::BackRef::from_raw(raw.cast::()) - }), - // SAFETY: caller contract — `raw` is a live `*mut HTTPResponseSink`. - HTTP_RESPONSE_SINK => SinkHandle::HttpResponse(unsafe { - bun_ptr::BackRef::from_raw_mut(raw.cast::()) - }), - // SAFETY: caller contract — `raw` is a live `*mut HTTPSResponseSink`. - HTTPS_RESPONSE_SINK => SinkHandle::HttpsResponse(unsafe { - bun_ptr::BackRef::from_raw_mut(raw.cast::()) - }), - // SAFETY: caller contract — `raw` is a live `*mut NetworkSink`. - NETWORK_SINK => SinkHandle::S3Upload(unsafe { - bun_ptr::BackRef::from_raw_mut(raw.cast::()) - }), - // SAFETY: caller contract — `raw` is a live `*mut FetchRequestBodySink`. - FETCH_REQUEST_BODY_SINK => SinkHandle::FetchRequestBody(unsafe { - bun_ptr::BackRef::from_raw_mut( - raw.cast::(), - ) - }), - // 1 (TextSink) and any unknown id → no native sink. - _ => SinkHandle::None, - } -} - /// Route a borrowed byte chunk from a native transform (`JSTransformStream` /// with `m_nativeSinkPtr` attached) into the concrete sink via -/// [`SinkHandle::write`]. +/// [`SinkHandle::write`]. The codegen'd `Bun__NativeTransformSink__writeBytes` +/// (generated_jssink.rs) maps `SinkID` + `m_sinkPtr` to `handle`. /// /// Return shape matches [`streams::result::Writable::to_js`] so /// `nativeSinkWriteIsBackpressure` reads a negative number / pending promise /// exactly as the previous `js_write_bytes` path produced. No /// [`JsSinkType::get_pending_error`] guard: every sink uses the trait-default /// `None`, so omitting it is behavior-preserving. -#[unsafe(no_mangle)] -#[allow(clippy::not_unsafe_ptr_arg_deref)] -pub extern "C" fn Bun__NativeTransformSink__writeBytes( - sink_id: u8, - sink_ptr: *mut c_void, +pub use crate::generated_jssink::sink_handle_from_id; + +pub(crate) fn native_transform_sink_write( + handle: crate::webcore::SinkHandle, global: &JSGlobalObject, - ptr: *const u8, - len: usize, + bytes: &[u8], ) -> JSValue { bun_core::mark_binding!(); - let Some(sink_ptr) = NonNull::new(sink_ptr) else { - return JSValue::js_number(0.0); - }; - if len == 0 || ptr.is_null() { + if bytes.is_empty() { return JSValue::js_number(0.0); } - // SAFETY: C++ caller passes a live `m_sinkPtr` of the type `sink_id` - // names, valid for the duration of this synchronous call. - let handle = unsafe { sink_handle_from_id(sink_id, sink_ptr) }; if handle.is_none() { return JSValue::UNDEFINED; } - // SAFETY: caller guarantees `[ptr, ptr+len)` is a live readable byte - // buffer for the duration of this call (a GC-kept `JSArrayBufferView` or - // a caller-owned scratch buffer). - let slice = unsafe { core::slice::from_raw_parts(ptr, len) }; handle - .write(&streams::Result::Temporary(bun_ptr::RawSlice::new(slice))) + .write(&streams::Result::Temporary(bun_ptr::RawSlice::new(bytes))) .to_js(global) } @@ -884,38 +845,36 @@ pub(crate) fn destructor_ptr_subprocess(ptr: *const c_void) -> usize { ((ptr as usize as u64 & ADDR_MASK) | (SUBPROCESS_TAG << ADDR_BITS)) as usize } -#[unsafe(no_mangle)] -pub(crate) extern "C" fn Bun__onSinkDestroyed(ptr_value: *mut c_void, sink_ptr: *mut c_void) { - let _ = sink_ptr; // autofix - let ptr = DestructorPtr::from(Some(ptr_value)); - - if ptr.is_null() { - return; - } - - // `is::()` covers the typed member and the Subprocess arm is - // matched by `is_valid()` below. - if ptr.is::() { - return; - } - if ptr.is_valid() { - // `Subprocess<'_>` cannot implement `UnionMember` (lifetime param), so - // it isn't part of `DestructorPtr`'s type list — cast the raw pointer - // directly (see `destructor_ptr_subprocess`, which encodes it). - // - // The decoded pointer must be - // masked to the low 49 address bits. `DestructorPtr::ptr()` is - // `TaggedPtr::to()` and *preserves* the tag bits (round-trip encoding), - // so casting that would hand `on_stdin_destroyed` a pointer with - // `0x07fe…` in the high word and ASAN SEGVs on the first field load. - // Use the masked address. - // - // SAFETY: caller (C++) guarantees a valid non-Detached tag points at a live - // Subprocess. - let subprocess: &mut Subprocess<'_> = - unsafe { &mut *(ptr.as_uintptr() as usize as *mut Subprocess<'_>) }; - subprocess.on_stdin_destroyed(); - return; - } - bun_core::debug_warn!("Unknown sink type"); +/// What a [`DestructorPtr`] names; decoded for the codegen'd +/// `Bun__onSinkDestroyed` (generated_jssink.rs), which owns the deref. +pub(crate) enum Destructor { + None, + Detached, + /// The masked address of the `Subprocess` whose stdin sink is going away. + Subprocess(*mut Subprocess<'static>), + Unknown, +} + +impl Destructor { + pub(crate) fn decode(ptr: DestructorPtr) -> Destructor { + if ptr.is_null() { + return Destructor::None; + } + // `is::()` covers the typed member and the Subprocess arm is + // matched by `is_valid()` below. + if ptr.is::() { + return Destructor::Detached; + } + if ptr.is_valid() { + // `Subprocess<'_>` cannot implement `UnionMember` (lifetime param), so + // it isn't part of `DestructorPtr`'s type list (see + // `destructor_ptr_subprocess`, which encodes it). The decoded + // pointer must be masked to the low 49 address bits: + // `DestructorPtr::ptr()` is `TaggedPtr::to()` and *preserves* the tag + // bits (round-trip encoding), so casting that would hand + // `on_stdin_destroyed` a pointer with `0x07fe…` in the high word. + return Destructor::Subprocess(ptr.as_uintptr() as usize as *mut Subprocess<'static>); + } + Destructor::Unknown + } } diff --git a/src/runtime/webcore/fetch.rs b/src/runtime/webcore/fetch.rs index ba84c7ace883..b058ed6e4520 100644 --- a/src/runtime/webcore/fetch.rs +++ b/src/runtime/webcore/fetch.rs @@ -168,7 +168,7 @@ fn data_url_response(url: BunString, global_this: &JSGlobalObject) -> JSValue { } }; - let response = bun_core::heap::into_raw(Box::new(Response::init( + let response = Response::init( response::Init { status_code: 200, status_text: BunString::create_atom(b"OK"), @@ -177,15 +177,12 @@ fn data_url_response(url: BunString, global_this: &JSGlobalObject) -> JSValue { Body::new(BodyValue::Blob(blob)), url, false, - ))); + ); - // Ownership of the boxed Response is transferred to the JS GC via - // `make_maybe_pooled` (which stores the raw `*mut Response` in the wrapper - // and finalizes it). Dropping a `Box` here would be a UAF. + // Ownership of the Response is transferred to the JS GC via + // `make_maybe_pooled` (the wrapper holds and finalizes it). JSPromise::resolved_promise_value( global_this, - // SAFETY: `response` is a freshly allocated heap `Response`; ownership - // transfers to JSC. Response::make_maybe_pooled(global_this, response), ) } @@ -1007,8 +1004,7 @@ fn fetch_impl( } if let Some(req) = request_mut!() { - let body_value = req.get_body_value(); - let already_used = match body_value { + let already_used = match req.body_value().get() { BodyValue::Used => true, BodyValue::Locked(locked) => { locked.action != BodyValueLockedAction::None @@ -1025,7 +1021,7 @@ fn fetch_impl( .throw()); } - if matches!(*body_value, BodyValue::Locked(_)) { + if matches!(req.body_value().get(), BodyValue::Locked(_)) { if let Some(readable) = req.get_body_readable_stream() { if readable.is_disturbed(global_this) || readable.is_locked(global_this) { return Err(global_this @@ -1039,34 +1035,31 @@ fn fetch_impl( readable_stream::Strong::init(readable, global_this), )); } - let body_value = req.get_body_value(); - if let BodyValue::Locked(locked) = body_value { - if locked.readable.has() { - break 'extract_body Some(HTTPRequestBody::ReadableStream( - readable_stream::Strong::init( - locked.readable.get().unwrap(), - global_this, - ), - )); - } - } - let readable = body_value.to_readable_stream(global_this)?; - if !readable.is_empty_or_undefined_or_null() { - if let BodyValue::Locked(locked) = body_value { + let held = req.body_value().with_mut(|body_value| { + if let BodyValue::Locked(locked) = &mut *body_value { if locked.readable.has() { - break 'extract_body Some(HTTPRequestBody::ReadableStream( - readable_stream::Strong::init( - locked.readable.get().unwrap(), - global_this, - ), - )); + return Ok(locked.readable.get()); + } + } + let readable = body_value.to_readable_stream(global_this)?; + if !readable.is_empty_or_undefined_or_null() { + if let BodyValue::Locked(locked) = body_value { + if locked.readable.has() { + return Ok(locked.readable.get()); + } } } + Ok::<_, jsc::JsError>(None) + })?; + if let Some(readable) = held { + break 'extract_body Some(HTTPRequestBody::ReadableStream( + readable_stream::Strong::init(readable, global_this), + )); } } break 'extract_body Some(HTTPRequestBody::AnyBlob( - req.get_body_value().use_as_any_blob(), + req.body_value().with_mut(|v| v.use_as_any_blob()), )); } @@ -1312,7 +1305,7 @@ fn fetch_impl( break 'blob Blob::find_or_create_file_from_path(&mut pathlike, global_this, true); }; - let response = bun_core::heap::into_raw(Box::new(Response::init( + let response = Response::init( response::Init { status_code: 200, ..Default::default() @@ -1320,14 +1313,12 @@ fn fetch_impl( Body::new(BodyValue::Blob(blob_to_use)), url_string, false, - ))); + ); - // Ownership of the boxed Response transfers to the JS GC; see + // Ownership of the Response transfers to the JS GC; see // `data_url_response` for the rationale. return Ok(JSPromise::resolved_promise_value( global_this, - // SAFETY: `response` is a freshly allocated heap `Response`; ownership - // transfers to JSC. Response::make_maybe_pooled(global_this, response), )); } @@ -1853,7 +1844,7 @@ impl<'a> S3StreamWrapper<'a> { // Box and Box<[u8]> Drop at end of scope. match result { s3::S3UploadResult::Success => { - let response = Box::new(Response::init( + let response = Response::init( response::Init { method: Method::PUT, status_code: 200, @@ -1862,16 +1853,14 @@ impl<'a> S3StreamWrapper<'a> { Body::new(BodyValue::Empty), BunString::create_atom_if_possible(self_.url.href), false, - )); - // SAFETY: `into_raw` yields a freshly allocated heap `Response`; - // ownership transfers to JSC. - let response_js = - Response::make_maybe_pooled(global, bun_core::heap::into_raw(response)); + ); + // Ownership transfers to JSC. + let response_js = Response::make_maybe_pooled(global, response); response_js.ensure_still_alive(); self_.promise.resolve(global, response_js)?; } s3::S3UploadResult::Failure(err) => { - let response = Box::new(Response::init( + let response = Response::init( response::Init { method: Method::PUT, status_code: 500, @@ -1884,12 +1873,10 @@ impl<'a> S3StreamWrapper<'a> { })), BunString::create_atom_if_possible(self_.url.href), false, - )); + ); - // SAFETY: `into_raw` yields a freshly allocated heap `Response`; - // ownership transfers to JSC. - let response_js = - Response::make_maybe_pooled(global, bun_core::heap::into_raw(response)); + // Ownership transfers to JSC. + let response_js = Response::make_maybe_pooled(global, response); response_js.ensure_still_alive(); self_.promise.resolve(global, response_js)?; } diff --git a/src/runtime/webcore/fetch/FetchRequestBodySink.rs b/src/runtime/webcore/fetch/FetchRequestBodySink.rs index 61cfb1078fb5..014ebca47892 100644 --- a/src/runtime/webcore/fetch/FetchRequestBodySink.rs +++ b/src/runtime/webcore/fetch/FetchRequestBodySink.rs @@ -61,7 +61,7 @@ pub struct FetchRequestBodySink { pub high_water_mark: BlobSizeType, /// Shared pending drain promise for `write()` and `flush(true)`; resolved /// in `on_drain()`. - pub pending: WritablePending, + pub pending: bun_ptr::JsCell, /// Bytes written since last on_drain; >0 guarantees a drain ack is owed. pub pending_bytes: BlobSizeType, pub ended: bool, @@ -74,7 +74,7 @@ impl Default for FetchRequestBodySink { task: None, source: SourceHandle::default(), high_water_mark: 16384, - pending: WritablePending::default(), + pending: bun_ptr::JsCell::new(WritablePending::default()), pending_bytes: 0, ended: false, done: false, @@ -141,9 +141,10 @@ impl FetchRequestBodySink { // on each so the event loop reaches the I/O poll before resuming; batching // sync writes here lets the HTTP thread re-enqueue the drain ack before // the microtask loop yields and starves uWS callbacks and timers. - self.pending.consumed = len; - self.pending.result = Writable::Owned(len); - Writable::Pending(core::ptr::from_mut(&mut self.pending)) + let pending = self.pending.get_mut_unique(); + pending.consumed = len; + pending.result = Writable::Owned(len); + Writable::Pending(bun_ptr::BackRef::new(&self.pending)) } pub fn write(&mut self, data: &StreamResult) -> Writable { @@ -174,9 +175,9 @@ impl FetchRequestBodySink { wait: bool, ) -> bun_sys::Result { use crate::webcore::streams::PendingState; - if self.pending.state == PendingState::Pending { + if self.pending.get().state == PendingState::Pending { return bun_sys::Result::Ok( - JSPromise::opaque_ref(self.pending.promise(global_this)).to_js(), + JSPromise::opaque_ref(self.pending.get_mut_unique().promise(global_this)).to_js(), ); } if self.done || self.ended { @@ -188,9 +189,9 @@ impl FetchRequestBodySink { if wait && self.pending_bytes > 0 { // Bytes were scheduled to the HTTP thread since the last drain ack, // so an `on_drain` is guaranteed to arrive and resolve this. - self.pending.result = Writable::Owned(self.pending_bytes); + self.pending.get_mut_unique().result = Writable::Owned(self.pending_bytes); return bun_sys::Result::Ok( - JSPromise::opaque_ref(self.pending.promise(global_this)).to_js(), + JSPromise::opaque_ref(self.pending.get_mut_unique().promise(global_this)).to_js(), ); } bun_sys::Result::Ok(JSPromise::resolved_promise_value( @@ -261,7 +262,7 @@ impl FetchRequestBodySink { pub fn on_drain(&mut self, _global_this: &JSGlobalObject) { bun_core::scoped_log!(FetchRequestBodySinkLog, "onDrain"); self.pending_bytes = 0; - self.pending.run(); + self.pending.get_mut_unique().run(); self.source.ready(None, None); } diff --git a/src/runtime/webcore/fetch/FetchTasklet.rs b/src/runtime/webcore/fetch/FetchTasklet.rs index fd72c2b4d30b..b771f7b5a076 100644 --- a/src/runtime/webcore/fetch/FetchTasklet.rs +++ b/src/runtime/webcore/fetch/FetchTasklet.rs @@ -111,7 +111,8 @@ pub struct FetchTasklet { /// response weak ref we need this to track the response JS lifetime pub(crate) response: jsc::Weak, /// native response ref if we still need it when JS is discarted - pub(crate) native_response: JsCell>>, + // `JsCell`: released from `on_body_stream_collected`, which only has a shared ref. + pub(crate) native_response: JsCell>>, /// The response body stream while this tasklet is its producer. pub(crate) response_stream: crate::webcore::byte_stream::ProducerHold, pub(crate) request_headers: Headers, @@ -477,7 +478,9 @@ impl FetchTasklet { } self.response.clear(); - self.native_response.set(None); + if let Some(response) = self.native_response.take() { + drop(response); + } self.clear_stream_handlers(); @@ -556,7 +559,7 @@ impl FetchTasklet { fn get_current_response(&self) -> Option<*mut Response> { // we need a body to resolve the promise when buffering - if let Some(response) = self.native_response.get().as_ref() { + if let Some(response) = self.native_response.get() { return Some(response.as_ptr()); } @@ -666,6 +669,7 @@ impl FetchTasklet { &global_this, stream.value, core::ptr::NonNull::from(&mut *sink), + |s| sink.source = s, ); assignment_result.ensure_still_alive(); @@ -768,8 +772,9 @@ impl FetchTasklet { if let Some(response) = self.current_response_mut() { // body value now owns the error let err = scopeguard::ScopeGuard::into_inner(err); - let body = response.get_body_value(); - body.to_error_instance(err, &global_this)?; + response + .body_value() + .with_mut(|body| body.to_error_instance(err, &global_this))?; } // Cancel the request-body sink last: closing the sink signal fires // the controller's onClose synchronously, which can re-enter the @@ -827,13 +832,10 @@ impl FetchTasklet { } } - // raw ptr: `body` and `get_fetch_headers()` are disjoint fields but borrowck can't see through the accessors. - let body: *mut BodyValue = response.get_body_value(); // `BodyAbortListener::on_abort` may have set `Error` while this // callback was queued; checked before `buffer_reset.set(false)` so // the defer still drops the bytes. - // SAFETY: just obtained from live `response`. - if !matches!(unsafe { &*body }, BodyValue::Locked(_)) { + if !matches!(response.body_value().get(), BodyValue::Locked(_)) { return Ok(()); } // we will reach here when not streaming, this is also the only case we dont wanna to reset the buffer @@ -841,40 +843,38 @@ impl FetchTasklet { if !self.result.has_more { let scheduled_response_buffer = core::mem::take(&mut self.scheduled_response_buffer.list); - // done resolve body - let old = core::mem::replace( - // SAFETY: just obtained from live `response`; uniquely accessed here. - unsafe { &mut *body }, - BodyValue::InternalBlob(InternalBlob { - bytes: scheduled_response_buffer, - was_string: false, - }), - ); - bun_output::scoped_log!( - FetchTasklet, - "onBodyReceived body_value length={}", - // SAFETY: see above. - match unsafe { &*body } { - BodyValue::InternalBlob(b) => b.bytes.len(), - _ => 0, - } - ); - self.scheduled_response_buffer = MutableString::default(); + // BodyValue::resolve takes `Option>` (opaque C++ handle + // mutated via FFI); the inherent `get_fetch_headers` returns `Option<&_>`, so + // erase the borrow into a raw NonNull. Disjoint from `body` (response.init vs + // response.body) and outlives this block. + let headers = response.get_fetch_headers().map(core::ptr::NonNull::from); + let global_this = self.global_this; + response.body_value().with_mut(|body| { + // done resolve body + let old = core::mem::replace( + body, + BodyValue::InternalBlob(InternalBlob { + bytes: scheduled_response_buffer, + was_string: false, + }), + ); + bun_output::scoped_log!( + FetchTasklet, + "onBodyReceived body_value length={}", + match &*body { + BodyValue::InternalBlob(b) => b.bytes.len(), + _ => 0, + } + ); - if matches!(old, BodyValue::Locked(_)) { - bun_output::scoped_log!(FetchTasklet, "onBodyReceived old.resolve"); - let mut old = old; - // BodyValue::resolve takes `Option>` (opaque C++ handle - // mutated via FFI); the inherent `get_fetch_headers` returns `Option<&_>`, so - // erase the borrow into a raw NonNull. Disjoint from `body` (response.init vs - // response.body) and outlives this block. - let headers = response.get_fetch_headers().map(core::ptr::NonNull::from); - // SAFETY: `body` points into `response.body`, disjoint from `headers` - // (response.init); both live for this block. - let body = unsafe { &mut *body }; - BodyValue::resolve(&mut old, body, &self.global_this, headers)?; - } + if matches!(old, BodyValue::Locked(_)) { + bun_output::scoped_log!(FetchTasklet, "onBodyReceived old.resolve"); + let mut old = old; + BodyValue::resolve(&mut old, body, &global_this, headers)?; + } + Ok::<(), bun_jsc::JsError>(()) + })?; } } Ok(()) @@ -1566,12 +1566,7 @@ impl FetchTasklet { readable: ReadableStream, ) { let this = Self::from_ctx(ctx); - if let crate::webcore::readable_stream::Source::Bytes(bytes) = readable.ptr { - // SAFETY: the caller holds the stream, which owns the live ByteStream. JS thread. - unsafe { this.response_stream.hold(bytes) }; - } else { - this.response_stream.release(); - } + this.response_stream.hold(&readable); } fn on_start_streaming_http_response_body_callback(ctx: NonNull) -> DrainResult { @@ -1717,7 +1712,7 @@ impl FetchTasklet { Some(FetchTasklet::on_start_streaming_http_response_body_callback); pending.on_readable_stream_available = Some(FetchTasklet::on_readable_stream_available); pending.on_start_buffering = Some(FetchTasklet::on_start_buffering_callback); - pending.producer = SourceHandle::FetchResponseBody(bun_ptr::BackRef::new_mut(self)); + pending.producer = SourceHandle::FetchResponseBody(bun_ptr::BackRef::new(self)); return BodyValue::Locked(pending); } @@ -1804,12 +1799,14 @@ impl FetchTasklet { .with_mut(|poll_ref| poll_ref.unref(bun_io::js_vm_ctx())); self.clear_stream_handlers(); self.response.clear(); - self.native_response.set(None); + if let Some(response) = self.native_response.take() { + drop(response); + } } fn on_resolve(&mut self) -> JSValue { bun_output::scoped_log!(FetchTasklet, "onResolve"); - let response = bun_core::heap::into_raw(Box::new(self.to_response())); + let response = self.to_response(); // The fetch() promise is about to resolve; from here the paused // transport should not by itself keep the event loop alive. The body // consumer hooks (`on_start_streaming_http_response_body_callback`, @@ -1818,10 +1815,10 @@ impl FetchTasklet { self.poll_ref .with_mut(|poll_ref| poll_ref.unref(bun_io::js_vm_ctx())); } - // SAFETY: response is a freshly allocated Response; makeMaybePooled takes ownership semantics on the JS side let global_this = self.global_this; - // SAFETY: `response` is freshly allocated above; ownership transfers to JSC. - let response_js = Response::make_maybe_pooled(&global_this, response); + // The JS wrapper owns the allocation; `native_response` is our reference, so + // the body can still be resolved after the JS Response is discarded. + let (response_js, native_response) = Box::new(response).to_js_retained(&global_this); response_js.ensure_still_alive(); self.response = jsc::Weak::::create( response_js, @@ -1829,14 +1826,11 @@ impl FetchTasklet { jsc::WeakRefType::FetchResponse, self, ); - // SAFETY: `response` is the live heap allocation owned by JSC after - // `make_maybe_pooled`. - self.native_response - .set(Some(unsafe { RefPtr::init_ref(response) })); + let response_this = native_response.this_ptr(); + self.native_response.set(Some(native_response)); // Response-owned listener so abort still errors the body after this tasklet detaches its own. if let Some(signal) = self.abort_signal() { - // SAFETY: `response` is the live heap allocation owned by JSC. - unsafe { Response::attach_abort_signal(response, &global_this, signal) }; + Response::attach_abort_signal(response_this, &global_this, signal); } response_js } @@ -2281,8 +2275,9 @@ impl FetchTasklet { } self.abort_task(); if let Some(sink) = self.sink_mut() { - sink.pending.result = Writable::Done; - sink.pending.run(); + let pending = sink.pending.get_mut_unique(); + pending.result = Writable::Done; + pending.run(); sink.source.close(None); if is_native { sink.task = None; @@ -2553,10 +2548,11 @@ impl FetchTasklet { #[bun_uws::uws_callback(export = "Bun__FetchResponse_finalize", no_catch)] pub(crate) fn on_response_finalize(&mut self) { bun_output::scoped_log!(FetchTasklet, "onResponseFinalize"); - let Some(response) = self.native_response.get().as_deref() else { + let native_response = self.native_response.get(); + let Some(response) = native_response.as_deref() else { return; }; - let BodyValue::Locked(locked) = response.get_body_value() else { + let BodyValue::Locked(locked) = response.body_value().get() else { // The body arrived or failed; nothing is underway. return; }; diff --git a/src/runtime/webcore/s3/client.rs b/src/runtime/webcore/s3/client.rs index cdce934bcfc4..2f3447b8fe58 100644 --- a/src/runtime/webcore/s3/client.rs +++ b/src/runtime/webcore/s3/client.rs @@ -648,7 +648,7 @@ impl S3UploadStreamWrapper { S3UploadResult::Success => { let uploaded = JSValue::js_number(self_.task.uploaded_bytes.get() as f64); if let Some(sink) = self_.sink_mut() { - sink.pending.run(); + sink.pending.get_mut_unique().run(); if settled.is_ok() && sink.flush_promise.has_value() { settled = sink.flush_promise.resolve(&global, JSValue::js_number(0.0)); } @@ -690,8 +690,9 @@ impl S3UploadStreamWrapper { ); sink.ended = true; sink.done = true; - sink.pending.result = crate::webcore::streams::Writable::Done; - sink.pending.run(); + let pending = sink.pending.get_mut_unique(); + pending.result = crate::webcore::streams::Writable::Done; + pending.run(); if settled.is_ok() && sink.flush_promise.has_value() { settled = sink.flush_promise.reject(&global, Ok(js_err)); } @@ -1049,7 +1050,8 @@ pub(crate) fn upload_stream( let assignment_result: JSValue = NetworkSinkJSSink::assign_to_stream( global_this, readable_stream.value, - NonNull::from(sink), + NonNull::from(&mut *sink), + |s| sink.source = s, ); assignment_result.ensure_still_alive(); @@ -1450,18 +1452,9 @@ pub(crate) fn readable_stream( let global_static = GlobalRef::from(global_this); // Ownership of the heap-allocated NewSource transfers to the JS wrapper (m_ctx) via - // `to_readable_stream()`/`to_js()`; the wrapper's finalize() reclaims it. - let reader: *mut crate::webcore::byte_stream::Source = - crate::webcore::byte_stream::Source::new(crate::webcore::readable_stream::NewSource { - context: ByteStream::default(), - global_this: Some(bun_ptr::BackRef::new(global_this)), - ..Default::default() - }); - // SAFETY: freshly heap-allocated via TrivialNew; exclusive access until handed to JS below. - let reader_mut = unsafe { &mut *reader }; - - reader_mut.context.setup(); - let readable_value = reader_mut.to_readable_stream(global_this)?; + // `to_readable_stream()`; the wrapper's finalize() releases it. + let reader = crate::webcore::byte_stream::Source::new(ByteStream::default(), global_this); + let readable_value = reader.to_readable_stream(global_this)?; let wrapper = S3DownloadStreamWrapper::new(S3DownloadStreamWrapper { stream: Default::default(), @@ -1469,15 +1462,13 @@ pub(crate) fn readable_stream( global: global_static, task: Cell::new(core::ptr::null_mut()), }); - // SAFETY: `reader` is the live source made above; `wrapper` the live heap allocation. - unsafe { (*wrapper).stream.hold(&raw mut reader_mut.context) }; + // SAFETY: `wrapper` is the live heap allocation made above. + unsafe { (*wrapper).stream.hold_source(&reader) }; - reader_mut + reader .producer .set(crate::webcore::streams::SourceHandle::S3DownloadBody( - // SAFETY: `wrapper` is the live heap allocation; cleared from the producer slot before - // it is freed (`ProducerHold::take`). - unsafe { bun_ptr::BackRef::from_raw(wrapper) }, + bun_ptr::BackRef::from(NonNull::new(wrapper).expect("heap::alloc")), )); let task = download_stream( diff --git a/src/runtime/webcore/streams.rs b/src/runtime/webcore/streams.rs index 3053f7fdb035..7da31786373a 100644 --- a/src/runtime/webcore/streams.rs +++ b/src/runtime/webcore/streams.rs @@ -1,7 +1,6 @@ use core::ffi::c_void; -use core::ptr::NonNull; -use bun_ptr::{BackRef, RawSlice, RefPtr}; +use bun_ptr::{BackRef, JsCell, RawSlice, RefPtr}; use crate::webcore::jsc::{ self as jsc, ArrayBuffer, CommonAbortReason, CommonAbortReasonExt as _, JSGlobalObject, @@ -23,7 +22,7 @@ bun_core::declare_scope!(NetworkSinkLog, visible); /// `bun.ObjectPool(bun.Vec, ...)::Node` — pooled buffer node type used by /// `HTTPServerWritable.pooled_buffer`. -type ByteListPoolNode = bun_collections::pool::Node>; +type ByteListPoolGuard = bun_collections::pool::PoolGuard<'static, Vec>; // NetworkSink stores a borrowed `*MultiPartUpload`. Now that `webcore::s3` is // wired, alias the module to the real type so `bun_s3::MultiPartUpload` resolves @@ -280,9 +279,9 @@ impl Start { pub enum StreamResult { // Self-referential: the pointee's `Pending.result` points back at this value, so a - // `&'a mut Pending` borrow can't be expressed; raw pointer with the BORROW_PARAM - // contract (pointee strictly outlives this result). - Pending(*mut Pending), + // `&'a mut Pending` borrow can't be expressed; the source's slot strictly + // outlives this result (it is turned into a promise synchronously). + Pending(BackRef>), Err(StreamError), Done, Owned(Vec), @@ -347,8 +346,8 @@ impl StreamResult { pub enum Writable { // Self-referential via WritablePending.result (see StreamResult::Pending above); - // raw pointer with the BORROW_PARAM contract. - Pending(*mut WritablePending), + // the sink's slot strictly outlives this result. + Pending(BackRef>), Err(SysError), Done, Owned(BlobSizeType), @@ -494,9 +493,7 @@ impl Writable { // undefined == noop, but we probably won't send it Writable::Done => JSValue::TRUE, Writable::Pending(pending) => { - // SAFETY: pending is a valid borrowed pointer per BORROW_PARAM - // classification; exclusive borrow scoped to the call. - let prom = unsafe { (*pending).promise(global_this) }; + let prom = pending.with_mut(|p| p.promise(global_this)); // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*const → &` deref. JSPromise::opaque_ref(prom).to_js() } @@ -564,39 +561,28 @@ impl Pending { .enqueue_task(bun_event_loop::Task::from_boxed(clone)); } - /// # Safety - /// `this` must be a valid, uniquely-owned pointer previously produced by - /// `bun_core::heap::into_raw` (via `Task::from_boxed` in `run_on_next_tick`). - // Forwards `this` to `bun_core::heap::take` without dereferencing it here; - // not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn run_from_js_thread(this: *mut Pending) { - // SAFETY: this was heap-allocated in run_on_next_tick - let mut boxed = unsafe { bun_core::heap::take(this) }; - boxed.run(); + /// The `run_on_next_tick` task (the boxed `Pending`) came off the queue. + #[allow(clippy::boxed_local)] + pub(crate) fn run_from_js_thread(mut this: Box) { + this.run(); } /// The loop refused the deferred fulfilment (VM teardown): nobody awaits /// the read any more, so drop the promise's root and the parked result. - pub(crate) fn release_without_running(this: *mut Pending) { - // SAFETY: heap-allocated in run_on_next_tick; refused, so we own it. - let mut boxed = unsafe { bun_core::heap::take(this) }; - boxed.state = PendingState::Used; - if let PendingFuture::Promise { promise, .. } = &boxed.future { + pub(crate) fn release_without_running(mut this: Box) { + this.state = PendingState::Used; + if let PendingFuture::Promise { promise, .. } = &this.future { JSPromise::opaque_ref(*promise).to_js().unprotect(); } - drop(boxed); + drop(this); } } -impl bun_event_loop::Taskable for Pending { - const TAG: bun_event_loop::TaskTag = bun_event_loop::task_tag::StreamPending; - /// Deferred out of a finalizer or a late completion: do the script-free - /// part of what the dispatch would have done. - unsafe fn release_unrun(this: *mut Self) { - Pending::release_without_running(this); - } -} +// Deferred out of a finalizer or a late completion: do the script-free part +// of what the dispatch would have done. +bun_event_loop::boxed_taskable!(Pending, StreamPending, |this| { + Pending::release_without_running(this) +}); pub enum PendingFuture { Promise { @@ -760,9 +746,7 @@ impl StreamResult { StreamResult::IntoArray(array) => Ok(JSValue::from(array.len)), StreamResult::IntoArrayAndDone(array) => Ok(JSValue::from(array.len)), StreamResult::Pending(pending) => { - // SAFETY: pending is a valid borrowed pointer per BORROW_PARAM - // classification; exclusive borrow scoped to the call. - let promise = unsafe { (**pending).promise(global_this) }; + let promise = pending.with_mut(|p| p.promise(global_this)); // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*const → &` deref. let promise_js = JSPromise::opaque_ref(promise).to_js(); promise_js.protect(); @@ -835,7 +819,7 @@ impl UpstreamSource for crate::webcore::ByteStream { impl UpstreamSource for crate::webcore::FileReader { #[inline] fn on_ready(&self) { - self.pull_into_sink(); + Self::pull_into_sink(self.this_ptr()); } #[inline] fn on_close(&self, _err: Option) { @@ -888,7 +872,7 @@ pub enum SourceHandle { /// `Subprocess<'a>`; the pointed-at allocation outlives this handle. Subprocess(BackRef>), ShellWritable(BackRef), - FetchResponseBody(BackRef), + FetchResponseBody(BackRef), ServerRequestBody(crate::server::AnyRequestContext), S3DownloadBody(BackRef), HTMLRewriter(BackRef), @@ -1027,7 +1011,8 @@ pub(crate) enum HTTPServerWritableState { pub struct HTTPServerWritable { pub(crate) res: Option, pub(crate) buffer: Vec, - pub(crate) pooled_buffer: Option>, + /// The pool slot `buffer` was checked out of; `buffer` goes back into it in `finalize`. + pub(crate) pooled_buffer: Option, pub offset: BlobSizeType, pub(crate) wrote: BlobSizeType, @@ -1039,7 +1024,7 @@ pub struct HTTPServerWritable { /// Backpressure promise returned from `write()` to a JS controller (direct /// stream `pull` or `readStreamIntoSink`). Resolved on drain via /// `flush_promise()` → `pending.run()`. - pub(crate) pending: WritablePending, + pub(crate) pending: JsCell, pub(crate) wrote_at_start_of_flush: BlobSizeType, // JSC_BORROW: process-lifetime VM global; `None` until `flush_from_js`/ // `end_from_js` install it. Safe `Deref` via `BackRef`. @@ -1088,7 +1073,7 @@ impl Default for HTTPServerWritable { state: HTTPServerWritableState::Writing, source: SourceHandle::default(), pending_flush: None, - pending: WritablePending::default(), + pending: JsCell::new(WritablePending::default()), wrote_at_start_of_flush: 0, global_this: None, high_water_mark: 2048, @@ -1251,9 +1236,10 @@ impl HTTPServerWritable { ) { return Writable::Backpressure(len); } - self.pending.consumed = len; - self.pending.result = Writable::Owned(len); - return Writable::Pending(core::ptr::from_mut(&mut self.pending)); + let pending = self.pending.get_mut_unique(); + pending.consumed = len; + pending.result = Writable::Owned(len); + return Writable::Pending(BackRef::new(&self.pending)); } Writable::Owned(len) } @@ -1524,15 +1510,9 @@ impl HTTPServerWritable { if self.buffer.capacity() == 0 { debug_assert!(self.pooled_buffer.is_none()); if FeatureFlags::HTTP_BUFFER_POOLING { - if let Some(pooled_node) = ByteListPool::get_if_exists() { - let pooled_node = NonNull::new(pooled_node) - .expect("ByteListPool::get_if_exists returns a live heap node when Some"); - self.pooled_buffer = Some(pooled_node); - // SAFETY: pooled_node is a valid pool checkout; `data` was - // written by `ByteListPool::push` (or zero-initialized). - // Move the Vec out by bitwise read and reset the slot. - self.buffer = - unsafe { core::mem::take((*pooled_node.as_ptr()).data.assume_init_mut()) }; + if let Some(mut pooled) = ByteListPool::try_get() { + self.buffer = core::mem::take(&mut *pooled); + self.pooled_buffer = Some(pooled); } } } @@ -1869,33 +1849,19 @@ impl HTTPServerWritable { bun_sys::Result::Ok(JSValue::from(self.wrote)) } - /// Takes `*mut Self`, not `&mut self`: closing the signal runs the controller's - /// JS `onClose`, which can cancel the stream, drain microtasks, and free this - /// sink. A `&mut self` argument protector must not be live across that free. - /// - /// # Safety - /// `this` must point at the live sink owned by the `RequestContext`. - pub(crate) unsafe fn abort(this: *mut Self) { + /// The response was aborted: mark the sink dead and settle what is parked + /// on it. Returns the source for the caller to `close(None)` once no borrow + /// of this sink is live: the close fires the JS `onClose` callback, and the + /// teardown it can re-enter (cancel, microtask drain) frees this sink. + #[must_use = "close the returned source after releasing the sink borrow"] + pub(crate) fn abort(&mut self) -> SourceHandle { bun_core::scoped_log!(HTTPServerWritableLog, "onAborted()"); - // SAFETY: caller contract — `this` is live, and every access here is scoped - // so no borrow spans the signal close below, which may free `*this`. - unsafe { - (*this).state = HTTPServerWritableState::Aborted; - (*this).res = None; - (*this).unregister_auto_flusher(); - } - - // SAFETY: nothing above freed `*this`; exclusive borrow scoped to the call. - unsafe { (*this).flush_promise() }; - // SAFETY: as above. - unsafe { (*this).finalize() }; - - // Close the source last and through a stack copy: the close fires the JS - // onClose callback, and the teardown it can re-enter frees this sink, so - // no reference into the allocation may be live across the call. - // SAFETY: as above; `source` is copied out before the close. - let mut source = unsafe { (*this).source }; - source.close(None); + self.state = HTTPServerWritableState::Aborted; + self.res = None; + self.unregister_auto_flusher(); + self.flush_promise(); + self.finalize(); + self.source } fn unregister_auto_flusher(&mut self) { @@ -1953,32 +1919,6 @@ impl HTTPServerWritable { false } - /// # Safety - /// `this` must be a valid, uniquely-owned heap pointer to `Self` produced - /// by `bun_core::heap::into_raw`; the caller transfers ownership. - // Forwards `this` to `bun_core::heap::take` without dereferencing it here; - // not_unsafe_ptr_arg_deref is a false positive on opaque-token forwarding. - #[allow(clippy::not_unsafe_ptr_arg_deref)] - pub(crate) fn destroy(this: *mut Self) { - bun_core::scoped_log!(HTTPServerWritableLog, "destroy()"); - // SAFETY: this was heap-allocated; destroy takes sole ownership. Reclaim - // the Box first so we never hold a `&mut *this` alongside the Box's - // unique pointer. - let mut this = unsafe { bun_core::heap::take(this) }; - // Callers may tear this sink down without routing through - // flushPromise() (e.g. handleResolveStream / handleRejectStream). - // Drop the GC root so the promise can be collected. - this.pending.result = Writable::Done; - this.pending.run(); - if let Some(prom) = this.pending_flush.take() { - // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*const → &` deref. - JSPromise::opaque_ref(prom).to_js().unprotect(); - } - this.buffer.clear_and_free(); - this.unregister_auto_flusher(); - drop(this); - } - /// This can be called _many_ times for the same instance /// so it must zero out state instead of make it pub fn finalize(&mut self) { @@ -2013,23 +1953,14 @@ impl HTTPServerWritable { debug_assert!(self.pooled_buffer.is_none()); } - if let Some(pooled) = self.pooled_buffer { + if let Some(mut pooled) = self.pooled_buffer.take() { self.buffer.clear(); if self.buffer.capacity() > 64 * 1024 { self.buffer.clear_and_free(); } - // SAFETY: pooled is a valid pool node checkout - unsafe { - (*pooled.as_ptr()).data = - core::mem::MaybeUninit::new(core::mem::take(&mut self.buffer)); - } - - self.buffer = Vec::::default(); - self.pooled_buffer = None; - // SAFETY: `pooled` was obtained from `ByteListPool::get_node` and is - // exclusively owned by this stream; `data` was rewritten just above, - // so it is initialized. Ownership returns to the pool. - unsafe { ByteListPool::release(pooled.as_ptr()) }; + *pooled = core::mem::take(&mut self.buffer); + // Ownership of the node returns to the pool. + drop(pooled); } else if self.buffer.capacity() == 0 { // } else if FeatureFlags::HTTP_BUFFER_POOLING && !ByteListPool::full() { @@ -2047,34 +1978,18 @@ impl HTTPServerWritable { pub(crate) fn flush_promise(&mut self) { // Settle any `write()` → `Pending` promise first so a parked JS writer // wakes on every drain/teardown path that reaches here. - self.pending.run(); + self.pending.get_mut_unique().run(); if let Some(prom) = self.pending_flush.take() { bun_core::scoped_log!(HTTPServerWritableLog, "flushPromise()"); - let global_this = self.global_this(); // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `* → &`/`&mut` deref. JSPromise::opaque_ref(prom).to_js().unprotect(); - let result = JSPromise::opaque_mut(prom).resolve( - global_this, - JSValue::js_number(self.wrote.saturating_sub(self.wrote_at_start_of_flush) as f64), - ); - // `this.wrote_at_start_of_flush = this.wrote` must read `this.wrote` - // AFTER resolve, which may reenter JS and mutate `wrote`. Read it here, - // not before the call. - // - // R-2 noalias mitigation (PORT_NOTES_PLAN R-2; precedent - // `b818e70e1c57` NodeHTTPResponse::cork): `&mut self` is `noalias` - // and `resolve()` receives nothing derived from `self`, so LLVM is - // licensed to forward the `self.wrote` read used in the - // `js_number(...)` argument above into this assignment — defeating - // the very ordering the note above exists to preserve. ASM-verified - // PROVEN_CACHED. Launder `self` so the post-resolve `wrote` read - // goes through an opaque pointer. - let this: *mut Self = core::hint::black_box(core::ptr::from_mut(self)); - // SAFETY: `this` is the live heap payload (refcounted via the JS - // wrapper); momentary access only. - unsafe { (*this).wrote_at_start_of_flush = (*this).wrote }; - // SAFETY: as above. + // Close this flush's window, then hand its count to JS. + let flushed = self.wrote.saturating_sub(self.wrote_at_start_of_flush); + self.wrote_at_start_of_flush = self.wrote; + let global_this = self.global_this(); + let result = JSPromise::opaque_mut(prom) + .resolve(global_this, JSValue::js_number(flushed as f64)); crate::dispatch::fold(result); } } @@ -2092,13 +2007,18 @@ impl crate::webcore::sink::JsSinkType for HTTPServerWritable) { - // SAFETY: trait contract — `this` is live, and only the RequestContext's - // `destroy` frees it (never the inherent `finalize`), so the `&mut` - // scoped to this call stays valid throughout. - unsafe { (*this.as_ptr()).finalize() } + fn finalize(_this: bun_ptr::ThisPtr) { + unreachable!("HTTPServerWritable is finalized through finalize_mut"); + } + fn finalize_mut(&mut self) { + self.finalize() } fn end_from_js(&mut self, global: &JSGlobalObject) -> bun_sys::Result { Self::end_from_js(self, global) @@ -2108,6 +2028,26 @@ impl crate::webcore::sink::JsSinkType for HTTPServerWritable Drop for HTTPServerWritable { + fn drop(&mut self) { + bun_core::scoped_log!(HTTPServerWritableLog, "destroy()"); + // Callers may tear this sink down without routing through + // flushPromise() (e.g. handleResolveStream / handleRejectStream). + // Drop the GC root so the promise can be collected. + let pending = self.pending.get_mut_unique(); + pending.result = Writable::Done; + pending.run(); + if let Some(prom) = self.pending_flush.take() { + // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `*const → &` deref. + JSPromise::opaque_ref(prom).to_js().unprotect(); + } + self.buffer.clear_and_free(); + self.unregister_auto_flusher(); + } +} + pub type HTTPSResponseSink = HTTPServerWritable; pub type HTTPResponseSink = HTTPServerWritable; @@ -2130,7 +2070,7 @@ pub struct NetworkSink { pub(crate) flush_promise: JSPromiseStrong, /// Backpressure promise returned from `write()` to a JS controller; /// resolved by `on_writable` → `pending.run()`. - pub(crate) pending: WritablePending, + pub(crate) pending: JsCell, pub(crate) end_promise: JSPromiseStrong, /// Upstream ByteStream error stashed by `end_from_stream` so the upload /// failure callback can reject with the original JS error (e.g. S3 @@ -2151,7 +2091,7 @@ impl Default for NetworkSink { source: SourceHandle::default(), global_this: None, flush_promise: JSPromiseStrong::default(), - pending: WritablePending::default(), + pending: JsCell::new(WritablePending::default()), end_promise: JSPromiseStrong::default(), upstream_error: jsc::strong::Optional::empty(), ended: false, @@ -2252,7 +2192,7 @@ impl NetworkSink { .resolve(&global, JSValue::js_number(flushed as f64)); crate::dispatch::fold(flushed); } - (*this).pending.run(); + (*this).pending.get_mut_unique().run(); (*this).source }; // Wake the upstream source (JS controller onPull or native ByteStream @@ -2291,8 +2231,9 @@ impl NetworkSink { pub(crate) fn abort(&mut self) { self.ended = true; self.done = true; - self.pending.result = Writable::Done; - self.pending.run(); + let pending = self.pending.get_mut_unique(); + pending.result = Writable::Done; + pending.run(); self.source.close(None); self.finalize(); } @@ -2307,9 +2248,10 @@ impl NetworkSink { ) { return Writable::Backpressure(len); } - self.pending.consumed = len; - self.pending.result = Writable::Owned(len); - Writable::Pending(core::ptr::from_mut(&mut self.pending)) + let pending = self.pending.get_mut_unique(); + pending.consumed = len; + pending.result = Writable::Owned(len); + Writable::Pending(BackRef::new(&self.pending)) } pub fn write(&mut self, data: &StreamResult) -> Writable { @@ -2389,8 +2331,9 @@ impl NetworkSink { // send EOF self.ended = true; - self.pending.result = Writable::Done; - self.pending.run(); + let pending = self.pending.get_mut_unique(); + pending.result = Writable::Done; + pending.run(); // flush everything and send EOF if let Some(task) = self.task_ref() { let _ = task.write_bytes(b"", true); diff --git a/src/runtime/webcore/wasm_streaming.rs b/src/runtime/webcore/wasm_streaming.rs index 6659f2859603..60c3dadcf76d 100644 --- a/src/runtime/webcore/wasm_streaming.rs +++ b/src/runtime/webcore/wasm_streaming.rs @@ -13,7 +13,7 @@ use bun_jsc::{ErrorCode, JSGlobalObject, JSValue, JsError, JsResult}; use crate::webcore::blob::{self, Any as AnyBlob, Blob, BlobExt}; use crate::webcore::body::{BodyMixin as _, Value as BodyValue}; -use crate::webcore::{ReadableStream, Response, response}; +use crate::webcore::{ReadableStream, Response}; unsafe extern "C" { // `streaming_compiler` is the opaque C++ `StreamingCompiler*` handed in by @@ -32,20 +32,16 @@ fn get_body_stream_or_bytes_for_wasm_streaming( response_value: JSValue, streaming_compiler: *mut c_void, ) -> JsResult { - let response: &mut Response = match response::from_js(response_value) { - // SAFETY: `from_js` returns a pointer to the GC-owned `Response` cell; - // the cell stays live for the duration of this host call (rooted on the - // C++ caller's stack). - Some(r) => unsafe { &mut *r }, - None => { - return Err(this.throw_invalid_argument_type_value2( - b"source", - // "an Promise" is byte-for-byte what Node's ERR_INVALID_ARG_TYPE - // formatter emits for an uppercase-initial non-class entry. - b"an instance of Response or an Promise resolving to Response", - response_value, - )); - } + // The GC-owned `Response` cell stays live for the duration of this host + // call (rooted on the C++ caller's stack). + let Some(response) = response_value.as_class_ref::() else { + return Err(this.throw_invalid_argument_type_value2( + b"source", + // "an Promise" is byte-for-byte what Node's ERR_INVALID_ARG_TYPE + // formatter emits for an uppercase-initial non-class entry. + b"an instance of Response or an Promise resolving to Response", + response_value, + )); }; { @@ -93,32 +89,34 @@ fn get_body_stream_or_bytes_for_wasm_streaming( .throw()); } - // Holding `body = response.get_body_value()` as a single live pointer - // through `getBodyReadableStream` would overlap two `&mut` borrows - // of `response`, so we re-borrow per use and capture scalars. - { - let body = response.get_body_value(); + // Each body borrow is closure-scoped so none spans `get_body_readable_stream`. + if let Some(err_js) = response.body_value().with_mut(|body| { if let BodyValue::Error(err) = body { - return Err(this.throw_value(err.to_js(this))); + return Some(err.to_js(this)); } // We're done validating. From now on, deal with extracting the body. body.to_blob_if_possible(); + None + }) { + return Err(this.throw_value(err_js)); } - if matches!(response.get_body_value(), BodyValue::Locked(_)) { + if matches!(response.body_value().get(), BodyValue::Locked(_)) { if let Some(stream) = response.get_body_readable_stream() { return Ok(stream.value); } } - let body = response.get_body_value(); - let any_blob: AnyBlob = match body { + let any_blob: AnyBlob = match response.body_value().with_mut(|body| match body { BodyValue::Locked(_) => match body.try_use_as_any_blob() { - Some(b) => b, - None => return body.to_readable_stream(this), + Some(b) => Ok(b), + None => Err(body.to_readable_stream(this)), }, - _ => body.use_as_any_blob(), + _ => Ok(body.use_as_any_blob()), + }) { + Ok(any_blob) => any_blob, + Err(stream) => return stream, }; // `Any::store()` only yields `Some` for the `Blob` variant; non-`Bytes` data means @@ -154,20 +152,13 @@ fn get_body_stream_or_bytes_for_wasm_streaming( Ok(JSValue::NULL) } -/// Plain C ABI -/// shim: returns `.zero` on thrown exception. -/// -/// # Safety -/// `this` must be a valid, live `JSGlobalObject` pointer for the duration of -/// the call (guaranteed by the C++ host caller). -#[unsafe(no_mangle)] -unsafe extern "C" fn Zig__GlobalObject__getBodyStreamOrBytesForWasmStreaming( - this: *mut JSGlobalObject, +/// Plain C ABI shim: returns `.zero` on thrown exception. +// HOST_EXPORT(Zig__GlobalObject__getBodyStreamOrBytesForWasmStreaming, c) +pub fn get_body_stream_or_bytes_for_wasm_streaming_export( + this: &JSGlobalObject, response_value: JSValue, streaming_compiler: *mut c_void, ) -> JSValue { - // SAFETY: C++ passes a live global object. - let this = unsafe { &*this }; match get_body_stream_or_bytes_for_wasm_streaming(this, response_value, streaming_compiler) { Ok(v) => v, Err(JsError::OutOfMemory) => { From b9b3482b2b013a800a49eb44a1184c3020d29664 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Tue, 25 Aug 2026 05:12:19 +0000 Subject: [PATCH 2/8] Response::into_js: one doc sentence --- src/runtime/webcore/Response.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/runtime/webcore/Response.rs b/src/runtime/webcore/Response.rs index b08ab94f6ccc..3c362e9a9426 100644 --- a/src/runtime/webcore/Response.rs +++ b/src/runtime/webcore/Response.rs @@ -373,9 +373,8 @@ impl Response { ::check_body_stream_ref(self, global_object) } - /// Move to the heap and create the JS wrapper, which owns the initial + /// Hand a heap `Response` to a new JS wrapper, which owns the initial /// reference (released in [`Response::finalize`]). - /// Hand a heap `Response` to a new JS wrapper, which holds that reference. #[inline] pub fn into_js(self: Box, global_object: &JSGlobalObject) -> JSValue { Self::create_js(RefPtr::from_box(self), global_object) From 10a396de77855c776cd9f0215244ee9a6624af79 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Thu, 27 Aug 2026 10:48:54 +0000 Subject: [PATCH 3/8] NewSource: the JS wrapper takes a reference of its own in to_readable_stream --- src/runtime/webcore/ReadableStream.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/runtime/webcore/ReadableStream.rs b/src/runtime/webcore/ReadableStream.rs index 6396ae6ed88c..102af949a4c4 100644 --- a/src/runtime/webcore/ReadableStream.rs +++ b/src/runtime/webcore/ReadableStream.rs @@ -1027,9 +1027,9 @@ const _: () = assert!(core::mem::offset_of!(NewSource, context) == 0 const _: () = assert!(core::mem::offset_of!(NewSource, context) == 0); impl NewSource { - /// Heap-allocate a source around `context`. The returned reference is the - /// one the JS wrapper takes over in [`Self::to_readable_stream`] (and - /// releases in [`Self::finalize`]); until then the caller holds it. + /// Heap-allocate a source around `context`. [`Self::to_readable_stream`] + /// gives the JS wrapper a reference of its own (released in + /// [`Self::finalize`]); the returned one is the caller's. pub fn new(context: C, global_this: &JSGlobalObject) -> RefPtr { RefPtr::new_cyclic(|self_root| NewSource { context, @@ -1203,9 +1203,10 @@ impl NewSource { let out_value = if let Some(v) = self.this_jsvalue.get().try_get() { v } else { - // The wrapper's `m_ctx` takes over the initial reference; the GC - // finalizer drives teardown through `finalize`. - C::js_create(self.this_ptr().as_ptr().cast::(), global_this) + // The wrapper's `m_ctx` holds a reference of its own; the GC + // finalizer releases it through `finalize`. + let wrapper_ref = RefPtr::from_this(self.this_ptr()); + C::js_create(RefPtr::into_raw(wrapper_ref).cast::(), global_this) }; out_value.ensure_still_alive(); if self.this_jsvalue.get().is_empty() { From 29b9d41a3a9edd50679d0e909fa9f784134fa2a7 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 29 Aug 2026 09:36:31 +0000 Subject: [PATCH 4/8] Rebase fixups: refCounted Response::finalize(&self), js_Response::take_ref, HTTP3 generic gone, JsSinkType::close_with_error takes ThisPtr with the raw-pointer split in the generated close thunk, NetworkSink fail_from_js_pump on JsCell pending --- src/codegen/generate-jssink.ts | 20 +++++-- src/jsc/AbortSignal.rs | 9 +--- src/runtime/api/html_rewriter.rs | 9 ++-- src/runtime/webcore/Sink.rs | 92 +++++++++++++++++--------------- src/runtime/webcore/streams.rs | 12 +++-- 5 files changed, 79 insertions(+), 63 deletions(-) diff --git a/src/codegen/generate-jssink.ts b/src/codegen/generate-jssink.ts index 283a90e3f597..9233bd8a5682 100644 --- a/src/codegen/generate-jssink.ts +++ b/src/codegen/generate-jssink.ts @@ -1253,14 +1253,26 @@ pub extern "C" fn ${name}__controllerDetached(this: &mut ${name}, controller: JS `; // ZIG_DECL JSC::EncodedJSValue ${name}__close(JSC::JSGlobalObject*, void* sinkPtr, JSC::EncodedJSValue reason) - // C++ caller null-checks `ptr` before calling. `*mut`: a failing close can - // re-enter the sink (see `JsSinkType::close_with_error`). + // C++ caller null-checks `ptr` before calling. `reason` is the empty value + // for a clean close, otherwise the failed source's reason. A failing close + // gets a `ThisPtr` (it can re-enter and free the sink, see + // `JsSinkType::close_with_error`); a clean one the usual `&mut`. symbols.push(`${name}__close`); templ += `#[allow(dead_code, unreachable_pub, unused)] #[unsafe(no_mangle)] pub unsafe extern "C" fn ${name}__close(global: &JSGlobalObject, this: *mut ${name}, reason: JSValue) -> JSValue { - // SAFETY: C++ passes its live, null-checked \`m_sinkPtr\`. - unsafe { ${JSSinkT}::js_close(global, this, reason) } + // SAFETY: C++ passes its live, null-checked \`m_sinkPtr\`; this borrow ends + // before the sink can be re-entered. + if let Some(v) = ${JSSinkT}::js_close_pending_error(global, unsafe { &mut *this }) { + return v; + } + if !reason.is_empty() && <${name} as crate::webcore::sink::JsSinkType>::CLOSES_WITH_ERROR { + // SAFETY: as above. + ${JSSinkT}::js_close_with_error(global, unsafe { bun_ptr::ThisPtr::new(this) }, reason) + } else { + // SAFETY: as above; a clean \`end\` does not free the sink. + ${JSSinkT}::js_close(global, unsafe { &mut *this }) + } } `; diff --git a/src/jsc/AbortSignal.rs b/src/jsc/AbortSignal.rs index 16684d6a8f65..811433dd3bd0 100644 --- a/src/jsc/AbortSignal.rs +++ b/src/jsc/AbortSignal.rs @@ -125,7 +125,7 @@ impl AbortSignal { C::on_abort(unsafe { bun_ptr::ThisPtr::new(ptr.cast::()) }, reason); } let ctx = listener.this_ptr().as_ptr().cast::(); - let signal = self.retain(); + let signal = self.ref_(); self.pending_activity_ref(); self.add_listener(ctx, callback::); AbortListenerRegistration { signal, ctx } @@ -211,13 +211,6 @@ impl AbortSignal { unsafe { AbortSignalRef::adopt(WebCore__AbortSignal__ref(self)) } } - /// Take a counted reference on this signal. - pub fn retain(&self) -> AbortSignalRef { - // SAFETY: `&AbortSignal` only exists for a live C++ `WebCore::AbortSignal` - // (opaque FFI handle); `ref_()` returns it with the count bumped. - unsafe { AbortSignalRef::adopt(self.ref_()) } - } - pub fn unref(&self) { WebCore__AbortSignal__unref(self) } diff --git a/src/runtime/api/html_rewriter.rs b/src/runtime/api/html_rewriter.rs index 9ce7c38c885b..7b64f4202c0b 100644 --- a/src/runtime/api/html_rewriter.rs +++ b/src/runtime/api/html_rewriter.rs @@ -1857,13 +1857,14 @@ impl crate::webcore::sink::JsSinkType for RewriterPipe { self.end_from_stream(err.map(StreamError::Error)); bun_sys::Result::Ok(()) } - unsafe fn close_with_error( - this: *mut Self, + const CLOSES_WITH_ERROR: bool = true; + fn close_with_error( + this: bun_ptr::ThisPtr, global: &JSGlobalObject, reason: JSValue, ) -> bun_sys::Result<()> { - // SAFETY: caller contract; `end_from_stream` pins the pipe itself. - unsafe { &*this }.end_from_stream(Some(StreamError::JSValue( + // `end_from_stream` pins the pipe itself. + this.end_from_stream(Some(StreamError::JSValue( jsc::strong::Optional::create(reason, global), ))); bun_sys::Result::Ok(()) diff --git a/src/runtime/webcore/Sink.rs b/src/runtime/webcore/Sink.rs index 7cbcc9ac727c..041b4331b60b 100644 --- a/src/runtime/webcore/Sink.rs +++ b/src/runtime/webcore/Sink.rs @@ -335,23 +335,24 @@ pub trait JsSinkType: Sized + JsSinkAbi { fn end_from_js(&mut self, global: &JSGlobalObject) -> sys::Result; fn flush(&mut self) -> sys::Result<()>; fn start(&mut self, config: streams::Start) -> sys::Result<()>; + /// Whether [`close_with_error`](Self::close_with_error) replaces the + /// clean `end(None)` when the source failed. + const CLOSES_WITH_ERROR: bool = false; /// The source failed, so the bytes written so far are a truncated body: /// `controller.close(error)` with a truthy argument, or the pump's close - /// for an errored stream, whose `reason` may be nullish. The default keeps - /// the clean end for sinks whose owner handles the pump promise rejection. + /// for an errored stream, whose `reason` may be nullish. Only called when + /// [`CLOSES_WITH_ERROR`](Self::CLOSES_WITH_ERROR); sinks whose owner + /// handles the pump promise rejection keep the default clean end. /// - /// Raw pointer: failing can re-enter the sink through its owner and may - /// free it. - /// - /// # Safety - /// `this` is the cell's live sink. - unsafe fn close_with_error( - this: *mut Self, + /// `ThisPtr`, not `&mut self`: failing can re-enter the sink through its + /// owner and may free it. + fn close_with_error( + _this: bun_ptr::ThisPtr, _global: &JSGlobalObject, _reason: JSValue, ) -> sys::Result<()> { - // SAFETY: caller contract; `end` does not free the sink. - unsafe { (*this).end(None) } + debug_assert!(false, "close_with_error without CLOSES_WITH_ERROR"); + sys::Result::Ok(()) } /// Allocate the sink a new JS wrapper will own (its `finalize` releases it). @@ -666,42 +667,49 @@ impl JSSink { this.controller_detached(); } - /// `${abi_name}__close` body — called from - /// `${controller}__closeWithReason` and `${name}__doClose` in JSSink.cpp - /// with a raw `m_sinkPtr` (not a host-fn callframe), so exceptions become - /// `.zero`. `reason` is the empty value for a clean close (`close()`, a - /// falsy `close(reason)` argument, or the sink's own `close()`), otherwise - /// the failed source's reason, which the pump may pass as `undefined`. - /// - /// # Safety - /// `this` is the cell's live sink. - pub(crate) unsafe fn js_close( + /// `${abi_name}__close`, first step (both close kinds): a pending error is + /// thrown instead of closing. Called from `${controller}__closeWithReason` + /// and `${name}__doClose` in JSSink.cpp with a raw `m_sinkPtr` (not a + /// host-fn callframe), so exceptions become `.zero`. + pub(crate) fn js_close_pending_error( + global: &crate::webcore::jsc::JSGlobalObject, + this: &mut T, + ) -> Option { + bun_core::mark_binding!(); + let err = this.get_pending_error()?; + // `throw_error` sets the pending JS exception and returns the + // `JsError` for `?`-propagation; this host fn returns bare + // `JSValue`, so report and return ZERO (caller checks exception). + let _ = global.vm().throw_error(global, err); + Some(crate::webcore::jsc::JSValue::ZERO) + } + + /// `${abi_name}__close` for a clean close: `close()`, a falsy + /// `close(reason)` argument, the sink's own `close()`, or a failed source + /// on a sink without [`CLOSES_WITH_ERROR`](JsSinkType::CLOSES_WITH_ERROR). + pub(crate) fn js_close( global: &crate::webcore::jsc::JSGlobalObject, - this: *mut T, + this: &mut T, + ) -> crate::webcore::jsc::JSValue { + Self::close_result_to_js(global, this.end(None)) + } + + /// `${abi_name}__close` for a failed source (`reason` may be `undefined`) + /// on a sink with [`CLOSES_WITH_ERROR`](JsSinkType::CLOSES_WITH_ERROR). + pub(crate) fn js_close_with_error( + global: &crate::webcore::jsc::JSGlobalObject, + this: bun_ptr::ThisPtr, reason: crate::webcore::jsc::JSValue, + ) -> crate::webcore::jsc::JSValue { + Self::close_result_to_js(global, T::close_with_error(this, global, reason)) + } + + fn close_result_to_js( + global: &crate::webcore::jsc::JSGlobalObject, + result: sys::Result<()>, ) -> crate::webcore::jsc::JSValue { use crate::webcore::jsc::JSValue; use bun_sys_jsc::ErrorJsc; - bun_core::mark_binding!(); - - // SAFETY: caller contract; the borrow ends before `close_with_error`, - // which may re-enter or free the sink. - if let Some(err) = unsafe { (*this).get_pending_error() } { - // `throw_error` sets the pending JS exception and returns the - // `JsError` for `?`-propagation; this host fn returns bare - // `JSValue`, so report and return ZERO (caller checks exception). - let _ = global.vm().throw_error(global, err); - return JSValue::ZERO; - } - - let result = if reason.is_empty() { - // SAFETY: as above; `end` does not free the sink. - unsafe { (*this).end(None) } - } else { - // SAFETY: caller contract. - unsafe { T::close_with_error(this, global, reason) } - }; - // TODO: properly propagate exception upwards match result { sys::Result::Ok(()) => JSValue::UNDEFINED, diff --git a/src/runtime/webcore/streams.rs b/src/runtime/webcore/streams.rs index 7da31786373a..8b6892165b0c 100644 --- a/src/runtime/webcore/streams.rs +++ b/src/runtime/webcore/streams.rs @@ -2361,8 +2361,9 @@ impl NetworkSink { } (*this).ended = true; (*this).done = true; - (*this).pending.result = Writable::Done; - (*this).pending.run(); + let pending = (*this).pending.get_mut_unique(); + pending.result = Writable::Done; + pending.run(); if !reason.is_empty_or_undefined_or_null() { (*this).upstream_error.set(global, reason); } @@ -2495,12 +2496,13 @@ impl crate::webcore::sink::JsSinkType for NetworkSink { Self::release_writer_holder(this.as_ptr()); } } - unsafe fn close_with_error( - this: *mut Self, + const CLOSES_WITH_ERROR: bool = true; + fn close_with_error( + this: bun_ptr::ThisPtr, global: &JSGlobalObject, reason: JSValue, ) -> bun_sys::Result<()> { - Self::fail_from_js_pump(this, global, reason); + Self::fail_from_js_pump(this.as_ptr(), global, reason); bun_sys::Result::Ok(()) } fn end_from_js(&mut self, global: &JSGlobalObject) -> bun_sys::Result { From c3dbeb9972e65b933c55b98e4245630f21190287 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 29 Aug 2026 09:52:10 +0000 Subject: [PATCH 5/8] [autofix.ci] apply automated fixes --- src/runtime/api/html_rewriter.rs | 6 +++--- src/runtime/server/RequestContext.rs | 28 +++++++++------------------- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/runtime/api/html_rewriter.rs b/src/runtime/api/html_rewriter.rs index 7b64f4202c0b..4650f7d8679d 100644 --- a/src/runtime/api/html_rewriter.rs +++ b/src/runtime/api/html_rewriter.rs @@ -1864,9 +1864,9 @@ impl crate::webcore::sink::JsSinkType for RewriterPipe { reason: JSValue, ) -> bun_sys::Result<()> { // `end_from_stream` pins the pipe itself. - this.end_from_stream(Some(StreamError::JSValue( - jsc::strong::Optional::create(reason, global), - ))); + this.end_from_stream(Some(StreamError::JSValue(jsc::strong::Optional::create( + reason, global, + )))); bun_sys::Result::Ok(()) } fn end_from_js(&mut self, _global: &JSGlobalObject) -> bun_sys::Result { diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index dbe014510c75..658adf6ad150 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -2089,10 +2089,7 @@ where .sink .take() .expect("infallible: sink_mut returned Some"); - ResponseStreamJSSink::::detach( - &mut wrapper.sink.source, - global_this, - ); + ResponseStreamJSSink::::detach(&mut wrapper.sink.source, global_this); crate::dispatch::fold(stream.cancel(global_this)); wrapper.sink.mark_done(); wrapper.sink.on_first_write = None; @@ -2153,13 +2150,12 @@ where ); // We are already corked! - let assignment_result: JSValue = - ResponseStreamJSSink::::assign_to_stream( - global_this, - stream.value, - NonNull::from(&mut response_stream.sink), - |s| response_stream.sink.source = s, - ); + let assignment_result: JSValue = ResponseStreamJSSink::::assign_to_stream( + global_this, + stream.value, + NonNull::from(&mut response_stream.sink), + |s| response_stream.sink.source = s, + ); assignment_result.ensure_still_alive(); @@ -2928,10 +2924,7 @@ where .sink .global_this .expect("sink.global_this set in do_render_stream"); - ResponseStreamJSSink::::detach( - &mut wrapper.sink.source, - &sink_global, - ); + ResponseStreamJSSink::::detach(&mut wrapper.sink.source, &sink_global); drop(owned_sink); } @@ -3040,10 +3033,7 @@ where .sink .global_this .expect("sink.global_this set in do_render_stream"); - ResponseStreamJSSink::::detach( - &mut wrapper.sink.source, - &sink_global, - ); + ResponseStreamJSSink::::detach(&mut wrapper.sink.source, &sink_global); drop(owned_sink); } From e062931d12ea05e56719d56d87dea0c2555aac8a Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 29 Aug 2026 10:24:20 +0000 Subject: [PATCH 6/8] flush_promise: start the next window after the reaction ran, as before --- src/runtime/webcore/streams.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/runtime/webcore/streams.rs b/src/runtime/webcore/streams.rs index 8b6892165b0c..faaacfe59011 100644 --- a/src/runtime/webcore/streams.rs +++ b/src/runtime/webcore/streams.rs @@ -1982,14 +1982,22 @@ impl HTTPServerWritable { if let Some(prom) = self.pending_flush.take() { bun_core::scoped_log!(HTTPServerWritableLog, "flushPromise()"); + let global_this = self.global_this(); // S008: `JSPromise` is an `opaque_ffi!` ZST — safe `* → &`/`&mut` deref. JSPromise::opaque_ref(prom).to_js().unprotect(); - // Close this flush's window, then hand its count to JS. - let flushed = self.wrote.saturating_sub(self.wrote_at_start_of_flush); - self.wrote_at_start_of_flush = self.wrote; - let global_this = self.global_this(); - let result = JSPromise::opaque_mut(prom) - .resolve(global_this, JSValue::js_number(flushed as f64)); + let result = JSPromise::opaque_mut(prom).resolve( + global_this, + JSValue::js_number(self.wrote.saturating_sub(self.wrote_at_start_of_flush) as f64), + ); + // The next window starts at `wrote` as it is AFTER the reaction ran + // (it may re-enter and write more); bytes written inside the + // reaction belong to no flush count. `&mut self` is noalias and + // `resolve()` gets nothing derived from it, so launder the pointer + // to keep LLVM from reusing the pre-resolve read (R-2). + let this: *mut Self = core::hint::black_box(core::ptr::from_mut(self)); + // SAFETY: `this` is the live heap payload (its JS wrapper holds a + // ref across the reaction); momentary field access only. + unsafe { (*this).wrote_at_start_of_flush = (*this).wrote }; crate::dispatch::fold(result); } } From 529b91c26e0d41091cc3e2bb9b4ab4f3f8ebc886 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sat, 29 Aug 2026 10:59:27 +0000 Subject: [PATCH 7/8] one shared parent accessor for FileReader/ByteStream; parent_const call sites use parent --- src/runtime/api/html_rewriter.rs | 10 +++++----- src/runtime/server/RequestContext.rs | 8 ++++---- src/runtime/webcore/ByteStream.rs | 28 ++++++++++++++-------------- src/runtime/webcore/FileReader.rs | 2 +- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/runtime/api/html_rewriter.rs b/src/runtime/api/html_rewriter.rs index 4650f7d8679d..e1420a56b7d0 100644 --- a/src/runtime/api/html_rewriter.rs +++ b/src/runtime/api/html_rewriter.rs @@ -888,8 +888,8 @@ impl RewriterPipe { return; } match src { - SourceHandle::ByteStream(bs) => bs.parent_const().set_sink_owner(JSValue::UNDEFINED), - SourceHandle::FileReader(fr) => fr.parent_const().set_sink_owner(JSValue::UNDEFINED), + SourceHandle::ByteStream(bs) => bs.parent().set_sink_owner(JSValue::UNDEFINED), + SourceHandle::FileReader(fr) => fr.parent().set_sink_owner(JSValue::UNDEFINED), _ => {} } js_HTMLRewriterTransform::input_stream_set_cached(cell, &self.global, JSValue::UNDEFINED); @@ -901,8 +901,8 @@ impl RewriterPipe { /// slot. Only called from terminal paths on the JS thread. Idempotent. fn detach_output(&self) { if let Some(out) = self.output.take() { - out.parent_const().set_owner(JSValue::UNDEFINED); - out.parent_const().producer.set(SourceHandle::None); + out.parent().set_owner(JSValue::UNDEFINED); + out.parent().producer.set(SourceHandle::None); } } @@ -1365,7 +1365,7 @@ impl RewriterPipe { // A reader rooting the output stream now roots the Transform cell // too, so the `producer` backref cannot outlive the pipe. Cleared // in `detach_output`. - bytes.parent_const().set_owner(this.cell.get()); + bytes.parent().set_owner(this.cell.get()); this.output.set(Some(bytes)); } js_HTMLRewriterTransform::output_stream_set_cached( diff --git a/src/runtime/server/RequestContext.rs b/src/runtime/server/RequestContext.rs index 658adf6ad150..23d9022b901b 100644 --- a/src/runtime/server/RequestContext.rs +++ b/src/runtime/server/RequestContext.rs @@ -3283,7 +3283,7 @@ where this.as_ctx_ptr(), ); } else if matches!( - byte_stream.parent_const().producer.get(), + byte_stream.parent().producer.get(), WebCore::streams::SourceHandle::HTMLRewriter(_) ) { // Defer status/headers to the first chunk/end @@ -4109,7 +4109,7 @@ where readable.value.ensure_still_alive(); if let Some(bytes) = readable.ptr.bytes() { - let source = bytes.parent_const(); + let source = bytes.parent(); source.producer.set(WebCore::streams::SourceHandle::None); let mut err = Body::ValueError::Message(BunString::static_( "Request body exceeded maxRequestBodySize", @@ -4178,7 +4178,7 @@ where let bytes = bun_ptr::BackRef::from( NonNull::new(bytes_ptr).expect("Source::Bytes payload is non-null"), ); - let source = bytes.parent_const(); + let source = bytes.parent(); source.producer.set(WebCore::streams::SourceHandle::None); bytes.on_data(WebCore::streams::Result::TemporaryAndDone(borrowed)); } @@ -4330,7 +4330,7 @@ where return; }; if let Some(bytes) = readable.ptr.bytes() { - let source = bytes.parent_const(); + let source = bytes.parent(); source.producer.set(WebCore::streams::SourceHandle::None); } } diff --git a/src/runtime/webcore/ByteStream.rs b/src/runtime/webcore/ByteStream.rs index 0ecf1e057c62..b228a16dcdf9 100644 --- a/src/runtime/webcore/ByteStream.rs +++ b/src/runtime/webcore/ByteStream.rs @@ -192,7 +192,7 @@ impl readable_stream::SourceContext for ByteStream { Self::finalize(self) } fn wrapper_finalized(&self) { - self.parent_const().producer.get().consumer_collected(); + self.parent().producer.get().consumer_collected(); } fn drain_internal_buffer(&self) -> Vec { Self::drain(self) @@ -211,7 +211,7 @@ impl readable_stream::SourceContext for ByteStream { // Every `ByteStream` is the `context` field of a heap-allocated `Source` // (ReadableStream.NewSource); never constructed standalone. -bun_core::impl_field_parent! { ByteStream => Source.context; pub fn shared parent; pub fn shared parent_const; } +bun_core::impl_field_parent! { ByteStream => Source.context; pub fn shared parent; } impl ByteStream { /// A stream seeded from what the producer had already buffered. @@ -350,14 +350,14 @@ impl ByteStream { } self.has_received_last_chunk.set(true); self.on_cancel(); - let source = self.parent_const(); + let source = self.parent(); let mut p = source.producer.replace(streams::SourceHandle::None); p.close(None); } #[inline] pub(crate) fn signal_drained(&self) { - self.parent_const().producer.get().ready(None, None); + self.parent().producer.get().ready(None, None); } /// Take the unread buffered bytes (`buffer[offset..]`) without signalling @@ -375,7 +375,7 @@ impl ByteStream { /// Called by native fast-paths after wiring `self.sink`: a consumer now /// waits for bytes, so a parked producer resumes. pub fn signal_consumer_attached(&self) { - self.parent_const().producer.get().start(); + self.parent().producer.get().start(); } pub(crate) fn on_data(&self, mut stream: streams::Result) { @@ -448,7 +448,7 @@ impl ByteStream { // (`?` would skip it). bun_output::scoped_log!(ByteStream, "ByteStream.onData err action.reject()"); - let global = self.parent_const().global_this(); + let global = self.parent().global_this(); // R-2: move the action out of the cell *before* `signal_drained` // and `reject`; both can re-enter and consume the slot. let mut action = self.buffer_action.replace(None).unwrap(); @@ -486,7 +486,7 @@ impl ByteStream { ); let mut blob = self.to_any_blob().unwrap(); - action.fulfill(self.parent_const().global_this(), &mut blob); + action.fulfill(self.parent().global_this(), &mut blob); return; } if self.buffer.get().capacity() == 0 { @@ -501,7 +501,7 @@ impl ByteStream { // `stream`). self.buffer.set(owned.move_to_list_managed()); let mut blob = self.to_any_blob().unwrap(); - action.fulfill(self.parent_const().global_this(), &mut blob); + action.fulfill(self.parent().global_this(), &mut blob); return; } } @@ -518,7 +518,7 @@ impl ByteStream { // (Temporary* variants are non-owning `RawSlice` and so are left alone). drop(stream); let mut blob = self.to_any_blob().unwrap(); - action.fulfill(self.parent_const().global_this(), &mut blob); + action.fulfill(self.parent().global_this(), &mut blob); return; } else { self.buffer @@ -538,7 +538,7 @@ impl ByteStream { // Derive the destination from the GC-rooted view: JS can detach or // transfer the backing ArrayBuffer between the pull and the data // arriving. A detached view re-derives to an empty slice. - let global = self.parent_const().global_this(); + let global = self.parent().global_this(); let mut pending_view = self .pending_value .get() @@ -671,7 +671,7 @@ impl ByteStream { fn set_value(&self, view: JSValue) { bun_jsc::mark_binding!(); - let global = self.parent_const().global_this(); + let global = self.parent().global_this(); self.pending_value.with_mut(|pv| pv.set(global, view)); } @@ -769,7 +769,7 @@ impl ByteStream { } if let Some(mut action) = self.buffer_action.replace(None) { - let global = self.parent_const().global_this(); + let global = self.parent().global_this(); action.reject( global, &streams::StreamError::AbortReason(jsc::CommonAbortReason::UserAbort), @@ -854,7 +854,7 @@ impl ByteStream { p.result.release(); p.result = streams::Result::Done; }); - self.parent_const().is_closed.set(true); + self.parent().is_closed.set(true); return Some(blob::Any::InternalBlob(blob::Internal { bytes: buffer, was_string: false, @@ -915,7 +915,7 @@ pub mod testing_apis { return Err(global.throw(format_args!("expected a ByteStream-backed ReadableStream"))); }; bytes - .parent_const() + .parent() .producer .set(streams::SourceHandle::TestingCancelOnDrain(bytes)); Ok(JSValue::UNDEFINED) diff --git a/src/runtime/webcore/FileReader.rs b/src/runtime/webcore/FileReader.rs index d480295f7f25..f77a6ad98962 100644 --- a/src/runtime/webcore/FileReader.rs +++ b/src/runtime/webcore/FileReader.rs @@ -984,7 +984,7 @@ impl FileReader { pub type Source = readable_stream::NewSource; // Every `FileReader` is the `context` field of a heap-allocated `Source`. -bun_core::impl_field_parent! { FileReader => Source.context; pub fn shared parent; pub fn shared parent_const; } +bun_core::impl_field_parent! { FileReader => Source.context; pub fn shared parent; } impl readable_stream::SourceContext for FileReader { const NAME: &'static str = "File"; From 421f2586590a27cfe5d63ce43c37b97c1fed01b4 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Sun, 6 Sep 2026 20:35:07 +0000 Subject: [PATCH 8/8] ReadableStream::from_bytes_then_error builds its FileReader source through NewSource::new; HTTPServerWritable names core::ptr::NonNull --- src/runtime/webcore/ReadableStream.rs | 12 ++++++------ src/runtime/webcore/streams.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/runtime/webcore/ReadableStream.rs b/src/runtime/webcore/ReadableStream.rs index 102af949a4c4..e77df307a6de 100644 --- a/src/runtime/webcore/ReadableStream.rs +++ b/src/runtime/webcore/ReadableStream.rs @@ -597,11 +597,11 @@ impl ReadableStream { pub fn from_bytes_then_error( global_this: &JSGlobalObject, bytes: Vec, - err: syscall::Error, + err: bun_sys::Error, ) -> JsResult { - let source = NewSource::::new_mut(NewSource { - global_this: Some(bun_ptr::BackRef::new(global_this)), - context: FileReader { + // The JS wrapper made by `to_readable_stream()` owns the source. + let source = NewSource::new( + FileReader { event_loop: core::cell::Cell::new(jsc::EventLoopHandle::init( global_this.bun_vm().as_mut().event_loop().cast(), )), @@ -611,8 +611,8 @@ impl ReadableStream { done: Cell::new(true), ..Default::default() }, - ..Default::default() - }); + global_this, + ); source.to_readable_stream(global_this) } diff --git a/src/runtime/webcore/streams.rs b/src/runtime/webcore/streams.rs index faaacfe59011..f38731e6bbc5 100644 --- a/src/runtime/webcore/streams.rs +++ b/src/runtime/webcore/streams.rs @@ -1059,7 +1059,7 @@ pub struct HTTPServerWritable { pub(crate) auto_flusher: AutoFlusher, /// The allocation's root pointer (what C++ holds as `m_sinkPtr`), set by /// the owner right after boxing; the auto-flush callback writes through it. - pub(crate) root: core::cell::Cell>>, + pub(crate) root: core::cell::Cell>>, } impl Default for HTTPServerWritable {