From 4b8dc0f32062486e07846c2aeb213971e664a1da Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 5 Oct 2026 08:26:35 +0800 Subject: [PATCH] feat(webidl): retain native BufferSource identity through conversion Add borrowed AllowShared BufferSource conversion for algorithms that copy after dictionaries are converted, including native range writes and atomic shared-memory byte access. Reject resizable/growable storage and author proxies using native identity, including detached resizable buffers and views. Tests cover late snapshots, bounded writes, surrounding bytes, detachment and native resizable flags. Individual API migrations are separate. --- moli-webidl/src/buffer_source.rs | 173 ++++++++++++++++++++ moli-webidl/src/lib.rs | 2 + moli-webidl/tests/array_buffer.rs | 54 ++++++ moli-webidl/tests/borrowed_buffer_source.rs | 102 ++++++++++++ vendor/v8-152.2.0/src/array_buffer.rs | 11 ++ vendor/v8-152.2.0/src/array_buffer_ext.cc | 14 ++ 6 files changed, 356 insertions(+) create mode 100644 moli-webidl/src/buffer_source.rs create mode 100644 moli-webidl/tests/array_buffer.rs create mode 100644 moli-webidl/tests/borrowed_buffer_source.rs create mode 100644 vendor/v8-152.2.0/src/array_buffer_ext.cc 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"