From 0f88092566bc7feaea1db8d7fe105d3a337ce38d Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 2 Sep 2026 03:12:55 +0800 Subject: [PATCH] fix(webidl): allocate factory results in relevant realms --- .../src/context_bootstrap/canvas/context2d.rs | 18 +- .../constructors/dom_implementation.rs | 158 +++++++++++------- .../src/context_bootstrap/event_document.rs | 66 ++++---- .../src/context_bootstrap/range/object.rs | 18 ++ .../document/construction/attributes.rs | 23 ++- .../detached_objects/method_forwarders.rs | 9 +- .../document/queries/traversal_factories.rs | 10 +- .../src/native_bridge/element/canvas.rs | 41 +++-- .../src/native_bridge/element/query.rs | 21 ++- .../src/script_vm/tests/dom_xhr/dom.rs | 51 ++++++ .../src/script_vm/tests/dom_xhr/mod.rs | 1 + .../script_vm/tests/dom_xhr/query_realms.rs | 90 ++++++++++ 12 files changed, 381 insertions(+), 125 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/dom_xhr/query_realms.rs diff --git a/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs b/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs index 134c1a1d8f..a1f16ac594 100644 --- a/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs +++ b/moli-renderer-v8/src/context_bootstrap/canvas/context2d.rs @@ -1897,10 +1897,13 @@ pub(crate) fn canvas_context_create_image_data_callback<'s>( rv.set(v8::undefined(scope).into()); return; }; - if let Some(image_data) = build_image_data_object(scope, width, height) { + let relevant_context = canvas_context_relevant_context(scope, args.this()) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + if let Some(image_data) = build_image_data_object(target_scope, width, height) { rv.set(image_data.into()); } else { - rv.set(v8::undefined(scope).into()); + rv.set(v8::undefined(target_scope).into()); } } @@ -2010,10 +2013,13 @@ pub(crate) fn canvas_context_get_image_data_callback<'s>( } else { blank_image_data(width, height) }; - let Some(image_data) = build_image_data_object_with_bytes(scope, width, height, bytes) - .or_else(|| build_image_data_object(scope, width, height)) - else { - rv.set(v8::undefined(scope).into()); + let relevant_context = canvas_context_relevant_context(scope, args.this()) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + let image_data = build_image_data_object_with_bytes(target_scope, width, height, bytes) + .or_else(|| build_image_data_object(target_scope, width, height)); + let Some(image_data) = image_data else { + rv.set(v8::undefined(target_scope).into()); return; }; rv.set(image_data.into()); diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/dom_implementation.rs b/moli-renderer-v8/src/context_bootstrap/constructors/dom_implementation.rs index c65cb71141..d9b92a60ac 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/dom_implementation.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_implementation.rs @@ -106,6 +106,34 @@ fn current_document_object<'s>( .and_then(|value| v8::Local::::try_from(value).ok()) } +fn dom_implementation_owner_document<'s>( + scope: &mut v8::PinScope<'s, '_>, + implementation: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_object( + scope, + implementation, + DOM_IMPLEMENTATION_OWNER_DOCUMENT_SLOT, + ) + .or_else(|| current_document_object(scope)) +} + +fn create_document_type_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + parsed: &DomImplementationCreateDocumentTypeArgs, + owner_document: Option>, +) -> Option> { + let qualified_name = v8_string(scope, &parsed.qualified_name)?; + let public_id = v8_string(scope, &parsed.public_id)?; + let system_id = v8_string(scope, &parsed.system_id)?; + let mut argv = vec![qualified_name.into(), public_id.into(), system_id.into()]; + if let Some(owner_document) = owner_document { + argv.push(owner_document.into()); + } + let (bridge, method) = global_bridge_method(scope, "__createDetachedDocumentType")?; + method.call(scope, bridge.into(), &argv) +} + fn dom_implementation_create_document_type_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -124,30 +152,13 @@ fn dom_implementation_create_document_type_callback<'s>( ); return; } - let Some(qualified_name) = v8_string(scope, &parsed.qualified_name) else { - rv.set_undefined(); - return; - }; - let Some(public_id) = v8_string(scope, &parsed.public_id) else { - rv.set_undefined(); - return; - }; - let Some(system_id) = v8_string(scope, &parsed.system_id) else { - rv.set_undefined(); - return; - }; - let mut argv = vec![qualified_name.into(), public_id.into(), system_id.into()]; - if let Some(owner_document) = - get_private_object(scope, args.this(), DOM_IMPLEMENTATION_OWNER_DOCUMENT_SLOT) - .or_else(|| current_document_object(scope)) - { - argv.push(owner_document.into()); - } - let Some((bridge, method)) = global_bridge_method(scope, "__createDetachedDocumentType") else { - rv.set_undefined(); - return; - }; - match method.call(scope, bridge.into(), &argv) { + let owner_document = dom_implementation_owner_document(scope, args.this()); + let relevant_context = owner_document + .and_then(|document| crate::native_bridge::node_relevant_context(scope, document)) + .or_else(|| args.this().get_creation_context(scope)) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + match create_document_type_value(target_scope, &parsed, owner_document) { Some(value) => rv.set(value), None => rv.set_undefined(), } @@ -168,39 +179,48 @@ fn dom_implementation_create_html_document_callback<'s>( else { return; }; - let Some((bridge, method)) = global_bridge_method(scope, "__createDetachedHTMLDocument") else { - rv.set_undefined(); - return; - }; - let call_args = match parsed.title { - Some(title) => { - let Some(title) = v8_string(scope, &title) else { - rv.set_undefined(); - return; - }; - vec![title.into()] - } - None => Vec::new(), - }; - match method.call(scope, bridge.into(), &call_args) { + let owner_document = dom_implementation_owner_document(scope, args.this()); + let relevant_context = owner_document + .and_then(|document| crate::native_bridge::node_relevant_context(scope, document)) + .or_else(|| args.this().get_creation_context(scope)) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + match create_html_document_value(target_scope, parsed.title.as_deref()) { Some(value) => rv.set(value), None => rv.set_undefined(), } } +fn create_html_document_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + title: Option<&str>, +) -> Option> { + let (bridge, method) = global_bridge_method(scope, "__createDetachedHTMLDocument")?; + let call_args = match title { + Some(title) => vec![v8_string(scope, title)?.into()], + None => Vec::new(), + }; + method.call(scope, bridge.into(), &call_args) +} + +enum DomImplementationQualifiedName { + Null, + String(String), +} + fn dom_implementation_create_document_qualified_name<'s>( scope: &mut v8::PinScope<'s, '_>, value: v8::Local<'s, v8::Value>, -) -> Option> { +) -> Option { if value.is_null() { - return Some(v8::null(scope).into()); + return Some(DomImplementationQualifiedName::Null); } match webidl::convert::( scope, value, webidl::Context::argument("DOMImplementation.createDocument", 2), ) { - Ok(value) => v8_string(scope, &value.0).map(v8::Local::::from), + Ok(value) => Some(DomImplementationQualifiedName::String(value.0)), Err(error) => { webidl::throw_error(scope, &error); None @@ -208,6 +228,28 @@ fn dom_implementation_create_document_qualified_name<'s>( } } +fn create_document_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + namespace_uri: Option<&str>, + qualified_name: &DomImplementationQualifiedName, + doctype: Option>, +) -> Option> { + let namespace_uri = match namespace_uri { + Some(namespace_uri) => v8_string(scope, namespace_uri)?.into(), + None => v8::null(scope).into(), + }; + let qualified_name = match qualified_name { + DomImplementationQualifiedName::Null => v8::null(scope).into(), + DomImplementationQualifiedName::String(value) => v8_string(scope, value)?.into(), + }; + let doctype = doctype + .map(v8::Local::::from) + .unwrap_or_else(|| v8::null(scope).into()); + let argv = [namespace_uri, qualified_name, doctype]; + let (bridge, method) = global_bridge_method(scope, "__createDetachedXmlDocument")?; + method.call(scope, bridge.into(), &argv) +} + fn dom_implementation_create_document_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -217,32 +259,24 @@ fn dom_implementation_create_document_callback<'s>( else { return; }; - let namespace_uri = match parsed.namespace_uri { - Some(namespace_uri) => { - let Some(namespace_uri) = v8_string(scope, &namespace_uri) else { - rv.set_undefined(); - return; - }; - namespace_uri.into() - } - None => v8::null(scope).into(), - }; let Some(qualified_name) = dom_implementation_create_document_qualified_name(scope, parsed.qualified_name) else { rv.set_undefined(); return; }; - let doctype = parsed - .doctype - .map(v8::Local::::from) - .unwrap_or_else(|| v8::null(scope).into()); - let argv = [namespace_uri, qualified_name, doctype]; - let Some((bridge, method)) = global_bridge_method(scope, "__createDetachedXmlDocument") else { - rv.set_undefined(); - return; - }; - match method.call(scope, bridge.into(), &argv) { + let owner_document = dom_implementation_owner_document(scope, args.this()); + let relevant_context = owner_document + .and_then(|document| crate::native_bridge::node_relevant_context(scope, document)) + .or_else(|| args.this().get_creation_context(scope)) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + match create_document_value( + target_scope, + parsed.namespace_uri.as_deref(), + &qualified_name, + parsed.doctype, + ) { Some(value) => rv.set(value), None => rv.set_undefined(), } diff --git a/moli-renderer-v8/src/context_bootstrap/event_document.rs b/moli-renderer-v8/src/context_bootstrap/event_document.rs index 7ceac7ef46..30f3248185 100644 --- a/moli-renderer-v8/src/context_bootstrap/event_document.rs +++ b/moli-renderer-v8/src/context_bootstrap/event_document.rs @@ -12,7 +12,7 @@ struct DocumentCreateEventArgs { interface: String, } -#[derive(Debug, PartialEq, Eq, strum::EnumString)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, strum::EnumString)] #[strum(serialize_all = "lowercase", ascii_case_insensitive)] enum DocumentCreateEventKind { BeforeUnloadEvent, @@ -68,6 +68,31 @@ fn throw_not_supported_dom_exception(scope: &mut v8::PinScope<'_, '_>, message: crate::context_bootstrap::throw_dom_exception_value(scope, message, "NotSupportedError"); } +fn new_uninitialized_document_event<'s>( + scope: &mut v8::PinScope<'s, '_>, + kind: DocumentCreateEventKind, +) -> Option> { + if matches!( + kind, + DocumentCreateEventKind::BeforeUnloadEvent | DocumentCreateEventKind::TextEvent + ) { + return match kind { + DocumentCreateEventKind::BeforeUnloadEvent => { + new_uninitialized_before_unload_event(scope) + } + DocumentCreateEventKind::TextEvent => new_uninitialized_text_event(scope), + _ => unreachable!(), + }; + } + let global = scope.get_current_context().global(scope); + let constructor_value = global.get(scope, v8str(scope, kind.constructor_name()).into())?; + let constructor = v8::Local::::try_from(constructor_value).ok()?; + let empty_type = v8str(scope, ""); + let event = constructor.new_instance(scope, &[empty_type.into()])?; + set_event_initialized(scope, event, false); + Some(event) +} + pub(super) fn document_create_event_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -80,40 +105,11 @@ pub(super) fn document_create_event_callback<'s>( throw_not_supported_dom_exception(scope, "The provided event type is not supported."); return; }; - if matches!( - kind, - DocumentCreateEventKind::BeforeUnloadEvent | DocumentCreateEventKind::TextEvent - ) { - let event = match kind { - DocumentCreateEventKind::BeforeUnloadEvent => { - new_uninitialized_before_unload_event(scope) - } - DocumentCreateEventKind::TextEvent => new_uninitialized_text_event(scope), - _ => unreachable!(), - }; - match event { - Some(event) => rv.set(event.into()), - None => rv.set_undefined(), - } - return; - } - let ctor_name = kind.constructor_name(); - - let global = scope.get_current_context().global(scope); - let Some(constructor_value) = global.get(scope, v8str(scope, ctor_name).into()) else { - rv.set_undefined(); - return; - }; - let Ok(constructor) = v8::Local::::try_from(constructor_value) else { - rv.set_undefined(); - return; - }; - let empty_type = v8str(scope, ""); - match constructor.new_instance(scope, &[empty_type.into()]) { - Some(event) => { - set_event_initialized(scope, event, false); - rv.set(event.into()); - } + let relevant_context = crate::native_bridge::node_relevant_context(scope, args.this()) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + match new_uninitialized_document_event(target_scope, kind) { + Some(event) => rv.set(event.into()), None => rv.set_undefined(), } } diff --git a/moli-renderer-v8/src/context_bootstrap/range/object.rs b/moli-renderer-v8/src/context_bootstrap/range/object.rs index 880442613b..4e0efe95a8 100644 --- a/moli-renderer-v8/src/context_bootstrap/range/object.rs +++ b/moli-renderer-v8/src/context_bootstrap/range/object.rs @@ -21,6 +21,24 @@ pub(in crate::context_bootstrap) fn current_document_object<'s>( pub(in crate::context_bootstrap) fn new_range_for_document<'s>( scope: &mut v8::PinScope<'s, '_>, document: v8::Local<'s, v8::Object>, +) -> Option> { + let relevant_context = crate::native_bridge::node_relevant_context(scope, document)?; + if relevant_context != scope.get_current_context() { + let document = v8::Global::new(scope, document); + let range = { + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + let document = v8::Local::new(target_scope, &document); + let range = new_range_for_document_in_current_context(target_scope, document)?; + v8::Global::new(target_scope, range) + }; + return Some(v8::Local::new(scope, &range)); + } + new_range_for_document_in_current_context(scope, document) +} + +fn new_range_for_document_in_current_context<'s>( + scope: &mut v8::PinScope<'s, '_>, + document: v8::Local<'s, v8::Object>, ) -> Option> { let global = scope.get_current_context().global(scope); let ctor = global.get(scope, v8str(scope, "Range").into())?; diff --git a/moli-renderer-v8/src/native_bridge/document/construction/attributes.rs b/moli-renderer-v8/src/native_bridge/document/construction/attributes.rs index 2d752bc1d4..93abb27b9b 100644 --- a/moli-renderer-v8/src/native_bridge/document/construction/attributes.rs +++ b/moli-renderer-v8/src/native_bridge/document/construction/attributes.rs @@ -35,7 +35,20 @@ pub(in crate::native_bridge) fn node_create_attribute_callback<'s>( } else { parsed.name }; - match new_attr_object(scope, &name, "", None, Some(args.this()), None, None, &name) { + let document = args.this(); + let relevant_context = crate::native_bridge::node_relevant_context(scope, document) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + match new_attr_object( + target_scope, + &name, + "", + None, + Some(document), + None, + None, + &name, + ) { Some(attr) => rv.set(attr.into()), None => rv.set_null(), } @@ -70,12 +83,16 @@ pub(in crate::native_bridge) fn node_create_attribute_ns_callback<'s>( return; } }; + let document = args.this(); + let relevant_context = crate::native_bridge::node_relevant_context(scope, document) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); match new_attr_object( - scope, + target_scope, &parsed.qualified_name, "", None, - Some(args.this()), + Some(document), namespace.as_deref(), prefix.as_deref(), &local_name, diff --git a/moli-renderer-v8/src/native_bridge/document/detached_objects/method_forwarders.rs b/moli-renderer-v8/src/native_bridge/document/detached_objects/method_forwarders.rs index 5f09bcc16c..bdecaf6a29 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_objects/method_forwarders.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_objects/method_forwarders.rs @@ -1016,9 +1016,12 @@ pub(in crate::native_bridge::document) fn detached_document_implementation_gette mut rv: v8::ReturnValue<'_, v8::Value>, ) { let document = args.this(); - let global = scope.get_current_context().global(scope); - let _ = crate::context_bootstrap::ensure_dom_implementation_singleton(scope, global); - match ensure_detached_document_implementation(scope, document) { + let relevant_context = crate::native_bridge::node_relevant_context(scope, document) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + let global = relevant_context.global(target_scope); + let _ = crate::context_bootstrap::ensure_dom_implementation_singleton(target_scope, global); + match ensure_detached_document_implementation(target_scope, document) { Some(implementation) => rv.set(implementation.into()), None => rv.set_null(), } diff --git a/moli-renderer-v8/src/native_bridge/document/queries/traversal_factories.rs b/moli-renderer-v8/src/native_bridge/document/queries/traversal_factories.rs index 6b7117732f..dbfc24cbe9 100644 --- a/moli-renderer-v8/src/native_bridge/document/queries/traversal_factories.rs +++ b/moli-renderer-v8/src/native_bridge/document/queries/traversal_factories.rs @@ -53,9 +53,12 @@ pub(in crate::native_bridge) fn node_create_node_iterator_callback<'s>( rv.set_null(); return; }; + let relevant_context = crate::native_bridge::node_relevant_context(scope, args.this()) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); let runtime = unsafe { &mut *runtime_ptr }; let iterator = traversal::build_node_iterator_wrapper( - scope, + target_scope, runtime_ptr, runtime.native_bridge_mut(), root, @@ -93,9 +96,12 @@ pub(in crate::native_bridge) fn node_create_tree_walker_callback<'s>( rv.set_null(); return; }; + let relevant_context = crate::native_bridge::node_relevant_context(scope, args.this()) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); let runtime = unsafe { &mut *runtime_ptr }; let walker = traversal::build_tree_walker_wrapper( - scope, + target_scope, runtime_ptr, runtime.native_bridge_mut(), root, diff --git a/moli-renderer-v8/src/native_bridge/element/canvas.rs b/moli-renderer-v8/src/native_bridge/element/canvas.rs index 2a9d754084..447b7d3127 100644 --- a/moli-renderer-v8/src/native_bridge/element/canvas.rs +++ b/moli-renderer-v8/src/native_bridge/element/canvas.rs @@ -8,7 +8,9 @@ use crate::{ webidl, }; -use super::super::node::node_runtime_and_handle_from_object_or_detached; +use super::super::node::{ + node_owner_document_relevant_context, node_runtime_and_handle_from_object_or_detached, +}; use super::{element_attribute, set_reflected_attribute}; const CANVAS_CONTEXT_KIND_SLOT: &str = "__moliCanvasContextKind"; @@ -31,6 +33,17 @@ fn canvas_context_slot(kind: CanvasContextKind) -> &'static str { } } +fn build_canvas_context_for_kind<'s>( + scope: &mut v8::PinScope<'s, '_>, + kind: CanvasContextKind, +) -> Option> { + match kind { + CanvasContextKind::TwoD => build_canvas_rendering_context_2d_object(scope).map(Into::into), + CanvasContextKind::WebGl => build_webgl_context_object(scope).map(Into::into), + CanvasContextKind::WebGl2 => build_webgl2_context_object(scope).map(Into::into), + } +} + pub(crate) fn html_canvas_width_getter_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, @@ -179,28 +192,32 @@ pub(crate) fn canvas_get_context_callback<'s>( } } - let Some(value) = (match kind { - CanvasContextKind::TwoD => build_canvas_rendering_context_2d_object(scope).map(Into::into), - CanvasContextKind::WebGl => build_webgl_context_object(scope).map(Into::into), - CanvasContextKind::WebGl2 => build_webgl2_context_object(scope).map(Into::into), - }) else { + let relevant_context = node_runtime_and_handle_from_object_or_detached(scope, canvas) + .ok() + .and_then(|(runtime_ptr, handle)| { + node_owner_document_relevant_context(scope, runtime_ptr, handle) + }) + .or_else(|| canvas.get_creation_context(scope)) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + let Some(value) = build_canvas_context_for_kind(target_scope, kind) else { rv.set_null(); return; }; - let Some(kind_value) = v8_string(scope, kind.label()) else { + let Some(kind_value) = v8_string(target_scope, kind.label()) else { rv.set_null(); return; }; let _ = canvas.define_own_property( - scope, - v8str(scope, CANVAS_CONTEXT_KIND_SLOT).into(), + target_scope, + v8str(target_scope, CANVAS_CONTEXT_KIND_SLOT).into(), kind_value.into(), v8::PropertyAttribute::DONT_ENUM, ); let slot = canvas_context_slot(kind); let _ = canvas.define_own_property( - scope, - v8str(scope, slot).into(), + target_scope, + v8str(target_scope, slot).into(), value, v8::PropertyAttribute::DONT_ENUM, ); @@ -209,7 +226,7 @@ pub(crate) fn canvas_get_context_callback<'s>( CanvasContextKind::TwoD | CanvasContextKind::WebGl | CanvasContextKind::WebGl2 ) && let Ok(context) = v8::Local::::try_from(value) { - attach_canvas_like_context_object(scope, canvas, context); + attach_canvas_like_context_object(target_scope, canvas, context); } rv.set(value); } diff --git a/moli-renderer-v8/src/native_bridge/element/query.rs b/moli-renderer-v8/src/native_bridge/element/query.rs index e6a96c503b..576eff4e58 100644 --- a/moli-renderer-v8/src/native_bridge/element/query.rs +++ b/moli-renderer-v8/src/native_bridge/element/query.rs @@ -1,7 +1,7 @@ use super::super::{ CollectionKind, LiveCollectionQueryKind, collections, encode_tag_name_ns_query, node::{ - node_is_document, node_runtime_and_handle_from_args, + node_is_document, node_owner_document_relevant_context, node_runtime_and_handle_from_args, node_runtime_and_handle_from_args_or_detached, node_runtime_and_handle_from_object_or_detached, receiver_has_detached_state, require_element_method_receiver, require_parent_node_receiver, set_wrapped_node_or_null, @@ -170,7 +170,24 @@ pub(in crate::native_bridge) fn node_query_selector_all_callback<'s>( }; match unsafe { &*runtime_ptr }.query_selector_all(Some(handle), &parsed.selectors) { Ok(handles) => { - let list = collections::build_node_list_from_handles(scope, runtime_ptr, &handles); + // Default-world wrappers are shared across same-origin documents, + // so their creation context need not be their document's realm. + // Isolated-world wrappers instead retain their own world identity. + let receiver_context = args.this().get_creation_context(scope); + let isolated_context = receiver_context.filter(|context| { + let host = unsafe { &*runtime_ptr }; + host.window_execution_context_identity_for_v8_context(scope, *context) + .is_some_and(|identity| { + !host.window_execution_context_identity_is_default_world(identity) + }) + }); + let relevant_context = isolated_context + .or_else(|| node_owner_document_relevant_context(scope, runtime_ptr, handle)) + .or(receiver_context) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + let list = + collections::build_node_list_from_handles(target_scope, runtime_ptr, &handles); rv.set(list.into()); } Err(error) => throw_native_selector_error_for_selector(scope, &parsed.selectors, &error), diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs index f74d4a3b28..05b05774e6 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs @@ -51,6 +51,57 @@ fn child_document_create_text_and_comment_nodes_work() { "function|3|#text|hello|true|function|8|#comment|note|2" ); } + +#[test] +fn child_document_factories_allocate_results_in_document_realm() { + let mut vm = new_storage_test_vm("https://child-document-factory-realm.test/"); + + let result = vm + .eval( + r#" +(() => { + const frame = document.createElement('iframe'); + (document.body || document.documentElement || document).appendChild(frame); + const child = frame.contentWindow; + const doc = child.document; + const ChildObject = child.Object; + child.Object = Object; + const range = doc.createRange(); + const canvas = doc.createElement('canvas'); + const context = canvas.getContext('2d'); + const createdImageData = context.createImageData(1, 1); + const readImageData = context.getImageData(0, 0, 1, 1); + const values = { + nodeList: doc.querySelectorAll('*'), + attribute: doc.createAttribute('data-value'), + namespacedAttribute: doc.createAttributeNS(null, 'data-value'), + event: doc.createEvent('Event'), + range, + clonedRange: range.cloneRange(), + nodeIterator: doc.createNodeIterator(doc), + treeWalker: doc.createTreeWalker(doc), + implementation: doc.implementation, + documentType: doc.implementation.createDocumentType('html', '', ''), + xmlDocument: doc.implementation.createDocument(null, 'root'), + htmlDocument: doc.implementation.createHTMLDocument('title'), + canvasContext: context, + createdImageData, + createdImageDataData: createdImageData.data, + readImageData, + readImageDataData: readImageData.data + }; + return Object.entries(values) + .filter(([, value]) => !(value instanceof ChildObject) || value instanceof Object) + .map(([name]) => name) + .join(','); +})() +"#, + ) + .expect("child document factory realm probe should evaluate"); + + assert_eq!(result, ""); +} + #[test] fn detached_character_data_accessors_parse_webidl_arguments() { let mut vm = new_storage_test_vm("https://detached-character-data-webidl.test/"); diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/mod.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/mod.rs index a6ba5e2460..fe5280aed6 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/mod.rs @@ -5,6 +5,7 @@ mod cssom; mod dom; mod forms; mod misc; +mod query_realms; mod shadow_dom; mod style_invalidation; mod xhr; diff --git a/moli-renderer-v8/src/script_vm/tests/dom_xhr/query_realms.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/query_realms.rs new file mode 100644 index 0000000000..d218a1db95 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/query_realms.rs @@ -0,0 +1,90 @@ +use super::*; + +#[test] +fn query_selector_all_preserves_isolated_world_node_identity() { + for universal_access in [false, true] { + let mut vm = new_parsed_test_vm( + "https://query-selector-realms.test/", + "", + ); + let context_id = vm + .create_isolated_world("query-selector-identity", universal_access) + .expect("isolated query realm should be created"); + let result = vm + .eval_in_isolated_context( + context_id, + r#" +(() => { + const assert = (condition, message) => { if (!condition) throw new Error(message); }; + const fragment = document.createDocumentFragment(); + const detached = document.createElement('section'); + const shadow = document.body.appendChild(document.createElement('div')).attachShadow({mode:'open'}); + for (const root of [document.body, fragment, detached, shadow]) { + for (let index = 0; index < 1100; ++index) { + const node = document.createElement('span'); + node.className = index === 0 ? 'hit first' : 'hit'; + root.appendChild(node); + } + } + for (const root of [document, document.body, fragment, detached, shadow]) { + for (const selector of ['.first', '.hit']) { + const first = root.querySelector(selector); + const list = root.querySelectorAll(selector); + assert(list[0] === first, 'querySelector and querySelectorAll must return the same wrapper'); + assert(Object.getPrototypeOf(list) === NodeList.prototype, 'NodeList belongs to the isolated receiver realm'); + assert(list.item(0) === first && [...list][0] === first, 'item and iteration preserve node identity'); + assert(list !== root.querySelectorAll(selector), 'each query creates a fresh static NodeList'); + const length = list.length; + first.remove(); + assert(list.length === length && list[0] === first, 'removed nodes remain in the static result'); + root === document ? document.body.prepend(first) : root.prepend(first); + } + } + return 'ok'; +})() +"#, + ) + .expect("isolated queries must not fall back to the main world"); + assert_eq!(result, "ok", "universal_access={universal_access}"); + } +} + +#[test] +fn query_selector_all_borrowed_methods_keep_the_receiver_realm() { + let mut vm = new_parsed_test_vm( + "https://query-selector-borrowed.test/", + "", + ); + let result = vm + .eval( + r#" +(() => { + const assert = (condition, message) => { if (!condition) throw new Error(message); }; + const frame = document.body.appendChild(document.createElement('iframe')); + const child = frame.contentWindow; + child.document.body.appendChild(child.document.createElement('span')).className = 'hit'; + for (const [owner, caller] of [[window, child], [child, window]]) { + const doc = owner.document; + const fragment = doc.createDocumentFragment(); + fragment.appendChild(doc.createElement('span')).className = 'hit'; + const shadow = doc.body.appendChild(doc.createElement('div')).attachShadow({mode:'open'}); + shadow.appendChild(doc.createElement('span')).className = 'hit'; + for (const [root, method] of [ + [doc, caller.Document.prototype.querySelectorAll], + [doc.body, caller.Element.prototype.querySelectorAll], + [fragment, caller.DocumentFragment.prototype.querySelectorAll], + [shadow, caller.DocumentFragment.prototype.querySelectorAll], + ]) { + const list = method.call(root, '.hit'); + assert(Object.getPrototypeOf(list) === owner.NodeList.prototype, 'result uses receiver realm, not borrowed method realm'); + assert(list[0] === root.querySelector('.hit'), 'borrowed query preserves native node identity'); + assert(list.item(0) === list[0] && [...list][0] === list[0], 'all collection access paths agree'); + } + } + return 'ok'; +})() +"#, + ) + .expect("querySelectorAll must retain the receiver realm across borrowed methods"); + assert_eq!(result, "ok"); +}