diff --git a/moli-renderer-v8/src/context_bootstrap/shared/clone.rs b/moli-renderer-v8/src/context_bootstrap/shared/clone.rs index b2881098ab..613ca21ed1 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared/clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared/clone.rs @@ -301,8 +301,17 @@ pub(crate) fn structured_clone_value_with_options<'s>( value: v8::Local<'s, v8::Value>, options: v8::Local<'s, v8::Value>, ) -> Option> { + let payload = structured_serialize_value_with_options(scope, value, options)?; + structured_deserialize_value(scope, &payload) +} + +pub(crate) fn structured_serialize_value_with_options<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + options: v8::Local<'s, v8::Value>, +) -> Option { let transfers = parse_structured_clone_options_transfer_list(scope, options)?; - let payload = serialize_for_wire_for_runtime_with_transfers( + serialize_for_wire_for_runtime_with_transfers( scope, value, &transfers.array_buffers, @@ -310,8 +319,7 @@ pub(crate) fn structured_clone_value_with_options<'s>( &transfers.readable_streams, &transfers.writable_streams, &transfers.transform_streams, - )?; - structured_deserialize_value(scope, &payload) + ) } pub(crate) fn structured_clone_value_for_storage<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/structured_clone.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/structured_clone.rs index b26b73ae81..15f0f265ae 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/structured_clone.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/structured_clone.rs @@ -5,9 +5,59 @@ pub(in crate::context_bootstrap) fn window_structured_clone_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if let Some(value) = structured_clone_value_with_options(scope, args.get(0), args.get(1)) { - rv.set(value); - } else { + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { rv.set_undefined(); + return; + }; + let receiver = match crate::native_bridge::WindowOperationReceiver::capture_and_authorize( + scope, + args.this(), + unsafe { &*host_ptr }, + ) { + Ok(receiver) => receiver, + Err(crate::native_bridge::WindowOperationReceiverCaptureError::IllegalInvocation) => { + throw_type_error(scope, "Illegal invocation"); + return; + } + Err(crate::native_bridge::WindowOperationReceiverCaptureError::CrossOrigin) => { + crate::native_bridge::throw_cross_origin_location_security_error(scope); + return; + } + }; + + if args.length() < 1 { + throw_type_error( + scope, + &crate::webidl::WebIdlError::missing_required(crate::webidl::Context::argument( + "Window.structuredClone", + 1, + )) + .to_string(), + ); + return; } + + // Web IDL converts `options`, and structured serialization can invoke + // author getters, while the operation function's Realm is still current. + // Only deserialization uses `this`'s relevant Realm. + let Some(payload) = structured_serialize_value_with_options(scope, args.get(0), args.get(1)) + else { + rv.set_undefined(); + return; + }; + let Some(binding) = receiver.resolve_live_binding(unsafe { &*host_ptr }) else { + // Chromium returns undefined when the receiver's Window has already + // been discarded; more importantly, no retired V8 context is entered. + rv.set_undefined(); + return; + }; + let cloned = binding.with_current_scope(scope, host_ptr, |scope, _dispatch_scope| { + structured_deserialize_value(scope, &payload).map(|value| v8::Global::new(scope, value)) + }); + let Some(Some(cloned)) = cloned else { + rv.set_undefined(); + return; + }; + let cloned = v8::Local::new(scope, cloned); + rv.set(cloned); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs index 63feb853e2..acd003a37a 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/structured_clone.rs @@ -1,5 +1,82 @@ use super::*; +#[test] +fn structured_clone_deserializes_in_the_receiver_realm() { + let mut vm = new_storage_test_vm("https://structured-clone-receiver-realm.test/"); + + vm.eval( + r#" + (() => { + const frame = document.createElement("iframe"); + (document.body || document.documentElement || document).appendChild(frame); + globalThis.__structuredCloneRealmFrame = frame; + return "ready"; + })() + "#, + ) + .expect("structuredClone receiver Realm setup should evaluate"); + vm.drain_pending_child_frame_work_for_test(); + + let result = vm + .eval( + r#" + (() => { + const frame = globalThis.__structuredCloneRealmFrame; + const child = frame.contentWindow; + const constructors = ["Object", "Array", "Date", "RegExp"]; + const intoTop = constructors.map(name => { + const clone = child.structuredClone.call(window, new child[name]); + return Object.getPrototypeOf(clone) === window[name].prototype; + }); + const intoChild = constructors.map(name => { + const clone = window.structuredClone.call(child, new window[name]); + return Object.getPrototypeOf(clone) === child[name].prototype; + }); + + const buffer = new child.ArrayBuffer(8); + const transferred = child.structuredClone.call(window, buffer, { + transfer: [buffer] + }); + + const exceptionRealm = callback => { + try { + callback(); + return "missing"; + } catch (error) { + return error instanceof child.TypeError ? "child" : "other"; + } + }; + const illegalInvocation = exceptionRealm(() => + child.structuredClone.call({}, 1)); + const missingArgument = exceptionRealm(() => + child.structuredClone.call(window)); + + frame.remove(); + const detached = child.structuredClone("detached") === undefined; + + return JSON.stringify({ + intoTop, + intoChild, + transfer: [ + buffer.byteLength, + transferred.byteLength, + transferred instanceof window.ArrayBuffer + ], + illegalInvocation, + missingArgument, + detached + }); + })() + "#, + ) + .expect("cross-Realm structuredClone probe should evaluate"); + + assert_eq!( + result, + r#"{"intoTop":[true,true,true,true],"intoChild":[true,true,true,true],"transfer":[0,8,true],"illegalInvocation":"child","missingArgument":"child","detached":true}"#, + ); +} + #[test] fn structured_clone_preserves_webassembly_module() { let mut vm = new_storage_test_vm("https://example.com/wasm-clone");