From 2f35db82b089de2e01a7ec8a6167086b2a9d2f39 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Fri, 17 Jul 2026 02:34:38 +0800 Subject: [PATCH] fix(trusted-types): prepare script text before type --- .../wpt-cross-current/failed-cases.txt | 2 - .../wpt-cross-current/passed-cases.txt | 2 + .../src/document_runtime/mutation_commands.rs | 9 +-- .../script_scheduling/script_handles.rs | 29 ++++++++-- moli-renderer-v8/src/host.rs | 6 +- moli-renderer-v8/src/host/scripts/loader.rs | 27 ++++----- moli-renderer-v8/src/host/scripts/runtime.rs | 58 +++++++++++++++++++ .../context_host/main_document_lifecycle.rs | 25 +++++++- moli-renderer-v8/src/native_bridge/element.rs | 4 +- .../native_bridge/element/script_execution.rs | 4 +- .../native_bridge/element/trusted_types.rs | 12 +++- .../tests/browser_api/trusted_types.rs | 58 ++++++++++++++++++- 12 files changed, 201 insertions(+), 35 deletions(-) diff --git a/moli-benchmark/wpt-cross-current/failed-cases.txt b/moli-benchmark/wpt-cross-current/failed-cases.txt index 18d89eb8cc..fce0b89030 100644 --- a/moli-benchmark/wpt-cross-current/failed-cases.txt +++ b/moli-benchmark/wpt-cross-current/failed-cases.txt @@ -3653,8 +3653,6 @@ trusted-types/eval-function-constructor-untrusted-arguments-and-applying-default trusted-types/inheriting-csp-for-local-schemes.html trusted-types/modify-attributes-in-callback.html trusted-types/require-trusted-types-for-TypeError-belongs-to-the-global-object-realm.html -trusted-types/script-enforcement-010.html -trusted-types/script-enforcement-011.html trusted-types/set-attributes-require-trusted-types-default-policy.html trusted-types/set-attributes-require-trusted-types-no-default-policy-error-cases.html trusted-types/set-attributes-require-trusted-types-no-default-policy.html diff --git a/moli-benchmark/wpt-cross-current/passed-cases.txt b/moli-benchmark/wpt-cross-current/passed-cases.txt index 3eb33a62e7..66ac0d5ed7 100644 --- a/moli-benchmark/wpt-cross-current/passed-cases.txt +++ b/moli-benchmark/wpt-cross-current/passed-cases.txt @@ -8049,6 +8049,8 @@ trusted-types/script-enforcement-004.html trusted-types/script-enforcement-005.html trusted-types/script-enforcement-006.html trusted-types/script-enforcement-007.html +trusted-types/script-enforcement-010.html +trusted-types/script-enforcement-011.html trusted-types/script-enforcement-012.html trusted-types/script-enforcement-013.html trusted-types/script-enforcement-014.html diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands.rs b/moli-renderer-v8/src/document_runtime/mutation_commands.rs index 126ad917b2..0568fe7657 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands.rs @@ -2054,10 +2054,11 @@ pub(super) fn finish_runtime_script_start_candidate( candidate: crate::mutation_coordinator::RuntimeScriptStartCandidate, ) { let (node, host_script_handle) = candidate.into_parts(); - let Some(plan) = runtime.host_plan_script_start(node, &host_script_handle) else { - return; - }; - match unsafe { &mut *host_ptr }.commit_current_main_runtime_script_start(scope, runtime, plan) { + match unsafe { &mut *host_ptr }.plan_and_commit_current_main_runtime_script_start( + scope, + node, + &host_script_handle, + ) { Ok(Some(committed)) => { execute_committed_inline_classic_script(runtime, scope, host_ptr, committed); } diff --git a/moli-renderer-v8/src/document_runtime/script_scheduling/script_handles.rs b/moli-renderer-v8/src/document_runtime/script_scheduling/script_handles.rs index 364f0336b9..57d9bceb42 100644 --- a/moli-renderer-v8/src/document_runtime/script_scheduling/script_handles.rs +++ b/moli-renderer-v8/src/document_runtime/script_scheduling/script_handles.rs @@ -1,9 +1,10 @@ use super::*; use crate::host::{ ModuleFailurePolicy, PreparedRuntimeScriptStartCommit, RuntimeScriptAdmission, - RuntimeScriptStartPlan, RuntimeScriptStartReservation, ScriptEventKind, ScriptEventTask, - ScriptHandleSource, cancel_runtime_script_start_admission, dispatch_script_event, - finish_runtime_script_start_admission, plan_script_start, prepare_runtime_script_start_commit, + RuntimeScriptStartPlan, RuntimeScriptStartReservation, RuntimeScriptTextPreparationReservation, + ScriptEventKind, ScriptEventTask, ScriptHandleSource, cancel_runtime_script_start_admission, + dispatch_script_event, finish_runtime_script_start_admission, plan_script_start, + prepare_runtime_script_start_commit, }; use crate::page_task_queue::PostParseLifecycleWork; use crate::planning::PreparedScript; @@ -17,7 +18,7 @@ impl DocumentRuntime { host_script_handle: &str, ) -> Option { let options = crate::host::ScriptElementLoaderOptions { - prepare_changed_empty_inline_source: self.requires_trusted_types_for_script(), + use_prepared_script_text: self.requires_trusted_types_for_script(), ..crate::host::ScriptElementLoaderOptions::with_scripting_enabled( self.document_scripting_enabled(), ) @@ -31,6 +32,26 @@ impl DocumentRuntime { ) } + pub(crate) fn begin_runtime_script_text_preparation( + &mut self, + node: DomHandle, + host_script_handle: &str, + ) -> Option { + RuntimeScriptTextPreparationReservation::begin( + &self.dom_host, + self.script_lifecycle.scripts_mut(), + node, + host_script_handle, + ) + } + + pub(crate) fn release_runtime_script_text_preparation( + &mut self, + reservation: RuntimeScriptTextPreparationReservation, + ) { + reservation.release(self.script_lifecycle.scripts_mut()); + } + pub(crate) fn prepare_runtime_script_start_commit( &mut self, plan: RuntimeScriptStartPlan, diff --git a/moli-renderer-v8/src/host.rs b/moli-renderer-v8/src/host.rs index 9000be27cc..64eb366c34 100644 --- a/moli-renderer-v8/src/host.rs +++ b/moli-renderer-v8/src/host.rs @@ -40,9 +40,9 @@ pub(super) use self::scripts::{ CommittedInlineClassicScript, FailedDynamicScript, HostScriptScheduler, ModuleFailurePolicy, PreparedRuntimeScriptStartCommit, QueuedScriptFailureKind, RuntimeScriptAdmission, RuntimeScriptAdmissionPayload, RuntimeScriptPreparationContext, RuntimeScriptStartDecision, - RuntimeScriptStartPlan, RuntimeScriptStartReservation, ScriptElementLoader, - ScriptElementLoaderOptions, ScriptEventKind, ScriptEventTask, ScriptHandleSource, - ScriptPageTaskExecutionKind, apply_parser_script_element_state_transition, + RuntimeScriptStartPlan, RuntimeScriptStartReservation, RuntimeScriptTextPreparationReservation, + ScriptElementLoader, ScriptElementLoaderOptions, ScriptEventKind, ScriptEventTask, + ScriptHandleSource, ScriptPageTaskExecutionKind, apply_parser_script_element_state_transition, apply_parser_script_element_state_without_execution, begin_prepared_document_write_script_start, build_runtime_prepared_script, cancel_runtime_script_start_admission, dispatch_script_event, diff --git a/moli-renderer-v8/src/host/scripts/loader.rs b/moli-renderer-v8/src/host/scripts/loader.rs index 1b18d4481f..05d423e0aa 100644 --- a/moli-renderer-v8/src/host/scripts/loader.rs +++ b/moli-renderer-v8/src/host/scripts/loader.rs @@ -93,10 +93,9 @@ pub(crate) struct ScriptElementLoaderOptions { /// Valid scripts still commit their already-started state, but they must not /// fetch, register an import map, or execute. pub(crate) scripting_disabled: bool, - /// The caller will apply Trusted Types script-text preparation to the - /// returned inline source, so a changed empty source may still become - /// executable before the HTML empty-source check takes effect. - pub(crate) prepare_changed_empty_inline_source: bool, + /// Trusted Types script-text preparation has already updated the script's + /// internal slot. Read that slot before the empty-source and type checks. + pub(crate) use_prepared_script_text: bool, } impl ScriptElementLoaderOptions { @@ -106,7 +105,7 @@ impl ScriptElementLoaderOptions { suppress_force_async: false, document_write_connected: false, scripting_disabled: !scripting_enabled, - prepare_changed_empty_inline_source: false, + use_prepared_script_text: false, } } } @@ -347,14 +346,16 @@ pub(crate) fn decide_runtime_script_start( let source = match script_src { Some(source) => source, None => { - let source = dom_host.dom().direct_text_content(node).unwrap_or_default(); - let changed_since_trusted_source = dom_host - .node(node) - .and_then(Node::as_element) - .is_some_and(|element| element.script_text_internal_slot() != source); - if source.is_empty() - && !(options.prepare_changed_empty_inline_source && changed_since_trusted_source) - { + let source = if options.use_prepared_script_text { + dom_host + .node(node) + .and_then(Node::as_element) + .map(|element| element.script_text_internal_slot().to_owned()) + .unwrap_or_default() + } else { + dom_host.dom().direct_text_content(node).unwrap_or_default() + }; + if source.is_empty() { return RuntimeScriptStartDecision::Skip { commit_start: false, reason: Some(ScriptSkipReason::EmptyInlineScript), diff --git a/moli-renderer-v8/src/host/scripts/runtime.rs b/moli-renderer-v8/src/host/scripts/runtime.rs index 5073f3b07e..e3c4696e43 100644 --- a/moli-renderer-v8/src/host/scripts/runtime.rs +++ b/moli-renderer-v8/src/host/scripts/runtime.rs @@ -162,6 +162,16 @@ impl PreparedRuntimeScriptStart { return Ok(None); } + self.execute_after_reservation(dom_host, scripts, host_script_handle) + } + + #[cfg(test)] + fn execute_after_reservation( + self, + dom_host: &mut DomHost, + scripts: &mut HostScriptScheduler, + host_script_handle: &str, + ) -> std::result::Result, String> { let PreparedRuntimeScriptStart { node, preparation, @@ -171,6 +181,7 @@ impl PreparedRuntimeScriptStart { match decision { RuntimeScriptStartDecision::Skip { commit_start, .. } => { if !commit_start { + scripts.cancel_script_start(host_script_handle, node); return Ok(None); } if !commit_runtime_script_start( @@ -556,6 +567,53 @@ pub(super) fn commit_runtime_script_start( true } +/// Holds the per-element start lane while a Trusted Types default policy runs. +/// +/// Policy callbacks can mutate the connected script and synchronously trigger +/// another preparation attempt. Reserving before entering JavaScript keeps +/// that nested attempt from preparing the same element a second time. The +/// reservation is released before the normal plan/commit pipeline resumes. +#[derive(Debug)] +pub(crate) struct RuntimeScriptTextPreparationReservation { + node: NativeNodeId, + host_script_handle: String, +} + +impl RuntimeScriptTextPreparationReservation { + pub(crate) fn begin( + dom_host: &DomHost, + scripts: &mut HostScriptScheduler, + node: NativeNodeId, + host_script_handle: &str, + ) -> Option { + let can_prepare = dom_host.is_connected(node) + && dom_host + .node(node) + .and_then(|node| node.as_element()) + .is_some_and(|element| { + element.is_script_element() && !element.script_already_started() + }); + if !can_prepare { + return None; + } + scripts.register_script_handle_with_source( + host_script_handle, + node, + ScriptHandleSource::RuntimeOwned, + ); + scripts + .reserve_script_start(host_script_handle, node) + .then(|| Self { + node, + host_script_handle: host_script_handle.to_owned(), + }) + } + + pub(crate) fn release(self, scripts: &mut HostScriptScheduler) { + scripts.cancel_script_start(&self.host_script_handle, self.node); + } +} + pub(crate) fn begin_prepared_document_write_script_start( dom_host: &mut DomHost, scripts: &mut HostScriptScheduler, diff --git a/moli-renderer-v8/src/native_bridge/context_host/main_document_lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/main_document_lifecycle.rs index 59008191cb..b19a0ad8be 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/main_document_lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/main_document_lifecycle.rs @@ -24,7 +24,30 @@ impl JsContextHost { node: crate::document_runtime::DomHandle, host_script_handle: &str, ) -> std::result::Result, String> { - let runtime = unsafe { &mut *self.runtime }; + let requires_trusted_types = self.requires_trusted_types_for_script(scope); + let host_ptr: *mut JsContextHost = self; + let runtime_ptr = self.runtime; + if requires_trusted_types { + let Some(reservation) = (unsafe { &mut *runtime_ptr }) + .begin_runtime_script_text_preparation(node, host_script_handle) + else { + return Ok(None); + }; + let source = unsafe { &*runtime_ptr } + .dom_host() + .dom() + .direct_text_content(node) + .unwrap_or_default(); + let prepared = crate::native_bridge::element::prepare_trusted_script_text( + scope, host_ptr, node, &source, + ); + unsafe { &mut *runtime_ptr }.release_runtime_script_text_preparation(reservation); + if prepared.is_none() { + return Ok(None); + } + } + + let runtime = unsafe { &mut *runtime_ptr }; let Some(plan) = runtime.host_plan_script_start(node, host_script_handle) else { return Ok(None); }; diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index d614e3d223..9c1ba41884 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -61,8 +61,8 @@ use trusted_types::{ TrustedScriptElementSink, trusted_script_element_sink_string, trusted_script_url_sink_string, }; pub(crate) use trusted_types::{ - set_svg_animated_string_base_value, trusted_attribute_type_name_for_names, - trusted_property_type_name_for_names, + prepare_trusted_script_text, set_svg_animated_string_base_value, + trusted_attribute_type_name_for_names, trusted_property_type_name_for_names, }; pub(crate) use forms::{ diff --git a/moli-renderer-v8/src/native_bridge/element/script_execution.rs b/moli-renderer-v8/src/native_bridge/element/script_execution.rs index bcb837f5df..ccb816d75a 100644 --- a/moli-renderer-v8/src/native_bridge/element/script_execution.rs +++ b/moli-renderer-v8/src/native_bridge/element/script_execution.rs @@ -5,7 +5,7 @@ use crate::{ native_bridge::JsContextHost, }; -use super::trusted_types::trusted_script_source_for_execution; +use super::trusted_types::prepare_trusted_script_text; pub(crate) fn prepare_inline_classic_frame_script_job_for_execution( scope: &mut v8::PinScope<'_, '_>, @@ -56,7 +56,7 @@ pub(crate) fn inline_script_source_for_execution( source: &str, request: ContentSecurityPolicyScriptElementRequest<'_>, ) -> Option { - let source = trusted_script_source_for_execution(scope, host_ptr, script, source)?; + let source = prepare_trusted_script_text(scope, host_ptr, script, source)?; let host = unsafe { &mut *host_ptr }; let Some(owner) = host.owner_dispatch_scope_for_node(script) else { // A script with no live owner has no document policy container to diff --git a/moli-renderer-v8/src/native_bridge/element/trusted_types.rs b/moli-renderer-v8/src/native_bridge/element/trusted_types.rs index 9e5eae2ce6..fdd6c17ee9 100644 --- a/moli-renderer-v8/src/native_bridge/element/trusted_types.rs +++ b/moli-renderer-v8/src/native_bridge/element/trusted_types.rs @@ -348,7 +348,7 @@ pub(crate) fn set_svg_animated_string_base_value<'s>( Some(value) } -pub(crate) fn trusted_script_source_for_execution( +pub(crate) fn prepare_trusted_script_text( scope: &mut v8::PinScope<'_, '_>, runtime_ptr: *mut JsContextHost, handle: DomHandle, @@ -374,7 +374,13 @@ pub(crate) fn trusted_script_source_for_execution( if source == trusted_source { return Some(source.to_owned()); } - crate::context_bootstrap::trusted_script_string_for_script_element_execution( + let source = crate::context_bootstrap::trusted_script_string_for_script_element_execution( scope, source, sink, - ) + )?; + // The Trusted Types integration updates the script-text slot before the + // HTML prepare algorithm reads the source and determines the script type. + let _ = unsafe { &mut *runtime_ptr } + .dom_host_mut() + .set_script_text_internal_slot(handle, &source); + Some(source) } diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs index 72a1c02116..1cfb536381 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs @@ -1135,6 +1135,63 @@ fn trusted_types_default_policy_prepares_runtime_import_maps_before_registration } } +#[test] +fn trusted_types_default_policy_type_mutation_precedes_script_classification() { + let mut vm = new_storage_test_vm("https://script-type-trusted-types.test/"); + vm.set_response_content_security_policies(&["require-trusted-types-for 'script'".to_owned()]); + + let result = vm + .eval( + r#" +(() => { + const html = document.documentElement || + document.appendChild(document.createElement("html")); + const body = document.body || html.appendChild(document.createElement("body")); + const svg = body.appendChild( + document.createElementNS("http://www.w3.org/2000/svg", "svg") + ); + const source = `globalThis.__trustedTypeScriptKinds.push("CLASSIC");`; + globalThis.__trustedTypeScriptKinds = []; + const defaultCalls = []; + let script; + trustedTypes.createPolicy("default", { + createScript(value, type, sink) { + defaultCalls.push([type, sink]); + if (script.hasAttribute("type")) { + script.removeAttribute("type"); + } else { + script.setAttribute("type", "text/plain"); + } + return value; + } + }); + + for (const [namespace, parent] of [ + ["http://www.w3.org/1999/xhtml", body], + ["http://www.w3.org/2000/svg", svg] + ]) { + script = document.createElementNS(namespace, "script"); + script.appendChild(document.createTextNode(source)); + script.setAttribute("type", "module"); + parent.appendChild(script); + + script = document.createElementNS(namespace, "script"); + script.appendChild(document.createTextNode(source)); + parent.appendChild(script); + } + + return JSON.stringify({ defaultCalls, runs: globalThis.__trustedTypeScriptKinds }); +})() +"#, + ) + .expect("Trusted Types script type mutation probe should evaluate"); + + assert_eq!( + result, + r#"{"defaultCalls":[["TrustedScript","HTMLScriptElement text"],["TrustedScript","HTMLScriptElement text"],["TrustedScript","SVGScriptElement text"],["TrustedScript","SVGScriptElement text"]],"runs":["CLASSIC","CLASSIC"]}"# + ); +} + #[test] fn inline_module_graph_roots_use_trusted_types_compliant_source() { let mut vm = new_storage_test_vm("https://module-source-trusted-types.test/"); @@ -2158,7 +2215,6 @@ fn script_execution_violation_outside_javascript_stack_avoids_v8_frame_probe() { const script = document.createElement("script"); script.id = "untrusted-script-source"; script.type = "application/json"; - script.appendChild(document.createTextNode("untrusted-source")); const root = document.body || (document.documentElement || document.appendChild(document.createElement("html"))) .appendChild(document.createElement("body"));