mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 08:07:38 +00:00
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.
This commit is contained in:
@@ -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<Self, WebIdlError> {
|
||||
let source = if let Ok(buffer) = v8::Local::<v8::ArrayBuffer>::try_from(value) {
|
||||
Self::Buffer(buffer)
|
||||
} else if let Ok(buffer) = v8::Local::<v8::SharedArrayBuffer>::try_from(value) {
|
||||
Self::Shared(buffer)
|
||||
} else if let Ok(view) = v8::Local::<v8::ArrayBufferView>::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<v8::SharedRef<v8::BackingStore>> {
|
||||
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<u8> {
|
||||
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::<AtomicU8>()
|
||||
.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::<AtomicU8>()
|
||||
.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<Self, WebIdlError> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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::<v8::ArrayBuffer>::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::<v8::Local<v8::ArrayBuffer>>(
|
||||
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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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::<AllowSharedBufferSource>(
|
||||
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::<v8::ArrayBuffer>::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::<AllowSharedBufferSource>(
|
||||
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::<v8::ArrayBuffer>::try_from(eval(scope, "resizable")).unwrap();
|
||||
buffer.detach(None).unwrap();
|
||||
assert!(
|
||||
moli_webidl::convert::<AllowSharedBufferSource>(
|
||||
scope,
|
||||
value,
|
||||
Context::argument("Test.buffer", 1)
|
||||
)
|
||||
.is_err(),
|
||||
"detached resizable view={view}"
|
||||
);
|
||||
}
|
||||
}
|
||||
+11
@@ -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 {
|
||||
|
||||
+14
@@ -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"
|
||||
Reference in New Issue
Block a user