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
171 changes: 154 additions & 17 deletions core/engine/src/builtins/array_buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ pub(crate) mod utils;
#[cfg(test)]
mod tests;

use std::ops::{Deref, DerefMut};
use std::{
ops::{Deref, DerefMut},
ptr::NonNull,
slice,
};

use aligned_vec::{ABox, AVec, ConstAlign};
pub use shared::SharedArrayBuffer;
Expand Down Expand Up @@ -197,12 +201,73 @@ impl BufferObject {
}
}

/// A region of embedder-owned memory backing an [`ArrayBuffer`] or a
/// [`SharedArrayBuffer`].
///
/// Boa never allocates, reallocates nor deallocates this region; the embedder
/// that created it is fully responsible for its lifetime and validity.
#[derive(Debug, Clone, Copy)]
pub(crate) struct ExternalMemory {
ptr: NonNull<u8>,
len: usize,
}

impl ExternalMemory {
fn as_slice(&self) -> &[u8] {
// SAFETY: The creator of an `ExternalMemory` guarantees that `ptr` is valid
// for reads and writes of `len` bytes for the whole lifetime of the buffer.
unsafe { slice::from_raw_parts(self.ptr.as_ptr(), self.len) }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One more precondition for slice::from_raw_parts: the region size must be <= isize::MAX bytes. It's a documented safety requirement of from_raw_parts, so it'd be worth either asserting it at construction (from_external_data/from_external_ptr) or adding it to the # Safety list the caller must uphold.

}

fn as_slice_mut(&mut self) -> &mut [u8] {
// SAFETY: The creator of an `ExternalMemory` guarantees that `ptr` is valid
// for reads and writes of `len` bytes for the whole lifetime of the buffer.
unsafe { slice::from_raw_parts_mut(self.ptr.as_ptr(), self.len) }
}

fn len(&self) -> usize {
self.len
}
}

/// The backing memory of an [`ArrayBuffer`].
#[derive(Debug, Clone)]
pub(crate) enum BufferData {
/// Memory allocated and owned by Boa.
Owned(AlignedVec<u8>),
/// Memory owned by the embedder. See [`ArrayBuffer::from_external_data`].
External(ExternalMemory),
}

impl BufferData {
fn as_slice(&self) -> &[u8] {
match self {
Self::Owned(vec) => vec,
Self::External(ext) => ext.as_slice(),
}
}

fn as_slice_mut(&mut self) -> &mut [u8] {
match self {
Self::Owned(vec) => vec,
Self::External(ext) => ext.as_slice_mut(),
}
}

fn len(&self) -> usize {
match self {
Self::Owned(vec) => vec.len(),
Self::External(ext) => ext.len(),
}
}
}

/// The internal representation of an `ArrayBuffer` object.
#[derive(Debug, Clone, Trace, Finalize, JsData)]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ArrayBuffer derives Clone, which is now unsound for the External variant: cloning copies the raw pointer, so two ArrayBuffers alias the same region and can each hand out &mut [u8] to it simultaneously. Owned buffers are fine (the Vec is deep-copied), but an external clone breaks the aliasing invariant from_external_data documents.

Would a manual Clone that deep-copies the External variant into an Owned one (or rejects cloning external buffers) work here?

pub struct ArrayBuffer {
/// The `[[ArrayBufferData]]` internal slot.
#[unsafe_ignore_trace]
data: Option<AlignedVec<u8>>,
data: Option<BufferData>,

/// The `[[ArrayBufferMaxByteLength]]` internal slot.
max_byte_len: Option<u64>,
Expand All @@ -214,26 +279,70 @@ pub struct ArrayBuffer {
impl ArrayBuffer {
pub(crate) fn from_data(data: AlignedVec<u8>, detach_key: JsValue) -> Self {
Self {
data: Some(data),
data: Some(BufferData::Owned(data)),
max_byte_len: None,
detach_key,
}
}

/// Creates a new `ArrayBuffer` over an embedder-owned memory region.
///
/// The bytes of the buffer alias the provided region directly; writes done through
/// JavaScript are immediately visible to the embedder and vice versa. Boa never
/// allocates, grows nor frees the region.
///
/// Externally-backed buffers are always fixed-length and cannot be detached,
/// resized nor transferred.
///
/// # Safety
///
/// The caller must guarantee that:
///
/// - `ptr` is non-null and valid for reads and writes of `len` bytes for the whole
/// lifetime of the returned buffer (and of every object that shares its data, e.g.
/// typed arrays or `DataView`s constructed over it).
/// - The region is not accessed (read or written) from Rust while JavaScript code
/// that may access the buffer is executing, and no other Rust references to the
/// region are alive while Boa holds a reference into it.
/// - The region is not aliased by another `ArrayBuffer` or `SharedArrayBuffer`
/// while both can be accessed.
///
/// # Panics
///
/// Panics if `ptr` is null.
#[must_use]
pub unsafe fn from_external_data(ptr: *mut u8, len: usize) -> Self {
let ptr = NonNull::new(ptr).expect("`ptr` must be non-null");
Self {
data: Some(BufferData::External(ExternalMemory { ptr, len })),
max_byte_len: None,
detach_key: JsValue::undefined(),
}
}

/// Returns `true` if this buffer is backed by embedder-owned memory.
#[must_use]
pub fn is_external(&self) -> bool {
matches!(self.data, Some(BufferData::External(_)))
}

pub(crate) fn len(&self) -> usize {
self.data.as_ref().map_or(0, AlignedVec::len)
self.data.as_ref().map_or(0, BufferData::len)
}

pub(crate) fn bytes(&self) -> Option<&[u8]> {
self.data.as_deref()
self.data.as_ref().map(BufferData::as_slice)
}

pub(crate) fn bytes_mut(&mut self) -> Option<&mut [u8]> {
self.data.as_deref_mut()
self.data.as_mut().map(BufferData::as_slice_mut)
}

pub(crate) fn vec_mut(&mut self) -> Option<&mut AlignedVec<u8>> {
self.data.as_mut()
match self.data.as_mut() {
Some(BufferData::Owned(vec)) => Some(vec),
_ => None,
}
}

/// Sets the maximum byte length of the buffer, returning the previous value if present.
Expand All @@ -244,7 +353,7 @@ impl ArrayBuffer {
/// Gets the inner bytes of the buffer without accessing the current atomic length.
#[track_caller]
pub(crate) fn bytes_with_len(&self, len: usize) -> Option<&[u8]> {
if let Some(s) = self.data.as_deref() {
if let Some(s) = self.bytes() {
Some(&s[..len])
} else {
None
Expand All @@ -254,7 +363,7 @@ impl ArrayBuffer {
/// Gets the mutable inner bytes of the buffer without accessing the current atomic length.
#[track_caller]
pub(crate) fn bytes_with_len_mut(&mut self, len: usize) -> Option<&mut [u8]> {
if let Some(s) = self.data.as_deref_mut() {
if let Some(s) = self.bytes_mut() {
Some(&mut s[..len])
} else {
None
Expand All @@ -264,11 +373,19 @@ impl ArrayBuffer {
/// Gets the underlying vector for this buffer.
#[must_use]
pub fn data(&self) -> Option<&[u8]> {
self.data.as_deref()
self.bytes()
}

/// Resizes the buffer to the new size, clamped to the maximum byte length if present.
pub fn resize(&mut self, new_byte_length: u64) -> JsResult<()> {
if self.is_external() {
return Err(JsNativeError::typ()
.with_message(
"ArrayBuffer.resize: cannot resize a buffer backed by embedder-owned memory",
)
.into());
}

let Some(max_byte_len) = self.max_byte_len else {
return Err(JsNativeError::typ()
.with_message("ArrayBuffer.resize: cannot resize a fixed-length buffer")
Expand Down Expand Up @@ -298,15 +415,25 @@ impl ArrayBuffer {
///
/// # Errors
///
/// Throws an error if the provided detach key is invalid.
/// Throws an error if the provided detach key is invalid, or if the buffer is backed by
/// embedder-owned memory (see [`ArrayBuffer::from_external_data`]).
pub fn detach(&mut self, key: &JsValue) -> JsResult<Option<AlignedVec<u8>>> {
if !JsValue::same_value(&self.detach_key, key) {
return Err(JsNativeError::typ()
.with_message("Cannot detach array buffer with different key")
.into());
}

Ok(self.data.take())
if self.is_external() {
return Err(JsNativeError::typ()
.with_message("cannot detach an ArrayBuffer backed by embedder-owned memory")
.into());
}

Ok(self.data.take().map(|data| match data {
BufferData::Owned(vec) => vec,
BufferData::External(_) => unreachable!("already checked for external data"),
}))
}

/// `IsDetachedBuffer ( arrayBuffer )`
Expand Down Expand Up @@ -788,12 +915,22 @@ impl ArrayBuffer {
};

// 5. If IsDetachedBuffer(arrayBuffer) is true, throw a TypeError exception.
let Some(mut bytes) = buf.borrow_mut().data_mut().data.take() else {
let Some(data) = buf.borrow_mut().data_mut().data.take() else {
return Err(JsNativeError::typ()
.with_message("cannot transfer a detached buffer")
.into());
};

let mut bytes = match data {
BufferData::Owned(bytes) => bytes,
data @ BufferData::External(_) => {
buf.borrow_mut().data_mut().data = Some(data);
return Err(JsNativeError::typ()
.with_message("cannot transfer an ArrayBuffer backed by embedder-owned memory")
.into());
}
};

// 6. If preserveResizability is preserve-resizability and IsResizableArrayBuffer(arrayBuffer)
// is true, then
// a. Let newMaxByteLength be arrayBuffer.[[ArrayBufferMaxByteLength]].
Expand All @@ -807,7 +944,7 @@ impl ArrayBuffer {

// 8. If arrayBuffer.[[ArrayBufferDetachKey]] is not undefined, throw a TypeError exception.
if !buf.borrow().data().detach_key.is_undefined() {
buf.borrow_mut().data_mut().data = Some(bytes);
buf.borrow_mut().data_mut().data = Some(BufferData::Owned(bytes));
return Err(JsNativeError::typ()
.with_message("cannot transfer a buffer with a detach key")
.into());
Expand All @@ -827,7 +964,7 @@ impl ArrayBuffer {
// 16. Return newBuffer.
if let Some(new_max_len) = new_max_len {
if new_len > new_max_len {
buf.borrow_mut().data_mut().data = Some(bytes);
buf.borrow_mut().data_mut().data = Some(BufferData::Owned(bytes));
return Err(JsNativeError::range()
.with_message("`length` cannot be bigger than `maxByteLength`")
.into());
Expand All @@ -851,7 +988,7 @@ impl ArrayBuffer {
context.root_shape(),
prototype,
ArrayBuffer {
data: Some(bytes),
data: Some(BufferData::Owned(bytes)),
max_byte_len: new_max_len,
detach_key: JsValue::undefined(),
},
Expand Down Expand Up @@ -904,7 +1041,7 @@ impl ArrayBuffer {
Self {
// 6. Set obj.[[ArrayBufferData]] to block.
// 7. Set obj.[[ArrayBufferByteLength]] to byteLength.
data: Some(block),
data: Some(BufferData::Owned(block)),
// 8. If allocatingResizableBuffer is true, then
// c. Set obj.[[ArrayBufferMaxByteLength]] to maxByteLength.
max_byte_len,
Expand Down
Loading
Loading