diff --git a/moli-renderer-v8/src/callback_invocation.rs b/moli-renderer-v8/src/callback_invocation.rs index 8ef688f301..069b83761b 100644 --- a/moli-renderer-v8/src/callback_invocation.rs +++ b/moli-renderer-v8/src/callback_invocation.rs @@ -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 }, diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index be2733e39e..ca0c5235fe 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -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()), ); } diff --git a/moli-renderer-v8/src/context_bootstrap/window_events.rs b/moli-renderer-v8/src/context_bootstrap/window_events.rs index b926a8400e..813e18935b 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events.rs @@ -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, diff --git a/moli-renderer-v8/src/context_bootstrap/window_events/accessors.rs b/moli-renderer-v8/src/context_bootstrap/window_events/accessors.rs index 5bee8fd0e6..3220a333ac 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events/accessors.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events/accessors.rs @@ -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> { + // 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(); } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index 5ed85db3a3..4ae3bfb477 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -64,3 +64,5 @@ mod worker_listener_invocation; mod document_domain_lifetime; mod document_domain_setter; + +mod window_current_event_private; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/window_current_event_private.js b/moli-renderer-v8/src/script_vm/tests/browser_api/window_current_event_private.js new file mode 100644 index 0000000000..cd3f89be08 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/window_current_event_private.js @@ -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; +})(); diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/window_current_event_private.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/window_current_event_private.rs new file mode 100644 index 0000000000..09d9d685f4 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/window_current_event_private.rs @@ -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 = ''; '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 = '
'; + 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]" + ); +}