Chrome · Input
CVE-2026-79106
Logic Error in Input
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
TransformSelectionBoundsAndUpdatecontent/browser/renderer_host/input/touch_selection_controller_client_child_frame.cc |
modified | |
ifcontent/browser/renderer_host/render_widget_host_view_child_frame.cc |
modified | |
TestTouchSelectionControllerClientManagercontent/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.cccontent/browser/renderer_host/render_widget_host_view_child_frame.cccontent/browser/renderer_host/render_widget_host_view_child_frame_unittest.cc
Patch
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.
References
On This Page