fix(runtime): recheck retained Location origin-domain access

Keep each live window's Location identity while rechecking protected access and cached members against Window origin policy. Use native receiver brands and caller-realm cross-origin descriptors while preserving href and replace navigation.

Cover both document.domain transition directions, cross-origin reflection and native Proxy identity. Record five newly passing WPT pages.

Validation: cargo fmt, strict workspace clippy, and full nextest (19,506 passed; 13 skipped). Related WPT: 48/50 passed, with the remaining failure and timeout unchanged from baseline.
This commit is contained in:
ldm0
2026-09-22 21:47:38 +08:00
parent c2c671e1ce
commit ab2a59ca2c
21 changed files with 900 additions and 52 deletions
@@ -2733,7 +2733,6 @@ html/browsers/history/the-location-interface/assign_before_load.html
html/browsers/history/the-location-interface/location_hash.html
html/browsers/history/the-location-interface/reload_document_write_onload.html
html/browsers/history/the-location-interface/same-hash.html
html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html
html/browsers/origin/cross-origin-objects/window-location-and-location-href-cross-realm-set.html
html/browsers/origin/relaxing-the-same-origin-restriction/document_domain_setter.html
html/browsers/the-window-object/accessing-other-browsing-contexts/indexed-browsing-contexts-02.html
@@ -6398,6 +6398,7 @@ html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/
html/browsers/history/the-history-interface/pushstate-replacestate-empty-string/replacestate.html
html/browsers/history/the-history-interface/traverse-during-beforeunload.html
html/browsers/history/the-history-interface/traverse-during-unload.html
html/browsers/history/the-location-interface/allow_prototype_cycle_through_location.sub.html
html/browsers/history/the-location-interface/assign-replace-from-iframe.html
html/browsers/history/the-location-interface/assign-replace-from-top-to-nested-iframe.html
html/browsers/history/the-location-interface/assign-with-nested-iframe.html
@@ -6413,6 +6414,9 @@ html/browsers/history/the-location-interface/location-protocol-setter-non-broken
html/browsers/history/the-location-interface/location-protocol-setter-sameish.html
html/browsers/history/the-location-interface/location-protocol-setter.html
html/browsers/history/the-location-interface/location-prototype-no-toString-valueOf.html
html/browsers/history/the-location-interface/location-prototype-setting-cross-origin-domain.sub.html
html/browsers/history/the-location-interface/location-prototype-setting-cross-origin.sub.html
html/browsers/history/the-location-interface/location-prototype-setting-goes-cross-origin-domain.sub.html
html/browsers/history/the-location-interface/location-prototype-setting-same-origin.html
html/browsers/history/the-location-interface/location-stringifier.html
html/browsers/history/the-location-interface/location-symbol-toprimitive.html
@@ -6436,6 +6440,7 @@ html/browsers/history/the-location-interface/location_search.html
html/browsers/history/the-location-interface/reload_document_write.html
html/browsers/history/the-location-interface/replace-with-nested-iframe.html
html/browsers/history/the-location-interface/security_location_0.htm
html/browsers/origin/cross-origin-objects/cross-origin-due-to-document-domain-only.html
html/browsers/origin/cross-origin-objects/cross-origin-objects-function-caching.html
html/browsers/origin/cross-origin-objects/cross-origin-objects-function-length.html
html/browsers/origin/cross-origin-objects/cross-origin-objects-function-name.html
File diff suppressed because one or more lines are too long
@@ -76,6 +76,7 @@ mod navigation_traversal;
mod navigation_traversal_execution;
mod navigation_traversal_plan;
mod navigation_window;
pub(crate) use navigation_window::window_location_for_holder;
mod navigator_runtime;
#[cfg(test)]
pub(crate) use navigator_runtime::{
@@ -5,6 +5,7 @@ use super::location_navigation::{
use super::navigation_callbacks::{document_location_getter, document_location_setter};
use super::*;
mod access;
mod helpers;
mod install;
mod methods;
@@ -12,6 +13,8 @@ mod navigation;
mod slots;
mod surface;
pub(in crate::context_bootstrap) use access::{location_target, wrap_location_object};
pub(super) use install::{
build_location_constructor_template, build_location_runtime_object,
install_location_runtime_state, location_belongs_to_current_local_window,
@@ -0,0 +1,447 @@
use super::*;
use crate::native_bridge::window_contexts_allow_access;
use crate::util::{get_private_value, new_null_prototype_object, set_private_value};
use crate::web_api_interfaces;
use moli_webapi_declare::WebApiObject;
use std::{cell::RefCell, rc::Rc};
const SURFACE_TARGET_SLOT: &str = "__moliLocationCrossOriginTarget";
const REFLECT_SET_SLOT: &str = "__moliLocationReflectSet";
#[derive(WebApiObject)]
#[webapi(plain)]
struct LocationProxyHandler<'s> {
reflect_set: v8::Local<'s, v8::Function>,
#[webapi(method, callback = location_proxy_set, data = self.reflect_set, length = 4)]
set: (),
#[webapi(method, callback = location_proxy_set_prototype, length = 2)]
set_prototype_of: (),
#[webapi(method, callback = location_proxy_prevent_extensions, length = 1)]
prevent_extensions: (),
}
pub(in crate::context_bootstrap) fn location_target<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
moli_webapi_declare::web_api_object_target(scope, object).unwrap_or(object)
}
pub(in crate::context_bootstrap) fn wrap_location_object<'s>(
scope: &mut v8::PinScope<'s, '_>,
target: v8::Local<'s, v8::Object>,
) -> anyhow::Result<v8::Local<'s, v8::Object>> {
let global = scope.get_current_context().global(scope);
let reflect_set = if let Some(function) = get_private_value(scope, global, REFLECT_SET_SLOT)
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
{
function
} else {
// The first Location is installed before author script. Reuse this
// intrinsic when navigation subsequently creates another Location.
let reflect = global
.get(scope, v8str(scope, "Reflect").into())
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
.ok_or_else(|| anyhow!("missing Reflect during Location bootstrap"))?;
let function = reflect
.get(scope, v8str(scope, "set").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
.ok_or_else(|| anyhow!("missing Reflect.set during Location bootstrap"))?;
set_private_value(scope, global, REFLECT_SET_SLOT, function.into());
function
};
let handler = LocationProxyHandler {
reflect_set,
set: (),
set_prototype_of: (),
prevent_extensions: (),
}
.bind(scope)?;
if handler.set_prototype(scope, v8::null(scope).into()) != Some(true) {
return Err(anyhow!("failed to initialize Location proxy handler"));
}
// Property access, descriptors and own keys go directly to V8's checked
// target. A shadow target cannot hide non-configurable author expandos
// after an origin change without violating JavaScript Proxy invariants.
let proxy = v8::Proxy::new(scope, target, handler)
.ok_or_else(|| anyhow!("failed to create Location proxy"))?;
moli_webapi_declare::register_web_api_proxy(scope, proxy)?;
Ok(proxy.into())
}
fn location_proxy_set<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Ok(target) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
return;
};
let caller = scope
.get_incumbent_context()
.unwrap_or_else(|| scope.get_current_context());
let scope = &mut v8::ContextScope::new(scope, caller);
if target
.get_creation_context(scope)
.is_some_and(|owner| window_contexts_allow_access(caller, owner))
{
let Ok(reflect_set) = v8::Local::<v8::Function>::try_from(args.data()) else {
return;
};
if let Some(value) = reflect_set.call(
scope,
v8::undefined(scope).into(),
&[target.into(), args.get(1), args.get(2), args.get(3)],
) {
rv.set(value);
}
return;
}
let Ok(key) = v8::Local::<v8::Name>::try_from(args.get(1)) else {
return;
};
if key != v8str(scope, "href") {
crate::native_bridge::throw_cross_origin_location_security_error(scope);
return;
}
let Some(surface) = surface_for(scope, target) else {
return;
};
let Some(descriptor) = surface
.get_own_property_descriptor(scope, key)
.and_then(|value| v8::Local::<v8::Object>::try_from(value).ok())
else {
return;
};
let Some(setter) = descriptor
.get(scope, v8str(scope, "set").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
else {
return;
};
// Preserve Reflect.set's receiver. Interceptor callbacks only expose the
// holder, and must not substitute it for a forged or author Proxy receiver.
if setter.call(scope, args.get(3), &[args.get(2)]).is_some() {
rv.set_bool(true);
}
}
fn location_proxy_set_prototype<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let Ok(target) = v8::Local::<v8::Object>::try_from(args.get(0)) else {
return;
};
let caller = scope
.get_incumbent_context()
.unwrap_or_else(|| scope.get_current_context());
let scope = &mut v8::ContextScope::new(scope, caller);
if !target
.get_creation_context(scope)
.is_some_and(|owner| window_contexts_allow_access(caller, owner))
{
rv.set_bool(args.get(1).is_null());
return;
}
if let Some(prototype) = target.get_prototype(scope) {
rv.set_bool(prototype.strict_equals(args.get(1)));
}
}
fn location_proxy_prevent_extensions<'s>(
_scope: &mut v8::PinScope<'s, '_>,
_args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
rv.set_bool(false);
}
// Weak entries do not retain removed frames. Each cached function keeps its
// surface alive through callback data, so keeping either function preserves
// both descriptor identities, as required by CrossOriginPropertyDescriptorMap.
#[derive(Default, Clone)]
struct CrossOriginSurfaces(Rc<RefCell<Vec<CrossOriginSurface>>>);
struct CrossOriginSurface {
context: v8::Weak<v8::Context>,
target: v8::Weak<v8::Object>,
surface: v8::Weak<v8::Object>,
}
#[derive(WebApiObject)]
#[webapi(plain, receiver = web_api_interfaces::Location::is_instance)]
struct CrossOriginSurfaceDeclaration<'s> {
surface: v8::Local<'s, v8::Object>,
#[webapi(method = "set href", callback = cross_origin_href_setter, data = self.surface, length = 1)]
href: (),
#[webapi(method, callback = super::methods::location_replace_callback, data = self.surface, length = 1, readonly)]
replace: (),
}
pub(super) fn install_access_check(template: v8::Local<'_, v8::ObjectTemplate>) {
template.set_security_token_access_check_and_handlers(
location_access_check,
v8::NamedPropertyHandlerConfiguration::new()
.getter(cross_origin_getter)
.query(cross_origin_query)
.descriptor(cross_origin_descriptor)
.enumerator(cross_origin_enumerator),
v8::IndexedPropertyHandlerConfiguration::new()
.getter(cross_origin_indexed_getter)
.descriptor(cross_origin_indexed_descriptor)
.enumerator(cross_origin_indexed_enumerator),
);
}
unsafe extern "C" fn location_access_check(
accessing_context: v8::Local<'_, v8::Context>,
object: v8::Local<'_, v8::Object>,
_data: v8::Local<'_, v8::Value>,
) -> bool {
let scope = std::pin::pin!(unsafe { v8::CallbackScope::new(accessing_context) });
let scope = &mut scope.init();
object
.get_creation_context(scope)
.is_some_and(|owner| window_contexts_allow_access(accessing_context, owner))
}
pub(super) fn require_same_origin(
scope: &mut v8::PinScope<'_, '_>,
object: v8::Local<'_, v8::Object>,
) -> bool {
let current = scope.get_current_context();
if object
.get_creation_context(scope)
.is_some_and(|owner| window_contexts_allow_access(current, owner))
{
return true;
}
crate::native_bridge::throw_dom_exception(
scope,
"SecurityError",
18,
"Blocked access to a cross-origin Location.",
);
false
}
pub(super) fn require_entry_origin<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> bool {
if !super::install::location_belongs_to_current_local_window(scope, object) {
return true;
}
let entry = scope.get_entered_or_microtask_context();
if object
.get_creation_context(scope)
.is_some_and(|owner| window_contexts_allow_access(entry, owner))
{
return true;
}
crate::native_bridge::throw_dom_exception(
scope,
"SecurityError",
18,
"Blocked access to a cross-origin Location.",
);
false
}
fn surface_for<'s>(
scope: &mut v8::PinScope<'s, '_>,
target: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let cache = scope
.get_slot::<CrossOriginSurfaces>()
.cloned()
.unwrap_or_else(|| {
let cache = CrossOriginSurfaces::default();
scope.set_slot(cache.clone());
cache
});
let context = scope.get_current_context();
{
let mut entries = cache.0.borrow_mut();
entries.retain(|entry| {
!entry.context.is_empty() && !entry.target.is_empty() && !entry.surface.is_empty()
});
for entry in entries.iter() {
if entry.context.to_local(scope) == Some(context)
&& entry.target.to_local(scope) == Some(target)
{
return entry.surface.to_local(scope);
}
}
}
let surface = new_null_prototype_object(scope);
set_private_value(scope, surface, SURFACE_TARGET_SLOT, target.into());
CrossOriginSurfaceDeclaration {
surface,
href: (),
replace: (),
}
.initialize(scope, surface)
.ok()?;
let setter_name = v8str(scope, "set href");
let setter = surface.get(scope, setter_name.into())?;
surface.delete(scope, setter_name.into())?;
crate::definitions::define_get_set_property(
scope,
surface,
v8str(scope, "href").into(),
v8::undefined(scope).into(),
setter,
v8::PropertyAttribute::DONT_ENUM,
"href",
)
.ok()?;
// initialize() binds the declaration prototype; the cache object must not
// inherit page-defined getters or property descriptor fields.
surface.set_prototype(scope, v8::null(scope).into())?;
for key in fallback_keys(scope) {
surface.define_own_property(
scope,
key,
v8::undefined(scope).into(),
v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_ENUM,
)?;
}
cache.0.borrow_mut().push(CrossOriginSurface {
context: v8::Weak::new(scope, context),
target: v8::Weak::new(scope, target),
surface: v8::Weak::new(scope, surface),
});
Some(surface)
}
fn fallback_keys<'s>(scope: &mut v8::PinScope<'s, '_>) -> [v8::Local<'s, v8::Name>; 4] {
[
v8str(scope, "then").into(),
v8::Symbol::get_to_string_tag(scope).into(),
v8::Symbol::get_has_instance(scope).into(),
v8::Symbol::get_is_concat_spreadable(scope).into(),
]
}
fn cross_origin_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 {
if key == v8str(scope, "href") {
return v8::Intercepted::kNo;
}
let Some(surface) = surface_for(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
if surface.has_own_property(scope, key) == Some(true)
&& let Some(value) = surface.get(scope, key.into())
{
rv.set(value);
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
fn cross_origin_href_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
let value = match crate::webidl::convert::<crate::webidl::UsvString>(
scope,
args.get(0),
crate::webidl::Context::member("Location", "href"),
) {
Ok(value) => value.0,
Err(error) => {
crate::webidl::throw_error(scope, &error);
return;
}
};
navigate_location_object(
scope,
args.this(),
LocationNavigationKind::Assign,
Some(value),
);
}
fn cross_origin_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(surface) = surface_for(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
if surface.has_own_property(scope, key) == Some(true)
&& let Some(attributes) = surface.get_property_attributes(scope, key.into())
{
rv.set_int32(attributes.as_u32() as i32);
return v8::Intercepted::kYes;
}
v8::Intercepted::kNo
}
fn cross_origin_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(surface) = surface_for(scope, args.holder()) else {
return v8::Intercepted::kNo;
};
if surface.has_own_property(scope, key) == Some(true)
&& let Some(descriptor) = surface.get_own_property_descriptor(scope, key)
{
rv.set(descriptor);
return v8::Intercepted::kYes;
}
// Declining here lets V8 expose an ordinary own descriptor on the target.
crate::native_bridge::throw_cross_origin_location_security_error(scope);
v8::Intercepted::kYes
}
fn cross_origin_enumerator<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
let mut keys = vec![v8str(scope, "href").into(), v8str(scope, "replace").into()];
keys.extend(fallback_keys(scope).map(v8::Local::<v8::Value>::from));
rv.set(v8::Array::new_with_elements(scope, &keys));
}
fn cross_origin_indexed_getter<'s>(
_scope: &mut v8::PinScope<'s, '_>,
_index: u32,
_args: v8::PropertyCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
v8::Intercepted::kNo
}
fn cross_origin_indexed_enumerator<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::PropertyCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Array>,
) {
rv.set(v8::Array::new(scope, 0));
}
fn cross_origin_indexed_descriptor<'s>(
scope: &mut v8::PinScope<'s, '_>,
_index: u32,
_args: v8::PropertyCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) -> v8::Intercepted {
crate::native_bridge::throw_cross_origin_location_security_error(scope);
v8::Intercepted::kYes
}
@@ -40,7 +40,7 @@ enum LocationAttribute {
}
#[derive(Default, WebApiObject)]
#[webapi(interface = web_api_interfaces::Location)]
#[webapi(interface = web_api_interfaces::Location, receiver)]
struct LocationOwnSurfaceDeclaration {
#[webapi(
accessor_property,
@@ -209,6 +209,7 @@ fn configure_location_instance_template(
.flags(v8::PropertyHandlerFlags::ONLY_INTERCEPT_STRINGS),
);
template.set_immutable_proto();
super::access::install_access_check(template);
}
pub(in crate::context_bootstrap) fn build_location_runtime_object<'s>(
@@ -231,6 +232,7 @@ pub(in crate::context_bootstrap) fn install_location_runtime_state<'s>(
location: v8::Local<'s, v8::Object>,
href: &str,
) -> Result<()> {
let location = super::access::location_target(scope, location);
sync_location_object_fields(scope, location, href);
// Location's legacy-unforgeable own properties are non-configurable.
// Window resets refresh the backing slots on the existing object without
@@ -439,6 +441,11 @@ fn location_attribute_getter<'s>(
attribute: LocationAttribute,
rv: &mut v8::ReturnValue<'_, v8::Value>,
) {
if !super::access::require_same_origin(scope, holder)
|| !super::access::require_entry_origin(scope, holder)
{
return;
}
let Some(current_href) = require_location_href_slot(scope, holder) else {
return;
};
@@ -522,10 +529,20 @@ fn location_writable_attribute_setter_callback<'s>(
rv.set_undefined();
return;
};
let holder = args.this();
if !matches!(attribute, LocationAttribute::Href)
&& !super::access::require_same_origin(scope, holder)
{
return;
}
let Some(value) = v8_value_to_string(scope, args.get(0)) else {
return;
};
let holder = args.this();
if !matches!(attribute, LocationAttribute::Href)
&& !super::access::require_entry_origin(scope, holder)
{
return;
}
if require_location_href_slot(scope, holder).is_none() {
return;
}
@@ -21,10 +21,15 @@ pub(super) fn location_assign_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
if !super::access::require_same_origin(scope, args.this()) {
return;
}
let Some(parsed) = webidl::parse_args::<LocationAssignArgs>(scope, &args) else {
return;
};
if require_location_href_slot(scope, args.this()).is_none() {
if !super::access::require_entry_origin(scope, args.this())
|| require_location_href_slot(scope, args.this()).is_none()
{
return;
}
navigate_location_object(
@@ -59,7 +64,10 @@ pub(super) fn location_reload_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
if require_location_href_slot(scope, args.this()).is_none() {
if !super::access::require_same_origin(scope, args.this())
|| !super::access::require_entry_origin(scope, args.this())
|| require_location_href_slot(scope, args.this()).is_none()
{
return;
}
navigate_location_object_with_child_navigate_event(
@@ -75,6 +83,11 @@ pub(super) fn location_to_string_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !super::access::require_same_origin(scope, args.this())
|| !super::access::require_entry_origin(scope, args.this())
{
return;
}
let Some(href) = require_location_href_slot(scope, args.this()) else {
return;
};
@@ -10,6 +10,7 @@ pub(super) fn location_ancestor_origins_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let object = super::access::location_target(scope, object);
get_private_object(scope, object, LOCATION_ANCESTOR_ORIGINS_SLOT)
}
@@ -18,6 +19,7 @@ pub(super) fn set_location_ancestor_origins_slot<'s>(
object: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Object>,
) {
let object = super::access::location_target(scope, object);
set_private_value(scope, object, LOCATION_ANCESTOR_ORIGINS_SLOT, value.into());
}
@@ -25,6 +27,7 @@ pub(super) fn clear_location_ancestor_origins_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) {
let object = super::access::location_target(scope, object);
let undefined = v8::undefined(scope);
set_private_value(
scope,
@@ -38,6 +41,7 @@ pub(super) fn location_empty_ancestor_origins_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let object = super::access::location_target(scope, object);
get_private_object(scope, object, LOCATION_EMPTY_ANCESTOR_ORIGINS_SLOT)
}
@@ -46,6 +50,7 @@ pub(super) fn set_location_empty_ancestor_origins_slot<'s>(
object: v8::Local<'s, v8::Object>,
value: v8::Local<'s, v8::Object>,
) {
let object = super::access::location_target(scope, object);
set_private_value(
scope,
object,
@@ -58,6 +63,7 @@ pub(super) fn location_relevant_document_id_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<u64> {
let object = super::access::location_target(scope, object);
let value = get_private_value(scope, object, LOCATION_RELEVANT_DOCUMENT_ID_SLOT)?;
let value = v8::Local::<v8::BigInt>::try_from(value).ok()?;
let (document_id, lossless) = value.u64_value();
@@ -69,6 +75,7 @@ pub(super) fn set_location_relevant_document_id_slot<'s>(
object: v8::Local<'s, v8::Object>,
document_id: u64,
) {
let object = super::access::location_target(scope, object);
let value = v8::BigInt::new_from_u64(scope, document_id);
set_private_value(
scope,
@@ -82,6 +89,7 @@ pub(super) fn location_relevant_local_window_id_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<u64> {
let object = super::access::location_target(scope, object);
let value = get_private_value(scope, object, LOCATION_RELEVANT_LOCAL_WINDOW_ID_SLOT)?;
let value = v8::Local::<v8::BigInt>::try_from(value).ok()?;
let (local_window_id, lossless) = value.u64_value();
@@ -93,6 +101,7 @@ pub(super) fn set_location_relevant_local_window_id_slot<'s>(
object: v8::Local<'s, v8::Object>,
local_window_id: u64,
) {
let object = super::access::location_target(scope, object);
let value = v8::BigInt::new_from_u64(scope, local_window_id);
set_private_value(
scope,
@@ -107,6 +116,7 @@ pub(super) fn set_location_href_slot<'s>(
object: v8::Local<'s, v8::Object>,
href: &str,
) {
let object = super::access::location_target(scope, object);
if let Some(href) = v8_string(scope, href) {
set_private_value(scope, object, WINDOW_LOCATION_HREF_SLOT, href.into());
}
@@ -116,6 +126,7 @@ pub(in crate::context_bootstrap) fn location_href_slot<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<String> {
let object = super::access::location_target(scope, object);
get_private_value(scope, object, WINDOW_LOCATION_HREF_SLOT)
.and_then(|value| value.to_string(scope))
.map(|value| value.to_rust_string_lossy(scope))
@@ -1,8 +1,8 @@
use super::location_history_storage::WINDOW_RUNTIME_OWNER_SLOT;
use super::location_runtime::{
build_location_runtime_object, install_location_runtime_state,
location_belongs_to_current_local_window, location_owner_has_current_realm,
sync_window_location_history_navigation_runtime_surface,
location_belongs_to_current_local_window, location_owner_has_current_realm, location_target,
sync_window_location_history_navigation_runtime_surface, wrap_location_object,
};
use super::navigation_activation::{
install_navigation_activation_runtime_state, set_navigation_current_entry,
@@ -43,7 +43,7 @@ fn new_location_runtime_object<'s>(
LocationRuntimeObjectDeclaration::new(window, href.to_owned())
.initialize(scope, location)
.map_err(|error| anyhow::anyhow!("failed to initialize Location object: {error}"))?;
Ok(location)
wrap_location_object(scope, location)
}
pub(crate) fn install_window_location_history_navigation_runtime_state<'s>(
@@ -87,6 +87,7 @@ pub(crate) fn reset_window_location_history_navigation_runtime_state<'s>(
Some(_) | None => None,
};
if let Some(location) = location {
let location = location_target(scope, location);
LocationRuntimeObjectDeclaration::new(window, href.to_owned())
.initialize(scope, location)
.map_err(|error| anyhow::anyhow!("failed to initialize Location object: {error}"))?;
@@ -8,6 +8,7 @@ pub(super) fn runtime_window_owner<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> v8::Local<'s, v8::Object> {
let object = moli_webapi_declare::web_api_object_target(scope, object).unwrap_or(object);
get_private_value(
scope,
object,
@@ -22,6 +23,7 @@ pub(super) fn set_runtime_window_owner<'s>(
object: v8::Local<'s, v8::Object>,
owner: v8::Local<'s, v8::Object>,
) {
let object = moli_webapi_declare::web_api_object_target(scope, object).unwrap_or(object);
set_private_value(
scope,
object,
@@ -61,7 +63,7 @@ pub(super) fn runtime_top_window_owner<'s>(
.unwrap_or_else(|| runtime_window_owner(scope, window))
}
pub(super) fn window_location_for_holder<'s>(
pub(crate) fn window_location_for_holder<'s>(
scope: &mut v8::PinScope<'s, '_>,
window: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
@@ -648,36 +648,7 @@ unsafe extern "C" fn window_access_check_callback(
let Some(accessed_context) = accessed_object.get_creation_context(scope) else {
return false;
};
if accessing_context == accessed_context {
return true;
}
let Some(accessing_host_ptr) =
crate::util::context_host_ptr_from_context_slot(accessing_context)
else {
return false;
};
let Some(accessed_host_ptr) = crate::util::context_host_ptr_from_context_slot(accessed_context)
else {
return false;
};
if accessing_host_ptr != accessed_host_ptr {
return false;
}
let host = unsafe { &*accessing_host_ptr };
if let (Some(accessing), Some(accessed)) = (
host.window_execution_context_identity_for_access_check(accessing_context),
host.window_execution_context_identity_for_access_check(accessed_context),
) && host.window_execution_context_identity_is_current(accessing)
&& host.window_execution_context_identity_is_current(accessed)
{
return host.window_execution_context_can_access(accessing, accessed);
}
// Execution registrations retire before script-held globals do. Their
// origin-domain and access policy still govern synchronous Window access.
host.window_context_origins_allow_access(accessing_context, accessed_context)
super::super::window_contexts_allow_access(accessing_context, accessed_context)
}
impl JsContextHost {
@@ -2273,7 +2244,10 @@ fn build_detached_cross_origin_location_proxy<'s>(
fn new_cross_origin_location_proxy_target<'s>(
scope: &mut v8::PinScope<'s, '_>,
) -> v8::Local<'s, v8::Object> {
new_null_prototype_object(scope)
let target = new_null_prototype_object(scope);
moli_webapi_declare::initialize_web_api_object(scope, target, "Location")
.expect("native Location target should accept its brand");
target
}
fn wrap_cross_origin_location_proxy<'s>(
@@ -2288,7 +2262,9 @@ fn wrap_cross_origin_location_proxy<'s>(
}
.bind(scope)
.ok()?;
set_null_prototype(scope, handler);
let proxy = v8::Proxy::new(scope, target, handler)?;
moli_webapi_declare::register_web_api_proxy(scope, proxy).ok()?;
let proxy: v8::Local<'s, v8::Value> = proxy.into();
v8::Local::<v8::Object>::try_from(proxy).ok()
}
@@ -2357,7 +2333,16 @@ fn child_window_cross_origin_access_surface<'s>(
scope: &mut v8::PinScope<'s, '_>,
holder: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
child_window_cross_origin_handler_data(scope, holder).map(|(surface, _)| surface)
let (surface, _) = child_window_cross_origin_handler_data(scope, holder)?;
if let Some(location) = crate::context_bootstrap::window_location_for_holder(scope, holder) {
set_private_value(
scope,
surface,
CROSS_ORIGIN_WINDOW_LOCATION_SLOT,
location.into(),
);
}
Some(surface)
}
fn child_window_cross_origin_handler_data<'s>(
@@ -2913,12 +2898,37 @@ fn cross_origin_window_length_getter_callback<'s>(
rv.set_uint32(count as u32);
}
fn live_location_for_cross_origin_window<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
if crate::web_api_interfaces::Window::is_instance(scope, receiver) {
return crate::context_bootstrap::window_location_for_holder(scope, receiver);
}
get_cross_origin_proxy_private_value(scope, receiver, CROSS_ORIGIN_WINDOW_LOCATION_SLOT)?;
let dispatch_scope = if let Some(popup_id) = cross_origin_lightweight_popup_id(scope, receiver)
{
super::super::OwnerDispatchScope::LightweightPopup(popup_id)
} else if is_cross_origin_top_window_proxy(scope, receiver) {
super::super::OwnerDispatchScope::Top
} else {
super::super::OwnerDispatchScope::Child(child_handle_from_object(scope, receiver)?)
};
let host_ptr = context_host_ptr_from_global_bridge(scope)?;
let host = unsafe { &mut *host_ptr };
let owner = host.current_window_execution_context_owner(dispatch_scope)?;
let (_, context) = host.window_execution_context(scope, owner, dispatch_scope)?;
crate::context_bootstrap::window_location_for_holder(scope, context.global(scope))
}
fn cross_origin_window_location_getter_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if let Some(location) =
if let Some(location) = live_location_for_cross_origin_window(scope, args.this()) {
rv.set(location.into());
} else if let Some(location) =
get_cross_origin_proxy_private_value(scope, args.this(), CROSS_ORIGIN_WINDOW_LOCATION_SLOT)
{
rv.set(location);
@@ -165,6 +165,7 @@ mod websockets;
mod window_document_tasks;
mod window_execution_context;
mod window_security_tokens;
pub(crate) use window_security_tokens::window_contexts_allow_access;
mod workers;
use window_security_tokens::DocumentDomainState;
pub(crate) use window_security_tokens::set_window_security_token;
@@ -9,6 +9,38 @@ use std::{cell::RefCell, rc::Rc};
const WINDOW_SECURITY_TOKEN_PREFIX: &str = "moli-window-origin-v1:";
const WINDOW_ISOLATED_WORLD_SECURITY_TOKEN_PREFIX: &str = "moli-window-isolated-origin-v1:";
pub(crate) fn window_contexts_allow_access(
accessing_context: v8::Local<'_, v8::Context>,
accessed_context: v8::Local<'_, v8::Context>,
) -> bool {
if accessing_context == accessed_context {
return true;
}
let Some(accessing_host_ptr) =
crate::util::context_host_ptr_from_context_slot(accessing_context)
else {
return false;
};
let Some(accessed_host_ptr) = crate::util::context_host_ptr_from_context_slot(accessed_context)
else {
return false;
};
if accessing_host_ptr != accessed_host_ptr {
return false;
}
let host = unsafe { &*accessing_host_ptr };
if let (Some(accessing), Some(accessed)) = (
host.window_execution_context_identity_for_access_check(accessing_context),
host.window_execution_context_identity_for_access_check(accessed_context),
) && host.window_execution_context_identity_is_current(accessing)
&& host.window_execution_context_identity_is_current(accessed)
{
return host.window_execution_context_can_access(accessing, accessed);
}
// Script can retain objects after their execution registrations retire.
host.window_context_origins_allow_access(accessing_context, accessed_context)
}
#[derive(Clone, Default)]
pub(in crate::native_bridge::context_host) struct DocumentDomainState(Rc<DocumentDomainStateData>);
@@ -63,6 +63,7 @@ pub(crate) use context_host::{
lightweight_popup_id_from_window, restore_active_lightweight_popup_scope,
restore_deferred_active_lightweight_popup_scope_if_present,
throw_cross_origin_location_security_error, throw_cross_origin_type_error,
window_contexts_allow_access,
};
pub(crate) const ACTIVE_CHILD_WINDOW_HANDLE_SLOT: &str = "__moliActiveChildWindowHandle";
@@ -1,5 +1,64 @@
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn retained_location_rechecks_origin_domain_access() {
for parent_first in [false, true] {
let server = StaticHttpServer::spawn_with_bodies(vec![
"<!doctype html><body>Location target</body>".to_owned(); 2
])
.await;
let loader = static_http_loader([server.resolve_entry("www.example.test")]);
let parent_url = server.url_for_host("www.example.test", "/page.html");
let mut vm =
new_storage_page_task_executor_test_vm_with_loader(parent_url.as_str(), &loader);
let script = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/retained-location-origin.js"
));
vm.exec(
&format!(
r#"
if (!document.documentElement) document.appendChild(document.createElement('html'));
if (!document.body) document.documentElement.appendChild(document.createElement('body'));
globalThis.__retainedLocationResult = null;
({script})({{parentFirst: {parent_first}}}).then(
result => {{ globalThis.__retainedLocationResult = result; }},
error => {{ globalThis.__retainedLocationResult = {{error: String(error)}}; }}
);
"#,
),
None,
)
.expect("retained Location probe should start");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(__retainedLocationResult !== null)",
"true",
"retained Location probe should finish",
)
.await;
let result: serde_json::Value = serde_json::from_str(
&vm.eval("JSON.stringify(__retainedLocationResult)")
.expect("retained Location observations"),
)
.unwrap();
assert_eq!(
result["checks"], 143,
"parent_first={parent_first}: {result}"
);
assert_eq!(
result["failures"],
serde_json::json!([]),
"parent_first={parent_first}: {result}"
);
assert_eq!(
server.finish_targets().await,
vec!["/child.html", "/peer.html"]
);
}
}
#[tokio::test(flavor = "current_thread")]
async fn retained_child_window_origin_rebinds_default_and_isolated_realms() {
let server = StaticHttpServer::spawn_with_bodies(vec![
@@ -182,6 +182,13 @@ const DIRECT_V8_CALL_ALLOWLIST: &[AllowedDirectCallFile] = &[
1,
DirectCallOwner::NativeForwardingOrScript,
),
// Location's native Proxy forwards to a captured Reflect.set intrinsic or
// its generated, receiver-checked href setter. Neither is an author callback.
allowed(
"context_bootstrap/location_runtime/access.rs",
2,
DirectCallOwner::NativeForwardingOrScript,
),
allowed(
"context_bootstrap/runtime_state.rs",
1,
@@ -0,0 +1,195 @@
async ({parentFirst = false} = {}) => {
const result = {checks: 0, failures: [], observations: []};
const check = (name, action, expected) => {
let actual;
try { actual = action(); } catch (error) { actual = error.name; }
result.checks++;
if (actual !== expected) result.failures.push({name, actual, expected});
};
const load = async path => {
const frame = document.createElement('iframe');
const loaded = new Promise(resolve => frame.onload = resolve);
frame.src = path;
document.body.append(frame);
await loaded;
return frame;
};
const frame = await load('/child.html#seed');
const peerFrame = await load('/peer.html');
const child = frame.contentWindow;
const childDocument = child.document;
const parentLocationIdentity = child.Function('expected', 'return parent.location === expected').bind(null, location);
const parentLocationRead = child.Function('held', 'try { return held.host; } catch (error) { return error.name; }').bind(null, location);
const peer = peerFrame.contentWindow;
const peerDocument = peer.document;
const held = child.location;
const href = held.href;
const proto = Object.getPrototypeOf(held);
const descriptors = Object.getOwnPropertyDescriptors(held);
const peerRead = peer.Function('target', 'return target.replace');
const peerFunctionPrototype = peer.Function.prototype;
const parentExceptionPrototype = DOMException.prototype;
const childExceptionPrototype = child.DOMException.prototype;
const childTypeErrorPrototype = child.TypeError.prototype;
const ownReplace = held.replace;
const childObjectPrototype = child.Object.prototype;
childObjectPrototype.get = () => 'polluted';
check('handler-does-not-inherit-get', () => held.href, href);
delete childObjectPrototype.get;
childObjectPrototype.getPrototypeOf = () => null;
check('handler-does-not-inherit-get-prototype', () => Object.getPrototypeOf(held) === proto, true);
delete childObjectPrototype.getPrototypeOf;
const symbol = Symbol('retained-location');
held.marker = 42;
held[0] = 'index';
held[4294967295] = 'non-index';
held[symbol] = 'symbol';
held.then = 'author then';
Object.defineProperty(held, 'locked', {value: 123});
let expandoReads = 0;
Object.defineProperty(held, 'accessor', {
get() { expandoReads++; return 1; }, configurable: true
});
check('initial-identity', () => held === child.location, true);
check('initial-marker', () => held.marker, 42);
if (parentFirst) {
document.domain = 'example.test';
peerDocument.domain = 'example.test';
} else {
childDocument.domain = 'example.test';
}
check('window-access-denied', () => child.status, 'SecurityError');
check('parent-location-identity', () => parentLocationIdentity(), true);
check('retained-parent-location-security', () => parentLocationRead(), 'SecurityError');
check('location-identity-survives-origin-change', () => child.location === held, true);
const crossWindowLocationGetter = Object.getOwnPropertyDescriptor(child, 'location').get;
check('cross-window-location-getter', () => crossWindowLocationGetter.call(child) === held, true);
check('cross-window-location-rejects-document', () => crossWindowLocationGetter.call(document), 'TypeError');
for (const key of ['marker', 'locked', 'accessor', 'missing', 0, 4294967295, symbol,
'href', 'host', 'hostname', 'port', 'protocol', 'hash', 'search', 'pathname',
'origin', 'ancestorOrigins', 'assign', 'reload', 'toString', 'valueOf', Symbol.toPrimitive]) {
check(`read:${String(key)}`, () => held[key], 'SecurityError');
}
check('blocked-expando-getter-not-called', () => expandoReads, 0);
for (const key of ['marker', 0, symbol, 'then', 'replace']) {
check(`set:${String(key)}`, () => Reflect.set(held, key, 7), 'SecurityError');
check(`define:${String(key)}`, () => Reflect.defineProperty(held, key, {value: 7}), 'SecurityError');
check(`delete:${String(key)}`, () => Reflect.deleteProperty(held, key), 'SecurityError');
}
check('has-expando', () => 'marker' in held, 'SecurityError');
check('has-href', () => 'href' in held, true);
check('has-fallback', () => 'then' in held, true);
check('descriptor-expando', () => Object.getOwnPropertyDescriptor(held, 'marker'), 'SecurityError');
for (const key of ['locked', 'missing', 0, symbol]) {
check(`descriptor:${String(key)}`, () => Object.getOwnPropertyDescriptor(held, key), 'SecurityError');
}
check('cross-origin-prototype', () => Object.getPrototypeOf(held), null);
check('set-null-prototype', () => Reflect.setPrototypeOf(held, null), true);
check('set-original-prototype', () => Reflect.setPrototypeOf(held, proto), false);
check('extensible', () => Reflect.isExtensible(held), true);
check('prevent-extensions', () => Reflect.preventExtensions(held), false);
check('own-keys', () => Reflect.ownKeys(held).map(String).join('|'),
'href|replace|then|Symbol(Symbol.toStringTag)|Symbol(Symbol.hasInstance)|Symbol(Symbol.isConcatSpreadable)');
check('enumerable-keys', () => Object.keys(held).join('|'), '');
for (const key of ['then', Symbol.toStringTag, Symbol.hasInstance, Symbol.isConcatSpreadable]) {
check(`fallback:${String(key)}`, () => held[key] === undefined, true);
}
check('href-descriptor', () => {
const d = Object.getOwnPropertyDescriptor(held, 'href');
return [typeof d.get, typeof d.set, d.enumerable, d.configurable,
Object.getPrototypeOf(d.set) === Function.prototype, d.set.name, d.set.length].join('|');
}, 'undefined|function|false|true|true|set href|1');
check('replace-descriptor', () => {
const d = Object.getOwnPropertyDescriptor(held, 'replace');
return [typeof d.value, d.enumerable, d.configurable, d.writable,
Object.getPrototypeOf(d.value) === Function.prototype, d.value.name, d.value.length].join('|');
}, 'function|false|true|false|true|replace|1');
check('cross-replace-cached', () => held.replace === held.replace, true);
check('cross-replace-new-function', () => held.replace !== ownReplace, true);
check('cross-replace-descriptor-cached', () => held.replace === Object.getOwnPropertyDescriptor(held, 'replace').value, true);
check('cross-setter-cached', () => Object.getOwnPropertyDescriptor(held, 'href').set === Object.getOwnPropertyDescriptor(held, 'href').set, true);
check('peer-cross-replace-realm', () => Object.getPrototypeOf(peerRead(held)) === peerFunctionPrototype, true);
check('peer-cross-replace-cached', () => peerRead(held) === peerRead(held), true);
check('distinct-cross-caller-functions', () => peerRead(held) !== held.replace, true);
for (const [key, d] of Object.entries(descriptors)) {
if (d.get) check(`cached-getter:${key}`, () => d.get.call(held), 'SecurityError');
}
check('cached-stringifier', () => descriptors.toString.value.call(held), 'SecurityError');
check('cached-hash-setter', () => { descriptors.hash.set.call(held, '#seed'); return 'accepted'; }, 'SecurityError');
for (const [label, action, expectedPrototype] of [
['property-security-realm', () => held.host, parentExceptionPrototype],
['cached-getter-security-realm', () => descriptors.host.get.call(held), childExceptionPrototype]
]) {
check(label, () => {
try { action(); return false; }
catch (error) { return error.name === 'SecurityError' && Object.getPrototypeOf(error) === expectedPrototype; }
}, true);
}
let conversions = 0;
const sentinel = {name: 'ConversionSentinel'};
const poison = {toString() { conversions++; throw sentinel; }};
for (const [label, action, expectedConversions] of [
['direct-hash', () => { held.hash = poison; }, 0],
['direct-href', () => { held.href = poison; }, 1],
['direct-replace', () => held.replace(poison), 1],
['cached-assign', () => descriptors.assign.value.call(held, poison), 1],
['cached-hash', () => descriptors.hash.set.call(held, poison), 1]
]) {
const before = conversions;
check(`conversion:${label}`, () => { action(); return 'accepted'; }, expectedConversions ? 'ConversionSentinel' : 'SecurityError');
check(`conversion-count:${label}`, () => conversions - before, expectedConversions);
}
for (const receiver of [{}, Object.create(held), new Proxy(held, {}), 1, null]) {
const before = conversions;
check('reflect-cross-setter-brand', () => Reflect.set(held, 'href', poison, receiver), 'TypeError');
check('reflect-cross-setter-conversion', () => conversions, before);
}
const revoked = Proxy.revocable(held, {});
revoked.revoke();
for (const receiver of [{}, Object.create(held), new Proxy(held, {}), revoked.proxy]) {
const before = conversions;
check('cached-getter-brand', () => {
try { descriptors.href.get.call(receiver); return false; }
catch (error) { return Object.getPrototypeOf(error) === childTypeErrorPrototype; }
}, true);
check('cached-setter-brand', () => descriptors.href.set.call(receiver, poison), 'TypeError');
check('cached-method-brand', () => descriptors.replace.value.call(receiver, poison), 'TypeError');
check('brand-before-conversion', () => conversions, before);
}
const waitForHref = async target => {
for (let i = 0; i < 100; i++) {
if (childDocument.URL === target) return true;
await new Promise(resolve => setTimeout(resolve, 10));
}
return false;
};
for (const [label, navigate] of [
['href', url => { held.href = url; }],
['replace', url => held.replace(url)],
['cached-href', url => descriptors.href.set.call(held, url)],
['cached-replace', url => descriptors.replace.value.call(held, url)]
]) {
const target = new URL(href);
target.hash = label;
check(`allowed-navigation:${label}`, () => { navigate(target.href); return true; }, true);
const reached = await waitForHref(target.href);
check(`navigation-completed:${label}`, () => reached, true);
}
check('cached-assign', () => { descriptors.assign.value.call(held, href); return 'accepted'; }, 'SecurityError');
check('cached-reload', () => { descriptors.reload.value.call(held); return 'accepted'; }, 'SecurityError');
check('cached-method-security-realm', () => {
try { descriptors.reload.value.call(held); return false; }
catch (error) { return error.name === 'SecurityError' && Object.getPrototypeOf(error) === childExceptionPrototype; }
}, true);
if (parentFirst) childDocument.domain = 'example.test';
else document.domain = 'example.test';
check('same-origin-restored-identity', () => child.location === held, true);
check('same-origin-restored-marker', () => held.marker, 42);
check('same-origin-restored-locked', () => held.locked, 123);
check('same-origin-restored-prototype', () => Object.getPrototypeOf(held) === proto, true);
check('same-origin-restored-method', () => held.replace === ownReplace, true);
check('same-origin-restored-getter', () => descriptors.href.get.call(held) === childDocument.URL, true);
frame.remove();
peerFrame.remove();
return result;
}
+12
View File
@@ -174,6 +174,18 @@ pub fn web_api_object_type<'s>(
})
}
/// Returns the native backing object for a branded instance. Only explicitly
/// registered native Proxies share their target; author and revoked Proxies
/// are rejected without invoking traps.
pub fn web_api_object_target<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Object>> {
let registry = scope.get_slot::<Rc<RefCell<TypeRegistry>>>().cloned()?;
object_type_id(scope, object, &registry)?;
native_identity_target(scope, object, &registry)
}
pub fn implements_interface<'s>(
scope: &mut v8::PinScope<'s, '_>,
object: v8::Local<'s, v8::Object>,
+1 -1
View File
@@ -279,7 +279,7 @@ macro_rules! web_api_constructor {
pub use brand::{
WebApiType, implements_interface, initialize_web_api_object, register_web_api_interfaces,
register_web_api_proxy, web_api_object_type,
register_web_api_proxy, web_api_object_target, web_api_object_type,
};
pub use declaration::{
+24
View File
@@ -206,17 +206,41 @@ fn only_explicitly_registered_native_proxies_share_target_identity() {
let native = v8::Proxy::new(scope, target, handler).unwrap();
let native_object = v8::Local::<v8::Object>::from(native);
assert_eq!(web_api_object_type(scope, native_object), None);
assert_eq!(
moli_webapi_declare::web_api_object_target(scope, native_object),
None
);
moli_webapi_declare::register_web_api_proxy(scope, native).unwrap();
assert!(implements_interface(scope, native_object, "TestBase"));
assert_eq!(
moli_webapi_declare::web_api_object_target(scope, native_object),
Some(target)
);
assert_eq!(
moli_webapi_declare::web_api_object_target(scope, target),
Some(target)
);
initialize_web_api_object(scope, native_object, "TestBase").unwrap();
let impostor = v8::Proxy::new(scope, target, handler).unwrap();
assert_eq!(web_api_object_type(scope, impostor.into()), None);
assert_eq!(
moli_webapi_declare::web_api_object_target(scope, impostor.into()),
None
);
let outer_handler = v8::Object::new(scope);
let outer = v8::Proxy::new(scope, native_object, outer_handler).unwrap();
assert_eq!(web_api_object_type(scope, outer.into()), None);
assert_eq!(
moli_webapi_declare::web_api_object_target(scope, outer.into()),
None
);
assert!(moli_webapi_declare::register_web_api_proxy(scope, outer).is_err());
native.revoke();
assert_eq!(web_api_object_type(scope, native_object), None);
assert_eq!(
moli_webapi_declare::web_api_object_target(scope, native_object),
None
);
}
#[derive(moli_webapi_declare::WebApiFunctionTemplate)]