From d144336a3ead90f3cb34b8b0baef540e8907587f Mon Sep 17 00:00:00 2001 From: ldm0 Date: Thu, 15 Oct 2026 09:03:20 +0800 Subject: [PATCH] fix(renderer): honor document contexts when dispatching Node events Project Node receivers, targets and Events into the listener world using the current native ownerDocument, while preserving adopted wrapper identity. Record inert Document Window provenance independently of execution authority; retired framed documents reject dispatch before click preactivation. Preserve constructor Window sources for DOMParser and Document, including borrowed factories, newTarget prototypes and retained factories after navigation. Use popup owner membership after publishing its Document and release inert provenance without retaining native V8 Context roots. Cover all three layout modes, cache/Realm retirement and fourteen factory variants. Align the initial same-origin popup regression with Chromium's retired Document dispatch behavior while preserving all Window and TreeWalker controls. Validation: cargo fmt --all; full all-feature Clippy; cargo nextest run --no-fail-fast (20,537 passed, 17 existing skipped). --- .../auxiliary_page_identity.rs | 25 +- moli-renderer-v8/src/context_bootstrap.rs | 5 +- .../constructors/document_nodes.rs | 31 +- .../constructors/dom_implementation.rs | 35 +- .../src/context_bootstrap/events.rs | 3 +- .../src/context_bootstrap/events/wrappers.rs | 50 ++- .../platform_object_worlds.rs | 8 +- .../context_bootstrap/shared_event_targets.rs | 5 + moli-renderer-v8/src/dom_parser.rs | 30 ++ moli-renderer-v8/src/host/events.rs | 49 ++- .../src/native_bridge/context_host/core.rs | 1 + .../document_execution_context.rs | 128 +++++++ .../src/native_bridge/context_host/mod.rs | 8 +- .../src/native_bridge/context_host/popups.rs | 4 + .../context_host/security_policy.rs | 6 +- .../window_execution_context/settings.rs | 6 + .../detached_objects/builders/documents.rs | 7 +- .../detached_surface/creation/clone_adopt.rs | 8 + moli-renderer-v8/src/native_bridge/mod.rs | 21 +- .../node/tree/mutation_methods.rs | 12 + moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../src/script_vm/tests/window_node_events.rs | 322 ++++++++++++++++++ .../src/script_vm/tests/window_node_worlds.rs | 2 +- moli-renderer-v8/src/window_host.rs | 8 + 24 files changed, 720 insertions(+), 55 deletions(-) create mode 100644 moli-renderer-v8/src/native_bridge/context_host/document_execution_context.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/window_node_events.rs 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