diff --git a/ui/views/widget/root_view_unittest.cc b/ui/views/widget/root_view_unittest.cc index 486cf8e5e648a..7d745b690c458 100644 --- a/ui/views/widget/root_view_unittest.cc +++ b/ui/views/widget/root_view_unittest.cc @@ -702,6 +702,158 @@ TEST_F(RootViewTest, DeleteViewOnMouseEnterDispatch) { EXPECT_TRUE(root_view->GetContentsView()->children().empty()); } +// ======================================================================== +// POC for +// UAF in RootView::HandleMouseEnteredOrMoved via sibling deletion in +// OnMouseExited. +// +// The local `View* v` at root_view.cc:873 is a plain pointer to the view +// being ENTERED. The kMouseExited event is dispatched to the OLD +// mouse_move_handler_ at line 892. If that handler deletes `v` (a sibling), +// `v` dangles. The target_destroyed check at line 898 only tracks the OLD +// handler, not `v`. Execution proceeds to dereference the freed `v` at +// lines 906, 913, 916, 917, 920, 922. +// ======================================================================== +namespace { + +// View that, on receiving kMouseExited, deletes a *sibling* view. +// This is the key difference from DeleteViewOnEvent which deletes itself. +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_) { + // Extract the raw pointer and clear our raw_ptr BEFORE deletion so our + // raw_ptr doesn't hold a BRP ref. The bug is in RootView's local + // `View* v` (root_view.cc:873), not in this test helper. + View* victim = sibling_to_delete_.get(); + sibling_to_delete_ = nullptr; + // RemoveChildViewT removes from parent's children_ vector AND deletes. + // After this returns, RootView's local `v` is dangling. + parent()->RemoveChildViewT(victim); + } + } + + private: + raw_ptr sibling_to_delete_ = nullptr; +}; + +BEGIN_METADATA(DeleteSiblingOnMouseExited) +END_METADATA + +} // namespace + +// POC: Deleting the *entered* view from the *exited* view's OnMouseExited +// handler causes UAF in HandleMouseEnteredOrMoved. +// +// Under ASAN this test produces heap-use-after-free. +TEST_F(RootViewTest, POC_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(); + + // Enable NotifyEnterExitOnChild on the common parent so that + // NotifyEnterExitOfDescendant (root_view.cc:906 -> :995) calls + // Contains(dangling_v), which reads v->parent_ from freed memory. + // This gives the earliest, cleanest UAF stack. Even without this flag, + // lines 913-922 still dereference the dangling pointer. + content->SetNotifyEnterExitOnChild(true); + + // View A: deletes sibling B when it receives kMouseExited. + DeleteSiblingOnMouseExited* view_a = + content->AddChildView(std::make_unique()); + view_a->SetBounds(10, 10, 100, 100); + + // View B: the victim. Plain View, no special behavior needed. + View* view_b = content->AddChildView(std::make_unique()); + view_b->SetBounds(200, 10, 100, 100); + + view_a->set_sibling_to_delete(view_b); + + // Step 1: Move mouse over A. RootView::mouse_move_handler_ = A. + ui::MouseEvent move1(ui::EventType::kMouseMoved, gfx::Point(50, 50), + gfx::Point(50, 50), ui::EventTimeForNow(), 0, 0); + root_view->OnMouseMoved(move1); + + // Step 2: Move mouse over B. In HandleMouseEnteredOrMoved: + // - line 873: v = B (plain View*) + // - line 892: DispatchEvent(A, kMouseExited) + // -> A::OnMouseExited deletes B. v now dangles. + // - line 898: target_destroyed == false (A was the target, A is alive) + // - line 902: mouse_move_handler_ == A != nullptr, proceed + // - line 906: NotifyEnterExitOfDescendant(..., A, dangling_v) + // -> line 991: p = A->parent() = content + // -> line 992: content->GetNotifyEnterExitOnChild() == true + // -> line 995: content->Contains(dangling_v) + // -> view.cc:371: reads dangling_v->parent_ *** UAF *** + // - (if we reach here) line 913: mouse_move_handler_ = dangling_v + // - line 916: dangling_v->GetNotifyEnterExitOnChild() *** UAF *** + // - line 917: dangling_v->Contains(A) *** UAF *** + // - line 922: DispatchEvent(dangling_v, ...) -> virtual call *** UAF *** + ui::MouseEvent move2(ui::EventType::kMouseMoved, gfx::Point(250, 50), + gfx::Point(250, 50), ui::EventTimeForNow(), 0, 0); + root_view->OnMouseMoved(move2); + + // ASAN should have crashed before reaching here. + // If we get here in a non-ASAN build, at minimum verify B is gone. + EXPECT_EQ(1u, content->children().size()); +} + +// Same POC but WITHOUT NotifyEnterExitOnChild on the parent. This skips the +// UAF at line 906/995 (NotifyEnterExitOfDescendant short-circuits) and instead +// hits the UAF at lines 913-922: dangling v is stored into mouse_move_handler_ +// then dereferenced for GetNotifyEnterExitOnChild(), Contains(), and finally +// DispatchEvent which does virtual calls through the freed vtable pointer. +// This proves the core UAF does NOT require NotifyEnterExitOnChild. +TEST_F(RootViewTest, POC_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(); + + // Deliberately NOT calling SetNotifyEnterExitOnChild(true). + // NotifyEnterExitOfDescendant at line 906 will iterate ancestors but + // skip them all (line 992: GetNotifyEnterExitOnChild() == false everywhere), + // never reaching the Contains(dangling) call. Execution falls through to + // line 913. + + DeleteSiblingOnMouseExited* view_a = + content->AddChildView(std::make_unique()); + view_a->SetBounds(10, 10, 100, 100); + + View* view_b = content->AddChildView(std::make_unique()); + view_b->SetBounds(200, 10, 100, 100); + + view_a->set_sibling_to_delete(view_b); + + // Step 1: mouse_move_handler_ = A + ui::MouseEvent move1(ui::EventType::kMouseMoved, gfx::Point(50, 50), + gfx::Point(50, 50), ui::EventTimeForNow(), 0, 0); + root_view->OnMouseMoved(move1); + + // Step 2: v = B; A's OnMouseExited deletes B; v dangles. + // - line 906: NotifyEnterExitOfDescendant loop finds no notify-ancestor, + // never dereferences dangling v. Returns cleanly. + // - line 913: mouse_move_handler_ = dangling_v + // (raw_ptr tolerates this) + // - line 916: dangling_v->GetNotifyEnterExitOnChild() + // -> reads notify_enter_exit_on_child_ bool from freed memory *** UAF *** + 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},