Chrome · Input
CVE-2026-87538
Logic Error in Input
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forcontent/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc |
modified | |
ifcontent/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc |
modified | |
ifcontent/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc |
modified |
Files Changed
content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cccontent/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc
Patch
From 98e65064c29f441b7d16eb3ad511cd867b2c3c56 Mon Sep 17 00:00:00 2001
From: Tzarial <zork@google.com>
Date: Wed, 12 Aug 2026 09:03:58 -0700
Subject: [PATCH] [agy][content] Drop bad touch selection events
TouchSelectionControllerClientChildFrame::ConvertFromRoot ignored
the return value of TransformPointToCoordSpaceForView. If the
transform failed, it returned untransformed root coordinates,
which were sent to the child renderer. A compromised embedder
could exploit this to retarget selection.
This CL fixes this by returning std::optional from ConvertFromRoot
and dropping the selection request if the transform fails.
--gtest_filter=*ConvertFromRootIgnoresTransformFailure*
Fixed: 522399466
Test: content_browsertests
Change-Id: Ic8b912f654d3588b630817e0c3a391381e4f270f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8107057
Commit-Queue: Tzarial <zork@chromium.org>
Reviewed-by: Aman Verma <amanvr@google.com>
Cr-Commit-Position: refs/heads/main@{#1678077}
---
diff --git a/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc b/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
index 06e0452..d43b105 100644
--- a/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
+++ b/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
@@ -17,6 +17,10 @@
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
+#include "components/viz/common/hit_test/aggregated_hit_test_region.h"
+#include "components/viz/common/hit_test/hit_test_query.h"
+#include "components/viz/host/host_frame_sink_manager.h"
+#include "content/browser/compositor/surface_utils.h"
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/browser/renderer_host/render_widget_host_view_event_handler.h"
@@ -223,6 +227,10 @@
return active_menu_client_;
}
+ bool ActiveClientIsInternal() const {
+ return active_client_ == &internal_client_;
+ }
+
bool IsMagnifierVisible() const {
return touch_selection_magnifier_ != nullptr;
}
@@ -805,6 +813,128 @@
final_start_handle_position - initial_start_handle_position);
}
+// Test for https://crbug.com/522399466.
+// Verifies that when a coordinate transform fails (e.g. if the embedding
+// renderer omits the child's FrameSinkId from the hit-test data), the child
+// frame's TouchSelectionControllerClient does not fall back to root-space
+// coordinates, which could lead to coordinate redirection.
+IN_PROC_BROWSER_TEST_P(
+ TouchSelectionControllerClientAuraSiteIsolationTest,
+ ConvertFromRootIgnoresTransformFailure_PoisonedHitTestRegion) {
+ // Step 1: Navigate to page with cross-origin OOPIF.
+ GURL test_url(embedded_test_server()->GetURL(
+ "a.com", "/cross_site_iframe_factory.html?a(a)"));
+ ASSERT_TRUE(NavigateToURL(shell(), test_url));
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ TestNavigationObserver observer(shell()->web_contents());
+ ASSERT_EQ(1u, root->child_count());
+ FrameTreeNode* child = root->child_at(0);
+
+ InitSelectionController(true);
+
+ GURL child_url(
+ embedded_test_server()->GetURL("b.com", "/touch_selection.html"));
+ ASSERT_TRUE(NavigateToURLFromRenderer(child, child_url));
+ child = root->child_at(0);
+ WaitForHitTestData(child->current_frame_host());
+
+ RenderWidgetHostViewChildFrame* child_view =
+ static_cast<RenderWidgetHostViewChildFrame*>(
+ child->current_frame_host()->GetRenderWidgetHost()->GetView());
+ RenderWidgetHostViewAura* parent_view = GetRenderWidgetHostViewAura();
+
+ // Step 2: Trigger selection in the child frame to make it the active client.
+ ui::test::EventGenerator generator(
+ parent_view->GetNativeView()->GetRootWindow());
+ const gfx::PointF child_text_point =
+ GetPointInTextInFrame(child->current_frame_host(), /*cursor_index=*/2);
+ const gfx::Point press_point = ConvertPointFromChildFrame(
+ child->current_frame_host(), generator.delegate(), child_text_point);
+ SelectWithLongPress(generator, press_point);
+ generator.ReleaseTouch();
+
+ ASSERT_EQ(ui::TouchSelectionController::ActiveStatus::kSelectionActive,
+ parent_view->selection_controller()->active_status());
+ ASSERT_FALSE(selection_controller_client()->ActiveClientIsInternal())
+ << "OOPIF child-frame client must be the active touch-selection client";
+
+ // Record the iframe origin in root coords and the root-space points.
+ // All child->root transforms are computed BEFORE poisoning.
+ const gfx::PointF child_origin_in_root =
+ child_view->TransformPointToRootCoordSpaceF(gfx::PointF());
+ const gfx::PointF base_child =
+ GetPointInTextInFrame(child->current_frame_host(), /*cursor_index=*/0);
+ const gfx::PointF extent_child =
+ GetPointInTextInFrame(child->current_frame_host(), /*cursor_index=*/4);
+ const gfx::PointF base_root =
+ child_view->TransformPointToRootCoordSpaceF(base_child);
+ const gfx::PointF extent_root =
+ child_view->TransformPointToRootCoordSpaceF(extent_child);
+ ASSERT_GT(child_origin_in_root.x(), 5.f);
+ ASSERT_GT(child_origin_in_root.y(), 5.f);
+
+ // Sanity: with un-poisoned data, root->child transform succeeds.
+ {
+ gfx::PointF out;
+ bool ok = parent_view->TransformPointToCoordSpaceForView(base_root,
+ child_view, &out);
+ ASSERT_TRUE(ok);
+ ASSERT_EQ(gfx::ToRoundedPoint(base_child), gfx::ToRoundedPoint(out));
+ }
+
+ // Step 3: Poison the hit-test data by replacing the child's FrameSinkId.
+ // This simulates a compromised parent renderer omitting the child's ID.
+ const viz::FrameSinkId child_fsid = child_view->GetFrameSinkId();
+ const viz::FrameSinkId root_fsid = parent_view->GetRootFrameSinkId();
+ ASSERT_TRUE(child_fsid.is_valid());
+ ASSERT_TRUE(root_fsid.is_valid());
+
+ const auto& query_map = GetHostFrameSinkManager()->GetDisplayHitTestQuery();
+ auto it = query_map.find(root_fsid);
+ ASSERT_NE(it, query_map.end());
+ viz::HitTestQuery* query = it->second.get();
+
+ std::vector<viz::AggregatedHitTestRegion> poisoned = query->GetHitTestData();
+ bool replaced = false;
+ for (auto& region : poisoned) {
+ if (region.frame_sink_id == child_fsid) {
+ region.frame_sink_id = viz::FrameSinkId(0xDEAD, 0xBEEF);
+ replaced = true;
+ }
+ }
+ ASSERT_TRUE(replaced) << "child FrameSinkId not found in aggregated data";
+ query->OnAggregatedHitTestRegionListUpdated(poisoned);
+
+ // Step 4: Verify that the transform now fails.
+ gfx::PointF out_after(-1, -1);
+ const bool ok_after = parent_view->TransformPointToCoordSpaceForView(
+ base_root, child_view, &out_after);
+ EXPECT_FALSE(ok_after) << "transform must fail when child FrameSinkId is "
+ "missing from aggregated hit-test data";
+ EXPECT_EQ(gfx::ToRoundedPoint(base_root), gfx::ToRoundedPoint(out_after))
+ << "on failure the out-param holds the untransformed root coordinate";
+ EXPECT_NE(gfx::ToRoundedPoint(base_child), gfx::ToRoundedPoint(out_after));
+
+ // Step 5: Try to drag selection handles. The transform failure should cause
+ // the selection request to be dropped, so the selection should remain
+ // unchanged.
+ static_cast<ui::TouchSelectionControllerClient*>(
+ selection_controller_client())
+ ->SelectBetweenCoordinates(base_root, extent_root);
+
+ // The handle drag was over B-local [0,4) of the textDiv, i.e. the word
+ // "Some". If the bug is fixed, the coordinate conversion fails and the
+ // selection request is dropped, so the selection should remain "Some".
+ std::string selected =
+ EvalJs(child->current_frame_host(), "window.getSelection().toString()")
+ .ExtractString();
+ EXPECT_EQ("Some", selected);
+ EXPECT_EQ(ui::TouchSelectionController::ActiveStatus::kInactive,
+ parent_view->selection_controller()->active_status());
+}
+
// Tests that the selection handles in a child view have their bounds updated
// when the main view is resized.
IN_PROC_BROWSER_TEST_P(TouchSelectionControllerClientAuraSiteIsolationTest,
diff --git a/content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc b/content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc
index b1dcbcf4..f21c7af 100644
--- a/content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc
+++ b/content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc
@@ -138,13 +138,24 @@
TransformSelectionBoundsAndUpdate();
}
-gfx::Point TouchSelectionControllerClientChildFrame::ConvertFromRoot(
+std::optional<gfx::Point>
+TouchSelectionControllerClientChildFrame::ConvertFromRoot(
const gfx::PointF& point_f) const {
- gfx::PointF transformed_point(point_f);
RenderWidgetHostViewBase* root_view = rwhv_->GetRootRenderWidgetHostView();
- if (root_view) {
- root_view->TransformPointToCoordSpaceForView(point_f, rwhv_,
- &transformed_point);
+ if (!root_view) {
+ return std::nullopt;
+ }
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc b/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
index 06e0452..d43b105 100644
--- a/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
+++ b/content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
@@ -17,6 +17,10 @@
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
+#include "components/viz/common/hit_test/aggregated_hit_test_region.h"
+#include "components/viz/common/hit_test/hit_test_query.h"
+#include "components/viz/host/host_frame_sink_manager.h"
+#include "content/browser/compositor/surface_utils.h"
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#include "content/browser/renderer_host/render_widget_host_view_child_frame.h"
#include "content/browser/renderer_host/render_widget_host_view_event_handler.h"
@@ -223,6 +227,10 @@
return active_menu_client_;
}
+ bool ActiveClientIsInternal() const {
+ return active_client_ == &internal_client_;
+ }
+
bool IsMagnifierVisible() const {
return touch_selection_magnifier_ != nullptr;
}
@@ -805,6 +813,128 @@
final_start_handle_position - initial_start_handle_position);
}
+// Test for https://crbug.com/522399466.
+// Verifies that when a coordinate transform fails (e.g. if the embedding
+// renderer omits the child's FrameSinkId from the hit-test data), the child
+// frame's TouchSelectionControllerClient does not fall back to root-space
+// coordinates, which could lead to coordinate redirection.
+IN_PROC_BROWSER_TEST_P(
+ TouchSelectionControllerClientAuraSiteIsolationTest,
+ ConvertFromRootIgnoresTransformFailure_PoisonedHitTestRegion) {
+ // Step 1: Navigate to page with cross-origin OOPIF.
+ GURL test_url(embedded_test_server()->GetURL(
+ "a.com", "/cross_site_iframe_factory.html?a(a)"));
+ ASSERT_TRUE(NavigateToURL(shell(), test_url));
+ FrameTreeNode* root = static_cast<WebContentsImpl*>(shell()->web_contents())
+ ->GetPrimaryFrameTree()
+ .root();
+ TestNavigationObserver observer(shell()->web_contents());
+ ASSERT_EQ(1u, root->child_count());
+ FrameTreeNode* child = root->child_at(0);
+
+ InitSelectionController(true);
+
+ GURL child_url(
+ embedded_test_server()->GetURL("b.com", "/touch_selection.html"));
+ ASSERT_TRUE(NavigateToURLFromRenderer(child, child_url));
+ child = root->child_at(0);
+ WaitForHitTestData(child->current_frame_host());
+
+ RenderWidgetHostViewChildFrame* child_view =
+ static_cast<RenderWidgetHostViewChildFrame*>(
+ child->current_frame_host()->GetRenderWidgetHost()->GetView());
+ RenderWidgetHostViewAura* parent_view = GetRenderWidgetHostViewAura();
+
+ // Step 2: Trigger selection in the child frame to make it the active client.
+ ui::test::EventGenerator generator(
+ parent_view->GetNativeView()->GetRootWindow());
+ const gfx::PointF child_text_point =
+ GetPointInTextInFrame(child->current_frame_host(), /*cursor_index=*/2);
+ const gfx::Point press_point = ConvertPointFromChildFrame(
+ child->current_frame_host(), generator.delegate(), child_text_point);
+ SelectWithLongPress(generator, press_point);
+ generator.ReleaseTouch();
+
+ ASSERT_EQ(ui::TouchSelectionController::ActiveStatus::kSelectionActive,
+ parent_view->selection_controller()->active_status());
+ ASSERT_FALSE(selection_controller_client()->ActiveClientIsInternal())
+ << "OOPIF child-frame client must be the active touch-selection client";
+
+ // Record the iframe origin in root coords and the root-space points.
+ // All child->root transforms are computed BEFORE poisoning.
+ const gfx::PointF child_origin_in_root =
+ child_view->TransformPointToRootCoordSpaceF(gfx::PointF());
+ const gfx::PointF base_child =
+ GetPointInTextInFrame(child->current_frame_host(), /*cursor_index=*/0);
+ const gfx::PointF extent_child =
+ GetPointInTextInFrame(child->current_frame_host(), /*cursor_index=*/4);
+ const gfx::PointF base_root =
+ child_view->TransformPointToRootCoordSpaceF(base_child);
+ const gfx::PointF extent_root =
+ child_view->TransformPointToRootCoordSpaceF(extent_child);
+ ASSERT_GT(child_origin_in_root.x(), 5.f);
+ ASSERT_GT(child_origin_in_root.y(), 5.f);
+
+ // Sanity: with un-poisoned data, root->child transform succeeds.
+ {
+ gfx::PointF out;
+ bool ok = parent_view->TransformPointToCoordSpaceForView(base_root,
+ child_view, &out);
+ ASSERT_TRUE(ok);
+ ASSERT_EQ(gfx::ToRoundedPoint(base_child), gfx::ToRoundedPoint(out));
+ }
+
+ // Step 3: Poison the hit-test data by replacing the child's FrameSinkId.
+ // This simulates a compromised parent renderer omitting the child's ID.
+ const viz::FrameSinkId child_fsid = child_view->GetFrameSinkId();
+ const viz::FrameSinkId root_fsid = parent_view->GetRootFrameSinkId();
+ ASSERT_TRUE(child_fsid.is_valid());
+ ASSERT_TRUE(root_fsid.is_valid());
+
+ const auto& query_map = GetHostFrameSinkManager()->GetDisplayHitTestQuery();
+ auto it = query_map.find(root_fsid);
+ ASSERT_NE(it, query_map.end());
+ viz::HitTestQuery* query = it->second.get();
+
+ std::vector<viz::AggregatedHitTestRegion> poisoned = query->GetHitTestData();
+ bool replaced = false;
+ for (auto& region : poisoned) {
+ if (region.frame_sink_id == child_fsid) {
+ region.frame_sink_id = viz::FrameSinkId(0xDEAD, 0xBEEF);
+ replaced = true;
+ }
+ }
+ ASSERT_TRUE(replaced) << "child FrameSinkId not found in aggregated data";
+ query->OnAggregatedHitTestRegionListUpdated(poisoned);
+
+ // Step 4: Verify that the transform now fails.
+ gfx::PointF out_after(-1, -1);
+ const bool ok_after = parent_view->TransformPointToCoordSpaceForView(
+ base_root, child_view, &out_after);
+ EXPECT_FALSE(ok_after) << "transform must fail when child FrameSinkId is "
+ "missing from aggregated hit-test data";
+ EXPECT_EQ(gfx::ToRoundedPoint(base_root), gfx::ToRoundedPoint(out_after))
+ << "on failure the out-param holds the untransformed root coordinate";
+ EXPECT_NE(gfx::ToRoundedPoint(base_child), gfx::ToRoundedPoint(out_after));
+
+ // Step 5: Try to drag selection handles. The transform failure should cause
+ // the selection request to be dropped, so the selection should remain
+ // unchanged.
+ static_cast<ui::TouchSelectionControllerClient*>(
+ selection_controller_client())
+ ->SelectBetweenCoordinates(base_root, extent_root);
+
+ // The handle drag was over B-local [0,4) of the textDiv, i.e. the word
+ // "Some". If the bug is fixed, the coordinate conversion fails and the
+ // selection request is dropped, so the selection should remain "Some".
+ std::string selected =
+ EvalJs(child->current_frame_host(), "window.getSelection().toString()")
+ .ExtractString();
+ EXPECT_EQ("Some", selected);
+ EXPECT_EQ(ui::TouchSelectionController::ActiveStatus::kInactive,
+ parent_view->selection_controller()->active_status());
+}
+
// Tests that the selection handles in a child view have their bounds updated
// when the main view is resized.
IN_PROC_BROWSER_TEST_P(TouchSelectionControllerClientAuraSiteIsolationTest,
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