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 <[email protected]> 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 <[email protected]> Reviewed-by: Dave Tapuska <[email protected]> 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