CVE-2026-11042
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/widget/root_view.cc |
modified | |
DeleteSiblingOnMouseExitedui/views/widget/root_view_unittest.cc |
modified | |
ifui/views/widget/root_view_unittest.cc |
modified | |
TEST_Fui/views/widget/root_view_unittest.cc |
modified |
Files Changed
ui/views/widget/root_view.ccui/views/widget/root_view_unittest.cc
Patch
From dd17b6164f0786abb933538e164daa75f65eea26 Mon Sep 17 00:00:00 2001 From: Keren Zhu <[email protected]> Date: Thu, 16 Apr 2026 10:30:16 -0700 Subject: [PATCH] views: Fix UAF in RootView::HandleMouseEnteredOrMoved A use-after-free vulnerability existed in RootView::HandleMouseEnteredOrMoved when the view being entered was deleted by the handler of the kMouseExited event dispatched to the old handler. This CL fixes it by using ViewTracker to track the entered view and returning early if it is destroyed. Also added tests to reproduce the bug (cleaned up comments from POC). TAG=agy Fixed: 498720094 Change-Id: I56f2b5bc871669635ad85dbf0a503a0bb9c67d41 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759981 Reviewed-by: Dana Fried <[email protected]> Commit-Queue: Keren Zhu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1615990} --- diff --git a/ui/views/widget/root_view.cc b/ui/views/widget/root_view.cc index 6b70d0bc..ab508f4 100644 --- a/ui/views/widget/root_view.cc +++ b/ui/views/widget/root_view.cc @@ -33,6 +33,7 @@ #include "ui/views/drag_controller.h" #include "ui/views/view_class_properties.h" #include "ui/views/view_targeter.h" +#include "ui/views/view_tracker.h" #include "ui/views/views_features.h" #include "ui/views/widget/root_view_targeter.h" #include "ui/views/widget/widget.h" @@ -860,21 +861,23 @@ } void RootView::HandleMouseEnteredOrMoved(const ui::MouseEvent& event) { - View* v = GetEventHandlerForPoint(event.location()); + View* raw_view = GetEventHandlerForPoint(event.location()); // Check for a disabled move handler. If the move handler became // disabled while handling moves, it's wrong to suddenly send // EventType::kMouseExited and EventType::kMouseEntered events, because the // mouse hasn't actually exited yet. if (mouse_move_handler_ && !mouse_move_handler_->GetEnabledInViewsSubtree() && - v->Contains(mouse_move_handler_)) { - v = mouse_move_handler_; + raw_view->Contains(mouse_move_handler_)) { + raw_view = mouse_move_handler_; } - if (v && v != this) { - if (v != mouse_move_handler_) { + ViewTracker view_tracker(raw_view); + + if (view_tracker.view() && view_tracker.view() != this) { + if (view_tracker.view() != mouse_move_handler_) { if (mouse_move_handler_ != nullptr && (!mouse_move_handler_->GetNotifyEnterExitOnChild() || - !mouse_move_handler_->Contains(v))) { + !mouse_move_handler_->Contains(view_tracker.view()))) { MouseEnterExitEvent exit(event, ui::EventType::kMouseExited); exit.ConvertLocationToTarget(static_cast<View*>(this), mouse_move_handler_.get()); @@ -883,6 +886,11 @@ if (dispatch_details.dispatcher_destroyed) { return; } + // If the entered view was destroyed by the exited view's handler, + // return early to avoid UAF. + if (!view_tracker.view()) { + return; + } // The mouse_move_handler_ could have been destroyed in the context of // the mouse exit event. if (!dispatch_details.target_destroyed) { @@ -893,14 +901,19 @@ return; } dispatch_details = NotifyEnterExitOfDescendant( - event, ui::EventType::kMouseExited, mouse_move_handler_, v); + event, ui::EventType::kMouseExited, mouse_move_handler_, + view_tracker.view()); if (dispatch_details.dispatcher_destroyed) { return; } + // Check again if v was destroyed during NotifyEnterExitOfDescendant + if (!view_tracker.view()) { + return; + } } } View* old_handler = mouse_move_handler_; - mouse_move_handler_ = v; + mouse_move_handler_ = view_tracker.view(); // TODO(crbug.com/40821061): This is for debug purpose only. // Remove it after resolving the issue. if (!mouse_move_handler_->GetNotifyEnterExitOnChild() || @@ -957,8 +970,9 @@ if (!mouse_move_handler_) { return; } - dispatch_details = NotifyEnterExitOfDescendant( - event, ui::EventType::kMouseExited, mouse_move_handler_, v); + dispatch_details = + NotifyEnterExitOfDescendant(event, ui::EventType::kMouseExited, + mouse_move_handler_, view_tracker.view()); if (dispatch_details.dispatcher_destroyed) { return; } diff --git a/ui/views/widget/root_view_unittest.cc b/ui/views/widget/root_view_unittest.cc index 0cd598a..9d31821 100644 --- a/ui/views/widget/root_view_unittest.cc +++ b/ui/views/widget/root_view_unittest.cc @@ -704,6 +704,98 @@ EXPECT_TRUE(root_view->GetContentsView()->children().empty()); } +namespace { + +// View that, on receiving kMouseExited, deletes a sibling view. +class DeleteSiblingOnMouseExited : public View { + METADATA_HEADER(DeleteSiblingOnMouseExited, View) + + public: + DeleteSiblingOnMouseExited() = default; + DeleteSiblingOnMouseExited(const DeleteSiblingOnMouseExited&) = delete; + DeleteSiblingOnMouseExited& operator=(const DeleteSiblingOnMouseExited&) = + delete; + + void set_sibling_to_delete(View* sibling) { sibling_to_delete_ = sibling; } + + void OnMouseExited(const ui::MouseEvent& event) override { + if (sibling_to_delete_) { + View* victim = sibling_to_delete_.get(); + sibling_to_delete_ = nullptr; + parent()->RemoveChildViewT(victim); + } + } + + private: + raw_ptr<View> sibling_to_delete_ = nullptr; +}; + +BEGIN_METADATA(DeleteSiblingOnMouseExited) +END_METADATA + +} // namespace + +// Verifies that deleting an entered view from the exited view's OnMouseExited +// handler does not cause a use-after-free. +TEST_F(RootViewTest, DeleteEnteredSiblingDuringMouseExitDispatch) { + RootViewTestState state(this, {.bounds = {10, 10, 500, 500}, + .type = Widget::InitParams::TYPE_POPUP}); + internal::RootView* root_view = state.GetRootView(); + View* content = root_view->GetContentsView(); + + content->SetNotifyEnterExitOnChild(true); + + DeleteSiblingOnMouseExited* view_a = + content->AddChildView(std::make_unique<DeleteSiblingOnMouseExited>()); + view_a->SetBounds(10, 10, 100, 100); + + View* view_b = content->AddChildView(std::make_unique<View>()); + view_b->SetBounds(200, 10, 100, 100); + + view_a->set_sibling_to_delete(view_b); + + // Move mouse over A. + ui::MouseEvent move1(ui::EventType::kMouseMoved, gfx::Point(50, 50), + gfx::Point(50, 50), ui::EventTimeForNow(), 0, 0); + root_view->OnMouseMoved(move1); + + // Move mouse over B, which should delete B during A's exit dispatch. + ui::MouseEvent move2(ui::EventType::kMouseMoved, gfx::Point(250, 50), + gfx::Point(250, 50), ui::EventTimeForNow(), 0, 0); + root_view->OnMouseMoved(move2); + + EXPECT_EQ(1u, content->children().size()); +} + +// Same as above but without NotifyEnterExitOnChild on the parent. +TEST_F(RootViewTest, DeleteEnteredSibling_NoNotifyAncestor) { + RootViewTestState state(this, {.bounds = {10, 10, 500, 500}, + .type = Widget::InitParams::TYPE_POPUP}); + internal::RootView* root_view = state.GetRootView(); + View* content = root_view->GetContentsView(); + + DeleteSiblingOnMouseExited* view_a = + content->AddChildView(std::make_unique<DeleteSiblingOnMouseExited>()); + view_a->SetBounds(10, 10, 100, 100); + + View* view_b = content->AddChildView(std::make_unique<View>()); + view_b->SetBounds(200, 10, 100, 100); + + view_a->set_sibling_to_delete(view_b); +
Regression Test / PoC
diff --git a/ui/views/widget/root_view_unittest.cc b/ui/views/widget/root_view_unittest.cc
index 0cd598a..9d31821 100644
--- a/ui/views/widget/root_view_unittest.cc
+++ b/ui/views/widget/root_view_unittest.cc
@@ -704,6 +704,98 @@
EXPECT_TRUE(root_view->GetContentsView()->children().empty());
}
+namespace {
+
+// View that, on receiving kMouseExited, deletes a sibling view.
+class DeleteSiblingOnMouseExited : public View {
+ METADATA_HEADER(DeleteSiblingOnMouseExited, View)
+
+ public:
+ DeleteSiblingOnMouseExited() = default;
+ DeleteSiblingOnMouseExited(const DeleteSiblingOnMouseExited&) = delete;
+ DeleteSiblingOnMouseExited& operator=(const DeleteSiblingOnMouseExited&) =
+ delete;
+
+ void set_sibling_to_delete(View* sibling) { sibling_to_delete_ = sibling; }
+
+ void OnMouseExited(const ui::MouseEvent& event) override {
+ if (sibling_to_delete_) {
+ View* victim = sibling_to_delete_.get();
+ sibling_to_delete_ = nullptr;
+ parent()->RemoveChildViewT(victim);
+ }
+ }
+
+ private:
+ raw_ptr<View> sibling_to_delete_ = nullptr;
+};
+
+BEGIN_METADATA(DeleteSiblingOnMouseExited)
+END_METADATA
+
+} // namespace
+
+// Verifies that deleting an entered view from the exited view's OnMouseExited
+// handler does not cause a use-after-free.
+TEST_F(RootViewTest, DeleteEnteredSiblingDuringMouseExitDispatch) {
+ RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
+ .type = Widget::InitParams::TYPE_POPUP});
+ internal::RootView* root_view = state.GetRootView();
+ View* content = root_view->GetContentsView();
+
+ content->SetNotifyEnterExitOnChild(true);
+
+ DeleteSiblingOnMouseExited* view_a =
+ content->AddChildView(std::make_unique<DeleteSiblingOnMouseExited>());
+ view_a->SetBounds(10, 10, 100, 100);
+
+ View* view_b = content->AddChildView(std::make_unique<View>());
+ view_b->SetBounds(200, 10, 100, 100);
+
+ view_a->set_sibling_to_delete(view_b);
+
+ // Move mouse over A.
+ ui::MouseEvent move1(ui::EventType::kMouseMoved, gfx::Point(50, 50),
+ gfx::Point(50, 50), ui::EventTimeForNow(), 0, 0);
+ root_view->OnMouseMoved(move1);
+
+ // Move mouse over B, which should delete B during A's exit dispatch.
+ ui::MouseEvent move2(ui::EventType::kMouseMoved, gfx::Point(250, 50),
+ gfx::Point(250, 50), ui::EventTimeForNow(), 0, 0);
+ root_view->OnMouseMoved(move2);
+
+ EXPECT_EQ(1u, content->children().size());
+}
+
+// Same as above but without NotifyEnterExitOnChild on the parent.
+TEST_F(RootViewTest, DeleteEnteredSibling_NoNotifyAncestor) {
+ RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
+ .type = Widget::InitParams::TYPE_POPUP});
+ internal::RootView* root_view = state.GetRootView();
+ View* content = root_view->GetContentsView();
+
+ DeleteSiblingOnMouseExited* view_a =
+ content->AddChildView(std::make_unique<DeleteSiblingOnMouseExited>());
+ view_a->SetBounds(10, 10, 100, 100);
+
+ View* view_b = content->AddChildView(std::make_unique<View>());
+ view_b->SetBounds(200, 10, 100, 100);
+
+ view_a->set_sibling_to_delete(view_b);
+
+ // Move mouse over A.
+ ui::MouseEvent move1(ui::EventType::kMouseMoved, gfx::Point(50, 50),
+ gfx::Point(50, 50), ui::EventTimeForNow(), 0, 0);
+ root_view->OnMouseMoved(move1);
+
+ // Move mouse over B, which should delete B during A's exit dispatch.
+ ui::MouseEvent move2(ui::EventType::kMouseMoved, gfx::Point(250, 50),
+ gfx::Point(250, 50), ui::EventTimeForNow(), 0, 0);
+ root_view->OnMouseMoved(move2);
+
+ EXPECT_EQ(1u, content->children().size());
+}
+
// Verifies removing a View in OnMouseEntered() doesn't crash.
TEST_F(RootViewTest, RemoveViewOnMouseEnterDispatch) {
RootViewTestState state(this, {.bounds = {10, 10, 500, 500},
Original Bug Report
UAF in RootView::HandleMouseEnteredOrMoved via synchronous exit event dispatch
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A potential Use-After-Free (UAF) vulnerability exists in the browser process’s RootView::HandleMouseEnteredOrMoved method. When the mouse moves to a new view, an exit event is synchronously dispatched to the old view. If the old view’s handler synchronously deletes the new view, the local raw pointer representing the new view becomes a dangling pointer, which is subsequently used for event dispatch and virtual method calls.
Affected files:
ui/views/widget/root_view.ccui/views/widget/root_view.hui/events/event_dispatcher.cc
Estimated timestamp from git blame: 2025-07-29
Summary
A potential Use-After-Free (UAF) vulnerability exists in the Browser process within ui/views/widget/root_view.cc. The issue occurs in RootView::HandleMouseEnteredOrMoved when a user moves the mouse from one view (V_old) to another (V_new). The code synchronously dispatches a kMouseExited event to V_old while holding a local raw pointer (View* v) to V_new. If V_old’s event handler triggers a UI update that deletes V_new, the pointer v becomes dangling. This pointer is later passed to DispatchEvent, leading to virtual method calls on freed memory.
Technical Details
In RootView::HandleMouseEnteredOrMoved, the following sequence can lead to a UAF:
- A local raw pointer
View* vis obtained viaGetEventHandlerForPoint()(around line 873). This pointer represents the view the mouse has moved into (V_new). - The code dispatches a
kMouseExitedevent to the previous handler,mouse_move_handler_(V_old) (around line 892). - If
V_old’sOnMouseExitedhandler (or a related observer) synchronously removes and deletes viewv(V_new), the pointervbecomes dangling. - MiraclePtr Bypass: Because
vis a localView*(not araw_ptr), and because removing the view from its parent clears theraw_ptrin the parent’schildren_vector, the BackupRefPtr (BRP) reference count drops to 0. The memory is returned directly to the PartitionAlloc freelist without being quarantined. - The execution returns from the
kMouseExiteddispatch. The code checksif (!dispatch_details.target_destroyed)(line 898), but this only verifies thatV_oldwasn’t destroyed; it doesn’t checkv. - The execution proceeds to use the dangling pointer
v:- Line 906:
NotifyEnterExitOfDescendantis called withv. This triggers a UAF read (v->parent_) inView::Contains. - Line 913:
mouse_move_handler_ = v;. Assigning the dangling pointer to araw_ptrincrements the BRP refcount on potentially attacker-reclaimed memory, finalizing the BRP bypass. - Line 922:
DispatchEventis called withmouse_move_handler_.
- Line 906:
DispatchEventeventually routes toui::EventDispatcher::ProcessEvent, which calls the pure virtual methodtarget->CanAcceptEvent(*event).
Impact and Exploitability
If an attacker can trigger a UI state change that synchronously deletes V_new during the exit dispatch of V_old, they can subsequently heap-spray the Browser process to reclaim the freed memory chunk backing V_new. The UAF read of v->parent_ can be bypassed by setting the fake parent_ to nullptr. The virtual method call to CanAcceptEvent will then dereference a fake vtable pointer provided by the attacker, leading to Remote Code Execution (RCE) and a full sandbox escape.
Note: This sequence describes a potential exploit path. Our tooling agent does not yet have the ability to run code or provide a working proof of concept (PoC).
Suggested Fix
Protect the local variable v using a ViewTracker.
// Include the tracker
#include "ui/views/view_tracker.h"
// In RootView::HandleMouseEnteredOrMoved:
View* raw_v = GetEventHandlerForPoint(event.location());
ViewTracker v_tracker(raw_v);
// ...
// Replace uses of `v` with `v_tracker.view()` and check if it is non-null after DispatchEvent returns.
Alternatively, v could be wrapped in a base::WeakPtr<View> if View supports it, or mouse_move_handler_ and event dispatch logic could be refactored to better handle asynchronous destruction.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.