Skip to content

add ArrayBuffer, ArrayBufferView, SharedArrayBuffer, and BackingStore to Rust JSG#6398

Open
anonrig wants to merge 2 commits intomainfrom
yagiz/arraybuffer-arraybufferview
Open

add ArrayBuffer, ArrayBufferView, SharedArrayBuffer, and BackingStore to Rust JSG#6398
anonrig wants to merge 2 commits intomainfrom
yagiz/arraybuffer-arraybufferview

Conversation

@anonrig
Copy link
Copy Markdown
Member

@anonrig anonrig commented Mar 24, 2026

Built on top of #6386 branch

@anonrig anonrig requested review from guybedford and jasnell March 24, 2026 15:34
@anonrig anonrig requested review from a team as code owners March 24, 2026 15:34
@anonrig anonrig force-pushed the yagiz/arraybuffer-arraybufferview branch from 77dedb2 to 2ccf890 Compare March 24, 2026 15:36
Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk bot left a comment

Choose a reason for hiding this comment

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

I'm Bonk, and I've done a quick review of your PR.

This PR adds ArrayBuffer, ArrayBufferView, SharedArrayBuffer, and BackingStore Rust bindings to the JSG layer, with extensive test coverage.

Issues

  1. [High] SharedArrayBuffer::as_slice / as_mut_slice and BackingStore::as_mut_slice for shared backing stores are unsoundSharedArrayBuffer memory can be concurrently modified by other threads or by JS (via Atomics or typed array writes from another agent). Creating &[u8] / &mut [u8] from shared memory is undefined behavior in Rust because it violates the aliasing/provenance model. The &mut [u8] case is especially dangerous: Rust assumes exclusive access through &mut references. Even the &[u8] case is technically UB if another thread writes during the borrow. The doc comments mention this but "caller is responsible for synchronization" isn't enforceable. This may be acceptable as a pragmatic choice for now since workerd is single-threaded per-isolate, but the API should either (a) make these methods unsafe to signal the invariant, or (b) return raw pointers / use UnsafeCell semantics instead. Adding unsafe would be the minimal fix. See inline comment.

  2. [Medium] new_uninit is misleading — the buffer IS zero-initializedArrayBuffer::new_uninit and SharedArrayBuffer::new_uninit call NewBackingStore(isolate, length) which zero-initializes the memory. The name new_uninit strongly implies the contents are uninitialized (as in MaybeUninit), but the test itself asserts the memory is zeroed. This is a naming footgun that could lead callers to skip initialization thinking it's redundant, or worse, treat the memory as potentially uninitialized. Consider new_zeroed or just with_byte_length. See inline comment.

  3. [Low] Doc comment references non-existent method namesBackingStore doc says "Obtained via Local<ArrayBuffer>::get_backing_store" but the actual method is called backing_store() (no get_ prefix).

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Mar 24, 2026

Review posted successfully. Here's a summary of the findings:

  1. [High] Soundness issue with SharedArrayBuffer slice methodsas_slice()/as_mut_slice() on SharedArrayBuffer and BackingStore create Rust references (&[u8]/&mut [u8]) to potentially concurrently-accessed memory, which is UB under Rust's aliasing model. Suggested making these unsafe fn.

  2. [Medium] new_uninit naming is misleading — The memory is actually zero-initialized by V8, but the name implies MaybeUninit semantics. Suggested renaming to new_zeroed or with_byte_length.

  3. [Low] Doc comment typoBackingStore docs reference get_backing_store but the method is actually backing_store().

github run

@anonrig anonrig force-pushed the yagiz/arraybuffer-arraybufferview branch from 2ccf890 to 22dbb60 Compare March 24, 2026 16:05

size_t local_array_buffer_get_backing_store(Isolate* isolate, const Local& buffer) {
return reinterpret_cast<size_t>(
new BackingStorePtr(local_as_ref_from_ffi<v8::ArrayBuffer>(buffer)->GetBackingStore()));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The fact that this is heap allocating a std::shared_ptr gets lost behind the using alias. newing a std::shared_ptr is itself a bit of an unsafe antipattern and it's unfortunate that we're having to resort to that here, especially under the guise of making things safer by porting them to rust.

In nothing else tho, I'd drop the using alias and just use the std::shared_ptr<v8::BackingStore> directly just so it's clearer at a glance what is happening.

auto* ptr = static_cast<uint8_t*>(ab->Data());
for (size_t i = 0; i < byteLen; i++) {
result.push_back(ptr[i]);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is there no memcpy equivalent? Does the rust compiler optimize this away? For large array buffers this O(n) loop is going to be slow.

result.push_back(ptr[i]);
}
return result;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

There's a fair amount of duplication between these unwrap methods. Can they can condensed into one?


::rust::Vec<uint8_t> unwrap_array_buffer_view(Isolate* isolate, Local value) {
auto v8Val = local_from_ffi<v8::Value>(kj::mv(value));
KJ_REQUIRE(v8Val->IsArrayBufferView());
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This will only work for Uint8Array, Uint8ClampedArray, and DataView. All the remaining TypedArray types are multi-byte, not to mention the Floating point and Bigint types that will have completely different intepretations.

}

// Unwrappers
::rust::Vec<uint8_t> unwrap_array_buffer(Isolate* isolate, Local value) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This isn't really "unwrap". It's really "copy"

kj::Maybe<Local> array_buffer_maybe_new(
Isolate* isolate, size_t byte_length, BackingStoreInitializationMode mode) {
auto maybe = v8::ArrayBuffer::MaybeNew(
isolate, byte_length, static_cast<v8::BackingStoreInitializationMode>(mode));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We should actually drop our MaybeNew patch in favor of the newer version of v8::ArrayBuffer::New that accepts the flag telling it how to handle allocation failure. This MaybeNew is redundant now.

// `ArrayBufferView`-specific implementations
// =============================================================================

impl<'a> Local<'a, ArrayBufferView> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

For streams use cases, this will need element size and whether the underlying view is an integer type or not.

pub unsafe fn buffer_data(&self) -> *mut u8 {
// SAFETY: handle is valid within the current HandleScope.
unsafe { ffi::local_array_buffer_view_buffer_data(self.isolate.as_ffi(), &self.handle) }
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we'll need the ability to get a view of the elements, not just the bytes. If we can't do that reasonably in the ArrayBufferView, then we might need separate rust classes for each of the TypedArray kinds.

}

impl Local<'_, ArrayBuffer> {
/// Returns the byte length of this `ArrayBuffer`.
Copy link
Copy Markdown
Collaborator

@jasnell jasnell Mar 25, 2026

Choose a reason for hiding this comment

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

For streams use cases we'll need a detach / is_detached / is_detachable API.

}
}

impl Local<'_, SharedArrayBuffer> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Honestly, I'm not sure there's enough value in this as a separate class given the duplication/overlap with ArrayBuffer. We could likely simplify and have ArrayBuffer encapsulate this and just add a is_shared API there.

@anonrig anonrig force-pushed the yagiz/add-typed-array-methods branch 2 times, most recently from 9b527bb to d88cf6b Compare March 26, 2026 19:23
Base automatically changed from yagiz/add-typed-array-methods to main March 26, 2026 19:59
Comment on lines +721 to +725
/// # Safety
/// For `SharedArrayBuffer` backing stores (`is_shared() == true`) the
/// underlying memory may be concurrently read or written by other agents
/// (JS threads, `Atomics` operations). The caller must ensure no concurrent
/// writes occur for the duration of the borrow, or use atomic operations.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is returning a non-mutable slice, so why does the safety not mention mutability?

///
/// Zero-copy. The caller must ensure no other live slice aliases this region.
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [u8] {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why isn't this unsafe?


/// Returns a mutable byte slice view into the `ArrayBuffer`'s backing store.
///
/// Zero-copy. The caller must ensure no other live slice aliases this region.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The caller must ensure no other live slice aliases this region.

This very much goes against the concept of singular ownership in Rust... do we not have any better guarantees ever?

///
/// Zero-copy. The caller must ensure no other live slice aliases this region.
#[inline]
pub fn as_mut_slice(&mut self) -> &mut [u8] {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Again, it is unclear to me why BackingStore::as_mut_slice is unsafe, while ArrayBuffer::as_mut_slice is? Would be good to understand better the difference in ownership between these modes.

Copy link
Copy Markdown
Contributor

@guybedford guybedford left a comment

Choose a reason for hiding this comment

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

Shouldn't we require a lock to obtain a mutable slice, so that the mutable slice is only valid for the duration of the lock? It seems this is necessary to properly support ownership semantics?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants