From aee29b5ccfd41d4fbc6f51b08b3df4c0c6cb32eb Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 7 Sep 2026 14:41:55 +0800 Subject: [PATCH] fix(script): mute cross-origin classic script errors --- moli-parser/src/script_planning.rs | 6 + .../src/document_script_scheduler.rs | 2 + .../document_script_scheduler/async_queues.rs | 17 +- .../completion_port.rs | 1 + .../document_script_scheduler/owner_work.rs | 2 + .../page_task_adapter.rs | 1 + .../parser_runner.rs | 1 + .../document_script_scheduler/post_parse.rs | 9 +- moli-renderer-v8/src/dynamic_script_owner.rs | 3 + moli-renderer-v8/src/page_task_queue.rs | 1 + .../page_task_queue/resource_completions.rs | 1 + moli-renderer-v8/src/parser_script/payload.rs | 10 +- moli-renderer-v8/src/planning.rs | 223 +++++++++++++++++- .../src/runtime/page_vm/tests/lifecycle.rs | 1 + .../src/runtime/page_vm/tests/mod.rs | 1 + .../src/runtime/script_preloads.rs | 2 + moli-renderer-v8/src/script_vm.rs | 1 + .../src/script_vm/classic_script_exception.rs | 36 ++- .../src/script_vm/tests/post_parse.rs | 57 +++++ .../src/service_worker_runtime/events.rs | 1 + .../service/fetch_settlement.rs | 2 + 21 files changed, 353 insertions(+), 25 deletions(-) diff --git a/moli-parser/src/script_planning.rs b/moli-parser/src/script_planning.rs index dd4c73a396..b3e5c68201 100644 --- a/moli-parser/src/script_planning.rs +++ b/moli-parser/src/script_planning.rs @@ -287,6 +287,11 @@ pub struct ScriptFetchMetadata { /// not equivalent to whether the eventual fetch runs on a parser-owned /// scheduler lane. pub parser_inserted: bool, + /// Whether synchronous exceptions from this classic script must expose only + /// the cross-origin-safe `Script error.` surface. This is false while the + /// request is prepared and is populated from the response taint once an + /// external classic script has loaded. + pub muted_errors: bool, } impl ScriptFetchMetadata { @@ -308,6 +313,7 @@ impl ScriptFetchMetadata { nonce: normalize_non_empty_attr(nonce), fetch_priority: FetchPriorityHint::from_attribute(fetch_priority), parser_inserted: false, + muted_errors: false, } } diff --git a/moli-renderer-v8/src/document_script_scheduler.rs b/moli-renderer-v8/src/document_script_scheduler.rs index 1dd2b581fe..ba799a6061 100644 --- a/moli-renderer-v8/src/document_script_scheduler.rs +++ b/moli-renderer-v8/src/document_script_scheduler.rs @@ -578,6 +578,7 @@ mod tests { source_result: Ok(source.into()), source_bytes: None, network_result: None, + muted_errors: false, } } @@ -586,6 +587,7 @@ mod tests { source_result: Err(error.into()), source_bytes: None, network_result: None, + muted_errors: false, } } diff --git a/moli-renderer-v8/src/document_script_scheduler/async_queues.rs b/moli-renderer-v8/src/document_script_scheduler/async_queues.rs index a83598ee0f..b3fcebe862 100644 --- a/moli-renderer-v8/src/document_script_scheduler/async_queues.rs +++ b/moli-renderer-v8/src/document_script_scheduler/async_queues.rs @@ -347,6 +347,7 @@ impl AsyncFallbackQueue { source_result: Err(error), source_bytes: None, network_result, + muted_errors: false, }), }); } @@ -388,6 +389,7 @@ impl AsyncFallbackQueue { entry.script.clone(), source, completion.outcome.source_bytes, + completion.outcome.muted_errors, ); entry.load_failure = None; } @@ -396,6 +398,7 @@ impl AsyncFallbackQueue { source_result: Err(error), source_bytes: completion.outcome.source_bytes, network_result: completion.outcome.network_result, + muted_errors: false, }); } } @@ -485,10 +488,11 @@ fn parse_time_task_from_load_outcome( source_result, source_bytes, network_result, + muted_errors, } = outcome; match source_result { Ok(source) => ParseTimeDocumentScriptTask::classic_async_script( - prepared_script_with_loaded_source(script, source, source_bytes), + prepared_script_with_loaded_source(script, source, source_bytes, muted_errors), load_delay_binding, ), Err(error) => ParseTimeDocumentScriptTask::async_script_failure( @@ -509,10 +513,11 @@ fn async_phase_page_task_from_load_outcome( source_result, source_bytes, network_result, + muted_errors, } = outcome; match source_result { Ok(source) => PostParseDocumentScriptTask::async_script( - prepared_script_with_loaded_source(script, source, source_bytes), + prepared_script_with_loaded_source(script, source, source_bytes, muted_errors), load_delay_binding, ), Err(error) => PostParseDocumentScriptTask::async_script_load_failure( @@ -530,8 +535,14 @@ fn fallback_entry_from_parse_time_entry(entry: ParseTimeAsyncEntry) -> AsyncFall source_result: Ok(source), source_bytes, network_result: _, + muted_errors, }) => AsyncFallbackEntry { - script: prepared_script_with_loaded_source(entry.original, source, source_bytes), + script: prepared_script_with_loaded_source( + entry.original, + source, + source_bytes, + muted_errors, + ), load_delay_binding: entry.load_delay_binding, awaiting_completion: false, source_load: None, diff --git a/moli-renderer-v8/src/document_script_scheduler/completion_port.rs b/moli-renderer-v8/src/document_script_scheduler/completion_port.rs index c33148e708..b961adfdf8 100644 --- a/moli-renderer-v8/src/document_script_scheduler/completion_port.rs +++ b/moli-renderer-v8/src/document_script_scheduler/completion_port.rs @@ -43,6 +43,7 @@ mod tests { source_result: Ok("ready".to_owned()), source_bytes: None, network_result: None, + muted_errors: false, } } diff --git a/moli-renderer-v8/src/document_script_scheduler/owner_work.rs b/moli-renderer-v8/src/document_script_scheduler/owner_work.rs index bcf5348d90..38daa64194 100644 --- a/moli-renderer-v8/src/document_script_scheduler/owner_work.rs +++ b/moli-renderer-v8/src/document_script_scheduler/owner_work.rs @@ -354,6 +354,7 @@ fn page_owned_document_script_work_from_source_load_outcome( source_result, source_bytes, network_result, + muted_errors, } = outcome; match source_result { Ok(source) => PageOwnedDocumentScriptWork::Script { @@ -362,6 +363,7 @@ fn page_owned_document_script_work_from_source_load_outcome( script, source, source_bytes, + muted_errors, )), runtime_script_claim, source_network_result: network_result, diff --git a/moli-renderer-v8/src/document_script_scheduler/page_task_adapter.rs b/moli-renderer-v8/src/document_script_scheduler/page_task_adapter.rs index 0ba73438bf..d657d9c6ff 100644 --- a/moli-renderer-v8/src/document_script_scheduler/page_task_adapter.rs +++ b/moli-renderer-v8/src/document_script_scheduler/page_task_adapter.rs @@ -230,6 +230,7 @@ mod tests { source_result: Ok("window.ready = true;".to_owned()), source_bytes: None, network_result: None, + muted_errors: false, }, )); diff --git a/moli-renderer-v8/src/document_script_scheduler/parser_runner.rs b/moli-renderer-v8/src/document_script_scheduler/parser_runner.rs index d369c3b96f..dc7ed6b605 100644 --- a/moli-renderer-v8/src/document_script_scheduler/parser_runner.rs +++ b/moli-renderer-v8/src/document_script_scheduler/parser_runner.rs @@ -127,6 +127,7 @@ impl ParserDeferredClassicSourceLoadRequest { source_result: Err(message.into()), source_bytes: None, network_result: None, + muted_errors: false, }, ) } diff --git a/moli-renderer-v8/src/document_script_scheduler/post_parse.rs b/moli-renderer-v8/src/document_script_scheduler/post_parse.rs index 77a59882a7..c68289ba5e 100644 --- a/moli-renderer-v8/src/document_script_scheduler/post_parse.rs +++ b/moli-renderer-v8/src/document_script_scheduler/post_parse.rs @@ -107,11 +107,16 @@ impl ResolvedDeferPhaseScript { source_result, source_bytes, network_result, + muted_errors, } = outcome; match source_result { Ok(source) => { - self.script = - prepared_script_with_loaded_source(self.script.clone(), source, source_bytes); + self.script = prepared_script_with_loaded_source( + self.script.clone(), + source, + source_bytes, + muted_errors, + ); self.classic_source_state = ParserDeferredClassicSourceState::Ready(network_result); } Err(error) => { diff --git a/moli-renderer-v8/src/dynamic_script_owner.rs b/moli-renderer-v8/src/dynamic_script_owner.rs index 6b4a5ea1c8..fa312cfdd7 100644 --- a/moli-renderer-v8/src/dynamic_script_owner.rs +++ b/moli-renderer-v8/src/dynamic_script_owner.rs @@ -2112,6 +2112,7 @@ impl DynamicScriptOwner { entry.script.clone(), source, completion.outcome.source_bytes.clone(), + completion.outcome.muted_errors, ); entry.ready_state = DynamicScriptReadyState::Ready { order, @@ -2290,6 +2291,7 @@ mod tests { source_result: Ok(source.into()), source_bytes: None, network_result: None, + muted_errors: false, }, } } @@ -2301,6 +2303,7 @@ mod tests { source_result: Err(error.into()), source_bytes: None, network_result: None, + muted_errors: false, }, } } diff --git a/moli-renderer-v8/src/page_task_queue.rs b/moli-renderer-v8/src/page_task_queue.rs index b34e2727c1..2fcf794ba2 100644 --- a/moli-renderer-v8/src/page_task_queue.rs +++ b/moli-renderer-v8/src/page_task_queue.rs @@ -844,6 +844,7 @@ mod tests { source_result: Ok(source.into()), source_bytes: None, network_result: None, + muted_errors: false, } } diff --git a/moli-renderer-v8/src/page_task_queue/resource_completions.rs b/moli-renderer-v8/src/page_task_queue/resource_completions.rs index 87de7745b3..5992a30dab 100644 --- a/moli-renderer-v8/src/page_task_queue/resource_completions.rs +++ b/moli-renderer-v8/src/page_task_queue/resource_completions.rs @@ -860,6 +860,7 @@ mod tests { source_result: Ok(format!("window.defer{parser_position} = true")), source_bytes: None, network_result: None, + muted_errors: false, }, ) } diff --git a/moli-renderer-v8/src/parser_script/payload.rs b/moli-renderer-v8/src/parser_script/payload.rs index ff4eebd988..281db53e07 100644 --- a/moli-renderer-v8/src/parser_script/payload.rs +++ b/moli-renderer-v8/src/parser_script/payload.rs @@ -121,10 +121,15 @@ impl ParserPreparedClassicScript { source_result, source_bytes, network_result, + muted_errors, } = outcome; let source = source_result?; - *self.script = - prepared_script_with_loaded_source((*self.script).clone(), source, source_bytes); + *self.script = prepared_script_with_loaded_source( + (*self.script).clone(), + source, + source_bytes, + muted_errors, + ); Ok(network_result) } @@ -269,6 +274,7 @@ impl ParserClassicScriptSourceResult { source_result: result, source_bytes: None, network_result: None, + muted_errors: false, }, ) } diff --git a/moli-renderer-v8/src/planning.rs b/moli-renderer-v8/src/planning.rs index be96b765a9..9fdbef591e 100644 --- a/moli-renderer-v8/src/planning.rs +++ b/moli-renderer-v8/src/planning.rs @@ -13,7 +13,9 @@ use url::Url; use crate::{ network::{RendererResourceTaskRunner, ResourceRequestClient}, - types::{ScriptKind, ScriptMode, SharedNavigationResponseResult}, + types::{ + AsyncSubresourceFetchResponseFilter, ScriptKind, ScriptMode, SharedNavigationResponseResult, + }, }; pub(crate) use moli_parser::{ @@ -189,6 +191,7 @@ impl SharedScriptSourceLoad { source_result: Ok(source.into()), source_bytes: None, network_result: None, + muted_errors: false, }); load } @@ -200,6 +203,7 @@ impl SharedScriptSourceLoad { source_result: Err(error.into()), source_bytes: None, network_result: None, + muted_errors: false, }); load } @@ -214,6 +218,7 @@ impl SharedScriptSourceLoad { source_result, source_bytes: None, network_result, + muted_errors: false, }); load } @@ -230,6 +235,7 @@ impl SharedScriptSourceLoad { source_result: task.await, source_bytes: None, network_result: None, + muted_errors: false, }); }); load @@ -260,10 +266,21 @@ impl Drop for SharedScriptSourceLoadCompleter { } pub(crate) fn prepared_script_with_loaded_source( - script: PreparedScript, + mut script: PreparedScript, source: String, source_bytes: Option>, + muted_errors: bool, ) -> PreparedScript { + debug_assert!( + !muted_errors + || (script.kind == ScriptKind::Classic + && script.source_kind == crate::types::ScriptSourceKind::External), + "only external classic scripts can carry muted errors" + ); + script.fetch_metadata.muted_errors = muted_errors; + if muted_errors { + script.base_url = Url::parse("about:blank").expect("about:blank should be a valid URL"); + } if script.kind == ScriptKind::Module && is_webassembly_module_script_url(&script.url) && let Some(bytes) = source_bytes @@ -282,6 +299,7 @@ pub(crate) struct PreparedScriptSourceLoadOutcome { pub(crate) source_result: std::result::Result, pub(crate) source_bytes: Option>, pub(crate) network_result: Option, + pub(crate) muted_errors: bool, } pub(crate) fn script_preload_network_result( @@ -303,12 +321,14 @@ pub(crate) async fn load_prepared_script_source_outcome_with_document_character_ source_result: Ok(source.clone()), source_bytes: None, network_result: None, + muted_errors: false, } } ScriptSource::LoadedBinary { source, bytes } => PreparedScriptSourceLoadOutcome { source_result: Ok(source.clone()), source_bytes: Some(bytes.clone()), network_result: None, + muted_errors: false, }, ScriptSource::External => { if let Some(outcome) = local_or_unsupported_external_script_source_load_outcome( @@ -337,6 +357,7 @@ pub(crate) async fn load_prepared_script_source_outcome_with_document_character_ source_result: Err(error.clone()), source_bytes: None, network_result: Some(Arc::new(Err(error))), + muted_errors: false, } } } @@ -401,13 +422,23 @@ pub(crate) async fn load_service_worker_aware_external_script_source_outcome( ) .await { - Ok(Some(response)) => external_script_source_load_outcome_from_response_inner( - script, - *response.response, - document_character_set, - response.response_filter, - None, - ), + Ok(Some(response)) => { + let provenance = if response.from_network_fallback { + ClassicScriptResponseProvenance::Network + } else { + ClassicScriptResponseProvenance::ServiceWorker { + filter: response.response_filter, + } + }; + external_script_source_load_outcome_from_response_inner( + script, + *response.response, + document_character_set, + response.response_filter, + None, + provenance, + ) + } Ok(None) => { load_prepared_script_source_outcome_with_document_character_set( script, @@ -424,6 +455,7 @@ pub(crate) async fn load_service_worker_aware_external_script_source_outcome( source_result: Err(message.clone()), source_bytes: None, network_result: Some(Arc::new(Err(message))), + muted_errors: false, } } } @@ -440,6 +472,7 @@ fn local_or_unsupported_external_script_source_load_outcome( .map_err(|error| error.to_string()), source_bytes: None, network_result: None, + muted_errors: false, }), "blob" => Some(match crate::network_host::local_url_response(&script.url) { Some(response) => { @@ -484,6 +517,7 @@ fn failed_external_script_source_load_outcome(message: String) -> PreparedScript source_result: Err(message.clone()), source_bytes: None, network_result: Some(Arc::new(Err(message))), + muted_errors: false, } } @@ -520,6 +554,52 @@ pub(crate) fn spawn_service_worker_aware_external_script_source_load( ) } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum ClassicScriptResponseProvenance { + Network, + ServiceWorker { + filter: Option, + }, +} + +fn classic_script_errors_are_muted( + script: &PreparedScript, + response: &crate::protocol_types::NavigationResponse, + provenance: ClassicScriptResponseProvenance, +) -> bool { + if script.kind != ScriptKind::Classic + || script.source_kind != crate::types::ScriptSourceKind::External + || external_script_request_mode(script.kind, &script.fetch_metadata) != RequestMode::NoCors + { + return false; + } + + match provenance { + ClassicScriptResponseProvenance::Network => { + !classic_script_response_url_chain_is_same_origin(script, response) + } + ClassicScriptResponseProvenance::ServiceWorker { filter } => matches!( + filter, + Some( + AsyncSubresourceFetchResponseFilter::Opaque + | AsyncSubresourceFetchResponseFilter::OpaqueRedirect + ) + ), + } +} + +fn classic_script_response_url_chain_is_same_origin( + script: &PreparedScript, + response: &crate::protocol_types::NavigationResponse, +) -> bool { + moli_url::same_origin(&script.initiator_url, &script.url) + && response.redirect_chain.iter().all(|redirect| { + moli_url::same_origin(&script.initiator_url, &redirect.from_url) + && moli_url::same_origin(&script.initiator_url, &redirect.to_url) + }) + && moli_url::same_origin(&script.initiator_url, &response.final_url) +} + pub(crate) fn external_script_source_load_outcome_from_response( script: &PreparedScript, request_origin: &moli_url::WebOrigin, @@ -546,6 +626,7 @@ pub(crate) fn external_script_source_load_outcome_from_response( document_character_set, response_filter, cors_error, + ClassicScriptResponseProvenance::Network, ) } @@ -572,7 +653,9 @@ fn external_script_source_load_outcome_from_response_inner( document_character_set: Option<&str>, response_filter: Option, cors_error: Option, + provenance: ClassicScriptResponseProvenance, ) -> PreparedScriptSourceLoadOutcome { + let muted_errors = classic_script_errors_are_muted(script, &response, provenance); let response_bytes = response.body_bytes().to_vec(); let opaque_status_zero = response_filter == Some(crate::types::AsyncSubresourceFetchResponseFilter::Opaque) @@ -609,6 +692,7 @@ fn external_script_source_load_outcome_from_response_inner( source_result, source_bytes: Some(response_bytes), network_result: Some(Arc::new(Ok(response))), + muted_errors, } } @@ -766,6 +850,7 @@ mod tests { source_result: Ok("window.ready = true;".to_owned()), source_bytes: None, network_result: None, + muted_errors: false, } }, RendererResourceTaskRunner::from_current_tokio() @@ -863,6 +948,24 @@ mod tests { crate::protocol_types::NavigationResponse::from_head_and_body(head, body, body_bytes) } + fn script_redirect(from_url: &Url, to_url: &Url) -> crate::types::NavigationRedirect { + crate::types::NavigationRedirect { + source: moli_fetch::RedirectSource::Network, + from_url: from_url.clone(), + to_url: to_url.clone(), + status: 302, + headers: Vec::new(), + network_extra_info_available: true, + request_extra_info: None, + response_extra_info: None, + redirect_has_extra_info: true, + request_cookie_report: None, + cookie_set_reports: Vec::new(), + from_cache: false, + negotiated_http_version: None, + } + } + fn prepared_external_script(url: &str, kind: ScriptKind) -> PreparedScript { let url = url::Url::parse(url).expect("test script url"); prepared_external_script_with_initiator(url.clone(), kind, url) @@ -1187,6 +1290,104 @@ mod tests { ); } + #[test] + fn classic_script_network_response_taint_controls_muted_errors() { + let document_url = Url::parse("https://document.test/page.html").unwrap(); + let same_origin_url = Url::parse("https://document.test/script.js").unwrap(); + let cross_origin_url = Url::parse("https://cdn.test/script.js").unwrap(); + + let same_origin_script = prepared_external_script_with_initiator( + same_origin_url.clone(), + ScriptKind::Classic, + document_url.clone(), + ); + let same_origin_response = script_response(&same_origin_url, Vec::new()); + assert!(!classic_script_errors_are_muted( + &same_origin_script, + &same_origin_response, + ClassicScriptResponseProvenance::Network, + )); + + let mut cross_origin_script = prepared_external_script_with_initiator( + cross_origin_url.clone(), + ScriptKind::Classic, + document_url.clone(), + ); + let cross_origin_response = script_response(&cross_origin_url, Vec::new()); + assert!(classic_script_errors_are_muted( + &cross_origin_script, + &cross_origin_response, + ClassicScriptResponseProvenance::Network, + )); + + cross_origin_script.fetch_metadata.cross_origin = Some("anonymous".to_owned()); + assert!(!classic_script_errors_are_muted( + &cross_origin_script, + &cross_origin_response, + ClassicScriptResponseProvenance::Network, + )); + + let mut through_cross_origin_response = script_response(&same_origin_url, Vec::new()); + through_cross_origin_response.redirected = true; + through_cross_origin_response.redirect_chain = vec![ + script_redirect(&same_origin_url, &cross_origin_url), + script_redirect(&cross_origin_url, &same_origin_url), + ]; + assert!(classic_script_errors_are_muted( + &same_origin_script, + &through_cross_origin_response, + ClassicScriptResponseProvenance::Network, + )); + } + + #[test] + fn classic_script_service_worker_response_filter_controls_muted_errors() { + let document_url = Url::parse("https://document.test/page.html").unwrap(); + let script_url = Url::parse("https://cdn.test/script.js").unwrap(); + let script = prepared_external_script_with_initiator( + script_url.clone(), + ScriptKind::Classic, + document_url, + ); + let response = script_response(&script_url, Vec::new()); + + assert!(classic_script_errors_are_muted( + &script, + &response, + ClassicScriptResponseProvenance::ServiceWorker { + filter: Some(AsyncSubresourceFetchResponseFilter::Opaque), + }, + )); + assert!(!classic_script_errors_are_muted( + &script, + &response, + ClassicScriptResponseProvenance::ServiceWorker { filter: None }, + )); + assert!(classic_script_errors_are_muted( + &script, + &response, + ClassicScriptResponseProvenance::Network, + )); + } + + #[test] + fn loaded_muted_classic_script_sanitizes_its_base_url() { + let document_url = Url::parse("https://document.test/page.html").unwrap(); + let script_url = Url::parse("https://cdn.test/script.js").unwrap(); + let script = + prepared_external_script_with_initiator(script_url, ScriptKind::Classic, document_url); + + let loaded = prepared_script_with_loaded_source( + script, + "throw new Error('secret')".to_owned(), + None, + true, + ); + + assert!(loaded.fetch_metadata.muted_errors); + assert_eq!(loaded.base_url.as_str(), "about:blank"); + } + #[test] fn classic_script_modes_map_to_chromium_resource_priorities() { let normal = prepared_external_classic_with_mode(ScriptMode::Normal); @@ -1376,9 +1577,13 @@ mod tests { None, Some(crate::types::AsyncSubresourceFetchResponseFilter::Opaque), None, + ClassicScriptResponseProvenance::ServiceWorker { + filter: Some(AsyncSubresourceFetchResponseFilter::Opaque), + }, ); assert_eq!(outcome.source_result.expect("opaque script source"), source); + assert!(outcome.muted_errors); assert_eq!( outcome.source_bytes.expect("opaque script source bytes"), source.as_bytes() diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs b/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs index f54a491a59..b11cd6a86b 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/lifecycle.rs @@ -3125,6 +3125,7 @@ document.body.appendChild(replacementFrame); ), source_bytes: None, network_result: None, + muted_errors: false, }, ), MainParserDeferredClassicSourceNetworkAttribution::new( diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs index 556ec4cbae..50bc906e16 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/mod.rs @@ -227,6 +227,7 @@ async fn page_resource_completion_rejects_stale_document_before_application() { source_result: Ok("globalThis.__staleDeferRan = true".to_owned()), source_bytes: None, network_result: network_error.map(|error| Arc::new(Err(error.to_owned()))), + muted_errors: false, }, ) }; diff --git a/moli-renderer-v8/src/runtime/script_preloads.rs b/moli-renderer-v8/src/runtime/script_preloads.rs index e789e2b6ef..217308b7bb 100644 --- a/moli-renderer-v8/src/runtime/script_preloads.rs +++ b/moli-renderer-v8/src/runtime/script_preloads.rs @@ -613,6 +613,7 @@ impl BufferedDocumentPreloadState { script.clone(), source, outcome.source_bytes, + outcome.muted_errors, ); ParserBlockingPreloadDisposition::Ready(AppliedPreloadedScriptSource { network_result: script_preload_network_result(outcome.network_result), @@ -690,6 +691,7 @@ impl BufferedDocumentPreloadState { script.clone(), source, outcome.source_bytes, + outcome.muted_errors, ); Some(AppliedPreloadedScriptSource { network_result: script_preload_network_result(outcome.network_result), diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 5fd3e75909..593ab7eba8 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -8492,6 +8492,7 @@ impl ScriptVm { Err(eval_exec::RawScriptExecutionError::Exception { report, .. }) => { self.report_classic_script_exception_and_finish_evaluation_best_effort( &report, + fetch_metadata.muted_errors, ); Ok(LoadedScriptExecutionOutcome::Completed( PreparedScriptBodyActivity::Entered, diff --git a/moli-renderer-v8/src/script_vm/classic_script_exception.rs b/moli-renderer-v8/src/script_vm/classic_script_exception.rs index 82b9db8f81..285f6942a0 100644 --- a/moli-renderer-v8/src/script_vm/classic_script_exception.rs +++ b/moli-renderer-v8/src/script_vm/classic_script_exception.rs @@ -17,9 +17,12 @@ impl ScriptVm { pub(super) fn report_classic_script_exception_and_finish_evaluation_best_effort( &mut self, report: &V8ExceptionReport, + muted_errors: bool, ) { log_uncaught_script_exception(report); - if let Err(error) = self.dispatch_classic_script_exception_and_finish_evaluation(report) { + if let Err(error) = + self.dispatch_classic_script_exception_and_finish_evaluation(report, muted_errors) + { self.record_runtime_warning(format_args!( "classic script exception reporting failed: {error}" )); @@ -29,6 +32,7 @@ impl ScriptVm { fn dispatch_classic_script_exception_and_finish_evaluation( &mut self, report: &V8ExceptionReport, + muted_errors: bool, ) -> Result<()> { let context_ptr: *const v8::Global = &self.page_default_context; let context_host = self._context_host.clone(); @@ -38,19 +42,33 @@ impl ScriptVm { let scope = &mut scope.init(); let context = unsafe { v8::Local::new(scope, &*context_ptr) }; let scope = &mut v8::ContextScope::new(scope, context); - let error_value = report - .exception - .as_ref() - .map(|exception| v8::Local::new(scope, exception)); + let error_value = if muted_errors { + None + } else { + report + .exception + .as_ref() + .map(|exception| v8::Local::new(scope, exception)) + }; + let (summary, source, line, column) = if muted_errors { + ("Script error.", "", 0, 0) + } else { + ( + report.summary.as_str(), + report.source.as_deref().unwrap_or(""), + report.line.unwrap_or(0) as u32, + report.column.unwrap_or(0) as u32, + ) + }; // SAFETY: as_ptr() — V8 callbacks are re-entrant; borrow_mut() panics. See util.rs. let host_ptr: *mut JsContextHost = (*context_host).as_ptr(); let dispatch_result = dispatch_window_error_event_with_details( scope, host_ptr, - &report.summary, - report.source.as_deref().unwrap_or(""), - report.line.unwrap_or(0) as u32, - report.column.unwrap_or(0) as u32, + summary, + source, + line, + column, error_value, ) .map_err(anyhow::Error::msg); diff --git a/moli-renderer-v8/src/script_vm/tests/post_parse.rs b/moli-renderer-v8/src/script_vm/tests/post_parse.rs index 91998ce50c..a142fefcb8 100644 --- a/moli-renderer-v8/src/script_vm/tests/post_parse.rs +++ b/moli-renderer-v8/src/script_vm/tests/post_parse.rs @@ -168,6 +168,63 @@ async fn classic_script_exception_reports_window_error_then_completes() { ); } +#[tokio::test] +async fn muted_classic_script_exceptions_expose_only_cross_origin_safe_details() { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_storage_test_vm_with_loader("https://example.com/", &loader); + vm.eval( + r#" + globalThis.__mutedClassicScriptErrors = []; + window.onerror = (message, source, line, column, error) => { + globalThis.__mutedClassicScriptErrors.push({ + message, + source, + line, + column, + errorIsNull: error === null, + }); + return true; + }; + "installed"; + "#, + ) + .expect("window error observer should install"); + + for (position, source) in [ + (8, "throw new Error('runtime secret');"), + (9, "function syntaxError( {"), + ] { + let mut script = ready_dynamic_runtime_script(position); + script.url = Url::parse(&format!("https://cross-origin.test/script-{position}.js")) + .expect("cross-origin script URL"); + script.base_url = script.url.clone(); + let script = crate::planning::prepared_script_with_loaded_source( + script, + source.to_owned(), + None, + true, + ); + + let outcome = vm + .execute_loaded_prepared_script_source(&script, source, None) + .await + .expect("a muted exception should still complete classic script evaluation"); + assert!(matches!( + outcome, + crate::script_vm::LoadedScriptExecutionOutcome::Completed( + crate::script_vm::PreparedScriptBodyActivity::Entered + ) + )); + assert_eq!(script.base_url.as_str(), "about:blank"); + } + + assert_eq!( + vm.eval("JSON.stringify(globalThis.__mutedClassicScriptErrors)") + .expect("muted classic script errors should remain observable"), + r#"[{"message":"Script error.","source":"","line":0,"column":0,"errorIsNull":true},{"message":"Script error.","source":"","line":0,"column":0,"errorIsNull":true}]"#, + ); +} + fn is_document_script_execution_work( work: &PostParsePageOwnedWork, lane: crate::document_script_scheduler::DocumentScriptExecutionLane, diff --git a/moli-renderer-v8/src/service_worker_runtime/events.rs b/moli-renderer-v8/src/service_worker_runtime/events.rs index 042a088190..c5a4d63627 100644 --- a/moli-renderer-v8/src/service_worker_runtime/events.rs +++ b/moli-renderer-v8/src/service_worker_runtime/events.rs @@ -709,6 +709,7 @@ pub(crate) enum ServiceWorkerDirectFetchResult { pub(crate) struct ServiceWorkerDirectFetchResponse { pub(crate) response: Box, pub(crate) response_filter: Option, + pub(crate) from_network_fallback: bool, } #[derive(Clone, Debug)] diff --git a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs index 55112497b1..0a4bdc0bcd 100644 --- a/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs +++ b/moli-renderer-v8/src/service_worker_runtime/service/fetch_settlement.rs @@ -330,6 +330,7 @@ impl ServiceWorkerRuntimeService { ServiceWorkerDirectFetchResult::Response(ServiceWorkerDirectFetchResponse { response: Box::new(navigation_response), response_filter, + from_network_fallback: true, }) } Err(error) => ServiceWorkerDirectFetchResult::Failure(error.to_string()), @@ -618,6 +619,7 @@ impl ServiceWorkerRuntimeService { ServiceWorkerDirectFetchResponse { response: Box::new(navigation_response), response_filter, + from_network_fallback: false, }, )); return;