tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 17c6bdb0c8cf7534561316bce0326aecab9e202e
parent 27ffb7a727e79483d9992944db5eb6477906b6c2
Author: Andreas Farre <farre@mozilla.com>
Date:   Wed, 15 Oct 2025 07:44:49 +0000

Bug 1993431 - Only consider cross document navigation for beforeunload. r=smaug

Differential Revision: https://phabricator.services.mozilla.com/D268124

Diffstat:
Mdocshell/shistory/nsSHistory.cpp | 84++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mdom/ipc/ContentChild.cpp | 27++++++++++++++++++++-------
Mdom/ipc/ContentChild.h | 6++++++
Mdom/ipc/PContent.ipdl | 4++++
Mdom/ipc/WindowGlobalParent.cpp | 11++++++++++-
5 files changed, 91 insertions(+), 41 deletions(-)

diff --git a/docshell/shistory/nsSHistory.cpp b/docshell/shistory/nsSHistory.cpp @@ -1465,43 +1465,40 @@ static bool MaybeCheckUnloadingIsCanceled( return false; } - // Step 4.3.2 + RefPtr<CanonicalBrowsingContext> traversable = aTraversable->Canonical(); + + RefPtr<WindowGlobalParent> windowGlobalParent = + traversable->GetCurrentWindowGlobal(); + // An efficiency trick. We've set this flag on the window context if we've + // seen a "navigate" and/or a "beforeunload" handler set. If not we know we + // can skip this. + if (!windowGlobalParent || (!windowGlobalParent->NeedsBeforeUnload() && + !windowGlobalParent->GetNeedsTraverse())) { + return false; + } + + // Step 4.2 auto found = std::find_if(aLoadResults.begin(), aLoadResults.end(), - [traversable = RefPtr{aTraversable}](const auto& result) { + [traversable](const auto& result) { return result.mBrowsingContext->Id() == traversable->Id(); }); - // Step 4.3.2 - bool needsBeforeUnload = found != aLoadResults.end(); - // Step 4.2 - // This is a bit fishy since we don't have a direct way of performing - // the step, but this does its best. - RefPtr<nsDocShellLoadState> loadState = - needsBeforeUnload ? found->mLoadState : nullptr; - RefPtr<CanonicalBrowsingContext> browsingContext = aTraversable->Canonical(); - MOZ_DIAGNOSTIC_ASSERT(!needsBeforeUnload || - found->mBrowsingContext == browsingContext); + // Step 4.3, since current entry is always different to not finding one. + if (found == aLoadResults.end()) { + return false; + } - nsCOMPtr<SessionHistoryEntry> currentEntry = - browsingContext->GetActiveSessionHistoryEntry(); - // If we didn't find a load state, it means that traversable stays at the - // current entry. + // Step 4.2 nsCOMPtr<SessionHistoryEntry> targetEntry = - loadState ? do_QueryInterface(loadState->SHEntry()) : currentEntry; + do_QueryInterface(found->mLoadState->SHEntry()); - // Step 4.3 - if (!currentEntry || currentEntry->GetID() == targetEntry->GetID()) { - return false; - } + nsCOMPtr<SessionHistoryEntry> currentEntry = + traversable->GetActiveSessionHistoryEntry(); - RefPtr<WindowGlobalParent> windowGlobalParent = - browsingContext->GetCurrentWindowGlobal(); - // An efficiency trick. We've set this flag on the window context if we've - // seen a "navigate" and/or a "beforeunload" handler set. If not we know we - // can skip this. - if (!windowGlobalParent || (!windowGlobalParent->NeedsBeforeUnload() && - !windowGlobalParent->GetNeedsTraverse())) { + // Step 4.3, but the actual checks in the spec. + if (!currentEntry || !targetEntry || + currentEntry->GetID() == targetEntry->GetID()) { return false; } @@ -1518,6 +1515,17 @@ static bool MaybeCheckUnloadingIsCanceled( return false; } + // Step 4.3.2 + // If we squint we can see spec here, insofar that for a traversable's + // beforeunload handler to fire, the target entry needs to be: + // * non-null, i.e. part of navigables being traversed + // * different from the current entry + // * cross document from the current entry + // * have beforeunload handlers + bool needsBeforeUnload = + windowGlobalParent->NeedsBeforeUnload() && + currentEntry->SharedInfo() != targetEntry->SharedInfo(); + // Step 4.3.3 isn't needed since that's what PermitUnloadChildNavigables // achieves by skipping top level navigable. @@ -1525,10 +1533,10 @@ static bool MaybeCheckUnloadingIsCanceled( // PermitUnloadTraversable only includes the process of the top level browsing // context. - // If we don't have any unload handlers registered, we still need to run - // navigate event handlers, but we don't need to show the prompt. + // If we're not going to run any beforeunload handlers, we still need to run + // navigate event handlers for the traversable. nsIDocumentViewer::PermitUnloadAction action = - windowGlobalParent->NeedsBeforeUnload() + needsBeforeUnload ? nsIDocumentViewer::PermitUnloadAction::ePrompt : nsIDocumentViewer::PermitUnloadAction::eDontPromptAndUnload; windowGlobalParent->PermitUnloadTraversable( @@ -1536,11 +1544,21 @@ static bool MaybeCheckUnloadingIsCanceled( [action, loadResults = CopyableTArray(std::move(aLoadResults)), windowGlobalParent, aResolver](nsIDocumentViewer::PermitUnloadResult aResult) mutable { - if (aResult != nsIDocumentViewer::eContinue) { + if (aResult != nsIDocumentViewer::PermitUnloadResult::eContinue) { aResolver(loadResults, aResult); return; } + // If the traversable didn't have beforeunloadun handlers, we won't run + // other navigable's unload handlers either. That will be handled by + // regular navigation. + if (action == + nsIDocumentViewer::PermitUnloadAction::eDontPromptAndUnload) { + aResolver(loadResults, + nsIDocumentViewer::PermitUnloadResult::eContinue); + return; + } + // PermitUnloadTraversable includes everything except the process of the // top level browsing context. windowGlobalParent->PermitUnloadChildNavigables( @@ -1610,7 +1628,7 @@ void nsSHistory::LoadURIs(const nsTArray<LoadEntryResult>& aLoadResults, return; } - // There's no unload handlers, resolve immediately. + // There's no beforeunload handlers, resolve immediately. aResolver(NS_OK); // And we fall back to the simple case if we shouldn't fire a "traverse" diff --git a/dom/ipc/ContentChild.cpp b/dom/ipc/ContentChild.cpp @@ -4367,13 +4367,6 @@ mozilla::ipc::IPCResult ContentChild::RecvDispatchBeforeUnloadToSubtree( return IPC_OK(); } -mozilla::ipc::IPCResult ContentChild::RecvInitNextGenLocalStorageEnabled( - const bool& aEnabled) { - mozilla::dom::RecvInitNextGenLocalStorageEnabled(aEnabled); - - return IPC_OK(); -} - /* static */ void ContentChild::DispatchBeforeUnloadToSubtree( BrowsingContext* aStartingAt, const mozilla::Maybe<SessionHistoryInfo>& aInfo, @@ -4421,6 +4414,26 @@ mozilla::ipc::IPCResult ContentChild::RecvInitNextGenLocalStorageEnabled( } } +mozilla::ipc::IPCResult ContentChild::RecvDispatchNavigateToTraversable( + const MaybeDiscarded<BrowsingContext>& aTraversable, + const mozilla::Maybe<SessionHistoryInfo>& aInfo, + DispatchNavigateToTraversableResolver&& aResolver) { + if (aTraversable.IsNullOrDiscarded() || !aTraversable->GetDocShell()) { + aResolver(nsIDocumentViewer::eContinue); + } else { + RefPtr docShell = nsDocShell::Cast(aTraversable->GetDocShell()); + aResolver(docShell->MaybeFireTraversableTraverseHistory(*aInfo, Nothing())); + } + return IPC_OK(); +} + +mozilla::ipc::IPCResult ContentChild::RecvInitNextGenLocalStorageEnabled( + const bool& aEnabled) { + mozilla::dom::RecvInitNextGenLocalStorageEnabled(aEnabled); + + return IPC_OK(); +} + mozilla::ipc::IPCResult ContentChild::RecvGoBack( const MaybeDiscarded<BrowsingContext>& aContext, const Maybe<int32_t>& aCancelContentJSEpoch, bool aRequireUserInteraction, diff --git a/dom/ipc/ContentChild.h b/dom/ipc/ContentChild.h @@ -789,6 +789,12 @@ class ContentChild final : public PContentChild, const mozilla::Maybe<SessionHistoryInfo>& aInfo, DispatchBeforeUnloadToSubtreeResolver&& aResolver); + MOZ_CAN_RUN_SCRIPT_BOUNDARY + mozilla::ipc::IPCResult RecvDispatchNavigateToTraversable( + const MaybeDiscarded<BrowsingContext>& aTraversable, + const mozilla::Maybe<SessionHistoryInfo>& aInfo, + DispatchNavigateToTraversableResolver&& aResolver); + mozilla::ipc::IPCResult RecvInitNextGenLocalStorageEnabled( const bool& aEnabled); diff --git a/dom/ipc/PContent.ipdl b/dom/ipc/PContent.ipdl @@ -1056,6 +1056,10 @@ child: async DispatchBeforeUnloadToSubtree(MaybeDiscardedBrowsingContext aStartingAt, SessionHistoryInfo? info) returns (PermitUnloadResult result); + // Only dispatches "navigate" to the traversable. Used in case we don't want to dispatch beforeunload. + async DispatchNavigateToTraversable(MaybeDiscardedBrowsingContext aTraversable, SessionHistoryInfo? info) + returns (PermitUnloadResult result); + // Update the cached list of codec supported in the given process. async UpdateMediaCodecsSupported(RemoteMediaIn aLocation, MediaCodecsSupported aSupported); diff --git a/dom/ipc/WindowGlobalParent.cpp b/dom/ipc/WindowGlobalParent.cpp @@ -41,6 +41,7 @@ #include "mozilla/dom/JSWindowActorBinding.h" #include "mozilla/dom/JSWindowActorParent.h" #include "mozilla/dom/MediaController.h" +#include "mozilla/dom/Navigation.h" #include "mozilla/dom/NavigatorLogin.h" #include "mozilla/dom/PBackgroundSessionStorageCache.h" #include "mozilla/dom/UseCounterMetrics.h" @@ -833,9 +834,17 @@ class CheckPermitUnloadRequest final : public PromiseNativeHandler, // If `aInfo` is passed, only dispatch to the content process of the top // level window. if (aInfo) { + MOZ_DIAGNOSTIC_ASSERT(Navigation::IsAPIEnabled()); ContentParent* cp = mWGP->GetContentParent(); mPendingRequests++; - cp->SendDispatchBeforeUnloadToSubtree(bc, aInfo, resolve, reject); + // Here eDontPromptAndUnload means that we ignore beforeunload handlers, + // but we still need to handle the traversable navigate handler. + if (mAction == + nsIDocumentViewer::PermitUnloadAction::eDontPromptAndUnload) { + cp->SendDispatchNavigateToTraversable(bc, aInfo, resolve, reject); + } else { + cp->SendDispatchBeforeUnloadToSubtree(bc, aInfo, resolve, reject); + } } else { bc->PreOrderWalk([&](dom::BrowsingContext* aBC) { if (WindowGlobalParent* wgp =