Medium watch CVSS 4.3 chrome Input Validation 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Input
DescriptionImproper input validation in Input
ComponentInput
Bug ClassInput Validation
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.
Tracker497456156
Fix commit65494ad63c86 (chromium/src) +170/-13
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-07-16 (public git)
Disclosed2026-08-25
Patch gap40 days — fix public in source before the advisory

Background

Touch Selection Quick Menu
the small floating menu (Copy/Paste/etc.) that Chrome shows near a touch text selection, positioned from selection edge coordinates reported by the renderer.
`TouchSelectionControllerClientChildFrame`
the browser-process client that takes a child (sub)frame’s selection bounds and transforms them into the root view’s coordinate space so the menu can be placed.
`gfx::SelectionBound`
a structure holding the edge_start/edge_end and visible_edge_start/visible_edge_end points describing one end of a selection.
`TransformPointToRootCoordSpaceF`
RenderWidgetHostViewChildFrame’s method that maps a point from a child frame’s local space up into the root view’s space.

Root Cause Analysis

In TouchSelectionControllerClientChildFrame::TransformSelectionBoundsAndUpdate, the browser process took the renderer-supplied selection_start_ and selection_end_ bounds and passed each edge point directly through rwhv_->TransformPointToRootCoordSpaceF() before handing them to manager_->UpdateClientSelectionBounds(). A compromised or malicious child-frame renderer controls the selection metadata it reports, so it could send edge coordinates far outside its own visual rectangle (the regression test uses (-50,-50) and (200,200) against a 100x100 view). Because no code constrained those coordinates to the child frame’s actual geometry, the resulting root-space coordinates could place the Quick Menu anywhere in the top-level window, outside the reporting frame’s visual area.

The fix builds local_bounds from rwhv_->GetViewBounds().size() and runs every edge point through the new ClampSelectionBoundToRect (via ClampPointToRect using std::clamp) before transformation, so all reported points are forced back into the child frame’s own rectangle. This restores the invariant that a subframe can only influence selection UI within its own visual boundaries.

Key insight
The single mistake was trusting renderer-reported selection coordinates as already lying within the child frame’s bounds; the fix clamps each selection edge to the frame’s local view rectangle before transforming it to root coordinates.

Attack Path

  1. Compromise a child frame An attacker runs code in a subframe (e.g. a cross-origin iframe) renderer, or fully compromises that renderer process.
  2. Report spoofed selection bounds The renderer sends selection metadata whose edge points lie well outside the frame’s own 100x100-style visual rectangle, e.g. negative or oversized coordinates.
  3. Unvalidated transform TransformSelectionBoundsAndUpdate passes those raw points straight into TransformPointToRootCoordSpaceF, mapping them into root-window space without clamping.
  4. Out-of-bounds menu placement UpdateClientSelectionBounds receives the spoofed root-space bounds and the Touch Selection Quick Menu is drawn at coordinates outside the reporting frame’s area, enabling UI spoofing/misplacement (inferred as the concrete UI consequence from the commit message).

Impact Assessment

An attacker controlling a child-frame renderer can cause the browser process to display the Touch Selection Quick Menu at attacker-chosen coordinates outside the frame’s visual boundaries, an input/UI-integrity issue rather than a memory-safety one. Preconditions are a touch-capable context (the child-frame path plus the Android-guarded observer code) and the ability to report selection metadata from the subframe renderer. The advisory classifies this as improper input validation in Input; no exploited-in-the-wild status is stated, and any further escalation is not demonstrated by the patch.

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
MockRenderWidgetHostView
content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
modified
RenderWidgetHostViewChildFrameTest
content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
modified
TEST_F
content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/render_widget_host_view_child_frame.cc
  • content/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
  • content/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc

Audit Directions

  • Renderer-reported geometry
    Audit every browser-process consumer of renderer-supplied coordinates/bounds (selection, IME, drag, tooltips) that transforms them to root space; confirm each clamps to the reporting view’s bounds first.
  • Child-frame to root coordinate transforms
    Wherever TransformPointToRootCoordSpaceF or similar transforms are fed subframe values, verify the values were validated against the subframe’s local rectangle before mapping.
  • Optional observer registration
    Check other Add/RemoveInputEventObserver-style calls for missing null guards like the one added here, especially on platform-specific (IS_ANDROID) paths.
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_;
+  }
+
+ 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…

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.