fix(webidl): enforce Window global receiver semantics

This commit is contained in:
ldm0
2026-09-23 00:11:52 +08:00
parent 9d644d6cc0
commit 4e3e525928
11 changed files with 356 additions and 50 deletions
+1 -1
View File
@@ -176,7 +176,7 @@ mod window_accessors;
mod window_events;
mod window_lazy_surface;
mod window_receiver;
pub(crate) use window_receiver::is_window_receiver;
pub(crate) use window_receiver::{is_window_receiver, mark_window_receiver};
mod window_runtime;
pub(crate) use form_data_runtime::{
@@ -57,6 +57,7 @@ pub(crate) const ORIGINAL_WEBASSEMBLY_GLOBAL_VALUE_GETTER_SLOT: &str =
"__moliOriginalWebAssemblyGlobalValueGetter";
const WINDOW_INDEXED_DB_SURFACE_SLOT: &str = "moli.Window.indexedDB";
const WINDOW_ORIGIN_RUNTIME_SLOT: &str = "__moliWindowOriginRuntime";
const WINDOW_STATUS_RUNTIME_SLOT: &str = "__moliWindowStatusRuntime";
const WINDOW_INTRINSIC_EVAL_SLOT: &str = "__moliWindowIntrinsicEval";
pub(in crate::context_bootstrap) const WINDOW_SECURE_CONTEXT_AVAILABLE_SLOT: &str =
"__moliWindowSecureContextAvailable";
@@ -266,6 +267,13 @@ struct WindowPublicSurfaceAccessorsDeclaration<'scope> {
setter = window_name_runtime_setter
)]
name: (),
#[webapi(
accessor_property,
enumerable,
getter = window_status_runtime_getter,
setter = window_status_runtime_setter
)]
status: (),
}
#[derive(Default, WebApiObject)]
@@ -505,20 +513,25 @@ fn legacy_unforgeable_window_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
rv.set(legacy_unforgeable_window_slot_value(
scope,
args.this(),
WINDOW_SELF_SLOT,
));
if let Some(value) = legacy_unforgeable_window_slot_value(scope, args.this(), WINDOW_SELF_SLOT)
{
rv.set(value);
}
}
fn legacy_unforgeable_window_slot_value<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
slot: &'static str,
) -> v8::Local<'s, v8::Value> {
object_hidden_value(scope, receiver, slot)
.unwrap_or_else(|| scope.get_current_context().global(scope).into())
) -> Option<v8::Local<'s, v8::Value>> {
if !super::is_window_receiver(scope, receiver) {
throw_type_error(scope, "Window getter called on incompatible receiver.");
return None;
}
Some(
object_hidden_value(scope, receiver, slot)
.unwrap_or_else(|| scope.get_current_context().global(scope).into()),
)
}
fn document_fullscreen_enabled_getter<'s>(
@@ -727,11 +740,10 @@ fn legacy_unforgeable_self_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
rv.set(legacy_unforgeable_window_slot_value(
scope,
args.this(),
WINDOW_SELF_SLOT,
));
if let Some(value) = legacy_unforgeable_window_slot_value(scope, args.this(), WINDOW_SELF_SLOT)
{
rv.set(value);
}
}
fn replaceable_window_alias_set<'s>(
@@ -739,6 +751,10 @@ fn replaceable_window_alias_set<'s>(
args: v8::FunctionCallbackArguments<'s>,
name: &'static str,
) {
if !super::is_window_receiver(scope, args.this()) {
throw_type_error(scope, "Window setter called on incompatible receiver.");
return;
}
define_replaceable_window_property(scope, args.this(), name, args.get(0));
}
@@ -755,11 +771,11 @@ fn legacy_unforgeable_parent_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
rv.set(legacy_unforgeable_window_slot_value(
scope,
args.this(),
WINDOW_PARENT_SLOT,
));
if let Some(value) =
legacy_unforgeable_window_slot_value(scope, args.this(), WINDOW_PARENT_SLOT)
{
rv.set(value);
}
}
fn replaceable_parent_setter<'s>(
@@ -775,11 +791,9 @@ fn legacy_unforgeable_top_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
rv.set(legacy_unforgeable_window_slot_value(
scope,
args.this(),
WINDOW_TOP_SLOT,
));
if let Some(value) = legacy_unforgeable_window_slot_value(scope, args.this(), WINDOW_TOP_SLOT) {
rv.set(value);
}
}
fn legacy_unforgeable_frames_getter<'s>(
@@ -787,11 +801,11 @@ fn legacy_unforgeable_frames_getter<'s>(
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
rv.set(legacy_unforgeable_window_slot_value(
scope,
args.this(),
WINDOW_FRAMES_SLOT,
));
if let Some(value) =
legacy_unforgeable_window_slot_value(scope, args.this(), WINDOW_FRAMES_SLOT)
{
rv.set(value);
}
}
fn replaceable_frames_setter<'s>(
@@ -892,6 +906,10 @@ fn window_surface_replaceable_setter<'s>(
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s, v8::Value>,
) {
if !super::is_window_receiver(scope, args.this()) {
throw_type_error(scope, "Window setter called on incompatible receiver.");
return;
}
let Some(name) = callback_data_item(
scope,
&args,
@@ -910,6 +928,10 @@ fn window_name_runtime_getter<'s>(
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let receiver = callback_this_object(scope, &args);
if !super::is_window_receiver(scope, receiver) {
throw_type_error(scope, "Window.name getter called on incompatible receiver.");
return;
}
let value = object_hidden_value(scope, receiver, WINDOW_NAME_SLOT)
.unwrap_or_else(|| v8::String::empty(scope).into());
rv.set(value);
@@ -921,6 +943,10 @@ fn window_name_runtime_setter<'s>(
_rv: v8::ReturnValue<'s, v8::Value>,
) {
let receiver = callback_this_object(scope, &args);
if !super::is_window_receiver(scope, receiver) {
throw_type_error(scope, "Window.name setter called on incompatible receiver.");
return;
}
let next = args
.get(0)
.to_string(scope)
@@ -934,6 +960,44 @@ fn window_name_runtime_setter<'s>(
define_non_enumerable_string_property(scope, receiver, WINDOW_NAME_SLOT, &next);
}
fn window_status_runtime_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
let receiver = callback_this_object(scope, &args);
if !super::is_window_receiver(scope, receiver) {
throw_type_error(
scope,
"Window.status getter called on incompatible receiver.",
);
return;
}
rv.set(
get_private_value(scope, receiver, WINDOW_STATUS_RUNTIME_SLOT)
.unwrap_or_else(|| v8::String::empty(scope).into()),
);
}
fn window_status_runtime_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'s, v8::Value>,
) {
let receiver = callback_this_object(scope, &args);
if !super::is_window_receiver(scope, receiver) {
throw_type_error(
scope,
"Window.status setter called on incompatible receiver.",
);
return;
}
let Some(next) = args.get(0).to_string(scope) else {
return;
};
set_private_value(scope, receiver, WINDOW_STATUS_RUNTIME_SLOT, next.into());
}
fn install_public_window_surface_accessors<'s>(
scope: &mut v8::PinScope<'s, '_>,
global: v8::Local<'s, v8::Object>,
@@ -1058,6 +1122,7 @@ fn install_public_window_surface_accessors<'s>(
session_storage: (),
indexed_db: (),
name: (),
status: (),
}
.initialize(scope, global)?;
WindowAdditionalReplaceableAccessorsDeclaration {
@@ -1,9 +1,11 @@
use super::shared::WINDOW_NAVIGATOR_SLOT;
use crate::util::{
callback_data_index_value, context_host_ptr_from_context_slot,
context_host_ptr_from_window_object, get_private_value, throw_type_error,
context_host_ptr_from_window_object, get_private_value, set_private_value, throw_type_error,
};
const WINDOW_BRAND_SLOT: &str = "__moliWindowBrand";
/// Recognizes a native Window receiver for WebIDL brand checks.
///
/// A retained, detached global proxy can lose both its creation context and
@@ -15,7 +17,9 @@ pub(crate) fn is_window_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> bool {
if is_live_window_receiver(scope, receiver) {
if is_live_window_receiver(scope, receiver)
|| get_private_value(scope, receiver, WINDOW_BRAND_SLOT).is_some()
{
return true;
}
let current_context = scope.get_current_context();
@@ -23,6 +27,16 @@ pub(crate) fn is_window_receiver<'s>(
&& context_host_ptr_from_context_slot(current_context).is_some()
}
/// Marks a Window-shaped native object that intentionally has no execution
/// context, such as an iframe owned by a DOMParser-created Document.
pub(crate) fn mark_window_receiver(
scope: &mut v8::PinScope<'_, '_>,
receiver: v8::Local<'_, v8::Object>,
) {
let branded = v8::Boolean::new(scope, true);
set_private_value(scope, receiver, WINDOW_BRAND_SLOT, branded.into());
}
/// Recognizes a Window whose V8 object still exposes its live realm markers.
///
/// This is deliberately narrower than the WebIDL brand check above. Bound
@@ -14,7 +14,8 @@ pub(super) use base64::{window_atob_callback, window_btoa_callback};
pub(super) use dialogs::entered_window_api_base_url;
pub(super) use dialogs::{window_alert_callback, window_confirm_callback, window_prompt_callback};
pub(crate) use dialogs::{
window_const_false_callback, window_noop_callback, window_open_callback, window_stop_callback,
window_blur_callback, window_const_false_callback, window_focus_callback, window_noop_callback,
window_open_callback, window_stop_callback,
};
pub(crate) use navigator::{
LegacyStorageQuotaCallbackOutcome, LegacyStorageQuotaCallbackTask,
@@ -68,7 +69,7 @@ struct ChildWindowOwnMethodsDeclaration {
open: (),
#[webapi(method, length = 0, callback = window_noop_callback)]
close: (),
#[webapi(method, length = 0, callback = window_noop_callback)]
#[webapi(method, length = 0, callback = window_blur_callback)]
blur: (),
#[webapi(method, length = 0, callback = window_const_false_callback)]
find: (),
@@ -68,6 +68,39 @@ pub(crate) fn window_noop_callback(
) {
}
pub(crate) fn window_focus_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
let receiver = args.this();
if !crate::context_bootstrap::is_window_receiver(scope, receiver) {
webidl::throw_type_error(scope, "Window.focus called on incompatible receiver.");
return;
}
let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else {
return;
};
if let Some(crate::native_bridge::OwnerDispatchScope::Child(handle)) =
super::super::navigation_window::runtime_window_dispatch_scope(scope, receiver)
{
crate::native_bridge::element::focus_element(scope, host_ptr, handle);
}
rv.set_undefined();
}
pub(crate) fn window_blur_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'_, v8::Value>,
) {
if !crate::context_bootstrap::is_window_receiver(scope, args.this()) {
webidl::throw_type_error(scope, "Window.blur called on incompatible receiver.");
return;
}
rv.set_undefined();
}
pub(crate) fn window_stop_callback<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -72,10 +72,10 @@ struct WindowEarlyTemplateMethodsDeclaration {
#[webapi(method, length = 0, callback = window_noop_callback)]
close: (),
#[webapi(method, length = 0, callback = window_noop_callback)]
#[webapi(method, length = 0, callback = window_focus_callback)]
focus: (),
#[webapi(method, length = 0, callback = window_noop_callback)]
#[webapi(method, length = 0, callback = window_blur_callback)]
blur: (),
#[webapi(method, length = 0, callback = window_const_false_callback)]
@@ -606,20 +606,13 @@ unsafe extern "C" fn window_access_check_callback(
return true;
}
let Some((accessing_host_ptr, accessing_identity)) = (|| {
let host_ptr = crate::util::context_host_ptr_from_context_slot(accessing_context)?;
let identity = unsafe { &*host_ptr }
.window_execution_context_identity_for_access_check(accessing_context)?;
Some((host_ptr, identity))
})() else {
let Some(accessing_host_ptr) =
crate::util::context_host_ptr_from_context_slot(accessing_context)
else {
return false;
};
let Some((accessed_host_ptr, accessed_identity)) = (|| {
let host_ptr = crate::util::context_host_ptr_from_context_slot(accessed_context)?;
let identity = unsafe { &*host_ptr }
.window_execution_context_identity_for_access_check(accessed_context)?;
Some((host_ptr, identity))
})() else {
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 {
@@ -627,7 +620,34 @@ unsafe extern "C" fn window_access_check_callback(
}
let host = unsafe { &*accessing_host_ptr };
host.window_execution_context_can_access(accessing_identity, accessed_identity)
let Some(accessing_identity) =
host.window_execution_context_identity_for_access_check(accessing_context)
else {
return false;
};
if let Some(accessed_identity) =
host.window_execution_context_identity_for_access_check(accessed_context)
&& host.window_execution_context_can_access(accessing_identity, accessed_identity)
{
return true;
}
if !host.window_execution_context_identity_is_current(accessing_identity)
|| accessed_context
.get_slot::<super::super::RuntimeObservableContextToken>()
.is_none()
{
return false;
}
// Removing a same-origin iframe retires its LocalWindow registration, but
// JavaScript can still retain that inner global long enough to clean up
// listeners and other realm-owned state. The context's internalized
// security token is the last live effective-origin snapshot, so equal
// tokens preserve that access without reopening retired cross-origin or
// opaque realms.
accessing_context
.get_security_token(scope)
.strict_equals(accessed_context.get_security_token(scope))
}
impl JsContextHost {
@@ -45,6 +45,7 @@ pub(super) fn build_detached_iframe_content_window<'s>(
let window = DetachedIframeWindowDeclaration::new(parent, top, document)
.bind(scope)
.ok()?;
crate::context_bootstrap::mark_window_receiver(scope, window);
set_document_associated_window(scope, document, window);
crate::network_host::install_fetch_constructors_for_base_url(scope, window, base_url);
install_detached_iframe_window_messaging(scope, window);
@@ -14493,7 +14493,7 @@ __domainAccessFrame.src = globalThis.__replacementDomainChildUrl;
async fn one_sided_document_domain_disables_original_tuple_origin_fast_path() {
const HOST: &str = "www.example.test";
let server = StaticHttpServer::spawn(1).await;
let server = StaticHttpServer::spawn(2).await;
let parent_url = server.url_for_host(HOST, "/page.html");
let child_url = server.url_for_host(HOST, "/child.html");
let loader = static_http_loader([server.resolve_entry(HOST)]);
@@ -14573,7 +14573,52 @@ globalThis.__probeOneSidedDomainFrame = () => {
.expect("restored exact-domain access probe should evaluate"),
"www.example.test"
);
assert_eq!(server.finish_targets().await, vec!["/child.html"]);
vm.exec(
r#"
const retiredFrame = document.createElement("iframe");
globalThis.__retiredOneSidedDomainChildLoaded = false;
retiredFrame.onload = () => { globalThis.__retiredOneSidedDomainChildLoaded = true; };
retiredFrame.src = globalThis.__oneSidedDomainChildUrl;
(document.body || document.documentElement || document).appendChild(retiredFrame);
globalThis.__retiredOneSidedDomainWindow = retiredFrame.contentWindow;
"#,
None,
)
.expect("one-sided document.domain retired Window setup should run");
advance_page_task_executor_until_eval_equals(
&mut vm,
&loader,
"String(globalThis.__retiredOneSidedDomainChildLoaded)",
"true",
"fresh child without document.domain should load",
)
.await;
assert_eq!(
vm.eval(
r#"
(() => {
const probe = () => {
try {
return __retiredOneSidedDomainWindow.document.domain;
} catch (error) {
return error && error.name;
}
};
const beforeRemoval = probe();
document.querySelectorAll("iframe")[1].remove();
return [beforeRemoval, probe()].join("|");
})()
"#,
)
.expect("retired one-sided document.domain Window probe should evaluate"),
"SecurityError|SecurityError"
);
assert_eq!(
server.finish_targets().await,
vec!["/child.html", "/child.html"]
);
}
#[test]
@@ -371,6 +371,126 @@ fn window_global_accessors_use_the_borrowed_window_receiver() {
);
}
#[test]
fn cross_realm_window_members_apply_global_interface_receiver_semantics() {
let mut vm = new_storage_test_vm("https://window-global-receiver.test/");
vm.eval(
r#"
(() => {
const root = document.documentElement ||
document.appendChild(document.createElement("html"));
const body = document.body || root.appendChild(document.createElement("body"));
const frame = document.createElement("iframe");
frame.id = "global-receiver-frame";
frame.name = "dummy";
body.appendChild(frame);
})()
"#,
)
.expect("cross-realm Window receiver child frame should be created");
materialize_single_child_default_realm_for_test(
&mut vm,
"cross-realm Window receiver child Realm",
);
let result = vm
.eval(
r#"
(() => {
const frame = document.getElementById("global-receiver-frame");
const other = frame.contentWindow;
const notWindow = Object.create(Object.getPrototypeOf(other));
const throwsOtherTypeError = callback => {
try {
callback();
return false;
} catch (error) {
return error instanceof other.TypeError && !(error instanceof TypeError);
}
};
const invalidGetters = [
() => { Object.create(other).window; },
() => Object.getOwnPropertyDescriptor(other, "history").get.call(notWindow),
() => Reflect.get(other, "screen", notWindow),
() => new Proxy(other, {}).onclick
].every(throwsOtherTypeError);
const invalidSetters = [
() => { Object.create(other).name = "ignored"; },
() => Object.getOwnPropertyDescriptor(other, "status").set.call(notWindow, "ignored"),
() => Reflect.set(other, "parent", window, notWindow),
() => { new Proxy(other, {}).location = location; }
].every(throwsOtherTypeError);
const invalidOperations = [
() => Object.create(other).focus(),
() => other.clearInterval.call(notWindow, 0),
() => Reflect.apply(other.blur, notWindow, []),
() => new Proxy(other, {}).removeEventListener("foo", () => {})
].every(throwsOtherTypeError);
const nameGetter = Object.getOwnPropertyDescriptor(other, "name").get;
const statusDescriptor = Object.getOwnPropertyDescriptor(other, "status");
const nullGetters =
Reflect.get(other, "self", null) === other &&
Reflect.get(other, "document", undefined) === other.document &&
nameGetter.call(null) === "dummy";
const newSelf = {};
const nullSetters =
Reflect.set(other, "self", newSelf, null) &&
Reflect.set(other, "name", "newName", undefined);
statusDescriptor.set.call(null, "ready");
const extractedFocus = other.focus;
extractedFocus();
let caughtEvent;
const eventListener = event => {
caughtEvent = event;
};
other.addEventListener.call(null, "global-receiver", eventListener);
const dispatchedEvent = new other.Event("global-receiver");
const extractedDispatch = other.dispatchEvent;
const dispatchResult = extractedDispatch(dispatchedEvent);
const focusedChild = document.activeElement === frame;
frame.remove();
let detachedCleanup = true;
try {
other.removeEventListener("global-receiver", eventListener);
} catch (error) {
detachedCleanup = false;
}
return [
invalidGetters,
invalidSetters,
invalidOperations,
nullGetters,
nullSetters,
other.self === newSelf,
other.name === "newName",
statusDescriptor.get.call(null) === "ready",
focusedChild,
dispatchResult,
caughtEvent === dispatchedEvent,
detachedCleanup
].join("|");
})()
"#,
)
.expect("cross-realm Window receiver semantics should evaluate");
assert_eq!(
result,
std::iter::repeat_n("true", 12)
.collect::<Vec<_>>()
.join("|")
);
}
#[test]
fn child_document_body_with_scope_exposes_insert_before() {
let mut vm = new_storage_test_vm("https://child-window-with-body.test/");
+7
View File
@@ -416,6 +416,9 @@ pub(super) fn event_target_remove_event_listener_callback<'s>(
event_target_handle_from_this(scope, &args, host_ptr, host)
};
let Some(target) = target else {
if crate::context_bootstrap::is_window_receiver(scope, args.this()) {
return;
}
throw_type_error(scope, "Illegal invocation");
return;
};
@@ -746,6 +749,10 @@ pub(super) fn window_clear_timer_callback<'s>(
args: v8::FunctionCallbackArguments<'s>,
_rv: v8::ReturnValue<'_, v8::Value>,
) {
if !crate::context_bootstrap::is_window_receiver(scope, args.this()) {
throw_type_error(scope, "Illegal invocation");
return;
}
let id_val = args.get(0);
let id = id_val.number_value(scope).unwrap_or(0.0) as u32;
cancel_window_timer_for_receiver(scope, args.this(), id);