diff --git a/moli-renderer-v8/src/blob.rs b/moli-renderer-v8/src/blob.rs index bcaecfa050..85f66fc5fd 100644 --- a/moli-renderer-v8/src/blob.rs +++ b/moli-renderer-v8/src/blob.rs @@ -15,6 +15,7 @@ use super::{ const BLOB_ID_SLOT: &str = "__lmBlobId"; const BLOB_PROTOTYPE_SLOT: &str = "__lmBlobPrototype"; +const BLOB_REALM_PROTOTYPE_SLOT: &str = "__lmBlobRealmPrototype"; fn native_blob_line_ending() -> &'static str { if moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE @@ -225,7 +226,11 @@ pub(super) fn blob_slice_callback<'s>( .map(ToOwned::to_owned) .unwrap_or_default(); - if let Some(blob) = build_blob_object(scope, sliced, mime_type) { + let prototype = blob_realm_prototype_from_object(scope, args.this()) + .or_else(|| current_realm_blob_prototype(scope)); + if let Some(blob) = prototype + .and_then(|prototype| build_blob_object_with_prototype(scope, sliced, mime_type, prototype)) + { rv.set(blob.into()); } else { rv.set(v8::undefined(scope).into()); @@ -244,6 +249,9 @@ pub(super) fn init_blob_object<'s>( BlobInstanceDeclaration::new(v8::BigInt::new_from_u64(scope, blob_id)) .initialize(scope, object) .expect("Blob instance declaration should initialize"); + if let Some(prototype) = current_realm_blob_prototype(scope) { + set_private_value(scope, object, BLOB_REALM_PROTOTYPE_SLOT, prototype.into()); + } track_blob_ref_lifetime(scope, object, move || release_blob_wrapper_ref(blob_id)); blob_id } @@ -252,15 +260,40 @@ pub(super) fn build_blob_object<'s>( scope: &mut v8::PinScope<'s, '_>, bytes: Vec, mime_type: String, +) -> Option> { + let prototype = current_realm_blob_prototype(scope)?; + build_blob_object_with_prototype(scope, bytes, mime_type, prototype) +} + +fn current_realm_blob_prototype<'s>( + scope: &mut v8::PinScope<'s, '_>, ) -> Option> { let global = scope.get_current_context().global(scope); - let prototype = get_private_value(scope, global, BLOB_PROTOTYPE_SLOT).or_else(|| { - let prototype = - crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "Blob").ok()?; - finalize_blob_realm_bindings(scope, prototype); - Some(prototype.into()) - }); - let prototype = prototype?; + get_private_value(scope, global, BLOB_PROTOTYPE_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + .or_else(|| { + let prototype = + crate::context_bootstrap::ensure_intrinsic_interface_prototype(scope, "Blob") + .ok()?; + finalize_blob_realm_bindings(scope, prototype); + Some(prototype) + }) +} + +fn blob_realm_prototype_from_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_value(scope, object, BLOB_REALM_PROTOTYPE_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) +} + +fn build_blob_object_with_prototype<'s>( + scope: &mut v8::PinScope<'s, '_>, + bytes: Vec, + mime_type: String, + prototype: v8::Local<'s, v8::Object>, +) -> Option> { let owner_id = current_resource_owner_id(scope); let partition_id = current_blob_storage_partition_identity(scope); let blob_id = blob_store().create_blob(owner_id, partition_id, bytes, mime_type); @@ -271,7 +304,8 @@ pub(super) fn build_blob_object<'s>( release_blob_wrapper_ref(blob_id); None })?; - let _ = object.set_prototype(scope, prototype); + let _ = object.set_prototype(scope, prototype.into()); + set_private_value(scope, object, BLOB_REALM_PROTOTYPE_SLOT, prototype.into()); track_blob_ref_lifetime(scope, object, move || release_blob_wrapper_ref(blob_id)); Some(object) } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 32ff5652b9..c93029c76d 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -3129,6 +3129,70 @@ fn blob_internal_builders_survive_global_blob_override() { ); } +#[test] +fn blob_slice_uses_receiver_realm_after_method_realm_is_detached() { + let mut vm = new_parsed_test_vm( + "https://blob-slice-receiver-realm.test/", + "", + ); + + vm.eval( + r#" +(() => { + const iframe = document.createElement("iframe"); + iframe.srcdoc = ""; + document.body.appendChild(iframe); + globalThis.__blobSliceRealmFrame = iframe; +})() +"#, + ) + .expect("detached-Realm Blob slice setup should evaluate"); + vm.drain_pending_child_frame_work_for_test(); + + vm.eval( + r#" +(() => { + const iframe = __blobSliceRealmFrame; + const detachedSlice = iframe.contentWindow.Blob.prototype.slice; + iframe.remove(); + + const blobSlice = detachedSlice.call(new Blob(["abcdef"]), 1, 4, "Text/Custom"); + const fileSlice = detachedSlice.call(new File(["uvwxyz"], "sample.txt"), 2, 5); + globalThis.__blobSliceRealmProbe = { + childDetached: iframe.contentWindow === null, + blobIsMainRealmBlob: blobSlice instanceof Blob, + blobPrototypeIsMainRealmBlob: Object.getPrototypeOf(blobSlice) === Blob.prototype, + fileSliceIsMainRealmBlob: fileSlice instanceof Blob, + fileSliceIsFile: fileSlice instanceof File, + fileSlicePrototypeIsMainRealmBlob: Object.getPrototypeOf(fileSlice) === Blob.prototype, + type: blobSlice.type + }; + Promise.all([blobSlice.text(), fileSlice.text()]).then( + ([blobText, fileText]) => { + __blobSliceRealmProbe.blobText = blobText; + __blobSliceRealmProbe.fileText = fileText; + }, + error => { __blobSliceRealmProbe.error = error && error.name; } + ); + return "scheduled"; +})() +"#, + ) + .expect("detached-Realm Blob slice probe should evaluate"); + + vm.eval("0") + .expect("detached-Realm Blob slice promise microtasks should drain"); + + let result = vm + .eval("JSON.stringify(globalThis.__blobSliceRealmProbe)") + .expect("detached-Realm Blob slice result should evaluate"); + + assert_eq!( + result, + r#"{"childDetached":true,"blobIsMainRealmBlob":true,"blobPrototypeIsMainRealmBlob":true,"fileSliceIsMainRealmBlob":true,"fileSliceIsFile":false,"fileSlicePrototypeIsMainRealmBlob":true,"type":"text/custom","blobText":"bcd","fileText":"wxy"}"# + ); +} + #[test] fn blob_stream_is_native_readable_stream_and_response_consumes_bytes() { let mut vm = new_storage_test_vm("https://blob-stream-reader.test/");