From 729d024c2abaed99a2d9d5ca6873a3ea7bdf649e Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 4 Sep 2026 11:23:47 +0800 Subject: [PATCH] fix(location): track ancestor origins per document --- .../src/context_bootstrap/indexed_db/mod.rs | 9 + .../src/context_bootstrap/location_runtime.rs | 3 +- .../location_runtime/install.rs | 162 ++++++++++++++++- .../location_runtime/slots.rs | 103 ++++++++++- .../context_bootstrap/navigation_bootstrap.rs | 23 ++- .../context_host/child_documents/commit.rs | 6 + .../child_documents/initial_empty.rs | 6 + .../context_host/child_frames.rs | 40 ++++- .../context_host/child_frames/lookup.rs | 3 + .../context_host/child_frames/registry.rs | 16 ++ .../child_frames/request_scope.rs | 15 +- .../context_host/window_security_tokens.rs | 170 +++++++++++++++++- .../extracted/window_and_frame_surfaces.rs | 124 +++++++++++++ 13 files changed, 662 insertions(+), 18 deletions(-) diff --git a/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs b/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs index 3651de691..fde7177a6 100644 --- a/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs +++ b/moli-renderer-v8/src/context_bootstrap/indexed_db/mod.rs @@ -60,6 +60,15 @@ pub(crate) fn flush_blocked_indexed_db_requests(scope: &mut v8::PinScope<'_, '_> flush_drain_blocked_open_requests_task(scope); } +pub(in crate::context_bootstrap) fn new_dom_string_list<'s>( + scope: &mut v8::PinScope<'s, '_>, + values: &[String], +) -> v8::Local<'s, v8::Object> { + super::exposed_interfaces::ensure_intrinsic_interface_constructor(scope, "DOMStringList") + .expect("DOMStringList constructor should materialize before creating an instance"); + new_idb_dom_string_list(scope, values) +} + pub(in crate::context_bootstrap) use self::core::indexed_db_usage_bytes_for_storage_key; pub(crate) use self::core::set_indexed_db_manager_for_context; #[cfg(test)] diff --git a/moli-renderer-v8/src/context_bootstrap/location_runtime.rs b/moli-renderer-v8/src/context_bootstrap/location_runtime.rs index 582b4f2a1..ac414863a 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_runtime.rs @@ -14,7 +14,8 @@ mod surface; pub(super) use install::{ build_location_constructor_template, build_location_runtime_object, - install_location_runtime_state, + install_location_runtime_state, location_belongs_to_current_local_window, + location_owner_has_current_realm, }; pub(super) use navigation::{ is_same_document_fragment_navigation, resolve_location_navigation_target, diff --git a/moli-renderer-v8/src/context_bootstrap/location_runtime/install.rs b/moli-renderer-v8/src/context_bootstrap/location_runtime/install.rs index f26e5416c..742a1d038 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_runtime/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_runtime/install.rs @@ -1,6 +1,6 @@ use super::super::constructors::illegal_constructor_callback; use super::super::navigation_window::{ - navigation_document_has_opaque_origin, runtime_window_owner, + navigation_document_has_opaque_origin, runtime_window_dispatch_scope, runtime_window_owner, }; use super::helpers::{ location_host_string, navigate_modified_location_url, parsed_location_url, @@ -10,9 +10,16 @@ use super::methods::{ location_assign_callback, location_reload_callback, location_replace_callback, location_to_string_callback, }; -use super::slots::{location_href_slot, sync_location_object_fields}; +use super::slots::{ + clear_location_ancestor_origins_slot, location_ancestor_origins_slot, + location_empty_ancestor_origins_slot, location_href_slot, location_relevant_document_id_slot, + location_relevant_local_window_id_slot, set_location_ancestor_origins_slot, + set_location_empty_ancestor_origins_slot, set_location_relevant_document_id_slot, + set_location_relevant_local_window_id_slot, sync_location_object_fields, +}; use super::*; use crate::context_bootstrap::exposed_interfaces::build_intrinsic_interface_instance; +use crate::context_bootstrap::indexed_db::new_dom_string_list; use crate::util::{callback_data_index_value, callback_data_item}; use crate::web_api_interfaces; use anyhow::{Result, anyhow}; @@ -233,9 +240,156 @@ pub(in crate::context_bootstrap) fn install_location_runtime_state<'s>( .initialize(scope, location) .map_err(|error| anyhow!("failed to initialize Location own surface: {error}"))?; } + install_location_ancestor_origins_state(scope, location); Ok(()) } +fn install_location_ancestor_origins_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + location: v8::Local<'s, v8::Object>, +) { + let Some((local_window_id, document_id)) = current_location_owner_ids(scope, location) else { + return; + }; + if location_relevant_local_window_id_slot(scope, location) == Some(local_window_id) + && location_relevant_document_id_slot(scope, location) == Some(document_id) + { + return; + } + // DOMStringList is otherwise a lazy interface. Record the relevant + // Document now, but materialize its list only when script first reads it. + clear_location_ancestor_origins_slot(scope, location); + set_location_relevant_local_window_id_slot(scope, location, local_window_id); + set_location_relevant_document_id_slot(scope, location, document_id); +} + +fn new_location_dom_string_list<'s>( + scope: &mut v8::PinScope<'s, '_>, + location: v8::Local<'s, v8::Object>, + values: &[String], +) -> v8::Local<'s, v8::Object> { + let Some(relevant_context) = location.get_creation_context(scope) else { + return new_dom_string_list(scope, values); + }; + if relevant_context == scope.get_current_context() { + return new_dom_string_list(scope, values); + } + let list = { + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + let list = new_dom_string_list(target_scope, values); + v8::Global::new(target_scope, list) + }; + v8::Local::new(scope, &list) +} + +fn current_location_document_state<'s>( + scope: &mut v8::PinScope<'s, '_>, + location: v8::Local<'s, v8::Object>, +) -> Option<(u64, u64, Vec)> { + let owner = runtime_window_owner(scope, location); + let dispatch_scope = runtime_window_dispatch_scope(scope, owner)?; + let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host = unsafe { &*host_ptr }; + let (local_window_id, document_id) = + location_owner_ids_for_dispatch_scope(host, dispatch_scope)?; + let ancestor_origins = match dispatch_scope { + crate::native_bridge::OwnerDispatchScope::Child(handle) => { + host.child_browsing_context_ancestor_origins(handle)? + } + crate::native_bridge::OwnerDispatchScope::Top + | crate::native_bridge::OwnerDispatchScope::LightweightPopup(_) => Vec::new(), + }; + Some((local_window_id, document_id, ancestor_origins)) +} + +fn current_location_owner_ids<'s>( + scope: &mut v8::PinScope<'s, '_>, + location: v8::Local<'s, v8::Object>, +) -> Option<(u64, u64)> { + let owner = runtime_window_owner(scope, location); + let dispatch_scope = runtime_window_dispatch_scope(scope, owner)?; + let host_ptr = context_host_ptr_from_global_bridge(scope)?; + location_owner_ids_for_dispatch_scope(unsafe { &*host_ptr }, dispatch_scope) +} + +fn location_owner_ids_for_dispatch_scope( + host: &crate::native_bridge::JsContextHost, + dispatch_scope: crate::native_bridge::OwnerDispatchScope, +) -> Option<(u64, u64)> { + match dispatch_scope { + crate::native_bridge::OwnerDispatchScope::Top => host + .current_main_document_task_owner() + .map(|owner| (owner.local_window_id.0, owner.document_id.0)), + crate::native_bridge::OwnerDispatchScope::Child(handle) => host + .current_child_document_task_owner(handle) + .map(|owner| (owner.local_window_id.0, owner.document_id.0)), + crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => Some(( + host.current_lightweight_popup_local_window_id(popup_id)? + .as_u64(), + host.current_lightweight_popup_document_owner(popup_id)? + .document_id() + .as_u64(), + )), + } +} + +pub(in crate::context_bootstrap) fn location_belongs_to_current_local_window<'s>( + scope: &mut v8::PinScope<'s, '_>, + location: v8::Local<'s, v8::Object>, +) -> bool { + location_relevant_local_window_id_slot(scope, location).is_some_and(|local_window_id| { + current_location_owner_ids(scope, location) + .is_some_and(|(current_local_window_id, _)| current_local_window_id == local_window_id) + }) +} + +pub(in crate::context_bootstrap) fn location_owner_has_current_realm<'s>( + scope: &mut v8::PinScope<'s, '_>, + owner: v8::Local<'s, v8::Object>, +) -> bool { + let Some(dispatch_scope) = runtime_window_dispatch_scope(scope, owner) else { + return false; + }; + let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + return false; + }; + unsafe { &*host_ptr } + .current_runtime_window_execution_context_identity_for_dispatch_scope(scope, dispatch_scope) + .is_some() +} + +fn location_ancestor_origins_for_holder<'s>( + scope: &mut v8::PinScope<'s, '_>, + holder: v8::Local<'s, v8::Object>, +) -> v8::Local<'s, v8::Object> { + let relevant_owner = location_relevant_local_window_id_slot(scope, holder) + .zip(location_relevant_document_id_slot(scope, holder)); + if let Some((local_window_id, document_id, ancestor_origins)) = + current_location_document_state(scope, holder) + && relevant_owner == Some((local_window_id, document_id)) + { + if let Some(origins) = location_ancestor_origins_slot(scope, holder) { + return origins; + } + let origins = new_location_dom_string_list(scope, holder, &ancestor_origins); + set_location_ancestor_origins_slot(scope, holder, origins); + // The Location can outlive its Document and realm. Cache its required + // inactive value while that realm can still supply DOMStringList's + // prototype, but only after script has requested ancestorOrigins. + if location_empty_ancestor_origins_slot(scope, holder).is_none() { + let empty = new_location_dom_string_list(scope, holder, &[]); + set_location_empty_ancestor_origins_slot(scope, holder, empty); + } + return origins; + } + if let Some(empty) = location_empty_ancestor_origins_slot(scope, holder) { + return empty; + } + let empty = new_location_dom_string_list(scope, holder, &[]); + set_location_empty_ancestor_origins_slot(scope, holder, empty); + empty +} + fn location_own_surface_installed( scope: &mut v8::PinScope<'_, '_>, location: v8::Local<'_, v8::Object>, @@ -289,7 +443,9 @@ fn location_attribute_getter<'s>( return; }; match attribute { - LocationAttribute::AncestorOrigins => rv.set(v8::Array::new(scope, 0).into()), + LocationAttribute::AncestorOrigins => { + rv.set(location_ancestor_origins_for_holder(scope, holder).into()); + } LocationAttribute::Href => { set_return_string(scope, rv, ¤t_href); } diff --git a/moli-renderer-v8/src/context_bootstrap/location_runtime/slots.rs b/moli-renderer-v8/src/context_bootstrap/location_runtime/slots.rs index 20503dffb..65c212bc8 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_runtime/slots.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_runtime/slots.rs @@ -1,5 +1,106 @@ use super::*; -use crate::util::{get_private_value, set_private_value}; +use crate::util::{get_private_object, get_private_value, set_private_value}; + +const LOCATION_ANCESTOR_ORIGINS_SLOT: &str = "__moliLocationAncestorOrigins"; +const LOCATION_EMPTY_ANCESTOR_ORIGINS_SLOT: &str = "__moliLocationEmptyAncestorOrigins"; +const LOCATION_RELEVANT_DOCUMENT_ID_SLOT: &str = "__moliLocationRelevantDocumentId"; +const LOCATION_RELEVANT_LOCAL_WINDOW_ID_SLOT: &str = "__moliLocationRelevantLocalWindowId"; + +pub(super) fn location_ancestor_origins_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_object(scope, object, LOCATION_ANCESTOR_ORIGINS_SLOT) +} + +pub(super) fn set_location_ancestor_origins_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + value: v8::Local<'s, v8::Object>, +) { + set_private_value(scope, object, LOCATION_ANCESTOR_ORIGINS_SLOT, value.into()); +} + +pub(super) fn clear_location_ancestor_origins_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) { + let undefined = v8::undefined(scope); + set_private_value( + scope, + object, + LOCATION_ANCESTOR_ORIGINS_SLOT, + undefined.into(), + ); +} + +pub(super) fn location_empty_ancestor_origins_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option> { + get_private_object(scope, object, LOCATION_EMPTY_ANCESTOR_ORIGINS_SLOT) +} + +pub(super) fn set_location_empty_ancestor_origins_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + value: v8::Local<'s, v8::Object>, +) { + set_private_value( + scope, + object, + LOCATION_EMPTY_ANCESTOR_ORIGINS_SLOT, + value.into(), + ); +} + +pub(super) fn location_relevant_document_id_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option { + let value = get_private_value(scope, object, LOCATION_RELEVANT_DOCUMENT_ID_SLOT)?; + let value = v8::Local::::try_from(value).ok()?; + let (document_id, lossless) = value.u64_value(); + lossless.then_some(document_id) +} + +pub(super) fn set_location_relevant_document_id_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + document_id: u64, +) { + let value = v8::BigInt::new_from_u64(scope, document_id); + set_private_value( + scope, + object, + LOCATION_RELEVANT_DOCUMENT_ID_SLOT, + value.into(), + ); +} + +pub(super) fn location_relevant_local_window_id_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, +) -> Option { + let value = get_private_value(scope, object, LOCATION_RELEVANT_LOCAL_WINDOW_ID_SLOT)?; + let value = v8::Local::::try_from(value).ok()?; + let (local_window_id, lossless) = value.u64_value(); + lossless.then_some(local_window_id) +} + +pub(super) fn set_location_relevant_local_window_id_slot<'s>( + scope: &mut v8::PinScope<'s, '_>, + object: v8::Local<'s, v8::Object>, + local_window_id: u64, +) { + let value = v8::BigInt::new_from_u64(scope, local_window_id); + set_private_value( + scope, + object, + LOCATION_RELEVANT_LOCAL_WINDOW_ID_SLOT, + value.into(), + ); +} pub(super) fn set_location_href_slot<'s>( scope: &mut v8::PinScope<'s, '_>, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs index 86470b3db..1908fcf18 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_bootstrap.rs @@ -2,6 +2,7 @@ use super::history_runtime::state::{history_window_owner, window_has_shared_hist use super::location_history_storage::WINDOW_RUNTIME_OWNER_SLOT; use super::location_runtime::{ build_location_runtime_object, install_location_runtime_state, + location_belongs_to_current_local_window, location_owner_has_current_realm, sync_window_location_history_navigation_runtime_surface, }; use super::navigation_activation::{ @@ -83,18 +84,24 @@ pub(crate) fn reset_window_location_history_navigation_runtime_state<'s>( if window_has_shared_history(scope, window) { return Ok(()); } - let location = match window_runtime_object(scope, window, WINDOW_LOCATION_SLOT) { - Some(location) => location, - None => { + let existing_location = window_runtime_object(scope, window, WINDOW_LOCATION_SLOT); + let location = match existing_location { + Some(location) if location_belongs_to_current_local_window(scope, location) => { + Some(location) + } + Some(_) | None if location_owner_has_current_realm(scope, window) => { let location = new_location_runtime_object(scope, window, href)?; set_private_value(scope, window, WINDOW_LOCATION_SLOT, location.into()); - location + Some(location) } + Some(_) | None => None, }; - LocationRuntimeObjectDeclaration::new(window, href.to_owned()) - .initialize(scope, location) - .map_err(|error| anyhow::anyhow!("failed to initialize Location object: {error}"))?; - install_location_runtime_state(scope, location, href)?; + if let Some(location) = location { + LocationRuntimeObjectDeclaration::new(window, href.to_owned()) + .initialize(scope, location) + .map_err(|error| anyhow::anyhow!("failed to initialize Location object: {error}"))?; + install_location_runtime_state(scope, location, href)?; + } let initial_seed = initial_navigation_history_seed(scope, window, href); let history = match window_runtime_object(scope, window, WINDOW_HISTORY_SLOT) { diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs index ee356deeb..68ffb79bb 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/commit.rs @@ -317,6 +317,12 @@ impl JsContextHost { expected_current_owner, )?; debug_assert_eq!(owner_transition.retired_owner(), expected_current_owner); + let ancestor_origins_refreshed = + self.refresh_current_child_document_ancestor_origins(handle); + debug_assert!( + ancestor_origins_refreshed, + "committed child Document must capture its ancestor origins" + ); match owner_transition.local_window_owner_transition() { FrameLocalWindowOwnerTransition::Replaced { .. } => { diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs index e73893ab9..eb62deea5 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/initial_empty.rs @@ -125,6 +125,12 @@ impl JsContextHost { let current_owner = owner_transition .current_owner() .expect("initial-empty frame initialization must install an owner"); + let ancestor_origins_initialized = + self.refresh_current_child_document_ancestor_origins(handle); + debug_assert!( + ancestor_origins_initialized, + "initial-empty child Document must capture its ancestor origins" + ); self.register_committed_document_resource_loader( crate::network::context::DocumentFetchContext::new( crate::native_bridge::WindowDocumentOwner::Frame(current_owner), diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs index b78ec81bb..27c9b7dfa 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames.rs @@ -2,7 +2,7 @@ use super::{ ChildBrowsingContextBootstrap, ChildBrowsingContextSnapshot, ChildFrameAttachmentSnapshot, JsContextHost, NavigationActivationSeed, NavigationHistoryDocumentId, NavigationHistoryEntryId, NavigationHistoryEntryKey, NavigationHistoryEntrySeed, NavigationHistorySerializedEntry, - child_documents::CompletedFrameOwnerResourceTiming, + child_documents::CompletedFrameOwnerResourceTiming, window_security_tokens::WindowAccessOrigin, }; use crate::{ document_runtime::{DocumentPolicyContainer, DocumentSandboxPolicy, DomHandle}, @@ -53,6 +53,8 @@ pub(super) struct ChildBrowsingContextEntry { committed_navigation_entry_seed: NavigationHistoryEntrySeed, cached_snapshot: Option, document_policy_container: ChildDocumentPolicyContainer, + document_internal_ancestor_origins: Vec, + ancestor_origins_referrer_policy_snapshot: ChildAncestorOriginsReferrerPolicy, completed_document_network: Option, completed_frame_owner_resource_timing: Option, performance_time_origin: ChildPerformanceTimeOrigin, @@ -73,6 +75,14 @@ struct CompletedChildDocumentNetwork { pub(super) type ChildDocumentPolicyContainer = DocumentPolicyContainer; +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub(in crate::native_bridge::context_host) enum ChildAncestorOriginsReferrerPolicy { + #[default] + Default, + NoReferrer, + SameOrigin, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) struct ChildPerformanceTimeOrigin(u64); @@ -238,6 +248,34 @@ impl ChildBrowsingContextEntry { self.document_policy_container.clone() } + pub(super) fn document_internal_ancestor_origins(&self) -> Vec { + self.document_internal_ancestor_origins.clone() + } + + pub(super) fn set_document_internal_ancestor_origins( + &mut self, + origins: Vec, + ) { + self.document_internal_ancestor_origins = origins; + } + + pub(super) fn clear_document_internal_ancestor_origins(&mut self) { + self.document_internal_ancestor_origins.clear(); + } + + pub(super) fn ancestor_origins_referrer_policy_snapshot( + &self, + ) -> ChildAncestorOriginsReferrerPolicy { + self.ancestor_origins_referrer_policy_snapshot + } + + pub(super) fn set_ancestor_origins_referrer_policy_snapshot( + &mut self, + policy: ChildAncestorOriginsReferrerPolicy, + ) { + self.ancestor_origins_referrer_policy_snapshot = policy; + } + pub(super) fn set_document_permissions_policy( &mut self, policy: crate::permissions_policy::DocumentPermissionsPolicy, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs index 34b2a444c..6a781834d 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs @@ -476,6 +476,8 @@ impl JsContextHost { if !self.child_browsing_contexts.contains_key(&handle) { return None; }; + let ancestor_origins_referrer_policy = + self.child_ancestor_origins_referrer_policy_from_owner_attribute(handle); let permissions_policy = self.child_browsing_context_permissions_policy_for_navigation(handle, &bootstrap); let Some(navigation) = self.replace_child_navigation_load(handle) else { @@ -487,6 +489,7 @@ impl JsContextHost { }; let entry = self.child_browsing_contexts.get_mut(&handle)?; entry.set_document_permissions_policy(permissions_policy); + entry.set_ancestor_origins_referrer_policy_snapshot(ancestor_origins_referrer_policy); entry.set_pending_navigation(bootstrap, reflects_window_state); self.note_child_frame_load_started_for_parent(handle); Some(navigation) diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs index 4e8f7d0b7..b512cbf1f 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/registry.rs @@ -27,6 +27,7 @@ impl JsContextHost { ) -> Option { if let Some(entry) = self.child_browsing_contexts.get_mut(&handle) { entry.clear_current_document_loader_id(); + entry.clear_document_internal_ancestor_origins(); } let transition = self.frame_owner_store.detach_current_child_document(handle); let retired_owner = transition.and_then(|item| item.retired_owner()); @@ -220,6 +221,19 @@ impl JsContextHost { let sandbox_policy_from_owner = super::document_sandbox_policy_from_attribute( self.dom_host().get_attribute(handle, "sandbox").as_deref(), ); + let ancestor_origins_referrer_policy_snapshot = + if is_new || attribute_bootstrap_changed { + self.child_ancestor_origins_referrer_policy_from_owner_attribute(handle) + } else { + existing + .as_ref() + .map(|entry| entry.ancestor_origins_referrer_policy_snapshot()) + .unwrap_or_default() + }; + let document_internal_ancestor_origins = existing + .as_ref() + .map(|entry| entry.document_internal_ancestor_origins()) + .unwrap_or_default(); let document_credentialless = if is_new { self.child_browsing_context_document_credentialless_for_owner( handle, @@ -417,6 +431,8 @@ impl JsContextHost { committed_navigation_entry_seed, cached_snapshot, document_policy_container, + document_internal_ancestor_origins, + ancestor_origins_referrer_policy_snapshot, completed_document_network: existing.as_ref().and_then(|entry| { entry .completed_document_network_for_refresh(attribute_bootstrap_changed) diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs index 44fa48f82..f83b90478 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/request_scope.rs @@ -825,9 +825,20 @@ impl JsContextHost { &self, handle: DomHandle, ) -> crate::document_runtime::DocumentSandboxPolicy { - document_sandbox_policy_from_attribute( + let owner_policy = document_sandbox_policy_from_attribute( self.dom_host().get_attribute(handle, "sandbox").as_deref(), - ) + ); + // A nested browsing context inherits the active sandboxing flags of + // its container Document. Preserve those restrictions when a network + // navigation replaces the initial empty Document. + self.child_browsing_context_parent_handle(handle) + .and_then(|parent| { + self.child_browsing_contexts + .get(&parent) + .map(|entry| entry.document_sandbox_policy()) + }) + .unwrap_or(self.document_policy_container().sandbox) + .with_response_content_security_policy(owner_policy) } pub(crate) fn child_browsing_context_document_credentialless(&self, handle: DomHandle) -> bool { diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs b/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs index 9dee838cc..8fb457e47 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs @@ -1,5 +1,6 @@ use super::{ JsContextHost, OwnerDispatchScope, WindowExecutionContextIdentity, WindowExecutionContextOwner, + child_frames::{ChildAncestorOriginsReferrerPolicy, ChildBrowsingContextEntry}, }; use crate::document_runtime::DomHandle; @@ -7,6 +8,79 @@ const WINDOW_SECURITY_TOKEN_PREFIX: &str = "moli-window-origin-v1:"; const WINDOW_ISOLATED_WORLD_SECURITY_TOKEN_PREFIX: &str = "moli-window-isolated-origin-v1:"; impl JsContextHost { + pub(in crate::native_bridge::context_host) fn child_ancestor_origins_referrer_policy_from_owner_attribute( + &self, + handle: DomHandle, + ) -> ChildAncestorOriginsReferrerPolicy { + match self + .dom_host() + .get_attribute(handle, "referrerpolicy") + .as_deref() + .and_then(crate::referrer_policy::normalize_referrer_policy) + .as_deref() + { + Some("no-referrer") => ChildAncestorOriginsReferrerPolicy::NoReferrer, + Some("same-origin") => ChildAncestorOriginsReferrerPolicy::SameOrigin, + Some(_) | None => ChildAncestorOriginsReferrerPolicy::Default, + } + } + + pub(in crate::native_bridge::context_host) fn refresh_current_child_document_ancestor_origins( + &mut self, + handle: DomHandle, + ) -> bool { + let Some(child_origin) = self.child_window_access_origin(handle) else { + return false; + }; + let Some(parent_scope) = self.owner_dispatch_scope_for_node(handle) else { + return false; + }; + let Some(parent_origin) = self.window_access_origin_for_dispatch_scope(parent_scope) else { + return false; + }; + let parent_ancestor_origins = match parent_scope { + OwnerDispatchScope::Child(parent) => self + .child_browsing_contexts + .get(&parent) + .map(ChildBrowsingContextEntry::document_internal_ancestor_origins) + .unwrap_or_default(), + OwnerDispatchScope::Top | OwnerDispatchScope::LightweightPopup(_) => Vec::new(), + }; + let Some(policy) = self + .child_browsing_contexts + .get(&handle) + .map(ChildBrowsingContextEntry::ancestor_origins_referrer_policy_snapshot) + else { + return false; + }; + let origins = build_child_document_internal_ancestor_origins( + parent_origin, + &parent_ancestor_origins, + &child_origin, + policy, + ); + let Some(entry) = self.child_browsing_contexts.get_mut(&handle) else { + return false; + }; + entry.set_document_internal_ancestor_origins(origins); + true + } + + pub(crate) fn child_browsing_context_ancestor_origins( + &self, + handle: DomHandle, + ) -> Option> { + self.current_child_document_task_owner(handle)?; + Some( + self.child_browsing_contexts + .get(&handle)? + .document_internal_ancestor_origins() + .into_iter() + .map(|origin| origin.serialized_origin()) + .collect(), + ) + } + pub(crate) fn main_default_world_security_token_key(&self) -> Option { let origin = moli_url::origin_ascii_serialization(self.document_url()); if self.document_domain_override.is_some() { @@ -358,6 +432,10 @@ impl WindowAccessOrigin { } } + fn opaque_without_identity() -> Self { + Self::Opaque { identity: None } + } + pub(in crate::native_bridge::context_host) fn from_serialized_origin( serialized_origin: String, document_domain: Option, @@ -412,7 +490,7 @@ impl WindowAccessOrigin { } } - fn has_same_origin(&self, target: &Self) -> bool { + pub(in crate::native_bridge::context_host) fn has_same_origin(&self, target: &Self) -> bool { match (self, target) { ( Self::Opaque { @@ -446,6 +524,36 @@ impl WindowAccessOrigin { } } +fn build_child_document_internal_ancestor_origins( + parent_origin: WindowAccessOrigin, + parent_ancestor_origins: &[WindowAccessOrigin], + child_origin: &WindowAccessOrigin, + policy: ChildAncestorOriginsReferrerPolicy, +) -> Vec { + let mut masked = match policy { + ChildAncestorOriginsReferrerPolicy::NoReferrer => true, + ChildAncestorOriginsReferrerPolicy::SameOrigin => { + !parent_origin.has_same_origin(child_origin) + } + ChildAncestorOriginsReferrerPolicy::Default => false, + }; + let mut origins = Vec::with_capacity(1 + parent_ancestor_origins.len()); + origins.push(if masked { + WindowAccessOrigin::opaque_without_identity() + } else { + parent_origin.clone() + }); + for ancestor_origin in parent_ancestor_origins { + if masked && ancestor_origin.has_same_origin(&parent_origin) { + origins.push(WindowAccessOrigin::opaque_without_identity()); + } else { + masked = false; + origins.push(ancestor_origin.clone()); + } + } + origins +} + pub(crate) fn set_window_security_token( scope: &mut v8::PinScope<'_, '_, ()>, context: v8::Local<'_, v8::Context>, @@ -487,7 +595,9 @@ fn window_isolated_world_security_token_key( #[cfg(test)] mod tests { use super::{ - WindowAccessOrigin, window_isolated_world_security_token_key, window_security_token_key, + ChildAncestorOriginsReferrerPolicy, WindowAccessOrigin, + build_child_document_internal_ancestor_origins, window_isolated_world_security_token_key, + window_security_token_key, }; use crate::{frame_owner_model::LocalWindowId, native_bridge::WindowExecutionContextOwner}; @@ -560,6 +670,62 @@ mod tests { assert!(original_with_domain.can_access(&relaxed_peer)); } + #[test] + fn ancestor_origins_mask_the_same_origin_parent_prefix_only() { + let origin_a = + WindowAccessOrigin::from_serialized_origin("https://a.example.test".to_owned(), None) + .expect("origin A"); + let origin_b = + WindowAccessOrigin::from_serialized_origin("https://b.example.test".to_owned(), None) + .expect("origin B"); + + let no_referrer = build_child_document_internal_ancestor_origins( + origin_b.clone(), + &[origin_b.clone(), origin_a.clone()], + &origin_a, + ChildAncestorOriginsReferrerPolicy::NoReferrer, + ); + assert_eq!( + no_referrer + .iter() + .map(WindowAccessOrigin::serialized_origin) + .collect::>(), + vec!["null", "null", "https://a.example.test"] + ); + + let same_origin = build_child_document_internal_ancestor_origins( + origin_b.clone(), + std::slice::from_ref(&origin_a), + &origin_a, + ChildAncestorOriginsReferrerPolicy::SameOrigin, + ); + assert_eq!( + same_origin + .iter() + .map(WindowAccessOrigin::serialized_origin) + .collect::>(), + vec!["null", "https://a.example.test"] + ); + + let default = build_child_document_internal_ancestor_origins( + origin_b, + &[origin_a], + &WindowAccessOrigin::from_serialized_origin( + "https://child.example.test".to_owned(), + None, + ) + .expect("child origin"), + ChildAncestorOriginsReferrerPolicy::Default, + ); + assert_eq!( + default + .iter() + .map(WindowAccessOrigin::serialized_origin) + .collect::>(), + vec!["https://b.example.test", "https://a.example.test"] + ); + } + #[test] fn isolated_world_uses_a_distinct_composite_origin_token() { let origin = "https://example.test".to_owned(); diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs index 52f165b95..783166eee 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs @@ -1578,3 +1578,127 @@ addEventListener("message", event => {{ vec!["/middle.html", "/grandchild.html"] ); } + +#[test] +fn location_ancestor_origins_is_a_stable_document_list_with_a_detached_empty_list() { + let mut vm = new_storage_test_vm("https://ancestor-origins.test/page.html"); + + let result = vm + .eval( + r#" +(() => { + const frame = document.createElement("iframe"); + (document.body || document.documentElement || document).appendChild(frame); + const location = frame.contentWindow.location; + const ChildDOMStringList = frame.contentWindow.DOMStringList; + const active = location.ancestorOrigins; + const activeAgain = location.ancestorOrigins; + const activeSnapshot = { + values: Array.from(active), + sameObject: active === activeAgain, + brand: active instanceof ChildDOMStringList, + topRealmBrand: active instanceof DOMStringList, + isArray: Array.isArray(active), + length: active.length, + item0: active.item(0), + item1IsNull: active.item(1) === null, + containsParent: active.contains(window.origin) + }; + frame.remove(); + const detached = location.ancestorOrigins; + + const unreadFrame = document.createElement("iframe"); + (document.body || document.documentElement || document).appendChild(unreadFrame); + const unreadLocation = unreadFrame.contentWindow.location; + unreadFrame.remove(); + const unreadDetached = unreadLocation.ancestorOrigins; + return JSON.stringify({ + active: activeSnapshot, + detachedValues: Array.from(detached), + detachedIsDifferent: detached !== active, + detachedIsStable: detached === location.ancestorOrigins, + detachedBrand: detached instanceof ChildDOMStringList, + unreadDetachedValues: Array.from(unreadDetached), + unreadDetachedIsArray: Array.isArray(unreadDetached), + unreadDetachedConstructor: unreadDetached.constructor.name + }); +})() +"#, + ) + .expect("Location ancestor origins lifetime probe should evaluate"); + + assert_eq!( + result, + r#"{"active":{"values":["https://ancestor-origins.test"],"sameObject":true,"brand":true,"topRealmBrand":false,"isArray":false,"length":1,"item0":"https://ancestor-origins.test","item1IsNull":true,"containsParent":true},"detachedValues":[],"detachedIsDifferent":true,"detachedIsStable":true,"detachedBrand":true,"unreadDetachedValues":[],"unreadDetachedIsArray":false,"unreadDetachedConstructor":"DOMStringList"}"# + ); +} + +#[test] +fn location_ancestor_origins_snapshots_frame_referrer_policy_per_navigation() { + let mut vm = new_storage_test_vm("https://ancestor-policy.test/page.html"); + + let initial = vm + .eval( + r#" +(() => { + const mount = document.body || document.documentElement || document; + + const snapshotted = document.createElement("iframe"); + snapshotted.srcdoc = "

snapshotted

"; + snapshotted.referrerPolicy = "no-referrer"; + mount.appendChild(snapshotted); + const snapshottedLocation = snapshotted.contentWindow.location; + const snapshottedInitialList = snapshottedLocation.ancestorOrigins; + snapshotted.referrerPolicy = ""; + + const futureNavigation = document.createElement("iframe"); + futureNavigation.referrerPolicy = "no-referrer"; + mount.appendChild(futureNavigation); + const futureLocation = futureNavigation.contentWindow.location; + const futureInitialList = futureLocation.ancestorOrigins; + futureNavigation.referrerPolicy = ""; + futureNavigation.srcdoc = "

future navigation

"; + + Object.assign(globalThis, { + __ancestorPolicySnapshottedLocation: snapshottedLocation, + __ancestorPolicySnapshottedInitialList: snapshottedInitialList, + __ancestorPolicyFutureLocation: futureLocation, + __ancestorPolicyFutureInitialList: futureInitialList + }); + return JSON.stringify({ + snapshotted: Array.from(snapshottedInitialList), + future: Array.from(futureInitialList) + }); +})() +"#, + ) + .expect("initial Location ancestor policy probe should evaluate"); + assert_eq!(initial, r#"{"snapshotted":["null"],"future":["null"]}"#); + + vm.drain_pending_child_frame_work_for_test(); + + let committed = vm + .eval( + r#" +(() => { + const snapshotted = __ancestorPolicySnapshottedLocation.ancestorOrigins; + const future = __ancestorPolicyFutureLocation.ancestorOrigins; + return JSON.stringify({ + snapshotted: Array.from(snapshotted), + snapshottedNewDocumentList: + snapshotted !== __ancestorPolicySnapshottedInitialList, + snapshottedStable: + snapshotted === __ancestorPolicySnapshottedLocation.ancestorOrigins, + future: Array.from(future), + futureNewDocumentList: future !== __ancestorPolicyFutureInitialList, + futureStable: future === __ancestorPolicyFutureLocation.ancestorOrigins + }); +})() +"#, + ) + .expect("committed Location ancestor policy probe should evaluate"); + assert_eq!( + committed, + r#"{"snapshotted":["null"],"snapshottedNewDocumentList":true,"snapshottedStable":true,"future":["https://ancestor-policy.test"],"futureNewDocumentList":true,"futureStable":true}"# + ); +}