From 59eef217f96e2d88b6eca4aa9d2c0bf559367b26 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 8 Sep 2026 15:27:05 +0800 Subject: [PATCH] fix(timers): preserve the initiating script nonce for string handlers --- .../wpt-cross-current/failed-cases.txt | 2 - .../wpt-cross-current/passed-cases.txt | 2 + moli-renderer-v8/src/host/timers.rs | 16 ++- moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../src/script_vm/tests/string_timers.rs | 99 +++++++++++++++++++ 5 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/string_timers.rs diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 9c433570c0..823cf4686c 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -2972,8 +2972,6 @@ html/semantics/scripting-1/the-script-element/microtasks/checkpoint-after-worker html/semantics/scripting-1/the-script-element/module/crossorigin.html html/semantics/scripting-1/the-script-element/module/dynamic-import/alpha/base-url-worker-importScripts.html html/semantics/scripting-1/the-script-element/module/dynamic-import/code-cache-base-url.html -html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-nonce-classic.html -html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-nonce-module.html html/semantics/scripting-1/the-script-element/module/inline-async-execorder.html html/semantics/scripting-1/the-script-element/moving-between-documents/ordering/delay-load-event-1.html html/semantics/scripting-1/the-script-element/moving-between-documents/ordering/delay-load-event-2.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index b2a6900302..2000d86fb8 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -6508,6 +6508,8 @@ html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compi html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-base-url-inline-module.html html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-classic.html html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-module.html +html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-nonce-classic.html +html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-nonce-module.html html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-of-promise-result.html html/semantics/scripting-1/the-script-element/module/dynamic-import/string-compilation-other-document.html html/semantics/scripting-1/the-script-element/module/dynamic-import/v8-code-cache.html diff --git a/moli-renderer-v8/src/host/timers.rs b/moli-renderer-v8/src/host/timers.rs index 104017ad88..b90e43f197 100644 --- a/moli-renderer-v8/src/host/timers.rs +++ b/moli-renderer-v8/src/host/timers.rs @@ -18,7 +18,8 @@ use crate::{ page_task_queue::RendererPageTimerSelection, script_provenance::CompiledStringProvenance, util::{ - context_host_ptr_from_global_bridge, create_script_origin_with_base_url, get_private_value, + context_host_ptr_from_global_bridge, create_script_origin_with_base_url_and_nonce, + get_private_value, script_nonce_from_host_defined_options, }, }; use moli_time::{ @@ -44,6 +45,8 @@ struct ScheduledTimerSource { use_target_context: bool, source: String, provenance: CompiledStringProvenance, + // A snapshot of the initiating script, not the eventual timer caller/realm. + script_nonce: Option, } struct ScheduledTimerFunction { @@ -493,6 +496,9 @@ impl HostTimeoutScheduler { owner: HostTimerOwner, extra_args: Vec>, ) -> u32 { + let script_nonce = scope + .get_current_host_defined_options() + .and_then(|options| script_nonce_from_host_defined_options(scope, options)); let Some(owner) = scheduled_timer_owner_for_target(scope, owner, Some(receiver), context) else { return 0; @@ -519,6 +525,7 @@ impl HostTimeoutScheduler { use_target_context, source, provenance, + script_nonce, }), owner, is_interval: false, @@ -541,6 +548,9 @@ impl HostTimeoutScheduler { owner: HostTimerOwner, extra_args: Vec>, ) -> u32 { + let script_nonce = scope + .get_current_host_defined_options() + .and_then(|options| script_nonce_from_host_defined_options(scope, options)); let Some(owner) = scheduled_timer_owner_for_target(scope, owner, Some(receiver), context) else { return 0; @@ -567,6 +577,7 @@ impl HostTimeoutScheduler { use_target_context, source, provenance, + script_nonce, }), owner, is_interval: true, @@ -1268,11 +1279,12 @@ fn run_window_timer_source( exception: None, })); }; - let origin = create_script_origin_with_base_url( + let origin = create_script_origin_with_base_url_and_nonce( &mut scope, source.provenance.source_url().as_str(), 0, Some(source.provenance.module_base_url()), + source.script_nonce.as_deref(), ); let Some(script) = v8::Script::compile(&scope, source_value, Some(&origin)) else { let exception = scope.exception(); diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index dc699f9cb0..07723c1faa 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -15622,6 +15622,7 @@ mod queue_microtask; mod rendering_update; mod script_terminal_completion; mod streams; +mod string_timers; mod url_components; mod webidl_collections; mod webidl_fetch; diff --git a/moli-renderer-v8/src/script_vm/tests/string_timers.rs b/moli-renderer-v8/src/script_vm/tests/string_timers.rs new file mode 100644 index 0000000000..4f8aee4120 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/string_timers.rs @@ -0,0 +1,99 @@ +use super::*; +use crate::script_provenance::CompiledStringProvenance; + +fn execute_nonce_script(vm: &mut ScriptVm, source: &str, url: &Url, nonce: Option<&str>) { + vm.exec_in_enclosing_script_turn_with_provenance( + source, + &CompiledStringProvenance::at_url(url.clone()), + 0, + nonce, + true, + ) + .map_err(|error| error.into_anyhow()) + .expect("the initiating script should execute"); +} + +fn consume_string_timer(vm: &mut ScriptVm) { + assert!(matches!( + vm.run_next_timeout_for_test() + .expect("string timer should execute"), + crate::host::HostTimeoutRunResult::Consumed + )); +} + +fn assert_import_nonce(vm: &mut ScriptVm, base_url: &Url, nonce: Option<&str>) { + let request = vm + .document_runtime + .take_next_native_dynamic_module_import() + .expect("the timer should queue a dynamic import") + .into_dynamic_import_request(); + assert_eq!(request.specifier(), "./dependency.mjs"); + assert_eq!(request.base_url(), base_url); + assert_eq!(request.fetch_metadata().nonce.as_deref(), nonce); + assert!(!request.fetch_metadata().parser_inserted); + assert!(request.fetch_metadata().integrity.is_none()); +} + +#[test] +fn string_timers_preserve_nonce_for_timeout_interval_and_nested_source() { + for (source, timer_turns) in [ + (r#"setTimeout("import('./dependency.mjs')", 0);"#, 1), + ( + r#"globalThis.__nonceInterval = setInterval("clearInterval(__nonceInterval); import('./dependency.mjs')", 0);"#, + 1, + ), + ( + r#"setTimeout("setTimeout(\"import('./dependency.mjs')\", 0)", 0);"#, + 2, + ), + ] { + let mut vm = new_storage_test_vm("https://timer-nonce.test/page.html"); + let script_url = Url::parse("https://timer-nonce.test/scripts/initiator.js").unwrap(); + execute_nonce_script(&mut vm, source, &script_url, Some("initiating-nonce")); + for _ in 0..timer_turns { + consume_string_timer(&mut vm); + } + assert_import_nonce(&mut vm, &script_url, Some("initiating-nonce")); + } +} + +#[test] +fn string_timers_keep_nonce_snapshots_separate_for_identical_source_and_url() { + let mut vm = new_storage_test_vm("https://timer-nonce.test/page.html"); + let script_url = Url::parse("https://timer-nonce.test/scripts/same.js").unwrap(); + let nonces = [Some("first-nonce"), None, Some("last-nonce")]; + for nonce in nonces { + execute_nonce_script( + &mut vm, + r#"setTimeout("import('./dependency.mjs')", 0);"#, + &script_url, + nonce, + ); + } + for nonce in nonces { + consume_string_timer(&mut vm); + assert_import_nonce(&mut vm, &script_url, nonce); + } +} + +#[test] +fn string_timers_use_the_active_function_nonce_without_borrowing_the_callers_nonce() { + for initiating_nonce in [Some("function-nonce"), None] { + let mut vm = new_storage_test_vm("https://timer-nonce.test/page.html"); + let script_url = Url::parse("https://timer-nonce.test/scripts/function.js").unwrap(); + execute_nonce_script( + &mut vm, + r#"globalThis.scheduleFromInitiator = () => setTimeout("import('./dependency.mjs')", 0);"#, + &script_url, + initiating_nonce, + ); + execute_nonce_script( + &mut vm, + "scheduleFromInitiator();", + &Url::parse("https://timer-nonce.test/other/caller.js").unwrap(), + Some("caller-nonce"), + ); + consume_string_timer(&mut vm); + assert_import_nonce(&mut vm, &script_url, initiating_nonce); + } +}