diff --git a/ui/ozone/platform/x11/test/x11_drag_drop_client_unittest.cc b/ui/ozone/platform/x11/test/x11_drag_drop_client_unittest.cc index 225d83cf8ceec..778227b5243b1 100644 --- a/ui/ozone/platform/x11/test/x11_drag_drop_client_unittest.cc +++ b/ui/ozone/platform/x11/test/x11_drag_drop_client_unittest.cc @@ -831,4 +831,117 @@ TEST_F(X11DragDropClientTest, RejectAfterMouseRelease) { EXPECT_EQ(DragOperation::kNone, result); } +namespace { + +// A delegate that owns its XDragDropClient (mirroring X11Window's ownership +// via `std::unique_ptr drag_drop_client_`) and destroys the +// client from inside a synchronous delegate callback. In production this +// simulates a drop-target handler that spins a nested kNestableTasksAllowed +// RunLoop during which a queued CloseNow task runs, tearing down X11Window and +// its owned XDragDropClient (see the equivalent WeakPtr guard applied on the +// Wayland path in wayland_window.cc:OnDragSessionClose). +class OwningDelegate : public XDragDropClient::Delegate { + public: + enum class DestroyIn { kNone, kPerformDrop, kOnBeforeDragLeave, kUpdateDrag }; + + explicit OwningDelegate(x11::Window xwindow) { + client_ = std::make_unique(this, xwindow); + } + ~OwningDelegate() override = default; + + XDragDropClient* client() { return client_.get(); } + void set_destroy_in(DestroyIn d) { destroy_in_ = d; } + + // XDragDropClient::Delegate: + std::optional GetDragWidget() override { + return std::nullopt; + } + int UpdateDrag(const gfx::Point&) override { + if (destroy_in_ == DestroyIn::kUpdateDrag) { + client_.reset(); // ~XDragDropClient(this) mid-CompleteXdndPosition. + } + return 0; + } + void UpdateCursor(mojom::DragOperation) override {} + void OnBeginForeignDrag(x11::Window) override {} + void OnEndForeignDrag() override {} + void OnBeforeDragLeave() override { + if (destroy_in_ == DestroyIn::kOnBeforeDragLeave) { + client_.reset(); // ~XDragDropClient(this) mid-OnXdndLeave. + } + } + mojom::DragOperation PerformDrop() override { + if (destroy_in_ == DestroyIn::kPerformDrop) { + client_.reset(); // ~XDragDropClient(this) mid-OnXdndDrop. + } + return mojom::DragOperation::kNone; + } + void EndDragLoop() override {} + + private: + DestroyIn destroy_in_ = DestroyIn::kNone; + std::unique_ptr client_; +}; + +x11::ClientMessageEvent MakeXdndEvent(const char* type, + x11::Window source_window) { + x11::ClientMessageEvent ev; + ev.type = x11::GetAtom(type); + ev.format = 32; + ev.data.data32.fill(0); + ev.data.data32[0] = static_cast(source_window); + return ev; +} + +} // namespace + +// Regression test: if the delegate destroys the XDragDropClient from inside +// PerformDrop() (via a nested RunLoop that runs CloseNow), OnXdndDrop must not +// continue to dereference |this|. Without a WeakPtr guard, the code proceeds +// to PrepareXdndClientMessage (reads freed this->xwindow_) and then makes a +// virtual SendXClientEvent call through the freed vtable pointer. +TEST_F(X11DragDropClientTest, ClientDestroyedDuringPerformDrop) { + OwningDelegate delegate(static_cast(0x11110001)); + delegate.set_destroy_in(OwningDelegate::DestroyIn::kPerformDrop); + + x11::Window fake_source = static_cast(0x22220001); + x11::ClientMessageEvent drop = MakeXdndEvent("XdndDrop", fake_source); + // UAF: after delegate_->PerformDrop() frees |this|, OnXdndDrop reads + // this->xwindow_ and dispatches the virtual SendXClientEvent on freed |this|. + delegate.client()->HandleXdndEvent(drop); +} + +// Regression test: OnXdndLeave -> delegate_->OnBeforeDragLeave() may destroy +// |this|; the subsequent ResetDragContext() then reads/writes freed members. +TEST_F(X11DragDropClientTest, ClientDestroyedDuringOnBeforeDragLeave) { + OwningDelegate delegate(static_cast(0x11110002)); + XDragDropClient* raw_client = delegate.client(); + + // Establish a drag context so ResetDragContext() takes the non-trivial path. + x11::Window fake_source = static_cast(0x22220002); + x11::ClientMessageEvent enter = MakeXdndEvent("XdndEnter", fake_source); + enter.data.data32[1] = (5u << 24); // XDND protocol version 5. + raw_client->HandleXdndEvent(enter); + + delegate.set_destroy_in(OwningDelegate::DestroyIn::kOnBeforeDragLeave); + x11::ClientMessageEvent leave = MakeXdndEvent("XdndLeave", fake_source); + // UAF: after delegate_->OnBeforeDragLeave() frees |this|, ResetDragContext() + // reads freed this->target_current_context_ and this->delegate_. + raw_client->HandleXdndEvent(leave); +} + +// Regression test: CompleteXdndPosition -> delegate_->UpdateDrag() may destroy +// |this|; the subsequent PrepareXdndClientMessage + virtual SendXClientEvent +// dereference freed |this|. +TEST_F(X11DragDropClientTest, ClientDestroyedDuringUpdateDrag) { + OwningDelegate delegate(static_cast(0x11110003)); + delegate.set_destroy_in(OwningDelegate::DestroyIn::kUpdateDrag); + + x11::Window fake_source = static_cast(0x22220003); + // UAF: after delegate_->UpdateDrag() frees |this|, CompleteXdndPosition + // reads this->xwindow_ and dispatches virtual SendXClientEvent on freed + // |this|. + delegate.client()->CompleteXdndPosition(fake_source, gfx::Point(10, 10)); +} + } // namespace ui