diff --git a/moli-webidl/src/buffer_source.rs b/moli-webidl/src/buffer_source.rs new file mode 100644 index 0000000000..9fa4cea8c4 --- /dev/null +++ b/moli-webidl/src/buffer_source.rs @@ -0,0 +1,173 @@ +//! Borrowed BufferSource conversion for algorithms that copy after dictionary conversion. + +use crate::{Context, WebIdlConverter, WebIdlError}; +use std::sync::atomic::{AtomicU8, Ordering}; + +/// `[AllowShared] BufferSource`, retaining the native buffer identity instead +/// of copying during conversion. Resizable and growable buffers require the +/// separate `[AllowResizable]` annotation and are rejected here. +pub enum AllowSharedBufferSource<'s> { + Buffer(v8::Local<'s, v8::ArrayBuffer>), + Shared(v8::Local<'s, v8::SharedArrayBuffer>), + View(v8::Local<'s, v8::ArrayBufferView>), +} + +impl<'s> WebIdlConverter<'s> for AllowSharedBufferSource<'s> { + type Options = (); + + fn convert( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + context: Context, + _options: &Self::Options, + ) -> Result { + let source = if let Ok(buffer) = v8::Local::::try_from(value) { + Self::Buffer(buffer) + } else if let Ok(buffer) = v8::Local::::try_from(value) { + Self::Shared(buffer) + } else if let Ok(view) = v8::Local::::try_from(value) { + Self::View(view) + } else { + return Err(WebIdlError::cannot_convert(context, "BufferSource")); + }; + let native_resizable = match &source { + Self::Buffer(buffer) => buffer.is_resizable_by_user_javascript(), + Self::View(view) => view.buffer(scope).is_some_and(|buffer| { + buffer.is_array_buffer() && buffer.is_resizable_by_user_javascript() + }), + Self::Shared(_) => false, + }; + if native_resizable + || source + .backing_store(scope) + .is_some_and(|backing| backing.is_resizable_by_user_javascript()) + { + return Err(WebIdlError::cannot_convert( + context, + "non-resizable BufferSource", + )); + } + Ok(source) + } +} + +impl AllowSharedBufferSource<'_> { + pub fn byte_length(&self) -> usize { + match self { + Self::Buffer(buffer) => buffer.byte_length(), + Self::Shared(buffer) => buffer.byte_length(), + Self::View(view) => view.byte_length(), + } + } + + fn byte_offset(&self) -> usize { + match self { + Self::View(view) => view.byte_offset(), + _ => 0, + } + } + + fn backing_store( + &self, + scope: &mut v8::PinScope<'_, '_>, + ) -> Option> { + match self { + Self::Buffer(buffer) => Some(buffer.get_backing_store()), + Self::Shared(buffer) => Some(buffer.get_backing_store()), + Self::View(view) => view.buffer(scope).map(|buffer| buffer.get_backing_store()), + } + } + + pub fn to_vec(&self, scope: &mut v8::PinScope<'_, '_>) -> Vec { + let len = self.byte_length(); + if len == 0 { + return Vec::new(); + } + let backing = self + .backing_store(scope) + .expect("BufferSource has native backing"); + let offset = self.byte_offset(); + if backing.is_shared() { + // SAFETY: fixed shared storage is retained by `backing`, each byte + // is aligned for AtomicU8, and the view bounds were checked by V8. + let bytes = unsafe { + std::slice::from_raw_parts( + backing + .data() + .expect("nonempty backing") + .as_ptr() + .cast::() + .add(offset), + len, + ) + }; + bytes + .iter() + .map(|byte| byte.load(Ordering::Relaxed)) + .collect() + } else { + backing[offset..offset + len] + .iter() + .map(|byte| byte.get()) + .collect() + } + } + + /// Copy into the native view's byte range, preserving surrounding bytes. + pub fn write_bytes(&self, scope: &mut v8::PinScope<'_, '_>, bytes: &[u8]) -> bool { + if bytes.len() > self.byte_length() { + return false; + } + if bytes.is_empty() { + return true; + } + let backing = self + .backing_store(scope) + .expect("BufferSource has native backing"); + let offset = self.byte_offset(); + if backing.is_shared() { + // SAFETY: fixed shared storage is retained and V8 validated the + // view range; byte stores are unordered as required for BufferSource. + let destination = unsafe { + std::slice::from_raw_parts( + backing + .data() + .expect("nonempty backing") + .as_ptr() + .cast::() + .add(offset), + bytes.len(), + ) + }; + for (cell, byte) in destination.iter().zip(bytes) { + cell.store(*byte, Ordering::Relaxed); + } + } else { + for (cell, byte) in backing[offset..offset + bytes.len()].iter().zip(bytes) { + cell.set(*byte); + } + } + true + } +} + +impl<'s> WebIdlConverter<'s> for v8::Local<'s, v8::ArrayBuffer> { + type Options = (); + + fn convert( + _scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + context: Context, + _options: &Self::Options, + ) -> Result { + let buffer = Self::try_from(value) + .map_err(|_| WebIdlError::cannot_convert(context, "ArrayBuffer"))?; + if buffer.is_resizable_by_user_javascript() { + return Err(WebIdlError::cannot_convert( + context, + "non-resizable ArrayBuffer", + )); + } + Ok(buffer) + } +} diff --git a/moli-webidl/src/lib.rs b/moli-webidl/src/lib.rs index 19de1aa414..32648cb465 100644 --- a/moli-webidl/src/lib.rs +++ b/moli-webidl/src/lib.rs @@ -61,7 +61,9 @@ extern crate self as moli_webidl; +mod buffer_source; mod convert; +pub use buffer_source::AllowSharedBufferSource; mod error; mod helpers; mod traits; diff --git a/moli-webidl/tests/array_buffer.rs b/moli-webidl/tests/array_buffer.rs new file mode 100644 index 0000000000..c7fef860e3 --- /dev/null +++ b/moli-webidl/tests/array_buffer.rs @@ -0,0 +1,54 @@ +use moli_webidl::Context; + +#[test] +fn array_buffer_conversion_preserves_identity_and_rejects_detached_resizable_buffers() { + moli_v8_test_util::ensure_v8(); + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let other = v8::Context::new(scope, Default::default()); + for owner in [context, other] { + for resizable in [false, true] { + for detached in [false, true] { + let buffer = { + let scope = &mut v8::ContextScope::new(scope, owner); + let source = if resizable { + "new ArrayBuffer(2, {maxByteLength: 4})" + } else { + "new ArrayBuffer(2)" + }; + let source = v8::String::new(scope, source).unwrap(); + let value = v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap(); + v8::Local::::try_from(value).unwrap() + }; + if detached { + assert_eq!(buffer.detach(None), Some(true)); + } + assert_eq!(buffer.was_detached(), detached); + assert_eq!(buffer.is_resizable_by_user_javascript(), resizable); + let scope = &mut v8::ContextScope::new(scope, context); + // Author properties cannot affect the native conversion. + let key = v8::String::new(scope, "resizable").unwrap(); + let value = v8::Boolean::new(scope, !resizable); + assert_eq!( + buffer.create_data_property(scope, key.into(), value.into()), + Some(true) + ); + let converted = moli_webidl::convert::>( + scope, + buffer.into(), + Context::argument("Test.buffer", 1), + ); + if resizable { + assert!(converted.is_err(), "resizable buffer, detached={detached}"); + } else { + assert!(converted.unwrap().strict_equals(buffer.into())); + } + } + } + } +} diff --git a/moli-webidl/tests/borrowed_buffer_source.rs b/moli-webidl/tests/borrowed_buffer_source.rs new file mode 100644 index 0000000000..34bfd82b7f --- /dev/null +++ b/moli-webidl/tests/borrowed_buffer_source.rs @@ -0,0 +1,102 @@ +use moli_webidl::{AllowSharedBufferSource, Context}; +fn eval<'s>(scope: &mut v8::PinScope<'s, '_>, source: &str) -> v8::Local<'s, v8::Value> { + let source = v8::String::new(scope, source).unwrap(); + v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap() +} +#[test] +fn borrowed_buffer_sources_copy_late_and_write_only_the_native_range() { + moli_v8_test_util::ensure_v8(); + let mut isolate = v8::Isolate::new(Default::default()); + let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + for shared in [false, true] { + let value = eval( + scope, + if shared { + "globalThis.storage=new SharedArrayBuffer(6); new Uint8Array(storage).set([1,2,3,4,5,6]); new DataView(storage,1,3)" + } else { + "globalThis.storage=new ArrayBuffer(6); new Uint8Array(storage).set([1,2,3,4,5,6]); new DataView(storage,1,3)" + }, + ); + let source = moli_webidl::convert::( + scope, + value, + Context::argument("Test.buffer", 1), + ) + .unwrap(); + assert_eq!(source.byte_length(), 3); + eval(scope, "new Uint8Array(storage)[2]=9"); + assert_eq!(source.to_vec(scope), [2, 9, 4]); + assert!(!source.write_bytes(scope, &[7, 8, 9, 10])); + assert_eq!(source.to_vec(scope), [2, 9, 4]); + assert!(source.write_bytes(scope, &[7, 8])); + assert!(source.write_bytes(scope, &[])); + assert_eq!( + eval(scope, "Array.from(new Uint8Array(storage)).join(',')") + .to_rust_string_lossy(scope), + "1,7,8,4,5,6" + ); + if !shared { + let buffer = v8::Local::::try_from(eval(scope, "storage")).unwrap(); + assert_eq!(buffer.detach(None), Some(true)); + assert_eq!(source.byte_length(), 0); + assert!(source.to_vec(scope).is_empty()); + assert!(!source.write_bytes(scope, &[1])); + } + } +} +#[test] +fn borrowed_buffer_sources_reject_forged_proxies_and_resizable_storage() { + moli_v8_test_util::ensure_v8(); + let mut isolate = v8::Isolate::new(Default::default()); + let scope = std::pin::pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + for expression in [ + "{}", + "Object.create(ArrayBuffer.prototype)", + "new Proxy(new Uint8Array(2),{})", + "new ArrayBuffer(2,{maxByteLength:4})", + "new Uint8Array(new ArrayBuffer(2,{maxByteLength:4}))", + "new SharedArrayBuffer(2,{maxByteLength:4})", + "new DataView(new SharedArrayBuffer(2,{maxByteLength:4}))", + ] { + let value = eval(scope, expression); + assert!( + moli_webidl::convert::( + scope, + value, + Context::argument("Test.buffer", 1) + ) + .is_err(), + "{expression}" + ); + } + for view in [false, true] { + let value = eval( + scope, + if view { + "globalThis.resizable=new ArrayBuffer(2,{maxByteLength:4}); new DataView(resizable)" + } else { + "globalThis.resizable=new ArrayBuffer(2,{maxByteLength:4}); resizable" + }, + ); + let buffer = v8::Local::::try_from(eval(scope, "resizable")).unwrap(); + buffer.detach(None).unwrap(); + assert!( + moli_webidl::convert::( + scope, + value, + Context::argument("Test.buffer", 1) + ) + .is_err(), + "detached resizable view={view}" + ); + } +} diff --git a/vendor/v8-152.2.0/src/array_buffer.rs b/vendor/v8-152.2.0/src/array_buffer.rs index ae267ea2e8..23723122f1 100644 --- a/vendor/v8-152.2.0/src/array_buffer.rs +++ b/vendor/v8-152.2.0/src/array_buffer.rs @@ -42,6 +42,9 @@ unsafe extern "C" { fn v8__ArrayBuffer__Data(this: *const ArrayBuffer) -> *mut c_void; fn v8__ArrayBuffer__IsDetachable(this: *const ArrayBuffer) -> bool; fn v8__ArrayBuffer__WasDetached(this: *const ArrayBuffer) -> bool; + fn v8__ArrayBuffer__IsResizableByUserJavaScript( + this: *const ArrayBuffer, + ) -> bool; fn v8__ArrayBuffer__ByteLength(this: *const ArrayBuffer) -> usize; fn v8__ArrayBuffer__GetBackingStore( this: *const ArrayBuffer, @@ -494,6 +497,14 @@ impl ArrayBuffer { unsafe { v8__ArrayBuffer__IsDetachable(self) } } + /// Returns whether this ArrayBuffer is resizable, including after detachment. + /// Unlike the backing store, the ArrayBuffer retains its resizable flag when + /// detached. + #[inline(always)] + pub fn is_resizable_by_user_javascript(&self) -> bool { + unsafe { v8__ArrayBuffer__IsResizableByUserJavaScript(self) } + } + /// Returns true if this ArrayBuffer was detached. #[inline(always)] pub fn was_detached(&self) -> bool { diff --git a/vendor/v8-152.2.0/src/array_buffer_ext.cc b/vendor/v8-152.2.0/src/array_buffer_ext.cc new file mode 100644 index 0000000000..6904012121 --- /dev/null +++ b/vendor/v8-152.2.0/src/array_buffer_ext.cc @@ -0,0 +1,14 @@ +// Copyright 2026 the Moli authors. MIT license. + +#include "support.h" +#include "v8-array-buffer.h" + +using namespace support; + +extern "C" { + +bool v8__ArrayBuffer__IsResizableByUserJavaScript(const v8::ArrayBuffer& self) { + return ptr_to_local(&self)->IsResizableByUserJavaScript(); +} + +} // extern "C"