CVE-2026-87442
Overview
Background
- `PrerendererImpl`
- a content-browser object, owned by
PreloadingDeciderasDocumentUserData, that turns speculation candidates into prerender/prefetch requests on behalf of a specific initiating document bound viarender_frame_host_. - Back/forward cache (BFCache)
- the mechanism that keeps a navigated-away document alive but inactive so it can be restored; a BFCached frame returns
falsefromRenderFrameHost::IsActive(). - `GetPrimaryMainFrame()` vs `GetOutermostMainFrame()`
GetPrimaryMainFrame()returns the tab’s current primary main frame, which after a navigation is a different document than the one that owns thePrerendererImpl, whereasrender_frame_host_->GetOutermostMainFrame()stays attached to the initiating document.- `PrefetchDocumentManager`
- per-document bookkeeping for prefetch-ahead-of-prerender, fetched with
GetOrCreateForCurrentDocument(frame).
Root Cause Analysis
PrerendererImpl outlives its owning document being the primary main frame — for example while that document sits in the back/forward cache — yet MaybePrerender and its helpers kept running and resolved the “current” document via WebContents::FromRenderFrameHost(...)->GetPrimaryMainFrame(). After the initiating document navigated away, that call returned a different, now-primary document, so prefetch-ahead-of-prerender bookkeeping (PrefetchDocumentManager::GetOrCreateForCurrentDocument) and the page UKM source id were attached to and acted upon on behalf of the wrong document — a confused-deputy condition where a stale/inactive initiator drives preloading against another page.
The fix stops honoring calls from an inactive bound frame: MaybePrerender returns false early when !render_frame_host_->IsActive(), and CHECK(render_frame_host_->IsActive()) is asserted at the top of the candidate-processing path and in OnLCPPredicted. It also rebinds all bookkeeping to the initiating document by replacing web_contents->GetPrimaryMainFrame() with render_frame_host_->GetOutermostMainFrame(), so prefetch state and UKM ids stay attached to the initiator. The companion AnchorElementInteractionHostImpl handlers add an if (!render_frame_host().IsActive()) return; guard so pointer/viewport IPCs from an inactive frame no longer feed PreloadingDecider. The removed CHECK(!render_frame_host_->IsInLifecycleState(kPrerendering)) was replaced by the broader active-frame gate.
GetPrimaryMainFrame() while the PrerendererImpl was bound to a possibly-inactive initiator, letting a stale frame act as a confused deputy against whatever document is now primary; the fix ignores calls from inactive frames and re-anchors bookkeeping to render_frame_host_->GetOutermostMainFrame().Attack Path
- Establish a prerendering initiator
A page with speculation rules / anchor-interaction heuristics creates a
PrerendererImplbound to itsrender_frame_host_viaPreloadingDecider. - Navigate away into BFCache
The user (or script-induced navigation) moves that document into the back/forward cache, so
render_frame_host_->IsActive()is nowfalsewhile thePrerendererImpland pending candidates remain alive. - Deliver a candidate/interaction from the inactive frame
A queued or renderer-sent speculation candidate reaches
MaybePrerender(or anAnchorElementInteractionHostImplpointer/viewport IPC arrives) while the frame is inactive. - Bookkeeping attaches to the wrong document
Pre-patch,
WebContents::FromRenderFrameHost(...)->GetPrimaryMainFrame()resolves to the now-primary (different) document, soPrefetchDocumentManagerstate and the page UKM source id are created/mutated against that other document. - Confused-deputy preloading (inferred) — The stale initiator thereby drives prefetch/prerender bookkeeping on behalf of a document that never requested it; the patch demonstrates the misattribution but no further memory-corruption or data-read primitive.
Impact Assessment
Prerender path. Preconditions are a document that registers speculation/anchor-interaction preloading and then becomes inactive while candidates are still pending. The advisory classifies this as an AuthorizationError / confused deputy in Prerender and reports no in-the-wild exploitation; any consequence beyond misattributed bookkeeping is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/preloading/prerenderer_impl.cc |
modified | |
switchcontent/browser/preloading/prerenderer_impl.cc |
modified | |
ifcontent/browser/preloading/prerenderer_impl.cc |
modified | |
TEST_Fcontent/browser/preloading/anchor_element_interaction_host_impl_unittest.cc |
modified | |
PrerendererBackForwardCacheTestcontent/browser/preloading/prerenderer_impl_browsertest.cc |
modified | |
PrerendererBackForwardCacheTestcontent/browser/preloading/prerenderer_impl_browsertest.cc |
modified | |
PrerendererImplBrowserTestPrefetchAheadcontent/browser/preloading/prerenderer_impl_browsertest.cc |
modified | |
ifcontent/browser/preloading/prerenderer_impl_browsertest.cc |
modified | |
TEST_Fcontent/browser/preloading/prerenderer_impl_unittest.cc |
modified |
Files Changed
content/browser/preloading/prerenderer_impl_unittest.cccontent/browser/preloading/anchor_element_interaction_host_impl_unittest.cccontent/browser/preloading/anchor_element_interaction_host_impl.cccontent/browser/preloading/prerenderer_impl_browsertest.cccontent/browser/preloading/prerenderer_impl.cc
Audit Directions
- `GetPrimaryMainFrame()` from a bound objectFlag any
DocumentUserData-owned or frame-bound object that resolves “the current document” viaWebContents::GetPrimaryMainFrame()instead of its ownrender_frame_host_, since the two diverge after navigation/BFCache. - Missing `IsActive()` gate on IPC handlersAudit mojo host implementations bound to a
RenderFrameHostthat act onPreloadingDecider/preloading state without first checkingrender_frame_host().IsActive(). - Objects that outlive frame primary-nessReview lifecycles where a browser-side helper can persist past its document leaving the primary main frame (BFCache, pending prerender) and still processes queued candidates or callbacks.
Patch
From 8c63c26c65982f9951e81a10dc56affaff974a47 Mon Sep 17 00:00:00 2001 From: Zainab Rizvi <[email protected]> Date: Fri, 14 Aug 2026 10:56:14 -0700 Subject: [PATCH] Prerenderer: use initiator RFH and ignore inactive frames PrerendererImpl is owned by PreloadingDecider (DocumentUserData) and is bound to a specific document via render_frame_host_. It can therefore outlive that document being the primary main frame, e.g. when the document is in the back/forward cache. Obtain the PrefetchDocumentManager and the page UKM source id from render_frame_host_ rather than GetPrimaryMainFrame() so that prefetch-ahead-of-prerender bookkeeping stays attached to the initiating document, and ignore MaybePrerender() calls when the bound frame is no longer active. Bug: 497443419 Change-Id: I0d3ae61523570aedd984163175e92debe4a56486 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8217707 Reviewed-by: Hiroki Nakagawa <[email protected]> Commit-Queue: Zainab Rizvi <[email protected]> Cr-Commit-Position: refs/heads/main@{#1679687} --- diff --git a/content/browser/preloading/anchor_element_interaction_host_impl.cc b/content/browser/preloading/anchor_element_interaction_host_impl.cc index 6f7d7a3a..e0f1471 100644 --- a/content/browser/preloading/anchor_element_interaction_host_impl.cc +++ b/content/browser/preloading/anchor_element_interaction_host_impl.cc @@ -140,6 +140,9 @@ void AnchorElementInteractionHostImpl::OnPointerDown(const GURL& url, bool renderer_enacted) { + if (!render_frame_host().IsActive()) { + return; + } auto* preloading_decider = PreloadingDecider::GetOrCreateForCurrentDocument(&render_frame_host()); preloading_decider->OnPointerDown(url, renderer_enacted); @@ -151,6 +154,9 @@ const GURL& url, blink::mojom::AnchorElementPointerDataPtr mouse_data, bool renderer_enacted) { + if (!render_frame_host().IsActive()) { + return; + } auto* preloading_decider = PreloadingDecider::GetOrCreateForCurrentDocument(&render_frame_host()); preloading_decider->OnPointerHover(url, std::move(mouse_data), @@ -162,6 +168,9 @@ const GURL& url, blink::mojom::AnchorElementPointerDataPtr mouse_data, bool renderer_enacted) { + if (!render_frame_host().IsActive()) { + return; + } auto* preloading_decider = PreloadingDecider::GetOrCreateForCurrentDocument(&render_frame_host()); preloading_decider->OnPointerHover( @@ -174,6 +183,9 @@ void AnchorElementInteractionHostImpl::OnModerateViewportHeuristicTriggered( const GURL& url, bool renderer_enacted) { + if (!render_frame_host().IsActive()) { + return; + } if (!base::FeatureList::IsEnabled( blink::features::kPreloadingModerateViewportHeuristics)) { ReportBadMessageAndDeleteThis( @@ -191,6 +203,9 @@ void AnchorElementInteractionHostImpl::OnEagerViewportHeuristicTriggered( std::vector<blink::mojom::AnchorElementInteractionTargetPtr> targets) { + if (!render_frame_host().IsActive()) { + return; + } if (!base::FeatureList::IsEnabled( blink::features::kPreloadingEagerViewportHeuristics)) { ReportBadMessageAndDeleteThis( diff --git a/content/browser/preloading/prerenderer_impl.cc b/content/browser/preloading/prerenderer_impl.cc index 1538de3a..ca75f15 100644 --- a/content/browser/preloading/prerenderer_impl.cc +++ b/content/browser/preloading/prerenderer_impl.cc @@ -168,6 +168,8 @@ return; } + CHECK(render_frame_host_->IsActive()); + // Extract only the candidates which apply to prerender, and sort them by URL // so we can efficiently compare them to `started_prerenders_`. // If both prerender and prerender-until-script are applied to the same URL, @@ -317,11 +319,9 @@ PrerenderFinalStatus::kSpeculationRuleRemoved)); if (base::FeatureList::IsEnabled( features::kPrerender2FallbackPrefetchSpecRules)) { - WebContents* web_contents = - WebContents::FromRenderFrameHost(&render_frame_host_.get()); auto* prefetch_document_manager = content::PrefetchDocumentManager::GetOrCreateForCurrentDocument( - web_contents->GetPrimaryMainFrame()); + render_frame_host_->GetOutermostMainFrame()); for (const auto& [url, preloading_type] : to_be_cancelled_prerender_list) { prefetch_document_manager->ResetPrefetchAheadOfPrerenderIfExist( preloading_type, url); @@ -352,6 +352,7 @@ } void PrerendererImpl::OnLCPPredicted() { + CHECK(render_frame_host_->IsActive()); blocked_ = false; for (auto& [candidate, enacting_predictor, confidence] : std::move(blocked_candidates_)) { @@ -363,6 +364,10 @@ const blink::mojom::SpeculationCandidatePtr& candidate, const PreloadingPredictor& enacting_predictor, PreloadingConfidence confidence) { + if (!render_frame_host_->IsActive()) { + return false; + } + // Check actions. Only Prerender and PrerenderUntilScript are allowed. switch (candidate->action) { case blink::mojom::SpeculationAction::kPrerender: @@ -392,10 +397,6 @@ return false; } - // Prerendering frames should not trigger any prerender request. - CHECK(!render_frame_host_->IsInLifecycleState( - RenderFrameHost::LifecycleState::kPrerendering)); - if (!registry_) { return false; } @@ -529,7 +530,7 @@ auto* prefetch_document_manager = content::PrefetchDocumentManager::GetOrCreateForCurrentDocument( - web_contents->GetPrimaryMainFrame()); + render_frame_host_->GetOutermostMainFrame()); prefetch_document_manager->PrefetchAheadOfPrerender( attributes.preload_pipeline_info, candidate.Clone(), enacting_predictor); @@ -547,7 +548,8 @@ creating_predictor, enacting_predictor, ConvertSpeculationActionToPreloadingType(candidate->action), std::move(same_url_matcher), - web_contents->GetPrimaryMainFrame()->GetPageUkmSourceId())); + render_frame_host_->GetOutermostMainFrame() + ->GetPageUkmSourceId())); preloading_attempt->SetSpeculationEagerness(candidate->eagerness); return registry_->CreateAndStartHost(attributes, preloading_attempt); } diff --git a/content/browser/preloading/anchor_element_interaction_host_impl_unittest.cc b/content/browser/preloading/anchor_element_interaction_host_impl_unittest.cc index b556fc6b..ba2123a 100644 --- a/content/browser/preloading/anchor_element_interaction_host_impl_unittest.cc +++ b/content/browser/preloading/anchor_element_interaction_host_impl_unittest.cc @@ -213,5 +213,66 @@ PredictorConfusionMatrix::kFalseNegative, 1); } +TEST_F(AnchorElementInteractionHostImplTest, IgnoreMessagesWhenInactive) { + base::test::ScopedFeatureList feature_list; + feature_list.InitWithFeatures( + {blink::features::kPreloadingModerateViewportHeuristics, + blink::features::kPreloadingEagerViewportHeuristics}, + /*disabled_features=*/{}); + + auto* render_frame_host = static_cast<RenderFrameHostImpl*>(main_rfh()); + + std::vector<blink::mojom::SpeculationCandidatePtr> candidates; + PreloadingDecider::GetOrCreateForCurrentDocument(render_frame_host) + ->UpdateSpeculationCandidates(candidates); + + mojo::Remote<blink::mojom::AnchorElementInteractionHost> remote; + AnchorElementInteractionHostImpl::Create(render_frame_host, + remote.BindNewPipeAndPassReceiver()); + + ScopedPreloadingDeciderObserver observer(render_frame_host); + auto* preloading_data = + PreloadingDataImpl::GetOrCreateForWebContents(web_contents()); + + render_frame_host->SetLifecycleState( + RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache); + EXPECT_FALSE(render_frame_host->IsActive()); + + const auto kUrl = GURL("https://example.com/page1.html"); + + // 1. OnPointerDown + remote->OnPointerDown(kUrl, /*renderer_enacted=*/false); + remote.FlushForTesting(); + EXPECT_FALSE(observer.on_pointer_down_url_.has_value()); + + // 2. OnPointerHoverModerate + remote->OnPointerHoverModerate( + kUrl, blink::mojom::AnchorElementPointerData::New(false, 0.0, 0.0), + /*renderer_enacted=*/false); + remote.FlushForTesting(); + EXPECT_FALSE(observer.on_pointer_hover_url_.has_value()); + + // 3. OnPointerHoverEager + remote->OnPointerHoverEager( + kUrl, blink::mojom::AnchorElementPointerData::New(false, 0.0, 0.0), + /*renderer_enacted=*/false); + remote.FlushForTesting(); + EXPECT_FALSE(observer.on_pointer_hover_url_.has_value()); + + // 4. OnModerateViewportHeuristicTriggered + remote->OnModerateViewportHeuristicTriggered(kUrl, + /*renderer_enacted=*/false); + remote.FlushForTesting(); + EXPECT_EQ(preloading_data->GetPredictionsSizeForTesting(), 0u); + + // 5. OnEagerViewportHeuristicTriggered + std::vector<blink::mojom::AnchorElementInteractionTargetPtr> targets; + targets.push_back(blink::mojom::AnchorElementInteractionTarget::New( + kUrl, /*renderer_enacted=*/false)); + remote->OnEagerViewportHeuristicTriggered(std::move(targets)); + remote.FlushForTesting(); + EXPECT_EQ(preloading_data->GetPredictionsSizeForTesting(), 0u); +} + } // namespace } // namespace content diff --git a/content/browser/preloading/prerenderer_impl_browsertest.cc b/content/browser/preloading/prerenderer_impl_browsertest.cc index f750517..111ef485f 100644 --- a/content/browser/preloading/prerenderer_impl_browsertest.cc +++ b/content/browser/preloading/prerenderer_impl_browsertest.cc @@ -7,6 +7,7 @@ #include "base/strings/utf_string_conversions.h" #include "base/test/scoped_feature_list.h" #include "base/test/test_future.h" +#include "content/browser/preloading/prefetch/prefetch_document_manager.h" #include "content/browser/preloading/prefetch/prefetch_features.h" #include "content/browser/preloading/prefetch/prefetch_match_resolver.h" #include "content/browser/preloading/prefetch/prefetch_service.h" @@ -20,6 +21,7 @@ #include "content/browser/preloading/prerender/prerender_host_registry.h" #include "content/public/browser/web_contents_delegate.h" #include "content/public/common/content_client.h" +#include "content/public/common/content_features.h" #include "content/public/common/isolated_world_ids.h" #include "content/public/test/browser_test.h" #include "content/public/test/content_browser_test.h" @@ -230,6 +232,17 @@ } }; +class PrerendererBackForwardCacheTest : public PrerendererImplBrowserTestBase { + public: + PrerendererBackForwardCacheTest() { + feature_list_.InitWithFeatures( + {features::kBackForwardCache, + features::kPrerender2FallbackPrefetchSpecRules}, + {features::kBackForwardCacheMemoryControls, + blink::features::kLCPTimingPredictorPrerender2}); + } +}; + class PrerendererImplBrowserTestPrefetchAhead : public PrerendererImplBrowserTestBase, public ::testing::WithParamInterface< @@ -1773,5 +1786,41 @@ ASSERT_EQ(expected, GetObservedRequests()); } +// Tests that PrerendererImpl::MaybePrerender ignores candidates from an +// inactive frame in the back/forward cache. +IN_PROC_BROWSER_TEST_F(PrerendererBackForwardCacheTest, + MaybePrerenderIgnoredWhenInactive) { + EXPECT_TRUE(NavigateToURL(shell(), GetUrl("/empty.html"))); + RenderFrameHostImpl* rfh_a = static_cast<RenderFrameHostImpl*>( + web_contents_impl().GetPrimaryMainFrame()); + const GlobalRenderFrameHostId rfh_id = rfh_a->GetGlobalId(); + PrerendererImpl& prerenderer_a = GetPrerendererImpl(); + + // Navigate away so that rfh_a enters the back/forward cache. + EXPECT_TRUE(NavigateToURL(shell(), GetCrossSiteUrl("/empty.html"))); + rfh_a = static_cast<RenderFrameHostImpl*>(RenderFrameHost::FromID(rfh_id)); + EXPECT_TRUE(rfh_a); + if (!rfh_a) { + return; + } + EXPECT_TRUE(rfh_a->IsInLifecycleState( + RenderFrameHost::LifecycleState::kInBackForwardCache)); + EXPECT_FALSE(rfh_a->IsActive()); + + const GURL prerender_url = GetCrossSiteUrl("/title1.html"); + blink::mojom::SpeculationCandidatePtr candidate = + CreateSpeculationCandidate(prerender_url); + + PreloadingPredictor enacting_predictor = GetPredictorForPreloadingTriggerType( + PreloadingTriggerType::kSpeculationRule); + EXPECT_FALSE(prerenderer_a.MaybePrerender(candidate, enacting_predictor, + PreloadingConfidence{100})); + + RenderFrameHostImpl* rfh_b = static_cast<RenderFrameHostImpl*>( + web_contents_impl().GetPrimaryMainFrame()); + EXPECT_NE(rfh_a, rfh_b); + EXPECT_FALSE(PrefetchDocumentManager::GetForCurrentDocument(rfh_b)); +} + } // namespace } // namespace content diff --git a/content/browser/preloading/prerenderer_impl_unittest.cc b/content/browser/preloading/prerenderer_impl_unittest.cc index efb8f9a4..5903913 100644 --- a/content/browser/preloading/prerenderer_impl_unittest.cc +++ b/content/browser/preloading/prerenderer_impl_unittest.cc @@ -355,5 +355,25 @@ EXPECT_TRUE(registry->FindHostByUrlForTesting(kPrerenderingUrl)); } +// Tests that PrerendererImpl::MaybePrerender ignores preloading attempts when +// the associated frame is no longer active (e.g., in back/forward cache). +TEST_F(PrerendererTest, MaybePrerenderIgnoredWhenInactive) { + PrerenderHostRegistry* registry = GetPrerenderHostRegistry(); + PrerendererImpl prerenderer(*GetRenderFrameHost()); + + GetRenderFrameHost()->SetLifecycleState( + RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache); + EXPECT_FALSE(GetRenderFrameHost()->IsActive()); + + const GURL kPrerenderingUrl = GetSameOriginUrl("/empty.html"); + const auto candidate = CreatePrerenderCandidate(kPrerenderingUrl); + + EXPECT_FALSE(prerenderer.MaybePrerender( + candidate, content_preloading_predictor::kSpeculationRules, + PreloadingConfidence{100})); + EXPECT_FALSE(prerenderer.ShouldWaitForPrerenderResult(kPrerenderingUrl)); + EXPECT_FALSE(registry->FindHostByUrlForTesting(kPrerenderingUrl)); +} + } // namespace } // namespace content
Regression Test / PoC
diff --git a/content/browser/preloading/anchor_element_interaction_host_impl_unittest.cc b/content/browser/preloading/anchor_element_interaction_host_impl_unittest.cc
index b556fc6b..ba2123a 100644
--- a/content/browser/preloading/anchor_element_interaction_host_impl_unittest.cc
+++ b/content/browser/preloading/anchor_element_interaction_host_impl_unittest.cc
@@ -213,5 +213,66 @@
PredictorConfusionMatrix::kFalseNegative, 1);
}
+TEST_F(AnchorElementInteractionHostImplTest, IgnoreMessagesWhenInactive) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitWithFeatures(
+ {blink::features::kPreloadingModerateViewportHeuristics,
+ blink::features::kPreloadingEagerViewportHeuristics},
+ /*disabled_features=*/{});
+
+ auto* render_frame_host = static_cast<RenderFrameHostImpl*>(main_rfh());
+
+ std::vector<blink::mojom::SpeculationCandidatePtr> candidates;
+ PreloadingDecider::GetOrCreateForCurrentDocument(render_frame_host)
+ ->UpdateSpeculationCandidates(candidates);
+
+ mojo::Remote<blink::mojom::AnchorElementInteractionHost> remote;
+ AnchorElementInteractionHostImpl::Create(render_frame_host,
+ remote.BindNewPipeAndPassReceiver());
+
+ ScopedPreloadingDeciderObserver observer(render_frame_host);
+ auto* preloading_data =
+ PreloadingDataImpl::GetOrCreateForWebContents(web_contents());
+
+ render_frame_host->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+ EXPECT_FALSE(render_frame_host->IsActive());
+
+ const auto kUrl = GURL("https://example.com/page1.html");
+
+ // 1. OnPointerDown
+ remote->OnPointerDown(kUrl, /*renderer_enacted=*/false);
+ remote.FlushForTesting();
+ EXPECT_FALSE(observer.on_pointer_down_url_.has_value());
+
+ // 2. OnPointerHoverModerate
+ remote->OnPointerHoverModerate(
+ kUrl, blink::mojom::AnchorElementPointerData::New(false, 0.0, 0.0),
+ /*renderer_enacted=*/false);
+ remote.FlushForTesting();
+ EXPECT_FALSE(observer.on_pointer_hover_url_.has_value());
+
+ // 3. OnPointerHoverEager
+ remote->OnPointerHoverEager(
+ kUrl, blink::mojom::AnchorElementPointerData::New(false, 0.0, 0.0),
+ /*renderer_enacted=*/false);
+ remote.FlushForTesting();
+ EXPECT_FALSE(observer.on_pointer_hover_url_.has_value());
+
+ // 4. OnModerateViewportHeuristicTriggered
+ remote->OnModerateViewportHeuristicTriggered(kUrl,
+ /*renderer_enacted=*/false);
+ remote.FlushForTesting();
+ EXPECT_EQ(preloading_data->GetPredictionsSizeForTesting(), 0u);
+
+ // 5. OnEagerViewportHeuristicTriggered
+ std::vector<blink::mojom::AnchorElementInteractionTargetPtr> targets;
+ targets.push_back(blink::mojom::AnchorElementInteractionTarget::New(
+ kUrl, /*renderer_enacted=*/false));
+ remote->OnEagerViewportHeuristicTriggered(std::move(targets));
+ remote.FlushForTesting();
+ EXPECT_EQ(preloading_data->GetPredictionsSizeForTesting(), 0u);
+}
+
} // namespace
} // namespace content
diff --git a/content/browser/preloading/prerenderer_impl_browsertest.cc b/content/browser/preloading/prerenderer_impl_browsertest.cc
index f750517..111ef485f 100644
--- a/content/browser/preloading/prerenderer_impl_browsertest.cc
+++ b/content/browser/preloading/prerenderer_impl_browsertest.cc
@@ -7,6 +7,7 @@
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
+#include "content/browser/preloading/prefetch/prefetch_document_manager.h"
#include "content/browser/preloading/prefetch/prefetch_features.h"
#include "content/browser/preloading/prefetch/prefetch_match_resolver.h"
#include "content/browser/preloading/prefetch/prefetch_service.h"
@@ -20,6 +21,7 @@
#include "content/browser/preloading/prerender/prerender_host_registry.h"
#include "content/public/browser/web_contents_delegate.h"
#include "content/public/common/content_client.h"
+#include "content/public/common/content_features.h"
#include "content/public/common/isolated_world_ids.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
@@ -230,6 +232,17 @@
}
};
+class PrerendererBackForwardCacheTest : public PrerendererImplBrowserTestBase {
+ public:
+ PrerendererBackForwardCacheTest() {
+ feature_list_.InitWithFeatures(
+ {features::kBackForwardCache,
+ features::kPrerender2FallbackPrefetchSpecRules},
+ {features::kBackForwardCacheMemoryControls,
+ blink::features::kLCPTimingPredictorPrerender2});
+ }
+};
+
class PrerendererImplBrowserTestPrefetchAhead
: public PrerendererImplBrowserTestBase,
public ::testing::WithParamInterface<
@@ -1773,5 +1786,41 @@
ASSERT_EQ(expected, GetObservedRequests());
}
+// Tests that PrerendererImpl::MaybePrerender ignores candidates from an
+// inactive frame in the back/forward cache.
+IN_PROC_BROWSER_TEST_F(PrerendererBackForwardCacheTest,
+ MaybePrerenderIgnoredWhenInactive) {
+ EXPECT_TRUE(NavigateToURL(shell(), GetUrl("/empty.html")));
+ RenderFrameHostImpl* rfh_a = static_cast<RenderFrameHostImpl*>(
+ web_contents_impl().GetPrimaryMainFrame());
+ const GlobalRenderFrameHostId rfh_id = rfh_a->GetGlobalId();
+ PrerendererImpl& prerenderer_a = GetPrerendererImpl();
+
+ // Navigate away so that rfh_a enters the back/forward cache.
+ EXPECT_TRUE(NavigateToURL(shell(), GetCrossSiteUrl("/empty.html")));
+ rfh_a = static_cast<RenderFrameHostImpl*>(RenderFrameHost::FromID(rfh_id));
+ EXPECT_TRUE(rfh_a);
+ if (!rfh_a) {
+ return;
+ }
+ EXPECT_TRUE(rfh_a->IsInLifecycleState(
+ RenderFrameHost::LifecycleState::kInBackForwardCache));
+ EXPECT_FALSE(rfh_a->IsActive());
+
+ const GURL prerender_url = GetCrossSiteUrl("/title1.html");
+ blink::mojom::SpeculationCandidatePtr candidate =
+ CreateSpeculationCandidate(prerender_url);
+
+ PreloadingPredictor enacting_predictor = GetPredictorForPreloadingTriggerType(
+ PreloadingTriggerType::kSpeculationRule);
+ EXPECT_FALSE(prerenderer_a.MaybePrerender(candidate, enacting_predictor,
+ PreloadingConfidence{100}));
+
+ RenderFrameHostImpl* rfh_b = static_cast<RenderFrameHostImpl*>(
+ web_contents_impl().GetPrimaryMainFrame());
+ EXPECT_NE(rfh_a, rfh_b);
+ EXPECT_FALSE(PrefetchDocumentManager::GetForCurrentDocument(rfh_b));
+}
+
} // namespace
} // namespace content
diff --git a/content/browser/preloading/prerenderer_impl_unittest.cc b/content/browser/preloading/prerenderer_impl_unittest.cc
index efb8f9a4..5903913 100644
--- a/content/browser/preloading/prerenderer_impl_unittest.cc
+++ b/content/browser/preloading/prerenderer_impl_unittest.cc
@@ -355,5 +355,25 @@
EXPECT_TRUE(registry->FindHostByUrlForTesting(kPrerenderingUrl));
}
+// Tests that PrerendererImpl::MaybePrerender ignores preloading attempts when
+// the associated frame is no longer active (e.g., in back/forward cache).
+TEST_F(PrerendererTest, MaybePrerenderIgnoredWhenInactive) {
+ PrerenderHostRegistry* registry = GetPrerenderHostRegistry();
+ PrerendererImpl prerenderer(*GetRenderFrameHost());
+
+ GetRenderFrameHost()->SetLifecycleState(
+ RenderFrameHostImpl::LifecycleStateImpl::kInBackForwardCache);
+ EXPECT_FALSE(GetRenderFrameHost()->IsActive());
+
+ const GURL kPrerenderingUrl = GetSameOriginUrl("/empty.html");
+ const auto candidate = CreatePrerenderCandidate(kPrerenderingUrl);
+
+ EXPECT_FALSE(prerenderer.MaybePrerender(
+ candidate, content_preloading_predictor::kSpeculationRules,
+ PreloadingConfidence{100}));
+ EXPECT_FALSE(prerenderer.ShouldWaitForPrerenderResult(kPrerenderingUrl));
+ EXPECT_FALSE(registry->FindHostByUrlForTesting(kPrerenderingUrl));
+}
+
} // namespace
} // namespace content