Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Views
DescriptionUse after free in Views
ComponentViews
Bug ClassUAF
Tracker498720094
Fix commitdd17b6164f07 (chromium/src) +116/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
ui/views/widget/root_view.cc
modified
DeleteSiblingOnMouseExited
ui/views/widget/root_view_unittest.cc
modified
if
ui/views/widget/root_view_unittest.cc
modified
TEST_F
ui/views/widget/root_view_unittest.cc
modified

Files Changed

  • ui/views/widget/root_view.cc
  • ui/views/widget/root_view_unittest.cc
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);
+
Loading diff…

Regression Test / PoC

shipped with the fix
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},
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • ui/views/widget/root_view.h
  • ui/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:

  1. A local raw pointer View* v is obtained via GetEventHandlerForPoint() (around line 873). This pointer represents the view the mouse has moved into (V_new).
  2. The code dispatches a kMouseExited event to the previous handler, mouse_move_handler_ (V_old) (around line 892).
  3. If V_old’s OnMouseExited handler (or a related observer) synchronously removes and deletes view v (V_new), the pointer v becomes dangling.
  4. MiraclePtr Bypass: Because v is a local View* (not a raw_ptr), and because removing the view from its parent clears the raw_ptr in the parent’s children_ vector, the BackupRefPtr (BRP) reference count drops to 0. The memory is returned directly to the PartitionAlloc freelist without being quarantined.
  5. The execution returns from the kMouseExited dispatch. The code checks if (!dispatch_details.target_destroyed) (line 898), but this only verifies that V_old wasn’t destroyed; it doesn’t check v.
  6. The execution proceeds to use the dangling pointer v:
    • Line 906: NotifyEnterExitOfDescendant is called with v. This triggers a UAF read (v->parent_) in View::Contains.
    • Line 913: mouse_move_handler_ = v;. Assigning the dangling pointer to a raw_ptr increments the BRP refcount on potentially attacker-reclaimed memory, finalizing the BRP bypass.
    • Line 922: DispatchEvent is called with mouse_move_handler_.
  7. DispatchEvent eventually routes to ui::EventDispatcher::ProcessEvent, which calls the pure virtual method target->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.

View on issue tracker