refactor(window): dispatch cross-origin access through native tables

Replace WeakMap-backed Window surfaces with native access-check dispatch.
Keep per-Window Location identity and resolve dynamic children from the
current frame tree. Reuse the ordinary Window accessor value helpers.

Apply cross-origin descriptor and fallback rules, preserve borrowed receiver
behavior, and cover navigation, named collisions, removal, and realm teardown.
This commit is contained in:
ldm0
2026-10-17 03:31:45 +08:00
committed by Donough Liu
parent b4f9a9677f
commit 67283068e3
18 changed files with 965 additions and 1079 deletions
+19 -21
View File
@@ -5113,7 +5113,10 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> {
'replace',\
'__moliChildBrowsingContextHandle',\
'unknownCrossOriginProbe'\
].map((name) => `${name}:${name in win.location}:${Object.prototype.hasOwnProperty.call(win.location, name)}`);\
].map((name) => {\
try { return `${name}:${name in win.location}:${Object.prototype.hasOwnProperty.call(win.location, name)}`; }\
catch (error) { return `${name}:${error.name}:${error instanceof DOMException}`; }\
});\
const calls = ['blur', 'focus', 'close'].map((name) => {\
try {\
return `${name}:${String(win[name]())}`;\
@@ -5150,14 +5153,18 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> {
const lengthDescriptor = Object.getOwnPropertyDescriptor(win, 'length');\
const locationDescriptor = Object.getOwnPropertyDescriptor(win, 'location');\
const locationHrefDescriptor = Object.getOwnPropertyDescriptor(win.location, 'href');\
const locationHashDescriptor = Object.getOwnPropertyDescriptor(win.location, 'hash');\
const deniedDescriptor = (object, name) => {\
try { Object.getOwnPropertyDescriptor(object, name); return { error: 'accepted' }; }\
catch (error) { return { error: `${error.name}:${error instanceof DOMException}` }; }\
};\
const locationHashDescriptor = deniedDescriptor(win.location, 'hash');\
const postMessageDescriptor = Object.getOwnPropertyDescriptor(win, 'postMessage');\
const noopDescriptors = ['blur', 'focus', 'close'].map((name) => {\
const descriptor = Object.getOwnPropertyDescriptor(win, name);\
return `${name}:${descriptor?.enumerable}:${descriptor?.configurable}:${descriptor?.writable}:${typeof descriptor?.value}:${descriptor?.value?.name}:${descriptor?.value?.length}`;\
});\
const locationReplaceDescriptor = Object.getOwnPropertyDescriptor(win.location, 'replace');\
const setTimeoutDescriptor = Object.getOwnPropertyDescriptor(win, 'setTimeout');\
const setTimeoutDescriptor = deniedDescriptor(win, 'setTimeout');\
let locationReplaceInvalidReceiver;\
try {\
win.location.replace.call({}, '/compat/window-child-browsing-context-target-name-a?via=bad-replace-receiver');\
@@ -5233,8 +5240,9 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> {
selfDescriptor: {\
enumerable: selfDescriptor?.enumerable,\
configurable: selfDescriptor?.configurable,\
writable: selfDescriptor?.writable,\
valueIsSelf: selfDescriptor?.value === win\
getterType: typeof selfDescriptor?.get,\
setterType: typeof selfDescriptor?.set,\
getterIsSelf: selfDescriptor?.get.call(win) === win\
},\
lengthDescriptor: {\
enumerable: lengthDescriptor?.enumerable,\
@@ -5254,12 +5262,7 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> {
getterType: typeof locationHrefDescriptor?.get,\
setterType: typeof locationHrefDescriptor?.set\
},\
locationHashDescriptor: {\
enumerable: locationHashDescriptor?.enumerable,\
configurable: locationHashDescriptor?.configurable,\
getterType: typeof locationHashDescriptor?.get,\
setterType: typeof locationHashDescriptor?.set\
},\
locationHashDescriptor,\
postMessageDescriptor: {\
enumerable: postMessageDescriptor?.enumerable,\
configurable: postMessageDescriptor?.configurable,\
@@ -5281,12 +5284,7 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> {
locationReplaceForgedReceiver,\
locationHrefSetterInvalidReceiver,\
locationGetterInvalidReceiver,\
setTimeoutDescriptor: {\
enumerable: setTimeoutDescriptor?.enumerable,\
configurable: setTimeoutDescriptor?.configurable,\
getterType: typeof setTimeoutDescriptor?.get,\
setterType: typeof setTimeoutDescriptor?.set\
},\
setTimeoutDescriptor,\
documentDescriptor: documentDescriptorShape,\
windowLocationAssignResult,\
locationStableAfterWindowAssign: win.location === locationBeforeWindowAssign,\
@@ -5304,7 +5302,7 @@ async fn cross_origin_window_proxy_exposes_standard_noop_shape() -> Result<()> {
assert_eq!(
result,
Some(
r#"{"self":true,"window":true,"frames":true,"parent":true,"top":true,"opener":true,"thenType":"undefined","length":3,"closed":false,"blurType":"function","focusType":"function","closeType":"function","postMessageType":"function","restrictedMutationProbe":["deleteDocument:SecurityError","deleteSetTimeout:SecurityError","defineDocument:SecurityError","definePostMessage:SecurityError","deleteLocationHref:SecurityError","defineLocationHref:SecurityError"],"hasProbe":["document:true:true","setTimeout:true:true","postMessage:true:true","location:true:true","self:true:true","window:true:true","frames:true:true","parent:true:true","top:true:true","closed:true:true","opener:true:true","then:true:true","__moliChildBrowsingContextHandle:SecurityError:true","__moliCrossOriginWindowLocation:SecurityError:true","unknownCrossOriginProbe:SecurityError:true"],"locationHasProbe":["href:true:true","hash:true:true","replace:true:true","__moliChildBrowsingContextHandle:false:false","unknownCrossOriginProbe:false:false"],"calls":["blur:undefined","focus:undefined","close:undefined"],"invalidNoopReceivers":["blur:TypeError:true","focus:TypeError:true","close:TypeError:true"],"postMessageWindowReceiver":"ok","postMessageInvalidReceiver":"TypeError:true","ownNamesLeakInternal":false,"ownKeysLeakInternal":false,"locationOwnNamesLeakInternal":false,"selfDescriptor":{"enumerable":false,"configurable":false,"writable":false,"valueIsSelf":true},"lengthDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"locationDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"locationHrefDescriptor":{"enumerable":false,"configurable":false,"getterType":"undefined","setterType":"function"},"locationHashDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"postMessageDescriptor":{"enumerable":false,"configurable":false,"writable":false,"valueType":"function","valueName":"postMessage","valueLength":1},"noopDescriptors":["blur:false:false:false:function:blur:0","focus:false:false:false:function:focus:0","close:false:false:false:function:close:0"],"locationReplaceDescriptor":{"enumerable":false,"configurable":false,"writable":false,"valueType":"function","valueName":"replace","valueLength":1},"locationReplaceInvalidReceiver":"TypeError:true","locationReplaceForgedReceiver":"TypeError:true","locationHrefSetterInvalidReceiver":"TypeError:true","locationGetterInvalidReceiver":"TypeError:true","setTimeoutDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"documentDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function"},"windowLocationAssignResult":"ok","locationStableAfterWindowAssign":true,"deniedWindowProbe":["document:SecurityError:true","frameElement:SecurityError:true","history:SecurityError:true","navigation:SecurityError:true","localStorage:SecurityError:true","sessionStorage:SecurityError:true","indexedDB:SecurityError:true","customElements:SecurityError:true","navigator:SecurityError:true","performance:SecurityError:true","console:SecurityError:true","screen:SecurityError:true","visualViewport:SecurityError:true","crypto:SecurityError:true","caches:SecurityError:true","clientInformation:SecurityError:true","cookieStore:SecurityError:true","credentialless:SecurityError:true","crossOriginIsolated:SecurityError:true","documentPictureInPicture:SecurityError:true","fetch:SecurityError:true","isSecureContext:SecurityError:true","origin:SecurityError:true","originAgentCluster:SecurityError:true","scheduler:SecurityError:true","speechSynthesis:SecurityError:true","structuredClone:SecurityError:true","trustedTypes:SecurityError:true","setTimeout:SecurityError:true","clearImmediate:SecurityError:true","addEventListener:SecurityError:true","dispatchEvent:SecurityError:true","queueMicrotask:SecurityError:true","requestAnimationFrame:SecurityError:true","getComputedStyle:SecurityError:true","getSelection:SecurityError:true","matchMedia:SecurityError:true","event:SecurityError:true","onerror:SecurityError:true","innerWidth:SecurityError:true","innerHeight:SecurityError:true","devicePixelRatio:SecurityError:true","scrollX:SecurityError:true","pageYOffset:SecurityError:true","scrollTo:SecurityError:true","open:SecurityError:true","stop:SecurityError:true","print:SecurityError:true","find:SecurityError:true","alert:SecurityError:true","confirm:SecurityError:true","prompt:SecurityError:true","reportError:SecurityError:true","btoa:SecurityError:true","atob:SecurityError:true"],"documentAccess":"SecurityError:true"}"#.to_owned(),
r#"{"self":true,"window":true,"frames":true,"parent":true,"top":true,"opener":true,"thenType":"undefined","length":3,"closed":false,"blurType":"function","focusType":"function","closeType":"function","postMessageType":"function","restrictedMutationProbe":["deleteDocument:SecurityError","deleteSetTimeout:SecurityError","defineDocument:SecurityError","definePostMessage:SecurityError","deleteLocationHref:SecurityError","defineLocationHref:SecurityError"],"hasProbe":["document:true:true","setTimeout:SecurityError:true","postMessage:true:true","location:true:true","self:true:true","window:true:true","frames:true:true","parent:true:true","top:true:true","closed:true:true","opener:true:true","then:true:true","__moliChildBrowsingContextHandle:SecurityError:true","__moliCrossOriginWindowLocation:SecurityError:true","unknownCrossOriginProbe:SecurityError:true"],"locationHasProbe":["href:true:true","hash:SecurityError:true","replace:true:true","__moliChildBrowsingContextHandle:SecurityError:true","unknownCrossOriginProbe:SecurityError:true"],"calls":["blur:undefined","focus:undefined","close:undefined"],"invalidNoopReceivers":["blur:TypeError:true","focus:TypeError:true","close:TypeError:true"],"postMessageWindowReceiver":"ok","postMessageInvalidReceiver":"TypeError:true","ownNamesLeakInternal":false,"ownKeysLeakInternal":false,"locationOwnNamesLeakInternal":false,"selfDescriptor":{"enumerable":false,"configurable":true,"getterType":"function","setterType":"undefined","getterIsSelf":true},"lengthDescriptor":{"enumerable":false,"configurable":true,"getterType":"function","setterType":"undefined"},"locationDescriptor":{"enumerable":false,"configurable":true,"getterType":"function","setterType":"function"},"locationHrefDescriptor":{"enumerable":false,"configurable":true,"getterType":"undefined","setterType":"function"},"locationHashDescriptor":{"error":"SecurityError:true"},"postMessageDescriptor":{"enumerable":false,"configurable":true,"writable":false,"valueType":"function","valueName":"postMessage","valueLength":1},"noopDescriptors":["blur:false:true:false:function:blur:0","focus:false:true:false:function:focus:0","close:false:true:false:function:close:0"],"locationReplaceDescriptor":{"enumerable":false,"configurable":true,"writable":false,"valueType":"function","valueName":"replace","valueLength":1},"locationReplaceInvalidReceiver":"TypeError:true","locationReplaceForgedReceiver":"TypeError:true","locationHrefSetterInvalidReceiver":"TypeError:true","locationGetterInvalidReceiver":"TypeError:true","setTimeoutDescriptor":{"error":"SecurityError:true"},"documentDescriptor":{"enumerable":false,"configurable":true,"getterType":"undefined","setterType":"undefined"},"windowLocationAssignResult":"ok","locationStableAfterWindowAssign":true,"deniedWindowProbe":["document:read","frameElement:SecurityError:true","history:SecurityError:true","navigation:SecurityError:true","localStorage:SecurityError:true","sessionStorage:SecurityError:true","indexedDB:SecurityError:true","customElements:SecurityError:true","navigator:SecurityError:true","performance:SecurityError:true","console:SecurityError:true","screen:SecurityError:true","visualViewport:SecurityError:true","crypto:SecurityError:true","caches:SecurityError:true","clientInformation:SecurityError:true","cookieStore:SecurityError:true","credentialless:SecurityError:true","crossOriginIsolated:SecurityError:true","documentPictureInPicture:SecurityError:true","fetch:SecurityError:true","isSecureContext:SecurityError:true","origin:SecurityError:true","originAgentCluster:SecurityError:true","scheduler:SecurityError:true","speechSynthesis:SecurityError:true","structuredClone:SecurityError:true","trustedTypes:SecurityError:true","setTimeout:SecurityError:true","clearImmediate:SecurityError:true","addEventListener:SecurityError:true","dispatchEvent:SecurityError:true","queueMicrotask:SecurityError:true","requestAnimationFrame:SecurityError:true","getComputedStyle:SecurityError:true","getSelection:SecurityError:true","matchMedia:SecurityError:true","event:SecurityError:true","onerror:SecurityError:true","innerWidth:SecurityError:true","innerHeight:SecurityError:true","devicePixelRatio:SecurityError:true","scrollX:SecurityError:true","pageYOffset:SecurityError:true","scrollTo:SecurityError:true","open:SecurityError:true","stop:SecurityError:true","print:SecurityError:true","find:SecurityError:true","alert:SecurityError:true","confirm:SecurityError:true","prompt:SecurityError:true","reportError:SecurityError:true","btoa:SecurityError:true","atob:SecurityError:true"],"documentAccess":"read"}"#.to_owned(),
),
"{}",
page.serialize_html_async().await.unwrap()
@@ -5454,7 +5452,7 @@ async fn cross_origin_window_proxy_exposes_named_child_frames() -> Result<()> {
assert_eq!(
before,
Some(
r#"{"hasNamed":true,"ownNamed":true,"hasDocumentCollision":true,"hasFocusCollision":true,"sameAsIndexed":true,"documentDescriptor":{"enumerable":false,"configurable":false,"getterType":"function","setterType":"function","valueType":"undefined"},"focusDescriptor":{"enumerable":false,"configurable":false,"writable":false,"valueType":"function"},"documentAccess":"SecurityError:true","focusType":"function","namedSelf":true,"namedWindow":true,"namedFrames":true,"namedParent":true,"namedTop":true,"namedLength":0,"descriptor":{"enumerable":false,"configurable":true,"writable":false,"valueType":"object"}}"#
r#"{"hasNamed":true,"ownNamed":true,"hasDocumentCollision":true,"hasFocusCollision":true,"sameAsIndexed":true,"documentDescriptor":{"enumerable":false,"configurable":true,"getterType":"undefined","setterType":"undefined","valueType":"object"},"focusDescriptor":{"enumerable":false,"configurable":true,"writable":false,"valueType":"function"},"documentAccess":"read","focusType":"function","namedSelf":true,"namedWindow":true,"namedFrames":true,"namedParent":true,"namedTop":true,"namedLength":0,"descriptor":{"enumerable":false,"configurable":true,"writable":false,"valueType":"object"}}"#
.to_owned(),
),
"{}",
@@ -5497,7 +5495,7 @@ async fn cross_origin_window_proxy_exposes_named_child_frames() -> Result<()> {
afterOwnNamed: probe(() => Object.prototype.hasOwnProperty.call(refreshed, 'nestedNamed')),\
afterNamedType: probe(() => typeof refreshed.nestedNamed),\
afterLength: refreshed.length,\
afterDocumentDescriptorGetter: typeof Object.getOwnPropertyDescriptor(refreshed, 'document')?.get,\
afterDocumentDescriptorGetter: probe(() => typeof Object.getOwnPropertyDescriptor(refreshed, 'document')?.get),\
afterFocusType: typeof refreshed.focus\
});\
})()",
@@ -5508,7 +5506,7 @@ async fn cross_origin_window_proxy_exposes_named_child_frames() -> Result<()> {
assert_eq!(
after,
Some(
r#"{"afterHasNamed":"SecurityError:true","afterOwnNamed":"SecurityError:true","afterNamedType":"SecurityError:true","afterLength":0,"afterDocumentDescriptorGetter":"function","afterFocusType":"function"}"#
r#"{"afterHasNamed":"SecurityError:true","afterOwnNamed":"SecurityError:true","afterNamedType":"SecurityError:true","afterLength":0,"afterDocumentDescriptorGetter":"SecurityError:true","afterFocusType":"function"}"#
.to_owned(),
),
"{}",
+2 -2
View File
@@ -208,8 +208,8 @@ mod webrtc;
mod websocket;
mod window_accessors;
pub(crate) use window_accessors::{
cached_cross_origin_window_surface, window_closed_getter, window_length_getter,
window_opener_getter, window_parent_getter, window_top_getter,
window_closed_value, window_frames_value, window_length_value, window_opener_value,
window_parent_value, window_self_value, window_top_value,
};
mod window_events;
mod window_lazy_surface;
@@ -1724,7 +1724,6 @@ pub(crate) fn finish_context_bootstrap(
document_runtime: &mut JsContextHost,
secure_context_url: &url::Url,
) -> Result<()> {
super::window_accessors::initialize_cross_origin_window_cache(scope)?;
super::exposed_interfaces::initialize_realm_interface_registry(
scope,
super::exposed_interfaces::RealmKind::Window,
@@ -8,11 +8,11 @@ use crate::{
};
mod child_context;
mod cross_origin_cache;
mod document;
mod helpers;
mod interceptors;
mod surface;
mod values;
use super::CHILD_BROWSING_CONTEXT_HANDLE_SLOT as WINDOW_CHILD_CONTEXT_HANDLE_SLOT;
@@ -22,8 +22,6 @@ pub(crate) use child_context::window_length_getter;
pub(super) use child_context::{
window_credentialless_getter, window_cross_origin_isolated_getter, window_frame_element_getter,
};
pub(crate) use cross_origin_cache::cached_cross_origin_window_surface;
pub(super) use cross_origin_cache::initialize_cross_origin_window_cache;
pub(super) use document::{WINDOW_DOCUMENT_SLOT, window_document_getter_template};
pub(crate) use document::{
bind_current_child_window_document, retain_window_document_in_retired_realm,
@@ -46,3 +44,7 @@ pub(super) use surface::{
window_screen_getter, window_scroll_x_getter, window_scroll_y_getter, window_self_getter,
window_speech_synthesis_getter, window_visual_viewport_getter, window_window_getter,
};
pub(crate) use values::{
window_closed_value, window_frames_value, window_length_value, window_opener_value,
window_parent_value, window_self_value, window_top_value,
};
@@ -1,6 +1,5 @@
use super::helpers::{
window_child_context_handle, window_current_dispatch_scope, window_hidden_value,
window_host_ptr, window_is_closed, window_receiver,
window_child_context_handle, window_hidden_value, window_host_ptr, window_receiver,
};
use super::*;
@@ -12,25 +11,9 @@ pub(crate) fn window_length_getter<'s>(
let Some(receiver) = window_receiver(scope, &args) else {
return;
};
if window_is_closed(scope, receiver) {
rv.set_uint32(0);
return;
if let Some(value) = super::values::window_length_value(scope, receiver) {
rv.set(value);
}
let Some(host_ptr) = window_host_ptr(scope, receiver) else {
rv.set(v8::Number::new(scope, 0.0).into());
return;
};
let runtime = unsafe { &mut *host_ptr };
let Some(owner) = window_current_dispatch_scope(scope, receiver, runtime) else {
rv.set(v8::Number::new(scope, 0.0).into());
return;
};
let count = if let Some(handle) = owner.child_window() {
runtime.child_browsing_context_child_frame_count(handle)
} else {
runtime.child_browsing_context_count()
};
rv.set(v8::Number::new(scope, count as f64).into());
}
pub(in crate::context_bootstrap) fn window_frame_element_getter<'s>(
@@ -1,83 +0,0 @@
use anyhow::{Result, anyhow};
use crate::util::{get_private_value, set_private_value, v8str};
const CACHE: &str = "__moliCrossOriginWindowSurfaceCache";
/// Capture the builtins before author code runs. The array is owned by V8's
/// realm graph, so neither the cache nor its functions root a retired realm.
/// Slot zero starts as the constructor and becomes a WeakMap on first use.
pub(in crate::context_bootstrap) fn initialize_cross_origin_window_cache(
scope: &mut v8::PinScope<'_, '_>,
) -> Result<()> {
let context = scope.get_current_context();
let anchor = context.get_extras_binding_object(scope);
if get_private_value(scope, anchor, CACHE).is_some() {
return Ok(());
}
let constructor = context
.global(scope)
.get(scope, v8str(scope, "WeakMap").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
.ok_or_else(|| anyhow!("missing intrinsic WeakMap constructor"))?;
let prototype = constructor
.get(scope, v8str(scope, "prototype").into())
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
.ok_or_else(|| anyhow!("missing intrinsic WeakMap prototype"))?;
let [get, set] = ["get", "set"].map(|name| {
prototype
.get(scope, v8str(scope, name).into())
.filter(|value| value.is_function())
.ok_or_else(|| anyhow!("missing intrinsic WeakMap.{name}"))
});
let entries = [constructor.into(), get?, set?];
let cache = v8::Array::new_with_elements(scope, &entries);
set_private_value(scope, anchor, CACHE, cache.into());
Ok(())
}
/// Each target owns an ephemeron cache keyed by the accessing realm. This
/// preserves per-observer function identity without retaining dead observers.
pub(crate) fn cached_cross_origin_window_surface<'s>(
scope: &mut v8::PinScope<'s, '_>,
target: v8::Local<'s, v8::Context>,
accessing: v8::Local<'s, v8::Context>,
build: impl FnOnce(&mut v8::PinScope<'s, '_>) -> v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
let (map, get, set) = {
let scope = &mut v8::ContextScope::new(scope, target);
let anchor = target.get_extras_binding_object(scope);
let cache = get_private_value(scope, anchor, CACHE)
.and_then(|value| v8::Local::<v8::Array>::try_from(value).ok())
.expect("Window cross-origin cache initialized during bootstrap");
let value = cache.get_index(scope, 0).expect("Window cache slot");
let map = if let Ok(constructor) = v8::Local::<v8::Function>::try_from(value) {
let map = constructor
.new_instance(scope, &[])
.expect("intrinsic WeakMap construction");
assert_eq!(cache.set_index(scope, 0, map.into()), Some(true));
map
} else {
v8::Local::<v8::Object>::try_from(value).expect("Window surface WeakMap")
};
let method = |scope: &mut v8::PinScope<'s, '_>, index| {
cache
.get_index(scope, index)
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
.expect("captured intrinsic WeakMap method")
};
(map, method(scope, 1), method(scope, 2))
};
let scope = &mut v8::ContextScope::new(scope, accessing);
let key = accessing.get_extras_binding_object(scope);
let existing = get
.call(scope, map.into(), &[key.into()])
.expect("intrinsic WeakMap.get");
if let Ok(surface) = v8::Local::<v8::Object>::try_from(existing) {
return surface;
}
let surface = build(scope);
set.call(scope, map.into(), &[key.into(), surface.into()])
.expect("intrinsic WeakMap.set");
surface
}
@@ -1,6 +1,5 @@
use super::helpers::{
window_child_context_handle, window_hidden_value, window_host_ptr, window_is_closed,
window_receiver,
window_child_context_handle, window_hidden_value, window_host_ptr, window_receiver,
};
use super::*;
@@ -94,19 +93,9 @@ fn set_receiver_window_alias<'s>(
let Some(receiver) = window_receiver(scope, args) else {
return;
};
if matches!(slot, WINDOW_PARENT_SLOT | WINDOW_TOP_SLOT) && window_is_closed(scope, receiver) {
rv.set_null();
return;
if let Some(value) = super::values::window_alias_value(scope, receiver, slot) {
rv.set(value);
}
// Cross-origin descriptors run in the accessing realm. Only these public
// Window aliases resolve their internal slots in the receiver's realm.
let context = receiver
.get_creation_context(scope)
.unwrap_or_else(|| scope.get_current_context());
let scope = &mut v8::ContextScope::new(scope, context);
rv.set(
window_hidden_value(scope, receiver, slot).unwrap_or_else(|| context.global(scope).into()),
);
}
pub(crate) fn window_opener_getter<'s>(
@@ -117,32 +106,9 @@ pub(crate) fn window_opener_getter<'s>(
let Some(receiver) = window_receiver(scope, &args) else {
return;
};
rv.set_null();
if window_is_closed(scope, receiver) || window_child_context_handle(scope, receiver).is_some() {
return;
if let Some(value) = super::values::window_opener_value(scope, receiver) {
rv.set(value);
}
let Some(host) = receiver
.get_creation_context(scope)
.and_then(context_host_ptr_from_context_slot)
else {
return;
};
let Some(environment) = unsafe { &*host }.page_script_environment() else {
return;
};
let Some(opener) = environment.opener_in_scope(scope) else {
return;
};
let Some(opener_host) = opener
.get_creation_context(scope)
.and_then(context_host_ptr_from_context_slot)
else {
return;
};
if unsafe { &*opener_host }.browsing_context_is_closed() {
return;
}
rv.set(opener.into());
}
pub(in crate::context_bootstrap) fn window_opener_setter<'s>(
@@ -363,5 +329,7 @@ pub(crate) fn window_closed_getter<'s>(
let Some(receiver) = window_receiver(scope, &args) else {
return;
};
rv.set_bool(window_is_closed(scope, receiver));
if let Some(value) = super::values::window_closed_value(scope, receiver) {
rv.set(value);
}
}
@@ -0,0 +1,83 @@
use super::helpers::{
window_current_dispatch_scope, window_hidden_value, window_host_ptr, window_is_closed,
};
use super::*;
pub(super) fn window_alias_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
slot: &'static str,
) -> Option<v8::Local<'s, v8::Value>> {
if matches!(slot, WINDOW_PARENT_SLOT | WINDOW_TOP_SLOT) && window_is_closed(scope, receiver) {
return Some(v8::null(scope).into());
}
let context = receiver
.get_creation_context(scope)
.unwrap_or_else(|| scope.get_current_context());
let scope = &mut v8::ContextScope::new(scope, context);
Some(window_hidden_value(scope, receiver, slot).unwrap_or_else(|| context.global(scope).into()))
}
macro_rules! alias {
($name:ident, $slot:ident) => {
pub(crate) fn $name<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
window_alias_value(scope, receiver, $slot)
}
};
}
alias!(window_self_value, WINDOW_SELF_SLOT);
alias!(window_frames_value, WINDOW_FRAMES_SLOT);
alias!(window_parent_value, WINDOW_PARENT_SLOT);
alias!(window_top_value, WINDOW_TOP_SLOT);
pub(crate) fn window_closed_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
let closed = window_is_closed(scope, receiver);
Some(v8::Boolean::new(scope, closed).into())
}
pub(crate) fn window_length_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
let count = if !window_is_closed(scope, receiver)
&& let Some(host) = window_host_ptr(scope, receiver)
&& let Some(owner) = window_current_dispatch_scope(scope, receiver, unsafe { &*host })
{
let host = unsafe { &mut *host };
match owner.child_window() {
Some(handle) => host.child_browsing_context_child_frame_count(handle),
None => host.child_browsing_context_count(),
}
} else {
0
};
Some(v8::Number::new(scope, count as f64).into())
}
pub(crate) fn window_opener_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
let null = v8::null(scope).into();
if window_is_closed(scope, receiver) || window_child_context_handle(scope, receiver).is_some() {
return Some(null);
}
let opener = (|| {
let host = receiver
.get_creation_context(scope)
.and_then(context_host_ptr_from_context_slot)?;
let environment = unsafe { &*host }.page_script_environment()?;
let opener = environment.opener_in_scope(scope)?;
let host = opener
.get_creation_context(scope)
.and_then(context_host_ptr_from_context_slot)?;
(!unsafe { &*host }.browsing_context_is_closed()).then_some(opener)
})();
Some(opener.map(Into::into).unwrap_or(null))
}
@@ -257,13 +257,6 @@ impl JsContextHost {
global,
projection.document,
);
self.install_child_window_proxy_cross_origin_access_surface(
scope,
init.handle,
global,
projection.parent,
projection.top,
);
}
Ok(())
}
@@ -67,11 +67,7 @@ pub(in crate::native_bridge::context_host::child_frame_runtime) fn initialize_ch
validate_child_window_realm_snapshot(host, &snapshot)?;
bind_document(host, scope, &snapshot, document)?;
Ok(ChildWindowRealmProjection {
parent,
top,
document,
})
Ok(ChildWindowRealmProjection { top, document })
}
pub(in crate::native_bridge::context_host::child_frame_runtime) fn rebind_child_window_realm_document_state<
@@ -55,8 +55,6 @@ pub(in crate::native_bridge::context_host::child_frame_runtime) struct ChildWind
pub(in crate::native_bridge::context_host::child_frame_runtime) struct ChildWindowRealmProjection<
's,
> {
pub(in crate::native_bridge::context_host::child_frame_runtime) parent:
v8::Local<'s, v8::Object>,
pub(in crate::native_bridge::context_host::child_frame_runtime) top: v8::Local<'s, v8::Object>,
pub(in crate::native_bridge::context_host::child_frame_runtime) document:
v8::Local<'s, v8::Object>,
@@ -4,7 +4,7 @@ use crate::{
CHILD_BROWSING_CONTEXT_HANDLE_SLOT,
dispatch_cross_document_navigation_navigate_event_for_window,
},
definitions::{define_function_accessor_property, define_get_set_property},
definitions::define_function_accessor_property,
document_runtime::DomHandle,
document_script_scheduler::FrameDocumentClassicScriptSchedulerWork,
native_bridge::{
@@ -19,13 +19,15 @@ use crate::{
},
webidl, window_host,
};
use moli_webapi_declare::WebApiObject;
use moli_webapi_declare::{WebApiCrossOriginDeclaration, WebApiObject};
use std::{
collections::{HashMap, HashSet},
rc::Rc,
};
mod main_page;
mod cross_origin_bindings;
mod location;
use cross_origin_bindings::*;
#[derive(Clone, Copy)]
struct ChildWindowProxyFacadeContextHandle(DomHandle);
@@ -51,7 +53,6 @@ struct ChildWindowProxyRecord {
realm_top_window_wrapper: Option<v8::Global<v8::Object>>,
live_window_exposed_to_top: bool,
cross_origin_window_proxy: Option<v8::Global<v8::Object>>,
cross_origin_access_surface: Option<v8::Global<v8::Object>>,
default_execution_context_id: Option<i64>,
}
@@ -114,11 +115,10 @@ impl ChildWindowProxyRecords {
self.records.get_mut(&handle)?.facade_context.take()
}
fn clear_retired_realm_surfaces(&mut self, handle: DomHandle) {
fn clear_retired_realm_projections(&mut self, handle: DomHandle) {
let Some(record) = self.records.get_mut(&handle) else {
return;
};
record.cross_origin_access_surface = None;
record.cross_origin_endpoint_projections.clear();
record.realm_top_window_wrapper = None;
}
@@ -131,7 +131,6 @@ impl ChildWindowProxyRecords {
) {
let record = self.record_mut(handle);
record.cross_origin_window_proxy = None;
record.cross_origin_access_surface = None;
record.live_window_wrapper = Some(v8::Global::new(scope, window));
}
@@ -276,30 +275,6 @@ impl ChildWindowProxyRecords {
self.record_mut(handle).cross_origin_window_proxy = Some(v8::Global::new(scope, proxy));
}
fn set_cross_origin_access_surface(
&mut self,
scope: &mut v8::PinScope<'_, '_>,
handle: DomHandle,
access_surface: v8::Local<'_, v8::Object>,
) {
self.record_mut(handle).cross_origin_access_surface =
Some(v8::Global::new(scope, access_surface));
}
fn cross_origin_handler_data<'s>(
&self,
scope: &mut v8::PinScope<'s, '_>,
handle: DomHandle,
) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> {
let record = self.records.get(&handle)?;
let access_surface = record.cross_origin_access_surface.as_ref()?;
let window_proxy = record.live_window_wrapper.as_ref()?;
Some((
v8::Local::new(scope, access_surface),
v8::Local::new(scope, window_proxy),
))
}
pub(in crate::native_bridge::context_host) fn set_default_execution_context_id(
&mut self,
handle: DomHandle,
@@ -599,21 +574,6 @@ const CROSS_ORIGIN_DENIED_WINDOW_PROPERTIES: &[&str] = &[
"atob",
];
const CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES: &[&str] = &[
"ancestorOrigins",
"assign",
"hash",
"host",
"hostname",
"origin",
"pathname",
"port",
"protocol",
"reload",
"search",
"toString",
];
const CROSS_ORIGIN_WINDOW_NOOP_METHODS: &[&str] = &["blur", "close", "focus"];
const CROSS_ORIGIN_WINDOW_LOCATION_SLOT: &str = "__moliCrossOriginWindowLocation";
@@ -789,19 +749,6 @@ struct CrossOriginPropertyDescriptorDeclaration<'scope> {
configurable: bool,
}
#[derive(Default, WebApiObject)]
#[webapi(plain)]
struct CrossOriginLocationMethodsDeclaration {
#[webapi(
method,
length = 1,
callback = cross_origin_location_replace_callback,
readonly,
dont_delete
)]
replace: (),
}
#[derive(WebApiObject)]
#[webapi(plain)]
struct CrossOriginLocationProxyHandlerDeclaration {
@@ -811,6 +758,10 @@ struct CrossOriginLocationProxyHandlerDeclaration {
set: (),
#[webapi(method, length = 2, callback = cross_origin_location_proxy_descriptor_callback)]
get_own_property_descriptor: (),
#[webapi(method, length = 1, callback = cross_origin_location_proxy_own_keys_callback)]
own_keys: (),
#[webapi(method, length = 2, callback = cross_origin_location_proxy_has_callback)]
has: (),
#[webapi(method, length = 2, callback = cross_origin_window_denied_callback)]
delete_property: (),
#[webapi(method, length = 3, callback = cross_origin_window_denied_callback)]
@@ -1047,11 +998,10 @@ impl JsContextHost {
// old inner global. Cross-origin callers can observe only the stable
// WindowProxy whitelist, so park that proxy on a restricted facade.
if self.top_window_can_access_child(handle) {
// These helpers are objects in the outgoing inner realm. Keep
// the stable detached proxy, but do not root its retired Context
// through the access surface while awaiting a replacement realm.
// Drop caller-realm endpoint projections while the stable
// WindowProxy waits for a replacement LocalWindow.
self.child_window_proxy_records
.clear_retired_realm_surfaces(handle);
.clear_retired_realm_projections(handle);
return true;
}
let Some(window_proxy) = self.child_window_proxy_records.live_window(scope, handle) else {
@@ -1098,19 +1048,6 @@ impl JsContextHost {
child_frame_count,
&named_indices,
);
let access_surface = new_null_prototype_object(facade_scope);
install_live_cross_origin_child_window_surface(
facade_scope,
access_surface,
handle,
parent,
top,
window_proxy,
child_frame_count,
&named_indices,
);
self.child_window_proxy_records
.set_cross_origin_access_surface(facade_scope, handle, access_surface);
self.child_window_proxy_records
.set_facade_context(handle, facade_context);
true
@@ -1121,42 +1058,6 @@ impl JsContextHost {
.live_window_exposed_to_top(handle)
}
pub(crate) fn install_child_window_proxy_cross_origin_access_surface<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
handle: DomHandle,
window_proxy: v8::Local<'s, v8::Object>,
realm_parent: v8::Local<'s, v8::Object>,
realm_top: v8::Local<'s, v8::Object>,
) {
// parent/top on a WindowProxy are browsing-context identities. Do not
// replace them with the caller-specific projections installed on a
// replacement inner global.
let parent = self
.child_window_proxy_records
.browsing_context_parent(scope, handle)
.unwrap_or(realm_parent);
let top = self
.child_window_proxy_records
.browsing_context_top(scope, handle)
.unwrap_or(realm_top);
let surface = new_null_prototype_object(scope);
let child_frame_count = self.child_browsing_context_child_frame_count(handle);
let named_indices = self.child_browsing_context_child_frame_named_indices(handle);
install_live_cross_origin_child_window_surface(
scope,
surface,
handle,
parent,
top,
window_proxy,
child_frame_count,
&named_indices,
);
self.child_window_proxy_records
.set_cross_origin_access_surface(scope, handle, surface);
}
pub(crate) fn child_browsing_context_window_proxy_for_top<'s>(
&mut self,
scope: &mut v8::PinScope<'s, '_>,
@@ -2037,22 +1938,7 @@ pub(super) fn build_cross_origin_location_proxy<'s>(
CHILD_BROWSING_CONTEXT_HANDLE_SLOT,
v8::Number::new(scope, handle.index() as f64).into(),
);
install_cross_origin_symbol_slots(scope, target, "Location");
for name in CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES {
let _ = define_function_accessor_property(
scope,
target,
name,
cross_origin_window_denied_callback,
None,
cross_origin_window_denied_callback,
None,
cross_origin_property_attributes(),
);
}
install_cross_origin_location_href(scope, target);
install_cross_origin_location_methods(scope, target);
set_cross_origin_object_slot(scope, target, "then", v8::undefined(scope).into());
let _ = install_cross_origin_location_keys(scope, target);
let location = wrap_cross_origin_location_proxy(scope, target).unwrap_or(target);
set_private_value(
scope,
@@ -2073,22 +1959,7 @@ fn build_detached_cross_origin_location_proxy<'s>(
CROSS_ORIGIN_LOCATION_PROXY_SLOT,
v8::Boolean::new(scope, true).into(),
);
install_cross_origin_symbol_slots(scope, target, "Location");
for name in CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES {
let _ = define_function_accessor_property(
scope,
target,
name,
cross_origin_window_denied_callback,
None,
cross_origin_window_denied_callback,
None,
cross_origin_property_attributes(),
);
}
install_cross_origin_location_href(scope, target);
install_cross_origin_location_methods(scope, target);
set_cross_origin_object_slot(scope, target, "then", v8::undefined(scope).into());
let _ = install_cross_origin_location_keys(scope, target);
let location = wrap_cross_origin_location_proxy(scope, target).unwrap_or(target);
set_private_value(
scope,
@@ -2113,6 +1984,8 @@ fn wrap_cross_origin_location_proxy<'s>(
get: (),
set: (),
get_own_property_descriptor: (),
own_keys: (),
has: (),
delete_property: (),
define_property: (),
}
@@ -2123,37 +1996,39 @@ fn wrap_cross_origin_location_proxy<'s>(
v8::Local::<v8::Object>::try_from(proxy).ok()
}
fn install_cross_origin_location_href<'s>(
fn install_cross_origin_location_keys<'s>(
scope: &mut v8::PinScope<'s, '_>,
location: v8::Local<'s, v8::Object>,
) {
let Some(setter) = v8::Function::builder(cross_origin_location_navigate_setter_callback)
.length(1)
.build(scope)
else {
return;
};
if let Some(setter_name) = v8_string(scope, "set href") {
setter.set_name(setter_name);
target: v8::Local<'s, v8::Object>,
) -> Option<()> {
for key in cross_origin_location_keys(scope) {
target
.define_own_property(
scope,
key.try_into().ok()?,
v8::undefined(scope).into(),
v8::PropertyAttribute::DONT_ENUM,
)
.filter(|defined| *defined)?;
}
let _ = define_get_set_property(
scope,
location,
v8str(scope, "href").into(),
v8::undefined(scope).into(),
setter.into(),
cross_origin_property_attributes(),
"href",
);
Some(())
}
fn install_cross_origin_location_methods<'s>(
fn cross_origin_location_keys<'s>(
scope: &mut v8::PinScope<'s, '_>,
location: v8::Local<'s, v8::Object>,
) {
CrossOriginLocationMethodsDeclaration::default()
.initialize(scope, location)
.expect("cross-origin Location methods declaration should initialize");
) -> Vec<v8::Local<'s, v8::Value>> {
let mut keys: Vec<v8::Local<v8::Value>> = LocationMembers::cross_origin_properties()
.iter()
.map(|member| v8str(scope, member.name()).into())
.collect();
keys.push(v8str(scope, "then").into());
for symbol in [
v8::Symbol::get_to_string_tag(scope),
v8::Symbol::get_has_instance(scope),
v8::Symbol::get_is_concat_spreadable(scope),
] {
keys.push(symbol.into());
}
keys
}
fn install_cross_origin_symbol_slots(
@@ -2182,302 +2057,6 @@ fn install_cross_origin_symbol_slots(
}
}
fn child_window_cross_origin_access_surface<'s>(
scope: &mut v8::PinScope<'s, '_>,
holder: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
child_window_cross_origin_handler_data(scope, holder).map(|(surface, _)| surface)
}
fn child_window_cross_origin_proxy_self<'s>(
scope: &mut v8::PinScope<'s, '_>,
holder: v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
child_window_cross_origin_handler_data(scope, holder)
.map(|(_, window_proxy)| window_proxy)
.unwrap_or(holder)
}
fn child_window_cross_origin_handler_data<'s>(
scope: &mut v8::PinScope<'s, '_>,
holder: v8::Local<'s, v8::Object>,
) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> {
let holder_context = holder.get_creation_context(scope)?;
let host_ptr = crate::util::context_host_ptr_from_context_slot(holder_context)?;
let host = unsafe { &*host_ptr };
if holder_context
.get_slot::<crate::util::MainDefaultWindowContext>()
.is_some()
&& host.browsing_context_is_closed()
{
return Some((
main_page::access_surface(scope, holder_context),
holder_context.global(scope),
));
}
if let Some(identity) = host.window_execution_context_identity_for_access_check(holder_context)
&& host.window_execution_context_identity_is_current(identity)
{
if !host.window_execution_context_identity_is_default_world(identity) {
return None;
}
if identity.dispatch_scope() == super::super::OwnerDispatchScope::Top {
let window = holder_context.global(scope);
return Some((main_page::access_surface(scope, holder_context), window));
}
let super::super::OwnerDispatchScope::Child(handle) = identity.dispatch_scope() else {
return None;
};
return host
.child_window_proxy_records
.cross_origin_handler_data(scope, handle);
}
// A WindowProxy belongs to the browsing context, not to one LocalWindow
// generation. A parked facade context keeps the stable proxy attached
// while the previous realm is retired and the replacement is pending.
let handle = holder_context
.get_slot::<ChildWindowProxyFacadeContextHandle>()?
.0;
if !host.child_browsing_context_is_live(handle) {
return None;
}
host.child_window_proxy_records
.cross_origin_handler_data(scope, handle)
}
fn child_window_cross_origin_identity_name(
scope: &mut v8::PinScope<'_, '_>,
key: v8::Local<'_, v8::Name>,
) -> bool {
v8::Local::<v8::String>::try_from(key)
.ok()
.map(|key| key.to_rust_string_lossy(scope))
.is_some_and(|key| matches!(key.as_str(), "self" | "window" | "globalThis" | "frames"))
}
fn child_window_cross_origin_named_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let holder = args.holder();
if child_window_cross_origin_identity_name(scope, key) {
rv.set(child_window_cross_origin_proxy_self(scope, holder).into());
return v8::Intercepted::kYes;
}
let Some(surface) = child_window_cross_origin_access_surface(scope, holder) else {
return v8::Intercepted::kNo;
};
if surface.has_own_property(scope, key).unwrap_or(false)
&& let Some(value) = cross_origin_surface_get(scope, holder, surface, key.into())
{
rv.set(value);
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
fn cross_origin_surface_get<'s>(
scope: &mut v8::PinScope<'s, '_>,
holder: v8::Local<'s, v8::Object>,
surface: v8::Local<'s, v8::Object>,
key: v8::Local<'s, v8::Value>,
) -> Option<v8::Local<'s, v8::Value>> {
// Main-page accessors resolve the call receiver. Property lookup must pass
// the WindowProxy through; otherwise `this` is the internal surface.
if main_page::is_main_surface(scope, surface) {
let window = child_window_cross_origin_proxy_self(scope, holder);
return surface.get_with_receiver(scope, key, window);
}
surface.get(scope, key)
}
fn child_window_cross_origin_named_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: v8::Local<'s, v8::Name>,
value: v8::Local<'s, v8::Value>,
args: v8::PropertyCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
let Some(surface) = child_window_cross_origin_access_surface(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let is_location = v8::Local::<v8::String>::try_from(key)
.ok()
.map(|key| key.to_rust_string_lossy(scope))
.as_deref()
== Some("location");
if is_location {
if main_page::is_main_surface(scope, surface) {
let window = child_window_cross_origin_proxy_self(scope, args.holder());
let _ = surface.set_with_receiver(scope, key.into(), value, window);
} else {
let _ = surface.set(scope, key.into(), value);
}
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
fn child_window_cross_origin_named_query<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Integer>,
) -> v8::Intercepted {
if child_window_cross_origin_identity_name(scope, key) {
rv.set_int32(cross_origin_property_attributes().as_u32() as i32);
return v8::Intercepted::kYes;
}
let Some(surface) = child_window_cross_origin_access_surface(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
if surface.has_own_property(scope, key).unwrap_or(false) {
rv.set_int32(cross_origin_property_attributes().as_u32() as i32);
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
fn child_window_cross_origin_named_enumerator<'s>(
scope: &mut v8::PinScope<'s, '_>,
callback_args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let mut property_names = v8::GetPropertyNamesArgsBuilder::new();
property_names.mode(v8::KeyCollectionMode::OwnOnly);
property_names.property_filter(v8::PropertyFilter::ALL_PROPERTIES);
property_names.index_filter(v8::IndexFilter::SkipIndices);
property_names.key_conversion(v8::KeyConversionMode::ConvertToString);
let names = child_window_cross_origin_access_surface(scope, callback_args.holder())
.and_then(|surface| surface.get_property_names(scope, property_names.build()))
.unwrap_or_else(|| v8::Array::new(scope, 0));
rv.set(names);
}
fn child_window_cross_origin_named_descriptor<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
if child_window_cross_origin_identity_name(scope, key) {
let value = child_window_cross_origin_proxy_self(scope, args.holder()).into();
let Ok(descriptor) =
CrossOriginPropertyDescriptorDeclaration::new(value, false, false, false).bind(scope)
else {
return v8::Intercepted::kNo;
};
rv.set(descriptor.into());
return v8::Intercepted::kYes;
}
let Some(surface) = child_window_cross_origin_access_surface(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let Some(descriptor) = surface.get_own_property_descriptor(scope, key) else {
return v8::Intercepted::kNo;
};
rv.set(descriptor);
v8::Intercepted::kYes
}
fn child_window_cross_origin_indexed_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(surface) = child_window_cross_origin_access_surface(scope, args.holder()) else {
rv.set_undefined();
return v8::Intercepted::kYes;
};
if main_page::is_main_surface(scope, surface)
&& !surface
.has_own_property(scope, v8_string(scope, &index.to_string()).unwrap().into())
.unwrap_or(false)
{
return v8::Intercepted::kNo;
}
match surface.get_index(scope, index) {
Some(value) => rv.set(value),
None => rv.set_undefined(),
}
v8::Intercepted::kYes
}
fn child_window_cross_origin_indexed_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
_index: u32,
_value: v8::Local<'s, v8::Value>,
_args: v8::PropertyCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
let _ = scope;
v8::Intercepted::kNo
}
fn child_window_cross_origin_indexed_query<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Integer>,
) -> v8::Intercepted {
let Some(surface) = child_window_cross_origin_access_surface(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let Some(key) = v8_string(scope, &index.to_string()) else {
return v8::Intercepted::kNo;
};
if !surface.has_own_property(scope, key.into()).unwrap_or(false) {
return v8::Intercepted::kNo;
}
rv.set_int32(cross_origin_index_property_attributes().as_u32() as i32);
v8::Intercepted::kYes
}
fn child_window_cross_origin_indexed_enumerator<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let holder = args.holder();
let count = child_window_cross_origin_access_surface(scope, holder)
.and_then(|surface| {
cross_origin_surface_get(scope, holder, surface, v8str(scope, "length").into())
})
.and_then(|length| length.uint32_value(scope))
.unwrap_or(0);
let array =
serialize_v8_iter_array(scope, 0..count).unwrap_or_else(|| v8::Array::new(scope, 0));
rv.set(array);
}
fn child_window_cross_origin_indexed_descriptor<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(surface) = child_window_cross_origin_access_surface(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let Some(key) = v8_string(scope, &index.to_string()) else {
return v8::Intercepted::kNo;
};
let Some(value) = surface.get(scope, key.into()) else {
return v8::Intercepted::kNo;
};
let Ok(descriptor) =
CrossOriginPropertyDescriptorDeclaration::new(value, false, true, false).bind(scope)
else {
return v8::Intercepted::kNo;
};
rv.set(descriptor.into());
v8::Intercepted::kYes
}
fn cross_origin_window_denied_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::FunctionCallbackArguments<'s>,
@@ -2575,21 +2154,28 @@ fn cross_origin_location_proxy_get_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if is_cross_origin_location_href_key_value(scope, args.get(1)) {
throw_cross_origin_location_security_error(scope);
return;
}
let Ok(target) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
rv.set_undefined();
let Ok(key) = v8::Local::<v8::Name>::try_from(args.get(1)) else {
return;
};
let receiver = v8::Local::<v8::Object>::try_from(args.get(2)).unwrap_or(target);
let surface = main_page::location_access_surface(scope, target).unwrap_or(target);
let accessing_context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, accessing_context);
match surface.get_with_receiver(scope, args.get(1), receiver) {
Some(value) => rv.set(value),
None => rv.set_undefined(),
let member = v8::Local::<v8::String>::try_from(key)
.ok()
.and_then(|key| LocationMembers::cross_origin_property(&key.to_rust_string_lossy(scope)));
let Some(member) = member.filter(|member| member.has_getter()) else {
if is_fallback_key(scope, key) {
rv.set_undefined();
} else {
throw_cross_origin_location_security_error(scope);
}
return;
};
let Ok(receiver) = v8::Local::<v8::Object>::try_from(args.get(2)) else {
throw_cross_origin_illegal_invocation(scope);
return;
};
let context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, context);
if let Some(value) = member.get(scope, receiver) {
rv.set(value);
}
}
@@ -2598,31 +2184,62 @@ fn cross_origin_location_proxy_descriptor_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Ok(target) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
return;
};
let Ok(key) = v8::Local::<v8::Name>::try_from(args.get(1)) else {
return;
};
let main_surface = main_page::location_access_surface(scope, target);
let surface = main_surface.unwrap_or(target);
let accessing_context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, accessing_context);
let Some(descriptor) = surface.get_own_property_descriptor(scope, key) else {
let member = v8::Local::<v8::String>::try_from(key)
.ok()
.and_then(|key| LocationMembers::cross_origin_property(&key.to_rust_string_lossy(scope)));
let context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, context);
if let Some(member) = member {
if let Some(descriptor) = member.descriptor(scope) {
rv.set(descriptor.into());
}
} else if is_fallback_key(scope, key) {
if let Ok(descriptor) = CrossOriginPropertyDescriptorDeclaration::new(
v8::undefined(scope).into(),
false,
false,
true,
)
.bind(scope)
{
rv.set(descriptor.into());
}
} else {
throw_cross_origin_location_security_error(scope);
}
}
fn cross_origin_location_proxy_own_keys_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, context);
let keys = cross_origin_location_keys(scope);
rv.set(v8::Array::new_with_elements(scope, &keys).into());
}
fn cross_origin_location_proxy_has_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Ok(key) = v8::Local::<v8::Name>::try_from(args.get(1)) else {
return;
};
if main_surface.is_some()
&& let Ok(descriptor) = v8::Local::<v8::Object>::try_from(descriptor)
{
// Main Location's target holds placeholders. Its descriptors are
// projected into each accessor realm and must remain configurable.
let _ = descriptor.set(
scope,
v8str(scope, "configurable").into(),
v8::Boolean::new(scope, true).into(),
);
let known = v8::Local::<v8::String>::try_from(key)
.ok()
.and_then(|key| LocationMembers::cross_origin_property(&key.to_rust_string_lossy(scope)))
.is_some();
if known || is_fallback_key(scope, key) {
rv.set_bool(true);
} else {
throw_cross_origin_location_security_error(scope);
}
rv.set(descriptor);
}
fn cross_origin_location_proxy_set_receiver<'s>(
@@ -2649,16 +2266,7 @@ fn cross_origin_location_proxy_set_callback<'s>(
return;
};
if !is_cross_origin_location_href_key_value(scope, args.get(1)) {
let receiver = v8::Local::<v8::Object>::try_from(args.get(3)).unwrap_or(target);
rv.set(
v8::Boolean::new(
scope,
target
.set_with_receiver(scope, args.get(1), args.get(2), receiver)
.unwrap_or(false),
)
.into(),
);
throw_cross_origin_location_security_error(scope);
return;
}
let Some(receiver) = cross_origin_location_proxy_set_receiver(scope, target, args.get(3))
@@ -2747,8 +2355,8 @@ fn cross_origin_location_navigate<'s>(
receiver: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Value>,
) -> bool {
if main_page::is_main_location(scope, receiver) {
return main_page::navigate(
if location::is_window_location(scope, receiver) {
return location::navigate(
scope,
receiver,
value,
@@ -2828,7 +2436,7 @@ fn cross_origin_location_replace_callback<'s>(
);
return;
}
if main_page::is_main_location(scope, args.this()) {
if location::is_window_location(scope, args.this()) {
let Some(parsed) = webidl::parse_args::<CrossOriginLocationReplaceArgs>(scope, &args)
else {
return;
@@ -2836,7 +2444,7 @@ fn cross_origin_location_replace_callback<'s>(
let Some(value) = v8_string(scope, &parsed.url) else {
return;
};
main_page::navigate(
location::navigate(
scope,
args.this(),
value.into(),
@@ -0,0 +1,492 @@
//! Native access-check handlers. Static members never retain a target Window
//! or an accessing realm; their functions are V8's per-context template instances.
use super::*;
use crate::context_bootstrap;
use moli_webapi_declare::{WebApiCrossOrigin, WebApiCrossOriginDeclaration};
// Fields are declaration metadata; no runtime object carries this state.
#[allow(dead_code)]
#[derive(WebApiCrossOrigin)]
#[webapi(plain, receiver = context_bootstrap::is_window_receiver)]
pub(super) struct WindowMembers {
#[webapi(accessor_property, getter = context_bootstrap::window_self_value)]
window: (),
#[webapi(accessor_property, name = "self", getter = context_bootstrap::window_self_value)]
self_window: (),
#[webapi(accessor_property, getter = location::window_location, setter = location::set_window_location)]
location: (),
#[webapi(method, callback = context_bootstrap::window_close_callback)]
close: (),
#[webapi(accessor_property, getter = closed_value)]
closed: (),
#[webapi(method, callback = context_bootstrap::window_noop_callback)]
focus: (),
#[webapi(method, callback = context_bootstrap::window_noop_callback)]
blur: (),
#[webapi(accessor_property, getter = context_bootstrap::window_frames_value)]
frames: (),
#[webapi(accessor_property, getter = length_value)]
length: (),
#[webapi(accessor_property, getter = top_value)]
top: (),
#[webapi(accessor_property, getter = context_bootstrap::window_opener_value)]
opener: (),
#[webapi(accessor_property, getter = parent_value)]
parent: (),
#[webapi(method, length = 1, callback = window_host::window_post_message_callback)]
post_message: (),
}
// Fields are declaration metadata; no runtime object carries this state.
#[allow(dead_code)]
#[derive(WebApiCrossOrigin)]
#[webapi(plain, receiver = is_location_receiver)]
pub(super) struct LocationMembers {
#[webapi(accessor_property, setter = set_href_value)]
href: (),
#[webapi(method, length = 1, callback = cross_origin_location_replace_callback)]
replace: (),
}
fn is_location_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
context_bootstrap::is_native_location(scope, receiver)
|| is_cross_origin_location_proxy(scope, receiver)
}
fn set_href_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Value>,
) -> Option<()> {
if !context_bootstrap::navigate_borrowed_location(
scope,
receiver,
value,
context_bootstrap::LocationNavigationKind::Assign,
) {
cross_origin_location_navigate(scope, receiver, value);
}
Some(())
}
/// A parked proxy has no LocalWindow identity yet. Its browsing context still
/// supplies parent/top and children while a replacement document is pending.
struct WindowTarget<'s> {
window: v8::Local<'s, v8::Object>,
context: v8::Local<'s, v8::Context>,
host: *mut JsContextHost,
child: Option<DomHandle>,
closed: bool,
}
impl<'s> WindowTarget<'s> {
fn resolve(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<Self> {
let context = receiver.get_creation_context(scope)?;
let host = crate::util::context_host_ptr_from_context_slot(context)?;
let owner = unsafe { &*host };
let identity = owner.window_execution_context_identity_for_access_check(context);
let current = identity
.filter(|identity| owner.window_execution_context_identity_is_current(*identity));
if current.is_some_and(|identity| {
!owner.window_execution_context_identity_is_default_world(identity)
}) {
return None;
}
let facade = context
.get_slot::<ChildWindowProxyFacadeContextHandle>()
.map(|handle| handle.0);
let child = current
.and_then(|identity| identity.dispatch_scope().child_window())
.or(facade);
let closed = if let Some(handle) = facade {
!owner.child_browsing_context_is_live(handle)
} else if current.is_some()
|| context
.get_slot::<crate::util::MainDefaultWindowContext>()
.is_some()
{
owner.browsing_context_is_closed()
} else {
true
};
Some(Self {
window: context.global(scope),
context,
host,
child,
closed,
})
}
fn children(&self, scope: &mut v8::PinScope<'s, '_>) -> Vec<DomHandle> {
if self.closed {
return Vec::new();
}
let host = unsafe { &mut *self.host };
let document = match self.child {
Some(handle) => host.child_browsing_context_document_handle(handle),
None => Some(host.document_handle()),
};
let scope = &mut v8::ContextScope::new(scope, self.context);
if let Some(document) = document {
host.sync_child_browsing_context_subtree(scope, document);
}
match self.child {
Some(handle) => host
.child_browsing_context_direct_host_handles(handle)
.into_iter()
.filter(|handle| host.child_browsing_context_is_live(*handle))
.collect(),
None => host.top_level_child_browsing_context_handles_in_frame_tree_order(),
}
}
fn indexed(
&self,
scope: &mut v8::PinScope<'s, '_>,
index: u32,
) -> Option<v8::Local<'s, v8::Object>> {
let handle = self.children(scope).get(index as usize).copied()?;
unsafe { &mut *self.host }.child_browsing_context_window_proxy_for_top(scope, handle)
}
fn named(
&self,
scope: &mut v8::PinScope<'s, '_>,
name: &str,
) -> Option<v8::Local<'s, v8::Object>> {
self.children(scope);
if self.closed {
return None;
}
let host = unsafe { &mut *self.host };
let handle = host.child_browsing_context_named_child_handle(self.child, name)?;
host.child_browsing_context_window_proxy_for_top(scope, handle)
}
}
fn closed_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
if let Some(target) = WindowTarget::resolve(scope, receiver) {
return Some(v8::Boolean::new(scope, target.closed).into());
}
context_bootstrap::window_closed_value(scope, receiver)
}
fn length_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
if let Some(context) = receiver.get_creation_context(scope)
&& let Some(handle) = context
.get_slot::<ChildWindowProxyFacadeContextHandle>()
.map(|handle| handle.0)
&& let Some(host) = crate::util::context_host_ptr_from_context_slot(context)
{
return Some(
v8::Number::new(
scope,
unsafe { &mut *host }.child_browsing_context_child_frame_count(handle) as f64,
)
.into(),
);
}
context_bootstrap::window_length_value(scope, receiver)
}
fn parent_top_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
top: bool,
) -> Option<v8::Local<'s, v8::Value>> {
if let Some(context) = receiver.get_creation_context(scope)
&& let Some(handle) = context
.get_slot::<ChildWindowProxyFacadeContextHandle>()
.map(|handle| handle.0)
&& let Some(host) = crate::util::context_host_ptr_from_context_slot(context)
{
let records = &unsafe { &*host }.child_window_proxy_records;
return if top {
records.browsing_context_top(scope, handle)
} else {
records.browsing_context_parent(scope, handle)
}
.map(Into::into);
}
if top {
context_bootstrap::window_top_value(scope, receiver)
} else {
context_bootstrap::window_parent_value(scope, receiver)
}
}
fn parent_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
parent_top_value(scope, receiver, false)
}
fn top_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
parent_top_value(scope, receiver, true)
}
fn member_name(scope: &mut v8::PinScope<'_, '_>, key: v8::Local<'_, v8::Name>) -> Option<String> {
v8::Local::<v8::String>::try_from(key)
.ok()
.map(|key| key.to_rust_string_lossy(scope))
}
pub(super) fn is_fallback_key(
scope: &mut v8::PinScope<'_, '_>,
key: v8::Local<'_, v8::Name>,
) -> bool {
member_name(scope, key).as_deref() == Some("then")
|| [
v8::Symbol::get_to_string_tag(scope),
v8::Symbol::get_has_instance(scope),
v8::Symbol::get_is_concat_spreadable(scope),
]
.into_iter()
.any(|symbol| key.strict_equals(symbol.into()))
}
fn value_descriptor<'s>(
scope: &mut v8::PinScope<'s, '_>,
value: v8::Local<'s, v8::Value>,
enumerable: bool,
) -> Option<v8::Local<'s, v8::Value>> {
CrossOriginPropertyDescriptorDeclaration::new(value, false, enumerable, true)
.bind(scope)
.ok()
.map(Into::into)
}
pub(super) fn child_window_cross_origin_named_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(target) = WindowTarget::resolve(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let name = member_name(scope, key);
if let Some(member) = name
.as_deref()
.and_then(WindowMembers::cross_origin_property)
{
let context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, context);
if let Some(value) = member.get(scope, target.window) {
rv.set(value);
}
return v8::Intercepted::kYes;
}
if let Some(value) = name.as_deref().and_then(|name| target.named(scope, name)) {
rv.set(value.into());
return v8::Intercepted::kYes;
}
if is_fallback_key(scope, key) {
rv.set_undefined();
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
pub(super) fn child_window_cross_origin_named_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: v8::Local<'s, v8::Name>,
value: v8::Local<'s, v8::Value>,
args: v8::PropertyCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
let Some(member) = member_name(scope, key)
.as_deref()
.and_then(WindowMembers::cross_origin_property)
.filter(|member| member.has_setter())
else {
return v8::Intercepted::kNo;
};
let Some(target) = WindowTarget::resolve(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let _ = member.set(scope, target.window, value);
v8::Intercepted::kYes
}
pub(super) fn child_window_cross_origin_named_query<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Integer>,
) -> v8::Intercepted {
let Some(target) = WindowTarget::resolve(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let name = member_name(scope, key);
if name
.as_deref()
.and_then(WindowMembers::cross_origin_property)
.is_some()
|| is_fallback_key(scope, key)
|| name.and_then(|name| target.named(scope, &name)).is_some()
{
rv.set_int32(v8::PropertyAttribute::DONT_ENUM.as_u32() as i32);
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
pub(super) fn child_window_cross_origin_named_enumerator<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
if WindowTarget::resolve(scope, args.holder()).is_none() {
return;
}
let mut names: Vec<v8::Local<v8::Value>> = WindowMembers::cross_origin_properties()
.iter()
.map(|member| v8str(scope, member.name()).into())
.collect();
// Named child access is dynamic, and never creates own properties on the
// target Window. The cross-origin own-key list contains static members.
names.push(v8str(scope, "then").into());
for symbol in [
v8::Symbol::get_to_string_tag(scope),
v8::Symbol::get_has_instance(scope),
v8::Symbol::get_is_concat_spreadable(scope),
] {
names.push(symbol.into());
}
let context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, context);
rv.set(v8::Array::new_with_elements(scope, &names));
}
pub(super) fn child_window_cross_origin_named_descriptor<'s>(
scope: &mut v8::PinScope<'s, '_>,
key: v8::Local<'s, v8::Name>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(target) = WindowTarget::resolve(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let name = member_name(scope, key);
let context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, context);
if let Some(member) = name
.as_deref()
.and_then(WindowMembers::cross_origin_property)
{
if let Some(descriptor) = member.descriptor(scope) {
rv.set(descriptor.into());
}
return v8::Intercepted::kYes;
}
let value = name
.as_deref()
.and_then(|name| target.named(scope, name))
.map(Into::into)
.or_else(|| is_fallback_key(scope, key).then(|| v8::undefined(scope).into()));
if let Some(value) = value {
if let Some(descriptor) = value_descriptor(scope, value, false) {
rv.set(descriptor);
}
} else {
throw_cross_origin_location_security_error(scope);
}
v8::Intercepted::kYes
}
pub(super) fn child_window_cross_origin_indexed_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
if let Some(value) =
WindowTarget::resolve(scope, args.holder()).and_then(|target| target.indexed(scope, index))
{
rv.set(value.into());
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
pub(super) fn child_window_cross_origin_indexed_setter<'s>(
_scope: &mut v8::PinScope<'s, '_>,
_index: u32,
_value: v8::Local<'s, v8::Value>,
_args: v8::PropertyCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Boolean>,
) -> v8::Intercepted {
v8::Intercepted::kNo
}
pub(super) fn child_window_cross_origin_indexed_query<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Integer>,
) -> v8::Intercepted {
if let Some(target) = WindowTarget::resolve(scope, args.holder())
&& (index as usize) < target.children(scope).len()
{
rv.set_int32(v8::PropertyAttribute::READ_ONLY.as_u32() as i32);
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
pub(super) fn child_window_cross_origin_indexed_enumerator<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let Some(target) = WindowTarget::resolve(scope, args.holder()) else {
return;
};
let count = target.children(scope).len() as u32;
if let Some(array) = serialize_v8_iter_array(scope, 0..count) {
rv.set(array);
}
}
pub(super) fn child_window_cross_origin_indexed_descriptor<'s>(
scope: &mut v8::PinScope<'s, '_>,
index: u32,
args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
let Some(target) = WindowTarget::resolve(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
let Some(handle) = target.children(scope).get(index as usize).copied() else {
throw_cross_origin_location_security_error(scope);
return v8::Intercepted::kYes;
};
if let Some(value) =
unsafe { &mut *target.host }.child_browsing_context_window_proxy_for_top(scope, handle)
{
let context = cross_origin_accessing_context(scope);
let scope = &mut v8::ContextScope::new(scope, context);
if let Some(descriptor) = value_descriptor(scope, value.into(), true) {
rv.set(descriptor);
}
}
v8::Intercepted::kYes
}
@@ -0,0 +1,146 @@
use super::*;
const LOCATION: &str = "__moliCrossOriginLocationObject";
const TARGET_WINDOW: &str = "__moliCrossOriginLocationWindow";
const WINDOW_LOCATION_SLOT: &str = "__moliWindowLocation";
fn ensure_shared_location<'s>(
scope: &mut v8::PinScope<'s, '_>,
anchor: v8::Local<'s, v8::Object>,
window: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
if let Some(location) = get_private_value(scope, anchor, LOCATION)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
return Some(location);
}
let target = new_null_prototype_object(scope);
let marker = v8::Boolean::new(scope, true).into();
set_private_value(scope, target, CROSS_ORIGIN_LOCATION_PROXY_SLOT, marker);
set_private_value(scope, target, TARGET_WINDOW, window.into());
install_cross_origin_location_keys(scope, target)?;
let location = wrap_cross_origin_location_proxy(scope, target)?;
set_private_value(
scope,
target,
CROSS_ORIGIN_LOCATION_PROXY_SELF_SLOT,
location.into(),
);
set_private_value(scope, anchor, LOCATION, location.into());
Some(location)
}
fn target_window<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
get_cross_origin_proxy_private_value(scope, receiver, TARGET_WINDOW)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
}
pub(super) fn is_window_location<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
target_window(scope, receiver).is_some()
}
pub(super) fn navigate<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Value>,
kind: crate::context_bootstrap::LocationNavigationKind,
) -> bool {
let Some(window) = target_window(scope, receiver) else {
return false;
};
// The stable WindowProxy follows its successor, but a Location belongs to
// the Window realm that created it. An old Location must not gain the new
// Document's navigation authority through that proxy.
if receiver.get_creation_context(scope) != window.get_creation_context(scope) {
return false;
}
navigate_window(scope, window, value, kind, true)
}
fn navigate_window<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Value>,
kind: crate::context_bootstrap::LocationNavigationKind,
block_javascript: bool,
) -> bool {
let raw = match webidl::convert::<webidl::UsvString>(
scope,
value,
webidl::Context::member("Location", "href"),
) {
Ok(value) => value.0,
Err(error) => {
webidl::throw_error(scope, &error);
return false;
}
};
if block_javascript && url::Url::parse(&raw).is_ok_and(|url| url.scheme() == "javascript") {
throw_cross_origin_location_security_error(scope);
return false;
}
let raw = if kind != crate::context_bootstrap::LocationNavigationKind::Reload {
crate::context_bootstrap::resolve_cross_window_location_target(scope, &raw).unwrap_or(raw)
} else {
raw
};
let Some(context) = window.get_creation_context(scope) else {
return false;
};
let scope = &mut v8::ContextScope::new(scope, context);
let Some(location) = window
.get(scope, v8str(scope, "location").into())
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
else {
return false;
};
crate::context_bootstrap::navigate_location_object_with_child_navigate_event(
scope,
location,
kind,
Some(raw),
);
true
}
fn shared_cross_origin_location<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let context = window.get_creation_context(scope)?;
let scope = &mut v8::ContextScope::new(scope, context);
let anchor = context.get_extras_binding_object(scope);
let window = context.global(scope);
ensure_shared_location(scope, anchor, window)
}
pub(super) fn window_location<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
if caller_can_access_window(scope, window) {
return get_private_value(scope, window, WINDOW_LOCATION_SLOT);
}
shared_cross_origin_location(scope, window).map(Into::into)
}
pub(super) fn set_window_location<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Value>,
) -> Option<()> {
let block_javascript = !caller_can_access_window(scope, window);
navigate_window(
scope,
window,
value,
crate::context_bootstrap::LocationNavigationKind::Assign,
block_javascript,
);
Some(())
}
@@ -1,370 +0,0 @@
use super::*;
use crate::context_bootstrap::{
cached_cross_origin_window_surface, window_closed_getter, window_length_getter,
window_opener_getter, window_parent_getter, window_top_getter,
};
const LOCATION: &str = "__moliMainWindowCrossOriginLocationObject";
const LOCATION_SURFACE: &str = "__moliMainWindowCrossOriginLocationSurface";
const MAIN_LOCATION: &str = "__moliMainWindowCrossOriginLocation";
const INDEXED_COUNT: &str = "__moliMainWindowCrossOriginIndexedCount";
const WINDOW_LOCATION_SLOT: &str = "__moliWindowLocation";
#[derive(WebApiObject)]
#[webapi(plain)]
struct MainWindowCrossOriginSurface {
#[webapi(method, length = 1, callback = window_host::window_post_message_callback, readonly, dont_delete)]
post_message: (),
#[webapi(method, callback = crate::context_bootstrap::window_close_callback, readonly, dont_delete)]
close: (),
#[webapi(method, callback = crate::context_bootstrap::window_noop_callback, readonly, dont_delete)]
focus: (),
#[webapi(method, callback = crate::context_bootstrap::window_noop_callback, readonly, dont_delete)]
blur: (),
#[webapi(accessor_property, getter = window_closed_getter)]
closed: (),
#[webapi(accessor_property, getter = window_length_getter)]
length: (),
#[webapi(accessor_property, getter = window_opener_getter)]
opener: (),
#[webapi(accessor_property, getter = window_parent_getter)]
parent: (),
#[webapi(accessor_property, getter = window_top_getter)]
top: (),
#[webapi(accessor_property, getter = location_getter, setter = location_setter)]
location: (),
}
pub(super) fn access_surface<'s>(
scope: &mut v8::PinScope<'s, '_>,
target_context: v8::Local<'s, v8::Context>,
) -> v8::Local<'s, v8::Object> {
let surface = cached_access_surface(scope, target_context);
refresh_indexed_children(scope, target_context, surface);
surface
}
fn cached_access_surface<'s>(
scope: &mut v8::PinScope<'s, '_>,
target_context: v8::Local<'s, v8::Context>,
) -> v8::Local<'s, v8::Object> {
let accessing_context = cross_origin_accessing_context(scope);
cached_cross_origin_window_surface(scope, target_context, accessing_context, |scope| {
let window = target_context.global(scope);
let location = shared_cross_origin_location(scope, window)
.expect("main Window cross-origin Location should initialize");
build_accessing_surface(scope, window, location)
})
}
fn ensure_shared_location<'s>(
scope: &mut v8::PinScope<'s, '_>,
anchor: v8::Local<'s, v8::Object>,
window: v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
if let Some(location) = get_private_value(scope, anchor, LOCATION)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
{
return location;
}
let target = new_null_prototype_object(scope);
let marker = v8::Boolean::new(scope, true).into();
set_private_value(scope, target, CROSS_ORIGIN_LOCATION_PROXY_SLOT, marker);
set_private_value(scope, target, MAIN_LOCATION, window.into());
// Preserve the target's keys without exposing realm-specific
// functions. Configurable placeholders allow each observer's
// get/descriptor projection to obey Proxy invariants.
let undefined = v8::undefined(scope).into();
for name in CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES
.iter()
.copied()
.chain(["href", "replace", "then"])
{
let key = v8str(scope, name).into();
let _ = target.define_own_property(scope, key, undefined, v8::PropertyAttribute::DONT_ENUM);
}
install_cross_origin_symbol_slots(scope, target, "Location");
let location = wrap_cross_origin_location_proxy(scope, target)
.expect("main Window cross-origin Location proxy should initialize");
set_private_value(
scope,
target,
CROSS_ORIGIN_LOCATION_PROXY_SELF_SLOT,
location.into(),
);
set_private_value(scope, anchor, LOCATION, location.into());
location
}
fn build_accessing_surface<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
location: v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
let surface = new_null_prototype_object(scope);
MainWindowCrossOriginSurface {
post_message: (),
close: (),
focus: (),
blur: (),
closed: (),
length: (),
opener: (),
parent: (),
top: (),
location: (),
}
.initialize(scope, surface)
.expect("main Window cross-origin surface should initialize");
for name in ["window", "self", "frames"] {
set_cross_origin_object_slot(scope, surface, name, window.into());
}
let undefined = v8::undefined(scope).into();
set_cross_origin_object_slot(scope, surface, "then", undefined);
set_private_value(
scope,
surface,
CROSS_ORIGIN_WINDOW_LOCATION_SLOT,
location.into(),
);
install_cross_origin_symbol_slots(scope, surface, "Window");
let location_surface = new_null_prototype_object(scope);
install_cross_origin_symbol_slots(scope, location_surface, "Location");
install_cross_origin_denied_accessors(
scope,
location_surface,
CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES,
);
let setter = v8::Function::builder(cross_origin_location_navigate_setter_callback)
.length(1)
.constructor_behavior(v8::ConstructorBehavior::Throw)
.build(scope)
.expect("cross-origin Location href setter should initialize");
setter.set_name(v8str(scope, "set href"));
let href = v8str(scope, "href").into();
define_get_set_property(
scope,
location_surface,
href,
undefined,
setter.into(),
cross_origin_property_attributes(),
"href",
)
.expect("cross-origin Location href descriptor should initialize");
install_cross_origin_location_methods(scope, location_surface);
set_cross_origin_object_slot(scope, location_surface, "then", undefined);
set_private_value(scope, surface, LOCATION_SURFACE, location_surface.into());
surface
}
pub(super) fn location_access_surface<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let window = main_location_window(scope, receiver)?;
let context = window.get_creation_context(scope)?;
let surface = cached_access_surface(scope, context);
get_private_value(scope, surface, LOCATION_SURFACE)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
}
pub(super) fn is_main_surface<'s>(
scope: &mut v8::PinScope<'s, '_>,
surface: v8::Local<'s, v8::Object>,
) -> bool {
get_private_value(scope, surface, INDEXED_COUNT).is_some()
}
fn refresh_indexed_children<'s>(
scope: &mut v8::PinScope<'s, '_>,
target_context: v8::Local<'s, v8::Context>,
surface: v8::Local<'s, v8::Object>,
) {
let previous = get_private_value(scope, surface, INDEXED_COUNT)
.and_then(|value| value.uint32_value(scope))
.unwrap_or(0);
let scope = &mut v8::ContextScope::new(scope, target_context);
let count = crate::util::context_host_ptr_from_context_slot(target_context)
.filter(|host| !unsafe { &**host }.browsing_context_is_closed())
.map_or(0, |host| {
let host = unsafe { &mut *host };
host.sync_child_browsing_context_subtree(scope, host.document_handle());
let count = host.child_browsing_context_count() as u32;
for index in 0..count {
if let Some(handle) = host.child_browsing_context_handle_by_index(index as usize)
&& let Some(window) =
host.child_browsing_context_window_proxy_for_top(scope, handle)
{
let key = v8_string(scope, &index.to_string()).unwrap();
let _ = surface.define_own_property(
scope,
key.into(),
window.into(),
v8::PropertyAttribute::READ_ONLY,
);
}
}
count
});
for index in count..previous {
let _ = surface.delete_index(scope, index);
}
let count = v8::Integer::new_from_unsigned(scope, count).into();
set_private_value(scope, surface, INDEXED_COUNT, count);
}
fn main_location_window<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
get_cross_origin_proxy_private_value(scope, receiver, MAIN_LOCATION)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
}
pub(super) fn is_main_location<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
main_location_window(scope, receiver).is_some()
}
pub(super) fn navigate<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Value>,
kind: crate::context_bootstrap::LocationNavigationKind,
) -> bool {
let Some(window) = main_location_window(scope, receiver) else {
return false;
};
// The stable WindowProxy follows its successor, but a Location belongs to
// the Window realm that created it. An old Location must not gain the new
// Document's navigation authority through that proxy.
if receiver.get_creation_context(scope) != window.get_creation_context(scope) {
return false;
}
navigate_window(scope, window, value, kind, true)
}
fn navigate_window<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Value>,
kind: crate::context_bootstrap::LocationNavigationKind,
block_javascript: bool,
) -> bool {
let raw = match webidl::convert::<webidl::UsvString>(
scope,
value,
webidl::Context::member("Location", "href"),
) {
Ok(value) => value.0,
Err(error) => {
webidl::throw_error(scope, &error);
return false;
}
};
if block_javascript && url::Url::parse(&raw).is_ok_and(|url| url.scheme() == "javascript") {
throw_cross_origin_location_security_error(scope);
return false;
}
let raw = if kind != crate::context_bootstrap::LocationNavigationKind::Reload {
crate::context_bootstrap::resolve_cross_window_location_target(scope, &raw).unwrap_or(raw)
} else {
raw
};
let Some(context) = window.get_creation_context(scope) else {
return false;
};
let scope = &mut v8::ContextScope::new(scope, context);
let Some(location) = window
.get(scope, v8str(scope, "location").into())
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
else {
return false;
};
crate::context_bootstrap::navigate_location_object_with_child_navigate_event(
scope,
location,
kind,
Some(raw),
);
true
}
fn require_window_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: &v8::FunctionCallbackArguments<'s>,
operation: &str,
name: &str,
) -> Option<v8::Local<'s, v8::Object>> {
let receiver = args.this();
// The internal surface carries the cross-origin location slot, but it is
// not a Window. Callers must pass the real WindowProxy as `this`.
if !is_main_surface(scope, receiver)
&& (crate::context_bootstrap::is_window_receiver(scope, receiver)
|| is_cross_origin_window_proxy(scope, receiver))
{
return Some(receiver);
}
throw_type_error(
scope,
&format!("Failed to {operation} the '{name}' property on 'Window': Illegal invocation"),
);
None
}
fn shared_cross_origin_location<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let context = window.get_creation_context(scope)?;
let scope = &mut v8::ContextScope::new(scope, context);
let anchor = context.get_extras_binding_object(scope);
let window = context.global(scope);
Some(ensure_shared_location(scope, anchor, window))
}
fn location_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(window) = require_window_receiver(scope, &args, "get", "location") else {
return;
};
if caller_can_access_window(scope, window) {
if let Some(location) = get_private_value(scope, window, WINDOW_LOCATION_SLOT) {
rv.set(location);
return;
}
throw_type_error(
scope,
"Failed to get the 'location' property on 'Window': Illegal invocation",
);
return;
}
if let Some(location) = shared_cross_origin_location(scope, window) {
rv.set(location.into());
}
}
fn location_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let Some(window) = require_window_receiver(scope, &args, "set", "location") else {
return;
};
let block_javascript = !caller_can_access_window(scope, window);
navigate_window(
scope,
window,
args.get(0),
crate::context_bootstrap::LocationNavigationKind::Assign,
block_javascript,
);
}
@@ -1211,7 +1211,7 @@ frame.src = "data:text/html,<body>cross-origin</body>";
assert_eq!(
result,
"true|true|true|[object Window]|SecurityError:true|SecurityError:true|SecurityError:true|SecurityError:true|SecurityError:true"
"true|true|true|[object Object]|SecurityError:true|SecurityError:true|SecurityError:true|SecurityError:true|SecurityError:true"
);
}
#[test]
@@ -2859,6 +2859,11 @@ for (const tag of ['iframe', 'embed', 'object']) {
}
async fn commit_opaque_child_for_test(vm: &mut StandaloneScriptVmHarness, element_id: &str) -> i64 {
let previous: std::collections::HashSet<_> = vm
.live_child_default_runtime_realm_inventory()
.into_iter()
.map(|realm| realm.context_id)
.collect();
let created = vm
.eval(&format!(
r#"
@@ -2881,7 +2886,7 @@ async fn commit_opaque_child_for_test(vm: &mut StandaloneScriptVmHarness, elemen
vm.live_child_default_runtime_realm_inventory()
.into_iter()
.map(|realm| realm.context_id)
.next()
.find(|id| !previous.contains(id))
.unwrap_or_else(|| panic!("{element_id}: opaque child realm should exist"))
}
@@ -2984,7 +2989,7 @@ async fn borrowed_cross_origin_window_accessors_follow_the_receiver() {
}
#[tokio::test]
async fn cross_origin_surface_cache_releases_removed_accessor_realm() {
async fn native_cross_origin_bindings_release_removed_accessor_realm() {
let mut vm = new_storage_test_vm("https://cross-origin-surface-cache.test/");
let control_id = commit_opaque_child_for_test(&mut vm, "control-frame").await;
@@ -3012,8 +3017,16 @@ document.getElementById("control-frame").remove();
let accessor_id = commit_opaque_child_for_test(&mut vm, "accessor-frame").await;
expose_real_top_window_for_test(&mut vm, accessor_id);
assert_eq!(
vm.eval_in_child_default_context(accessor_id, "String(__realTopWindow.closed)")
.expect("cross-origin closed read should evaluate"),
vm.eval_in_child_default_context(
accessor_id,
r#"
void __realTopWindow.postMessage;
void Object.getOwnPropertyDescriptor(__realTopWindow, 'parent').get;
void __realTopWindow.location;
String(__realTopWindow.closed)
"#
)
.expect("cross-origin closed read should evaluate"),
"false"
);
let accessor_weak = child_context_weak_for_test(&mut vm, accessor_id);
@@ -3039,7 +3052,7 @@ document.getElementById("accessor-frame").remove();
}
#[tokio::test]
async fn cross_origin_window_cache_ignores_author_weak_map_overrides() {
async fn native_cross_origin_bindings_ignore_author_weak_map_overrides() {
let mut vm = new_storage_test_vm("https://cross-origin-cache-intrinsics.test/");
let child_id = commit_opaque_child_for_test(&mut vm, "observer").await;
expose_real_top_window_for_test(&mut vm, child_id);
@@ -3170,6 +3183,73 @@ async fn cross_origin_ancestor_search_reaches_the_actual_top_window() {
assert_eq!(result, "[true,2,true,true]");
}
#[tokio::test]
async fn cross_origin_functions_share_the_observer_realm_and_follow_borrowed_receivers() {
let mut vm = new_storage_test_vm("https://cross-origin-native-members.test/");
let observer = commit_opaque_child_for_test(&mut vm, "observer").await;
commit_opaque_child_for_test(&mut vm, "target").await;
let result = vm.eval_in_child_default_context(observer, r#"
(() => {
const a = top, b = top[1];
const descriptor = Object.getOwnPropertyDescriptor(a, 'parent');
const locationA = a.location, locationB = b.location;
const href = Object.getOwnPropertyDescriptor(locationA, 'href');
const names = ['window', 'self', 'location', 'close', 'closed', 'focus', 'blur', 'frames', 'length', 'top', 'opener', 'parent', 'postMessage'];
function throwsTypeError(callback) { try { callback(); } catch (e) { return e instanceof TypeError; } return false; }
return JSON.stringify({
methodIdentity: a.postMessage === b.postMessage && a.postMessage === a.postMessage,
getterIdentity: descriptor.get === Object.getOwnPropertyDescriptor(b, 'parent').get,
locationIdentity: locationA === a.location && locationB === b.location && locationA !== locationB,
replaceIdentity: locationA.replace === locationB.replace,
realm: [a.postMessage, descriptor.get, href.set, locationA.replace].every(fn => Object.getPrototypeOf(fn) === Function.prototype),
borrowed: descriptor.get.call(window) === parent,
invalidReceiver: throwsTypeError(() => descriptor.get.call({})) && throwsTypeError(() => a.postMessage.call({})) && throwsTypeError(() => href.set.call({}, '#bad')),
metadata: descriptor.get.name === 'get parent' && descriptor.get.length === 0 && href.set.name === 'set href' && href.set.length === 1 && a.postMessage.length === 1 && !Object.hasOwn(a.postMessage, 'prototype'),
descriptors: names.every(name => { const d = Object.getOwnPropertyDescriptor(a, name); return d.configurable && !d.enumerable; }) && href.get === undefined && href.configurable && !href.enumerable,
keys: Object.getOwnPropertyNames(locationA).join(',') === 'href,replace,then',
fallbacks: a.then === undefined && a[Symbol.hasInstance] === undefined && locationA.then === undefined
});
})()
"#).expect("native cross-origin member projection");
assert_eq!(
result,
r#"{"methodIdentity":true,"getterIdentity":true,"locationIdentity":true,"replaceIdentity":true,"realm":true,"borrowed":true,"invalidReceiver":true,"metadata":true,"descriptors":true,"keys":true,"fallbacks":true}"#
);
}
#[tokio::test]
async fn cross_origin_named_children_precede_fallbacks_and_refresh_after_removal() {
let mut vm = new_storage_test_vm("https://cross-origin-named-fallback.test/");
let observer = commit_opaque_child_for_test(&mut vm, "observer").await;
vm.eval(
r#"
const named = document.createElement('iframe');
named.id = 'named-then';
named.name = 'then';
document.body.appendChild(named);
void named.contentWindow;
"#,
)
.unwrap();
let result = vm.eval_in_child_default_context(observer, r#"
(() => {
const descriptor = Object.getOwnPropertyDescriptor(top, 'then');
const errors = ['unknownProperty', '4294967294'].map(name => {
try { Object.getOwnPropertyDescriptor(top, name); return 'accepted'; }
catch (error) { return error.name + ':' + (error instanceof DOMException); }
});
return JSON.stringify([top.then === top[1], descriptor.value === top[1], !descriptor.writable && !descriptor.enumerable && descriptor.configurable, errors]);
})()
"#).unwrap();
assert_eq!(
result,
r#"[true,true,true,["SecurityError:true","SecurityError:true"]]"#
);
vm.eval("document.getElementById('named-then').remove()")
.unwrap();
assert_eq!(vm.eval_in_child_default_context(observer, "top.then === undefined && Object.getOwnPropertyDescriptor(top, 'then').value === undefined").unwrap(), "true");
}
#[test]
fn frame_owner_content_accessors_live_on_exact_owner_prototypes() {
let mut vm = new_storage_html_test_vm("https://frame-owner-content-accessors.test/");
@@ -164,13 +164,6 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[
DirectCallOwner::BrowserAlgorithm,
),
// Captured native intrinsics, DOM/native forwarding, and compiled jobs.
// These two calls invoke WeakMap.get/set captured before author code runs.
// No page-supplied callback or mutable prototype method reaches this cache.
allowed(
"context_bootstrap/window_accessors/cross_origin_cache.rs",
2,
DirectCallOwner::NativeForwardingOrScript,
),
allowed(
"context_bootstrap/constructors/document_nodes.rs",
1,