From 3545547741a6dea95c5d2323c51a8024eeede7dc Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 6 Oct 2026 08:47:38 +0800 Subject: [PATCH] fix(renderer): reconcile native Window lifecycle with protocol Page ownership Use the canonical Window.document and Location objects across related Pages, retain producer identity and inherited about-document settings, and preserve the Page dispatch policy for cross-document history traversal. Route stop and close through exact Document owner actions and apply them through ordered protocol target lanes. Keep unload reentry checks and bootstrap output ordering. Window.opener=null preserves its accessor as specified; adjust the two conflicting protocol expectations while keeping the renderer regression. Register the main realm owner before native HistoryEntry bootstrap so inherited origins remain in committed history; replace an initial-empty entry consistently in both navigate events and Location seeds. Empty URL reuse can retain a focus activation with navigation_requested=false while preserving every document/history/load assertion. The renderer enum producer and exhaustive protocol consumer change together. Formatting, strict workspace Clippy and the complete nextest suite passed against unchanged final source with 32 workers and zero retries. --- moli-core/src/page/mod.rs | 4 +- .../browser_context/target_session_owner.rs | 26 ++ moli-protocol/src/conn/state/navigation.rs | 11 +- moli-protocol/src/conn/state/page_slot.rs | 19 ++ moli-protocol/src/conn/state/runtime_slot.rs | 14 ++ .../output_ingress/prepared_outputs.rs | 8 + .../src/domains/activity/output_slot.rs | 5 + moli-protocol/src/domains/page.rs | 4 + moli-protocol/src/domains/page/navigation.rs | 24 +- .../src/domains/page/navigation_commit.rs | 2 +- moli-protocol/src/domains/page/output.rs | 42 ++++ moli-protocol/src/domains/target/popup.rs | 1 + .../p1_target_multipage.rs | 4 +- moli-protocol/src/testing.rs | 188 ++++++++------- moli-renderer-v8/src/context_bootstrap.rs | 17 +- .../constructors/dom_exception.rs | 10 - .../src/context_bootstrap/history_mutation.rs | 9 +- .../history_runtime/admission.rs | 4 +- .../context_bootstrap/location_navigation.rs | 116 ++++++--- .../navigation_cancellation.rs | 8 +- .../src/context_bootstrap/navigation_entry.rs | 4 +- .../navigation_interception.rs | 8 + .../navigation_projection.rs | 4 +- .../src/context_bootstrap/navigation_seed.rs | 6 +- .../context_bootstrap/navigation_traversal.rs | 46 +++- .../navigation_traversal_execution.rs | 72 ++++-- .../context_bootstrap/navigation_window.rs | 24 +- .../src/context_bootstrap/session_history.rs | 8 +- .../session_history_traversal.rs | 78 +++--- .../src/context_bootstrap/window_accessors.rs | 4 +- .../window_accessors/child_context.rs | 6 +- .../window_accessors/document.rs | 19 +- .../window_accessors/surface.rs | 17 +- .../src/context_bootstrap/window_runtime.rs | 4 +- .../window_runtime/dialogs.rs | 44 +++- .../src/frame_owner_model/records.rs | 10 +- .../src/frame_owner_model/store.rs | 68 ++++-- .../src/frame_owner_model/store_tests.rs | 47 ++++ moli-renderer-v8/src/lib.rs | 18 +- .../context_host/child_documents/lifecycle.rs | 1 + .../child_frame_runtime/isolated_world.rs | 2 +- .../child_frame_runtime/window.rs | 223 ++++++++++++------ .../child_frame_runtime/window/main_page.rs | 210 ++++++----------- .../context_host/child_frames/discovery.rs | 7 + .../context_host/child_frames/lookup.rs | 11 +- .../src/native_bridge/context_host/core.rs | 32 ++- .../context_host/main_document_lifecycle.rs | 33 ++- .../src/native_bridge/context_host/mod.rs | 2 +- .../native_bridge/context_host/navigation.rs | 18 ++ .../src/native_bridge/context_host/popups.rs | 153 +++++------- .../context_host/popups/realm.rs | 2 +- .../context_host/popups/renderer_owned.rs | 59 ++++- .../context_host/runtime_observable.rs | 18 +- .../window_execution_context/settings.rs | 2 +- .../context_host/window_security_tokens.rs | 9 +- .../element/activation/targets.rs | 144 ++++++++--- .../src/native_bridge/identity.rs | 74 ++++-- .../identity/dense_reflector_map.rs | 7 - .../src/runtime/owner/document_response.rs | 25 +- .../src/runtime/owner_local_store/entry.rs | 6 + .../src/runtime/page_vm/auxiliary_page.rs | 5 +- .../runtime/page_vm/followed_navigation.rs | 26 +- moli-renderer-v8/src/runtime/page_vm/mod.rs | 15 +- .../src/runtime/protocol_output/item.rs | 3 + .../protocol_output/transport_memory.rs | 3 +- moli-renderer-v8/src/script_vm.rs | 33 ++- .../src/script_vm/document_environment.rs | 73 ++++-- .../src/script_vm/document_isolate.rs | 17 ++ .../src/script_vm/execution_contexts.rs | 7 +- .../src/script_vm/initial_window_document.rs | 50 ++-- moli-renderer-v8/src/script_vm/popup_close.rs | 22 ++ moli-renderer-v8/src/script_vm/post_parse.rs | 14 +- .../src/script_vm/related_page.rs | 16 ++ .../src/script_vm/runtime_bindings.rs | 5 +- .../src/script_vm/tests/browser_api/mod.rs | 2 - .../browser_api/window_open_empty_url.rs | 9 +- .../tests/extracted/realms_and_teardown.rs | 4 +- moli-renderer-v8/src/script_vm/tests/mod.rs | 1 - moli-renderer-v8/src/window_host.rs | 37 ++- moli-window-features/src/lib.rs | 5 - 80 files changed, 1610 insertions(+), 778 deletions(-) diff --git a/moli-core/src/page/mod.rs b/moli-core/src/page/mod.rs index d94f4eea58..5834022b96 100644 --- a/moli-core/src/page/mod.rs +++ b/moli-core/src/page/mod.rs @@ -136,8 +136,8 @@ pub use moli_renderer_v8::{ RendererServiceWorkerVersionStatus, RendererSetDocumentContentResult, RendererSharedWorkerConsoleMessage, RendererSharedWorkerTargetEvent, RendererSharedWorkerTargetInfo, RendererSyntheticResponseBody, RendererTextSearchMatch, - RendererTouchEventInput, RendererTouchPoint, RendererVisualStateToken, RendererWindowDocumentSource, - RuntimeConsoleMessageSnapshot, + RendererTouchEventInput, RendererTouchPoint, RendererVisualStateToken, + RendererWindowDocumentSource, RuntimeConsoleMessageSnapshot, }; pub use moli_renderer_v8::{ RendererAppManifest, RendererAppManifestDisplayMode, RendererAppManifestError, diff --git a/moli-protocol/src/conn/browser_context/target_session_owner.rs b/moli-protocol/src/conn/browser_context/target_session_owner.rs index 471b0150e9..c6292bc2a9 100644 --- a/moli-protocol/src/conn/browser_context/target_session_owner.rs +++ b/moli-protocol/src/conn/browser_context/target_session_owner.rs @@ -1407,6 +1407,32 @@ impl CdpConnection { .clear_pending_navigation_history_update() } + pub(crate) fn stop_renderer_location_navigation_for_owner( + &mut self, + owner: &CommandOwnerScope, + source_document: moli_core::RendererDocumentLifecycleIdentity, + ) { + let Some(mut resolved) = self.target_session_owner_mut_for_owner(owner) else { + return; + }; + let target = resolved.target_mut(); + // stop() aborts traversal signals and API promises, but script cannot + // cancel the browser's physical cross-document history traversal. + if target + .owner_state + .navigation_history_state + .is_pending_traversal() + { + return; + } + if target + .runtime_slot + .stop_pending_document_navigation(source_document) + { + target.owner_state.clear_pending_navigation_history_update(); + } + } + pub(crate) async fn mark_target_crashed_for_owner_async( &mut self, owner: &CommandOwnerScope, diff --git a/moli-protocol/src/conn/state/navigation.rs b/moli-protocol/src/conn/state/navigation.rs index 739919f64b..9158a40e21 100644 --- a/moli-protocol/src/conn/state/navigation.rs +++ b/moli-protocol/src/conn/state/navigation.rs @@ -239,6 +239,13 @@ impl TargetNavigationHistoryState { self.pending_update = None; } + pub(crate) fn is_pending_traversal(&self) -> bool { + matches!( + self.pending_update, + Some(PendingNavigationHistoryUpdate::TraverseToEntry(_)) + ) + } + pub(crate) fn is_pending_reload(&self) -> bool { matches!( self.pending_update, @@ -287,8 +294,8 @@ impl TargetNavigationHistoryState { } let mutation = match self.pending_update { Some( - PendingNavigationHistoryUpdate::ReplaceCurrent - | PendingNavigationHistoryUpdate::ReplaceInitialEmptyDocument, + PendingNavigationHistoryUpdate::ReplaceCurrent(_) + | PendingNavigationHistoryUpdate::ReplaceInitialEmptyDocument(_), ) => moli_page_types::NavigationHistoryMutation::Replace, _ => moli_page_types::NavigationHistoryMutation::Push, }; diff --git a/moli-protocol/src/conn/state/page_slot.rs b/moli-protocol/src/conn/state/page_slot.rs index 40795a4fe3..6ca8037db9 100644 --- a/moli-protocol/src/conn/state/page_slot.rs +++ b/moli-protocol/src/conn/state/page_slot.rs @@ -680,6 +680,25 @@ impl TargetPageSlot { } } + pub(crate) fn cancel_uncommitted_document_navigation(&mut self) -> bool { + let Some(token) = self + .pending_navigation_request + .as_ref() + .filter(|request| !request.committed) + .map(|request| request.token.clone()) + else { + return false; + }; + self.cancel_pending_document_navigation(); + if matches!(&self.pending_renderer_page, + Some(PendingRendererPageBinding::DocumentNavigation { navigation, .. }) + if navigation == &token) + { + self.pending_renderer_page = None; + } + true + } + pub(crate) fn document_navigation_cancellation_handle( &self, token: &DocumentNavigationToken, diff --git a/moli-protocol/src/conn/state/runtime_slot.rs b/moli-protocol/src/conn/state/runtime_slot.rs index 174b09c911..0b2d9fbee8 100644 --- a/moli-protocol/src/conn/state/runtime_slot.rs +++ b/moli-protocol/src/conn/state/runtime_slot.rs @@ -665,6 +665,20 @@ impl TargetRuntimeSlot { self.page_slot.has_pending_document_navigation() } + pub(crate) fn stop_pending_document_navigation( + &mut self, + source_document: RendererDocumentLifecycleIdentity, + ) -> bool { + if self + .committed_renderer_document_binding() + .map(CommittedRendererDocumentBinding::renderer_document_identity) + != Some(source_document) + { + return false; + } + self.page_slot.cancel_uncommitted_document_navigation() + } + pub(crate) fn moli_memory_diagnostics(&self) -> Value { json!({ "hasLoadedPage": self.has_loaded_page(), diff --git a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs index 8891730a92..a2054e593a 100644 --- a/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs +++ b/moli-protocol/src/domains/activity/output_ingress/prepared_outputs.rs @@ -209,6 +209,14 @@ impl PreparedProtocolOutputs { ) -> Self { let mut prepared = Self::empty(); match action { + RendererOwnerAction::StopTopLevelLocationNavigation { source_document } => { + crate::domains::page::PagePreparedOutputs::from_renderer_location_navigation_stop( + conn, + owner, + source_document, + ) + .append_to_location_navigation_stop_output_sink(&mut prepared); + } RendererOwnerAction::FileChooser(activation) => { crate::domains::input::InputPreparedOutputs::from_renderer_file_chooser_activation( conn, owner, activation, diff --git a/moli-protocol/src/domains/activity/output_slot.rs b/moli-protocol/src/domains/activity/output_slot.rs index 56fb472897..c0f5d9d3b3 100644 --- a/moli-protocol/src/domains/activity/output_slot.rs +++ b/moli-protocol/src/domains/activity/output_slot.rs @@ -32,6 +32,7 @@ pub(in crate::domains) enum ProtocolOutputSlot { WebMcp, PendingSubresourceContinueEvents, TopLevelLocationNavigation, + StopTopLevelLocationNavigation, TopLevelHistoryTraversal, FileChooser, Download, @@ -83,6 +84,7 @@ impl ProtocolOutputSlot { | Self::SessionHistoryUpdate | Self::SameDocumentNavigation | Self::TopLevelLocationNavigation + | Self::StopTopLevelLocationNavigation | Self::TopLevelHistoryTraversal => ProtocolOutputDelivery::OwnerAction, Self::WindowOpen | Self::WebMcp @@ -117,6 +119,7 @@ impl ProtocolOutputSlot { ProtocolOutputResponseOrder::AfterResponse } Self::PendingSubresourceContinueEvents + | Self::StopTopLevelLocationNavigation // Blink's file-input activation probe queues // Page.fileChooserOpened synchronously. A script may continue into // document.open(), but Chromium still flushes the chooser event @@ -246,6 +249,7 @@ impl ProtocolOutputSlot { | Self::SessionHistoryUpdate | Self::SameDocumentNavigation | Self::TopLevelLocationNavigation + | Self::StopTopLevelLocationNavigation | Self::TopLevelHistoryTraversal => { crate::domains::page::project_page_output_async(self, conn, context, payloads) .await; @@ -288,6 +292,7 @@ mod tests { BeforeResponse, ), (TopLevelLocationNavigation, OwnerAction, AfterResponse), + (StopTopLevelLocationNavigation, OwnerAction, BeforeResponse), (TopLevelHistoryTraversal, OwnerAction, AfterResponse), (FileChooser, OwnerAction, BeforeResponse), (Download, OwnerAction, AfterResponse), diff --git a/moli-protocol/src/domains/page.rs b/moli-protocol/src/domains/page.rs index 939b8ec833..6797e35916 100644 --- a/moli-protocol/src/domains/page.rs +++ b/moli-protocol/src/domains/page.rs @@ -553,6 +553,10 @@ pub(crate) struct PagePreparedOutputs { moli_page_types::SessionHistoryUpdate, )>, top_level_location_navigation: Option, + stopped_location_navigations: Vec<( + crate::conn::TargetPageResidenceIdentity, + RendererDocumentLifecycleIdentity, + )>, top_level_history_traversal: Option, } diff --git a/moli-protocol/src/domains/page/navigation.rs b/moli-protocol/src/domains/page/navigation.rs index 9354126741..4ca0aa046f 100644 --- a/moli-protocol/src/domains/page/navigation.rs +++ b/moli-protocol/src/domains/page/navigation.rs @@ -2320,17 +2320,9 @@ pub(super) fn start_session_owner_navigation_from_renderer( conn.mark_next_auxiliary_navigation_history_for_owner(owner, kind); } let result_projection = NavigationResultProjection::Cdp(result_payload); - // Top-level Navigation API traversals hand admission to the browser, - // unlike ordinary renderer navigations whose entry points own the - // source check. Keep this separate from the event's initiator. - let beforeunload = if navigation_history - .as_ref() - .is_some_and(|history| history.navigation_type() == Some("traverse")) - { - BeforeUnloadCheck::Required - } else { - BeforeUnloadCheck::RendererHandled - }; + // Renderer entry points check the exact outgoing document tree before + // handing the request to the browser, including named Window reuse. + let beforeunload = BeforeUnloadCheck::RendererHandled; start_navigate_to_url_command_with_background_policy_and_request( conn, None, @@ -2925,9 +2917,13 @@ fn start_navigate_to_url_command_with_background_policy( allow_background_navigation, request_load_policy, initiator, - // Browser commands and renderer History API handoffs reach this - // entry point before checking the outgoing document tree. - BeforeUnloadCheck::Required, + // Renderer History API handoffs have already checked the outgoing + // tree. Browser commands still own that check here. + if matches!(initiator, NavigationStartInitiator::Renderer) { + BeforeUnloadCheck::RendererHandled + } else { + BeforeUnloadCheck::Required + }, None, None, None, diff --git a/moli-protocol/src/domains/page/navigation_commit.rs b/moli-protocol/src/domains/page/navigation_commit.rs index d0d1876ea8..e4159af69e 100644 --- a/moli-protocol/src/domains/page/navigation_commit.rs +++ b/moli-protocol/src/domains/page/navigation_commit.rs @@ -36,7 +36,7 @@ pub(super) async fn commit_loaded_navigation_async( response_from_cache, main_document_body, initial_runtime_realms, - mut renderer_output_predecessor, + renderer_output_predecessor, main_document_commit, progress_gate, network_error_page, diff --git a/moli-protocol/src/domains/page/output.rs b/moli-protocol/src/domains/page/output.rs index 2f6aa7f56a..46dd76ddd8 100644 --- a/moli-protocol/src/domains/page/output.rs +++ b/moli-protocol/src/domains/page/output.rs @@ -1,6 +1,30 @@ use super::*; impl PagePreparedOutputs { + pub(crate) fn from_renderer_location_navigation_stop( + conn: &CdpConnection, + owner: &CommandOwnerScope, + source_document: RendererDocumentLifecycleIdentity, + ) -> Self { + let Some(residence) = conn.target_page_residence_identity_for_owner(owner) else { + return Self::default(); + }; + Self { + stopped_location_navigations: vec![(residence, source_document)], + ..Self::default() + } + } + + pub(in crate::domains) fn append_to_location_navigation_stop_output_sink( + self, + sink: &mut (impl ProtocolOutputSink + ?Sized), + ) { + if !self.stopped_location_navigations.is_empty() { + sink.push_produced_slot(ProtocolOutputSlot::StopTopLevelLocationNavigation); + sink.push_prepared_payload(PagePreparedOutputSlot::from_outputs(self).into()); + } + } + pub(crate) fn from_renderer_auxiliary_window_navigation( conn: &CdpConnection, owner: &CommandOwnerScope, @@ -382,6 +406,8 @@ impl PagePreparedOutputs { } self.same_document_navigations .extend(other.same_document_navigations); + self.stopped_location_navigations + .extend(other.stopped_location_navigations); if self.top_level_location_navigation.is_none() { self.top_level_location_navigation = other.top_level_location_navigation; } @@ -510,6 +536,7 @@ impl PagePreparedOutputs { child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -539,6 +566,7 @@ impl PagePreparedOutputs { child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -590,6 +618,7 @@ impl PagePreparedOutputs { child_frame_activities: vec![activity], same_document_navigations: Vec::new(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -616,6 +645,7 @@ impl PagePreparedOutputs { }) .collect(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: None, top_level_history_traversal: None, } @@ -637,6 +667,7 @@ impl PagePreparedOutputs { child_frame_activities: Vec::new(), same_document_navigations: Vec::new(), session_history_updates: Vec::new(), + stopped_location_navigations: Vec::new(), top_level_location_navigation: navigation .map(|navigation| PagePreparedTopLevelLocationNavigation::new(owner, navigation)), top_level_history_traversal: None, @@ -935,6 +966,17 @@ pub(in crate::domains) async fn project_page_output_async( prepared_outputs, ); } + ProtocolOutputSlot::StopTopLevelLocationNavigation => { + if let Some(slot) = prepared_outputs.page_mut() { + for (residence, source_document) in + std::mem::take(&mut slot.outputs.stopped_location_navigations) + { + if conn.target_page_residence_identity_is_current(&residence) { + conn.stop_renderer_location_navigation_for_owner(&owner, source_document); + } + } + } + } ProtocolOutputSlot::TopLevelHistoryTraversal => { let mut events = Vec::new(); emit_top_level_history_traversal_activity_background_events_async( diff --git a/moli-protocol/src/domains/target/popup.rs b/moli-protocol/src/domains/target/popup.rs index 78ecb34c69..1b9835159f 100644 --- a/moli-protocol/src/domains/target/popup.rs +++ b/moli-protocol/src/domains/target/popup.rs @@ -687,6 +687,7 @@ pub(crate) async fn complete_popup_target_navigation_owner_action_async( &[], request_kind, None, + None, auxiliary_navigation, document_response, initial_document_environment, diff --git a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs index 5b26f66a0b..00607bf60a 100644 --- a/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs +++ b/moli-protocol/src/domains/target/tests/tests_cdp_chromium_imports/p1_target_multipage.rs @@ -1277,7 +1277,7 @@ async fn rust_cdp_chromium_target_named_reuse_updates_dom_opener_before_returnin &mut ctx, &parent_session, 269_027, - "child.opener=null;Object.getOwnPropertyDescriptor(child,'opener').value===null", + "child.opener=null;child.opener===null && typeof Object.getOwnPropertyDescriptor(child,'opener').get==='function'", ) .await; assert_eq!(disowned["result"]["result"]["value"], true); @@ -1285,7 +1285,7 @@ async fn rust_cdp_chromium_target_named_reuse_updates_dom_opener_before_returnin &mut ctx, &sibling_session, 269_028, - "open('', 'child').opener===null", + "open('', 'child').opener===window", ) .await; assert_eq!(shadowed["result"]["result"]["value"], true); diff --git a/moli-protocol/src/testing.rs b/moli-protocol/src/testing.rs index 8673b9bcb0..3cbb076e11 100644 --- a/moli-protocol/src/testing.rs +++ b/moli-protocol/src/testing.rs @@ -15,7 +15,7 @@ use super::conn::{ }; use crate::automation::{AutomationCommand, AutomationResult, DevToolsError}; use crate::domains::activity::{ - ProtocolSchedulerWork, ProtocolSchedulerWorkKind, RuntimeCommandOutputBarrierCompletion, + ProtocolSchedulerWork, RuntimeCommandOutputBarrierCompletion, RuntimeCommandOutputBarrierPermit, RuntimeCommandOutputBarriers, }; use moli_core::{ @@ -920,7 +920,30 @@ impl TestContext { // renderer Page build, so composing both futures inline can // exceed Rust's default test-thread stack before the owner // turn gets a chance to yield. - let completed = Box::pin(pending.wait()).await; + let executes_javascript = pending.runtime_command_executes_page_javascript(); + let mut completion = Box::pin(pending.wait()); + let completed = if executes_javascript { + loop { + tokio::select! { + biased; + completed = &mut completion => break completed, + input = Box::pin(self.receive_one_test_scheduler_input()) => { + let (_, input) = input.expect( + "pending command lost its renderer scheduler inputs" + ); + // Receiving is cancellation-safe; projecting + // a publication is not. Finish its actions + // even if the command becomes ready meanwhile. + let mut work = VecDeque::from([input]); + Box::pin(self.route_test_scheduler_work_queue(&mut work)).await; + } + } + } + } else { + // Page construction must install its owner binding + // before admitting the renderer's first publication. + completion.await + }; step = Box::pin(self.conn.complete_pending_command_dispatch_with_context( completed, command_context, @@ -1374,75 +1397,76 @@ impl TestContext { return ready; } - let mut work = VecDeque::new(); + let Some((input_kind, input)) = Box::pin(self.receive_one_test_scheduler_input()).await + else { + return TestSchedulerTurnOutcome::Idle; + }; + let mut work = VecDeque::from([input]); + Box::pin(self.route_test_scheduler_work_queue(&mut work)).await; + TestSchedulerTurnOutcome::Processed(input_kind) + } + + async fn receive_one_test_scheduler_input( + &mut self, + ) -> Option<(TestSchedulerInputKind, TestSchedulerWork)> { let background_navigation_scheduler_enabled = self.background_navigation_scheduler_enabled; - let input_kind = if !self.pending_runtime_deferred_replies.is_empty() { + if !self.pending_runtime_deferred_replies.is_empty() { tokio::select! { biased; maybe_response = self.runtime_inspector_response_ready_rx.recv() => { - let Some(response) = maybe_response else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::RuntimeDeferredReplyReady(response)); - TestSchedulerInputKind::RuntimeDeferredReply + maybe_response.map(|response| ( + TestSchedulerInputKind::RuntimeDeferredReply, + TestSchedulerWork::RuntimeDeferredReplyReady(response), + )) } maybe_completion = self.background_navigation_completion_rx.recv(), if background_navigation_scheduler_enabled => { - let Some(completion) = maybe_completion else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::BackgroundNavigationCompletion(completion)); - TestSchedulerInputKind::BackgroundNavigationCompletion + maybe_completion.map(|completion| ( + TestSchedulerInputKind::BackgroundNavigationCompletion, + TestSchedulerWork::BackgroundNavigationCompletion(completion), + )) } maybe_event = self.background_event_rx.recv(), if background_navigation_scheduler_enabled => { - let Some(event) = maybe_event else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::BackgroundEvent(event)); - TestSchedulerInputKind::BackgroundEvent + maybe_event.map(|event| ( + TestSchedulerInputKind::BackgroundEvent, + TestSchedulerWork::BackgroundEvent(event), + )) } maybe_publication = self.renderer_publication_rx.recv() => { - let Some(publication) = maybe_publication else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::RendererPublication(publication)); - TestSchedulerInputKind::RendererPublication + maybe_publication.map(|publication| ( + TestSchedulerInputKind::RendererPublication, + TestSchedulerWork::RendererPublication(publication), + )) } } } else { tokio::select! { biased; maybe_completion = self.background_navigation_completion_rx.recv(), if background_navigation_scheduler_enabled => { - let Some(completion) = maybe_completion else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::BackgroundNavigationCompletion(completion)); - TestSchedulerInputKind::BackgroundNavigationCompletion + maybe_completion.map(|completion| ( + TestSchedulerInputKind::BackgroundNavigationCompletion, + TestSchedulerWork::BackgroundNavigationCompletion(completion), + )) } maybe_event = self.background_event_rx.recv(), if background_navigation_scheduler_enabled => { - let Some(event) = maybe_event else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::BackgroundEvent(event)); - TestSchedulerInputKind::BackgroundEvent + maybe_event.map(|event| ( + TestSchedulerInputKind::BackgroundEvent, + TestSchedulerWork::BackgroundEvent(event), + )) } maybe_publication = self.renderer_publication_rx.recv() => { - let Some(publication) = maybe_publication else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::RendererPublication(publication)); - TestSchedulerInputKind::RendererPublication + maybe_publication.map(|publication| ( + TestSchedulerInputKind::RendererPublication, + TestSchedulerWork::RendererPublication(publication), + )) } maybe_response = self.runtime_inspector_response_ready_rx.recv() => { - let Some(response) = maybe_response else { - return TestSchedulerTurnOutcome::Idle; - }; - work.push_back(TestSchedulerWork::RuntimeDeferredReplyReady(response)); - TestSchedulerInputKind::RuntimeDeferredReply + maybe_response.map(|response| ( + TestSchedulerInputKind::RuntimeDeferredReply, + TestSchedulerWork::RuntimeDeferredReplyReady(response), + )) } } - }; - Box::pin(self.route_test_scheduler_work_queue(&mut work)).await; - TestSchedulerTurnOutcome::Processed(input_kind) + } } async fn route_protocol_events_like_scheduler( @@ -1565,47 +1589,9 @@ impl TestContext { work: &mut VecDeque, ) { loop { - let selected_index = match self.pending_protocol_scheduler_work.front() { - Some(front) if front.is_ready() => 0, - Some(front) - if front.kind() == ProtocolSchedulerWorkKind::MainDocumentLoadOwnerAction => - { - let Some(index) = - self.pending_protocol_scheduler_work - .iter() - .position(|candidate| { - candidate.is_ready() - && candidate.navigation_dependency() - == crate::ProtocolNavigationDependency::ReplacesPendingLoad - }) - else { - return; - }; - // Production checks the pending load observer out of the - // FIFO while it waits for the renderer. An unconstrained - // location owner action may then run and replace that - // exact source Document, which completes the observer as - // Superseded. Keeping the pending observer at the front in - // this protocol-only harness would deadlock the action - // needed to make it terminal. - index - } - Some(_) | None => return, - }; - if !self.background_navigation_scheduler_enabled - && self - .pending_protocol_scheduler_work - .get(selected_index) - .is_some_and(ProtocolSchedulerWork::requires_background_navigation_scheduler) - { - // The default protocol fixture has no owner task lane. Keep - // independent popup navigation resident rather than invoking - // the production function's synchronous fallback while the - // exact renderer cursor is still being projected. Tests that - // assert navigation progress opt into the production-shaped - // background scheduler and drive its typed completions. + let Some(selected_index) = self.next_ready_protocol_work_index() else { return; - } + }; let protocol_work = self .pending_protocol_scheduler_work .remove(selected_index) @@ -1624,6 +1610,34 @@ impl TestContext { } } + fn next_ready_protocol_work_index(&self) -> Option { + // Match production's per-target ordering. An unfinished opener load + // cannot block an independent popup, and a replacement must reach + // the owner that supersedes its pending load. + let mut blocked_targets = Vec::new(); + for (index, candidate) in self.pending_protocol_scheduler_work.iter().enumerate() { + let target = candidate.navigation_gate_target_id(); + if target.is_some_and(|target| blocked_targets.contains(&target)) + && candidate.navigation_dependency() + != crate::ProtocolNavigationDependency::ReplacesPendingLoad + { + continue; + } + if !candidate.is_ready() + || (!self.background_navigation_scheduler_enabled + && candidate.requires_background_navigation_scheduler()) + { + // An unscoped predecessor orders all later work. Fixtures + // that need popup navigation enable the background channels; + // otherwise retain that action on its target's lane. + blocked_targets.push(target?); + continue; + } + return Some(index); + } + None + } + async fn ingest_renderer_publication_like_scheduler( &mut self, publication: RendererOutputTransportMessage, diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index 49db58b453..4a6b018c57 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -90,8 +90,8 @@ mod navigation_restore; mod navigation_result; mod navigation_seed; mod navigation_serialize; -pub(crate) use navigation_serialize::current_document_referrer_policy; pub(crate) use navigation_entry::navigation_entry_public_token; +pub(crate) use navigation_serialize::current_document_referrer_policy; pub(crate) use navigation_serialize::{ document_referrer_policy_for_native_document, history_document_id_for_holder, publish_top_level_navigation_history, @@ -183,17 +183,15 @@ pub(crate) use css_fontface_runtime::{ pub(crate) use form_navigation::FormNavigationHistory; pub(crate) use history_mutation::update_history_for_document_open; pub(crate) use location_navigation::{ + HyperlinkNavigationOptions, LocationNavigationKind, blocks_ancestor_location_navigation, dispatch_top_level_navigation_event_with_source_element, - HyperlinkNavigationOptions, LocationNavigationKind, navigate_location_object_for_hyperlink, - is_native_location, + history_entry_seed_for_cross_document_location, is_native_location, meta_refresh_navigation_kind, navigate_borrowed_location, + navigate_location_object_for_element_fragment, navigate_location_object_for_hyperlink, navigate_location_object_with_child_navigate_event, - history_entry_seed_for_cross_document_location, - navigate_location_object_with_child_navigate_event_and_initiator_url, navigate_location_object_with_source_element, navigate_top_level_meta_refresh, navigate_top_level_same_document_from_browser, resolve_cross_window_location_target, - navigate_location_object_for_element_fragment, }; pub(crate) use navigation_cancellation::{ NavigationCancellationReason, inform_about_canceled_navigation_for_window, @@ -367,9 +365,8 @@ pub(crate) use self::indexed_db::{ flush_blocked_indexed_db_requests, flush_indexed_db_connection_notification, flush_indexed_db_task_by_id, flush_indexed_db_transaction_starts, flush_next_indexed_db_task, indexed_db_has_pending_tasks, install_worker_indexed_db_runtime_state, - retire_indexed_db_context, set_indexed_db_manager_for_context, - set_worker_indexed_db_task_wake_for_context, - retain_indexed_db_state_in_retired_realm, + retain_indexed_db_state_in_retired_realm, retire_indexed_db_context, + set_indexed_db_manager_for_context, set_worker_indexed_db_task_wake_for_context, }; #[cfg(test)] pub(crate) use self::indexed_db::{ @@ -474,7 +471,6 @@ pub(crate) use self::runtime_state::install_webassembly_runtime_state; #[cfg(feature = "wpt-extensions")] pub(crate) use self::runtime_state::install_wpt_webdriver_runtime_state; pub(crate) use self::runtime_state::set_window_origin_runtime_state; -pub(crate) use self::runtime_state::window_realm_secure_context_available; pub(crate) use self::runtime_state::{ ORIGINAL_WEBASSEMBLY_COMPILE_ERROR_CONSTRUCTOR_SLOT, ORIGINAL_WEBASSEMBLY_GLOBAL_VALUE_GETTER_SLOT, ORIGINAL_WEBASSEMBLY_INSTANCE_CONSTRUCTOR_SLOT, @@ -1199,7 +1195,6 @@ pub(crate) fn live_ranges_detached_text_split<'s>( pub(crate) use self::window_accessors::{ bind_current_child_window_document, current_window_style_viewport, retain_window_document_in_retired_realm, window_parent_or_top, - }; pub(crate) use session_history::{ diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs index 928273ed71..aa3c4a60e2 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception.rs @@ -545,16 +545,6 @@ pub(crate) fn new_dom_exception_value<'s>( scope, "DOMException", ) - .or_else(|| { - // A native exception must materialize its own realm's intrinsic. - // Reading the public Window property can run author code or enter - // an access check again after navigation detaches its global. - crate::context_bootstrap::exposed_interfaces::ensure_intrinsic_interface_prototype( - scope, - "DOMException", - ) - .ok() - }) { let exception = v8::Object::new(scope); initialize_dom_exception(scope, exception, message, name); diff --git a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs index fce43c7e04..62309a717f 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_mutation.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_mutation.rs @@ -27,7 +27,6 @@ use super::navigation_window::{ runtime_window_is_global, window_history_for_holder, window_location_for_holder, window_navigation_for_holder, }; -use super::*; use crate::context_bootstrap::navigation_entry::wrappers as entry_wrappers; use crate::structured_clone::{deserialize_history_state, serialize_history_state}; use crate::webidl; @@ -210,7 +209,7 @@ fn mutate_history_object<'s>( // Admission follows argument conversion, state serialization and URL // validation, but precedes navigation cancellation and event dispatch. // https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-state-steps - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return; }; let Some(dispatch_scope) = runtime_window_dispatch_scope(scope, owner) else { @@ -411,7 +410,7 @@ fn history_api_base_url<'s>( owner: v8::Local<'s, v8::Object>, current_url: &url::Url, ) -> url::Url { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return current_url.clone(); }; let host = unsafe { &*host_ptr }; @@ -439,7 +438,7 @@ fn history_document_url<'s>( owner: v8::Local<'s, v8::Object>, current_href: &str, ) -> Option { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return url::Url::parse(current_href).ok(); }; let host = unsafe { &*host_ptr }; @@ -466,7 +465,7 @@ fn effective_history_mutation_kind<'s>( if !matches!(kind, HistoryMutationKind::Push) { return kind; } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return kind; }; let host = unsafe { &*host_ptr }; diff --git a/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs b/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs index e2c80fa977..084399a306 100644 --- a/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs +++ b/moli-renderer-v8/src/context_bootstrap/history_runtime/admission.rs @@ -1,5 +1,5 @@ use super::super::navigation_window::{navigation_document_is_active, runtime_window_owner}; -use super::super::{context_host_ptr_from_global_bridge, throw_dom_exception_value}; +use super::super::throw_dom_exception_value; /// History API algorithms call this after WebIDL argument conversion. pub(in crate::context_bootstrap) fn require_fully_active_history_owner<'s>( @@ -10,7 +10,7 @@ pub(in crate::context_bootstrap) fn require_fully_active_history_owner<'s>( let is_fully_active = if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) { - context_host_ptr_from_global_bridge(scope) + super::super::window_accessors::window_host_ptr(scope, owner) .is_some_and(|host_ptr| unsafe { &*host_ptr }.lightweight_popup_is_open(popup_id)) } else { navigation_document_is_active(scope, owner) diff --git a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs index 7a541f171a..7c62a7ca3d 100644 --- a/moli-renderer-v8/src/context_bootstrap/location_navigation.rs +++ b/moli-renderer-v8/src/context_bootstrap/location_navigation.rs @@ -84,6 +84,7 @@ struct LocationNavigationOptions { dispatch_child_navigate_event_for_all_kinds: bool, force_exact_same_document_navigation: bool, explicit_initiator_url: Option, + explicit_navigation_initiator: Option, user_initiated: bool, source_can_access_target: Option, hyperlink: bool, @@ -93,7 +94,7 @@ struct LocationNavigationOptions { pub(crate) struct HyperlinkNavigationOptions { pub(crate) user_initiated: bool, pub(crate) source_can_access_target: bool, - pub(crate) initiator_url: url::Url, + pub(crate) initiator: crate::runtime::RendererNavigationInitiator, pub(crate) named_popup: Option, } @@ -115,7 +116,8 @@ pub(crate) fn navigate_location_object_for_hyperlink<'s>( source_can_access_target: Some(options.source_can_access_target), hyperlink: true, force_exact_same_document_navigation: true, - explicit_initiator_url: Some(options.initiator_url), + explicit_initiator_url: Some(options.initiator.url().clone()), + explicit_navigation_initiator: Some(options.initiator), named_hyperlink_popup: options.named_popup, ..LocationNavigationOptions::default() }, @@ -237,12 +239,7 @@ pub(crate) fn navigate_top_level_same_document_from_browser( // fragment-only string through Location: Chromium pushes a same-document // history entry and runs the Navigation/popstate surfaces even though the // serialized URL does not change. - navigate_location_object_for_same_document( - scope, - location, - &target, - kind, - ); + navigate_location_object_for_same_document(scope, location, &target, kind); true } @@ -371,6 +368,28 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( source_element: Option>, options: LocationNavigationOptions, ) { + if let Some(context) = location.get_creation_context(scope) + && context != scope.get_current_context() + && is_native_location(scope, location) + && crate::util::context_host_ptr_from_context_slot(context) + != context_host_ptr_from_global_bridge(scope) + { + // URL parsing retains the entry settings before entering the native + // receiver's realm. The incumbent source remains available for the + // initiator, origin and about-document inheritance checks below. + let raw_target = + raw_target.map(|raw| resolve_cross_window_location_target(scope, &raw).unwrap_or(raw)); + let scope = &mut v8::ContextScope::new(scope, context); + navigate_location_object_with_source_element_and_child_navigate_event( + scope, + location, + kind, + raw_target, + source_element, + options, + ); + return; + } // The Location setters and methods return before URL parsing when their // relevant Document is null, including when argument conversion removed it. if !location_has_relevant_document(scope, location) { @@ -393,6 +412,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( dispatch_child_navigate_event_for_all_kinds, force_exact_same_document_navigation, explicit_initiator_url, + explicit_navigation_initiator, user_initiated, source_can_access_target, hyperlink, @@ -425,7 +445,13 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( let navigation_source = if matches!(kind, LocationNavigationKind::Reload) { None } else { - capture_location_navigation_source(scope, owner, &resolved) + let captured = capture_location_navigation_source(scope, owner, &resolved); + match (explicit_navigation_initiator, captured) { + (Some(initiator), captured) => { + Some((initiator, captured.and_then(|(_, environment)| environment))) + } + (None, captured) => captured, + } }; if resolved.scheme() == "javascript" { let source_context = scope @@ -464,6 +490,14 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( }); let owner = runtime_window_owner(scope, location); let source_can_access_target = source_can_access_target.unwrap_or_else(|| { + if runtime_window_is_global(scope, owner) + && let Some(context) = owner.get_creation_context(scope) + { + let source = scope + .get_incumbent_context() + .unwrap_or_else(|| scope.get_current_context()); + return crate::native_bridge::window_contexts_allow_access(source, context); + } let Some(host_ptr) = context_host_ptr_for_navigation_owner(scope, owner) else { return true; }; @@ -477,6 +511,16 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( target, ) }); + // Child navigations capture the source origin and recheck the target when + // their queued task executes. Preserve that check instead of rejecting the + // Location write synchronously as for a top-level related Page. + if resolved.scheme() == "javascript" + && !source_can_access_target + && runtime_window_is_global(scope, owner) + { + crate::native_bridge::throw_cross_origin_location_security_error(scope); + return; + } let event_source_element = source_element.filter(|_| source_can_access_target); if navigation_unload_event_active(scope, owner) || super::navigation_cancellation::window_navigation_is_stopping(scope, owner) @@ -549,14 +593,16 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( let replaces_initial_about_blank = super::navigation_window::navigation_document_is_initial_empty(scope, owner) || child_handle.is_some_and(|handle| { - context_host_ptr_for_navigation_owner(scope, owner).is_some_and(|host_ptr| { - unsafe { &*host_ptr }.child_current_document_is_initial_empty(handle) - }) - }) || popup_id.is_some_and(|popup_id| { - context_host_ptr_for_navigation_owner(scope, owner).is_some_and(|host_ptr| { - unsafe { &*host_ptr }.lightweight_popup_current_document_is_initial_empty(popup_id) - }) - }); + context_host_ptr_for_navigation_owner(scope, owner).is_some_and(|host_ptr| { + unsafe { &*host_ptr }.child_current_document_is_initial_empty(handle) + }) + }) + || popup_id.is_some_and(|popup_id| { + context_host_ptr_for_navigation_owner(scope, owner).is_some_and(|host_ptr| { + unsafe { &*host_ptr } + .lightweight_popup_current_document_is_initial_empty(popup_id) + }) + }); let effective_kind = match kind { LocationNavigationKind::Assign if replaces_initial_about_blank => { LocationNavigationKind::Replace @@ -574,6 +620,9 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( }; let mut navigate_outcome = navigation.map(|navigation| { let _ = cancel_active_navigation_event(scope, navigation); + super::navigation_result::cancel_active_cross_document_navigation( + scope, navigation, None, + ); cancel_pending_precommit_same_document_navigation(scope, navigation); cancel_pending_precommit_history_traversal(scope, navigation); cancel_active_intercepted_same_document_navigation(scope, navigation); @@ -794,6 +843,16 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( return; } + let kind = if kind == LocationNavigationKind::Assign + && resolved.scheme() != "javascript" + && runtime_window_is_global(scope, owner) + && context_host_ptr_for_navigation_owner(scope, owner) + .is_some_and(|host| unsafe { &*host }.main_document_is_initial_empty()) + { + LocationNavigationKind::Replace + } else { + kind + }; let child_handle = if runtime_window_is_global(scope, owner) { None } else { @@ -804,11 +863,7 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( && let Some(navigation) = super::navigation_window::window_navigation_for_holder(scope, owner) { - if popup_id.is_some() { - super::navigation_result::cancel_active_cross_document_navigation( - scope, navigation, None, - ); - } + super::navigation_result::cancel_active_cross_document_navigation(scope, navigation, None); let _ = cancel_active_navigation_event(scope, navigation); cancel_pending_precommit_same_document_navigation(scope, navigation); cancel_pending_precommit_history_traversal(scope, navigation); @@ -863,14 +918,12 @@ fn navigate_location_object_with_source_element_and_child_navigate_event<'s>( ); return; } - if popup_id.is_some() { - super::navigation_result::track_cross_document_location_navigation( - scope, - navigation, - outcome.signal, - resolved.as_str(), - ); - } + super::navigation_result::track_cross_document_location_navigation( + scope, + navigation, + outcome.signal, + resolved.as_str(), + ); } if let Some(popup_id) = popup_id { @@ -996,6 +1049,7 @@ pub(crate) fn is_native_location<'s>( scope: &mut v8::PinScope<'s, '_>, receiver: v8::Local<'s, v8::Object>, ) -> bool { + let receiver = moli_webapi_declare::web_api_object_target(scope, receiver).unwrap_or(receiver); crate::util::get_private_value(scope, receiver, WINDOW_LOCATION_HREF_SLOT).is_some() } @@ -1093,7 +1147,7 @@ fn capture_location_navigation_source<'s>( } } -fn blocks_ancestor_location_navigation<'s>( +pub(crate) fn blocks_ancestor_location_navigation<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, destination: &url::Url, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs b/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs index 7ed4313d36..6d178e34c7 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_cancellation.rs @@ -16,7 +16,7 @@ use super::{ use crate::native_bridge::{ JsContextHost, OwnerDispatchScope, WindowDocumentTaskTarget, WindowExecutionContextBinding, }; -use crate::util::{context_host_ptr_from_global_bridge, get_private_value, set_private_value}; +use crate::util::{get_private_value, set_private_value}; const WINDOW_STOP_ACTIVE_SLOT: &str = "__lmWindowStopActive"; @@ -133,7 +133,7 @@ pub(crate) fn stop_navigation_for_window_and_descendants<'s>( window: v8::Local<'s, v8::Object>, binding: WindowExecutionContextBinding, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, window) else { return; }; let host = unsafe { &*host_ptr }; @@ -216,7 +216,7 @@ pub(super) fn clear_pending_cross_document_navigation_for_window<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, ) { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, window) else { return; }; let host = unsafe { &mut *host_ptr }; @@ -238,7 +238,7 @@ pub(super) fn clear_pending_cross_document_navigation_for_window<'s>( } match dispatch_scope { Some(crate::native_bridge::OwnerDispatchScope::Top) => { - host.clear_pending_location_navigation(); + host.cancel_pending_location_navigation(); } Some(crate::native_bridge::OwnerDispatchScope::Child(handle)) => { host.cancel_pending_child_browsing_context_navigation(handle); diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs b/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs index cd3c2ab78a..b2e31c1c5b 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_entry.rs @@ -181,7 +181,9 @@ pub(super) fn navigation_document_origin<'s>( ) -> String { super::navigation_window::runtime_window_dispatch_scope(scope, owner) .and_then(|dispatch_scope| { - crate::util::context_host_ptr_from_global_bridge(scope) + owner + .get_creation_context(scope) + .and_then(crate::util::context_host_ptr_from_context_slot) .and_then(|host| unsafe { &*host }.window_document_origin(dispatch_scope)) }) .or_else(|| { diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_interception.rs b/moli-renderer-v8/src/context_bootstrap/navigation_interception.rs index 925c9747a5..99b403d875 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_interception.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_interception.rs @@ -118,6 +118,14 @@ pub(super) fn dispatch_cross_document_navigation_event<'s>( return false; } if !outcome.intercepted { + if !same_document && download_request.is_none() { + super::navigation_result::track_cross_document_location_navigation( + scope, + navigation, + outcome.signal, + href, + ); + } return true; } cancel_pending_same_document_navigation_finishes(scope, navigation); diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs index 667dcd8278..a5a5879e56 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_projection.rs @@ -1,7 +1,5 @@ use super::history_runtime::native; -use super::navigation_window::{ - runtime_window_dispatch_scope, -}; +use super::navigation_window::runtime_window_dispatch_scope; use crate::context_bootstrap::navigation_entry::wrappers as entry_wrappers; use crate::util::{context_host_ptr_from_global_bridge, serialize_v8_iter_array}; use moli_history::HistoryEntryRef; diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs b/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs index 9b7e2004ff..39ba6010b0 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_seed.rs @@ -105,7 +105,11 @@ pub(super) fn build_current_navigation_entry_from_seed<'s>( HistoryEntry { url: "about:blank".to_owned(), inherited_origin: None, - document_origin: super::navigation_entry::navigation_document_origin(scope, owner, "about:blank"), + document_origin: super::navigation_entry::navigation_document_origin( + scope, + owner, + "about:blank", + ), referrer_policy: None, history_state: state.clone(), navigation_state: state, diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs index 6fa79cc4b0..b8ee65863f 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal.rs @@ -285,9 +285,7 @@ pub(super) fn navigation_reload_callback<'s>( if navigation_can_update_current_entry(scope, args.this()) && let Some(navigation) = window_navigation_for_holder(scope, owner) { - if crate::native_bridge::lightweight_popup_id_from_window(scope, owner).is_some() { - cancel_active_cross_document_navigation(scope, navigation, None); - } + cancel_active_cross_document_navigation(scope, navigation, None); let _ = cancel_active_navigation_event(scope, navigation); cancel_pending_precommit_same_document_navigation(scope, navigation); cancel_pending_precommit_history_traversal(scope, navigation); @@ -397,6 +395,48 @@ pub(super) fn navigation_reload_callback<'s>( rv.set(pending.object.into()); return; } + if super::navigation_window::runtime_window_is_global(scope, owner) + && let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) + && let Some(context) = owner.get_creation_context(scope) + && let Some(entry_seed) = + super::navigation_seed::history_entry_seed_for_reload(scope, owner) + && let Ok(url) = url::Url::parse(¤t_href) + { + let scope = &mut v8::ContextScope::new(scope, context); + if !unsafe { &mut *host_ptr }.dispatch_main_document_tree_beforeunload(scope) { + rv.set( + reload_canceled_after_dispatch_result( + scope, + navigation, + &outcome, + ¤t_href, + ) + .into(), + ); + return; + } + let about_document_state = unsafe { &*host_ptr }.capture_about_document_state( + crate::native_bridge::OwnerDispatchScope::Top, + &url, + None, + ); + unsafe { &mut *host_ptr }.record_pending_location_navigation_with_kind( + url, + Some(entry_seed), + moli_fetch::BrowserNavigationRequestKind::Reload, + about_document_state, + ); + rv.set( + navigation_cross_document_pending_result( + scope, + navigation, + outcome.signal, + ¤t_href, + ) + .into(), + ); + return; + } if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) && let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) && let Some(entry_seed) = diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs index f846f5c717..089392b960 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_traversal_execution.rs @@ -5,14 +5,12 @@ use super::navigation_events::{ NavigationDispatchOutcome, dispatch_navigation_traverse_event_with_outcome, }; use super::navigation_result::{ - cancel_active_cross_document_navigation, navigation_dom_exception, navigation_pending_result, + cancel_active_cross_document_navigation, navigation_dom_exception, navigation_rejected_dom_exception_result, track_cross_document_traversal_navigation, }; use super::navigation_seed::history_entry_seed_for_traversal; use super::navigation_window::{navigation_document_is_active, window_navigation_for_holder}; -use super::navigation_window::{ - runtime_window_is_global, window_history_for_holder, window_task_target_for_runtime_owner, -}; +use super::navigation_window::{window_history_for_holder, window_task_target_for_runtime_owner}; use super::*; use crate::native_bridge::{ PendingCrossDocumentTraversal, PendingHistoryTraversalAction, PendingNavigationResult, @@ -45,7 +43,7 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( "SecurityError", )); } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, target.owner) else { return Some(navigation_rejected_dom_exception_result( scope, "Navigation was canceled", @@ -60,7 +58,7 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( "AbortError", )); }; - if let Some((target_url, mut seed)) = history_entry_seed_for_traversal( + if let Some((_target_url, mut seed)) = history_entry_seed_for_traversal( scope, target.owner, target.current_index, @@ -68,10 +66,6 @@ pub(super) fn queue_navigation_traversal_with_result<'s>( ) { seed.session_history.target_step = target.joint_step; super::session_history::capture_for_navigation(scope, target.owner, &mut seed); - if runtime_window_is_global(scope, target.owner) { - host.record_pending_location_navigation(target_url, Some(seed.clone()), None); - return Some(navigation_pending_result(scope)); - } let target_key = traversal_target_entry(scope, &target) .map(|entry| entry.borrow().key.as_str().to_owned()); let receiver_context = target @@ -124,7 +118,7 @@ pub(super) fn queue_history_traversal_by_delta<'s>( delta: i64, ) { let owner = super::navigation_window::runtime_window_owner(scope, history); - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return; }; let host = unsafe { &mut *host_ptr }; @@ -175,6 +169,7 @@ fn apply_history_traversal_by_delta( crate::native_bridge::OwnerDispatchScope::Top, crate::native_bridge::OwnerDispatchScope::Top, ) + && host.dispatch_main_document_tree_beforeunload(scope) { host.record_pending_top_level_history_traversal(delta); } @@ -322,14 +317,18 @@ pub(in crate::context_bootstrap) fn apply_pending_cross_document_traversal<'s, ' } // close() queues retirement of this exact Window. Keep its dispatched // event and API results alive until the close task aborts them together. - let closing_popup = matches!(dispatch_scope, - crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) - if host.lightweight_popup_is_closing(popup_id)) - && host.current_window_document_task_target_for_dispatch_scope(dispatch_scope) - == retiring_document; + let closing_window = match dispatch_scope { + crate::native_bridge::OwnerDispatchScope::Top => host.browsing_context_is_closed(), + crate::native_bridge::OwnerDispatchScope::LightweightPopup(popup_id) => { + host.lightweight_popup_is_closing(popup_id) + } + crate::native_bridge::OwnerDispatchScope::Child(_) => false, + } && host + .current_window_document_task_target_for_dispatch_scope(dispatch_scope) + == retiring_document; if outcome.proceed && outcome.abort_error.is_none() - && (is_current(scope, host) || closing_popup) + && (is_current(scope, host) || closing_window) && let Some(navigation) = navigation { track_cross_document_traversal_navigation( @@ -344,7 +343,7 @@ pub(in crate::context_bootstrap) fn apply_pending_cross_document_traversal<'s, ' // cross-document traversals cannot be canceled by script. Only continue // while the original Window and Document are still active. if !is_current(scope, host) { - if !closing_popup { + if !closing_window { reject_cross_document_traversal(scope, &traversal); } return false; @@ -366,7 +365,42 @@ pub(in crate::context_bootstrap) fn apply_pending_cross_document_traversal<'s, ' queued } crate::native_bridge::OwnerDispatchScope::Top => { - unreachable!("top-level traversals are handed off to the browser") + if host.page_script_environment().is_none_or(|environment| { + environment.top_level_navigation_dispatch() + == crate::RendererTopLevelNavigationDispatch::DelegateToBrowser + }) { + let Some(step) = traversal.seed.session_history.target_step else { + reject_cross_document_traversal(scope, &traversal); + return false; + }; + let binding = super::session_history::binding(scope, host, owner); + let Some(delta) = host.session_histories.get_mut(binding.popup).delta_to(step) + else { + reject_cross_document_traversal(scope, &traversal); + return false; + }; + if let Some(key) = traversal.target_key { + host.top_level_navigation_history() + .select_joint_traversal(key, step); + } + host.record_pending_top_level_history_traversal(delta); + return true; + } + let Ok(url) = url::Url::parse(&target_url) else { + reject_cross_document_traversal(scope, &traversal); + return false; + }; + if let Some(key) = traversal.target_key + && let Some(step) = traversal.seed.session_history.target_step + { + host.top_level_navigation_history() + .select_joint_traversal(key, step); + } + // The standalone adapter has no browser history controller to + // consume a delta. Retain its concrete destination and seed after + // the shared navigate/beforeunload dispatch above. + host.record_pending_location_navigation(url, Some(traversal.seed), None); + true } } } diff --git a/moli-renderer-v8/src/context_bootstrap/navigation_window.rs b/moli-renderer-v8/src/context_bootstrap/navigation_window.rs index 5b61a34a8d..e6fb889706 100644 --- a/moli-renderer-v8/src/context_bootstrap/navigation_window.rs +++ b/moli-renderer-v8/src/context_bootstrap/navigation_window.rs @@ -31,7 +31,7 @@ fn navigation_owner_local_window_id<'s>( owner: v8::Local<'s, v8::Object>, ) -> Option { let dispatch_scope = runtime_window_dispatch_scope(scope, owner)?; - let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host_ptr = super::window_accessors::window_host_ptr(scope, owner)?; let owner = unsafe { &*host_ptr }.current_window_execution_context_owner(dispatch_scope)?; Some(match owner { crate::native_bridge::WindowExecutionContextOwner::Frame(local_window_id) => { @@ -63,7 +63,7 @@ fn navigation_owner_document<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) -> Option { - let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host_ptr = super::window_accessors::window_host_ptr(scope, owner)?; super::window_accessors::window_document_handle(scope, owner, unsafe { &*host_ptr }) } @@ -247,7 +247,7 @@ pub(super) fn navigation_document_is_initial_empty<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) -> bool { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return false; }; let host = unsafe { &*host_ptr }; @@ -270,7 +270,7 @@ pub(super) fn navigation_document_has_disabled_entries<'s>( if navigation_document_has_opaque_origin(scope, owner) { return true; } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return false; }; let host = unsafe { &*host_ptr }; @@ -279,6 +279,9 @@ pub(super) fn navigation_document_has_disabled_entries<'s>( if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) { return !host.lightweight_popup_has_committed_navigation(popup_id); } + if runtime_window_is_global(scope, owner) { + return !host.main_document_has_committed_navigation(); + } child_browsing_context_handle_for_runtime_owner(scope, owner) .is_some_and(|handle| !host.child_has_committed_navigation(handle)) } @@ -287,7 +290,7 @@ pub(super) fn navigation_document_has_opaque_origin<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) -> bool { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return false; }; let host = unsafe { &*host_ptr }; @@ -311,7 +314,7 @@ pub(crate) fn navigation_unload_event_active<'s>( ) -> bool { get_private_value(scope, owner, WINDOW_UNLOAD_EVENT_ACTIVE_SLOT) .is_some_and(|value| value.is_true()) - || context_host_ptr_from_global_bridge(scope).is_some_and(|host_ptr| { + || super::window_accessors::window_host_ptr(scope, owner).is_some_and(|host_ptr| { let host = unsafe { &*host_ptr }; super::window_accessors::window_document_handle(scope, owner, host) .is_some_and(|document| host.has_document_unload_counter(document)) @@ -341,7 +344,7 @@ pub(super) fn navigation_document_base_url<'s>( owner: v8::Local<'s, v8::Object>, fallback_href: &str, ) -> Option { - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return url::Url::parse(fallback_href).ok(); }; let host = unsafe { &*host_ptr }; @@ -374,16 +377,17 @@ fn navigation_document_is_live<'s>( owner: v8::Local<'s, v8::Object>, ) -> bool { if let Some(popup_id) = crate::native_bridge::lightweight_popup_id_from_window(scope, owner) { - return context_host_ptr_from_global_bridge(scope) + return super::window_accessors::window_host_ptr(scope, owner) .is_some_and(|host_ptr| unsafe { &*host_ptr }.lightweight_popup_is_open(popup_id)); } if runtime_window_is_global(scope, owner) { - return true; + return super::window_accessors::window_host_ptr(scope, owner) + .is_some_and(|host_ptr| !unsafe { &*host_ptr }.browsing_context_is_closed()); } let Some(handle) = child_browsing_context_handle_for_runtime_owner(scope, owner) else { return false; }; - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::window_accessors::window_host_ptr(scope, owner) else { return false; }; let host = unsafe { &*host_ptr }; diff --git a/moli-renderer-v8/src/context_bootstrap/session_history.rs b/moli-renderer-v8/src/context_bootstrap/session_history.rs index 02820cbb71..fae7be6e8e 100644 --- a/moli-renderer-v8/src/context_bootstrap/session_history.rs +++ b/moli-renderer-v8/src/context_bootstrap/session_history.rs @@ -234,13 +234,15 @@ pub(super) fn length<'s>( scope: &mut v8::PinScope<'s, '_>, owner: v8::Local<'s, v8::Object>, ) -> usize { - if super::navigation_window::navigation_document_is_initial_empty(scope, owner) { - return 1; - } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return 1; }; let host = unsafe { &mut *host_ptr }; + if super::navigation_window::runtime_window_is_global(scope, owner) + && !host.main_document_has_committed_navigation() + { + return 0; + } let binding = binding(scope, host, owner); host.session_histories .get_mut(binding.popup) diff --git a/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs b/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs index 00499d6b8b..4feea848d7 100644 --- a/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs +++ b/moli-renderer-v8/src/context_bootstrap/session_history_traversal.rs @@ -92,17 +92,9 @@ pub(super) fn apply_step( if let Some(index) = root { let target = &plan.targets[index]; if runtime_window_is_global(scope, target.owner) { - let Some(delta) = host.session_histories.get_mut(binding.popup).delta_to(step) else { - return; - }; - if let Some(entry) = history_entries(scope, target.history) - .and_then(|entries| entries.get(target.target_index as usize).cloned()) - { - let key = entry.borrow().key.as_str().to_owned(); - host.top_level_navigation_history() - .select_joint_traversal(key, step); + if let Some(traversal) = cross_document_traversal(scope, host, target, step, method) { + apply_pending_cross_document_traversal(scope, host, traversal); } - host.record_pending_top_level_history_traversal(delta); return; } } @@ -130,38 +122,8 @@ pub(super) fn apply_step( host.retain_active_history_delta_position(binding.popup); let accepted = if let Some(index) = root { let target = &plan.targets[index]; - let pending = window_task_target_for_runtime_owner(scope, host, target.owner).zip( - history_entry_seed_for_traversal( - scope, - target.owner, - target.current_index, - target.target_index, - ), - ); - if let Some((exact, (_url, mut seed))) = pending { - seed.session_history.target_step = Some(step); - let key = history_entries(scope, target.history) - .and_then(|entries| entries.get(target.target_index as usize).cloned()) - .map(|entry| entry.borrow().key.as_str().to_owned()); - let (info, results) = method.map_or_else( - || (None, Vec::new()), - |method| (method.info, method.results), - ); - apply_pending_cross_document_traversal( - scope, - host, - PendingCrossDocumentTraversal { - target: exact, - target_index: target.target_index, - target_key: key, - seed, - info, - results, - }, - ) - } else { - false - } + cross_document_traversal(scope, host, target, step, method) + .is_some_and(|traversal| apply_pending_cross_document_traversal(scope, host, traversal)) } else { let history = window_history_for_holder(scope, owner).expect("admitted traversal has a history"); @@ -263,3 +225,35 @@ pub(crate) fn finish_without_document_commit( finish(scope, host_ptr, root, popup, step); } } + +fn cross_document_traversal<'s>( + scope: &mut v8::PinScope<'s, '_>, + host: &JsContextHost, + target: &super::navigation_traversal_execution::TraversalTarget<'s>, + step: SessionHistoryStepId, + method: Option, +) -> Option { + let exact = window_task_target_for_runtime_owner(scope, host, target.owner)?; + let (_, mut seed) = history_entry_seed_for_traversal( + scope, + target.owner, + target.current_index, + target.target_index, + )?; + seed.session_history.target_step = Some(step); + let key = history_entries(scope, target.history) + .and_then(|entries| entries.get(target.target_index as usize).cloned()) + .map(|entry| entry.borrow().key.as_str().to_owned()); + let (info, results) = method.map_or_else( + || (None, Vec::new()), + |method| (method.info, method.results), + ); + Some(PendingCrossDocumentTraversal { + target: exact, + target_index: target.target_index, + target_key: key, + seed, + info, + results, + }) +} diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors.rs index 26b1187e3f..9997c6d858 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors.rs @@ -41,7 +41,8 @@ pub(super) use interceptors::{ window_named_property_query, }; pub(crate) use surface::{ - window_parent_or_top, window_closed_getter, window_opener_getter, window_parent_getter, window_top_getter, + window_closed_getter, window_opener_getter, window_parent_getter, window_parent_or_top, + window_top_getter, }; pub(super) use surface::{ window_custom_elements_getter, window_device_pixel_ratio_getter, window_frames_getter, @@ -50,5 +51,4 @@ pub(super) use surface::{ window_outer_height_getter, window_outer_width_getter, window_performance_getter, window_screen_getter, window_scroll_x_getter, window_scroll_y_getter, window_self_getter, window_speech_synthesis_getter, window_visual_viewport_getter, window_window_getter, - }; diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs index c3f42de909..1edd589439 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/child_context.rs @@ -1,7 +1,7 @@ use super::helpers::{ - same_origin_window_receiver, - window_child_context_handle, window_document_handle, window_has_discarded_child_browsing_context, - window_hidden_value, window_host_ptr, window_is_closed, window_receiver, + same_origin_window_receiver, window_child_context_handle, window_document_handle, + window_has_discarded_child_browsing_context, window_hidden_value, window_host_ptr, + window_is_closed, window_receiver, }; use super::*; diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/document.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/document.rs index 85af20d8b7..f9d10b9275 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/document.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/document.rs @@ -5,8 +5,7 @@ use crate::{ util::{get_private_object, set_private_value}, }; -pub(in crate::context_bootstrap) const WINDOW_DOCUMENT_SLOT: &str = - "__moliWindowAssociatedDocument"; +pub(in crate::context_bootstrap) use super::super::shared::WINDOW_DOCUMENT_SLOT; pub(in crate::context_bootstrap) fn window_document_getter_template<'s>( scope: &mut v8::PinScope<'s, '_, ()>, @@ -19,9 +18,9 @@ pub(in crate::context_bootstrap) fn window_document_getter_template<'s>( None => v8::FunctionTemplate::new(scope, window_document_getter), }; template.remove_prototype(); - // V8 checks the receiver against the actual access Context before - // invoking the callback, including when the getter is borrowed. - template.set_accept_any_receiver(false); + // WindowProxy checks ordinary property reads before using the cache. + // Explicit getter calls must reach our receiver check in the callee's + // realm so a borrowed getter creates errors with the correct intrinsic. template } @@ -30,9 +29,7 @@ pub(crate) fn retain_window_document_in_retired_realm( ) -> Option<()> { let context = scope.get_current_context(); let host_ptr = crate::util::context_host_ptr_from_context_slot(context)?; - let _ = WindowEnvironmentSettings::bind_current_main_document_for_retirement(scope, unsafe { - &*host_ptr - }); + let _ = WindowEnvironmentSettings::bind_current_main_document(scope, unsafe { &*host_ptr }); let window = context.global(scope); if get_private_object(scope, window, WINDOW_DOCUMENT_SLOT).is_some() { return Some(()); @@ -94,6 +91,12 @@ pub(in crate::context_bootstrap) fn window_document_getter<'s>( rv.set(document.into()); return; } + if crate::native_bridge::lightweight_popup_id_from_window(scope, receiver).is_some() { + // Popup bootstrap may precede its Document projection. The opener's + // host Document is never this Window's associated Document. + rv.set_null(); + return; + } let handle = if let Some(settings) = WindowEnvironmentSettings::for_current_realm(scope) { settings.document_handle() } else if let Some(handle) = window_child_context_handle(scope, receiver) { diff --git a/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs b/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs index 321196c24e..fae57b7f14 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_accessors/surface.rs @@ -1,6 +1,6 @@ use super::helpers::{ - window_child_context_handle, window_has_discarded_child_browsing_context, window_hidden_value, window_host_ptr, window_is_closed, - window_receiver, + window_child_context_handle, window_has_discarded_child_browsing_context, window_hidden_value, + window_host_ptr, window_is_closed, window_receiver, }; use super::*; use crate::{ @@ -176,7 +176,6 @@ pub(crate) fn window_parent_or_top<'s>( } pub(crate) fn window_opener_getter<'s>( - scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, @@ -193,17 +192,23 @@ pub(crate) fn window_opener_getter<'s>( return; }; rv.set_null(); - if window_is_closed(scope, receiver) || window_has_discarded_child_browsing_context(scope, receiver) { + if window_is_closed(scope, receiver) + || window_has_discarded_child_browsing_context(scope, receiver) + { return; } if let Some(host_ptr) = window_host_ptr(scope, receiver) { let host = unsafe { &*host_ptr }; if let Some(popup_id) = lightweight_popup_id_from_window(scope, receiver) { - if let Some(opener) = host.lightweight_popup_opener_window(scope, popup_id) { rv.set(opener.into()); } + if let Some(opener) = host.lightweight_popup_opener_window(scope, popup_id) { + rv.set(opener.into()); + } return; } if let Some(handle) = window_child_context_handle(scope, receiver) { - if let Some(opener) = host.child_browsing_context_opener(scope, handle) { rv.set(opener.into()); } + if let Some(opener) = host.child_browsing_context_opener(scope, handle) { + rv.set(opener.into()); + } return; } } diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime.rs index f7bbe43995..8e2ac9370b 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime.rs @@ -16,8 +16,8 @@ pub(super) use base64::{ pub(super) use dialogs::entered_window_api_base_url; pub(super) use dialogs::{window_alert_callback, window_confirm_callback, window_prompt_callback}; pub(crate) use dialogs::{ - window_blur_callback, window_close_callback, window_const_false_callback, window_focus_callback, - window_noop_callback, window_open_callback, window_stop_callback, + window_blur_callback, window_close_callback, window_const_false_callback, + window_focus_callback, window_noop_callback, window_open_callback, window_stop_callback, }; pub(crate) use navigator::{ LegacyStorageQuotaCallbackOutcome, LegacyStorageQuotaCallbackTask, diff --git a/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs b/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs index daabf0577e..2cb203565e 100644 --- a/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs +++ b/moli-renderer-v8/src/context_bootstrap/window_runtime/dialogs.rs @@ -12,7 +12,6 @@ use crate::{ SpecialBrowsingContextTarget, navigate_existing_browsing_context_target, navigate_iframe_target, }, - entered_child_window_handle, }, runtime::{ RendererPendingJavaScriptDialog, RendererPendingPopupActivation, @@ -88,8 +87,15 @@ pub(crate) fn window_close_callback<'s>( }); if let Some(host_ptr) = receiver_context.and_then(context_host_ptr_from_context_slot) { let host = unsafe { &mut *host_ptr }; - if let Some(window) = host.auxiliary_window() { - host.request_auxiliary_window_close(window); + if let Some(window) = host.auxiliary_window() + && host.request_auxiliary_window_close(window) + && let Some(context) = receiver_context + { + // Closing has its own synchronous beforeunload check, including + // close() called from pagehide. The native close transition above + // rejects reentry; the queued close task owns final retirement. + let scope = &mut v8::ContextScope::new(scope, context); + host.dispatch_main_document_tree_beforeunload_for_close(scope); } } } @@ -142,9 +148,15 @@ pub(crate) fn window_stop_callback<'s>( if !crate::context_bootstrap::require_same_origin_window_receiver(scope, args.this(), false) { return; } - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = super::super::window_accessors::window_host_ptr(scope, args.this()) else { return; }; + let Some(context) = args.this().get_creation_context(scope) else { + return; + }; + // The receiver was authorized above. Its native binding belongs to its + // own host, including when stop() is borrowed from another Window. + let scope = &mut v8::ContextScope::new(scope, context); let receiver = match crate::native_bridge::WindowOperationReceiver::capture_and_authorize( scope, args.this(), @@ -462,7 +474,7 @@ pub(crate) fn window_open_callback<'s>( // The entered Window identifies the creator even when `noopener` removes // the opener relationship. Its child marker is more precise than the // ambient dispatch scope while a child callback is running. - let source_child_handle = window_open_receiver_child_handle(scope, entered_window); + let source_child_handle = window_receiver_child_handle(scope, entered_window); let popup_source_scope = source_child_handle .map(OwnerDispatchScope::Child) .unwrap_or(source_scope); @@ -480,7 +492,10 @@ pub(crate) fn window_open_callback<'s>( _ => crate::RendererPopupDisposition::Foreground, }; if host.has_browser_owned_auxiliary_page_factory() - && (!suppress_opener || url.as_deref().is_none_or(|url| Url::parse(url).is_ok_and(|url| moli_url::is_about_blank(&url)))) + && (!suppress_opener + || url + .as_deref() + .is_none_or(|url| Url::parse(url).is_ok_and(|url| moli_url::is_about_blank(&url)))) { let creator_child_handle = window_receiver_child_handle(scope, entered_window); match host.open_renderer_owned_auxiliary_window( @@ -496,6 +511,14 @@ pub(crate) fn window_open_callback<'s>( true, ) { Ok(opened) => { + if !parsed.raw_url.is_empty() && !opened.prepare_navigation_activation(scope) { + if suppress_opener { + rv.set_null(); + } else { + rv.set(opened.window_proxy.into()); + } + return; + } let disposition = if opened.pending_page.is_some() || host.protocol_user_gesture_activation() || host.current_input_event().is_some() @@ -552,7 +575,7 @@ pub(crate) fn window_open_callback<'s>( url.as_deref(), entered_base_url, creator_policy_container, - true, + false, ), _ => host.open_lightweight_popup_window( scope, @@ -582,7 +605,9 @@ pub(crate) fn window_open_callback<'s>( let popup_id = opened_popup.popup_id; if opened_popup.created_new_browsing_context { host.set_lightweight_popup_is_popup(popup_id, parsed_features.is_popup()); - } else if url.is_none() || !opened_popup.allows_navigation_activation(scope, host) { + } else if (url.is_none() && host.lightweight_popup_has_pending_document_load(popup_id)) + || !opened_popup.allows_navigation_activation(scope, host) + { if suppress_opener { rv.set(v8::null(scope).into()); } else { @@ -796,7 +821,8 @@ fn open_dialog( if !allows_modals { return None; } - let source_url = window_open_entered_document_url(scope, host).to_string(); + let source_url = + window_open_document_url(host, window_open_entry_scope(scope, host)).to_string(); let dialog_id = host.allocate_javascript_dialog_id(); host.open_modal_javascript_dialog( target, diff --git a/moli-renderer-v8/src/frame_owner_model/records.rs b/moli-renderer-v8/src/frame_owner_model/records.rs index b0b500508a..597d007636 100644 --- a/moli-renderer-v8/src/frame_owner_model/records.rs +++ b/moli-renderer-v8/src/frame_owner_model/records.rs @@ -926,7 +926,11 @@ impl DocumentLifecycleRecord { self.domcontentloaded_transition_token = None; self.complete_transition_token = None; self.readiness = Some(DocumentReadinessState::Complete); - self.load = DocumentLoadEventProgress::Ready; + self.load = if self.load_delivery_kind == DocumentLoadDeliveryKind::Main { + DocumentLoadEventProgress::Suppressed + } else { + DocumentLoadEventProgress::Ready + }; self.completely_loaded = true; true } @@ -1129,6 +1133,10 @@ impl DocumentLifecycleRecord { Some(finished) } + pub(super) fn unload_has_started(&self) -> bool { + self.unload != DocumentUnloadEventProgress::Pending + } + pub(super) fn begin_unload_dispatch(&mut self) -> bool { if !self.child_load_event_has_started() || self.unload != DocumentUnloadEventProgress::Pending diff --git a/moli-renderer-v8/src/frame_owner_model/store.rs b/moli-renderer-v8/src/frame_owner_model/store.rs index 0b2d44c6bc..747655949e 100644 --- a/moli-renderer-v8/src/frame_owner_model/store.rs +++ b/moli-renderer-v8/src/frame_owner_model/store.rs @@ -179,6 +179,38 @@ impl FrameOwnerStore { realm_id } + pub(crate) fn mark_main_document_initial_empty(&mut self) { + let owner = self + .current_main_owner_snapshot() + .expect("an initial main Document must have an installed owner"); + let document = self + .documents + .get_mut(&owner.document_id) + .expect("the current main Document must retain its owner record"); + document.creation_kind = DocumentCreationKind::InitialEmpty; + document.has_committed_navigation = false; + } + + pub(crate) fn main_document_has_committed_navigation(&self) -> bool { + self.current_main_owner_snapshot() + .and_then(|owner| self.documents.get(&owner.document_id)) + .is_some_and(|document| document.has_committed_navigation) + } + + pub(crate) fn complete_current_main_initial_empty_document(&mut self) -> bool { + let Some(owner) = self.current_main_document_task_owner() else { + return false; + }; + self.documents + .get_mut(&owner.document_id) + .filter(|document| moli_url::is_about_blank(&document.url)) + .is_some_and(|document| { + document + .lifecycle_progress + .complete_initial_empty_document() + }) + } + pub(crate) fn ensure_child_frame( &mut self, child_handle: DomHandle, @@ -397,25 +429,27 @@ impl FrameOwnerStore { .lifecycle_progress .document_open_load_continuation(); let completely_loaded = retired_document.lifecycle_progress.is_completely_loaded(); - let has_committed_navigation = if matches!(creation_kind, DocumentCreationKind::DocumentOpen { .. }) { - retired_document.has_committed_navigation - } else { - !creation_kind.is_initial_empty() - }; + let has_committed_navigation = + if matches!(creation_kind, DocumentCreationKind::DocumentOpen { .. }) { + retired_document.has_committed_navigation + } else { + !creation_kind.is_initial_empty() + }; retired_document.lifecycle = DocumentLifecycleState::Replaced; retired_document.lifecycle_progress.retire(); retired_document.active_requests.clear(); let document_id = transition.current_owner().document_id; - let lifecycle_progress = if matches!(creation_kind, DocumentCreationKind::DocumentOpen { .. }) { - self.new_loading_document_lifecycle_for_document_open( - DocumentLoadDeliveryKind::Main, - load_continuation, - completely_loaded, - ) - } else { - self.new_loading_document_lifecycle(DocumentLoadDeliveryKind::Main) - }; + let lifecycle_progress = + if matches!(creation_kind, DocumentCreationKind::DocumentOpen { .. }) { + self.new_loading_document_lifecycle_for_document_open( + DocumentLoadDeliveryKind::Main, + load_continuation, + completely_loaded, + ) + } else { + self.new_loading_document_lifecycle(DocumentLoadDeliveryKind::Main) + }; self.documents.insert( document_id, DocumentRecord { @@ -2085,6 +2119,12 @@ impl FrameOwnerStore { }) } + pub(crate) fn current_main_document_unload_has_started(&self) -> bool { + self.current_main_document_task_owner() + .and_then(|owner| self.documents.get(&owner.document_id)) + .is_some_and(|document| document.lifecycle_progress.unload_has_started()) + } + pub(crate) fn begin_current_main_document_unload(&mut self) -> Option { let owner = self.current_main_document_task_owner()?; self.documents diff --git a/moli-renderer-v8/src/frame_owner_model/store_tests.rs b/moli-renderer-v8/src/frame_owner_model/store_tests.rs index 91119a9c75..6cf017525d 100644 --- a/moli-renderer-v8/src/frame_owner_model/store_tests.rs +++ b/moli-renderer-v8/src/frame_owner_model/store_tests.rs @@ -381,6 +381,53 @@ fn initial_main_document_commit_prepares_owners_before_replacement_without_turn_ ); } +#[test] +fn main_initial_document_stays_provisional_through_open_until_navigation_commits() { + let mut store = FrameOwnerStore::default(); + store.ensure_main_frame( + handle(1), + url("about:blank"), + url("https://creator.test/"), + "https://creator.test".to_owned(), + policy_container(), + policy_context(), + None, + ); + store.mark_main_document_initial_empty(); + assert!(!store.main_document_has_committed_navigation()); + assert!(store.complete_current_main_initial_empty_document()); + assert!(!store.complete_current_main_initial_empty_document()); + let initial = store.current_main_document_task_owner().unwrap(); + assert_eq!( + store.current_document_is_completely_loaded(initial.document_owner()), + Some(true) + ); + assert_eq!( + store.current_main_document_complete_transition_is_ready(initial), + Some(false) + ); + store + .replace_main_document( + handle(1), + url("https://creator.test/written"), + url("https://creator.test/written"), + ) + .unwrap(); + assert!(!store.main_document_has_committed_navigation()); + let transition = store + .prepare_initial_main_document_owner_transition() + .unwrap(); + store + .replace_initial_main_document( + transition, + handle(2), + url("https://creator.test/committed"), + url("https://creator.test/committed"), + ) + .unwrap(); + assert!(store.main_document_has_committed_navigation()); +} + #[test] fn main_document_replacement_rotates_document_owner_without_replacing_window_or_realm() { let mut store = FrameOwnerStore::default(); diff --git a/moli-renderer-v8/src/lib.rs b/moli-renderer-v8/src/lib.rs index cd6d31831f..7d5d617dd8 100644 --- a/moli-renderer-v8/src/lib.rs +++ b/moli-renderer-v8/src/lib.rs @@ -232,10 +232,9 @@ pub use runtime::{ CompletedWorkerRuntimeInspectorCommandDispatch, DetachedParserScriptFetchContinuation, DevToolsSessionKey, ExternalRawDocumentBodyStream, JsRuntime, JsRuntimeOwner, PageId, PendingHtmlPage, PendingWorkerRuntimeInspectorSessionResponse, PreparedRendererDocument, - RendererAboutDocumentState, - RendererAccessibilityPayloadsForObjectId, RendererActivityDiagnostics, - RendererAgentAttachmentId, RendererAutofillAddressField, RendererAutofillCreditCard, - RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, + RendererAboutDocumentState, RendererAccessibilityPayloadsForObjectId, + RendererActivityDiagnostics, RendererAgentAttachmentId, RendererAutofillAddressField, + RendererAutofillCreditCard, RendererAutofillTriggerOutcome, RendererAutofillTriggerRequest, RendererAuxiliaryDocumentResponse, RendererAuxiliaryNavigationKind, RendererAuxiliaryWindow, RendererBrowserContextRuntime, RendererBrowserContextRuntimeId, RendererBrowserContextRuntimeOwner, RendererBrowserContextRuntimeOwnerAccess, @@ -286,11 +285,12 @@ pub use runtime::{ RendererNativeCommandReadyResponse, RendererNativeCommandResponseGuard, RendererNativeCommandTerminal, RendererNativeOperation, RendererNativeOperationStep, RendererNativeProtocolError, RendererNativeProtocolNotification, - RendererNativeProtocolResponse, RendererNativeProtocolStateUpdate, RendererNavigationInitiator, RendererNavigationHistory, RendererNavigationHistoryRequest, - RendererOutputCursor, RendererOutputFence, RendererOutputFenceLeaseId, RendererOutputItem, - RendererOutputPublication, RendererOutputPublicationOrdering, RendererOutputRecord, - RendererOutputResidenceIdentity, RendererOutputStreamCloseReason, RendererOutputStreamControl, - RendererOutputStreamEpoch, RendererOutputStreamIdentity, RendererOutputTransportDiagnostics, + RendererNativeProtocolResponse, RendererNativeProtocolStateUpdate, RendererNavigationHistory, + RendererNavigationHistoryRequest, RendererNavigationInitiator, RendererOutputCursor, + RendererOutputFence, RendererOutputFenceLeaseId, RendererOutputItem, RendererOutputPublication, + RendererOutputPublicationOrdering, RendererOutputRecord, RendererOutputResidenceIdentity, + RendererOutputStreamCloseReason, RendererOutputStreamControl, RendererOutputStreamEpoch, + RendererOutputStreamIdentity, RendererOutputTransportDiagnostics, RendererOutputTransportMessage, RendererOutputTransportReceiver, RendererOutputTransportSendError, RendererOutputTransportSender, RendererOwnerAction, RendererOwnerCommand, RendererOwnerHandle, RendererOwnerLocalHostId, RendererOwnerReply, diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs index 301ca8d828..fc0172a719 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_documents/lifecycle.rs @@ -1229,6 +1229,7 @@ impl JsContextHost { host_ptr: *mut Self, document_handle: DomHandle, ) { + let _unload = unsafe { &*host_ptr }.enter_document_unload(document_handle); let handles = unsafe { &*host_ptr }.child_browsing_context_handles_in_document_order(); for handle in handles { if unsafe { &*host_ptr } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs index f895dd3be7..7a1a2f72f1 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/isolated_world.rs @@ -151,7 +151,7 @@ impl JsContextHost { self.retire_window_realm_resources(stale.runtime_observable_context_token); self.retire_window_execution_contexts_for_context_token( stale.runtime_observable_context_token, - config.resource_owner_id, + Some(config.resource_owner_id), ); if self.child_window_proxy_frame_is_current(handle, &stale.frame_id) { stale_context.detach_global(); diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs index 431bc5b33b..b1f9e3557c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window.rs @@ -65,6 +65,7 @@ impl CrossOriginWindowProperty { /// on the Window or reading its private slots from the accessing realm. pub(crate) struct CrossOriginWindowAccessor<'s> { surface: v8::Local<'s, v8::Object>, + receiver: v8::Local<'s, v8::Object>, } impl<'s> CrossOriginWindowAccessor<'s> { @@ -110,26 +111,35 @@ impl<'s> CrossOriginWindowAccessor<'s> { receiver: v8::Local<'s, v8::Object>, ) -> Option { let context = receiver.get_creation_context(scope)?; + let accessing_context = cross_origin_accessing_context(scope); if !receiver.strict_equals(context.global(scope).into()) && crate::native_bridge::lightweight_popup_id_from_window(scope, receiver).is_some() { - let accessing_context = scope.get_current_context(); if synthetic_window_context_can_access(scope, accessing_context, receiver) { return None; } - return window_cross_origin_access_surface(scope, receiver) - .map(|surface| Self { surface }); + return window_cross_origin_backing_surface(scope, receiver) + .map(|surface| Self { surface, receiver }); } if !receiver.strict_equals(context.global(scope).into()) - || super::super::window_contexts_allow_access(scope.get_current_context(), context) + || contexts_can_script_access(scope, accessing_context, context) { return None; } // Only native Window realms install this context slot. A copied bridge // property, inherited Window prototype, or author Proxy cannot supply it. + if context + .get_slot::() + .is_some() + { + return Some(Self { + surface: context.get_extras_binding_object(scope), + receiver, + }); + } context.get_slot::()?; - let surface = window_cross_origin_access_surface(scope, receiver)?; - Some(Self { surface }) + let surface = window_cross_origin_backing_surface(scope, receiver)?; + Some(Self { surface, receiver }) } pub(crate) fn get( @@ -137,6 +147,14 @@ impl<'s> CrossOriginWindowAccessor<'s> { scope: &mut v8::PinScope<'s, '_>, property: CrossOriginWindowProperty, ) -> Option> { + if self + .receiver + .get_creation_context(scope)? + .get_slot::() + .is_some() + { + return main_page::window_property_value(scope, self.receiver, property); + } self.surface .get(scope, v8str(scope, property.name()).into()) } @@ -220,6 +238,23 @@ impl ChildWindowProxyRecords { .map(|window| v8::Local::new(scope, window)) } + fn cross_origin_handler_data<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + handle: DomHandle, + ) -> Option<(v8::Local<'s, v8::Object>, v8::Local<'s, v8::Object>)> { + let window = self.live_window(scope, handle)?; + let context = window.get_creation_context(scope)?; + let target_scope = &mut v8::ContextScope::new(scope, context); + let surface = get_private_value( + target_scope, + window, + CROSS_ORIGIN_WINDOW_ACCESS_SURFACE_SLOT, + ) + .and_then(|value| v8::Local::::try_from(value).ok())?; + Some((surface, window)) + } + pub(in crate::native_bridge::context_host) fn has_live_window( &self, handle: DomHandle, @@ -880,6 +915,8 @@ struct CrossOriginLocationProxyHandlerDeclaration { set: (), #[webapi(method, length = 2, callback = cross_origin_location_proxy_descriptor_callback)] get_own_property_descriptor: (), + #[webapi(method, length = 1, callback = cross_origin_location_proxy_own_keys_callback)] + own_keys: (), #[webapi(method, length = 2, callback = cross_origin_window_denied_callback)] delete_property: (), #[webapi(method, length = 3, callback = cross_origin_window_denied_callback)] @@ -1502,6 +1539,18 @@ impl JsContextHost { &self, scope: &mut v8::PinScope<'s, '_>, ) -> Option> { + if self.auxiliary_window().is_some() + && let Some(environment) = self.page_script_environment() + { + // Named reuse changes the related Page's opener synchronously. + // Its script environment also records an explicitly disowned + // opener; an older endpoint must not restore that relationship. + let opener = environment.opener_in_scope(scope)?; + let host = opener + .get_creation_context(scope) + .and_then(crate::util::context_host_ptr_from_context_slot)?; + return (!unsafe { &*host }.browsing_context_is_closed()).then_some(opener); + } let (endpoint, opener) = self.top_window_opener.as_ref()?; let opener = v8::Local::new(scope, opener); self.window_opener_endpoint_is_live(scope, *endpoint, opener) @@ -1559,6 +1608,15 @@ impl JsContextHost { } } + pub(crate) fn child_browsing_context_top_window_for_current_realm<'s>( + &self, + scope: &mut v8::PinScope<'s, '_>, + handle: DomHandle, + ) -> Option> { + self.child_window_proxy_records + .browsing_context_top(scope, handle) + } + pub(in crate::native_bridge::context_host) fn child_browsing_context_parent_window<'s>( &mut self, scope: &mut v8::PinScope<'s, '_>, @@ -2361,6 +2419,7 @@ fn wrap_cross_origin_location_proxy<'s>( get: (), set: (), get_own_property_descriptor: (), + own_keys: (), delete_property: (), define_property: (), } @@ -2518,6 +2577,11 @@ fn window_cross_origin_access_surface<'s>( scope: &mut v8::PinScope<'s, '_>, holder: v8::Local<'s, v8::Object>, ) -> Option> { + if let Some((surface, _)) = child_window_cross_origin_handler_data(scope, holder) + && main_page::is_main_surface(scope, surface) + { + return Some(surface); + } if crate::native_bridge::lightweight_popup_id_from_window(scope, holder).is_some() { let surface = get_private_value(scope, holder, CROSS_ORIGIN_WINDOW_ACCESS_SURFACE_SLOT) .and_then(|value| v8::Local::::try_from(value).ok())?; @@ -2555,6 +2619,23 @@ fn window_cross_origin_access_surface<'s>( } } +// Native getter projection reads the target's backing surface. Public +// descriptors use the caller surface and must validate their actual receiver. +fn window_cross_origin_backing_surface<'s>( + scope: &mut v8::PinScope<'s, '_>, + holder: v8::Local<'s, v8::Object>, +) -> Option> { + let context = holder.get_creation_context(scope)?; + let scope = &mut v8::ContextScope::new(scope, context); + get_private_value(scope, holder, CROSS_ORIGIN_WINDOW_ACCESS_SURFACE_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) + .or_else(|| { + context + .get_slot::() + .and_then(|surface| surface.0.to_local(scope)) + }) +} + fn window_cross_origin_caller_surface<'s>( scope: &mut v8::PinScope<'s, '_>, storage: v8::Local<'s, v8::Object>, @@ -2635,7 +2716,9 @@ fn child_window_cross_origin_handler_data<'s>( if let Some(identity) = host.window_execution_context_identity_for_access_check(holder_context) && host.window_execution_context_identity_is_current(identity) { - if !host.window_execution_context_identity_is_default_world(identity) { + let (_, default_context) = + host.window_execution_context(scope, identity.owner(), identity.dispatch_scope())?; + if default_context != holder_context { return None; } if identity.dispatch_scope() == super::super::OwnerDispatchScope::Top { @@ -2759,7 +2842,7 @@ fn child_window_cross_origin_named_enumerator<'s>( property_names.index_filter(v8::IndexFilter::SkipIndices); property_names.key_conversion(v8::KeyConversionMode::ConvertToString); let names = window_cross_origin_access_surface(scope, callback_args.holder()) - .and_then(|surface| surface.get_own_property_names(scope, property_names.build())) + .and_then(|surface| surface.get_property_names(scope, property_names.build())) .unwrap_or_else(|| v8::Array::new(scope, 0)); rv.set(names); } @@ -2813,7 +2896,16 @@ fn child_window_cross_origin_named_child_value<'s>( if cross_origin_window_has_discarded_browsing_context(scope, surface) { return None; } - let parent = if let Some(handle) = child_handle_from_object(scope, surface) { + let main_host = main_page::is_main_surface(scope, surface) + .then(|| { + let window = get_private_value(scope, surface, CROSS_ORIGIN_WINDOW_SELF_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok())?; + crate::util::context_host_ptr_from_context_slot(window.get_creation_context(scope)?) + }) + .flatten(); + let parent = if main_host.is_some() { + super::super::OwnerDispatchScope::Top + } else if let Some(handle) = child_handle_from_object(scope, surface) { super::super::OwnerDispatchScope::Child(handle) } else if let Some(id) = cross_origin_lightweight_popup_id(scope, surface) { super::super::OwnerDispatchScope::LightweightPopup(id) @@ -2822,7 +2914,10 @@ fn child_window_cross_origin_named_child_value<'s>( } else { return None; }; - let host_ptr = context_host_ptr_from_global_bridge(scope)?; + let host_ptr = main_host.or_else(|| context_host_ptr_from_global_bridge(scope))?; + if unsafe { &*host_ptr }.browsing_context_is_closed() { + return None; + } let child_handle = unsafe { &*host_ptr } .child_browsing_context_named_child_handle_for_scope(parent, &key_name)?; unsafe { &mut *host_ptr } @@ -2838,7 +2933,9 @@ fn child_window_cross_origin_indexed_getter<'s>( ) -> v8::Intercepted { if let Some(surface) = window_cross_origin_access_surface(scope, args.holder()) && main_page::is_main_surface(scope, surface) - && !surface.has_own_property(scope, v8_string(scope, &index.to_string()).unwrap().into()).unwrap_or(false) + && !surface + .has_own_property(scope, v8_string(scope, &index.to_string()).unwrap().into()) + .unwrap_or(false) { return v8::Intercepted::kNo; } @@ -2923,9 +3020,7 @@ fn child_window_cross_origin_indexed_enumerator<'s>( ) { let holder = args.holder(); let count = window_cross_origin_access_surface(scope, holder) - .filter(|surface| { - !cross_origin_window_has_discarded_browsing_context(scope, *surface) - }) + .filter(|surface| !cross_origin_window_has_discarded_browsing_context(scope, *surface)) .and_then(|surface| { cross_origin_surface_get(scope, holder, surface, v8str(scope, "length").into()) }) @@ -3052,7 +3147,10 @@ fn cross_origin_location_proxy_get_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - if is_cross_origin_location_href_key_value(scope, args.get(1)) { + if !v8::Local::::try_from(args.get(1)) + .is_ok_and(|key| cross_origin_location_key_is_allowed(scope, key)) + || is_cross_origin_location_href_key_value(scope, args.get(1)) + { throw_cross_origin_location_security_error(scope); return; } @@ -3061,10 +3159,9 @@ fn cross_origin_location_proxy_get_callback<'s>( return; }; let receiver = v8::Local::::try_from(args.get(2)).unwrap_or(target); - let surface = main_page::location_access_surface(scope, target).unwrap_or(target); let accessing_context = cross_origin_accessing_context(scope); let scope = &mut v8::ContextScope::new(scope, accessing_context); - match surface.get_with_receiver(scope, args.get(1), receiver) { + match target.get_with_receiver(scope, args.get(1), receiver) { Some(value) => rv.set(value), None => rv.set_undefined(), } @@ -3081,27 +3178,50 @@ fn cross_origin_location_proxy_descriptor_callback<'s>( let Ok(key) = v8::Local::::try_from(args.get(1)) else { return; }; - let main_surface = main_page::location_access_surface(scope, target); - let surface = main_surface.unwrap_or(target); + if !cross_origin_location_key_is_allowed(scope, key) { + throw_cross_origin_location_security_error(scope); + return; + } let accessing_context = cross_origin_accessing_context(scope); let scope = &mut v8::ContextScope::new(scope, accessing_context); - let Some(descriptor) = surface.get_own_property_descriptor(scope, key) else { + let Some(descriptor) = target.get_own_property_descriptor(scope, key) else { return; }; - if main_surface.is_some() - && let Ok(descriptor) = v8::Local::::try_from(descriptor) - { - // Main Location's target holds placeholders. Its descriptors are - // projected into each accessor realm and must remain configurable. - let _ = descriptor.set( - scope, - v8str(scope, "configurable").into(), - v8::Boolean::new(scope, true).into(), - ); - } rv.set(descriptor); } +fn cross_origin_location_key_is_allowed<'s>( + scope: &mut v8::PinScope<'s, '_>, + key: v8::Local<'s, v8::Name>, +) -> bool { + ["href", "replace", "then"] + .into_iter() + .any(|name| key == v8str(scope, name)) + || [ + v8::Symbol::get_to_string_tag(scope), + v8::Symbol::get_has_instance(scope), + v8::Symbol::get_is_concat_spreadable(scope), + ] + .into_iter() + .any(|symbol| key == symbol) +} + +fn cross_origin_location_proxy_own_keys_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + _args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let keys = [ + v8str(scope, "href").into(), + v8str(scope, "replace").into(), + v8str(scope, "then").into(), + v8::Symbol::get_to_string_tag(scope).into(), + v8::Symbol::get_has_instance(scope).into(), + v8::Symbol::get_is_concat_spreadable(scope).into(), + ]; + rv.set(v8::Array::new_with_elements(scope, &keys).into()); +} + fn cross_origin_location_proxy_set_receiver<'s>( scope: &mut v8::PinScope<'s, '_>, target: v8::Local<'s, v8::Object>, @@ -3126,16 +3246,7 @@ fn cross_origin_location_proxy_set_callback<'s>( return; }; if !is_cross_origin_location_href_key_value(scope, args.get(1)) { - let receiver = v8::Local::::try_from(args.get(3)).unwrap_or(target); - rv.set( - v8::Boolean::new( - scope, - target - .set_with_receiver(scope, args.get(1), args.get(2), receiver) - .unwrap_or(false), - ) - .into(), - ); + throw_cross_origin_location_security_error(scope); return; } let Some(receiver) = cross_origin_location_proxy_set_receiver(scope, target, args.get(3)) @@ -3255,7 +3366,11 @@ fn cross_origin_window_has_discarded_browsing_context<'s>( get_cross_origin_proxy_private_value(scope, window, CROSS_ORIGIN_WINDOW_SELF_SLOT) .and_then(|value| v8::Local::::try_from(value).ok()) .unwrap_or(window); - let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { + let Some(host_ptr) = identity + .get_creation_context(scope) + .and_then(crate::util::context_host_ptr_from_context_slot) + .or_else(|| context_host_ptr_from_global_bridge(scope)) + else { return true; }; // DOM handles survive removal/reinsertion, and a WindowProxy survives @@ -3463,14 +3578,6 @@ fn cross_origin_location_navigate<'s>( receiver: v8::Local<'s, v8::Object>, value: v8::Local<'s, v8::Value>, ) -> bool { - if main_page::is_main_location(scope, receiver) { - return main_page::navigate( - scope, - receiver, - value, - crate::context_bootstrap::LocationNavigationKind::Assign, - ); - } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return false; }; @@ -3547,22 +3654,6 @@ fn cross_origin_location_replace_callback<'s>( ); return; } - if main_page::is_main_location(scope, args.this()) { - let Some(parsed) = webidl::parse_args::(scope, &args) - else { - return; - }; - let Some(value) = v8_string(scope, &parsed.url) else { - return; - }; - main_page::navigate( - scope, - args.this(), - value.into(), - crate::context_bootstrap::LocationNavigationKind::Replace, - ); - return; - } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { return; }; diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs index 75e5cbef93..140c25d2ce 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frame_runtime/window/main_page.rs @@ -4,12 +4,49 @@ use crate::context_bootstrap::{ window_opener_getter, window_parent_getter, window_top_getter, }; -const LOCATION: &str = "__moliMainWindowCrossOriginLocationObject"; -const LOCATION_SURFACE: &str = "__moliMainWindowCrossOriginLocationSurface"; -const MAIN_LOCATION: &str = "__moliMainWindowCrossOriginLocation"; const INDEXED_COUNT: &str = "__moliMainWindowCrossOriginIndexedCount"; const WINDOW_LOCATION_SLOT: &str = "__moliWindowLocation"; +pub(super) fn window_property_value<'s>( + scope: &mut v8::PinScope<'s, '_>, + window: v8::Local<'s, v8::Object>, + property: CrossOriginWindowProperty, +) -> Option> { + if matches!(property, CrossOriginWindowProperty::Location) { + return location_for_window(scope, window).map(Into::into); + } + let context = window.get_creation_context(scope)?; + let host = crate::util::context_host_ptr_from_context_slot(context); + let closed = host.is_none_or(|host| unsafe { &*host }.browsing_context_is_closed()); + Some(match property { + CrossOriginWindowProperty::Closed => v8::Boolean::new(scope, closed).into(), + CrossOriginWindowProperty::Length => { + let length = if closed { + 0 + } else { + unsafe { &*host? }.child_browsing_context_count() + }; + v8::Integer::new_from_unsigned(scope, length as u32).into() + } + CrossOriginWindowProperty::Opener => { + if closed { + v8::null(scope).into() + } else { + unsafe { &*host? } + .top_window_opener(scope) + .map_or_else(|| v8::null(scope).into(), Into::into) + } + } + CrossOriginWindowProperty::Parent | CrossOriginWindowProperty::Top if closed => { + v8::null(scope).into() + } + CrossOriginWindowProperty::Parent + | CrossOriginWindowProperty::Top + | CrossOriginWindowProperty::Window => window.into(), + CrossOriginWindowProperty::Location => unreachable!(), + }) +} + #[derive(WebApiObject)] #[webapi(plain)] struct MainWindowCrossOriginSurface { @@ -51,51 +88,12 @@ fn cached_access_surface<'s>( let accessing_context = cross_origin_accessing_context(scope); cached_cross_origin_window_surface(scope, target_context, accessing_context, |scope| { let window = target_context.global(scope); - let location = shared_cross_origin_location(scope, window) + let location = location_for_window(scope, window) .expect("main Window cross-origin Location should initialize"); build_accessing_surface(scope, window, location) }) } -fn ensure_shared_location<'s>( - scope: &mut v8::PinScope<'s, '_>, - anchor: v8::Local<'s, v8::Object>, - window: v8::Local<'s, v8::Object>, -) -> v8::Local<'s, v8::Object> { - if let Some(location) = get_private_value(scope, anchor, LOCATION) - .and_then(|value| v8::Local::::try_from(value).ok()) - { - return location; - } - let target = new_null_prototype_object(scope); - let marker = v8::Boolean::new(scope, true).into(); - set_private_value(scope, target, CROSS_ORIGIN_LOCATION_PROXY_SLOT, marker); - set_private_value(scope, target, MAIN_LOCATION, window.into()); - // Preserve the target's keys without exposing realm-specific - // functions. Configurable placeholders allow each observer's - // get/descriptor projection to obey Proxy invariants. - let undefined = v8::undefined(scope).into(); - for name in CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES - .iter() - .copied() - .chain(["href", "replace", "then"]) - { - let key = v8str(scope, name).into(); - let _ = target.define_own_property(scope, key, undefined, v8::PropertyAttribute::DONT_ENUM); - } - install_cross_origin_symbol_slots(scope, target, "Location"); - let location = wrap_cross_origin_location_proxy(scope, target) - .expect("main Window cross-origin Location proxy should initialize"); - set_private_value( - scope, - target, - CROSS_ORIGIN_LOCATION_PROXY_SELF_SLOT, - location.into(), - ); - set_private_value(scope, anchor, LOCATION, location.into()); - location -} - fn build_accessing_surface<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, @@ -127,49 +125,12 @@ fn build_accessing_surface<'s>( CROSS_ORIGIN_WINDOW_LOCATION_SLOT, location.into(), ); - install_cross_origin_symbol_slots(scope, surface, "Window"); + set_private_value(scope, surface, CROSS_ORIGIN_WINDOW_SELF_SLOT, window.into()); + install_cross_origin_symbol_slots(scope, surface); - let location_surface = new_null_prototype_object(scope); - install_cross_origin_symbol_slots(scope, location_surface, "Location"); - install_cross_origin_denied_accessors( - scope, - location_surface, - CROSS_ORIGIN_LOCATION_DENIED_PROPERTIES, - ); - let setter = v8::Function::builder(cross_origin_location_navigate_setter_callback) - .length(1) - .constructor_behavior(v8::ConstructorBehavior::Throw) - .build(scope) - .expect("cross-origin Location href setter should initialize"); - setter.set_name(v8str(scope, "set href")); - let href = v8str(scope, "href").into(); - define_get_set_property( - scope, - location_surface, - href, - undefined, - setter.into(), - cross_origin_property_attributes(), - "href", - ) - .expect("cross-origin Location href descriptor should initialize"); - install_cross_origin_location_methods(scope, location_surface); - set_cross_origin_object_slot(scope, location_surface, "then", undefined); - set_private_value(scope, surface, LOCATION_SURFACE, location_surface.into()); surface } -pub(super) fn location_access_surface<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, -) -> Option> { - let window = main_location_window(scope, receiver)?; - let context = window.get_creation_context(scope)?; - let surface = cached_access_surface(scope, context); - get_private_value(scope, surface, LOCATION_SURFACE) - .and_then(|value| v8::Local::::try_from(value).ok()) -} - pub(super) fn is_main_surface<'s>( scope: &mut v8::PinScope<'s, '_>, surface: v8::Local<'s, v8::Object>, @@ -193,9 +154,11 @@ fn refresh_indexed_children<'s>( host.sync_child_browsing_context_subtree(scope, host.document_handle()); let count = host.child_browsing_context_count() as u32; for index in 0..count { - if let Some(handle) = host.child_browsing_context_handle_by_index(index as usize) - && let Some(window) = - host.child_browsing_context_window_proxy_for_top(scope, handle) + if let Some(handle) = host.child_browsing_context_handle_by_index_for_document( + host.document_handle(), + index as usize, + ) && let Some(window) = + host.child_browsing_context_window_proxy_for_current_realm(scope, handle) { let key = v8_string(scope, &index.to_string()).unwrap(); let _ = surface.define_own_property( @@ -215,39 +178,6 @@ fn refresh_indexed_children<'s>( set_private_value(scope, surface, INDEXED_COUNT, count); } -fn main_location_window<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, -) -> Option> { - get_cross_origin_proxy_private_value(scope, receiver, MAIN_LOCATION) - .and_then(|value| v8::Local::::try_from(value).ok()) -} - -pub(super) fn is_main_location<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, -) -> bool { - main_location_window(scope, receiver).is_some() -} - -pub(super) fn navigate<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, - value: v8::Local<'s, v8::Value>, - kind: crate::context_bootstrap::LocationNavigationKind, -) -> bool { - let Some(window) = main_location_window(scope, receiver) else { - return false; - }; - // The stable WindowProxy follows its successor, but a Location belongs to - // the Window realm that created it. An old Location must not gain the new - // Document's navigation authority through that proxy. - if receiver.get_creation_context(scope) != window.get_creation_context(scope) { - return false; - } - navigate_window(scope, window, value, kind, true) -} - fn navigate_window<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, @@ -270,19 +200,29 @@ fn navigate_window<'s>( throw_cross_origin_location_security_error(scope); return false; } - let raw = if kind != crate::context_bootstrap::LocationNavigationKind::Reload { + let Some(context) = window.get_creation_context(scope) else { + return false; + }; + let accessing_context = cross_origin_accessing_context(scope); + let raw = if context != accessing_context + && kind != crate::context_bootstrap::LocationNavigationKind::Reload + { crate::context_bootstrap::resolve_cross_window_location_target(scope, &raw).unwrap_or(raw) } else { raw }; - let Some(context) = window.get_creation_context(scope) else { + if let Ok(destination) = url::Url::parse(&raw) + && crate::context_bootstrap::blocks_ancestor_location_navigation( + scope, + window, + &destination, + ) + { + throw_cross_origin_location_security_error(scope); return false; - }; + } let scope = &mut v8::ContextScope::new(scope, context); - let Some(location) = window - .get(scope, v8str(scope, "location").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) - else { + let Some(location) = crate::context_bootstrap::window_location_for_holder(scope, window) else { return false; }; crate::context_bootstrap::navigate_location_object_with_child_navigate_event( @@ -316,15 +256,14 @@ fn require_window_receiver<'s>( None } -fn shared_cross_origin_location<'s>( +pub(super) fn location_for_window<'s>( scope: &mut v8::PinScope<'s, '_>, window: v8::Local<'s, v8::Object>, ) -> Option> { let context = window.get_creation_context(scope)?; let scope = &mut v8::ContextScope::new(scope, context); - let anchor = context.get_extras_binding_object(scope); - let window = context.global(scope); - Some(ensure_shared_location(scope, anchor, window)) + get_private_value(scope, window, WINDOW_LOCATION_SLOT) + .and_then(|value| v8::Local::::try_from(value).ok()) } fn location_getter<'s>( @@ -335,18 +274,7 @@ fn location_getter<'s>( let Some(window) = require_window_receiver(scope, &args, "get", "location") else { return; }; - if caller_can_access_window(scope, window) { - if let Some(location) = get_private_value(scope, window, WINDOW_LOCATION_SLOT) { - rv.set(location); - return; - } - throw_type_error( - scope, - "Failed to get the 'location' property on 'Window': Illegal invocation", - ); - return; - } - if let Some(location) = shared_cross_origin_location(scope, window) { + if let Some(location) = location_for_window(scope, window) { rv.set(location.into()); } } diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/discovery.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/discovery.rs index b7462e739e..df84b2f8d8 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/discovery.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/discovery.rs @@ -215,6 +215,13 @@ impl JsContextHost { } pub(crate) fn child_browsing_context_host_is_active(&self, handle: DomHandle) -> bool { + if self + .dom_host() + .owner_document_handle(handle) + .is_none_or(|document| !self.document_has_browsing_context(document)) + { + return false; + } if self .dom_host() .owner_document_handle(handle) diff --git a/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs b/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs index 504966119f..55e4c38694 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/child_frames/lookup.rs @@ -419,13 +419,16 @@ impl JsContextHost { } }; let root_name_matches = match root { - OwnerDispatchScope::Top => !self.browsing_context_is_closed() - && self.browsing_context_name().get() == name, - OwnerDispatchScope::LightweightPopup(id) => self.lightweight_popup_record(id) + OwnerDispatchScope::Top => { + !self.browsing_context_is_closed() && self.browsing_context_name().get() == name + } + OwnerDispatchScope::LightweightPopup(id) => self + .lightweight_popup_record(id) .is_some_and(|record| record.is_open() && record.name.get() == name), OwnerDispatchScope::Child(_) => false, }; - if root_window.is_some() && root_name_matches + if root_window.is_some() + && root_name_matches && related_to.is_none_or(|source| self.browsing_contexts_are_familiar(source, root)) { return Some(root); diff --git a/moli-renderer-v8/src/native_bridge/context_host/core.rs b/moli-renderer-v8/src/native_bridge/context_host/core.rs index 983b47487c..79bb1065ee 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/core.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/core.rs @@ -15,6 +15,7 @@ fn slotchange_microtask_callback( impl JsContextHost { pub(crate) fn mark_main_document_initial_empty(&mut self) { assert!(moli_url::is_about_blank(self.document_url())); + self.frame_owner_store.mark_main_document_initial_empty(); self.initial_empty_main_document = true; } @@ -22,6 +23,16 @@ impl JsContextHost { self.initial_empty_main_document } + pub(crate) fn main_document_has_committed_navigation(&self) -> bool { + self.frame_owner_store + .main_document_has_committed_navigation() + } + + pub(crate) fn complete_current_main_initial_empty_document(&mut self) -> bool { + self.frame_owner_store + .complete_current_main_initial_empty_document() + } + pub(crate) fn rebind_initial_main_document_senders( &mut self, resource: RendererResourceCompletionSender, @@ -47,14 +58,18 @@ impl JsContextHost { &self, origin: &str, inherited_opaque_origin_matches: bool, - inherited_document_domain: Option<&str>, + inherited_origin: Option<&super::WindowAccessOrigin>, policy: &crate::document_runtime::DocumentPolicyContainer, ) -> crate::frame_owner_model::FrameDocumentLocalWindowTransition { - let same_origin = self.document_domain_override.get().as_deref() - == inherited_document_domain - && (inherited_opaque_origin_matches - || moli_url::WebOrigin::from_serialized(&self.main_document_security_origin()) - .same_origin(&moli_url::WebOrigin::from_serialized(origin))); + let same_origin = inherited_opaque_origin_matches + || if let Some(origin @ super::WindowAccessOrigin::Tuple { .. }) = inherited_origin { + self.main_window_access_origin() + .is_some_and(|current| current.can_access(origin)) + } else { + self.document_domain_override.get().is_none() + && moli_url::WebOrigin::from_serialized(&self.main_document_security_origin()) + .same_origin(&moli_url::WebOrigin::from_serialized(origin)) + }; crate::frame_owner_model::FrameDocumentLocalWindowTransition::for_document_commit( self.initial_empty_main_document, same_origin, @@ -143,11 +158,14 @@ impl JsContextHost { pub(crate) fn request_auxiliary_window_close( &mut self, window: crate::runtime::RendererAuxiliaryWindow, - ) { + ) -> bool { if window.close() { self.append_live_turn_items(vec![crate::runtime::RendererOutputItem::OwnerAction( crate::runtime::RendererOwnerAction::CloseAuxiliaryWindow(window), )]); + true + } else { + false } } 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 41666ae0c2..95c47612e4 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 @@ -21,6 +21,21 @@ impl JsContextHost { pub(crate) fn dispatch_main_document_tree_beforeunload( &mut self, scope: &mut v8::PinScope<'_, '_>, + ) -> bool { + self.dispatch_main_document_tree_beforeunload_with_close(scope, false) + } + + pub(crate) fn dispatch_main_document_tree_beforeunload_for_close( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + ) -> bool { + self.dispatch_main_document_tree_beforeunload_with_close(scope, true) + } + + fn dispatch_main_document_tree_beforeunload_with_close( + &mut self, + scope: &mut v8::PinScope<'_, '_>, + closing: bool, ) -> bool { let Some(owner) = self.current_main_document_task_owner() else { return false; @@ -30,9 +45,14 @@ impl JsContextHost { }; let scope = &mut v8::ContextScope::new(scope, context); let window = context.global(scope); - if crate::context_bootstrap::navigation_unload_event_active(scope, window) { + if !closing && crate::context_bootstrap::navigation_unload_event_active(scope, window) { return false; } + let was_closed = self.browsing_context_is_closed(); + let is_current = |host: &Self| { + host.main_document_task_owner_is_current(owner) + && host.browsing_context_is_closed() == was_closed + }; let descendants = self.child_document_descendants_unload_snapshot(self.document_handle()); // Keep the navigation target protected while checking its descendants: // a descendant's beforeunload must not erase the target with open/write. @@ -42,11 +62,14 @@ impl JsContextHost { let previous = crate::context_bootstrap::replace_navigation_unload_event_active(scope, window, true); crate::context_bootstrap::dispatch_beforeunload_for_runtime_owner(scope, window); - self.dispatch_child_documents_beforeunload(scope, descendants, None, |host| { - host.main_document_task_owner_is_current(owner) - }); + self.dispatch_child_documents_beforeunload(scope, descendants, None, is_current); crate::context_bootstrap::replace_navigation_unload_event_active(scope, window, previous); - self.main_document_task_owner_is_current(owner) + is_current(self) + } + + pub(crate) fn main_document_unload_has_started(&self) -> bool { + self.frame_owner_store + .current_main_document_unload_has_started() } pub(crate) fn dispatch_main_document_unload_for_navigation_commit( diff --git a/moli-renderer-v8/src/native_bridge/context_host/mod.rs b/moli-renderer-v8/src/native_bridge/context_host/mod.rs index 4a72fa7218..aebfe5be85 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/mod.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/mod.rs @@ -1460,7 +1460,7 @@ impl JsContextHost { self.shutdown_workers(); self.output_journal = None; self.root_document_lifecycle = None; - self.child_default_context_bootstrap = None; + self.window_default_context_bootstrap = None; self.command_turn_output = None; self.lightweight_popup_browsing_contexts.clear(); self.internal_inspector_value_references.clear(); diff --git a/moli-renderer-v8/src/native_bridge/context_host/navigation.rs b/moli-renderer-v8/src/native_bridge/context_host/navigation.rs index e0cb6be2a6..cfa47c81f0 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/navigation.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/navigation.rs @@ -487,6 +487,24 @@ impl JsContextHost { } } + pub(crate) fn cancel_pending_location_navigation(&mut self) { + self.clear_pending_location_navigation(); + // A request already handed to the browser no longer occupies the + // renderer's pending slot. Cancel through the same ordered owner + // channel, retaining the Document that authorized this action. + if let Some(source_document) = self + .root_document_lifecycle + .as_ref() + .map(RendererDocumentLifecycleJournalHandle::identity) + { + self.append_live_turn_owner_action( + crate::runtime::RendererOwnerAction::StopTopLevelLocationNavigation { + source_document, + }, + ); + } + } + pub(crate) fn record_pending_top_level_history_traversal(&mut self, delta: i64) { self.clear_pending_top_level_navigation(); let traversal = RendererPendingTopLevelHistoryTraversal { delta }; diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups.rs b/moli-renderer-v8/src/native_bridge/context_host/popups.rs index 6b3a70cbf6..a164365e50 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups.rs @@ -247,6 +247,10 @@ impl PendingLightweightPopupHistory { window: v8::Local<'s, v8::Object>, final_url: &Url, ) { + let Some(context) = window.get_creation_context(scope) else { + return; + }; + let scope = &mut v8::ContextScope::new(scope, context); let mut seed = match self.entry_seed() { Some(seed) => seed.clone(), None => { @@ -525,7 +529,7 @@ enum LightweightPopupLifecycle { } pub(super) struct LightweightPopupBrowsingContextRecord { - name: crate::runtime::RendererBrowsingContextName, + pub(in crate::native_bridge::context_host) name: crate::runtime::RendererBrowsingContextName, auxiliary_window: crate::runtime::RendererAuxiliaryWindow, name_lookup_allowed: bool, window_proxy: v8::Global, @@ -546,7 +550,7 @@ impl LightweightPopupBrowsingContextRecord { matches!(self.lifecycle, LightweightPopupLifecycle::Open(_)) } - fn is_open(&self) -> bool { + pub(in crate::native_bridge::context_host) fn is_open(&self) -> bool { self.is_live() && !self.is_closing && !self.auxiliary_window.is_closed() } } @@ -666,7 +670,7 @@ impl LightweightPopupStorageScope { } impl JsContextHost { - fn lightweight_popup_record( + pub(in crate::native_bridge::context_host) fn lightweight_popup_record( &self, popup_id: u64, ) -> Option<&LightweightPopupBrowsingContextRecord> { @@ -796,7 +800,11 @@ impl JsContextHost { let Some(transition) = self.take_lightweight_popup_close_transition(popup_id) else { return false; }; - let window_handle = self.lightweight_popup_record(popup_id).expect("closed popup record retained").auxiliary_window.clone(); + let window_handle = self + .lightweight_popup_record(popup_id) + .expect("closed popup record retained") + .auxiliary_window + .clone(); self.request_auxiliary_window_close(window_handle); self.popup_nested_histories .remove(&OwnerDispatchScope::LightweightPopup(popup_id)); @@ -902,9 +910,12 @@ impl JsContextHost { pub(crate) fn named_lightweight_popup_id(&self, target_name: &str) -> Option { let name = trackable_lightweight_popup_window_name(target_name)?; - self.lightweight_popup_browsing_contexts.iter().find_map(|(id, record)| { - (record.is_open() && record.name_lookup_allowed && record.name.get() == name).then_some(*id) - }) + self.lightweight_popup_browsing_contexts + .iter() + .find_map(|(id, record)| { + (record.is_open() && record.name_lookup_allowed && record.name.get() == name) + .then_some(*id) + }) } pub(crate) fn open_lightweight_popup_window<'s>( @@ -959,7 +970,7 @@ impl JsContextHost { }) } - fn popup_sandbox_policy_for_source( + pub(in crate::native_bridge::context_host) fn popup_sandbox_policy_for_source( &self, source_scope: OwnerDispatchScope, ) -> Option { @@ -1012,41 +1023,42 @@ impl JsContextHost { return None; } let creator_is_secure_context = self.popup_creator_is_secure_context(scope, creator_scope); - let window = self.lightweight_popup_window(scope, popup_id)?; - if update_existing_opener && let Some(opener) = opener { - set_private_value(scope, window, LIGHTWEIGHT_POPUP_OPENER_SLOT, opener.into()); - let endpoint = lightweight_popup_initiator_endpoint(scope, Some(opener), opener_child_handle); - if let Some(record) = self.lightweight_popup_record_mut(popup_id) { - record.opener = endpoint; - record.opener_window = Some(v8::Global::new(scope, opener)); - } + let window = self.lightweight_popup_window(scope, popup_id)?; + if update_existing_opener && let Some(opener) = opener { + set_private_value(scope, window, LIGHTWEIGHT_POPUP_OPENER_SLOT, opener.into()); + let endpoint = + lightweight_popup_initiator_endpoint(scope, Some(opener), opener_child_handle); + if let Some(record) = self.lightweight_popup_record_mut(popup_id) { + record.opener = endpoint; + record.opener_window = Some(v8::Global::new(scope, opener)); } - let Some(href) = href else { - return Some(OpenedLightweightPopup { - window, - popup_id, - created_new_browsing_context: false, - document_response: None, - }); - }; - let (document_response, receiver) = self.auxiliary_document_response_channel(href); - let window = self.reopen_lightweight_popup_window( - scope, + } + let Some(href) = href else { + return Some(OpenedLightweightPopup { + window, popup_id, - opener, - opener_child_handle, - href, - creator_base_url.clone(), - creator_policy_container.clone(), - creator_is_secure_context, - receiver, - )?; - Some(OpenedLightweightPopup { - window, - popup_id, - created_new_browsing_context: false, - document_response, - }) + created_new_browsing_context: false, + document_response: None, + }); + }; + let (document_response, receiver) = self.auxiliary_document_response_channel(href); + let window = self.reopen_lightweight_popup_window( + scope, + popup_id, + opener, + opener_child_handle, + href, + creator_base_url.clone(), + creator_policy_container.clone(), + creator_is_secure_context, + receiver, + )?; + Some(OpenedLightweightPopup { + window, + popup_id, + created_new_browsing_context: false, + document_response, + }) } pub(crate) fn create_lightweight_popup_window<'s>( @@ -2310,7 +2322,10 @@ impl JsContextHost { &self, popup_id: u64, ) -> Option { - if !self.lightweight_popup_is_open(popup_id) { + if !self + .lightweight_popup_record(popup_id) + .is_some_and(|record| record.is_live()) + { return None; } self.lightweight_popup_document_record(popup_id) @@ -5928,60 +5943,6 @@ fn lightweight_popup_window_name_setter<'s>( } } -fn lightweight_popup_opener_getter<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let open = lightweight_popup_id_from_window(scope, args.this()) - .and_then(|id| { - context_host_ptr_from_global_bridge(scope) - .map(|host| unsafe { &*host }.lightweight_popup_is_open(id)) - }) - .unwrap_or(false); - if open - && let Some(opener) = get_private_value(scope, args.this(), LIGHTWEIGHT_POPUP_OPENER_SLOT) - { - rv.set(opener); - return; - } - rv.set_null(); -} - -fn lightweight_popup_opener_setter<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - _rv: v8::ReturnValue<'_, v8::Value>, -) { - set_private_value( - scope, - args.this(), - LIGHTWEIGHT_POPUP_OPENER_SLOT, - args.get(0), - ); - if args.get(0).is_null() - && let Some(popup_id) = lightweight_popup_id_from_window(scope, args.this()) - && let Some(host) = context_host_ptr_from_global_bridge(scope) - && let Some(record) = unsafe { &mut *host }.lightweight_popup_record_mut(popup_id) - { - record.opener = None; - } -} - -fn lightweight_popup_closed_getter<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - let closed = lightweight_popup_id_from_window(scope, args.this()) - .and_then(|id| { - context_host_ptr_from_global_bridge(scope) - .and_then(|host| unsafe { &*host }.lightweight_popup_record(id)) - }) - .is_none_or(|record| !record.is_open()); - rv.set(v8::Boolean::new(scope, closed).into()); -} - fn lightweight_popup_window_opener_getter<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups/realm.rs b/moli-renderer-v8/src/native_bridge/context_host/popups/realm.rs index 913f5182c3..73e30726a9 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups/realm.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups/realm.rs @@ -98,7 +98,7 @@ impl JsContextHost { } self.retire_window_execution_contexts_for_context_token( retired.realm_token, - config.resource_owner_id, + Some(config.resource_owner_id), ); context.detach_global(); } diff --git a/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs b/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs index 38c0128aee..8b45978ab2 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/popups/renderer_owned.rs @@ -12,6 +12,30 @@ pub(crate) struct OpenedRendererWindow<'s> { Option, } +impl<'s> OpenedRendererWindow<'s> { + pub(crate) fn prepare_navigation_activation(&self, scope: &mut v8::PinScope<'s, '_>) -> bool { + if self + .window + .as_ref() + .is_some_and(|window| window.is_closed()) + || crate::context_bootstrap::navigation_unload_event_active(scope, self.window_proxy) + { + return false; + } + if self.pending_page.is_some() { + return true; + } + let Some(context) = self.window_proxy.get_creation_context(scope) else { + return false; + }; + let Some(host_ptr) = crate::util::context_host_ptr_from_context_slot(context) else { + return false; + }; + let scope = &mut v8::ContextScope::new(scope, context); + unsafe { &mut *host_ptr }.dispatch_main_document_tree_beforeunload(scope) + } +} + impl JsContextHost { pub(crate) fn has_browser_owned_auxiliary_page_factory(&self) -> bool { self.browser_context_runtime @@ -165,13 +189,29 @@ impl JsContextHost { } else { about_blank_url() }; + policy.document_referrer = Url::parse(&policy.document_referrer) + .ok() + .and_then(|source| { + moli_fetch::referrer_value( + &source, + if exposes_opener { + &source + } else { + &initial_url + }, + None, + policy.referrer_policy.as_deref(), + ) + }) + .unwrap_or_default(); let base_url = if exposes_opener { lightweight_popup_initial_base_url(&initial_url, creator_base_url) } else { initial_url.clone() }; - let sandbox = creator_child_handle - .and_then(|handle| self.child_browsing_context_popup_opener_sandbox_policy(handle)); + let sandbox = creator_child_handle.and_then(|handle| { + self.popup_sandbox_policy_for_source(OwnerDispatchScope::Child(handle)) + }); if policy.sandbox.allows_popups_to_escape { policy.sandbox = DocumentSandboxPolicy::default(); } @@ -202,6 +242,19 @@ impl JsContextHost { let loader = source_loader .request_client() .fork_with_isolated_page_network_policy(); + let about_document_state = self + .capture_about_document_state( + creator_child_handle.map_or(OwnerDispatchScope::Top, OwnerDispatchScope::Child), + &initial_url, + None, + ) + .map(|state| { + crate::runtime::RendererAboutDocumentState::new( + state.origin().clone(), + base_url.clone(), + policy.clone(), + ) + }); let env = crate::runtime::PageVmEnvConfig { root_frame_id: None, main_document_commit: None, @@ -235,6 +288,8 @@ impl JsContextHost { layout_configuration: Default::default(), wpt_extensions_enabled: false, navigation_bootstrap_entry: None, + navigation_history_source: None, + about_document_state, reserved_service_worker_client_id: None, }; let mut dom_host = crate::dom::native::DomHost::from_dom( diff --git a/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs b/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs index 6b4d90fb82..b36c8df3e2 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/runtime_observable.rs @@ -162,14 +162,14 @@ impl JsContextHost { .collect::>(); realm_tokens.sort_unstable(); realm_tokens.dedup(); + let resource_owner_id = self + .window_default_context_bootstrap + .as_ref() + .map(|config| config.resource_owner_id); for realm_token in realm_tokens { self.retire_window_realm_resources(realm_token); - let resource_owner_id = self - .child_default_context_bootstrap - .as_ref() - .map(|config| config.resource_owner_id); - self.retire_window_execution_context_registry(realm_token, resource_owner_id); + self.retire_window_execution_contexts_for_context_token(realm_token, resource_owner_id); } for owner in owners { @@ -490,14 +490,6 @@ impl JsContextHost { } pub(crate) fn retire_window_execution_contexts_for_context_token( - &mut self, - context_token: RuntimeObservableContextToken, - resource_owner_id: crate::resource_owner::ResourceOwnerId, - ) -> usize { - self.retire_window_execution_context_registry(context_token, Some(resource_owner_id)) - } - - fn retire_window_execution_context_registry( &mut self, context_token: RuntimeObservableContextToken, resource_owner_id: Option, diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs index d86aca5ea0..9c179b5104 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/window_execution_context/settings.rs @@ -59,7 +59,7 @@ impl WindowEnvironmentSettings { scope.get_current_context().get_slot::() } - pub(crate) fn bind_current_main_document_for_retirement( + pub(crate) fn bind_current_main_document( scope: &mut v8::PinScope<'_, '_>, host: &JsContextHost, ) -> Option<()> { diff --git a/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs b/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs index 3daecda867..e2af3e163c 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/window_security_tokens.rs @@ -21,8 +21,7 @@ pub(crate) fn window_contexts_allow_access( else { return false; }; - let Some(accessed_host_ptr) = - crate::util::context_host_ptr_from_context_slot(accessed_context) + let Some(accessed_host_ptr) = crate::util::context_host_ptr_from_context_slot(accessed_context) else { return false; }; @@ -236,7 +235,6 @@ impl JsContextHost { return Some(owner.origin); } WindowSecurityOrigin::for_context(context) - } pub(in crate::native_bridge) fn document_has_same_origin_as_entry<'s>( @@ -285,7 +283,6 @@ impl JsContextHost { origin .zip(entry_origin) .is_some_and(|(origin, entry)| origin.has_same_origin(&entry)) - } pub(crate) fn install_window_context_security_origin( @@ -821,7 +818,9 @@ impl JsContextHost { } } - pub(in crate::native_bridge::context_host) fn main_window_access_origin(&self) -> Option { + pub(in crate::native_bridge::context_host) fn main_window_access_origin( + &self, + ) -> Option { if !self .document_policy_container() .sandbox diff --git a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs index 16ef1320f9..dd4677766d 100644 --- a/moli-renderer-v8/src/native_bridge/element/activation/targets.rs +++ b/moli-renderer-v8/src/native_bridge/element/activation/targets.rs @@ -265,12 +265,8 @@ fn navigate_element_popup_target( .or_else(|| creator.policy_container.referrer_policy.clone()) }, ); - creator.policy_container.document_referrer = if relations.suppress_opener { - navigation_initiator - .document_referrer(&url::Url::parse(resolved_url).expect("resolved hyperlink URL")) - } else { - navigation_initiator.outgoing_referrer() - }; + creator.policy_container.document_referrer = navigation_initiator + .document_referrer(&url::Url::parse(resolved_url).expect("resolved hyperlink URL")); let opener = (!relations.suppress_opener).then_some(creator.opener); let source_child_handle = match dispatch_scope { crate::native_bridge::OwnerDispatchScope::Child(handle) => Some(handle), @@ -306,6 +302,9 @@ fn navigate_element_popup_target( return false; } }; + if !opened.prepare_navigation_activation(scope) { + return true; + } let disposition = if opened.pending_page.is_some() || runtime.protocol_user_gesture_activation() || runtime.current_input_event().is_some() @@ -455,6 +454,14 @@ pub(in crate::native_bridge) fn choose_form_navigation_target( return None; } let relations = element_popup_relations(unsafe { &*runtime_ptr }, form, target_name); + if let Some(ExistingHyperlinkTarget::RelatedPage(window)) = + existing_hyperlink_target(scope, runtime_ptr, source, form, Some(target_name)) + && crate::context_bootstrap::navigation_unload_event_active(scope, window) + { + // A related Page being unloaded is still the named target. Do not + // fall through to creating a second legacy popup with the same name. + return None; + } let mut creator = element_popup_creator(scope, runtime_ptr, form)?; creator.policy_container.document_referrer = if relations.suppress_referrer { String::new() @@ -486,6 +493,7 @@ pub(in crate::native_bridge) fn choose_form_navigation_target( None, creator.base_url, creator.policy_container, + false, ), _ => runtime.open_lightweight_popup_window( scope, @@ -496,6 +504,7 @@ pub(in crate::native_bridge) fn choose_form_navigation_target( None, creator.base_url, creator.policy_container, + false, ), }?; if !opened.allows_navigation_activation(scope, runtime) { @@ -641,19 +650,26 @@ pub(crate) fn navigate_existing_browsing_context_target<'s>( if navigated { Some(target_window) } else { None } } +#[derive(Clone, Copy)] +enum ExistingHyperlinkTarget<'s> { + Local(crate::native_bridge::OwnerDispatchScope), + RelatedPage(v8::Local<'s, v8::Object>), +} + /// Resolve an existing target without reading script-visible WindowProxy properties. -fn existing_hyperlink_target( - scope: &mut v8::PinScope<'_, '_>, +fn existing_hyperlink_target<'s>( + scope: &mut v8::PinScope<'s, '_>, runtime_ptr: *mut JsContextHost, source: crate::native_bridge::OwnerDispatchScope, + source_handle: DomHandle, target_name: Option<&str>, -) -> Option { +) -> Option> { use crate::native_bridge::OwnerDispatchScope; - let runtime = unsafe { &*runtime_ptr }; + let runtime = unsafe { &mut *runtime_ptr }; let Some(name) = target_name else { - return Some(source); + return Some(ExistingHyperlinkTarget::Local(source)); }; - match SpecialBrowsingContextTarget::parse(name) { + let local = match SpecialBrowsingContextTarget::parse(name) { Some(SpecialBrowsingContextTarget::Current) => Some(source), Some( special @ (SpecialBrowsingContextTarget::Top | SpecialBrowsingContextTarget::Parent), @@ -668,9 +684,23 @@ fn existing_hyperlink_target( Some(target) } Some(SpecialBrowsingContextTarget::Blank) => None, - None => unsafe { &mut *runtime_ptr } - .browsing_context_target_by_name_for_navigation(scope, name, source), + None => runtime.browsing_context_target_by_name_for_navigation(scope, name, source), + }; + if let Some(local) = local { + return Some(ExistingHyperlinkTarget::Local(local)); } + if SpecialBrowsingContextTarget::parse(name).is_some() + || element_popup_relations(runtime, source_handle, name).suppress_opener + { + return None; + } + let related = runtime + .page_script_environment()? + .named_related_window(name, runtime.browsing_context_name())?; + related + .window_proxy_in_scope(scope) + .ok() + .map(ExistingHyperlinkTarget::RelatedPage) } pub(in crate::native_bridge) fn navigate_element_target_browsing_context( @@ -689,7 +719,9 @@ pub(in crate::native_bridge) fn navigate_element_target_browsing_context( let Some(source) = unsafe { &*runtime_ptr }.owner_dispatch_scope_for_node(source_handle) else { return false; }; - let Some(target) = existing_hyperlink_target(scope, runtime_ptr, source, target_name) else { + let Some(target) = + existing_hyperlink_target(scope, runtime_ptr, source, source_handle, target_name) + else { return target_name.is_some_and(|name| { navigate_element_popup_target( scope, @@ -702,8 +734,10 @@ pub(in crate::native_bridge) fn navigate_element_target_browsing_context( }); }; let runtime = unsafe { &mut *runtime_ptr }; - if url::Url::parse(resolved_url) - .is_ok_and(|destination| runtime.blocks_ancestor_navigation(source, target, &destination)) + if let ExistingHyperlinkTarget::Local(target) = &target + && url::Url::parse(resolved_url).is_ok_and(|destination| { + runtime.blocks_ancestor_navigation(source, *target, &destination) + }) { return true; } @@ -711,29 +745,77 @@ pub(in crate::native_bridge) fn navigate_element_target_browsing_context( return false; }; let initiator_url = runtime.document_url_for_handle(document); - let Some(source_element) = crate::util::node_wrapper_from_handle(scope, source_handle) else { + let initiator = crate::runtime::RendererNavigationInitiator::new( + runtime + .document_referrer_source_url_for_dispatch_scope(source) + .unwrap_or(initiator_url), + runtime + .document_resource_loader_for_dispatch_scope(source) + .map(|loader| loader.fetch_context().request_origin()) + .unwrap_or(moli_url::WebOrigin::Opaque), + if element_popup_relations(runtime, source_handle, target_name.unwrap_or("_self")) + .suppress_referrer + { + Some("no-referrer".to_owned()) + } else { + element_popup_referrer_policy(runtime, source_handle) + .map(str::to_owned) + .or_else(|| { + runtime + .document_policy_container_for_inheritance(source) + .and_then(|policy| policy.referrer_policy) + }) + }, + ); + let Some(source_element) = + runtime + .native_bridge_mut() + .wrap_handle(scope, runtime_ptr, source_handle) + else { return false; }; // Resolve the native target realm before reading its private Location slot. // The source's access check controls event dispatch and sourceElement exposure. - let context = match target { - OwnerDispatchScope::Child(handle) => runtime - .ensure_prebootstrapped_child_default_context(scope, handle) + let context = match &target { + ExistingHyperlinkTarget::Local(OwnerDispatchScope::Child(handle)) => runtime + .ensure_prebootstrapped_child_default_context(scope, *handle) .ok(), - _ => runtime - .current_registered_window_execution_context_identity(target) - .and_then(|identity| runtime.window_execution_context(scope, identity.owner(), target)) + ExistingHyperlinkTarget::Local(OwnerDispatchScope::LightweightPopup(id)) => runtime + .lightweight_popup_window(scope, *id) + .and_then(|window| window.get_creation_context(scope)), + ExistingHyperlinkTarget::Local(target) => runtime + .current_registered_window_execution_context_identity(*target) + .and_then(|identity| runtime.window_execution_context(scope, identity.owner(), *target)) .map(|(_, context)| context), + ExistingHyperlinkTarget::RelatedPage(window) => window.get_creation_context(scope), }; let Some(context) = context else { return false }; + let native_popup_context = matches!(target, ExistingHyperlinkTarget::RelatedPage(_)) + || matches!(target, ExistingHyperlinkTarget::Local(OwnerDispatchScope::LightweightPopup(id)) + if runtime.lightweight_popup_window(scope, id) + .is_some_and(|window| window.strict_equals(context.global(scope).into()))); let can_access = runtime .current_registered_window_execution_context_identity(source) .is_some_and(|identity| { - runtime.window_execution_context_can_access_dispatch_scope(identity, target) + if native_popup_context { + runtime + .window_execution_context(scope, identity.owner(), source) + .is_some_and(|(_, source_context)| { + crate::native_bridge::window_contexts_allow_access(source_context, context) + }) + } else { + let ExistingHyperlinkTarget::Local(target) = target else { + unreachable!("related Pages use native context access checks") + }; + runtime.window_execution_context_can_access_dispatch_scope(identity, target) + } }); let scope = &mut v8::ContextScope::new(scope, context); - let window = match target { - OwnerDispatchScope::LightweightPopup(id) => runtime.lightweight_popup_window(scope, id), + let window = match &target { + ExistingHyperlinkTarget::Local(OwnerDispatchScope::LightweightPopup(id)) => { + runtime.lightweight_popup_window(scope, *id) + } + ExistingHyperlinkTarget::RelatedPage(window) => Some(*window), _ => Some(context.global(scope)), }; let Some(location) = window @@ -749,11 +831,13 @@ pub(in crate::native_bridge) fn navigate_element_target_browsing_context( crate::context_bootstrap::HyperlinkNavigationOptions { user_initiated, source_can_access_target: can_access, - initiator_url, + initiator, named_popup: target_name .filter(|name| { - matches!(target, OwnerDispatchScope::LightweightPopup(_)) - && SpecialBrowsingContextTarget::parse(name).is_none() + matches!( + target, + ExistingHyperlinkTarget::Local(OwnerDispatchScope::LightweightPopup(_)) + ) && SpecialBrowsingContextTarget::parse(name).is_none() }) .map(|name| NamedHyperlinkPopup { source_handle, diff --git a/moli-renderer-v8/src/native_bridge/identity.rs b/moli-renderer-v8/src/native_bridge/identity.rs index a2c5d423d9..050510646a 100644 --- a/moli-renderer-v8/src/native_bridge/identity.rs +++ b/moli-renderer-v8/src/native_bridge/identity.rs @@ -449,6 +449,10 @@ pub(crate) fn contexts_share_wrapper_world( right: v8::Local<'_, v8::Context>, ) -> bool { left == right + // Related native Pages share the isolate's default wrapper world, + // even though each native DOM host owns a separate reflector cache. + || (left.get_slot::().is_some() + && right.get_slot::().is_some()) || match ( left.get_slot::>(), right.get_slot::>(), @@ -587,16 +591,40 @@ pub(crate) fn clear_context_wrapper_cache_for_teardown( .map(|token| token.as_u64()); if let Some(cache) = context.get_slot::>() { let mut entries = cache.borrow_mut(); - for entry in entries.wrappers.values_mut() { - if all_entries || entry.creation_realm.map(NonZeroU64::get) == realm { - entry.wrapper.retain_in_realm(scope); + entries + .retired_wrappers + .retain(|_, wrapper| !wrapper.is_empty()); + entries + .retired_live_collection_wrappers + .retain(|_, wrapper| !wrapper.is_empty()); + // A retained realm must preserve wrappers that JS still reaches without + // making every detached node reachable through the realm itself. + let mut retired = Vec::new(); + entries.wrappers.retain(|id, entry| { + if !all_entries && entry.creation_realm.map(NonZeroU64::get) != realm { + return true; } - } - for entry in entries.live_collection_wrappers.values_mut() { - if all_entries || entry.creation_realm.map(NonZeroU64::get) == realm { - entry.wrapper.retain_in_realm(scope); + if let Some(wrapper) = entry.wrapper.to_local(scope) { + retired.push((id, v8::Weak::new(scope, wrapper))); } - } + false + }); + entries.retired_wrappers.extend(retired); + let mut retired_collections = Vec::new(); + entries + .live_collection_wrappers + .retain(|descriptor, entry| { + if !all_entries && entry.creation_realm.map(NonZeroU64::get) != realm { + return true; + } + if let Some(wrapper) = entry.wrapper.to_local(scope) { + retired_collections.push((descriptor.clone(), v8::Weak::new(scope, wrapper))); + } + false + }); + entries + .retired_live_collection_wrappers + .extend(retired_collections); drop(entries); crate::util::retain_context_v8_handle_state_for_safe_release(context, cache); } @@ -620,6 +648,24 @@ pub(super) struct BridgeIdentityStore { } impl BridgeIdentityStore { + fn current_wrapper_cache( + &self, + scope: &mut v8::PinScope<'_, '_>, + ) -> Rc> { + if scope + .get_current_context() + .get_slot::() + .is_some() + { + // Reflector IDs are local to this native host. A cross-Page call + // in the same default world must use this store's canonical cache, + // not the calling Page's cache with potentially colliding IDs. + self.default_world_wrapper_cache.clone() + } else { + context_wrapper_cache(scope) + } + } + pub(super) fn install_default_world_wrapper_cache(&self, context: v8::Local<'_, v8::Context>) { let _ = context.set_slot(self.default_world_wrapper_cache.clone()); let _ = context.set_slot(Rc::new(SharedDefaultWorldWrapperCache)); @@ -655,13 +701,13 @@ impl BridgeIdentityStore { if matches!(self.bridge_handle(reflector_id), Some(BridgeHandle::Window)) { return context_window_wrapper(scope); } - context_wrapper_cache(scope) + self.current_wrapper_cache(scope) .borrow() .wrappers .get(&reflector_id) .and_then(|entry| entry.wrapper.to_local(scope)) .or_else(|| { - context_wrapper_cache(scope) + self.current_wrapper_cache(scope) .borrow() .retired_wrappers .get(&reflector_id) @@ -679,7 +725,7 @@ impl BridgeIdentityStore { set_context_window_wrapper(scope, wrapper); return; } - let cache = context_wrapper_cache(scope); + let cache = self.current_wrapper_cache(scope); let mut cache = cache.borrow_mut(); if scope .get_current_context() @@ -712,13 +758,13 @@ impl BridgeIdentityStore { scope: &mut v8::PinScope<'s, '_>, descriptor: &LiveCollectionDescriptor, ) -> Option> { - context_wrapper_cache(scope) + self.current_wrapper_cache(scope) .borrow() .live_collection_wrappers .get(descriptor) .and_then(|entry| entry.wrapper.to_local(scope)) .or_else(|| { - context_wrapper_cache(scope) + self.current_wrapper_cache(scope) .borrow() .retired_live_collection_wrappers .get(descriptor) @@ -732,7 +778,7 @@ impl BridgeIdentityStore { descriptor: LiveCollectionDescriptor, wrapper: v8::Local<'_, v8::Object>, ) { - let cache = context_wrapper_cache(scope); + let cache = self.current_wrapper_cache(scope); let mut cache = cache.borrow_mut(); if scope .get_current_context() diff --git a/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs b/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs index ad867a9b03..32973600da 100644 --- a/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs +++ b/moli-renderer-v8/src/native_bridge/identity/dense_reflector_map.rs @@ -113,13 +113,6 @@ impl DenseReflectorMap { pub(super) fn values(&self) -> impl Iterator { self.dense.iter().flatten().chain(self.sparse.values()) } - - pub(super) fn values_mut(&mut self) -> impl Iterator { - self.dense - .iter_mut() - .flatten() - .chain(self.sparse.values_mut()) - } } #[cfg(test)] diff --git a/moli-renderer-v8/src/runtime/owner/document_response.rs b/moli-renderer-v8/src/runtime/owner/document_response.rs index be79011629..cc3fb82b10 100644 --- a/moli-renderer-v8/src/runtime/owner/document_response.rs +++ b/moli-renderer-v8/src/runtime/owner/document_response.rs @@ -24,12 +24,18 @@ impl RendererCreateStreamingRawPageRequest { local_executor: JsLocalExecutor, runtime_hooks: PageVmRuntimeHooks, ) -> Result { - let navigation_bootstrap_entry = self.navigation_history + let navigation_bootstrap_entry = self + .navigation_history .as_ref() .map(|history| history.resolve(&self.final_url)) .transpose()?; - self.bootstrap_with_navigation_seed(page_id, local_executor, runtime_hooks, navigation_bootstrap_entry) - .await + self.bootstrap_with_navigation_seed( + page_id, + local_executor, + runtime_hooks, + navigation_bootstrap_entry, + ) + .await } pub(in crate::runtime) async fn bootstrap_with_navigation_seed( @@ -85,9 +91,12 @@ impl RendererCreateStreamingRawPageRequest { let document_policy_container = about_document_state .as_ref() .map(|state| state.policy_container().clone()) - .unwrap_or_else(|| DocumentPolicyContainer::from_navigation_response_headers( - &response_headers, &final_url, - )) + .unwrap_or_else(|| { + DocumentPolicyContainer::from_navigation_response_headers( + &response_headers, + &final_url, + ) + }) .with_content_security_policy_bypass(document_settings.bypass_content_security_policy); let document_default_language = crate::document_language::document_default_language_from_headers(&response_headers); @@ -119,7 +128,9 @@ impl RendererCreateStreamingRawPageRequest { main_document_commit, top_level_storage_key: None, navigation_bootstrap_entry, - navigation_history_source: navigation_history.as_ref().map(|history| history.source_history()), + navigation_history_source: navigation_history + .as_ref() + .map(|history| history.source_history()), reserved_service_worker_client_id: reserved_service_worker_client .map(RendererReservedServiceWorkerClient::release), }; diff --git a/moli-renderer-v8/src/runtime/owner_local_store/entry.rs b/moli-renderer-v8/src/runtime/owner_local_store/entry.rs index 114b49b6c6..e96d265f9b 100644 --- a/moli-renderer-v8/src/runtime/owner_local_store/entry.rs +++ b/moli-renderer-v8/src/runtime/owner_local_store/entry.rs @@ -342,6 +342,12 @@ impl LivePageEntry { dispatch: RendererTopLevelNavigationDispatch, ) { self.top_level_navigation_dispatch = dispatch; + if let Some(environment) = self + .active_page_vm() + .and_then(PageVm::renderer_page_script_environment) + { + environment.set_top_level_navigation_dispatch(dispatch); + } } pub(in crate::runtime) fn top_level_navigation_dispatch( diff --git a/moli-renderer-v8/src/runtime/page_vm/auxiliary_page.rs b/moli-renderer-v8/src/runtime/page_vm/auxiliary_page.rs index b4df9eb557..f28f6ef18a 100644 --- a/moli-renderer-v8/src/runtime/page_vm/auxiliary_page.rs +++ b/moli-renderer-v8/src/runtime/page_vm/auxiliary_page.rs @@ -64,7 +64,10 @@ impl PageVm { ); vm.install_page_task_capabilities(capabilities); vm.set_root_document_lifecycle(lifecycle.clone()); - vm.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete)?; + // This Document completed synchronously before window.open returned. + // Complete the native lifecycle too, so parser scheduling cannot emit + // a second load that observers mistake for the requested navigation. + vm.complete_related_initial_empty_document()?; for milestone in [ RendererDocumentLifecycleMilestone::DomContentLoaded, RendererDocumentLifecycleMilestone::Load, diff --git a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs index 33e4aaae85..2db5d5bae6 100644 --- a/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs +++ b/moli-renderer-v8/src/runtime/page_vm/followed_navigation.rs @@ -1133,20 +1133,26 @@ impl PageVm { pub(in crate::runtime) fn validate_prepared_document_response( &mut self, - request: crate::runtime::owner::RendererCreateStreamingRawPageRequest, + mut request: crate::runtime::owner::RendererCreateStreamingRawPageRequest, mut bootstrap: crate::script_vm::RendererDocumentIsolateBootstrap, reservation: crate::runtime::owner_local_store::RendererDocumentIsolateReservation, ) -> Result { request.validate_bootstrap_configuration()?; - let navigation_bootstrap_entry = match request + // Browser requests retain the destination identity while resolving the + // latest source state at commit. A session-history position alone is + // only a fallback for requests without that explicit history handle. + let navigation_bootstrap_entry = if let Some(history) = request.navigation_history.as_ref() + { + Some(history.resolve(&request.final_url)?) + } else if let Some(position) = request .main_document_commit .as_ref() .and_then(|commit| commit.session_history_position) { - Some(position) => self - .vm_mut() - .capture_inherited_history_for_browser_commit(&request.final_url, position)?, - None => None, + self.vm_mut() + .capture_inherited_history_for_browser_commit(&request.final_url, position)? + } else { + None }; if moli_url::is_about_blank(&request.final_url) && let Some(replacement) = request.document_replacement.as_ref() @@ -1167,6 +1173,14 @@ impl PageVm { ); } } + if let Some(state) = bootstrap + .initial_document_environment + .as_ref() + .and_then(|environment| environment.about_document_state.clone()) + && let Some(history) = request.navigation_history.take() + { + request.navigation_history = Some(history.with_about_document_state(Some(state))); + } ensure!( request.lifecycle_decider.is_none(), "a Page replacement cannot use a Page-creation lifecycle decider" diff --git a/moli-renderer-v8/src/runtime/page_vm/mod.rs b/moli-renderer-v8/src/runtime/page_vm/mod.rs index 54f89f3801..883c3cedb6 100644 --- a/moli-renderer-v8/src/runtime/page_vm/mod.rs +++ b/moli-renderer-v8/src/runtime/page_vm/mod.rs @@ -1956,6 +1956,12 @@ impl PageVm { let Some(vm) = self.vm.as_mut() else { return; }; + // A native auxiliary Window is closed on the protocol owner's task. + // Settle its Navigation API while the exact realm and callbacks remain + // registered, before clearing the context's resources. + if let Err(error) = vm.cancel_closed_main_window_navigation() { + tracing::warn!(%error, "failed to cancel closed Window navigation before retirement"); + } vm.detach_default_inspector_context_for_context_teardown(); vm.close_page_context_resources_for_context_teardown(); } @@ -4658,11 +4664,10 @@ impl PageVm { mode, AuxiliaryEnvironmentApply::Navigation | AuxiliaryEnvironmentApply::InheritedNavigation ) { - self.vm_mut() - .install_navigation_bootstrap_from_history( - env.navigation_bootstrap_entry.clone(), - env.navigation_history_source.clone(), - ); + self.vm_mut().install_navigation_bootstrap_from_history( + env.navigation_bootstrap_entry.clone(), + env.navigation_history_source.clone(), + ); if env.navigation_bootstrap_entry.is_none() && let Some(position) = env .main_document_commit diff --git a/moli-renderer-v8/src/runtime/protocol_output/item.rs b/moli-renderer-v8/src/runtime/protocol_output/item.rs index a5b10a09aa..436019e10a 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/item.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/item.rs @@ -64,6 +64,9 @@ pub enum RendererOwnerAction { update: moli_page_types::SessionHistoryUpdate, }, TopLevelLocationNavigation(RendererDocumentSourcedTopLevelLocationNavigation), + StopTopLevelLocationNavigation { + source_document: RendererDocumentLifecycleIdentity, + }, TopLevelHistoryTraversal(RendererPendingTopLevelHistoryTraversal), SubresourceFetchPause { source_document: RendererDocumentLifecycleIdentity, diff --git a/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs b/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs index ef8040a777..8930ca0bd9 100644 --- a/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs +++ b/moli-renderer-v8/src/runtime/protocol_output/transport_memory.rs @@ -313,7 +313,8 @@ fn owner_action_transport_charge_bytes(action: &RendererOwnerAction) -> usize { .unwrap_or_default(), ) .saturating_add(headers_charge(event.request_headers())), - RendererOwnerAction::TopLevelHistoryTraversal(_) => 0, + RendererOwnerAction::TopLevelHistoryTraversal(_) + | RendererOwnerAction::StopTopLevelLocationNavigation { .. } => 0, RendererOwnerAction::SubresourceFetchPause { info, .. } => { info.renderer_transport_charge_bytes() } diff --git a/moli-renderer-v8/src/script_vm.rs b/moli-renderer-v8/src/script_vm.rs index 3dc580b3c3..a36e89986c 100644 --- a/moli-renderer-v8/src/script_vm.rs +++ b/moli-renderer-v8/src/script_vm.rs @@ -809,9 +809,9 @@ mod inspector; mod promise_rejection_task; mod script_preparation_error; pub(crate) use inspector::{dispatch_inspector_io_owner_wake, dispatch_inspector_main_owner_wake}; +mod canvas_blob_serialization; mod document_context_transition; mod initial_window_document; -mod canvas_blob_serialization; mod isolated_worlds; mod main_document_lifecycle; mod main_document_lifecycle_body; @@ -2019,6 +2019,7 @@ pub(crate) struct ScriptVmInitialDocumentEnvironment { pub(crate) policy_container: crate::document_runtime::DocumentPolicyContainer, fallback_base_url: Option, storage_key: Option, + pub(crate) about_document_state: Option, } pub(crate) use document_environment::ScriptVmCapturedDocumentEnvironment; @@ -2050,6 +2051,9 @@ impl ScriptVmPageRealmBootstrap { .initial_document_environment .take() }); + let about_document_state = initial_environment + .as_ref() + .and_then(|environment| environment.about_document_state.clone()); let document_handle = dom_host.document_handle(); if let Some(base) = initial_environment .as_ref() @@ -2182,6 +2186,9 @@ impl ScriptVmPageRealmBootstrap { .bind_deferred_context_host_release_queue( renderer_document_isolate.deferred_context_host_release_queue(), ); + context_host + .borrow_mut() + .set_main_about_document_state(about_document_state); if let Some(root_frame_id) = root_frame_id.as_deref() { let name = context_host .borrow() @@ -2485,10 +2492,32 @@ impl ScriptVmDefaultWorldBootstrap { >, about_document_state: Option, ) -> std::result::Result { + let about_document_state = renderer_document_isolate_bootstrap + .initial_document_environment + .as_ref() + .and_then(|environment| environment.about_document_state.clone()) + .or(about_document_state); let top_level_storage_key = about_document_state .as_ref() .and_then(crate::runtime::RendererAboutDocumentState::storage_key) .or(top_level_storage_key); + let initial_environment = if renderer_document_isolate_bootstrap + .initial_document_environment + .is_none() + { + about_document_state + .as_ref() + .map(|state| ScriptVmInitialDocumentEnvironment { + security_token: None, + origin: state.origin().serialized_origin(), + policy_container: state.policy_container().clone(), + fallback_base_url: Some(state.base_url().clone()), + storage_key: state.storage_key(), + about_document_state: Some(state.clone()), + }) + } else { + None + }; let mut bootstrap = ScriptVmPageRealmBootstrap::new_from_dom_host( bootstrap_dom_host, bypass_content_security_policy, @@ -2505,7 +2534,7 @@ impl ScriptVmDefaultWorldBootstrap { main_document_commit, top_level_storage_key, reserved_service_worker_client_id, - None, + initial_environment, )?; if let Some(state) = &about_document_state { bootstrap diff --git a/moli-renderer-v8/src/script_vm/document_environment.rs b/moli-renderer-v8/src/script_vm/document_environment.rs index c7f22ab4b8..636278ef9d 100644 --- a/moli-renderer-v8/src/script_vm/document_environment.rs +++ b/moli-renderer-v8/src/script_vm/document_environment.rs @@ -85,6 +85,23 @@ impl ScriptVmInitialDocumentEnvironment { host.inherited_document_domain(child) }) .unwrap_or_default(); + let about_document_state = crate::util::context_host_ptr_from_context_slot(context) + .and_then(|host| { + let host = unsafe { &*host }; + let owner = host.window_dispatch_scope_for_context(scope, context)?; + host.capture_about_document_state( + owner, + &Url::parse("about:blank").expect("valid about:blank URL"), + None, + ) + }) + .map(|state| { + crate::runtime::RendererAboutDocumentState::new( + state.origin().clone(), + base_url.clone(), + policy_container.clone(), + ) + }); Ok(Self { security_token, document_domain_override, @@ -92,6 +109,7 @@ impl ScriptVmInitialDocumentEnvironment { policy_container, fallback_base_url: Some(base_url), storage_key: Some(storage_key), + about_document_state, }) } } @@ -423,23 +441,47 @@ impl ScriptVm { Some(host.document_base_url_for_handle(host.document_handle())) }; let mut policy_container = host.document_policy_container().clone(); - let source = if preserve_navigation_referrer { - url::Url::parse(&policy_container.document_referrer).ok() - } else { - Some(host.document_url().clone()) - }; - policy_container.document_referrer = source - .as_ref() - .and_then(|source| { - moli_fetch::referrer_value( - source, - host.document_url(), - None, - policy_container.referrer_policy.as_deref(), - ) - }) + if preserve_navigation_referrer && host.main_document_is_initial_empty() { + // The synchronous initial Document may expose the creator's + // full URL. A browser reload reapplies the navigation policy + // to that retained source for its actual about:blank target. + policy_container.document_referrer = + Url::parse(&policy_container.document_referrer) + .ok() + .and_then(|source| { + moli_fetch::referrer_value( + &source, + host.document_url(), + None, + policy_container.referrer_policy.as_deref(), + ) + }) + .unwrap_or_default(); + } else if !preserve_navigation_referrer { + policy_container.document_referrer = moli_fetch::referrer_value( + host.document_url(), + host.document_url(), + None, + policy_container.referrer_policy.as_deref(), + ) .unwrap_or_default(); + } let token = scope.get_current_context().get_security_token(scope); + let about_document_state = host + .capture_about_document_state( + crate::native_bridge::OwnerDispatchScope::Top, + &Url::parse("about:blank").expect("valid about:blank URL"), + None, + ) + .map(|state| { + crate::runtime::RendererAboutDocumentState::new( + state.origin().clone(), + fallback_base_url + .clone() + .unwrap_or_else(|| host.document_url().clone()), + policy_container.clone(), + ) + }); Ok(ScriptVmInitialDocumentEnvironment { security_token: Some(v8::Global::new(scope, token)), document_domain_override: host.inherited_document_domain(None), @@ -447,6 +489,7 @@ impl ScriptVm { policy_container, fallback_base_url, storage_key: Some(host.top_web_storage_scope().storage_key().clone()), + about_document_state, }) }) } diff --git a/moli-renderer-v8/src/script_vm/document_isolate.rs b/moli-renderer-v8/src/script_vm/document_isolate.rs index f6673bf811..a8b5e7af57 100644 --- a/moli-renderer-v8/src/script_vm/document_isolate.rs +++ b/moli-renderer-v8/src/script_vm/document_isolate.rs @@ -277,6 +277,7 @@ struct RendererPageScriptEnvironmentInner { auxiliary_window: OnceCell, opener: RefCell>>, document_host: RefCell>>, + top_level_navigation_dispatch: Cell, closed: Cell, } @@ -360,6 +361,9 @@ impl RendererPageScriptEnvironment { auxiliary_window: OnceCell::new(), opener: RefCell::new(None), document_host: RefCell::new(Weak::new()), + top_level_navigation_dispatch: Cell::new( + crate::RendererTopLevelNavigationDispatch::FollowInStandaloneAdapter, + ), closed: Cell::new(false), }), }; @@ -380,6 +384,19 @@ impl RendererPageScriptEnvironment { *self.inner.document_host.borrow_mut() = Rc::downgrade(host); } + pub(crate) fn set_top_level_navigation_dispatch( + &self, + dispatch: crate::RendererTopLevelNavigationDispatch, + ) { + self.inner.top_level_navigation_dispatch.set(dispatch); + } + + pub(crate) fn top_level_navigation_dispatch( + &self, + ) -> crate::RendererTopLevelNavigationDispatch { + self.inner.top_level_navigation_dispatch.get() + } + pub(crate) fn enqueue_related_page_turn_completion(&self) { let host = self.inner.document_host.borrow().clone(); self.inner diff --git a/moli-renderer-v8/src/script_vm/execution_contexts.rs b/moli-renderer-v8/src/script_vm/execution_contexts.rs index b5561c858c..c59029a1e2 100644 --- a/moli-renderer-v8/src/script_vm/execution_contexts.rs +++ b/moli-renderer-v8/src/script_vm/execution_contexts.rs @@ -424,7 +424,7 @@ impl ScriptVm { host.retire_window_realm_resources(context.runtime_observable_context_token); host.retire_window_execution_contexts_for_context_token( context.runtime_observable_context_token, - self.resource_owner_id, + Some(self.resource_owner_id), ); } let reuses_window_proxy = self @@ -562,7 +562,6 @@ impl ScriptVm { context.local_window_id, ), ); - self.clear_context_wrapper_cache_for_context_ptr(&context.context, false); } { let mut host = self._context_host.borrow_mut(); @@ -570,7 +569,7 @@ impl ScriptVm { host.retire_window_realm_resources(context.runtime_observable_context_token); host.retire_window_execution_contexts_for_context_token( context.runtime_observable_context_token, - self.resource_owner_id, + Some(self.resource_owner_id), ); } } @@ -646,7 +645,7 @@ impl ScriptVm { host.retire_window_realm_resources(context.runtime_observable_context_token); host.retire_window_execution_contexts_for_context_token( context.runtime_observable_context_token, - self.resource_owner_id, + Some(self.resource_owner_id), ); } assert!( diff --git a/moli-renderer-v8/src/script_vm/initial_window_document.rs b/moli-renderer-v8/src/script_vm/initial_window_document.rs index 1f754b4cf1..a74a1e9d61 100644 --- a/moli-renderer-v8/src/script_vm/initial_window_document.rs +++ b/moli-renderer-v8/src/script_vm/initial_window_document.rs @@ -25,17 +25,23 @@ impl ScriptVm { } else { false }; - let inherited_domain = - inherited.map(|environment| environment.document_domain_override.get()); + let inherited_origin = inherited.and_then(|environment| { + let mut origin = environment.about_document_state.as_ref()?.origin().clone(); + if let crate::native_bridge::WindowAccessOrigin::Tuple { + document_domain, .. + } = &mut origin + { + *document_domain = environment.document_domain_override.get(); + } + Some(origin) + }); Ok(self ._context_host .borrow() .main_document_local_window_transition_for_commit( origin, inherited_opaque_origin_matches, - inherited_domain - .as_ref() - .and_then(|domain| domain.as_deref()), + inherited_origin.as_ref(), policy, )) } @@ -121,6 +127,12 @@ impl ScriptVm { environment.document_domain_override.clone(), ); } + host.set_main_about_document_state( + inherited + .as_ref() + .and_then(|environment| environment.about_document_state.clone()) + .or_else(|| env.about_document_state.clone()), + ); host.rebind_initial_main_document_senders( resource_completion, page_task_tx.top_level_navigation_handoff_sender(), @@ -161,19 +173,27 @@ impl ScriptVm { let scope = &mut scope.init(); let context = v8::Local::new(scope, context); let scope = &mut v8::ContextScope::new(scope, context); - crate::context_bootstrap::sync_global_location_runtime_state( - scope, - document_url.as_str(), - ); - let global = context.global(scope); - if let Some(document) = global - .get(scope, crate::util::v8str(scope, "document").into()) - .and_then(|value| v8::Local::::try_from(value).ok()) + let window = context.global(scope); { - crate::context_bootstrap::sync_document_location_runtime_state_from_window( - scope, document, global, + let mut host = vm._context_host.borrow_mut(); + crate::native_bridge::WindowEnvironmentSettings::bind_current_main_document(scope, &host) + .ok_or_else(|| anyhow!("retained Window has no current Document settings"))?; + let host_ptr = &mut *host as *mut JsContextHost; + let document = host.native_bridge_mut() + .wrap_handle(scope, host_ptr, document_handle) + .ok_or_else(|| anyhow!("retained Window Document could not be wrapped"))?; + crate::util::set_private_value( + scope, + window, + crate::context_bootstrap::WINDOW_DOCUMENT_SLOT, + document.into(), ); } + crate::context_bootstrap::reset_window_location_history_navigation_runtime_state( + scope, + window, + document_url.as_str(), + )?; Ok(()) })?; Ok(()) diff --git a/moli-renderer-v8/src/script_vm/popup_close.rs b/moli-renderer-v8/src/script_vm/popup_close.rs index 8fc4abc753..85bee67e6b 100644 --- a/moli-renderer-v8/src/script_vm/popup_close.rs +++ b/moli-renderer-v8/src/script_vm/popup_close.rs @@ -11,6 +11,28 @@ use crate::{ }; impl ScriptVm { + pub(crate) fn cancel_closed_main_window_navigation(&mut self) -> Result<()> { + if !self._context_host.borrow().browsing_context_is_closed() { + return Ok(()); + } + self.with_default_context_scope(|scope, host_ptr| { + let window = scope.get_current_context().global(scope); + // Closing from pagehide/unload must not settle the outgoing + // Document's Navigation API promises after its unload has begun. + if !unsafe { &*host_ptr }.main_document_unload_has_started() { + inform_about_canceled_navigation_for_window( + scope, + window, + NavigationCancellationReason::WindowClose, + ); + } + crate::native_bridge::JsContextHost::dispatch_main_document_unload_for_navigation_commit( + scope, host_ptr, + ); + Ok(()) + }) + } + pub(crate) fn current_popup_close_owner( &self, popup_id: u64, diff --git a/moli-renderer-v8/src/script_vm/post_parse.rs b/moli-renderer-v8/src/script_vm/post_parse.rs index 6afaf3beaf..08980ffa77 100644 --- a/moli-renderer-v8/src/script_vm/post_parse.rs +++ b/moli-renderer-v8/src/script_vm/post_parse.rs @@ -570,7 +570,8 @@ impl PendingWindowRealmBootstrapRegistration { realm_token: crate::native_bridge::RuntimeObservableContextToken, resource_owner_id: ResourceOwnerId, ) -> Result> { - let Some((owner, dispatch_scope, access_policy)) = mode.registration() else { + let Some((owner, dispatch_scope, access_policy)) = mode.registration(unsafe { &*host }) + else { return Ok(None); }; if !unsafe { &mut *host }.register_window_execution_context_realm( @@ -599,7 +600,7 @@ impl Drop for PendingWindowRealmBootstrapRegistration { if !self.committed { unsafe { &mut *self.host }.retire_window_execution_contexts_for_context_token( self.realm_token, - self.resource_owner_id, + Some(self.resource_owner_id), ); } } @@ -608,13 +609,20 @@ impl Drop for PendingWindowRealmBootstrapRegistration { impl WindowContextBootstrapMode { fn registration( self, + host: &JsContextHost, ) -> Option<( crate::native_bridge::WindowExecutionContextOwner, crate::native_bridge::OwnerDispatchScope, crate::native_bridge::WindowExecutionContextAccessPolicy, )> { match self { - Self::MainDefault => None, + Self::MainDefault => Some(( + host.current_window_execution_context_owner( + crate::native_bridge::OwnerDispatchScope::Top, + )?, + crate::native_bridge::OwnerDispatchScope::Top, + crate::native_bridge::WindowExecutionContextAccessPolicy::EnforceWebOrigin, + )), Self::PopupDefault { popup_id, local_window_id, diff --git a/moli-renderer-v8/src/script_vm/related_page.rs b/moli-renderer-v8/src/script_vm/related_page.rs index 6acaaf6278..e92432e1e6 100644 --- a/moli-renderer-v8/src/script_vm/related_page.rs +++ b/moli-renderer-v8/src/script_vm/related_page.rs @@ -1,5 +1,18 @@ use super::*; +impl ScriptVm { + pub(crate) fn complete_related_initial_empty_document(&mut self) -> Result<()> { + anyhow::ensure!( + self._context_host + .borrow_mut() + .complete_current_main_initial_empty_document(), + "related initial Document rejected synchronous completion" + ); + self.document_runtime.note_dom_content_loaded_dispatched(); + self.set_document_ready_state(crate::dom::native::DocumentReadyState::Complete) + } +} + impl ScriptVmDefaultWorldBootstrap { #[allow(clippy::too_many_arguments)] pub(crate) fn related_page_in_scope( @@ -43,6 +56,7 @@ impl ScriptVmDefaultWorldBootstrap { storage_key: None, origin: inherited_origin.clone(), policy_container: env.document_policy_container.clone(), + about_document_state: env.about_document_state.clone(), }), )?; // Noopener blank pages commit a navigation; only pages exposing an @@ -107,6 +121,7 @@ impl ScriptVmPageRealmBootstrap { root_frame_id, context_host, prebootstrapped_child_default_contexts, + popup_default_contexts, page_context_cancel_tx, post_domcontentloaded_page_task_tx, page_runtime_wake_tx, @@ -193,6 +208,7 @@ impl ScriptVmPageRealmBootstrap { root_frame_id, context_host, prebootstrapped_child_default_contexts, + popup_default_contexts, page_context_cancel_tx, post_domcontentloaded_page_task_tx, page_runtime_wake_tx, diff --git a/moli-renderer-v8/src/script_vm/runtime_bindings.rs b/moli-renderer-v8/src/script_vm/runtime_bindings.rs index 2fa3b08599..345e63a205 100644 --- a/moli-renderer-v8/src/script_vm/runtime_bindings.rs +++ b/moli-renderer-v8/src/script_vm/runtime_bindings.rs @@ -71,10 +71,7 @@ fn queue_agent_pending_promise_rejections(scope: &mut v8::PinScope<'_, '_>) -> u .get_slot_mut::() .map(|pending| std::mem::take(&mut pending.0)) .unwrap_or_default(); - pending - .into_iter() - .map(queue_promise_rejection_state) - .sum() + pending.into_iter().map(queue_promise_rejection_state).sum() } pub(super) fn promise_reject_dispatch_slot( diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs index e152ea9d33..e3cd1024f5 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/mod.rs @@ -115,8 +115,6 @@ mod window_open_named_targets; mod window_open_referrer; mod window_open_special_targets; mod worker_listener_invocation; - - mod error_event_init; mod message_event_init; mod window_current_event_private; diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/window_open_empty_url.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/window_open_empty_url.rs index 426463f4f7..504531b9a4 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/window_open_empty_url.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/window_open_empty_url.rs @@ -132,7 +132,14 @@ setTimeout(() => { "#, ) .unwrap(); - assert!(vm.take_pending_popup_activations().is_empty(), "{kind}"); + // Named reuse may publish a focus/activation record. Its browser + // handoff must explicitly prohibit navigation and another load. + assert!( + vm.take_pending_popup_activations() + .iter() + .all(|activation| !activation.navigation_requested()), + "{kind}" + ); advance_page_task_executor_until_eval_equals( &mut vm, &loader, diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs index 29c8293b8c..16c74ccd83 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/realms_and_teardown.rs @@ -3122,7 +3122,9 @@ async fn borrowed_cross_origin_window_accessors_follow_the_receiver() { } catch (error) { plain = error instanceof TypeError ? "type-error" : String(error && error.name); } - setLocation.call(window, "#pr957-receiver"); + // A relative fragment on srcdoc resolves against the inherited API base URL. + // Use its explicit URL to isolate which Window the borrowed setter navigates. + setLocation.call(window, "about:srcdoc#pr957-receiver"); return JSON.stringify({ plain, normalParentIsSelf: w.parent === w, diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 2e2aa4b274..f52d907f78 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -2252,7 +2252,6 @@ mod domrect_receiver_consolidation; mod dynamic_inline_scripts; mod element_click; mod eme_owner_interface_exposure_interfaces; -mod encoded_video_chunk_shell; mod encoded_video_chunk; mod event_receivers; mod extracted; diff --git a/moli-renderer-v8/src/window_host.rs b/moli-renderer-v8/src/window_host.rs index fd8d0a55a4..071da10237 100644 --- a/moli-renderer-v8/src/window_host.rs +++ b/moli-renderer-v8/src/window_host.rs @@ -917,10 +917,14 @@ pub(crate) fn window_post_message_callback<'s>( .as_ref() .map(|(identity, _, _)| *identity) .or_else(|| { - active_lightweight_popup_id(scope) - .map(PendingWindowMessageEndpoint::LightweightPopup) - .and_then(|endpoint| current_window_message_source_identity(scope, host, endpoint)) - .or_else(|| incumbent_window_message_source_identity(scope, host)) + incumbent_window_message_source_identity(scope, host) + .or_else(|| { + active_lightweight_popup_id(scope) + .map(PendingWindowMessageEndpoint::LightweightPopup) + .and_then(|endpoint| { + current_window_message_source_identity(scope, host, endpoint) + }) + }) .or_else(|| { host.current_window_message_source().and_then(|endpoint| { current_window_message_source_identity(scope, host, endpoint) @@ -1889,6 +1893,31 @@ fn dispatch_window_message_event<'s>( } } +fn top_window_message_source_for_target<'s>( + scope: &mut v8::PinScope<'s, '_>, + host: &JsContextHost, + target: PendingWindowMessageEndpoint, +) -> Option> { + let PendingWindowMessageEndpoint::ChildWindow(handle) = target else { + return host + .page_default_context(scope) + .map(|context| context.global(scope).into()); + }; + let top = host.child_browsing_context_top_window_for_current_realm(scope, handle); + if moli_trace::window_message_trace_enabled() { + tracing::info!( + target: "moli_window_message_trace", + handle = handle.index(), + relation_found = top.is_some(), + top_is_target_global = top.is_some_and(|top| { + top.strict_equals(scope.get_current_context().global(scope).into()) + }), + stage = "top_window_message_source_resolved", + ); + } + top.map(Into::into) +} + fn child_window_message_source<'s>( scope: &mut v8::PinScope<'s, '_>, host: &mut JsContextHost, diff --git a/moli-window-features/src/lib.rs b/moli-window-features/src/lib.rs index 686bd18392..4e8c18e8cb 100644 --- a/moli-window-features/src/lib.rs +++ b/moli-window-features/src/lib.rs @@ -144,11 +144,6 @@ impl WindowOpenFeatures { self.noreferrer } - /// Returns whether the features request an independent popup window. - pub fn is_popup(&self) -> bool { - self.is_popup - } - /// Returns the normalized enabled features used by the popup runtime. pub fn enabled_feature_strings(&self) -> Vec { let mut enabled = Vec::new();