diff --git a/ui/views/widget/drop_helper_unittest.cc b/ui/views/widget/drop_helper_unittest.cc index ce2d61940a0d9a..e3ec0c73016a12 100644 --- a/ui/views/widget/drop_helper_unittest.cc +++ b/ui/views/widget/drop_helper_unittest.cc @@ -123,5 +123,108 @@ TEST_F(DropHelperTest, DropCoordinates) { EXPECT_EQ(drop_target->drop_location(), gfx::Point(50, 25)); } +// A drop-target view whose OnDragExited() handler synchronously removes and +// deletes a sibling view from the hierarchy. Used to demonstrate the UAF in +// DropHelper::OnDragOver when the old target's OnDragExited deletes the view +// that was just selected as the new target. +class DragExitDeletesSibling : public views::View { + METADATA_HEADER(DragExitDeletesSibling, View) + + public: + DragExitDeletesSibling() = default; + + void set_sibling_to_delete(View* sibling) { sibling_to_delete_ = sibling; } + + // Make this view a valid drop target so DropHelper selects it. + bool GetDropFormats( + int* formats, + std::set* format_types) override { + *formats = ui::OSExchangeData::STRING; + return true; + } + bool CanDrop(const OSExchangeData& data) override { return true; } + int OnDragUpdated(const ui::DropTargetEvent& event) override { + return static_cast(ui::mojom::DragOperation::kCopy); + } + + // The critical override: when the drag leaves this view, synchronously + // destroy the sibling (which DropHelper has just chosen as the *new* + // target and is holding in a plain View* local). + void OnDragExited() override { + if (sibling_to_delete_) { + View* victim = sibling_to_delete_.get(); + // Release our raw_ptr ref before deletion so BRP refcount on the victim + // is zero at delete-time -- matching the production scenario where + // ResetTargetViewIfEquals clears deepest_view_ before ~View runs. + sibling_to_delete_ = nullptr; + victim->parent()->RemoveChildViewT(victim); + } + } + + private: + raw_ptr sibling_to_delete_ = nullptr; +}; + +BEGIN_METADATA(DragExitDeletesSibling) +END_METADATA + +// Regression / PoC test for the DropHelper::OnDragOver UAF. +// +// Sequence: +// 1. Drag over view A -> target_view_ = A. +// 2. Drag over view B -> CalculateTargetViewImpl returns B into a *plain* +// View* local; NotifyDragExit() runs A->OnDragExited(), which deletes B; +// target_view_ = (freed) B; NotifyDragEntered() dereferences freed B via +// ConvertPointToTarget and a virtual OnDragEntered() call. +// +// Under ASAN this produces heap-use-after-free in NotifyDragEntered. +TEST_F(DropHelperTest, OnDragExitedDeletesNewTarget) { + Widget widget; + Widget::InitParams init_params( + CreateParams(Widget::InitParams::TYPE_WINDOW_FRAMELESS)); + init_params.ownership = Widget::InitParams::CLIENT_OWNS_WIDGET; + widget.Init(std::move(init_params)); + widget.SetBounds(gfx::Rect(0, 0, 500, 500)); + + views::View* container = + widget.SetContentsView(std::make_unique()); + + // View A: the "old" drop target whose OnDragExited deletes B. + DragExitDeletesSibling* view_a = + container->AddChildView(std::make_unique()); + view_a->SetBounds(10, 10, 100, 100); + + // View B: the "new" drop target that will be freed mid-OnDragOver. + TestDropTargetView* view_b = + container->AddChildView(std::make_unique()); + view_b->SetBounds(200, 10, 100, 100); + + view_a->set_sibling_to_delete(view_b); + widget.Show(); + + auto drop_helper = std::make_unique(widget.GetRootView()); + + auto data = std::make_unique(); + data->SetString(u"payload"); + const int drag_op = static_cast(ui::mojom::DragOperation::kCopy); + + // Step 1: drag over A. target_view_ becomes A. + drop_helper->OnDragOver(*data, gfx::Point(60, 60), drag_op); + + // Step 2: drag over B. Inside this call: + // - local `view` = B + // - NotifyDragExit() -> A->OnDragExited() -> deletes B + // - target_view_ = (freed) B + // - NotifyDragEntered() -> UAF on B + drop_helper->OnDragOver(*data, gfx::Point(250, 60), drag_op); + + // If we get here without ASAN tripping, the fix is in place. B was deleted, + // so only A should remain. + EXPECT_EQ(1u, container->children().size()); + + // Avoid dangling raw_ptr deepest_view_ at DropHelper teardown. + drop_helper->OnDragExit(); +} + } // namespace } // namespace views