fix(location): track ancestor origins per document

This commit is contained in:
ldm0
2026-09-27 19:28:50 +08:00
parent 2be166c7b8
commit 729d024c2a
13 changed files with 662 additions and 18 deletions
@@ -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)]
@@ -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,
@@ -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<String>)> {
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, &current_href);
}
@@ -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<v8::Local<'s, v8::Object>> {
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<v8::Local<'s, v8::Object>> {
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<u64> {
let value = get_private_value(scope, object, LOCATION_RELEVANT_DOCUMENT_ID_SLOT)?;
let value = v8::Local::<v8::BigInt>::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<u64> {
let value = get_private_value(scope, object, LOCATION_RELEVANT_LOCAL_WINDOW_ID_SLOT)?;
let value = v8::Local::<v8::BigInt>::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, '_>,
@@ -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) {
@@ -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 { .. } => {
@@ -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),
@@ -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<ChildBrowsingContextSnapshot>,
document_policy_container: ChildDocumentPolicyContainer,
document_internal_ancestor_origins: Vec<WindowAccessOrigin>,
ancestor_origins_referrer_policy_snapshot: ChildAncestorOriginsReferrerPolicy,
completed_document_network: Option<CompletedChildDocumentNetwork>,
completed_frame_owner_resource_timing: Option<CompletedFrameOwnerResourceTiming>,
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<WindowAccessOrigin> {
self.document_internal_ancestor_origins.clone()
}
pub(super) fn set_document_internal_ancestor_origins(
&mut self,
origins: Vec<WindowAccessOrigin>,
) {
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,
@@ -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)
@@ -27,6 +27,7 @@ impl JsContextHost {
) -> Option<crate::frame_owner_model::FrameDocumentOwnerTransition> {
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)
@@ -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 {
@@ -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<Vec<String>> {
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<String> {
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<String>,
@@ -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<WindowAccessOrigin> {
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<_>>(),
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<_>>(),
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<_>>(),
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();
@@ -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 = "<!doctype html><p>snapshotted</p>";
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 = "<!doctype html><p>future navigation</p>";
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}"#
);
}