diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index e6cc54de40..03d86c0cab 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -257,7 +257,7 @@ pub(crate) use 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, require_same_origin_window_receiver}; mod window_runtime; pub(crate) use form_data_runtime::{ diff --git a/moli-renderer-v8/src/context_bootstrap/window_receiver.rs b/moli-renderer-v8/src/context_bootstrap/window_receiver.rs index 4b94cb4bae..41bff66cc9 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_receiver.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_receiver.rs @@ -21,12 +21,12 @@ pub(crate) fn is_window_receiver<'s>( && context_host_ptr_from_context_slot(current_context).is_some() } -pub(in crate::context_bootstrap) fn require_same_origin_window_receiver<'s>( +pub(crate) fn require_same_origin_window_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Object>, lenient_this: bool, ) -> bool { - // An extracted accessor bypasses WindowProxy's property access check. + // An extracted method or accessor bypasses WindowProxy's property access check. // Authorize native globals before reading slots, which can themselves // trigger V8's cross-origin fallback. Leniency only applies to objects // without the Window brand, never to a cross-origin Window. @@ -43,7 +43,7 @@ pub(in crate::context_bootstrap) fn require_same_origin_window_receiver<'s>( return true; } if !lenient_this { - throw_type_error(scope, "Window getter called on incompatible receiver."); + throw_type_error(scope, "Window member called on incompatible receiver."); } return false; } diff --git a/moli-renderer-v8/src/queue_microtask.rs b/moli-renderer-v8/src/queue_microtask.rs index 27b1f31931..8171718a00 100644 --- a/moli-renderer-v8/src/queue_microtask.rs +++ b/moli-renderer-v8/src/queue_microtask.rs @@ -12,7 +12,16 @@ use crate::{ }; // The two global surfaces enqueue the same browser-created V8 trampoline. -pub(crate) use queue_microtask_callback as window_queue_microtask_callback; +pub(crate) fn window_queue_microtask_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + rv: v8::ReturnValue<'_, v8::Value>, +) { + if !crate::context_bootstrap::require_same_origin_window_receiver(scope, args.this(), false) { + return; + } + queue_microtask_callback(scope, args, rv); +} pub(crate) use queue_microtask_callback as worker_queue_microtask_callback; #[derive(webidl::WebIdlArgs)] diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 53f677bf4d..3f964c24c8 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2481,3 +2481,4 @@ mod sqlite_web_storage; mod svg_number_list_native_receivers; mod webtransport_stream_interface_exposure; mod window_restricted_accessors; +mod window_scheduling_receivers; diff --git a/moli-renderer-v8/src/script_vm/tests/window_scheduling_receivers.rs b/moli-renderer-v8/src/script_vm/tests/window_scheduling_receivers.rs new file mode 100644 index 0000000000..81ae941dc2 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/window_scheduling_receivers.rs @@ -0,0 +1,103 @@ +use super::*; + +#[test] +fn window_scheduling_methods_accept_native_bridge_receivers_without_inheriting_their_brand() { + let mut vm = new_storage_test_vm("https://window-scheduling-receivers.test/"); + assert_eq!( + vm.eval( + r#" +(() => { + const native = __moliNativeBridge.window; + const cancellations = {setTimeout:'clearTimeout', setInterval:'clearInterval', + requestAnimationFrame:'cancelAnimationFrame', requestIdleCallback:'cancelIdleCallback'}; + let traps = 0, conversions = 0; + const proxy = new Proxy(native, {get() { traps++; throw new Error('unexpected trap'); }}); + const number = {valueOf() { conversions++; return 2147483647; }}; + globalThis.__nativeSchedulingCallbacks = 0; + for (const name of [...Object.keys(cancellations), ...Object.values(cancellations), 'queueMicrotask']) { + const args = cancellations[name] || name === 'queueMicrotask' ? + [() => { __nativeSchedulingCallbacks++; }, number] : [number]; + for (const receiver of [proxy, Object.create(native), {__moliNativeBridge}]) { + try { + window[name].apply(receiver, args); + return name + ' accepted forged receiver'; + } catch (error) { + if (!(error instanceof TypeError)) return name + ' wrong receiver error'; + } + } + if (conversions !== 0) return name + ' converted before rejecting forged receiver'; + const id = window[name].apply(native, args); + if (cancellations[name]) { + if (!Number.isInteger(id) || id <= 0) return name + ' native timer handle'; + window[cancellations[name]].call(native, id); + } else if (id !== undefined) { + return name + ' native return value'; + } + conversions = 0; + } + return traps === 0 ? 'ok' : 'receiver invoked traps'; +})() +"#, + ) + .unwrap(), + "ok", + ); + assert_eq!(vm.eval("__nativeSchedulingCallbacks").unwrap(), "1"); +} + +#[tokio::test(flavor = "current_thread")] +async fn window_scheduling_methods_validate_receivers_before_conversion_and_queuing() { + let script = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/window-scheduling-receivers.js" + )); + let server = StaticHttpServer::spawn_with_bodies(vec![ + "
local".to_owned(), + "remote".to_owned(), + ]) + .await; + let loader = static_http_loader([ + server.resolve_entry("localhost"), + server.resolve_entry("127.0.0.1"), + ]); + let parent_url = server.url_for_host("localhost", "/page.html"); + let same_url = server.url_for_host("localhost", "/local.html"); + let cross_url = server.url_for_host("127.0.0.1", "/remote.html"); + let mut vm = new_storage_page_task_executor_test_vm_with_loader(parent_url.as_str(), &loader); + vm.exec( + &format!( + r#" +if (!document.documentElement) document.appendChild(document.createElement('html')); +if (!document.body) document.documentElement.appendChild(document.createElement('body')); +globalThis.__schedulingReceiversResult = null; +({script})({{sameURL: {same_url:?}, crossURL: {cross_url:?}}}).then( + result => {{ __schedulingReceiversResult = result; }}, + error => {{ __schedulingReceiversResult = {{error: String(error)}}; }} +); +"#, + same_url = same_url.as_str(), + cross_url = cross_url.as_str(), + ), + None, + ) + .unwrap(); + advance_page_task_executor_until_eval_equals( + &mut vm, + &loader, + "String(__schedulingReceiversResult !== null)", + "true", + "Window scheduling receiver regression should finish", + ) + .await; + let result: serde_json::Value = serde_json::from_str( + &vm.eval("JSON.stringify(__schedulingReceiversResult)") + .expect("Window scheduling receiver observations"), + ) + .unwrap(); + assert_eq!(result["checks"], 863, "{result}"); + assert_eq!(result["failures"], serde_json::json!([]), "{result}"); + assert_eq!( + server.finish_targets().await, + ["/local.html", "/remote.html"] + ); +} diff --git a/moli-renderer-v8/src/window_host.rs b/moli-renderer-v8/src/window_host.rs index 2d2c9b45b4..08c9b4bda8 100644 --- a/moli-renderer-v8/src/window_host.rs +++ b/moli-renderer-v8/src/window_host.rs @@ -11,6 +11,7 @@ use super::{ context_bootstrap::is_simple_event_target, context_bootstrap::mark_event_trusted, context_bootstrap::performance_slot_number, + context_bootstrap::require_same_origin_window_receiver, context_bootstrap::set_event_trusted, context_bootstrap::simple_event_target_add_event_listener_callback, context_bootstrap::simple_event_target_dispatch_event_callback, @@ -634,6 +635,9 @@ pub(super) fn window_set_timeout_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_same_origin_window_receiver(scope, args.this(), false) { + return; + } rv.set_uint32(0); let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return; @@ -679,6 +683,9 @@ pub(super) fn window_set_interval_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_same_origin_window_receiver(scope, args.this(), false) { + return; + } rv.set_uint32(0); let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return; @@ -817,8 +824,7 @@ 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"); + if !require_same_origin_window_receiver(scope, args.this(), false) { return; } let id_val = args.get(0); @@ -831,8 +837,7 @@ pub(super) fn window_cancel_animation_frame_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"); + if !require_same_origin_window_receiver(scope, args.this(), false) { return; } let Some(parsed) = webidl::parse_args::