fix(window): keep current events in private realm state

This commit is contained in:
ldm0
2026-10-06 02:38:13 +08:00
committed by Donough Liu
parent 29bb4dc91d
commit 45d89eacf9
7 changed files with 249 additions and 17 deletions
+4 -6
View File
@@ -5,7 +5,7 @@ use crate::exception_reporting::{
use crate::{
host::WINDOW_EVENT_SLOT,
native_bridge::{JsContextHost, RuntimeObservableContextToken, WindowExecutionContextIdentity},
util::v8str,
util::{get_private_value, set_private_value},
};
use moli_webidl_callback::{
PreparedWebIdlCallbackFunction, PreparedWebIdlCallbackInterface, WebIdlCallbackInvocation,
@@ -244,11 +244,9 @@ impl CallbackInvoker {
.and(invocation.current_event)
.map(|event| {
let global = relevant_context.global(scope);
let event_key = v8str(scope, WINDOW_EVENT_SLOT);
let previous = global
.get(scope, event_key.into())
let previous = get_private_value(scope, global, WINDOW_EVENT_SLOT)
.unwrap_or_else(|| v8::undefined(scope).into());
let _ = global.set(scope, event_key.into(), event.into());
set_private_value(scope, global, WINDOW_EVENT_SLOT, event.into());
previous
});
@@ -287,7 +285,7 @@ impl CallbackInvoker {
if let Some(previous) = previous_window_event {
let global = relevant_context.global(scope);
let _ = global.set(scope, v8str(scope, WINDOW_EVENT_SLOT).into(), previous);
set_private_value(scope, global, WINDOW_EVENT_SLOT, previous);
}
result
},
@@ -461,7 +461,7 @@ struct ConsoleObjectDeclaration {
struct WindowBootstrapGlobalSlotsDeclaration<'scope> {
#[webapi(slot = WINDOW_CONSOLE_SLOT)]
console: v8::Local<'scope, v8::Object>,
#[webapi(data_property = WINDOW_EVENT_SLOT, init = "undefined")]
#[webapi(slot = WINDOW_EVENT_SLOT, init = "undefined")]
event: (),
#[webapi(data_property = WINDOW_ONERROR_SLOT, init = "null")]
on_error: (),
@@ -581,7 +581,7 @@ fn document_fullscreen_enabled_lenient_setter<'s>(
) {
}
fn define_replaceable_window_property(
pub(in crate::context_bootstrap) fn define_replaceable_window_property(
scope: &mut v8::PinScope<'_, '_>,
receiver: v8::Local<'_, v8::Object>,
name: &'static str,
@@ -641,11 +641,11 @@ fn window_outer_height_replaceable_getter<'s>(
fn window_event_replaceable_getter<'s>(
scope: &mut v8::PinScope<'s, '_>,
_args: v8::FunctionCallbackArguments<'s>,
args: v8::FunctionCallbackArguments<'s>,
mut rv: v8::ReturnValue<'s, v8::Value>,
) {
rv.set(
global_hidden_value(scope, WINDOW_EVENT_SLOT)
super::window_events::window_event_value_for_receiver(scope, args.this())
.unwrap_or_else(|| v8::undefined(scope).into()),
);
}
@@ -89,9 +89,10 @@ pub(crate) use accessors::{
};
pub(super) use accessors::{
window_console_getter, window_event_getter, window_event_setter,
window_onerror_getter_function, window_onerror_setter_function,
window_onrejectionhandled_getter_function, window_onrejectionhandled_setter_function,
window_onunhandledrejection_getter_function, window_onunhandledrejection_setter_function,
window_event_value_for_receiver, window_onerror_getter_function,
window_onerror_setter_function, window_onrejectionhandled_getter_function,
window_onrejectionhandled_setter_function, window_onunhandledrejection_getter_function,
window_onunhandledrejection_setter_function,
};
pub(super) use console::{
console_assert_callback, console_debug_callback, console_error_callback,
@@ -141,12 +141,53 @@ pub(in crate::context_bootstrap) fn window_event_getter<'s>(
if !require_window_receiver(scope, &args) {
return;
}
match global_hidden_value(scope, WINDOW_EVENT_SLOT) {
match window_event_value_for_receiver(scope, args.this()) {
Some(value) => rv.set(value),
None => rv.set(v8::undefined(scope).into()),
}
}
pub(in crate::context_bootstrap) fn window_event_value_for_receiver<'s>(
scope: &mut v8::PinScope<'s, '_>,
receiver: v8::Local<'s, v8::Object>,
) -> Option<v8::Local<'s, v8::Value>> {
// Real globals keep the current event in their concrete callback realm.
// Resolving only the Document owner would read the default world's slot
// even when this receiver is an isolated world's Window.
if let Some(context) = receiver.get_creation_context(scope)
&& receiver.strict_equals(context.global(scope).into())
{
return get_private_value(scope, receiver, WINDOW_EVENT_SLOT);
}
let target_event = context_host_ptr_from_window_object(scope, receiver)
.or_else(|| context_host_ptr_from_global_bridge(scope))
.and_then(|host_ptr| {
let dispatch_scope = if let Some(popup_id) =
crate::native_bridge::lightweight_popup_id_from_window(scope, receiver)
{
crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id)
} else if let Some(handle) = window_child_context_handle(scope, receiver) {
crate::native_bridge::OwnerDispatchScope::Child(handle)
} else {
crate::native_bridge::OwnerDispatchScope::Top
};
let host = unsafe { &*host_ptr };
let owner = host.current_window_execution_context_owner(dispatch_scope)?;
let (_, context) = host.window_execution_context(scope, owner, dispatch_scope)?;
let global = context.global(scope);
// An empty target Window current event is still authoritative;
// do not fall back to the caller Window's active event.
Some(
get_private_value(scope, global, WINDOW_EVENT_SLOT)
.unwrap_or_else(|| v8::undefined(scope).into()),
)
});
target_event.or_else(|| {
let global = scope.get_current_context().global(scope);
get_private_value(scope, global, WINDOW_EVENT_SLOT)
})
}
pub(in crate::context_bootstrap) fn window_event_setter<'s>(
scope: &mut v8::PinScope<'s, '_>,
args: v8::FunctionCallbackArguments<'s>,
@@ -155,9 +196,12 @@ pub(in crate::context_bootstrap) fn window_event_setter<'s>(
if !require_window_receiver(scope, &args) {
return;
}
let global = scope.get_current_context().global(scope);
let key = v8str(scope, WINDOW_EVENT_SLOT);
let _ = global.set(scope, key.into(), args.get(0));
super::super::runtime_state::define_replaceable_window_property(
scope,
args.this(),
"event",
args.get(0),
);
rv.set_undefined();
}
@@ -64,3 +64,5 @@ mod worker_listener_invocation;
mod document_domain_lifetime;
mod document_domain_setter;
mod window_current_event_private;
@@ -0,0 +1,117 @@
(() => {
const child = document.getElementById('child').contentWindow;
const frame = child.document.createElement('iframe');
child.document.body.appendChild(frame);
const realms = [['root', window], ['child', child], ['nested', frame.contentWindow]];
const descriptors = realms.map(([label, realm]) => ({label, realm,
descriptor: Object.getOwnPropertyDescriptor(realm, 'event')}));
const rows = [];
for (const {label, realm, descriptor} of descriptors) {
for (const mode of ['ordinary', 'overwrite', 'accessor', 'prototype']) {
const checks = [];
const check = (name, pass) => checks.push({name, pass: !!pass});
const oldName = '__moliWindowEvent';
const previousSlot = Object.getOwnPropertyDescriptor(realm, oldName);
const prototype = Object.getPrototypeOf(realm);
const previousPrototypeSlot = Object.getOwnPropertyDescriptor(prototype, oldName);
const previousError = realm.onerror;
const authorValue = {};
const publicValue = {};
let reads = 0, writes = 0, caught = 0;
const trace = [];
const read = () => descriptor.get.call(realm);
check('no internal own name', !Object.getOwnPropertyNames(realm).includes(oldName));
check('no internal reflected key', !Reflect.ownKeys(realm).includes(oldName));
check('public accessor', typeof descriptor.get === 'function' && typeof descriptor.set === 'function');
try {
const map = new realm.WeakMap();
for (const name of Object.getOwnPropertyNames(realm)) {
if (/[A-Z][A-Za-z0-9]+Event$/.test(name)) map.set(realm[name], name);
}
check('EventRecorder constructor scan', true);
} catch (error) { check('EventRecorder constructor scan', false); }
if (mode !== 'ordinary') {
delete realm[oldName];
if (mode === 'overwrite') Object.defineProperty(realm, oldName,
{value: authorValue, writable: true, configurable: true});
else Object.defineProperty(mode === 'prototype' ? prototype : realm, oldName,
{configurable: true, get() { reads++; return authorValue; }, set() { writes++; }});
}
const target = new realm.EventTarget();
const state = {read, check, target, trace, outer: null, replaced: false,
visible: () => realm.event, publicValue,
otherWindowsClear: () => descriptors.filter(d => d.realm !== realm)
.every(d => d.descriptor.get.call(d.realm) === undefined)};
const callbacks = realm.Function('s', `return {
outer(event) {
s.trace.push('outer'); s.outer = event;
s.check('outer current', s.read() === event);
s.check('other windows clear', s.otherWindowsClear());
s.check('outer public value', s.visible() === (s.replaced ? s.publicValue : event));
s.target.dispatchEvent(new Event('inner'));
s.check('outer restored', s.read() === event);
s.trace.push('restored');
},
inner(event) {
s.trace.push('inner');
s.check('inner current', s.read() === event && event !== s.outer);
s.check('inner other windows clear', s.otherWindowsClear());
},
object: { get handleEvent() {
s.trace.push('lookup'); s.check('current before handleEvent lookup', s.read()?.type === 'lookup');
return event => s.check('object callback current', s.read() === event);
} },
fail() { throw new Error('expected current-event probe failure'); },
report() {
s.trace.push('error');
s.check('exception report current', s.read()?.type === 'error');
return true;
}
}`)(state);
target.addEventListener('outer', callbacks.outer);
target.addEventListener('inner', callbacks.inner);
target.addEventListener('lookup', callbacks.object);
target.addEventListener('fail', callbacks.fail);
realm.onerror = callbacks.report;
try {
check('undefined before dispatch', read() === undefined);
target.dispatchEvent(new realm.Event('outer'));
check('undefined after nested dispatch', read() === undefined);
target.dispatchEvent(new realm.Event('lookup'));
check('undefined after object callback', read() === undefined);
target.dispatchEvent(new realm.Event('fail'));
check('undefined after exception reporting', read() === undefined);
realm.event = publicValue;
check('public Replaceable value', realm.event === publicValue);
check('replacement does not change native current event', read() === undefined);
const replacement = Object.getOwnPropertyDescriptor(realm, 'event');
check('Replaceable data descriptor', replacement.value === publicValue &&
replacement.writable && replacement.enumerable && replacement.configurable);
state.replaced = true;
target.dispatchEvent(new realm.Event('outer'));
check('replacement survives callback', realm.event === publicValue);
check('native current cleared with replacement', read() === undefined);
check('trace', trace.join(',') === 'outer,inner,restored,lookup,error,outer,inner,restored');
check('author getter ignored', reads === 0);
check('author setter ignored', writes === 0);
if (mode === 'overwrite') check('author slot value unchanged', realm[oldName] === authorValue);
if (mode === 'ordinary') check('dispatch does not publish internal state', !Object.hasOwn(realm, oldName));
} catch (error) { caught++; check('dispatch completed without uncaught exception', false); }
finally {
Object.defineProperty(realm, 'event', descriptor);
realm.onerror = previousError;
delete realm[oldName];
if (previousSlot) Object.defineProperty(realm, oldName, previousSlot);
if (mode === 'prototype') {
delete prototype[oldName];
if (previousPrototypeSlot) Object.defineProperty(prototype, oldName, previousPrototypeSlot);
}
}
rows.push({label, mode, complete: caught === 0 && checks.every(c => c.pass),
passed: checks.filter(c => c.pass).length, total: checks.length, checks, trace, reads, writes, caught});
}
}
globalThis.__uiEventResults = {rows, complete: rows.every(r => r.complete),
passed: rows.reduce((n, r) => n + r.passed, 0), total: rows.reduce((n, r) => n + r.total, 0)};
return __uiEventResults.complete;
})();
@@ -0,0 +1,70 @@
use super::*;
#[test]
fn window_current_events_ignore_author_slot_names_and_public_replacements() {
let mut vm = new_storage_html_test_vm("https://private-window-event.test/");
vm.eval("document.body.innerHTML = '<iframe id=child></iframe>'; 'ready'")
.unwrap();
vm.eval(include_str!("window_current_event_private.js"))
.unwrap();
let facts: serde_json::Value =
serde_json::from_str(&vm.eval("JSON.stringify(__uiEventResults)").unwrap()).unwrap();
assert_eq!(facts["rows"].as_array().unwrap().len(), 12);
assert_eq!(facts["total"], 378);
assert_eq!(facts["complete"], true, "{facts}");
}
#[test]
fn window_current_events_use_private_state_in_each_listener_world() {
let mut vm = new_storage_html_test_vm("https://private-window-event-worlds.test/");
vm.eval(
r#"
document.body.innerHTML = '<div id=target></div>';
globalThis.target = document.getElementById('target');
globalThis.facts = [];
Object.defineProperty(globalThis, '__moliWindowEvent', {
get() { throw new Error('author slot getter'); },
set() { throw new Error('author slot setter'); }
});
target.addEventListener('outer', event => {
facts.push(window.event === event);
target.dispatchEvent(new Event('inner'));
facts.push(window.event === event);
});
'ready'
"#,
)
.unwrap();
let isolated = vm
.create_isolated_world("private-current-event", false)
.unwrap();
vm.eval_in_isolated_context(
isolated,
r#"
globalThis.target = document.getElementById('target');
globalThis.facts = [];
Object.defineProperty(globalThis, '__moliWindowEvent', {
get() { throw new Error('isolated author slot getter'); },
set() { throw new Error('isolated author slot setter'); }
});
target.addEventListener('inner', event => {
facts.push(window.event === event);
});
'ready'
"#,
)
.unwrap();
assert_eq!(
vm.eval("target.dispatchEvent(new Event('outer')); JSON.stringify([facts, window.event === undefined])")
.unwrap(),
"[[true,true],true]"
);
assert_eq!(
vm.eval_in_isolated_context(
isolated,
"JSON.stringify([facts, window.event === undefined])"
)
.unwrap(),
"[[true],true]"
);
}