Chrome · Prerender
CVE-2026-87442
Logic Error in Prerender
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/preloading/anchor_element_interaction_host_impl_unittest.cc |
modified | |
forcontent/browser/preloading/prerenderer_impl.cc |
modified | |
switchcontent/browser/preloading/prerenderer_impl.cc |
modified |
Files Changed
content/browser/preloading/anchor_element_interaction_host_impl.cccontent/browser/preloading/anchor_element_interaction_host_impl_unittest.cccontent/browser/preloading/prerenderer_impl.cc
Patch
From 8c63c26c65982f9951e81a10dc56affaff974a47 Mon Sep 17 00:00:00 2001
From: Zainab Rizvi <rizvis@google.com>
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 <nhiroki@chromium.org>
Commit-Queue: Zainab Rizvi <rizvis@google.com>
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/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.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(
Loading diff…
Regression Test / PoC
shipped with the fix
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
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page