From 6faf8d3fed2981c60ef5d9d8ce1c9c70afcbcd11 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 18 Oct 2026 01:07:41 +0800 Subject: [PATCH] fix(html): schedule refresh mutations in loaded child documents Route native meta insertion and attribute mutation candidates by their owning document, instead of limiting mutation refresh handling to the top document. Loaded child documents now register adopted or inserted refresh elements using their own URL, sandbox policy and exact document timer owner. Ignore shadow-tree candidates and retain the existing deadline for identical refresh candidates. Use the active runtime borrow directly while registering child timers and create the native callback in the child Window realm. Initial load capture remains the fallback for child documents that are still loading. Add deterministic source-first/source-last sandbox adoption tests and post-load insertion and shadow-tree controls. The old frozen Release also reproduces the related allow-scripts-flag-changing-1 WPT flake (7 pass/3 timeout in10 rounds); the new source-last adoption and post-load insertion tests fail before this fix. Validation: - 13 related child meta refresh tests pass - cargo fmt --all - cargo clippy --workspace --all-targets --all-features -- -D warnings - cargo nextest run --no-fail-fast --- .../src/document_runtime/meta_refresh.rs | 37 ++++++-- .../src/document_runtime/mutation_commands.rs | 23 ++++- .../native_bridge/context_host/host_loads.rs | 74 ++++++++++++++-- .../extracted/frame_navigation_and_history.rs | 86 +++++++++++++++++-- 4 files changed, 196 insertions(+), 24 deletions(-) diff --git a/moli-renderer-v8/src/document_runtime/meta_refresh.rs b/moli-renderer-v8/src/document_runtime/meta_refresh.rs index 4b9bf7b78b..44d5da5856 100644 --- a/moli-renderer-v8/src/document_runtime/meta_refresh.rs +++ b/moli-renderer-v8/src/document_runtime/meta_refresh.rs @@ -7,6 +7,11 @@ pub(crate) struct MetaRefreshNavigation { pub(crate) url: Url, } +pub(super) struct MetaRefreshMutationCandidate { + pub(super) document: DomHandle, + pub(super) navigation: MetaRefreshNavigation, +} + #[derive(Debug)] pub(crate) struct ScheduledMetaRefreshNavigation { pub(crate) owner: FrameDocumentTaskOwner, @@ -231,7 +236,7 @@ pub(super) fn meta_refresh_navigations_from_mutation( dom_host: &DomHost, effects: &DomMutationEffects, document_url: &Url, -) -> Vec { +) -> Vec { let document_handle = dom_host.document_handle(); let mut handles = Vec::new(); for &root in effects.tree().connected_roots() { @@ -264,17 +269,31 @@ pub(super) fn meta_refresh_navigations_from_mutation( if handles.is_empty() { return Vec::new(); } - let document_base_url = dom_host - .document_base_url() - .unwrap_or_else(|| document_url.clone()); handles .into_iter() - .filter(|&handle| { - dom_host.owner_document_handle(handle) == Some(document_handle) - && dom_host.is_connected(handle) - }) .filter_map(|handle| { - meta_refresh_navigation_for_element(dom_host, handle, document_url, &document_base_url) + let document = dom_host.owner_document_handle(handle)?; + if !dom_host.is_connected(handle) || dom_host.root_node_handle(handle) != Some(document) + { + return None; + } + let (owner_url, owner_base_url) = if document == document_handle { + (document_url, dom_host.document_base_url()) + } else { + ( + dom_host + .document_url_for_handle(document) + .unwrap_or(document_url), + dom_host.document_base_url_for_handle(document), + ) + }; + let owner_base_url = owner_base_url.unwrap_or_else(|| owner_url.clone()); + let navigation = + meta_refresh_navigation_for_element(dom_host, handle, owner_url, &owner_base_url)?; + Some(MetaRefreshMutationCandidate { + document, + navigation, + }) }) .collect() } diff --git a/moli-renderer-v8/src/document_runtime/mutation_commands.rs b/moli-renderer-v8/src/document_runtime/mutation_commands.rs index 077ef4b340..f1f3548ea1 100644 --- a/moli-renderer-v8/src/document_runtime/mutation_commands.rs +++ b/moli-renderer-v8/src/document_runtime/mutation_commands.rs @@ -1720,7 +1720,7 @@ impl DocumentRuntime { pub(crate) struct RuntimeMutationApplyResult { changed: bool, - meta_refresh_candidates: Vec, + meta_refresh_candidates: Vec, devtools_dom_mutations: Vec, runtime_script_start_candidates: Vec, removed_open_popovers: Vec, @@ -1768,8 +1768,16 @@ pub(super) fn finish_runtime_mutation_effects( runtime.queue_devtools_dom_mutations(devtools_dom_mutations); - if let Some(scheduled) = runtime.note_top_level_meta_refresh_candidates(meta_refresh_candidates) - { + let mut top_level_refreshes = Vec::new(); + let mut child_refreshes = Vec::new(); + for candidate in meta_refresh_candidates { + if candidate.document == runtime.document_handle() { + top_level_refreshes.push(candidate.navigation); + } else { + child_refreshes.push(candidate); + } + } + if let Some(scheduled) = runtime.note_top_level_meta_refresh_candidates(top_level_refreshes) { let delay_ms = scheduled.navigation.delay_ms; let url = scheduled.navigation.url.clone(); let owner = scheduled.owner; @@ -1790,6 +1798,15 @@ pub(super) fn finish_runtime_mutation_effects( } } + for candidate in child_refreshes { + unsafe { &mut *host_ptr }.note_child_meta_refresh_after_mutation( + scope, + runtime, + candidate.document, + candidate.navigation, + ); + } + for candidate in runtime_script_start_candidates { finish_runtime_script_start_candidate(runtime, scope, host_ptr, candidate); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs index 448adc5ec9..b0ef5112d9 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/host_loads.rs @@ -4,7 +4,8 @@ use crate::{ LocationNavigationKind, construct_original_event, construct_original_page_transition_event, meta_refresh_navigation_kind, navigate_location_object_with_child_navigate_event, }, - document_runtime::{DomHandle, EventTargetHandle, MetaRefreshNavigation}, + document_runtime::{DocumentRuntime, DomHandle, EventTargetHandle, MetaRefreshNavigation}, + dom::native::DocumentReadyState, frame_owner_model::{ FrameDocumentLoadDeliveryAction, FrameDocumentLoadDeliveryPhase, FrameDocumentLoadDeliveryProgress, @@ -20,6 +21,7 @@ pub(in crate::native_bridge::context_host) struct ChildMetaRefreshNavigationTask owner: crate::frame_owner_model::FrameDocumentTaskOwner, target_url: url::Url, navigation_kind: LocationNavigationKind, + delay_ms: u32, } #[derive(Debug, Clone, PartialEq, Eq, Serialize)] @@ -616,7 +618,6 @@ impl JsContextHost { scope: &mut v8::PinScope<'_, '_>, handle: DomHandle, ) { - self.cancel_child_meta_refresh_navigation(handle); if self .child_browsing_contexts .get(&handle) @@ -641,11 +642,71 @@ impl JsContextHost { ) else { return; }; + let runtime = unsafe { &mut *self.runtime }; + self.schedule_child_meta_refresh_navigation(scope, runtime, handle, navigation); + } + + pub(crate) fn note_child_meta_refresh_after_mutation( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + runtime: &mut DocumentRuntime, + document: DomHandle, + navigation: MetaRefreshNavigation, + ) { + if runtime.dom_host().document_ready_state_for_handle(document) + != Some(DocumentReadyState::Complete) + { + return; + } + let Some(handle) = self.child_browsing_context_host_for_document_handle(document) else { + return; + }; + // Mutation application already owns the runtime borrow; use it directly + // rather than borrowing it again through the host's runtime pointer. + self.schedule_child_meta_refresh_navigation(scope, runtime, handle, navigation); + } + + fn schedule_child_meta_refresh_navigation( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + runtime: &mut DocumentRuntime, + handle: DomHandle, + navigation: MetaRefreshNavigation, + ) { + if self + .child_browsing_contexts + .get(&handle) + .is_none_or(|entry| !entry.document_sandbox_allows_scripts()) + { + return; + } + let Some(document_url) = self.child_browsing_context_current_url(handle) else { + return; + }; let Some(owner) = self.current_child_document_task_owner(handle) else { return; }; let navigation_kind = meta_refresh_navigation_kind(&document_url, &navigation.url, navigation.delay_ms); + if self + .child_meta_refresh_navigations + .get(&handle) + .is_some_and(|task| { + task.owner == owner + && task.target_url == navigation.url + && task.navigation_kind == navigation_kind + && task.delay_ms == navigation.delay_ms + }) + { + return; + } + let Some(window) = self.child_window_proxy_records.live_window(scope, handle) else { + return; + }; + let Some(context) = window.get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); let data = v8::BigInt::new_from_u64(scope, handle.index() as u64); let Some(callback) = v8::Function::builder(child_meta_refresh_callback) .data(data.into()) @@ -653,7 +714,7 @@ impl JsContextHost { else { return; }; - let timer_id = self.queue_timeout( + let timer_id = runtime.queue_timeout( scope, callback, u64::from(navigation.delay_ms), @@ -663,15 +724,18 @@ impl JsContextHost { if timer_id == 0 { return; } - self.child_meta_refresh_navigations.insert( + if let Some(previous) = self.child_meta_refresh_navigations.insert( handle, ChildMetaRefreshNavigationTask { timer_id, owner, target_url: navigation.url, navigation_kind, + delay_ms: navigation.delay_ms, }, - ); + ) { + runtime.cancel_timer(previous.timer_id); + } } pub(in crate::native_bridge::context_host) fn cancel_child_meta_refresh_navigation( diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs index 3885668360..98ed91c5ad 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/frame_navigation_and_history.rs @@ -1413,9 +1413,14 @@ async fn child_meta_refresh_can_be_inserted_in_owner_load_callback() { assert_child_meta_refresh_can_be_inserted_during_load(false).await; } -async fn assert_child_meta_refresh_survives_adoption_during_load(source_loads_first: bool) { +async fn assert_child_meta_refresh_survives_adoption_during_load( + source_loads_first: bool, + source_sandboxed: bool, +) { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); let mut vm = new_storage_test_vm_with_loader("https://child-meta-refresh.test/", &loader); + vm.eval(&format!("globalThis.sourceSandboxed = {source_sandboxed};")) + .unwrap(); vm.eval( r#" globalThis.sourceLoads = 0; @@ -1423,7 +1428,7 @@ globalThis.destinationLoads = 0; const root = document.documentElement || document.appendChild(document.createElement('html')); const source = document.createElement('iframe'); const destination = document.createElement('iframe'); -destination.setAttribute('sandbox', 'allow-same-origin'); +(sourceSandboxed ? source : destination).setAttribute('sandbox', 'allow-same-origin'); function adoptMetaWhenBothFramesHaveLoaded() { if (sourceLoads === 1 && destinationLoads === 1) { destination.contentDocument.body.appendChild(source.contentDocument.querySelector('meta')); @@ -1452,7 +1457,7 @@ destination.srcdoc = '

destination

'; vm.eval("String(destination.contentDocument.querySelectorAll('meta').length)") .unwrap(), "1", - "the load callback should adopt the refresh element into the sandboxed document", + "the load callback should adopt the refresh element into the destination document", ); vm.advance_timers_until_deadline_for_test(&loader) .await @@ -1460,19 +1465,86 @@ destination.srcdoc = '

destination

'; assert_eq!( vm.eval("JSON.stringify([source.contentWindow.location.href, destination.contentWindow.location.href, sourceLoads, destinationLoads])") .unwrap(), - r#"["about:srcdoc#refreshed","about:srcdoc",1,1]"#, - "adoption must preserve the original refresh and must not refresh the sandboxed destination", + if source_sandboxed { + r#"["about:srcdoc","about:srcdoc#refreshed",1,1]"# + } else { + r#"["about:srcdoc#refreshed","about:srcdoc",1,1]"# + }, + "refreshes belong to each document's sandbox policy when the meta is processed", ); } #[tokio::test] async fn child_meta_refresh_survives_adoption_when_source_loads_last() { - assert_child_meta_refresh_survives_adoption_during_load(false).await; + assert_child_meta_refresh_survives_adoption_during_load(false, false).await; } #[tokio::test] async fn child_meta_refresh_survives_adoption_when_destination_loads_last() { - assert_child_meta_refresh_survives_adoption_during_load(true).await; + assert_child_meta_refresh_survives_adoption_during_load(true, false).await; +} + +#[tokio::test] +async fn child_meta_refresh_adoption_into_loaded_unsandboxed_document_when_source_loads_last() { + assert_child_meta_refresh_survives_adoption_during_load(false, true).await; +} + +#[tokio::test] +async fn child_meta_refresh_adoption_into_unsandboxed_document_when_destination_loads_last() { + assert_child_meta_refresh_survives_adoption_during_load(true, true).await; +} + +async fn assert_child_meta_refresh_insertion_after_load(in_shadow_tree: bool) { + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + let mut vm = new_storage_test_vm_with_loader("https://child-meta-refresh.test/", &loader); + vm.eval( + r#" +const root = document.documentElement || document.appendChild(document.createElement('html')); +const frame = document.createElement('iframe'); +frame.srcdoc = '

loaded

'; +root.appendChild(frame); +'queued' +"#, + ) + .unwrap(); + vm.drain_pending_child_frame_work_for_test(); + vm.eval(&format!( + "globalThis.insertInShadowTree = {in_shadow_tree};" + )) + .unwrap(); + vm.eval( + r#" +const container = frame.contentDocument.createElement('div'); +frame.contentDocument.body.appendChild(container); +const target = insertInShadowTree ? container.attachShadow({mode:'open'}) : container; +target.innerHTML = ''; +String(target.querySelectorAll('meta').length) +"#, + ) + .map(|count| assert_eq!(count, "1")) + .unwrap(); + vm.advance_timers_until_deadline_for_test(&loader) + .await + .unwrap(); + assert_eq!( + vm.eval("frame.contentWindow.location.href").unwrap(), + if in_shadow_tree { + "about:srcdoc" + } else { + "about:srcdoc#inserted" + }, + "a post-load refresh must apply only in the document's light tree", + ); +} + +#[tokio::test] +async fn child_meta_refresh_applies_to_insertion_after_load() { + assert_child_meta_refresh_insertion_after_load(false).await; +} + +#[tokio::test] +async fn child_meta_refresh_ignores_insertion_into_shadow_tree_after_load() { + assert_child_meta_refresh_insertion_after_load(true).await; } #[tokio::test]