diff --git a/moli-protocol-server/src/protocol_server/tests/cdp_dynamic_page/auxiliary_page_identity.rs b/moli-protocol-server/src/protocol_server/tests/cdp_dynamic_page/auxiliary_page_identity.rs index 2deb4959ab..2d07e5e544 100644 --- a/moli-protocol-server/src/protocol_server/tests/cdp_dynamic_page/auxiliary_page_identity.rs +++ b/moli-protocol-server/src/protocol_server/tests/cdp_dynamic_page/auxiliary_page_identity.rs @@ -1170,8 +1170,22 @@ async fn initial_same_origin_popup_commit_preserves_window_and_replaces_document ) .await; wait_for_value(&mut child, "document.readyState", json!("complete")).await; - let observed = evaluate_window_name_probe(&mut opener, 4, - "oldDocument.dispatchEvent(new p.Event('retained'));[p.marker??null,p.serverMarker,p.Array===oldArray,p.document!==oldDocument,oldDocument.URL,oldDocument.getElementById('old').textContent,p.onloadMarker,oldWalker.filter===oldFilter,oldWalker.nextNode().textContent,p.listenerMarker,p.oldDocumentMarker]").await; + // Chromium reuses this initial Window, but shuts down the outgoing + // Document's execution context. Retaining that Document preserves its + // DOM and TreeWalker identity without granting event-dispatch authority. + let observed = evaluate_window_name_probe( + &mut opener, + 4, + r#"(() => { + const event = new p.Event('retained'); + const dispatched = oldDocument.dispatchEvent(event); + return [p.marker??null,p.serverMarker,p.Array===oldArray,p.document!==oldDocument, + oldDocument.URL,oldDocument.getElementById('old').textContent,p.onloadMarker, + oldWalker.filter===oldFilter,oldWalker.nextNode().textContent,p.listenerMarker, + p.oldDocumentMarker??null,dispatched,event.target===null,event.currentTarget===null]; + })()"#, + ) + .await; navigate_dynamic_page_and_wait_for_load(&mut child, 2, &format!("{base}/replacement")).await; let subsequent = evaluate_window_name_probe( &mut opener, @@ -1193,9 +1207,12 @@ async fn initial_same_origin_popup_commit_preserves_window_and_replaces_document true, "initial", 92, - 93 + null, + false, + true, + true ]), - "first same-origin commit must keep the original Window and intrinsics" + "first same-origin commit keeps the Window and intrinsics, while retiring the old Document" ); assert_eq!( subsequent, diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 06c524585e..f90cf20bc0 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -285,8 +285,9 @@ pub(crate) use self::events::{ event_initialized, event_internal_bool_flag, event_is_dispatching, event_is_error_event, event_is_mouse_event, event_private_value, initialize_event_object, initialize_event_wrapper, mark_agent_submit_event, mark_event_trusted, new_event_state, new_event_wrapper, - set_event_composed_path, set_event_internal_flag, set_event_private_value, set_event_trusted, - submit_event_submitter_value, window_event_callback_values, + node_event_callback_values, set_event_composed_path, set_event_internal_flag, + set_event_private_value, set_event_trusted, submit_event_submitter_value, + window_event_callback_values, }; pub(crate) use self::file_api::{ DataTransferStringCallbackTask, DataTransferStringCallbackTaskEffect, diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/document_nodes.rs b/moli-renderer-v8/src/context_bootstrap/constructors/document_nodes.rs index 9ff9e3eb07..a3d9815f2f 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/document_nodes.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/document_nodes.rs @@ -15,9 +15,9 @@ struct CommentConstructorArgs { data: String, } -pub(in crate::context_bootstrap) fn document_constructor_callback( - scope: &mut v8::PinScope<'_, '_>, - args: v8::FunctionCallbackArguments<'_>, +pub(in crate::context_bootstrap) fn document_constructor_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { if !args.is_construct_call() { @@ -27,8 +27,31 @@ pub(in crate::context_bootstrap) fn document_constructor_callback( ); return; } + // The native source is the constructor Realm's Document; newTarget may + // choose a prototype from an entirely different Window. + let context = scope.get_current_context(); + let Some(host_ptr) = crate::util::context_host_ptr_from_context_slot(context) else { + return; + }; + let execution_context = unsafe { &*host_ptr }.current_realm_document_execution_context(scope); match call_global_bridge_method(scope, "__createDetachedDocument", &[]) { - Some(value) => rv.set(value), + Some(value) => { + if let Ok(document) = v8::Local::::try_from(value) + && let Ok((host, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached( + scope, document, + ) + { + unsafe { &mut *host } + .set_inert_document_execution_context(handle, execution_context); + if let Some(prototype) = args.this().get_prototype(scope) + && document.set_prototype(scope, prototype) != Some(true) + { + return; + } + } + rv.set(value); + } None => rv.set_undefined(), } } 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 103852f8cd..bdb14b4a21 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/dom_implementation.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_implementation.rs @@ -180,17 +180,39 @@ fn dom_implementation_create_html_document_callback<'s>( return; }; let owner_document = dom_implementation_owner_document(scope, args.this()); + let execution_context = owner_document.and_then(|document| { + let (host, handle) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, document) + .ok()?; + unsafe { &*host }.document_execution_context_for_node(handle) + }); 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), + Some(value) => { + set_created_document_execution_context(target_scope, value, execution_context); + rv.set(value); + } None => rv.set_undefined(), } } +fn set_created_document_execution_context<'s>( + scope: &mut v8::PinScope<'s, '_>, + value: v8::Local<'s, v8::Value>, + execution_context: Option, +) { + if let Ok(document) = v8::Local::::try_from(value) + && let Ok((host, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, document) + { + unsafe { &mut *host }.set_inert_document_execution_context(handle, execution_context); + } +} + fn create_html_document_value<'s>( scope: &mut v8::PinScope<'s, '_>, title: Option<&str>, @@ -232,6 +254,12 @@ fn dom_implementation_create_document_callback<'s>( return; }; let owner_document = dom_implementation_owner_document(scope, args.this()); + let execution_context = owner_document.and_then(|document| { + let (host, handle) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, document) + .ok()?; + unsafe { &*host }.document_execution_context_for_node(handle) + }); let relevant_context = owner_document .and_then(|document| crate::native_bridge::node_relevant_context(scope, document)) .or_else(|| args.this().get_creation_context(scope)) @@ -243,7 +271,10 @@ fn dom_implementation_create_document_callback<'s>( &parsed.qualified_name, parsed.doctype, ) { - Some(value) => rv.set(value), + Some(value) => { + set_created_document_execution_context(target_scope, value, execution_context); + rv.set(value); + } None => rv.set_undefined(), } } diff --git a/moli-renderer-v8/src/context_bootstrap/events.rs b/moli-renderer-v8/src/context_bootstrap/events.rs index 03c0cf3099..062edc101a 100644 --- a/moli-renderer-v8/src/context_bootstrap/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/events.rs @@ -495,7 +495,8 @@ pub(super) fn track_event_track_getter_function<'s>( pub(crate) use wrappers::{ event_attribute, event_bool_attribute, event_private_value, initialize_event_wrapper, - new_event_state, new_event_wrapper, set_event_private_value, window_event_callback_values, + new_event_state, new_event_wrapper, node_event_callback_values, set_event_private_value, + window_event_callback_values, }; pub(in crate::context_bootstrap) use message::message_event_init_callback; diff --git a/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs b/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs index b609ff17fb..2957a99a0c 100644 --- a/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs +++ b/moli-renderer-v8/src/context_bootstrap/events/wrappers.rs @@ -31,8 +31,8 @@ impl AttributeProjection { } "from" => Self::NavigationEntry, "destination" => Self::NavigationDestination, - // Arbitrary JS payloads (detail, data, reason, info, error, state) - // retain their identity; they are not platform-object projections. + // Arbitrary JS payloads have their own value access semantics; + // they are separate from platform-object world projection. _ => Self::Value, } } @@ -153,11 +153,9 @@ pub(crate) fn new_event_wrapper<'s>( Some(wrapper) } -/// Project another Realm of the same live Window through the existing weak -/// Event cache. The dispatch state stays shared and same-world identity stays -/// intact. Foreign-Window callbacks retain their existing behavior: resolving -/// an isolated world of another target Window requires an alias/world resolver -/// that this path does not yet provide. +/// Use the target Window's member of the listener world. Native dispatch and +/// cancellation remain shared; an existing wrapper in that world keeps its +/// identity, including when its creation Window differs from the target. pub(crate) fn window_event_callback_values<'s>( scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Value>, @@ -175,8 +173,42 @@ pub(crate) fn window_event_callback_values<'s>( // Blink wraps native events in the target Window's Context in the // listener's world, while invoking the callback in its relevant Realm. let context = target.get_creation_context(scope)?; + Some((target.into(), event_in_realm(scope, event, context)?)) +} + +pub(crate) fn node_event_callback_values<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Value>, + event: v8::Local<'s, v8::Object>, + target_context: v8::Local<'s, v8::Context>, + listener_context: v8::Local<'s, v8::Context>, +) -> Option<(v8::Local<'s, v8::Value>, v8::Local<'s, v8::Object>)> { + let context = match crate::native_bridge::window_context_in_wrapper_world( + scope, + target_context, + listener_context, + ) { + Ok(context) => context?, + Err(error) => { + super::super::throw_error( + scope, + &format!("Node event world projection failed: {error}"), + ); + return None; + } + }; + let target = v8::Local::::try_from(receiver).ok()?; + let receiver = super::super::shared_event_targets::target_in_realm(scope, target, context); + Some((receiver.into(), event_in_realm(scope, event, context)?)) +} + +fn event_in_realm<'s>( + scope: &mut v8::PinScope<'s, '_>, + event: v8::Local<'s, v8::Object>, + context: v8::Local<'s, v8::Context>, +) -> Option> { if world_wrappers::belongs_to_world(scope, event, context) { - return Some((target.into(), event)); + return Some(event); } let backing = event_backing(scope, event); let wrapper = if let Some(wrapper) = world_wrappers::get(scope, backing, context) { @@ -185,7 +217,7 @@ pub(crate) fn window_event_callback_values<'s>( let scope = &mut v8::ContextScope::new(scope, context); new_event_wrapper(scope, backing)? }; - Some((target.into(), wrapper)) + Some(wrapper) } fn bind_attribute<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/platform_object_worlds.rs b/moli-renderer-v8/src/context_bootstrap/platform_object_worlds.rs index 6e0c041ff0..c3b0e6b949 100644 --- a/moli-renderer-v8/src/context_bootstrap/platform_object_worlds.rs +++ b/moli-renderer-v8/src/context_bootstrap/platform_object_worlds.rs @@ -36,10 +36,10 @@ pub(super) fn in_realm<'s>( let (host_ptr, handle) = crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, object) .ok()?; - return unsafe { &mut *host_ptr } - .native_bridge_mut() - .wrap_handle(scope, host_ptr, handle) - .map(Into::into); + return crate::native_bridge::document::detached_native_object_for_handle( + scope, host_ptr, handle, + ) + .map(Into::into); } else if web_api_interfaces::Blob::is_instance(scope, object) { let wrapper = crate::blob::world_wrapper(scope, object)?; if web_api_interfaces::File::is_instance(scope, object) { diff --git a/moli-renderer-v8/src/context_bootstrap/shared_event_targets.rs b/moli-renderer-v8/src/context_bootstrap/shared_event_targets.rs index e42ca70dff..855607181a 100644 --- a/moli-renderer-v8/src/context_bootstrap/shared_event_targets.rs +++ b/moli-renderer-v8/src/context_bootstrap/shared_event_targets.rs @@ -63,6 +63,11 @@ pub(super) fn target_in_realm<'s>( if let Some(window) = window_target_in_realm(scope, target, context) { return window; } + if crate::web_api_interfaces::Node::is_instance(scope, target) { + return super::platform_object_worlds::in_realm(scope, target.into(), context) + .and_then(|value| v8::Local::::try_from(value).ok()) + .unwrap_or(target); + } let Some(owner) = get_private_object(scope, target, OWNER) else { return target; }; diff --git a/moli-renderer-v8/src/dom_parser.rs b/moli-renderer-v8/src/dom_parser.rs index 00cf7c3639..8df9bc62f5 100644 --- a/moli-renderer-v8/src/dom_parser.rs +++ b/moli-renderer-v8/src/dom_parser.rs @@ -18,6 +18,7 @@ use super::{ }; pub(crate) const DOM_PARSER_FOREIGN_NODE_SLOT: &str = "__moliDomParserForeignNode"; +const DOM_PARSER_CONSTRUCTION_REALM_SLOT: &str = "__moliDomParserConstructionRealm"; const HTML_NAMESPACE: &str = "http://www.w3.org/1999/xhtml"; const PARSER_ERROR_STYLE: &str = "display: block; white-space: pre; border: 2px solid #c77; padding: 0 1em 0 1em; margin: 1em; background-color: #fdd; color: black"; const PARSER_ERROR_DETAIL_STYLE: &str = "font-family:monospace;font-size:12px"; @@ -118,6 +119,16 @@ pub(super) fn dom_parser_constructor_callback( throw_type_error(scope, "DOMParser constructor must be called with new"); return; } + // The constructor's Window is independent of newTarget.prototype. Keep + // a V8-traced creation-Realm witness, not a native persistent Context root + // or a WindowProxy that can be rebound to another Window on navigation. + let realm = crate::util::new_null_prototype_object(scope); + crate::util::set_private_value( + scope, + args.this(), + DOM_PARSER_CONSTRUCTION_REALM_SLOT, + realm.into(), + ); rv.set(args.this().into()); } @@ -150,11 +161,30 @@ pub(super) fn dom_parser_parse_from_string_callback<'s>( source } }; + // Borrowing parseFromString from another Realm does not change the + // parser's associated Window or the returned Document's wrapper Realm. + let Some(context) = get_private_object(scope, args.this(), DOM_PARSER_CONSTRUCTION_REALM_SLOT) + .and_then(|realm| realm.get_creation_context(scope)) + else { + throw_type_error(scope, "Illegal invocation"); + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return; + }; + let execution_context = unsafe { &*host_ptr }.current_realm_window_for_inert_document(scope); let Some(obj) = parse_detached_document_from_string(scope, &source, parsed.mime.as_mime()) else { rv.set(v8::null(scope).into()); return; }; + if let Ok((runtime_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, obj) + { + unsafe { &mut *runtime_ptr } + .set_inert_document_execution_context(handle, execution_context); + } rv.set(obj.into()); } diff --git a/moli-renderer-v8/src/host/events.rs b/moli-renderer-v8/src/host/events.rs index bc27f6f3ec..0354f6765b 100644 --- a/moli-renderer-v8/src/host/events.rs +++ b/moli-renderer-v8/src/host/events.rs @@ -156,21 +156,29 @@ pub(crate) fn invoke_prepared_event_callback<'s>( ) -> Option> { let receiver = event_target_receiver(scope, host_ptr, target, event); let has_event_argument = arguments.len() == 1 && arguments[0].strict_equals(event.into()); - let (receiver, event) = if matches!( - target, - EventTargetHandle::Window | EventTargetHandle::ChildWindow(_) - ) { + let (receiver, event) = { let context = callback.relevant_context(scope); let scope = &mut v8::ContextScope::new(scope, context); let catch = std::pin::pin!(v8::TryCatch::new(scope)); let scope = &mut catch.init(); - if let Ok(window) = v8::Local::::try_from(receiver) { + // The current native ownerDocument supplies a Node's execution + // context. Its wrapper can have been created in another Window or + // adopted there, and an inert Document need not have a frame. + let node_context = if let EventTargetHandle::Node(node) = target { + Some(unsafe { &*host_ptr }.node_event_execution_context(scope, node)?) + } else { + None + }; + let window = node_context + .map(|context| context.global(scope)) + .or_else(|| v8::Local::::try_from(receiver).ok()); + if let Some(window) = window { match crate::native_bridge::window_event_access(scope, window, context) { crate::native_bridge::WindowEventAccess::Retired => return None, crate::native_bridge::WindowEventAccess::Denied => { let exception = crate::context_bootstrap::new_dom_exception_value( scope, - "Blocked a Window event listener from accessing a cross-origin target.", + "Blocked an event listener from accessing a cross-origin target.", "SecurityError", ); return Some(v8::Global::new(scope, exception)); @@ -179,14 +187,21 @@ pub(crate) fn invoke_prepared_event_callback<'s>( | crate::native_bridge::WindowEventAccess::Unmanaged => {} } } - let projection = - crate::context_bootstrap::window_event_callback_values(scope, receiver, event, context); + let projection = if let Some(node_context) = node_context { + crate::context_bootstrap::node_event_callback_values( + scope, + receiver, + event, + node_context, + context, + ) + } else { + crate::context_bootstrap::window_event_callback_values(scope, receiver, event, context) + }; if let Some(exception) = scope.exception() { return Some(v8::Global::new(scope, exception)); } projection? - } else { - (receiver, event) }; // Window.onerror's five arguments are JS values, not an Event conversion. let event_arguments = [event.into()]; @@ -640,11 +655,12 @@ fn event_listener_error_event_from_report<'s>( #[derive(Clone, Copy, Debug, Default)] pub(crate) struct PublicEventDispatchResult { pub default_prevented: bool, + canceled_before_dispatch: bool, } impl PublicEventDispatchResult { pub(crate) fn allows_default(self) -> bool { - !self.default_prevented + !self.default_prevented && !self.canceled_before_dispatch } pub(crate) fn dispatch_event_return_value(self) -> bool { @@ -1299,6 +1315,16 @@ pub(crate) fn dispatch_public_event_with_original_target<'s, 'i>( event: v8::Local<'s, v8::Object>, ) -> std::result::Result { unsafe { &*host_ptr }.debug_assert_not_in_structural_mutation("public event dispatch"); + if let EventTargetHandle::Node(node) = dispatch_target + && unsafe { &*host_ptr } + .document_execution_context_for_node(node) + .is_none() + { + return Ok(PublicEventDispatchResult { + canceled_before_dispatch: true, + default_prevented: event_default_prevented(scope, event), + }); + } let target_object = event_target_object(scope, host_ptr, dispatch_target)?; let event_type = normalize_public_event_record(scope, event, target_object.into())?; @@ -1690,6 +1716,7 @@ pub(crate) fn dispatch_public_event_with_original_target<'s, 'i>( clear_event_composed_path(scope, event); Ok(PublicEventDispatchResult { default_prevented: event_default_prevented(scope, event), + canceled_before_dispatch: false, }) } diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 4e55c4b150..80130f4ad5 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -382,6 +382,7 @@ impl JsContextHost { text_codecs: TextCodecStore::default(), child_browsing_contexts: IndexMap::new(), frame_owner_store, + inert_document_execution_contexts: None, frame_parser_classic_scripts: FrameParserClassicScriptRunnerStore::default(), frame_parser_deferred_script_order: FrameParserDeferredScriptOrderStore::default(), frame_document_blocking_stylesheets: FrameDocumentBlockingStylesheetStore::default(), diff --git a/moli-renderer-v8/src/native_bridge/context_host/document_execution_context.rs b/moli-renderer-v8/src/native_bridge/context_host/document_execution_context.rs new file mode 100644 index 0000000000..953fb5bf14 --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/document_execution_context.rs @@ -0,0 +1,128 @@ +//! A Document's native Window association is distinct from a Node wrapper's +//! creation Realm and from permission to execute a callback in that Window. + +use super::{ + DomHandle, JsContextHost, OwnerDispatchScope, WindowEnvironmentSettings, + WindowExecutionContextIdentity, WindowExecutionContextOwner, +}; +use std::collections::HashMap; + +/// Allocate the Document provenance ledger only when an inert factory is used; +/// ordinary Hosts retain a single optional pointer, not a resident map header. +#[derive(Default)] +pub(super) struct InertDocumentExecutionContexts { + documents: HashMap>, +} + +/// Scalar provenance only. Live execution remains authorized by the existing +/// Window/Realm registry; this record never keeps a V8 Context or Host alive. +#[derive(Clone, Copy, Debug)] +pub(crate) struct DocumentExecutionContext { + owner: WindowExecutionContextOwner, + dispatch_scope: OwnerDispatchScope, +} + +impl From for DocumentExecutionContext { + fn from(identity: WindowExecutionContextIdentity) -> Self { + Self { + owner: identity.owner(), + dispatch_scope: identity.dispatch_scope(), + } + } +} + +impl JsContextHost { + pub(crate) fn document_execution_context_for_node( + &self, + node: DomHandle, + ) -> Option { + let document = self.dom_host().owner_document_handle(node)?; + if let Some(source) = self + .inert_document_execution_contexts + .as_ref() + .and_then(|sources| sources.documents.get(&document)) + { + // Inert documents retain the association even after the Window + // is destroyed. The original framed Document instead loses its + // execution context when it leaves the current frame membership. + return *source; + } + if self.page_context_resources_closed { + return None; + } + let dispatch_scope = self.owner_dispatch_scope_for_node(document)?; + Some(DocumentExecutionContext { + owner: self.current_window_execution_context_owner(dispatch_scope)?, + dispatch_scope, + }) + } + + pub(crate) fn current_realm_document_execution_context( + &self, + scope: &mut v8::PinScope<'_, '_>, + ) -> Option { + self.current_runtime_window_execution_context_identity(scope) + .map(Into::into) + .or_else(|| { + WindowEnvironmentSettings::for_current_realm(scope).and_then(|settings| { + self.document_execution_context_for_node(settings.document_handle()) + }) + }) + } + + /// DOMParser owns a Window, rather than borrowing the current Document's + /// execution context. A retained parser can still construct inert DOM + /// after that Window's framed Document has shut down. + pub(crate) fn current_realm_window_for_inert_document( + &self, + scope: &mut v8::PinScope<'_, '_>, + ) -> Option { + self.current_runtime_window_execution_context_identity(scope) + .map(Into::into) + .or_else(|| { + WindowEnvironmentSettings::for_current_realm(scope) + .map(|settings| settings.execution_context_identity().into()) + }) + } + + pub(crate) fn set_inert_document_execution_context( + &mut self, + document: DomHandle, + source: Option, + ) { + debug_assert!( + self.dom_host() + .node(document) + .is_some_and(|node| node.is_document()) + ); + self.inert_document_execution_contexts + .get_or_insert_with(Default::default) + .documents + .insert(document, source); + } + + pub(super) fn forget_inert_document_execution_context(&mut self, document: DomHandle) { + if let Some(sources) = self.inert_document_execution_contexts.as_mut() { + sources.documents.remove(&document); + if sources.documents.is_empty() { + self.inert_document_execution_contexts = None; + } + } + } + + pub(crate) fn node_event_execution_context<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + node: DomHandle, + ) -> Option> { + let source = self.document_execution_context_for_node(node)?; + if !self.window_execution_context_owner_is_current(source.owner, source.dispatch_scope) { + return None; + } + let (_, context) = + self.window_execution_context(scope, source.owner, source.dispatch_scope)?; + let identity = self.window_execution_context_identity_for_access_check(context)?; + self.window_execution_context_identity_is_current(identity) + .then_some(context) + } +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 45f5df7828..0f81225472 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -156,6 +156,7 @@ mod security_policy; mod selection_records; mod service_workers; pub(crate) use service_workers::ServiceWorkerWindowOwner; +mod document_execution_context; mod shared_workers; mod signal_bridge; mod storage_events; @@ -199,6 +200,7 @@ pub(crate) use child_frame_snapshots::{ }; use child_frames::ChildBrowsingContextEntry; pub(in crate::native_bridge::context_host) use child_frames::ChildParserClassicScriptCandidate; +pub(crate) use document_execution_context::DocumentExecutionContext; pub(crate) use event_callbacks::{EventCallbackId, PreparedEventCallback}; pub(crate) use host_loads::ChildFrameAttachmentSnapshot; #[cfg(test)] @@ -249,8 +251,8 @@ pub(crate) use window_world::install_isolated_window_world_for_context; pub(crate) use window_world::{ IsolatedWindowWorld, IsolatedWindowWorldId, PrebootstrappedIsolatedWindowContext, PrebootstrappedWindowContexts, WindowEventAccess, install_isolated_window_world_definition, - isolated_window_world_for_context, remember_isolated_window_context, window_event_access, - window_in_wrapper_world, + isolated_window_world_for_context, remember_isolated_window_context, + window_context_in_wrapper_world, window_event_access, window_in_wrapper_world, }; use workers::WorkerConnectionState; pub(crate) use workers::WorkerOwnerScope; @@ -877,6 +879,8 @@ pub(crate) struct JsContextHost { // Key order is browsing-context insertion order and therefore frame-tree sibling order. child_browsing_contexts: IndexMap, frame_owner_store: FrameOwnerStore, + inert_document_execution_contexts: + Option>, frame_parser_classic_scripts: FrameParserClassicScriptRunnerStore, frame_parser_deferred_script_order: FrameParserDeferredScriptOrderStore, frame_document_blocking_stylesheets: FrameDocumentBlockingStylesheetStore, diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index d796d0fa6a..fe1121331f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -2834,6 +2834,10 @@ impl JsContextHost { scope, document, ) { + // This projection now represents a framed Window Document. Its + // lifetime follows the popup owner, rather than its parser's + // former inert-document association with the opener. + self.forget_inert_document_execution_context(document_handle); if self.lightweight_popup_document_handle(popup_id) != Some(document_handle) { self.forget_lightweight_popup_document_handle(popup_id); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs index 5b94a742b4..b33d192fa2 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/security_policy.rs @@ -347,15 +347,15 @@ impl JsContextHost { &self, handle: DomHandle, ) -> Option { - if let Some(popup_id) = self.lightweight_popup_id_for_node_owner_document(handle) { - return Some(OwnerDispatchScope::LightweightPopup(popup_id)); - } let node = self.dom_host().node(handle)?; let owner_document = if node.is_document() { handle } else { node.owner_document()? }; + if let Some(popup_id) = self.lightweight_popup_id_for_document_handle(owner_document) { + return Some(OwnerDispatchScope::LightweightPopup(popup_id)); + } if owner_document == self.document_handle() { return Some(OwnerDispatchScope::Top); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs index 2f641b81d8..357d82a547 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs @@ -132,6 +132,12 @@ impl WindowEnvironmentSettings { self.document } + /// Metadata for native inert Document construction, never execution + /// authority. Callers must independently authorize any callback/task. + pub(crate) fn execution_context_identity(&self) -> WindowExecutionContextIdentity { + self.execution_identity + } + pub(crate) fn api_base_url(&self, host: &JsContextHost) -> Option { let document = host.dom_host().node(self.document)?.as_document()?; // Preserve the existing bootstrap view while an initial empty Document diff --git a/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/documents.rs b/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/documents.rs index 1930be84ea..e7a1391494 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/documents.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_objects/builders/documents.rs @@ -63,11 +63,14 @@ fn create_native_detached_document_handle_with_url( ) -> Option { let runtime_ptr = context_host_ptr_from_global_bridge(scope)?; let runtime = unsafe { &mut *runtime_ptr }; - Some(if kind == "html" { + let source = runtime.current_realm_document_execution_context(scope); + let handle = if kind == "html" { runtime.create_detached_html_document_with_url(url) } else { runtime.create_detached_xml_document_with_url(url) - }) + }; + runtime.set_inert_document_execution_context(handle, source); + Some(handle) } fn detached_document_url(parsed: &DomHost) -> Url { diff --git a/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/clone_adopt.rs b/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/clone_adopt.rs index 063b8b1ade..7027af7fbc 100644 --- a/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/clone_adopt.rs +++ b/moli-renderer-v8/src/native_bridge/document/detached_surface/creation/clone_adopt.rs @@ -47,6 +47,11 @@ fn clone_detached_document_shell<'s>( document: v8::Local<'s, v8::Object>, deep: bool, ) -> Option> { + let (runtime_ptr, source_handle) = + crate::native_bridge::node_runtime_and_handle_from_object_or_detached(scope, document) + .ok()?; + let execution_context = + unsafe { &*runtime_ptr }.document_execution_context_for_node(source_handle)?; let document_kind = detached_state_string(scope, document, "documentKind"); let root_is_html = detached_document_root_local_name(scope, document) .is_some_and(|name| name.eq_ignore_ascii_case("html")); @@ -64,6 +69,9 @@ fn clone_detached_document_shell<'s>( }; let cloned = call_global_bridge_method(scope, helper, &[]) .and_then(|value| v8::Local::::try_from(value).ok())?; + let cloned_handle = detached_native_handle_for_runtime(scope, runtime_ptr, cloned)?; + unsafe { &mut *runtime_ptr } + .set_inert_document_execution_context(cloned_handle, Some(execution_context)); let content_type = detached_document_content_type_value(scope, document)?; set_detached_document_content_type(scope, cloned, &content_type)?; if html_shell { diff --git a/moli-renderer-v8/src/native_bridge/mod.rs b/moli-renderer-v8/src/native_bridge/mod.rs index d0f0aeac55..3533153c7b 100644 --- a/moli-renderer-v8/src/native_bridge/mod.rs +++ b/moli-renderer-v8/src/native_bridge/mod.rs @@ -54,17 +54,18 @@ pub(crate) use collections::{ build_static_handle_node_list_wrapper, install_collection_template_bindings, }; pub(crate) use context_host::{ - DetachedChildBrowsingContextDocumentSnapshot, ImageDecodeRequestId, IsolatedWindowWorld, - RuntimeObservableContextToken, WindowEventAccess, cross_origin_lightweight_popup_id, - current_runtime_observable_context_token, defer_active_lightweight_popup_restore, - enter_active_lightweight_popup_scope, enter_top_level_lightweight_popup_scope, - install_child_window_proxy_access_check_handlers, install_isolated_window_world_definition, - install_runtime_observable_context_token_for_context, is_cross_origin_top_window_proxy, - isolated_window_world_for_context, lightweight_popup_id_from_window, - remember_isolated_window_context, restore_active_lightweight_popup_scope, + DetachedChildBrowsingContextDocumentSnapshot, DocumentExecutionContext, ImageDecodeRequestId, + IsolatedWindowWorld, RuntimeObservableContextToken, WindowEventAccess, + cross_origin_lightweight_popup_id, current_runtime_observable_context_token, + defer_active_lightweight_popup_restore, enter_active_lightweight_popup_scope, + enter_top_level_lightweight_popup_scope, install_child_window_proxy_access_check_handlers, + install_isolated_window_world_definition, install_runtime_observable_context_token_for_context, + is_cross_origin_top_window_proxy, isolated_window_world_for_context, + lightweight_popup_id_from_window, remember_isolated_window_context, + restore_active_lightweight_popup_scope, restore_deferred_active_lightweight_popup_scope_if_present, - throw_cross_origin_location_security_error, throw_cross_origin_type_error, window_event_access, - window_in_wrapper_world, + throw_cross_origin_location_security_error, throw_cross_origin_type_error, + window_context_in_wrapper_world, window_event_access, window_in_wrapper_world, }; #[cfg(test)] pub(crate) use context_host::{IsolatedWindowWorldId, install_isolated_window_world_for_context}; diff --git a/moli-renderer-v8/src/native_bridge/node/tree/mutation_methods.rs b/moli-renderer-v8/src/native_bridge/node/tree/mutation_methods.rs index 66b1fb12de..3cc9723dc4 100644 --- a/moli-renderer-v8/src/native_bridge/node/tree/mutation_methods.rs +++ b/moli-renderer-v8/src/native_bridge/node/tree/mutation_methods.rs @@ -36,9 +36,21 @@ pub(in crate::native_bridge) fn node_clone_node_callback<'s>( } let deep = args.get(0).boolean_value(scope); let runtime = unsafe { &mut *runtime_ptr }; + let is_document = runtime + .dom_host() + .node(handle) + .is_some_and(|node| node.is_document()); + let execution_context = runtime.document_execution_context_for_node(handle); + if is_document && execution_context.is_none() { + rv.set_null(); + return; + } let Some(clone) = runtime.clone_node(scope, runtime_ptr, handle, deep) else { throw_dom_exception(scope, "NotSupportedError", 9, "Not supported"); return; }; + if is_document { + runtime.set_inert_document_execution_context(clone, execution_context); + } set_wrapped_handle_or_null_for_receiver(scope, &mut rv, runtime_ptr, args.this(), Some(clone)); } diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 3210a6bb10..b14ff0109b 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2230,6 +2230,7 @@ mod websocket; mod webtransport_stream_interface_exposure; mod window_execution_context; mod window_getter_consolidation; +mod window_node_events; mod window_node_worlds; mod worklet_interfaces; diff --git a/moli-renderer-v8/src/script_vm/tests/window_node_events.rs b/moli-renderer-v8/src/script_vm/tests/window_node_events.rs new file mode 100644 index 0000000000..dd8dc54a80 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/window_node_events.rs @@ -0,0 +1,322 @@ +use super::window_node_worlds::child_node_world; +use super::*; + +#[tokio::test(flavor = "current_thread")] +async fn isolated_node_events_project_receivers_targets_and_share_dispatch_state() { + for policy in [ + moli_page_types::LayoutPolicy::Mock, + moli_page_types::LayoutPolicy::OnDemand, + moli_page_types::LayoutPolicy::Realtime, + ] { + let mut vm = new_storage_test_vm("https://node-events.test/"); + vm.set_layout_policy(policy); + let child = child_node_world(&mut vm, "node-events").await; + vm.eval( + r#" + globalThis.defaultFacts=[]; + const target=frame.contentDocument.getElementById('childTarget'); + target.defaultOnly='default-node'; + target.addEventListener('node-world',function(e){ + defaultFacts.push([this===target,e===original,e.target===target, + e.currentTarget===target,e.isolatedOnly===undefined]); + });'ready' + "#, + ) + .unwrap(); + vm.eval_in_isolated_context(child,r#" + globalThis.node=document.getElementById('childTarget'); + globalThis.facts=[]; + node.addEventListener('node-world',function(e){ + globalThis.savedEvent=e; + facts.push([this===node,this.defaultOnly===undefined,e.defaultOnly===undefined, + e instanceof CustomEvent,e.target===node,e.currentTarget===node, + e.composedPath()[0]===node,e.composedPath().at(-1)===window, + window.event===e,e.detail==='scalar-payload']); + this.isolatedOnly='isolated-node';e.isolatedOnly='isolated-event';e.preventDefault(); + }); + const listener={handleEvent(e){facts.push([this===listener,e===savedEvent, + e.defaultPrevented,e.currentTarget===node]);}}; + node.addEventListener('node-world',listener);'ready' + "#).unwrap(); + let builds = vm.layout_pass_observability_for_test().1; + assert_eq!(vm.eval(r#" + globalThis.original=new CustomEvent('node-world',{bubbles:true,cancelable:true,detail:'scalar-payload'}); + original.defaultOnly='default-event'; + JSON.stringify([frame.contentDocument.getElementById('childTarget').dispatchEvent(original), + original.defaultPrevented,original.currentTarget===null,original.composedPath().length===0, + original.isolatedOnly===undefined,frame.contentDocument.getElementById('childTarget').isolatedOnly===undefined]) + "#).unwrap(),"[false,true,true,true,true,true]","{policy:?}"); + assert_eq!( + vm.eval("JSON.stringify(defaultFacts)").unwrap(), + "[[true,true,true,true,true]]", + "{policy:?}" + ); + assert_eq!( + vm.eval_in_isolated_context(child, "JSON.stringify(facts)") + .unwrap(), + "[[true,true,true,true,true,true,true,true,true,true],[true,true,true,true]]", + "{policy:?}" + ); + assert_eq!(vm.eval_in_isolated_context(child,"JSON.stringify([savedEvent.currentTarget===null,savedEvent.composedPath().length===0,savedEvent.defaultPrevented])").unwrap(),"[true,true,true]"); + assert_eq!( + vm.layout_pass_observability_for_test().1, + builds, + "projection must not request layout" + ); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn adopted_node_events_use_current_document_window_in_the_listener_world() { + for policy in [ + moli_page_types::LayoutPolicy::Mock, + moli_page_types::LayoutPolicy::OnDemand, + moli_page_types::LayoutPolicy::Realtime, + ] { + let mut vm = new_storage_test_vm("https://adopted-node-events.test/"); + vm.set_layout_policy(policy); + let child = child_node_world(&mut vm, "adopted-node-events").await; + vm.eval_in_isolated_context( + child, + r#" + globalThis.adopted=parent.document.createElement('p'); + document.adoptNode(adopted);document.body.append(adopted); + globalThis.facts=[]; + adopted.addEventListener('adopt-world',function(e){facts.push([ + this===adopted,this instanceof parent.HTMLParagraphElement, + !(this instanceof HTMLParagraphElement),e instanceof CustomEvent, + !(e instanceof parent.CustomEvent),e.target===adopted, + e.currentTarget===adopted,e.target.ownerDocument===document, + e.composedPath()[0]===adopted,e.composedPath().at(-1)===window]);});'ready' + "#, + ) + .unwrap(); + assert_eq!(vm.eval("String(frame.contentDocument.querySelector('p').dispatchEvent(new CustomEvent('adopt-world',{bubbles:true})))").unwrap(),"true"); + assert_eq!( + vm.eval_in_isolated_context(child, "JSON.stringify(facts)") + .unwrap(), + "[[true,true,true,true,true,true,true,true,true,true]]", + "{policy:?}" + ); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn inert_document_factories_preserve_native_execution_context_for_node_events() { + for policy in [ + moli_page_types::LayoutPolicy::Mock, + moli_page_types::LayoutPolicy::OnDemand, + moli_page_types::LayoutPolicy::Realtime, + ] { + let mut vm = new_storage_test_vm("https://inert-node-events.test/"); + vm.set_layout_policy(policy); + let child = child_node_world(&mut vm, "inert-node-events").await; + vm.eval( + r#" + globalThis.injectFacts=[]; + addEventListener('factory-inject',e=>{ + const node=e.relatedTarget; + const event=new CustomEvent('factory-node');event.defaultOnly='default'; + injectFacts.push([node instanceof Node,node.dispatchEvent(event)]); + });'ready' + "#, + ) + .unwrap(); + let facts=vm.eval_in_isolated_context(child,r#"(()=>{ + const childParser=new DOMParser(),parentParser=new parent.DOMParser(); + const childImpl=document.implementation,parentImpl=parent.document.implementation; + const factories=[ + [true,true,()=>childParser.parseFromString('

','text/html')], + [false,false,()=>parentParser.parseFromString('

','text/html')], + [true,true,()=>childImpl.createHTMLDocument('child')], + [false,false,()=>parentImpl.createHTMLDocument('parent')], + [true,true,()=>parentImpl.createHTMLDocument.call(childImpl,'borrowed')], + [true,true,()=>parentParser.parseFromString.call(childParser,'

','text/html')], + [true,true,()=>document.cloneNode(false)], + [true,true,()=>parent.Node.prototype.cloneNode.call(document,false)], + [true,true,()=>new Document()],[false,false,()=>new parent.Document()], + [true,true,()=>Reflect.construct(DOMParser,[],parent.DOMParser).parseFromString('

','text/html')], + [false,false,()=>Reflect.construct(parent.DOMParser,[],DOMParser).parseFromString('

','text/html')], + [false,true,()=>Reflect.construct(Document,[],parent.Document)], + [true,false,()=>Reflect.construct(parent.Document,[],Document)]]; + const facts=[]; + for(const [wrapperChild,eventChild,factory] of factories){ + const doc=factory(),node=doc.createElement('button'); + const row=[doc.defaultView===null,wrapperChild===(doc instanceof Document),wrapperChild!== (doc instanceof parent.Document)]; + node.addEventListener('factory-node',function(e){row.push(this===node,e.target===node, + eventChild===(e instanceof CustomEvent),eventChild!==(e instanceof parent.CustomEvent),e.defaultOnly===undefined);}); + parent.dispatchEvent(new FocusEvent('factory-inject',{relatedTarget:node}));facts.push(row); + } + return JSON.stringify(facts); + })()"#).unwrap(); + assert_eq!( + serde_json::from_str::(&facts).unwrap(), + serde_json::to_value(vec![vec![true; 8]; 14]).unwrap(), + "{policy:?}" + ); + assert_eq!( + vm.eval("JSON.stringify(injectFacts)").unwrap(), + "[[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true],[true,true]]" + ); + } +} + +#[tokio::test(flavor = "current_thread")] +async fn retired_frame_nodes_and_inert_factories_distinguish_execution_context_presence() { + for policy in [ + moli_page_types::LayoutPolicy::Mock, + moli_page_types::LayoutPolicy::OnDemand, + moli_page_types::LayoutPolicy::Realtime, + ] { + let mut vm = new_storage_test_vm("https://retired-node-events.test/"); + vm.set_layout_policy(policy); + let child = child_node_world(&mut vm, "retired-node-events").await; + vm.eval_in_isolated_context( + child, + r#" + parent.savedFactories={parser:new DOMParser(),implementation:document.implementation, + sourceDocument:document,sourceNode:document.createElement('input'), + inert:document.implementation.createHTMLDocument('before'),constructor:Document}; + parent.savedFactories.sourceNode.type='checkbox';'ready' + "#, + ) + .unwrap(); + let parent = vm + .page_isolated_world_contexts + .contexts_with_ids() + .find_map(|(id, world)| (!world.explicit && world.child_handle.is_none()).then_some(id)) + .unwrap(); + vm.eval("frame.srcdoc='replacement'; 'navigate'") + .unwrap(); + run_child_navigation_commit_and_host_load_for_test(&mut vm, "retire Node source Document") + .await; + let facts=vm.eval_in_isolated_context(parent,r#"(()=>{ + const old=savedFactories.sourceNode; + let invalid;try{old.dispatchEvent({});}catch(e){invalid=e.name;} + let nested;document.addEventListener('nested',e=>{try{old.dispatchEvent(e);}catch(error){nested=error.name;}},{once:true}); + document.dispatchEvent(new Event('nested')); + const documents=[savedFactories.sourceDocument,savedFactories.inert, + savedFactories.parser.parseFromString('

','text/html'), + savedFactories.implementation.createHTMLDocument('after'), + savedFactories.inert.implementation.createHTMLDocument('again'), + savedFactories.inert.cloneNode(false),new savedFactories.constructor()]; + const dispatches=documents.map(doc=>{let called=0;const node=doc.createElement('button'); + node.addEventListener('after',()=>called++);const e=new Event('after'); + const result=node.dispatchEvent(e);return [result,called,e.currentTarget===null];}); + const click=old.dispatchEvent(new MouseEvent('click',{bubbles:true})); + return JSON.stringify({invalid,nested,dispatches,click,checked:old.checked, + cloneNull:savedFactories.sourceDocument.cloneNode(false)===null}); + })()"#).unwrap(); + assert_eq!( + serde_json::from_str::(&facts).unwrap(), + serde_json::json!({ + "invalid":"TypeError","nested":"InvalidStateError", + "dispatches":[[false,0,true],[true,0,true],[true,0,true],[false,0,true], + [true,0,true],[true,0,true],[false,0,true]], + "click":false,"checked":false,"cloneNull":true + }), + "{policy:?}" + ); + } +} + +#[test] +fn inert_node_execution_context_survives_creation_realm_and_wrapper_cache_retirement() { + for policy in [ + moli_page_types::LayoutPolicy::Mock, + moli_page_types::LayoutPolicy::OnDemand, + moli_page_types::LayoutPolicy::Realtime, + ] { + let mut vm = new_storage_test_vm("https://inert-node-realm-retirement.test/"); + vm.set_layout_policy(policy); + let isolated = vm + .create_isolated_world("inert-source-realm", false) + .unwrap(); + vm.eval_in_isolated_context(isolated, + "globalThis.node=document.implementation.createHTMLDocument('inert').createElement('button');'ready'").unwrap(); + let context_ptr = &vm + .page_isolated_world_contexts + .context(isolated) + .unwrap() + .context as *const _; + let handle = vm + .with_context_scope_by_ptr_and_checkpoint_for_test(context_ptr, |scope, _| { + let global = scope.get_current_context().global(scope); + let node = global + .get(scope, crate::util::v8str(scope, "node").into()) + .unwrap(); + let node = v8::Local::::try_from(node).unwrap(); + Ok( + crate::native_bridge::node_runtime_and_handle_from_object_or_detached( + scope, node, + )? + .1, + ) + }) + .unwrap(); + vm.destroy_isolated_world_context(isolated); + vm.collect_renderer_document_isolate_garbage().unwrap(); + assert_eq!( + vm._context_host + .borrow() + .native_bridge() + .isolated_world_wrapper_cache_state_for_test(), + None + ); + vm.with_default_context_scope_and_checkpoint_for_test(|scope, host_ptr| { + let node = crate::native_bridge::document::detached_native_object_for_handle( + scope, host_ptr, handle, + ) + .unwrap(); + let global = scope.get_current_context().global(scope); + assert_eq!( + global.set( + scope, + crate::util::v8str(scope, "retainedNode").into(), + node.into() + ), + Some(true) + ); + Ok(()) + }) + .unwrap(); + assert_eq!(vm.eval(r#"(()=>{ + const facts=[];const event=new Event('after-realm'); + retainedNode.addEventListener('after-realm',function(e){facts.push(this===retainedNode,e===event,e.target===retainedNode);}); + return JSON.stringify([retainedNode.dispatchEvent(event),facts]); + })()"#).unwrap(),"[true,[true,true,true]]","the source Window remains live: {policy:?}"); + let host = vm.context_host_weak_for_test(); + let isolate = vm.renderer_document_isolate.clone(); + drop(vm); + for _ in 0..2 { + isolate.with_renderer_document_isolate_mut(|isolate| isolate.low_memory_notification()); + } + assert!( + host.upgrade().is_none(), + "native Document lineage must not root the Host" + ); + } +} + +#[test] +fn popup_document_event_authority_follows_its_window_instead_of_parser_provenance() { + for policy in [ + moli_page_types::LayoutPolicy::Mock, + moli_page_types::LayoutPolicy::OnDemand, + moli_page_types::LayoutPolicy::Realtime, + ] { + let mut vm = new_storage_test_vm("https://popup-node-events.test/"); + vm.set_layout_policy(policy); + let facts=vm.eval(r#"(()=>{ + const popup=window.open('about:blank'); + const oldDocument=popup.document,node=oldDocument.createElement('input');node.type='checkbox'; + let called=0;node.addEventListener('click',()=>called++); + const before=node.dispatchEvent(new MouseEvent('click')); + popup.close(); + const after=node.dispatchEvent(new MouseEvent('click')); + return JSON.stringify([before,after,called,node.checked, + oldDocument.cloneNode(false)===null,popup.closed]); + })()"#).unwrap(); + assert_eq!(facts, "[true,false,1,true,true,true]", "{policy:?}"); + } +} diff --git a/moli-renderer-v8/src/script_vm/tests/window_node_worlds.rs b/moli-renderer-v8/src/script_vm/tests/window_node_worlds.rs index 5e18d4dc56..d9811ecd3c 100644 --- a/moli-renderer-v8/src/script_vm/tests/window_node_worlds.rs +++ b/moli-renderer-v8/src/script_vm/tests/window_node_worlds.rs @@ -1,6 +1,6 @@ use super::*; -async fn child_node_world(vm: &mut ScriptVm, name: &str) -> i64 { +pub(super) async fn child_node_world(vm: &mut ScriptVm, name: &str) -> i64 { vm.eval( "globalThis.frame=document.createElement('iframe');\ frame.srcdoc='child';\ diff --git a/moli-renderer-v8/src/window_host.rs b/moli-renderer-v8/src/window_host.rs index 9e2ae4bbec..fabaff6bb5 100644 --- a/moli-renderer-v8/src/window_host.rs +++ b/moli-renderer-v8/src/window_host.rs @@ -390,6 +390,14 @@ pub(super) fn event_target_dispatch_event_callback<'s>( rv.set_bool(false); return; }; + // Binding validation precedes this test. Reject a retired framed + // Document before click pre-activation, counters or Event mutation. + if let EventTargetHandle::Node(node) = target + && host.document_execution_context_for_node(node).is_none() + { + rv.set_bool(false); + return; + } let click_activation_target = if event_type.as_deref() == Some("click") && event_is_mouse_event(scope, event) && let EventTargetHandle::Node(handle) = target