diff --git a/moli-core/tests/history_child.rs b/moli-core/tests/history_child.rs index b12099b810..7ab926a6b6 100644 --- a/moli-core/tests/history_child.rs +++ b/moli-core/tests/history_child.rs @@ -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(), ), "{}", diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index b5b590251b..da313ea20f 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -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; diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index 08559fa5af..7921602207 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors.rs index e5ac1a5ebe..0f899e5fc1 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors.rs @@ -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, +}; diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs index 4d27b72656..ca9f30eee1 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs @@ -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>( diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/cross_origin_cache.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/cross_origin_cache.rs deleted file mode 100644 index 0016d0298a..0000000000 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/cross_origin_cache.rs +++ /dev/null @@ -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::::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::::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::::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::::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::::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::::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::::try_from(existing) { - return surface; - } - let surface = build(scope); - set.call(scope, map.into(), &[key.into(), surface.into()]) - .expect("intrinsic WeakMap.set"); - surface -} diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs index 0b86f5a810..e21a22d3a9 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs @@ -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); + } } diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/values.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/values.rs new file mode 100644 index 0000000000..a6645e2e7e --- /dev/null +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/values.rs @@ -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> { + 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> { + 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> { + 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> { + 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> { + 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)) +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs index 54483e1d01..b559a13d97 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs @@ -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(()) } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs index 1da548f056..de005c2e91 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/initialize.rs @@ -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< diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/model.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/model.rs index d1c46076e5..5db4ecbef3 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/model.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/realm_state/model.rs @@ -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>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs index a236042f2f..d2639652da 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs @@ -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>, live_window_exposed_to_top: bool, cross_origin_window_proxy: Option>, - cross_origin_access_surface: Option>, default_execution_context_id: Option, } @@ -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::::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> { + let mut keys: Vec> = 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> { - 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::() - .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::()? - .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::::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> { - // 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::::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::::try_from(args.get(0)) else { - rv.set_undefined(); + let Ok(key) = v8::Local::::try_from(args.get(1)) else { return; }; - let receiver = v8::Local::::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::::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::::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::::try_from(args.get(0)) else { - return; - }; let Ok(key) = v8::Local::::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::::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::::try_from(args.get(1)) else { return; }; - if main_surface.is_some() - && let Ok(descriptor) = v8::Local::::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::::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::::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::(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(), diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/cross_origin_bindings.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/cross_origin_bindings.rs new file mode 100644 index 0000000000..1aaa49d172 --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/cross_origin_bindings.rs @@ -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, + closed: bool, +} + +impl<'s> WindowTarget<'s> { + fn resolve( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + ) -> Option { + 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::() + .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::() + .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 { + 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> { + 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> { + 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> { + 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> { + if let Some(context) = receiver.get_creation_context(scope) + && let Some(handle) = context + .get_slot::() + .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> { + if let Some(context) = receiver.get_creation_context(scope) + && let Some(handle) = context + .get_slot::() + .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> { + parent_top_value(scope, receiver, false) +} +fn top_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, +) -> Option> { + parent_top_value(scope, receiver, true) +} + +fn member_name(scope: &mut v8::PinScope<'_, '_>, key: v8::Local<'_, v8::Name>) -> Option { + v8::Local::::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> { + 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> = 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 +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/location.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/location.rs new file mode 100644 index 0000000000..c19154c2be --- /dev/null +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/location.rs @@ -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> { + if let Some(location) = get_private_value(scope, anchor, LOCATION) + .and_then(|value| v8::Local::::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> { + get_cross_origin_proxy_private_value(scope, receiver, TARGET_WINDOW) + .and_then(|value| v8::Local::::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::( + 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::::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> { + 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> { + 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(()) +} diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs deleted file mode 100644 index 75e5cbef93..0000000000 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs +++ /dev/null @@ -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::::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> { - 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::::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> { - get_cross_origin_proxy_private_value(scope, receiver, MAIN_LOCATION) - .and_then(|value| v8::Local::::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::( - 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::::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> { - 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> { - 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, - ); -} diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs index a2c9d1353c..68dcd0d5dc 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/window_and_frame_surfaces.rs @@ -1211,7 +1211,7 @@ frame.src = "data:text/html,cross-origin"; 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] diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs index 3d83eda2fd..ded4db0b0f 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs @@ -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/"); diff --git a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs index fb64bae1d0..ffc7c85a23 100644 --- a/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs +++ b/moli-renderer-v8/src/webidl_callback_source_boundary_tests.rs @@ -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,