diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt
index 2962c2dca6..af40396c1a 100644
--- a/moli-benchmark/wpt-cross-current/passed-cases.txt
+++ b/moli-benchmark/wpt-cross-current/passed-cases.txt
@@ -5213,6 +5213,8 @@ fetch/api/request/url-encoding.html
fetch/api/response/json.any.js?moli-wpt-any=dedicatedworker
fetch/api/response/json.any.js?moli-wpt-any=window
fetch/api/response/multi-globals/url-parsing.html
+fetch/api/response/response-arraybuffer-realm.window.js?moli-wpt-script=window
+fetch/api/response/response-blob-realm.any.js?moli-wpt-any=window
fetch/api/response/response-body-read-task-handling.html
fetch/api/response/response-clone.any.js?moli-wpt-any=dedicatedworker
fetch/api/response/response-clone.any.js?moli-wpt-any=window
diff --git a/moli-core/tests/fixtures/runtime/fetch_body_realm.js b/moli-core/tests/fixtures/runtime/fetch_body_realm.js
new file mode 100644
index 0000000000..01505a8b35
--- /dev/null
+++ b/moli-core/tests/fixtures/runtime/fetch_body_realm.js
@@ -0,0 +1,92 @@
+async function runFetchBodyRealmProbe(scenario, bodyURL, networkBytes) {
+ if (document.readyState !== 'complete') {
+ await new Promise(resolve => addEventListener('load', resolve, {once: true}));
+ }
+ const frame = document.createElement('iframe');
+ const loaded = new Promise(resolve => frame.onload = resolve);
+ frame.srcdoc = '
';
+ document.body.append(frame);
+ await loaded;
+ const errors = [];
+ let checked = 0;
+ const check = (condition, label) => { if (!condition) errors.push(label); };
+ try {
+ for (const direction of ['parent-method', 'child-method']) {
+ const methodRealm = direction === 'parent-method' ? self : frame.contentWindow;
+ const receiverRealm = direction === 'parent-method' ? frame.contentWindow : self;
+ const expectedBufferPrototype = receiverRealm.ArrayBuffer.prototype;
+ const expectedBytesPrototype = receiverRealm.Uint8Array.prototype;
+ for (const type of ['Request', 'Response']) {
+ const sources = scenario === 'network'
+ ? ['network', 'rewrapped-network', 'cloned-network']
+ : ['null', 'buffered', 'receiver-stream', 'method-stream', 'async-stream', 'changed-prototype', 'cloned-stream'];
+ for (const source of sources) {
+ for (const method of ['arrayBuffer', 'bytes']) {
+ const label = [direction, type, source, method].join('/');
+ try {
+ let expected = source === 'null' ? [] : [65, 0, 66, 255];
+ let body = source === 'null' ? null : new receiverRealm.Uint8Array(expected);
+ let object;
+ if (scenario === 'network') {
+ expected = networkBytes;
+ const fetchRealm = source === 'rewrapped-network' ? methodRealm : receiverRealm;
+ object = await fetchRealm.fetch(bodyURL);
+ body = object.body;
+ if (type === 'Request' || source === 'rewrapped-network') object = undefined;
+ } else if (source.endsWith('stream')) {
+ const streamRealm = source === 'receiver-stream' ? receiverRealm : methodRealm;
+ body = new streamRealm.ReadableStream({start(controller) {
+ const send = () => {
+ controller.enqueue(new streamRealm.Uint8Array([9, ...expected, 9]).subarray(1, 5));
+ controller.close();
+ };
+ if (source === 'async-stream') setTimeout(send, 5); else send();
+ }});
+ }
+ if (!object) {
+ object = type === 'Request'
+ ? new receiverRealm.Request(bodyURL, {method: 'POST', body, duplex: 'half'})
+ : new receiverRealm.Response(body);
+ }
+ if (source.startsWith('cloned-')) object = object.clone();
+ if (source === 'changed-prototype') {
+ Object.setPrototypeOf(object, methodRealm[type].prototype);
+ }
+ const result = await methodRealm[type].prototype[method].call(object);
+ const buffer = method === 'bytes' ? result.buffer : result;
+ check(Object.getPrototypeOf(buffer) === expectedBufferPrototype, label + ': buffer realm');
+ if (method === 'bytes') {
+ check(Object.getPrototypeOf(result) === expectedBytesPrototype, label + ': bytes realm');
+ }
+ const bytes = new Uint8Array(buffer);
+ check(bytes.length === expected.length && bytes.every((byte, index) => byte === expected[index]), label + ': contents');
+ check(object.bodyUsed === (source !== 'null'), label + ': bodyUsed');
+ checked++;
+ } catch (error) {
+ errors.push(label + ': ' + String(error));
+ }
+ }
+ }
+ if (scenario !== 'network') {
+ for (const method of ['arrayBuffer', 'bytes']) {
+ const reason = new receiverRealm.Error('stream failure');
+ const body = new receiverRealm.ReadableStream({start(controller) {controller.error(reason)}});
+ const object = type === 'Request'
+ ? new receiverRealm.Request(bodyURL, {method: 'POST', body, duplex: 'half'})
+ : new receiverRealm.Response(body);
+ try {
+ await methodRealm[type].prototype[method].call(object);
+ errors.push(direction + '/' + type + '/' + method + ': error stream fulfilled');
+ } catch (error) {
+ check(error === reason, direction + '/' + type + '/' + method + ': error identity');
+ }
+ checked++;
+ }
+ }
+ }
+ }
+ } finally {
+ frame.remove();
+ }
+ return {errors, checked};
+}
diff --git a/moli-core/tests/web_apis.rs b/moli-core/tests/web_apis.rs
index 508d29855c..113d120f5a 100644
--- a/moli-core/tests/web_apis.rs
+++ b/moli-core/tests/web_apis.rs
@@ -20,6 +20,8 @@ mod callback_cleanup;
mod event_dispatch;
#[path = "web_apis/fetch_body_native.rs"]
mod fetch_body_native;
+#[path = "web_apis/fetch_body_realm.rs"]
+mod fetch_body_realm;
#[path = "web_apis/pipe_disturbed.rs"]
mod pipe_disturbed;
#[path = "web_apis/request_init.rs"]
diff --git a/moli-core/tests/web_apis/fetch_body_realm.rs b/moli-core/tests/web_apis/fetch_body_realm.rs
new file mode 100644
index 0000000000..0d33130bd1
--- /dev/null
+++ b/moli-core/tests/web_apis/fetch_body_realm.rs
@@ -0,0 +1,38 @@
+use super::*;
+
+async fn assert_binary_body_realms(scenario: &str, expected_cases: usize) -> Result<()> {
+ let server = FixtureServer::spawn().await?;
+ let browser = Browser::new(AppConfig::default())?;
+ let expected = b"naive-\xe4\xbd\xa0\xe5\xa5\xbd";
+ let source = format!(
+ "{}\nrunFetchBodyRealmProbe({}, {}, {}).then(finish, error => finish({{error: String(error)}}));",
+ include_str!("../fixtures/runtime/fetch_body_realm.js"),
+ serde_json::to_string(scenario)?,
+ serde_json::to_string(&server.url("/streaming/chunked-html"))?,
+ serde_json::to_string(expected.as_slice())?,
+ );
+ for target in ["window", "child"] {
+ let observed = tokio::time::timeout(
+ Duration::from_secs(20),
+ super::event_dispatch::run_probe(&browser, &server, target, &source),
+ )
+ .await??;
+ assert_eq!(
+ observed,
+ serde_json::json!({"errors": [], "checked": expected_cases}),
+ "{scenario}/{target}",
+ );
+ }
+ server.shutdown().await;
+ Ok(())
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn fetch_binary_body_results_use_receiver_realm() -> Result<()> {
+ assert_binary_body_realms("memory", 64).await
+}
+
+#[tokio::test(flavor = "multi_thread")]
+async fn fetch_binary_network_body_results_use_receiver_realm() -> Result<()> {
+ assert_binary_body_realms("network", 24).await
+}
diff --git a/moli-renderer-v8/src/network_host/body_source.rs b/moli-renderer-v8/src/network_host/body_source.rs
index cd30d1d68f..9cc3950a59 100644
--- a/moli-renderer-v8/src/network_host/body_source.rs
+++ b/moli-renderer-v8/src/network_host/body_source.rs
@@ -240,8 +240,8 @@ impl Drop for NetworkBodySourceState {
enum PendingBodyMaterializationKind {
Text,
Json,
- ArrayBuffer,
- Bytes,
+ ArrayBuffer(v8::Global),
+ Bytes(v8::Global),
Blob { mime_type: String },
FormData { content_type: String },
}
@@ -289,28 +289,41 @@ pub(in crate::network_host) enum NetworkBodyConsumptionKind {
FormData { content_type: String },
}
-impl From for PendingBodyMaterializationKind {
- fn from(kind: NetworkBodyConsumptionKind) -> Self {
+impl PendingBodyMaterializationKind {
+ fn new<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ object: v8::Local<'s, v8::Object>,
+ kind: NetworkBodyConsumptionKind,
+ ) -> Self {
match kind {
NetworkBodyConsumptionKind::Text => Self::Text,
NetworkBodyConsumptionKind::Json => Self::Json,
- NetworkBodyConsumptionKind::ArrayBuffer => Self::ArrayBuffer,
- NetworkBodyConsumptionKind::Bytes => Self::Bytes,
+ NetworkBodyConsumptionKind::ArrayBuffer | NetworkBodyConsumptionKind::Bytes => {
+ // Fetch explicitly allocates binary results in the Body
+ // receiver's relevant realm, even for a borrowed method or a
+ // stream created in another realm. Retain it until completion.
+ let realm = object
+ .get_creation_context(scope)
+ .expect("body owner must have a creation context");
+ let realm = v8::Global::new(scope, realm);
+ if matches!(kind, NetworkBodyConsumptionKind::ArrayBuffer) {
+ Self::ArrayBuffer(realm)
+ } else {
+ Self::Bytes(realm)
+ }
+ }
NetworkBodyConsumptionKind::Blob { mime_type } => Self::Blob { mime_type },
NetworkBodyConsumptionKind::FormData { content_type } => {
Self::FormData { content_type }
}
}
}
-}
-
-impl PendingBodyMaterializationKind {
fn clone_for_ready(&self) -> Self {
match self {
Self::Text => Self::Text,
Self::Json => Self::Json,
- Self::ArrayBuffer => Self::ArrayBuffer,
- Self::Bytes => Self::Bytes,
+ Self::ArrayBuffer(realm) => Self::ArrayBuffer(realm.clone()),
+ Self::Bytes(realm) => Self::Bytes(realm.clone()),
Self::Blob { mime_type } => Self::Blob {
mime_type: mime_type.clone(),
},
@@ -1226,6 +1239,7 @@ pub(in crate::network_host) fn consume_filtered_response_internal_body_value_fro
{
return Some(consume_readable_body_stream(
scope,
+ object,
stream,
kind,
Some(chunk_callback),
@@ -1280,7 +1294,7 @@ fn consume_network_body_value_from_source_inner<'s>(
if let Some(chunk_callback) = chunk_callback
&& let Some(stream) = readable_body_stream_from_object(scope, object)
{
- return consume_readable_body_stream(scope, stream, kind, Some(chunk_callback));
+ return consume_readable_body_stream(scope, object, stream, kind, Some(chunk_callback));
}
let Some(id) = registry_body_source_id(scope, source) else {
return (NetworkBodyConsumption::Failed, None);
@@ -1289,7 +1303,7 @@ fn consume_network_body_value_from_source_inner<'s>(
return (NetworkBodyConsumption::Failed, None);
};
let promise = resolver.get_promise(scope);
- let materialization_kind = PendingBodyMaterializationKind::from(kind);
+ let materialization_kind = PendingBodyMaterializationKind::new(scope, object, kind);
let mut ready = None;
let mut rejected = None;
if let Some(host) = context_host_mut(scope) {
@@ -1339,7 +1353,7 @@ fn consume_network_body_value_from_source_inner<'s>(
if explicit_source.is_none()
&& let Some(stream) = readable_body_stream_from_object(scope, object)
{
- return consume_readable_body_stream(scope, stream, kind, chunk_callback);
+ return consume_readable_body_stream(scope, object, stream, kind, chunk_callback);
}
let bytes = match try_network_body_bytes_from_storage(scope, source, true) {
@@ -1350,7 +1364,8 @@ fn consume_network_body_value_from_source_inner<'s>(
Ok(None) => return (NetworkBodyConsumption::Failed, None),
Err(_) => return (NetworkBodyConsumption::Failed, None),
};
- match body_materialization_value(scope, &bytes, PendingBodyMaterializationKind::from(kind)) {
+ let kind = PendingBodyMaterializationKind::new(scope, object, kind);
+ match body_materialization_value(scope, &bytes, kind) {
Ok(value) => (NetworkBodyConsumption::Ready(value), None),
Err(error) => (NetworkBodyConsumption::Rejected(error), None),
}
@@ -1775,17 +1790,11 @@ fn body_materialization_value<'s>(
.unwrap_or_else(|| v8::undefined(&scope).into())
})
}
- PendingBodyMaterializationKind::ArrayBuffer => {
- blob::array_buffer_from_bytes(scope, bytes.to_vec())
- .map(Into::into)
- .ok_or_else(|| v8::undefined(scope).into())
+ PendingBodyMaterializationKind::ArrayBuffer(realm) => {
+ binary_body_materialization_value(scope, bytes, realm, false)
}
- PendingBodyMaterializationKind::Bytes => {
- let byte_len = bytes.len();
- blob::array_buffer_from_bytes(scope, bytes.to_vec())
- .and_then(|buffer| v8::Uint8Array::new(scope, buffer, 0, byte_len))
- .map(Into::into)
- .ok_or_else(|| v8::undefined(scope).into())
+ PendingBodyMaterializationKind::Bytes(realm) => {
+ binary_body_materialization_value(scope, bytes, realm, true)
}
PendingBodyMaterializationKind::Blob { mime_type } => {
blob::build_blob_object(scope, bytes.to_vec(), mime_type)
@@ -1821,6 +1830,30 @@ fn body_materialization_value<'s>(
}
}
+fn binary_body_materialization_value<'s>(
+ scope: &mut v8::PinScope<'s, '_>,
+ bytes: &[u8],
+ realm: v8::Global,
+ as_uint8_array: bool,
+) -> Result, v8::Local<'s, v8::Value>> {
+ let realm = v8::Local::new(scope, realm);
+ let value = {
+ let scope = &mut v8::ContextScope::new(scope, realm);
+ blob::array_buffer_from_bytes(scope, bytes.to_vec())
+ .and_then(|buffer| {
+ if as_uint8_array {
+ v8::Uint8Array::new(scope, buffer, 0, bytes.len()).map(Into::into)
+ } else {
+ Some(v8::Local::::from(buffer))
+ }
+ })
+ .map(|value| v8::Global::new(scope, value))
+ };
+ value
+ .map(|value| v8::Local::new(scope, value))
+ .ok_or_else(|| v8::undefined(scope).into())
+}
+
fn registry_body_source_id<'s>(
scope: &mut v8::PinScope<'s, '_>,
source: v8::Local<'s, v8::Object>,
diff --git a/moli-renderer-v8/src/network_host/body_source/stream_consumer.rs b/moli-renderer-v8/src/network_host/body_source/stream_consumer.rs
index 532d9f5fc9..20fb56d4d5 100644
--- a/moli-renderer-v8/src/network_host/body_source/stream_consumer.rs
+++ b/moli-renderer-v8/src/network_host/body_source/stream_consumer.rs
@@ -10,6 +10,7 @@ use crate::context_bootstrap::{
use crate::util::set_null_prototype;
const STREAM: &str = "__moliBodyConsumerStream";
+const BODY_OWNER: &str = "__moliBodyConsumerOwner";
const RESOLVER: &str = "__moliBodyConsumerResolver";
const CHUNKS: &str = "__moliBodyConsumerChunks";
const KIND: &str = "__moliBodyConsumerKind";
@@ -26,6 +27,8 @@ const REASON: &str = "__moliBodyConsumerReason";
#[derive(WebApiObject)]
#[webapi(plain)]
struct BodyConsumerDeclaration<'scope> {
+ #[webapi(slot = BODY_OWNER)]
+ body_owner: v8::Local<'scope, v8::Object>,
#[webapi(slot = STREAM)]
stream: v8::Local<'scope, v8::Object>,
#[webapi(slot = RESOLVER)]
@@ -134,6 +137,7 @@ impl<'s> Consumer<'s> {
pub(super) fn consume_readable_body_stream<'s>(
scope: &mut v8::PinScope<'s, '_>,
+ object: v8::Local<'s, v8::Object>,
stream: v8::Local<'s, v8::Object>,
kind: NetworkBodyConsumptionKind,
chunk_callback: Option>,
@@ -157,9 +161,17 @@ pub(super) fn consume_readable_body_stream<'s>(
let chunks = v8::Array::new(scope, 0);
set_null_prototype(scope, chunks.into());
let consumer = Consumer(
- BodyConsumerDeclaration::new(stream, resolver.into(), chunks, kind, mime, chunk_callback)
- .bind(scope)
- .expect("body consumer declaration must bind"),
+ BodyConsumerDeclaration::new(
+ object,
+ stream,
+ resolver.into(),
+ chunks,
+ kind,
+ mime,
+ chunk_callback,
+ )
+ .bind(scope)
+ .expect("body consumer declaration must bind"),
);
let callbacks = [
v8::Function::builder(chunk_callback_step)
@@ -295,20 +307,24 @@ fn materialize_callback<'s>(
.to_rust_string_lossy(scope)
.as_str()
{
- "text" => PendingBodyMaterializationKind::Text,
- "json" => PendingBodyMaterializationKind::Json,
- "arrayBuffer" => PendingBodyMaterializationKind::ArrayBuffer,
- "bytes" => PendingBodyMaterializationKind::Bytes,
- "blob" => PendingBodyMaterializationKind::Blob { mime_type: mime },
- "formData" => PendingBodyMaterializationKind::FormData { content_type: mime },
+ "text" => NetworkBodyConsumptionKind::Text,
+ "json" => NetworkBodyConsumptionKind::Json,
+ "arrayBuffer" => NetworkBodyConsumptionKind::ArrayBuffer,
+ "bytes" => NetworkBodyConsumptionKind::Bytes,
+ "blob" => NetworkBodyConsumptionKind::Blob { mime_type: mime },
+ "formData" => NetworkBodyConsumptionKind::FormData { content_type: mime },
_ => unreachable!("body materialization kind is private"),
};
+ let object = v8::Local::try_from(consumer.value(scope, BODY_OWNER))
+ .expect("body materialization owner must exist");
+ let kind = PendingBodyMaterializationKind::new(scope, object, kind);
resolve_body_materialization(scope, resolver, bytes, kind);
} else {
let error = consumer.value(scope, REASON);
let _ = resolver.reject(scope, error);
}
for slot in [
+ BODY_OWNER,
STREAM,
RESOLVER,
CHUNKS,