Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Input
DescriptionImproper input validation in Input
ComponentInput
Bug ClassLogic Error
Tracker497456156
Fix commit65494ad63c86 (chromium/src) +170/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TransformSelectionBoundsAndUpdate
content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc
modified
if
content/browser/renderer_host/render_widget_host_view_child_frame.cc
modified
TestTouchSelectionControllerClientManager
content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc
  • content/browser/renderer_host/render_widget_host_view_child_frame.cc
  • content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
From 65494ad63c8668d6cebb76fb15ea3105592a5e09 Mon Sep 17 00:00:00 2001
From: Zhenyao Mo <zmo@chromium.org>
Date: Thu, 16 Jul 2026 08:57:32 -0700
Subject: [PATCH] Clamp subframe touch selection bounds to subframe view bounds

A compromised child frame renderer can send selection metadata with
coordinates outside its visual boundaries. When transforming these
bounds to the root view coordinate space without validation, the browser
process could display the Touch Selection Quick Menu at spoofed
coordinates outside the reporting frame's visual area.

This CL updates TouchSelectionControllerClientChildFrame to clamp all
reported selection bounds to the local view geometry of the child frame
before transforming them to root coordinate space, preventing
out-of-bounds menu placement.

Bug: 497456156
Test: RenderWidgetHostViewChildFrameTest.SelectionBoundsClampedToViewBounds
Change-Id: I1b5304b7233f0bafa67599917e0d59ab5b504990
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8100702
Commit-Queue: Zhenyao Mo <zmo@chromium.org>
Reviewed-by: Dave Tapuska <dtapuska@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1663152}
---

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 b16b601..b1dcbcf4 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
@@ -4,6 +4,8 @@
 
 #include "content/browser/renderer_host/input/touch_selection_controller_client_child_frame.h"
 
+#include <algorithm>
+
 #include "base/check.h"
 #include "base/notreached.h"
 #include "content/browser/renderer_host/render_widget_host_delegate.h"
@@ -16,11 +18,33 @@
 #include "ui/base/mojom/menu_source_type.mojom.h"
 #include "ui/base/ui_base_features.h"
 #include "ui/gfx/geometry/point_conversions.h"
+#include "ui/gfx/geometry/rect_f.h"
+#include "ui/gfx/geometry/size_f.h"
 #include "ui/strings/grit/ui_strings.h"
 #include "ui/touch_selection/touch_editing_controller.h"
 
 namespace content {
 
+namespace {
+
+gfx::PointF ClampPointToRect(const gfx::PointF& point, const gfx::RectF& rect) {
+  return gfx::PointF(std::clamp(point.x(), rect.x(), rect.right()),
+                     std::clamp(point.y(), rect.y(), rect.bottom()));
+}
+
+gfx::SelectionBound ClampSelectionBoundToRect(const gfx::SelectionBound& bound,
+                                              const gfx::RectF& rect) {
+  gfx::SelectionBound clamped_bound(bound);
+  clamped_bound.SetEdge(ClampPointToRect(bound.edge_start(), rect),
+                        ClampPointToRect(bound.edge_end(), rect));
+  clamped_bound.SetVisibleEdge(
+      ClampPointToRect(bound.visible_edge_start(), rect),
+      ClampPointToRect(bound.visible_edge_end(), rect));
+  return clamped_bound;
+}
+
+}  // namespace
+
 TouchSelectionControllerClientChildFrame::
     TouchSelectionControllerClientChildFrame(
         RenderWidgetHostViewChildFrame* rwhv,
@@ -54,27 +78,34 @@
 
 void TouchSelectionControllerClientChildFrame::
     TransformSelectionBoundsAndUpdate() {
-  gfx::SelectionBound transformed_selection_start(selection_start_);
-  gfx::SelectionBound transformed_selection_end(selection_end_);
+  gfx::RectF local_bounds(gfx::SizeF(rwhv_->GetViewBounds().size()));
+  gfx::SelectionBound clamped_selection_start =
+      ClampSelectionBoundToRect(selection_start_, local_bounds);
+  gfx::SelectionBound clamped_selection_end =
+      ClampSelectionBoundToRect(selection_end_, local_bounds);
+  gfx::SelectionBound transformed_selection_start(clamped_selection_start);
+  gfx::SelectionBound transformed_selection_end(clamped_selection_end);
 
   // TODO(wjmaclean): Get the transform between the views to lower the
   // overhead here, instead of calling the transform functions four times.
-  transformed_selection_start.SetEdge(
-      rwhv_->TransformPointToRootCoordSpaceF(selection_start_.edge_start()),
-      rwhv_->TransformPointToRootCoordSpaceF(selection_start_.edge_end()));
+  transformed_selection_start.SetEdge(rwhv_->TransformPointToRootCoordSpaceF(
+                                          clamped_selection_start.edge_start()),
+                                      rwhv_->TransformPointToRootCoordSpaceF(
+                                          clamped_selection_start.edge_end()));
   transformed_selection_start.SetVisibleEdge(
       rwhv_->TransformPointToRootCoordSpaceF(
-          selection_start_.visible_edge_start()),
+          clamped_selection_start.visible_edge_start()),
       rwhv_->TransformPointToRootCoordSpaceF(
-          selection_start_.visible_edge_end()));
+          clamped_selection_start.visible_edge_end()));
   transformed_selection_end.SetEdge(
-      rwhv_->TransformPointToRootCoordSpaceF(selection_end_.edge_start()),
-      rwhv_->TransformPointToRootCoordSpaceF(selection_end_.edge_end()));
+      rwhv_->TransformPointToRootCoordSpaceF(
+          clamped_selection_end.edge_start()),
+      rwhv_->TransformPointToRootCoordSpaceF(clamped_selection_end.edge_end()));
   transformed_selection_end.SetVisibleEdge(
       rwhv_->TransformPointToRootCoordSpaceF(
-          selection_end_.visible_edge_start()),
+          clamped_selection_end.visible_edge_start()),
       rwhv_->TransformPointToRootCoordSpaceF(
-          selection_end_.visible_edge_end()));
+          clamped_selection_end.visible_edge_end()));
 
   manager_->UpdateClientSelectionBounds(transformed_selection_start,
                                         transformed_selection_end, this, this);
diff --git a/content/browser/renderer_host/render_widget_host_view_child_frame.cc b/content/browser/renderer_host/render_widget_host_view_child_frame.cc
index fb05dcf2..7c4702a3 100644
--- a/content/browser/renderer_host/render_widget_host_view_child_frame.cc
+++ b/content/browser/renderer_host/render_widget_host_view_child_frame.cc
@@ -120,7 +120,9 @@
       manager->RemoveObserver(this);
 #if BUILDFLAG(IS_ANDROID)
       auto* observer = root_view->GetTouchSelectionControllerInputObserver();
-      host()->RemoveInputEventObserver(observer);
+      if (observer) {
+        host()->RemoveInputEventObserver(observer);
+      }
 #endif
     }
   } else {
@@ -188,7 +190,9 @@
 
 #if BUILDFLAG(IS_ANDROID)
       auto* observer = root_view->GetTouchSelectionControllerInputObserver();
-      host()->AddInputEventObserver(observer);
+      if (observer) {
+        host()->AddInputEventObserver(observer);
+      }
 #endif
     }
   }
diff --git a/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc b/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
index 288bce5..0d5018b 100644
--- a/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
@@ -15,6 +15,7 @@
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/task_environment.h"
 #include "build/build_config.h"
+#include "cc/trees/render_frame_metadata.h"
 #include "components/input/child_frame_input_helper.h"
 #include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
 #include "components/viz/test/begin_frame_args_test.h"
@@ -49,6 +50,7 @@
 #include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h"
 #include "ui/base/ui_base_features.h"
 #include "ui/compositor/compositor.h"
+#include "ui/gfx/selection_bound.h"
 
 #if BUILDFLAG(IS_WIN)
 #include "components/stylus_handwriting/win/features.h"
@@ -132,12 +134,71 @@
   raw_ptr<RenderWidgetHostViewBase> root_host_view_ = nullptr;
 };
 
+class TestTouchSelectionControllerClientManager
+    : public TouchSelectionControllerClientManager {
+ public:
+  TestTouchSelectionControllerClientManager() = default;
+  ~TestTouchSelectionControllerClientManager() override = default;
+
+  void DidStopFlinging() override {}
+  void OnSwipeToMoveCursorBegin() override {}
+  void OnSwipeToMoveCursorEnd() override {}
+  void OnClientHitTestRegionUpdated(
+      ui::TouchSelectionControllerClient* client) override {}
+  void UpdateClientSelectionBounds(
+      const gfx::SelectionBound& start,
+      const gfx::SelectionBound& end,
+      ui::TouchSelectionControllerClient* client,
+      ui::TouchSelectionMenuClient* menu_client) override {
+    last_selection_start_ = start;
+    last_selection_end_ = end;
+  }
+  void InvalidateClient(ui::TouchSelectionControllerClient* client) override {}
+  ui::TouchSelectionController* GetTouchSelectionController() override {
+    return nullptr;
+  }
+  void AddObserver(Observer* observer) override {}
+  void RemoveObserver(Observer* observer) override {}
+
+  const gfx::SelectionBound& last_selection_start() const {
+    return last_selection_start_;
+  }
+  const gfx::SelectionBound& last_selection_end() const {
+    return last_selection_end_;
+  }
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc b/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
index 288bce5..0d5018b 100644
--- a/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
@@ -15,6 +15,7 @@
 #include "base/task/single_thread_task_runner.h"
 #include "base/test/task_environment.h"
 #include "build/build_config.h"
+#include "cc/trees/render_frame_metadata.h"
 #include "components/input/child_frame_input_helper.h"
 #include "components/viz/common/surfaces/parent_local_surface_id_allocator.h"
 #include "components/viz/test/begin_frame_args_test.h"
@@ -49,6 +50,7 @@
 #include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h"
 #include "ui/base/ui_base_features.h"
 #include "ui/compositor/compositor.h"
+#include "ui/gfx/selection_bound.h"
 
 #if BUILDFLAG(IS_WIN)
 #include "components/stylus_handwriting/win/features.h"
@@ -132,12 +134,71 @@
   raw_ptr<RenderWidgetHostViewBase> root_host_view_ = nullptr;
 };
 
+class TestTouchSelectionControllerClientManager
+    : public TouchSelectionControllerClientManager {
+ public:
+  TestTouchSelectionControllerClientManager() = default;
+  ~TestTouchSelectionControllerClientManager() override = default;
+
+  void DidStopFlinging() override {}
+  void OnSwipeToMoveCursorBegin() override {}
+  void OnSwipeToMoveCursorEnd() override {}
+  void OnClientHitTestRegionUpdated(
+      ui::TouchSelectionControllerClient* client) override {}
+  void UpdateClientSelectionBounds(
+      const gfx::SelectionBound& start,
+      const gfx::SelectionBound& end,
+      ui::TouchSelectionControllerClient* client,
+      ui::TouchSelectionMenuClient* menu_client) override {
+    last_selection_start_ = start;
+    last_selection_end_ = end;
+  }
+  void InvalidateClient(ui::TouchSelectionControllerClient* client) override {}
+  ui::TouchSelectionController* GetTouchSelectionController() override {
+    return nullptr;
+  }
+  void AddObserver(Observer* observer) override {}
+  void RemoveObserver(Observer* observer) override {}
+
+  const gfx::SelectionBound& last_selection_start() const {
+    return last_selection_start_;
+  }
+  const gfx::SelectionBound& last_selection_end() const {
+    return last_selection_end_;
+  }
+
+ private:
+  gfx::SelectionBound last_selection_start_;
+  gfx::SelectionBound last_selection_end_;
+};
+
 class MockRenderWidgetHostView : public TestRenderWidgetHostView {
  public:
   explicit MockRenderWidgetHostView(RenderWidgetHost* rwh)
       : TestRenderWidgetHostView(rwh) {}
   ~MockRenderWidgetHostView() override = default;
 
+  TouchSelectionControllerClientManager*
+  GetTouchSelectionControllerClientManager() override {
+    return &selection_manager_;
+  }
+
+  bool TransformPointToCoordSpaceForView(
+      const gfx::PointF& point,
+      input::RenderWidgetHostViewInput* target_view,
+      gfx::PointF* transformed_point) override {
+    *transformed_point = point;
+    return true;
+  }
+
+  bool TransformPointToLocalCoordSpace(
+      const gfx::PointF& point,
+      const viz::FrameSinkId& original_frame_sink_id,
+      gfx::PointF* transformed_point) override {
+    *transformed_point = point;
+    return true;
+  }
+
 #if BUILDFLAG(IS_MAC)
   MOCK_METHOD(void,
               ShowSharePicker,
@@ -148,6 +209,13 @@
                blink::mojom::ShareService::ShareCallback callback),
               (override));
 #endif
+
+  TestTouchSelectionControllerClientManager* selection_manager() {
+    return &selection_manager_;
+  }
+
+ private:
+  TestTouchSelectionControllerClientManager selection_manager_;
 };
 
 class RenderWidgetHostViewChildFrameTest
@@ -704,4 +772,58 @@
 }
 #endif  // BUILDFLAG(IS_WIN)
 
+TEST_F(RenderWidgetHostViewChildFrameTest, SelectionBoundsClampedToViewBounds) {
+  auto root_view =
+      std::make_unique<testing::NiceMock<MockRenderWidgetHostView>>(
+          widget_host_.get());
+  RenderWidgetHostViewChildFrame* child_view =
+      RenderWidgetHostViewChildFrame::Create(widget_host_.get(),
+                                             display::ScreenInfos());
+  std::unique_ptr<MockFrameConnector> connector =
+      std::make_unique<MockFrameConnector>();
+  connector->SetRootRenderWidgetHostView(root_view.get());
+  connector->SetView(child_view, false);
+
+  // Set local frame bounds and size DIP to 100x100.
+  connector->SetRectInParentView(gfx::Rect(0, 0, 100, 100));
+  connector->SetLocalFrameSize(gfx::Size(100, 100));
+  child_view->SetSize(gfx::Size(100, 100));
+
+  // Report selection bounds with spoofed coordinates far outside [0, 0, 100,
+  // 100].
+  cc::RenderFrameMetadata metadata;
+  metadata.selection.start.set_type(gfx::SelectionBound::LEFT);
+  metadata.selection.start.set_visible(true);
+  metadata.selection.start.SetEdge(gfx::PointF(-50.0f, -50.0f),
+                                   gfx::PointF(-50.0f, -10.0f));
+  metadata.selection.start.SetVisibleEdge(gfx::PointF(-50.0f, -50.0f),
+                                          gfx::PointF(-50.0f, -10.0f));
+
+  metadata.selection.end.set_type(gfx::SelectionBound::RIGHT);
+  metadata.selection.end.set_visible(true);
+  metadata.selection.end.SetEdge(gfx::PointF(200.0f, 200.0f),
+                                 gfx::PointF(200.0f, 250.0f));
+  metadata.selection.end.SetVisibleEdge(gfx::PointF(200.0f, 200.0f),
+                                        gfx::PointF(200.0f, 250.0f));
+
+  widget_host_->render_frame_metadata_provider()
+      ->SetLastRenderFrameMetadataForTest(metadata);
+  child_view->OnRenderFrameMetadataChangedAfterActivation(base::TimeTicks());
+
+  // Verify that start and end bounds sent to the manager were clamped to [0,
+  // 100].
+  gfx::SelectionBound start =
+      root_view->selection_manager()->last_selection_start();
+  gfx::SelectionBound end =
+      root_view->selection_manager()->last_selection_end();
+
+  EXPECT_EQ(gfx::PointF(0.0f, 0.0f), start.edge_start());
+  EXPECT_EQ(gfx::PointF(0.0f, 0.0f), start.edge_end());
+  EXPECT_EQ(gfx::PointF(100.0f, 100.0f), end.edge_start());
+  EXPECT_EQ(gfx::PointF(100.0f, 100.0f), end.edge_end());
+
+  child_view->Destroy();
+  connector->SetRootRenderWidgetHostView(nullptr);
+}
+
 }  // 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.