CVE-2026-87538
Overview
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
RenderWidgetHostViewBasemethod 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, returningboolfor success. - Coordinate poisoning
- a compromised embedding renderer omitting or corrupting a child’s
FrameSinkIdin 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.
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
- Set up a cross-origin OOPIF
An attacker embeds a cross-origin child frame (the test uses
a.comembedding ab.comframe at/touch_selection.html) and gets the child frame to become the active touch-selection client so itsTouchSelectionControllerClientChildFrameservices selection requests. - 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 withviz::FrameSinkId(0xDEAD, 0xBEEF)), so the root→child transform can no longer resolve the child’s region. - Trigger a selection request
A touch selection operation (e.g.
SelectBetweenCoordinates(base_root, extent_root)) causesConvertFromRootto callTransformPointToCoordSpaceForView, which now returnsfalse; pre-patch, the return was ignored and the raw root-space coordinate was returned instead of a child-space point. - 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
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
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc |
modified | |
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 |
Files Changed
content/browser/renderer_host/input/touch_selection_controller_client_child_frame.hcontent/browser/renderer_host/input/touch_selection_controller_client_child_frame.cccontent/browser/renderer_host/input/touch_selection_controller_client_aura_browsertest.cc
Audit Directions
- Ignored transform return valuesSearch for callers of
TransformPointToCoordSpaceForView(and sibling coordinate-mapping helpers) that discard theboolresult and then use the out-parameter, since a failed transform can leave it holding untransformed source coordinates. - Untrusted out-parameters on failureAudit 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 dataReview 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.
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_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,
Regression Test / PoC
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,