Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions src/jsc/array_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,21 @@ impl ArrayBuffer {
}
}

pub fn from_bytes(bytes: &mut [u8], typed_array_type: JSType) -> ArrayBuffer {
/// Wrap a borrowed slice's pointer in an `ArrayBuffer` without taking
/// ownership. The returned struct (and any JS object created from it)
/// stores `bytes.as_mut_ptr()` with no lifetime parameter.
///
/// Prefer [`ArrayBuffer::from_owned_bytes`] / [`ArrayBuffer::from_owned_vec`]
/// — they make the ownership transfer visible in the type system.
///
/// # Safety
/// The allocation backing `bytes` must outlive every use of the returned
/// `ArrayBuffer`, including the GC finalizer of any JS object created from
/// it. If a deallocator is installed (`to_js`, `to_js_unchecked`,
/// `to_js_with_context`), that deallocator becomes the sole owner of the
/// allocation: it must be valid to free `bytes.as_mut_ptr()` with it
/// exactly once, and nothing else may free or reuse the allocation.
pub unsafe fn from_bytes(bytes: &mut [u8], typed_array_type: JSType) -> ArrayBuffer {
ArrayBuffer {
len: u32::try_from(bytes.len()).expect("int cast") as usize,
byte_len: u32::try_from(bytes.len()).expect("int cast") as usize,
Expand Down Expand Up @@ -435,6 +449,24 @@ impl ArrayBuffer {
}
}

/// [`ArrayBuffer::from_owned_bytes`] for a `Vec<u8>` whose capacity may
/// exceed its length. The excess capacity is discarded without
/// reallocating: the deallocator installed by `to_js*` frees the whole
/// allocation from its data pointer, so the capacity is not needed.
pub fn from_owned_vec(bytes: Vec<u8>, typed_array_type: JSType) -> ArrayBuffer {
// Ownership transfers to JSC (see `from_owned_bytes`); suppress the
// Vec's Drop and keep only ptr/len.
let mut bytes = core::mem::ManuallyDrop::new(bytes);
let len = bytes.len();
ArrayBuffer {
len: u32::try_from(len).expect("int cast") as usize,
byte_len: u32::try_from(len).expect("int cast") as usize,
typed_array_type,
ptr: bytes.as_mut_ptr(),
..Default::default()
}
}

pub fn to_js_unchecked(self, ctx: &JSGlobalObject) -> JsResult<JSValue> {
// The reason for this is
// JSC C API returns a detached arraybuffer
Expand Down Expand Up @@ -917,15 +949,13 @@ impl MarkedArrayBuffer {
}

pub fn from_string(str: &[u8]) -> Result<MarkedArrayBuffer, bun_alloc::AllocError> {
// allocator.dupe(u8, str) → Box::<[u8]>::from(str), but we need a raw
// pointer because the buffer is later freed via the default allocator
// (`MarkedArrayBuffer_deallocator` → `default_alloc::free`).
let buf: Box<[u8]> = Box::from(str);
let len = buf.len();
let ptr = bun_core::heap::into_raw(buf).cast::<u8>();
// SAFETY: ptr/len from heap::alloc; backed by the global allocator.
let bytes = unsafe { bun_core::ffi::slice_mut(ptr, len) };
Ok(MarkedArrayBuffer::from_bytes(bytes, JSType::Uint8Array))
// allocator.dupe(u8, str) → Box::<[u8]>::from(str); the buffer is later
// freed via the default allocator (`MarkedArrayBuffer_deallocator` →
// `default_alloc::free`).
Ok(MarkedArrayBuffer::from_owned_bytes(
Box::from(str),
JSType::Uint8Array,
))
}

pub fn from_js(global: &JSGlobalObject, value: JSValue) -> Option<MarkedArrayBuffer> {
Expand All @@ -936,9 +966,13 @@ impl MarkedArrayBuffer {
})
}

pub fn from_bytes(bytes: &mut [u8], typed_array_type: JSType) -> MarkedArrayBuffer {
/// Take ownership of a default-allocator `Box<[u8]>` and wrap it as an
/// owning `MarkedArrayBuffer`. The buffer is freed exactly once: either by
/// [`MarkedArrayBuffer::destroy`] or by the deallocator installed when the
/// buffer is handed to JSC (`to_js` / `to_node_buffer`).
pub fn from_owned_bytes(bytes: Box<[u8]>, typed_array_type: JSType) -> MarkedArrayBuffer {
MarkedArrayBuffer {
buffer: ArrayBuffer::from_bytes(bytes, typed_array_type),
buffer: ArrayBuffer::from_owned_bytes(bytes, typed_array_type),
owns_buffer: true,
}
}
Expand All @@ -959,7 +993,7 @@ impl MarkedArrayBuffer {
}

/// Releases the owned byte buffer if this `MarkedArrayBuffer` was created with an
/// allocator (e.g. via `from_string`/`from_bytes`). Does not free the struct itself;
/// allocator (e.g. via `from_string`/`from_owned_bytes`). Does not free the struct itself;
/// `MarkedArrayBuffer` is passed and stored by value, so callers own its storage.
pub fn destroy(&mut self) {
if self.owns_buffer {
Expand Down
24 changes: 8 additions & 16 deletions src/runtime/api/BunObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2630,12 +2630,10 @@ pub mod JSZlib {
list.shrink_to_fit();
// Ownership of the allocation transfers to JSC; freed via
// `global_deallocator` once the ArrayBuffer is finalized.
let leaked: &'static mut [u8] = list.leak();
let ptr = leaked.as_mut_ptr();
let array_buffer = ArrayBuffer::from_bytes(leaked, jsc::JSType::Uint8Array);
let array_buffer = ArrayBuffer::from_owned_vec(list, jsc::JSType::Uint8Array);
array_buffer.to_js_with_context(
global_this,
ptr.cast::<c_void>(),
array_buffer.ptr.cast::<c_void>(),
Some(global_deallocator),
)
}
Expand Down Expand Up @@ -2680,12 +2678,10 @@ pub mod JSZlib {

// Ownership of the allocation transfers to JSC; freed via
// `global_deallocator` once the ArrayBuffer is finalized.
let leaked: &'static mut [u8] = list.leak();
let ptr = leaked.as_mut_ptr();
let array_buffer = ArrayBuffer::from_bytes(leaked, jsc::JSType::Uint8Array);
let array_buffer = ArrayBuffer::from_owned_vec(list, jsc::JSType::Uint8Array);
array_buffer.to_js_with_context(
global_this,
ptr.cast::<c_void>(),
array_buffer.ptr.cast::<c_void>(),
Some(global_deallocator),
)
}
Expand Down Expand Up @@ -2781,12 +2777,10 @@ pub mod JSZlib {
list.shrink_to_fit();
// Ownership of the allocation transfers to JSC; freed via
// `global_deallocator` once the ArrayBuffer is finalized.
let leaked: &'static mut [u8] = list.leak();
let ptr = leaked.as_mut_ptr();
let array_buffer = ArrayBuffer::from_bytes(leaked, jsc::JSType::Uint8Array);
let array_buffer = ArrayBuffer::from_owned_vec(list, jsc::JSType::Uint8Array);
array_buffer.to_js_with_context(
global_this,
ptr.cast::<c_void>(),
array_buffer.ptr.cast::<c_void>(),
Some(global_deallocator),
)
}
Expand Down Expand Up @@ -2824,12 +2818,10 @@ pub mod JSZlib {

// Ownership of the allocation transfers to JSC; freed via
// `global_deallocator` once the ArrayBuffer is finalized.
let leaked: &'static mut [u8] = list.leak();
let ptr = leaked.as_mut_ptr();
let array_buffer = ArrayBuffer::from_bytes(leaked, jsc::JSType::Uint8Array);
let array_buffer = ArrayBuffer::from_owned_vec(list, jsc::JSType::Uint8Array);
array_buffer.to_js_with_context(
global_this,
ptr.cast::<c_void>(),
array_buffer.ptr.cast::<c_void>(),
Some(global_deallocator),
)
}
Expand Down
10 changes: 5 additions & 5 deletions src/runtime/api/bun/Terminal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1810,11 +1810,11 @@ impl Terminal {
return true;
}
v.extend_from_slice(chunk);
// MarkedArrayBuffer::from_bytes takes a `&mut [u8]` it will own (freed
// via mimalloc on the C++ side) — leak the Box and hand over the slice.
let bytes: &'static mut [u8] = Box::leak(v.into_boxed_slice());
let data = MarkedArrayBuffer::from_bytes(bytes, jsc::JSType::Uint8Array)
.to_node_buffer(global_this);
// MarkedArrayBuffer::from_owned_bytes takes ownership of the allocation
// (freed via mimalloc on the C++ side).
let data =
MarkedArrayBuffer::from_owned_bytes(v.into_boxed_slice(), jsc::JSType::Uint8Array)
.to_node_buffer(global_this);

global_this.bun_vm().event_loop_mut().run_callback(
callback,
Expand Down
15 changes: 8 additions & 7 deletions src/runtime/api/bun/subprocess/SubprocessPipeReader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,13 +314,14 @@ impl PipeReader {
State::Done(bytes) => {
let bytes = core::mem::take(bytes);
// `defer this.state = .{ .done = &.{} }` — state.done is now empty via take().
// PORT NOTE: `MarkedArrayBuffer::from_bytes` takes a borrowed `&mut [u8]`
// with `owns_buffer = true` (freed via mimalloc on the JS side); leak the
// boxed slice so JS becomes the owner — same pattern as
// `MarkedArrayBuffer::from_string`.
let slice: &'static mut [u8] = Box::leak(bytes.into_boxed_slice());
MarkedArrayBuffer::from_bytes(slice, jsc::JSType::Uint8Array)
.to_node_buffer(global_this)
// `MarkedArrayBuffer::from_owned_bytes` adopts the allocation with
// `owns_buffer = true` (freed via mimalloc on the JS side) — same
// pattern as `MarkedArrayBuffer::from_string`.
MarkedArrayBuffer::from_owned_bytes(
bytes.into_boxed_slice(),
jsc::JSType::Uint8Array,
)
.to_node_buffer(global_this)
}
_ => JSValue::UNDEFINED,
}
Expand Down
7 changes: 5 additions & 2 deletions src/runtime/ffi/FFIObject.rs
Original file line number Diff line number Diff line change
Expand Up @@ -838,9 +838,12 @@ pub fn to_array_buffer(
}
}

// SAFETY: ptr/len came from get_ptr_slice; FFI-owned memory.
// SAFETY: ptr/len came from get_ptr_slice; the memory is owned by the
// FFI caller, who promises (per the `toArrayBuffer` API contract) that
// it stays valid until the optional finalization callback runs.
let slice = unsafe { core::slice::from_raw_parts_mut(ptr, len) };
ArrayBuffer::from_bytes(slice, jsc::JSType::ArrayBuffer).to_js_with_context(
// SAFETY: see above — lifetime/ownership is the FFI caller's contract.
unsafe { ArrayBuffer::from_bytes(slice, jsc::JSType::ArrayBuffer) }.to_js_with_context(
global_this,
ctx.unwrap_or(core::ptr::null_mut()),
callback,
Expand Down
9 changes: 7 additions & 2 deletions src/runtime/image/Image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1754,8 +1754,13 @@ impl<'a> PipelineTask<'a> {
out_slice.len(),
)
};
let v = ArrayBuffer::from_bytes(mut_slice, jsc::JSType::Uint8Array)
.to_js_with_context(global, core::ptr::null_mut(), Some(out.free));
// SAFETY: `out.bytes` is the codec-owned allocation whose
// ownership transfers to JSC; `out.free` (installed below)
// is the sole release path now that the codec `Drop` is
// suppressed via ManuallyDrop.
let v =
unsafe { ArrayBuffer::from_bytes(mut_slice, jsc::JSType::Uint8Array) }
.to_js_with_context(global, core::ptr::null_mut(), Some(out.free));
match v {
Ok(v) => promise.resolve(global, v)?,
Err(_) => return promise.reject(global, Err(jsc::JsError::Thrown)),
Expand Down
10 changes: 4 additions & 6 deletions src/runtime/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -410,12 +410,10 @@ impl<R> MaybeSysExt<R> for Maybe<R, bun_sys::Error> {
// `ArrayBuffer.fromBytes` and ownership transfers to JSC — the
// GC-installed deallocator (`MarkedArrayBuffer_deallocator`)
// calls `mi_free` on the buffer when the JS object is
// collected. Leak the `Vec` here to hand the allocation to
// JSC; Bun's global allocator is mimalloc, so `to_js`'s
// collected. Bun's global allocator is mimalloc, so `to_js`'s
// `mi_is_in_heap_region` check succeeds and the buffer is
// freed by JSC, not Rust.
let bytes: &mut [u8] = Vec::leak(r.into());
bun_jsc::ArrayBuffer::from_bytes(bytes, bun_jsc::JSType::ArrayBuffer)
bun_jsc::ArrayBuffer::from_owned_vec(r.into(), bun_jsc::JSType::ArrayBuffer)
.to_js(global_object)
}
Err(e) => Ok(e.to_js(global_object)),
Expand Down Expand Up @@ -643,8 +641,8 @@ impl MaybeToJs for Vec<u8> {
// PORT NOTE: ownership transfers to JSC (freed via
// `MarkedArrayBuffer_deallocator` → `mi_free`); see
// `MaybeSysExt::to_array_buffer` above for the full rationale.
let bytes: &mut [u8] = Vec::leak(self);
bun_jsc::ArrayBuffer::from_bytes(bytes, bun_jsc::JSType::ArrayBuffer).to_js(global_object)
bun_jsc::ArrayBuffer::from_owned_vec(self, bun_jsc::JSType::ArrayBuffer)
.to_js(global_object)
}
}

Expand Down
30 changes: 9 additions & 21 deletions src/runtime/node/node_fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7077,16 +7077,10 @@ impl NodeFS {
if let Some(file) = unsafe { &mut *graph }.find(path.as_bytes()) {
let contents: &[u8] = file.contents.as_bytes();
return if args.encoding == Encoding::Buffer {
// PORTING.md §Forbidden bans `Vec::leak()`; round-trip through
// `into_boxed_slice()` so the allocation layout JSC frees with
// matches what we hand it (capacity == len).
let raw =
bun_core::heap::into_raw(contents.to_vec().into_boxed_slice());
// SAFETY: ownership of the allocation is transferred to JSC; the
// ArrayBuffer finalizer reconstructs the Box and frees it
// (PORTING.md:348 — `heap::alloc`/`from_raw` across FFI).
Ok(ret::ReadFileWithOptions::Buffer(Buffer::from_bytes(
unsafe { &mut *raw },
// Ownership of the allocation is transferred to JSC; the
// ArrayBuffer finalizer frees it.
Ok(ret::ReadFileWithOptions::Buffer(Buffer::from_owned_bytes(
contents.to_vec().into_boxed_slice(),
bun_jsc::JSType::Uint8Array,
)))
} else if string_type == ReadFileStringType::Default {
Expand Down Expand Up @@ -7218,15 +7212,11 @@ impl NodeFS {
};
}
}
let raw = bun_core::heap::into_raw(
// Ownership transferred to JSC; freed via ArrayBuffer finalizer.
Ok(ret::ReadFileWithOptions::Buffer(Buffer::from_owned_bytes(
temporary_read_buffer_before_stat_call
.to_vec()
.into_boxed_slice(),
);
// SAFETY: ownership transferred to JSC; freed via ArrayBuffer finalizer
// (PORTING.md:348 — `heap::alloc`/`from_raw` across FFI).
Ok(ret::ReadFileWithOptions::Buffer(Buffer::from_bytes(
unsafe { &mut *raw },
bun_jsc::JSType::Uint8Array,
)))
}
Expand Down Expand Up @@ -7391,11 +7381,9 @@ impl NodeFS {
match args.encoding {
Encoding::Buffer => {
buf.truncate(final_len);
let raw = bun_core::heap::into_raw(buf.into_boxed_slice());
// SAFETY: ownership transferred to JSC; freed via ArrayBuffer finalizer
// (PORTING.md:348 — `heap::alloc`/`from_raw` across FFI).
Ok(ret::ReadFileWithOptions::Buffer(Buffer::from_bytes(
unsafe { &mut *raw },
// Ownership transferred to JSC; freed via ArrayBuffer finalizer.
Ok(ret::ReadFileWithOptions::Buffer(Buffer::from_owned_bytes(
buf.into_boxed_slice(),
bun_jsc::JSType::Uint8Array,
)))
}
Expand Down
9 changes: 3 additions & 6 deletions src/runtime/shell/subproc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2363,12 +2363,9 @@ impl PipeReader {
pub fn to_buffer(&mut self, global_this: &JSGlobalObject) -> JSValue {
match &mut self.state {
PipeReaderState::Done(bytes) => {
// `MarkedArrayBuffer::from_bytes` adopts the allocation (freed
// by the JSC ArrayBuffer destructor). `heap::release` names that
// FFI hand-off — it is `Box::leak` under the hood; the JSC
// ArrayBuffer destructor is the reclaim, not this scope.
let slice: &'static mut [u8] = bun_core::heap::release(core::mem::take(bytes));
MarkedArrayBuffer::from_bytes(slice, jsc::JSType::Uint8Array)
// `MarkedArrayBuffer::from_owned_bytes` adopts the allocation
// (freed by the JSC ArrayBuffer destructor).
MarkedArrayBuffer::from_owned_bytes(core::mem::take(bytes), jsc::JSType::Uint8Array)
.to_node_buffer(global_this)
}
_ => JSValue::UNDEFINED,
Expand Down
5 changes: 4 additions & 1 deletion src/runtime/webcore/ArrayBufferSink.rs
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,10 @@ impl ArrayBufferSink {

pub fn end_from_js(&mut self, _global_this: &JSGlobalObject) -> bun_sys::Result<ArrayBuffer> {
if self.done {
return Ok(ArrayBuffer::from_bytes(&mut [], JSType::ArrayBuffer));
return Ok(ArrayBuffer::from_owned_bytes(
Box::default(),
JSType::ArrayBuffer,
));
}

debug_assert!(self.next.is_none());
Expand Down
Loading
Loading