Low watch CVSS 4.2 chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactClickjacking in Input
DescriptionClickjacking in Input
ComponentInput
Sub-componentContent
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker522399466
Fix commit98e65064c29f (chromium/src) +178/-12
CWECWE-1021
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-06-10 (per vendor advisory)
Fix landed2026-08-12 (public git)
Disclosed2026-09-08
Patch gap27 days — fix public in source before the advisory

Background

OOPIF (out-of-process iframe)
a cross-origin child frame rendered by a separate renderer process, whose surface is composited into a parent frame owned by a potentially different renderer.
`TouchSelectionControllerClientChildFrame`
the browser-process client that services touch text-selection requests (caret moves, range selection) on behalf of a child frame’s RenderWidgetHostViewChildFrame (rwhv_).
`TransformPointToCoordSpaceForView`
a RenderWidgetHostViewBase method that maps a point from the root view’s coordinate space into a target child view’s space by walking the renderer-supplied aggregated hit-test region list, returning bool for success.
Coordinate poisoning
a compromised embedding renderer omitting or corrupting a child’s FrameSinkId in the hit-test data so the root→child transform fails.

Root Cause Analysis

The pre-patch TouchSelectionControllerClientChildFrame::ConvertFromRoot seeded a local transformed_point with the incoming root-space point_f, then called root_view->TransformPointToCoordSpaceForView(point_f, rwhv_, &transformed_point) while discarding its bool return value. When the transform failed, the out-parameter was left holding the original root coordinate, and the function returned that value as though it were a valid child-space point — the removed lines and the added comment (the out-param is left holding the *root* coordinate) confirm this was the old behavior. That untransformed point flowed unconditionally into host_delegate->MoveCaret, host_delegate->MoveRangeSelectionExtent, and host_delegate->SelectRange, so a selection request meant to land at one place in the child frame could instead be applied at the raw root coordinate. The invariant violated is that a point dispatched to the child renderer must actually be in that child’s coordinate space.

The fix changes ConvertFromRoot to return std::optional<gfx::Point>, returning std::nullopt both when root_view is null and when TransformPointToCoordSpaceForView returns false; each caller now checks the optional and, if empty, calls HideAndDisallowShowingAutomatically() and returns instead of dispatching, so no untransformed coordinate ever reaches the child renderer.

Key insight
The single core mistake is ignoring the success/failure return value of TransformPointToCoordSpaceForView, causing a failed transform to silently fall back to root-space coordinates; the fix propagates that failure as std::nullopt and makes callers drop (and hide) the selection request rather than act on an untransformed point.

Attack Path

  1. Set up a cross-origin OOPIF An attacker embeds a cross-origin child frame (the test uses a.com embedding a b.com frame at /touch_selection.html) and gets the child frame to become the active touch-selection client so its TouchSelectionControllerClientChildFrame services selection requests.
  2. Poison the hit-test data A compromised embedding (parent) renderer supplies aggregated hit-test data that omits or replaces the child’s FrameSinkId (the test replaces it with viz::FrameSinkId(0xDEAD, 0xBEEF)), so the root→child transform can no longer resolve the child’s region.
  3. Trigger a selection request A touch selection operation (e.g. SelectBetweenCoordinates(base_root, extent_root)) causes ConvertFromRoot to call TransformPointToCoordSpaceForView, which now returns false; pre-patch, the return was ignored and the raw root-space coordinate was returned instead of a child-space point.
  4. Redirect the selection The untransformed root coordinate is dispatched to the child renderer via host_delegate->SelectRange / MoveCaret / MoveRangeSelectionExtent, retargeting the selection to attacker-chosen coordinates; the resulting user-visible selection redirection is the clickjacking/coordinate-redirection impact described by the advisory. (inferred) — the diff proves the untransformed point is dispatched but does not itself demonstrate any further escalation.

Impact Assessment

An attacker in control of the embedding renderer can, when the hit-test transform fails, cause touch text-selection requests to be applied at untransformed root-space coordinates in a cross-origin child frame rather than the intended child-space location, i.e. redirect where a selection or caret lands — consistent with the advisory’s stated impact of “Clickjacking in Input.” The precondition is an active touch text-selection session over an OOPIF and a compromised/malicious embedding renderer able to supply poisoned aggregated hit-test data. The advisory classifies this as a LogicError clickjacking issue in the Input component and does not report exploitation in the wild; any escalation beyond selection/coordinate redirection is (inferred) and not demonstrated by the patch.

Changed Functions

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

Files Changed

  • content/browser/renderer_host/input/touch_selection_controller_client_child_frame.h
  • content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc
  • content/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc

Audit Directions

  • Ignored transform return values
    Search for callers of TransformPointToCoordSpaceForView (and sibling coordinate-mapping helpers) that discard the bool result and then use the out-parameter, since a failed transform can leave it holding untransformed source coordinates.
  • Untrusted out-parameters on failure
    Audit APIs that write an out-param only on success but leave a caller-supplied fallback in place on failure; ensure callers treat failure as “drop the request” rather than “use the input value.”
  • Renderer-supplied hit-test / coordinate data
    Review browser-process paths that transform points through renderer-controlled aggregated hit-test data (FrameSinkId, AggregatedHitTestRegion), verifying a compromised embedder cannot force a fallback that retargets input into a cross-origin child frame.
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_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;
+  }
+
+  gfx::PointF transformed_point(point_f);
+  // The root->child transform walks the renderer-supplied aggregated
+  // HitTestRegionList. If the (potentially compromised) embedding renderer
+  // omitted this child's FrameSinkId, the transform fails and the out-param
+  // is left holding the *root* coordinate. Drop the request in that case
+  // rather than dispatching root coordinates to the child renderer.
+  // See https://crbug.com/522399466
+  if (!root_view->TransformPointToCoordSpaceForView(point_f, rwhv_,
+                                                    &transformed_point)) {
+    return std::nullopt;
   }
 
   return gfx::ToRoundedPoint(transformed_point);
@@ -161,24 +172,48 @@
 void TouchSelectionControllerClientChildFrame::MoveCaret(
     const gfx::PointF& position) {
   RenderWidgetHostDelegate* host_delegate = rwhv_->host()->delegate();
-  if (host_delegate)
-    host_delegate->MoveCaret(ConvertFromRoot(position));
+  if (!host_delegate) {
+    return;
+  }
+  std::optional<gfx::Point> child_position = ConvertFromRoot(position);
+  if (!child_position) {
+    manager_->GetTouchSelectionController()
+        ->HideAndDisallowShowingAutomatically();
+    return;
+  }
+  host_delegate->MoveCaret(*child_position);
 }
 
 void TouchSelectionControllerClientChildFrame::MoveRangeSelectionExtent(
     const gfx::PointF& extent) {
   RenderWidgetHostDelegate* host_delegate = rwhv_->host()->delegate();
-  if (host_delegate)
-    host_delegate->MoveRangeSelectionExtent(ConvertFromRoot(extent));
+  if (!host_delegate) {
+    return;
+  }
+  std::optional<gfx::Point> child_extent = ConvertFromRoot(extent);
+  if (!child_extent) {
+    manager_->GetTouchSelectionController()
+        ->HideAndDisallowShowingAutomatically();
+    return;
+  }
+  host_delegate->MoveRangeSelectionExtent(*child_extent);
 }
 
 void TouchSelectionControllerClientChildFrame::SelectBetweenCoordinates(
     const gfx::PointF& base,
     const gfx::PointF& extent) {
   RenderWidgetHostDelegate* host_delegate = rwhv_->host()->delegate();
-  if (host_delegate) {
-    host_delegate->SelectRange(ConvertFromRoot(base), ConvertFromRoot(extent));
+  if (!host_delegate) {
+    return;
   }
+  std::optional<gfx::Point> child_base = ConvertFromRoot(base);
+  std::optional<gfx::Point> child_extent = ConvertFromRoot(extent);
+  if (!child_base || !child_extent) {
+    manager_->GetTouchSelectionController()
+        ->HideAndDisallowShowingAutomatically();
+    return;
+  }
+  host_delegate->SelectRange(*child_base, *child_extent);
 }
 
 void TouchSelectionControllerClientChildFrame::OnSelectionEvent(
diff --git a/content/browser/renderer_host/input/touch_selection_controller_client_child_frame.h b/content/browser/renderer_host/input/touch_selection_controller_client_child_frame.h
index 4d112ce7..74b30ffc 100644
--- a/content/browser/renderer_host/input/touch_selection_controller_client_child_frame.h
+++ b/content/browser/renderer_host/input/touch_selection_controller_client_child_frame.h
@@ -6,6 +6,7 @@
 #define CONTENT_BROWSER_RENDERER_HOST_INPUT_TOUCH_SELECTION_CONTROLLER_CLIENT_CHILD_FRAME_H_
 
 #include <memory>
+#include <optional>
 
 #include "base/memory/raw_ptr.h"
 #include "components/viz/common/quads/selection.h"
@@ -70,7 +71,7 @@
   bool ShouldShowQuickMenu(bool can_paste) override;
   std::u16string GetSelectedText() override;
 
-  gfx::Point ConvertFromRoot(const gfx::PointF& point) const;
+  std::optional<gfx::Point> ConvertFromRoot(const gfx::PointF& point) const;
 
   // Not owned, non-null for the lifetime of this object.
   raw_ptr<RenderWidgetHostViewChildFrame> rwhv_;
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…

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.