Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactClickjacking in Input
DescriptionClickjacking in Input
ComponentInput
Bug ClassLogic Error
Tracker522399466
Fix commit98e65064c29f (chromium/src) +178/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
for
content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
modified
if
content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
modified
if
content/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.cc
  • content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc
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.