diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index eda6ac902b..123f251dd1 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -247,7 +247,9 @@ pub(crate) use self::css_stylesheet_runtime::{ sync_css_style_sheet_media_list_from_owner, sync_css_style_sheet_shadow_root_adopted_owner_tracking, }; -pub(crate) use self::dom_rect::build_dom_rect_object; +pub(crate) use self::dom_rect::{ + build_dom_rect_clone_object, build_dom_rect_object, dom_rect_clone_data, +}; pub(crate) use self::events::{ EVENT_DISPATCHING_SLOT, EVENT_PASSIVE_SLOT, EVENT_STOP_IMMEDIATE_PROPAGATION_SLOT, EVENT_STOP_PROPAGATION_SLOT, clear_event_composed_path, event_attribute, event_backing, diff --git a/moli-renderer-v8/src/context_bootstrap/dom_rect.rs b/moli-renderer-v8/src/context_bootstrap/dom_rect.rs index 6fe99b0164..62fce1344c 100644 --- a/moli-renderer-v8/src/context_bootstrap/dom_rect.rs +++ b/moli-renderer-v8/src/context_bootstrap/dom_rect.rs @@ -561,6 +561,37 @@ fn dom_rect_receiver_branded<'s>( web_api_interfaces::DOMRectReadOnly::is_instance(scope, receiver) } +pub(crate) fn dom_rect_clone_data<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option<(bool, [f64; 4])> { + if !dom_rect_receiver_branded(scope, object) { + return None; + } + let mutable = web_api_interfaces::DOMRect::is_instance(scope, object); + Some(( + mutable, + [ + dom_rect_slot(object, scope, DOM_RECT_X_SLOT), + dom_rect_slot(object, scope, DOM_RECT_Y_SLOT), + dom_rect_slot(object, scope, DOM_RECT_WIDTH_SLOT), + dom_rect_slot(object, scope, DOM_RECT_HEIGHT_SLOT), + ], + )) +} + +pub(crate) fn build_dom_rect_clone_object<'s>( + scope: &mut v8::PinScope<'s, '_>, + mutable: bool, + [x, y, width, height]: [f64; 4], +) -> v8::Local<'s, v8::Object> { + if mutable { + build_dom_rect_object(scope, x, y, width, height) + } else { + build_dom_rect_readonly_object(scope, x, y, width, height) + } +} + const DOM_RECT_WRITABLE_ATTRIBUTE_SLOTS: &[&str] = &[ DOM_RECT_X_SLOT, DOM_RECT_Y_SLOT, diff --git a/moli-renderer-v8/src/script_vm/tests/dom_rect_structured_clone.rs b/moli-renderer-v8/src/script_vm/tests/dom_rect_structured_clone.rs new file mode 100644 index 0000000000..227d17a534 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/dom_rect_structured_clone.rs @@ -0,0 +1,56 @@ +use super::*; + +#[test] +fn structured_clone_preserves_native_rectangle_kind_values_and_graph_identity() { + let mut vm = new_storage_test_vm("https://rect-clone.test/"); + assert_eq!(vm.eval(r#" +(() => { + const check = (ok, message) => { if (!ok) throw Error(message); }; + if (!document.documentElement) document.appendChild(document.createElement('html')); + if (!document.body) document.documentElement.appendChild(document.createElement('body')); + const iframe = document.createElement('iframe'); + document.body.appendChild(iframe); + const other = iframe.contentWindow; + for (const realm of [window, other]) { + for (const name of ['DOMRect', 'DOMRectReadOnly']) { + const C = realm[name]; + const prototype = C.prototype; + const values = [-0, NaN, Infinity, -Infinity]; + const rect = new C(...values); + Object.defineProperty(rect, 'x', { enumerable: true, get() { throw Error('public x must not be read'); } }); + Object.defineProperty(rect, 'toJSON', { enumerable: true, get() { throw Error('toJSON must not be read'); } }); + const subclass = new (class extends C {})(1, 2, -3, -4); + realm[name] = function PoisonedConstructor() { throw Error('public constructor must not be called'); }; + try { + const graph = realm.structuredClone({ rect, again: rect, list: [rect], subclass }); + const clone = graph.rect; + check(clone !== rect && graph.again === clone && graph.list[0] === clone, name + ' cloned graph identity'); + check(Object.getPrototypeOf(clone) === prototype, name + ' realm-local native prototype'); + check(Object.keys(clone).length === 0, name + ' ignores expandos'); + for (const [i, key] of ['x', 'y', 'width', 'height'].entries()) + check(Object.is(clone[key], values[i]), name + ' preserves ' + key); + check(Object.getPrototypeOf(graph.subclass) === prototype, name + ' strips author subclass prototype'); + check(graph.subclass.x === 1 && graph.subclass.y === 2 && graph.subclass.width === -3 && graph.subclass.height === -4, name + ' subclass native state'); + if (name === 'DOMRect') { + clone.x = 10; + check(clone.x === 10, name + ' mutable clone'); + } else { + check(Reflect.set(clone, 'x', 10) === false && Object.is(clone.x, -0), name + ' readonly clone'); + } + } finally { realm[name] = C; } + } + } + const source = new other.DOMRect(5, 6, -7, -8); + const cross = structuredClone(source); + check(Object.getPrototypeOf(cross) === DOMRect.prototype && cross.x === 5 && cross.width === -7, 'cross-realm reconstruction'); + let traps = 0; + let error; + try { structuredClone(new Proxy(source, { ownKeys() { traps++; return []; } })); } catch (caught) { error = caught; } + check(error instanceof DOMException && error.name === 'DataCloneError' && traps === 0, 'author Proxy rejection'); + history.replaceState({ rect: new DOMRectReadOnly(-0, 2, -3, 4) }, '', '#rectangle'); + const stored = history.state.rect; + check(Object.getPrototypeOf(stored) === DOMRectReadOnly.prototype && Object.is(stored.x, -0) && stored.width === -3, 'history storage reconstruction'); + return 'passed'; +})() +"#).unwrap(), "passed"); +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 0f78f4f1aa..c36aeb02e0 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2193,3 +2193,4 @@ mod observer_element_arguments; mod media_device_interfaces; mod dom_rect_factory_descriptors; +mod dom_rect_structured_clone; diff --git a/moli-renderer-v8/src/structured_clone.rs b/moli-renderer-v8/src/structured_clone.rs index 66909b86a5..69ef37e5b0 100644 --- a/moli-renderer-v8/src/structured_clone.rs +++ b/moli-renderer-v8/src/structured_clone.rs @@ -11,13 +11,14 @@ use crate::{ CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, FileSystemFileSnapshotClonePayload, FileSystemHandleClonePayload, ImageDataClonePayload, ReadableStreamClonePayload, TransformStreamClonePayload, WritableStreamClonePayload, - attach_file_system_file_snapshot_clone_payload, build_file_object, - build_file_system_handle_from_clone_payload, build_image_data_object_from_clone_payload, - build_readable_stream_clone_shell, build_transform_stream_clone_shell, - build_writable_stream_clone_shell, crypto_key_clone_payload_from_object, - crypto_key_object_from_clone_payload, detach_message_port_owner_for_transfer, - detach_transferred_message_port, dom_exception_clone_fields, - ensure_message_port_wrapper_for_id, file_system_file_snapshot_clone_payload_from_object, + attach_file_system_file_snapshot_clone_payload, build_dom_rect_clone_object, + build_file_object, build_file_system_handle_from_clone_payload, + build_image_data_object_from_clone_payload, build_readable_stream_clone_shell, + build_transform_stream_clone_shell, build_writable_stream_clone_shell, + crypto_key_clone_payload_from_object, crypto_key_object_from_clone_payload, + detach_message_port_owner_for_transfer, detach_transferred_message_port, + dom_exception_clone_fields, dom_rect_clone_data, ensure_message_port_wrapper_for_id, + file_system_file_snapshot_clone_payload_from_object, file_system_handle_clone_payload_from_object, image_data_clone_payload_from_object, initialize_readable_stream_clone_shell, initialize_transform_stream_clone_shell, initialize_writable_stream_clone_shell, message_port_id_from_object, @@ -44,6 +45,9 @@ pub(crate) const HOST_OBJECT_TAG_FILE_SYSTEM_HANDLE: u32 = 7; const HOST_OBJECT_TAG_QUOTA_EXCEEDED_ERROR: u32 = 8; const HOST_OBJECT_TAG_WRITABLE_STREAM: u32 = 9; const HOST_OBJECT_TAG_TRANSFORM_STREAM: u32 = 10; +const HOST_OBJECT_TAG_GEOMETRY: u32 = 11; +const GEOMETRY_KIND_DOM_RECT_READONLY: u32 = 2; +const GEOMETRY_KIND_DOM_RECT: u32 = 3; #[derive(Clone, Debug, Default)] pub(crate) struct V8StructuredClonePayload { @@ -368,6 +372,20 @@ impl v8::ValueSerializerImpl for WireSerializer { return Some(true); } } + Some("DOMRect" | "DOMRectReadOnly") => { + if let Some((mutable, values)) = dom_rect_clone_data(scope, object) { + serializer.write_uint32(HOST_OBJECT_TAG_GEOMETRY); + serializer.write_uint32(if mutable { + GEOMETRY_KIND_DOM_RECT + } else { + GEOMETRY_KIND_DOM_RECT_READONLY + }); + for value in values { + serializer.write_double(value); + } + return Some(true); + } + } Some("ImageData") => { if let Some(payload) = image_data_clone_payload_from_object(scope, object) { write_image_data_payload(serializer, payload); @@ -583,6 +601,25 @@ impl v8::ValueDeserializerImpl for WireDeserializer { } ensure_message_port_wrapper_for_id(scope, port_id) } + HOST_OBJECT_TAG_GEOMETRY => { + let kind = read_u32(deserializer)?; + let mutable = match kind { + GEOMETRY_KIND_DOM_RECT_READONLY => false, + GEOMETRY_KIND_DOM_RECT => true, + _ => { + throw_data_clone_exception(scope, "Unsupported geometry clone kind."); + return None; + } + }; + let mut values = [0.0; 4]; + for value in &mut values { + if !deserializer.read_double(value) { + throw_data_clone_exception(scope, "Invalid DOMRect clone payload."); + return None; + } + } + Some(build_dom_rect_clone_object(scope, mutable, values)) + } HOST_OBJECT_TAG_IMAGE_DATA => read_image_data_payload(scope, deserializer), HOST_OBJECT_TAG_CRYPTO_KEY => { read_crypto_key_payload(scope, deserializer).or_else(|| { diff --git a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs index f0599bb416..8730853878 100644 --- a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs @@ -3028,3 +3028,44 @@ async fn worker_does_not_expose_window_media_device_interfaces() { r#"{"MediaDeviceInfo":false,"InputDeviceInfo":false}"# ); } + +#[tokio::test] +async fn worker_rectangle_clones_use_native_prototypes_and_message_channel_delivery() { + ensure_v8(); + let mut handle = spawn_worker( + r#" + (async () => { + const check = (ok, label) => { if (!ok) throw Error(label); }; + for (const name of ['DOMRect', 'DOMRectReadOnly']) { + const C = self[name]; + const prototype = C.prototype; + const rect = new C(-0, NaN, Infinity, -Infinity); + Object.defineProperty(rect, 'x', {get() { throw Error('must read native state'); }}); + self[name] = () => { throw Error('must not call public constructor'); }; + try { + const clone = structuredClone({rect, again: rect}); + check(clone.rect === clone.again, name + ' graph alias'); + check(Object.getPrototypeOf(clone.rect) === prototype, name + ' intrinsic prototype'); + check(Object.is(clone.rect.x, -0) && Number.isNaN(clone.rect.y) && + clone.rect.width === Infinity && clone.rect.height === -Infinity, name + ' values'); + const channel = new MessageChannel(); + const received = new Promise(resolve => { channel.port2.onmessage = event => resolve(event.data); }); + channel.port1.postMessage({rect, again: rect}); + const message = await received; + check(message.rect === message.again && message.rect !== rect, name + ' message graph'); + check(Object.getPrototypeOf(message.rect) === prototype, name + ' message receiver prototype'); + check(Object.is(message.rect.x, -0) && Number.isNaN(message.rect.y) && + message.rect.width === Infinity && message.rect.height === -Infinity, name + ' message values'); + channel.port1.close(); + channel.port2.close(); + } finally { self[name] = C; } + } + postMessage('passed'); + close(); + })().catch(error => { postMessage(String(error)); close(); }); + "# + .into(), + "https://worker-rectangle.test/clone.js".into(), + ); + assert_eq!(recv_post_json(&mut handle).await, r#""passed""#); +}