diff --git a/moli-renderer-v8/src/callback_invocation.rs b/moli-renderer-v8/src/callback_invocation.rs index 1b944327c5..7142858882 100644 --- a/moli-renderer-v8/src/callback_invocation.rs +++ b/moli-renderer-v8/src/callback_invocation.rs @@ -192,6 +192,60 @@ impl CallbackInvoker { callback_name: &str, invocation: CallbackInvocation<'s, '_>, ) -> CallbackInvocationOutcome { + Self::invoke_with_completion( + scope, + callback_kind, + log_label, + log_level, + callback_name, + invocation, + false, + |_scope, outcome| outcome, + ) + } + + pub(crate) fn invoke_event_and_then<'s, R>( + scope: &mut v8::PinScope<'s, '_>, + callback_kind: &str, + log_label: &str, + log_level: CallbackExceptionLogLevel, + callback_name: &str, + invocation: CallbackInvocation<'s, '_>, + complete: impl FnOnce(&mut v8::PinScope<'s, '_>, CallbackInvocationOutcome) -> R, + ) -> R { + let host_ptr = invocation.host_ptr; + let event_callback_scope = host_ptr + .map(|host_ptr| unsafe { &mut *host_ptr }.enter_event_callback_invocation_scope()); + let defer_window_event_restore = event_callback_scope + .as_ref() + .is_some_and(|event_scope| event_scope.is_outermost()) + && host_ptr + .is_some_and(|host_ptr| !unsafe { &*host_ptr }.explicit_event_dispatch_is_active()); + let result = Self::invoke_with_completion( + scope, + callback_kind, + log_label, + log_level, + callback_name, + invocation, + defer_window_event_restore, + complete, + ); + drop(event_callback_scope); + result + } + + #[allow(clippy::too_many_arguments)] + fn invoke_with_completion<'s, R>( + scope: &mut v8::PinScope<'s, '_>, + callback_kind: &str, + log_label: &str, + log_level: CallbackExceptionLogLevel, + callback_name: &str, + invocation: CallbackInvocation<'s, '_>, + defer_window_event_restore: bool, + complete: impl FnOnce(&mut v8::PinScope<'s, '_>, CallbackInvocationOutcome) -> R, + ) -> R { if let Some(host_ptr) = invocation.host_ptr { unsafe { &*host_ptr }.debug_assert_not_in_structural_mutation("callback invocation"); } @@ -199,10 +253,10 @@ impl CallbackInvoker { (invocation.host_ptr, invocation.relevant_identity) && !unsafe { &*host_ptr }.window_execution_context_identity_is_current(identity) { - return CallbackInvocationOutcome::Retired; + return complete(scope, CallbackInvocationOutcome::Retired); } - let result = with_webidl_callback_contexts( + with_webidl_callback_contexts( scope, invocation.relevant_context, invocation.incumbent_context, @@ -255,18 +309,29 @@ 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); - } - result - }, - ); + let outcome = match result { + Ok(value) => CallbackInvocationOutcome::Returned(value), + Err(report) => CallbackInvocationOutcome::Threw(report), + }; + let completed = complete(scope, outcome); - match result { - Ok(value) => CallbackInvocationOutcome::Returned(value), - Err(report) => CallbackInvocationOutcome::Threw(report), - } + if let Some(previous) = previous_window_event { + if defer_window_event_restore && let Some(host_ptr) = invocation.host_ptr { + crate::context_bootstrap::enqueue_window_event_restore_after_microtask_checkpoint( + scope, + host_ptr, + invocation.relevant_identity, + relevant_context, + previous, + ); + } else { + let global = relevant_context.global(scope); + let _ = global.set(scope, v8str(scope, WINDOW_EVENT_SLOT).into(), previous); + } + } + completed + }, + ) } } diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index b5f0011870..b25ed70bdd 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -328,6 +328,7 @@ pub(crate) use self::message_ports::{ }; use self::message_ports::{schedule_host_callback, schedule_scope_callback}; pub(crate) use self::microtask_checkpoint::{ + enqueue_window_event_restore_after_microtask_checkpoint, install_agent_microtask_checkpoint_tasks, run_end_of_microtask_checkpoint_tasks, }; pub(crate) use self::navigation_bootstrap::{ diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/callbacks.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/callbacks.rs index 1facc61bc9..68d23591ff 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/callbacks.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/callbacks.rs @@ -1,4 +1,5 @@ use super::*; +use crate::util::context_host_ptr_from_global_bridge; pub(crate) fn simple_event_target_add_event_listener_callback<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -37,5 +38,7 @@ pub(crate) fn simple_event_target_dispatch_event_callback<'s>( rv.set(v8::Boolean::new(scope, true).into()); return; }; + let _explicit_dispatch_scope = context_host_ptr_from_global_bridge(scope) + .map(|host_ptr| unsafe { &mut *host_ptr }.enter_explicit_event_dispatch_scope()); simple_object_event_target_dispatch(scope, &args, slot_name, &mut rv); } diff --git a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs index c8be775661..bdd8c1fed9 100644 --- a/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs +++ b/moli-renderer-v8/src/context_bootstrap/media_queries/events/simple_event_target/dispatch.rs @@ -257,28 +257,29 @@ fn invoke_simple_event_callback_with_invocation<'s>( let target_name = simple_event_target_interface_name(scope, callback_target); host.schedule_dom_debugger_event_listener_pause_for_interface(event_type, &target_name) }); - match CallbackInvoker::invoke( + CallbackInvoker::invoke_event_and_then( scope, "event listener", "simple event listener threw", CallbackExceptionLogLevel::Debug, callback_name, invocation, - ) { - CallbackInvocationOutcome::Returned(value) => Some(value), - CallbackInvocationOutcome::Threw(report) => { - if let Some(host_ptr) = host_ptr { - report_event_callback_exception( - scope, - host_ptr, - event_type, - relevant_identity, - None, - &report, - ); + |scope, outcome| match outcome { + CallbackInvocationOutcome::Returned(value) => Some(value), + CallbackInvocationOutcome::Threw(report) => { + if let Some(host_ptr) = host_ptr { + report_event_callback_exception( + scope, + host_ptr, + event_type, + relevant_identity, + None, + &report, + ); + } + None } - None - } - CallbackInvocationOutcome::Retired => None, - } + CallbackInvocationOutcome::Retired => None, + }, + ) } diff --git a/moli-renderer-v8/src/context_bootstrap/microtask_checkpoint.rs b/moli-renderer-v8/src/context_bootstrap/microtask_checkpoint.rs index f520fbdf56..6e6dfaf0bd 100644 --- a/moli-renderer-v8/src/context_bootstrap/microtask_checkpoint.rs +++ b/moli-renderer-v8/src/context_bootstrap/microtask_checkpoint.rs @@ -10,6 +10,38 @@ enum AgentMicrotaskCheckpointTask { context: v8::Global, transaction: v8::Global, }, + RestoreWindowEvent { + host_ptr: *mut crate::native_bridge::JsContextHost, + relevant_identity: Option, + context: v8::Global, + previous: v8::Global, + }, +} + +pub(crate) fn enqueue_window_event_restore_after_microtask_checkpoint( + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut crate::native_bridge::JsContextHost, + relevant_identity: Option, + context: v8::Local<'_, v8::Context>, + previous: v8::Local<'_, v8::Value>, +) { + if scope.get_slot::().is_none() { + assert!( + scope.set_slot(AgentMicrotaskCheckpointTasks::default()), + "agent checkpoint state should be installed once on first use" + ); + } + let task = AgentMicrotaskCheckpointTask::RestoreWindowEvent { + host_ptr, + relevant_identity, + context: v8::Global::new(scope, context), + previous: v8::Global::new(scope, previous), + }; + scope + .get_slot_mut::() + .expect("agent checkpoint state should exist after installation") + .tasks + .push(task); } pub(crate) fn install_agent_microtask_checkpoint_tasks(isolate: &mut v8::Isolate) { @@ -50,14 +82,27 @@ pub(crate) fn run_end_of_microtask_checkpoint_tasks(scope: &mut v8::PinScope<'_, return; }; + let mut window_event_restores = Vec::new(); for task in tasks { match task { AgentMicrotaskCheckpointTask::DeactivateIndexedDbTransaction { context, transaction, } => run_indexed_db_transaction_deactivation(scope, context, transaction), + AgentMicrotaskCheckpointTask::RestoreWindowEvent { + host_ptr, + relevant_identity, + context, + previous, + } => { + window_event_restores.push((host_ptr, relevant_identity, context, previous)); + } } } + for (host_ptr, relevant_identity, context, previous) in window_event_restores.into_iter().rev() + { + restore_window_event(scope, host_ptr, relevant_identity, context, previous); + } } fn run_indexed_db_transaction_deactivation( @@ -73,3 +118,26 @@ fn run_indexed_db_transaction_deactivation( transaction, ); } + +fn restore_window_event( + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut crate::native_bridge::JsContextHost, + relevant_identity: Option, + context: v8::Global, + previous: v8::Global, +) { + if relevant_identity.is_some_and(|identity| { + !unsafe { &*host_ptr }.window_execution_context_identity_is_current(identity) + }) { + return; + } + let context = v8::Local::new(scope, &context); + let scope = &mut v8::ContextScope::new(scope, context); + let global = context.global(scope); + let previous = v8::Local::new(scope, &previous); + let _ = global.set( + scope, + crate::util::v8str(scope, crate::host::WINDOW_EVENT_SLOT).into(), + previous, + ); +} diff --git a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs index 86a70ed2d3..a8b8dbf6d4 100644 --- a/moli-renderer-v8/src/context_bootstrap/runtime_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/runtime_state.rs @@ -630,11 +630,11 @@ fn window_length_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) + 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 e8d260c44c..c92c1d71cf 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events.rs @@ -158,9 +158,10 @@ pub(crate) const BODY_OR_FRAMESET_WINDOW_EVENT_HANDLER_PROPERTIES: &[&str] = &[ 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 13c17ea0bc..f1e56ca04d 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events/accessors.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events/accessors.rs @@ -142,12 +142,37 @@ 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> { + 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); + object_own_hidden_value(scope, global, WINDOW_EVENT_SLOT) + }); + target_event.or_else(|| global_hidden_value(scope, WINDOW_EVENT_SLOT)) +} + pub(in crate::context_bootstrap) fn window_event_setter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/context_bootstrap/window_events/error.rs b/moli-renderer-v8/src/context_bootstrap/window_events/error.rs index c9d2940a10..3b297a6eb3 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_events/error.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_events/error.rs @@ -132,6 +132,21 @@ pub(crate) fn dispatch_window_error_event_with_details<'s>( colno: u32, error_value: Option>, ) -> std::result::Result<(), String> { + let current_context = scope.get_current_context(); + let reporting_owner = unsafe { &*host_ptr } + .window_execution_context_identity_for_v8_context(scope, current_context) + .map(|identity| identity.owner()); + let _error_reporting_scope = if let Some(owner) = reporting_owner { + let Some(reporting_scope) = + (unsafe { &mut *host_ptr }).enter_window_error_reporting_scope(owner) + else { + return Ok(()); + }; + Some(reporting_scope) + } else { + None + }; + let global = scope.get_current_context().global(scope); ensure_window_reflecting_body_onerror_handler(scope); let error_value = error_value.unwrap_or_else(|| v8::null(scope).into()); diff --git a/moli-renderer-v8/src/host/events.rs b/moli-renderer-v8/src/host/events.rs index 3affb807d0..41817c1631 100644 --- a/moli-renderer-v8/src/host/events.rs +++ b/moli-renderer-v8/src/host/events.rs @@ -302,28 +302,29 @@ fn invoke_prepared_event_callback_with_receiver<'s>( current_event, ) .with_execution_context_currentness(host_ptr, relevant_identity); - match CallbackInvoker::invoke( + CallbackInvoker::invoke_event_and_then( scope, "event listener", "host event listener threw", crate::exception_reporting::CallbackExceptionLogLevel::Debug, callback_name, invocation, - ) { - CallbackInvocationOutcome::Returned(value) => Some(value), - CallbackInvocationOutcome::Threw(report) => { - report_event_callback_exception( - scope, - host_ptr, - event_type, - relevant_identity, - None, - &report, - ); - None - } - CallbackInvocationOutcome::Retired => None, - } + |scope, outcome| match outcome { + CallbackInvocationOutcome::Returned(value) => Some(value), + CallbackInvocationOutcome::Threw(report) => { + report_event_callback_exception( + scope, + host_ptr, + event_type, + relevant_identity, + None, + &report, + ); + None + } + CallbackInvocationOutcome::Retired => None, + }, + ) } fn invoke_event_handler_property<'s>( @@ -573,10 +574,6 @@ pub(crate) fn report_event_callback_exception<'s>( child_handle: Option, report: &V8ExceptionReport, ) { - if event_type == "error" { - return; - } - let child_handle = child_handle.or_else(|| { relevant_identity.and_then(|identity| match identity.dispatch_scope() { crate::native_bridge::OwnerDispatchScope::Child(handle) => Some(handle), @@ -585,6 +582,25 @@ pub(crate) fn report_event_callback_exception<'s>( }) }); if let Some(handle) = child_handle { + let reporting_owner = relevant_identity + .map(|identity| identity.owner()) + .or_else(|| { + unsafe { &*host_ptr }.current_window_execution_context_owner( + crate::native_bridge::OwnerDispatchScope::Child(handle), + ) + }); + let _error_reporting_scope = if let Some(owner) = reporting_owner { + let Some(reporting_scope) = + (unsafe { &mut *host_ptr }).enter_window_error_reporting_scope(owner) + else { + return; + }; + Some(reporting_scope) + } else if event_type == "error" { + return; + } else { + None + }; let event = { let host = unsafe { &mut *host_ptr }; host.child_browsing_context_window_wrapper(scope, handle) diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 76792e27c9..8dd8c25455 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -370,6 +370,9 @@ impl JsContextHost { child_window_event_listeners: HashMap::new(), next_child_window_event_registration_id: 0, event_callbacks: Default::default(), + explicit_event_dispatch_depth: 0, + event_callback_invocation_depth: 0, + active_window_error_report_owners: HashSet::new(), browser_context_runtime, top_level_navigation_handoff_tx, service_worker_task_tx, diff --git a/moli-renderer-v8/src/native_bridge/context_host/event_callbacks.rs b/moli-renderer-v8/src/native_bridge/context_host/event_callbacks.rs index ef8b998649..d7cba57beb 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/event_callbacks.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/event_callbacks.rs @@ -17,6 +17,55 @@ pub(crate) struct PreparedEventCallback { relevant_identity: Option, } +pub(crate) struct WindowErrorReportingScope { + host_ptr: *mut JsContextHost, + owner: WindowExecutionContextOwner, +} + +pub(crate) struct ExplicitEventDispatchScope { + host_ptr: *mut JsContextHost, +} + +pub(crate) struct EventCallbackInvocationScope { + host_ptr: *mut JsContextHost, + outermost: bool, +} + +impl EventCallbackInvocationScope { + pub(crate) fn is_outermost(&self) -> bool { + self.outermost + } +} + +impl Drop for EventCallbackInvocationScope { + fn drop(&mut self) { + let host = unsafe { &mut *self.host_ptr }; + host.event_callback_invocation_depth = host + .event_callback_invocation_depth + .checked_sub(1) + .expect("event callback invocation scope must be active"); + } +} + +impl Drop for ExplicitEventDispatchScope { + fn drop(&mut self) { + let host = unsafe { &mut *self.host_ptr }; + host.explicit_event_dispatch_depth = host + .explicit_event_dispatch_depth + .checked_sub(1) + .expect("explicit EventTarget dispatch scope must be active"); + } +} + +impl Drop for WindowErrorReportingScope { + fn drop(&mut self) { + let removed = unsafe { &mut *self.host_ptr } + .active_window_error_report_owners + .remove(&self.owner); + debug_assert!(removed, "Window error reporting scope must be active"); + } +} + impl PreparedEventCallback { pub(crate) fn callback<'s>( &self, @@ -80,6 +129,36 @@ impl EventCallbackRegistry { } impl JsContextHost { + pub(crate) fn enter_event_callback_invocation_scope(&mut self) -> EventCallbackInvocationScope { + let outermost = self.event_callback_invocation_depth == 0; + self.event_callback_invocation_depth += 1; + EventCallbackInvocationScope { + host_ptr: self, + outermost, + } + } + + pub(crate) fn enter_explicit_event_dispatch_scope(&mut self) -> ExplicitEventDispatchScope { + self.explicit_event_dispatch_depth += 1; + ExplicitEventDispatchScope { host_ptr: self } + } + + pub(crate) fn explicit_event_dispatch_is_active(&self) -> bool { + self.explicit_event_dispatch_depth != 0 + } + + pub(crate) fn enter_window_error_reporting_scope( + &mut self, + owner: WindowExecutionContextOwner, + ) -> Option { + self.active_window_error_report_owners + .insert(owner) + .then(|| WindowErrorReportingScope { + host_ptr: self, + owner, + }) + } + pub(crate) fn register_target_event_listener<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 3b99e6c4f5..1316b2a344 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -982,6 +982,9 @@ pub(crate) struct JsContextHost { HashMap>>, next_child_window_event_registration_id: u64, event_callbacks: event_callbacks::EventCallbackRegistry, + explicit_event_dispatch_depth: usize, + event_callback_invocation_depth: usize, + active_window_error_report_owners: HashSet, browser_context_runtime: crate::runtime::RendererBrowserContextRuntime, top_level_navigation_handoff_tx: crate::page_task_queue::RendererTopLevelNavigationHandoffSender, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs index 0e28e860e6..90549b2e05 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/events_selection_storage.rs @@ -2175,6 +2175,192 @@ fn cross_realm_listener_throw_reports_listener_global_not_target_global() { r#"[["function",true,true],["function-window-event-restored",true],["object",true,true],["object-window-event-restored",true]]"# ); } + +#[test] +fn listener_exception_reporting_preserves_outer_realm_window_event() { + let mut vm = new_storage_test_vm("https://event-listener-nested-window-event.test/"); + + vm.eval( + r#" + (() => { + const host = document.body || document.documentElement || document; + const listenerFrame = document.createElement("iframe"); + listenerFrame.srcdoc = `