CVE-2026-87514
Overview
Background
- `DesktopDragDropClientOzone`
- the Views/Ozone (Linux) browser-process object that drives a drag-and-drop session, tracking the current target window/view and dispatching drag events to observers and the drop
delegate_. - `DropIfAllowed`
- a free function in
desktop_drag_drop_client_ozone.ccthat consultsui::DataTransferPolicyControllerand, when allowed, runs the drop callback; per the commit message it can invoke that callback synchronously. - Drop callback / `PerformDrop`
- the closure obtained from
delegate_->GetDropCallback(*event)that performs the actual data drop; running it can synchronously destroy the target view or window, which in turn destroys theDesktopDragDropClientOzone(this). - `base::WeakPtr` / `weak_factory_`
- a weak reference that becomes null when its owning object is destroyed, letting a method detect that
thiswas freed during a re-entrant callback.
Root Cause Analysis
DesktopDragDropClientOzone::OnDragDrop invoked DropIfAllowed(...), which can synchronously run the drop callback; a drop handler is free to destroy the target view/window and thereby destroy this. The pre-patch code violated the invariant that this outlives the method: after DropIfAllowed returned it still executed observers_.Notify(&DragDropClientObserver::OnDragCompleted, *event) and then ResetDragDropTarget(/*send_exit=*/false) on the now-freed object, as the commit message states (“continued executing on this after DropIfAllowed returned, notifying observers and resetting the target on a freed object”).
The fix moves the OnDragCompleted notification to before DropIfAllowed is called, captures auto alive = weak_factory_.GetWeakPtr() at the top of OnDragDrop, and guards the trailing ResetDragDropTarget with if (alive), so no member is touched once this may be gone. The same pattern (a weak_factory_.GetWeakPtr() liveness check after an observers_.Notify(...) call) is added to two other drag handlers in the file that likewise notify observers and could re-enter and free this.
this, yet OnDragDrop kept using this (observer notification and target reset) after that point. The fix reorders the observer notification ahead of the drop and gates every subsequent member access on a WeakPtr liveness check, so a self-destroying drop no longer leaves the method running on freed memory.Attack Path
- Start a drag-and-drop over a Views/Ozone target
On a platform using the Ozone backend, the attacker (or a page/UI flow) drives a drag session so that
DesktopDragDropClientOzoneacquires a target anddata_to_drop_is set. (inferred: the exact UI/content that initiates the drag is not shown in the diff.) - Trigger the drop
OnDragDropobtainsdrop_cbfromdelegate_->GetDropCallback(*event)and callsDropIfAllowed(...), which per the commit message can run the drop callback synchronously. - Destroy the client from inside the drop handler
The drop handler tears down the target view/window, which destroys the owning
DesktopDragDropClientOzone(this); the shipped testDestroyClientDuringDropreproduces this by having the delegate’sPerformDroprunon_drop_callback_=ResetClient, which callsclient_.reset()mid-drop. - Use freed `this`
Control returns into
OnDragDrop, which (pre-patch) calledobservers_.Notify(&DragDropClientObserver::OnDragCompleted, *event)andResetDragDropTarget(/*send_exit=*/false)on the freed object — the use-after-free. - Escalation Any read/write primitive, memory corruption, or code execution built on top of this freed-object access is (inferred); the patch only demonstrates the invalid access, not a concrete downstream sink.
Impact Assessment
DesktopDragDropClientOzone in the browser process on Ozone/Linux, exploitable through the object’s members touched after free (observers_ notification and ResetDragDropTarget). The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.1. A drop path that synchronously destroys the target view/window; the advisory classifies this as a UAF in Views but states no severity, and reports no in-the-wild exploitation. Escalation to memory corruption or code execution is (inferred) and not established by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc |
modified | |
ifui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc |
modified | |
TEST_Fui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc |
modified |
Files Changed
ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.ccui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
Audit Directions
- Synchronous callbacks that can free `this`Audit any method that runs a delegate/observer callback (
GetDropCallback,observers_.Notify,DropIfAllowed, drop closures) and then continues touching members; require aWeakPtrliveness check or reorder the member use before the re-entrant call. - Observer notifications ordered after a destructive sinkLook for
observers_.Notify(...)or state resets placed after a call that may destroy the notifier; the fix here worked partly by movingOnDragCompletedahead ofDropIfAllowed, so verify notify-before-mutate ordering elsewhere in drag/drop and Views lifetime code. - Trailing cleanup on drag/window teardown pathsReview other
DragDropClient/ Ozone /desktop_aurahandlers (OnDragLeave,DragCancel,ResetDragDropTargetcallers) where a target view or window can be destroyed mid-operation, ensuring post-callback cleanup is gated on object liveness.
Patch
From 3d5bba92b661705c5e5b37c38303d04908c628b7 Mon Sep 17 00:00:00 2001 From: Tom Anderson <[email protected]> Date: Wed, 02 Sep 2026 13:53:53 -0700 Subject: [PATCH] [Ozone] Prevent Use-After-Free in DesktopDragDropClientOzone::OnDragDrop During OnDragDrop, DropIfAllowed can invoke the drop callback synchronously. If the target view or window is destroyed as part of the drop handler, DesktopDragDropClientOzone is destroyed as well. Previously, OnDragDrop continued executing on `this` after DropIfAllowed returned, notifying observers and resetting the target on a freed object. Fix this by: 1. Notifying DragDropClientObserver::OnDragCompleted before DropIfAllowed is called. 2. Tracking DesktopDragDropClientOzone lifetime with a WeakPtr across observer and drop callbacks. 3. Guarding subsequent calls with liveness checks. Fixed: 532916987 Change-Id: Iebe9d6d3cc1fe893fcfaf422fb33924d78675606 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8347108 Reviewed-by: Thomas Lukaszewicz <[email protected]> Commit-Queue: Thomas Anderson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1691070} --- diff --git a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc index 123561d..51cbdb37 100644 --- a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc +++ b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc @@ -94,7 +94,7 @@ // stack uninitialised) or if there are rules allowing the data transfer. // Otherwise the drag is cancelled. void DropIfAllowed(const ui::OSExchangeData* drag_data, - aura::client::DragUpdateInfo& drag_info, + const aura::client::DragUpdateInfo& drag_info, base::OnceClosure drop_cb) { if (ui::DataTransferPolicyController::HasInstance()) { ui::DataTransferPolicyController::Get()->DropIfAllowed( @@ -215,6 +215,9 @@ if (!drag_succeeded) { selected_operation_ = DragOperation::kNone; observers_.Notify(&aura::client::DragDropClientObserver::OnDragCancelled); + if (!alive) { + return DragOperation::kNone; + } } if (cursor_client) { @@ -295,8 +298,12 @@ int client_operation = ui::DragDropTypes::DRAG_NONE; auto event = UpdateTargetAndCreateDropEvent(); if (event) { + auto alive = weak_factory_.GetWeakPtr(); observers_.Notify(&aura::client::DragDropClientObserver::OnDragUpdated, *event); + if (!alive) { + return ui::DragDropTypes::DRAG_NONE; + } if (delegate_) { current_drag_update_info_ = delegate_->OnDragUpdated(*event); client_operation = current_drag_update_info_.drag_operation; @@ -307,12 +314,19 @@ void DesktopDragDropClientOzone::OnDragDrop(int modifiers) { modifiers_ = modifiers; + auto alive = weak_factory_.GetWeakPtr(); // Ensure |data_to_drop_| is set, so crashes, such as // https://crbug.com/1151836, are avoided. if (data_to_drop_) { auto event = UpdateTargetAndCreateDropEvent(); if (delegate_ && event) { if (auto drop_cb = delegate_->GetDropCallback(*event)) { + observers_.Notify( + &aura::client::DragDropClientObserver::OnDragCompleted, *event); + if (!alive) { + return; + } + base::ScopedClosureRunner drag_cancel( base::BindOnce(&DesktopDragDropClientOzone::DragCancel, weak_factory_.GetWeakPtr())); @@ -322,13 +336,12 @@ data_to_drop_raw, current_drag_update_info_, base::BindOnce(&PerformDrop, std::move(drop_cb), std::move(data_to_drop_), std::move(drag_cancel))); - - observers_.Notify( - &aura::client::DragDropClientObserver::OnDragCompleted, *event); } } } - ResetDragDropTarget(/*send_exit=*/false); + if (alive) { + ResetDragDropTarget(/*send_exit=*/false); + } } void DesktopDragDropClientOzone::OnDragLeave() { diff --git a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc index 1f6b3d5f7..3e263ab8 100644 --- a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc +++ b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc @@ -217,6 +217,10 @@ destination_operation_ = operation; } + void set_on_drop_callback(base::RepeatingClosure callback) { + on_drop_callback_ = std::move(callback); + } + private: // aura::client::DragDropDelegate: void OnDragEntered(const ui::DropTargetEvent& event) override { @@ -239,7 +243,7 @@ DropCallback GetDropCallback(const ui::DropTargetEvent& event) override { last_event_flags_ = event.flags(); return base::BindOnce(&FakeDragDropDelegate::PerformDrop, - base::Unretained(this)); + weak_factory_.GetWeakPtr()); } void PerformDrop(std::unique_ptr<ui::OSExchangeData> data, @@ -248,6 +252,9 @@ ++num_drops_; received_data_ = std::move(data); output_drag_op = destination_operation_; + if (on_drop_callback_) { + on_drop_callback_.Run(); + } } int num_enters_ = 0; @@ -257,6 +264,8 @@ std::unique_ptr<ui::OSExchangeData> received_data_; DragOperation destination_operation_; int last_event_flags_ = ui::EF_NONE; + base::RepeatingClosure on_drop_callback_; + base::WeakPtrFactory<FakeDragDropDelegate> weak_factory_{this}; }; } // namespace @@ -277,6 +286,15 @@ platform_window_->set_modifiers(modifiers); } + void ResetClient() { + SetWmDropHandler(platform_window_.get(), nullptr); + client_.reset(); + } + + base::WeakPtr<DesktopDragDropClientOzoneTest> GetWeakPtr() { + return weak_factory_.GetWeakPtr(); + } + DragOperation StartDragAndDrop(int allowed_operations) { auto data = std::make_unique<ui::OSExchangeData>(); data->SetString(u"Test"); @@ -335,6 +353,8 @@ // The widget used to initiate drags. std::unique_ptr<Widget> widget_; + + base::WeakPtrFactory<DesktopDragDropClientOzoneTest> weak_factory_{this}; }; TEST_F(DesktopDragDropClientOzoneTest, StartDrag) { @@ -601,4 +621,27 @@ EXPECT_EQ(DragOperation::kNone, operation); } +TEST_F(DesktopDragDropClientOzoneTest, DestroyClientDuringDrop) { + dragdrop_delegate_->SetOperation(DragOperation::kMove); + dragdrop_delegate_->set_on_drop_callback(base::BindRepeating( + &DesktopDragDropClientOzoneTest::ResetClient, GetWeakPtr())); + + const std::u16string sample_data = u"DestroyDuringDrop"; + std::unique_ptr<ui::OSExchangeData> data = + std::make_unique<ui::OSExchangeData>(); + data->SetString(sample_data); + + int suggested_operation = + ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE; + platform_window_->OnDragEnter(kStartDragLocation, std::move(data), + suggested_operation); + platform_window_->OnDragMotion(kStartDragLocation, suggested_operation); + platform_window_->OnDragDrop(); + platform_window_->OnDragLeave(); + + EXPECT_EQ(1, dragdrop_delegate_->num_enters()); + EXPECT_EQ(1, dragdrop_delegate_->num_updates()); + EXPECT_EQ(1, dragdrop_delegate_->num_drops()); +} + } // namespace views
Regression Test / PoC
diff --git a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
index 1f6b3d5f7..3e263ab8 100644
--- a/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
+++ b/ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
@@ -217,6 +217,10 @@
destination_operation_ = operation;
}
+ void set_on_drop_callback(base::RepeatingClosure callback) {
+ on_drop_callback_ = std::move(callback);
+ }
+
private:
// aura::client::DragDropDelegate:
void OnDragEntered(const ui::DropTargetEvent& event) override {
@@ -239,7 +243,7 @@
DropCallback GetDropCallback(const ui::DropTargetEvent& event) override {
last_event_flags_ = event.flags();
return base::BindOnce(&FakeDragDropDelegate::PerformDrop,
- base::Unretained(this));
+ weak_factory_.GetWeakPtr());
}
void PerformDrop(std::unique_ptr<ui::OSExchangeData> data,
@@ -248,6 +252,9 @@
++num_drops_;
received_data_ = std::move(data);
output_drag_op = destination_operation_;
+ if (on_drop_callback_) {
+ on_drop_callback_.Run();
+ }
}
int num_enters_ = 0;
@@ -257,6 +264,8 @@
std::unique_ptr<ui::OSExchangeData> received_data_;
DragOperation destination_operation_;
int last_event_flags_ = ui::EF_NONE;
+ base::RepeatingClosure on_drop_callback_;
+ base::WeakPtrFactory<FakeDragDropDelegate> weak_factory_{this};
};
} // namespace
@@ -277,6 +286,15 @@
platform_window_->set_modifiers(modifiers);
}
+ void ResetClient() {
+ SetWmDropHandler(platform_window_.get(), nullptr);
+ client_.reset();
+ }
+
+ base::WeakPtr<DesktopDragDropClientOzoneTest> GetWeakPtr() {
+ return weak_factory_.GetWeakPtr();
+ }
+
DragOperation StartDragAndDrop(int allowed_operations) {
auto data = std::make_unique<ui::OSExchangeData>();
data->SetString(u"Test");
@@ -335,6 +353,8 @@
// The widget used to initiate drags.
std::unique_ptr<Widget> widget_;
+
+ base::WeakPtrFactory<DesktopDragDropClientOzoneTest> weak_factory_{this};
};
TEST_F(DesktopDragDropClientOzoneTest, StartDrag) {
@@ -601,4 +621,27 @@
EXPECT_EQ(DragOperation::kNone, operation);
}
+TEST_F(DesktopDragDropClientOzoneTest, DestroyClientDuringDrop) {
+ dragdrop_delegate_->SetOperation(DragOperation::kMove);
+ dragdrop_delegate_->set_on_drop_callback(base::BindRepeating(
+ &DesktopDragDropClientOzoneTest::ResetClient, GetWeakPtr()));
+
+ const std::u16string sample_data = u"DestroyDuringDrop";
+ std::unique_ptr<ui::OSExchangeData> data =
+ std::make_unique<ui::OSExchangeData>();
+ data->SetString(sample_data);
+
+ int suggested_operation =
+ ui::DragDropTypes::DRAG_COPY | ui::DragDropTypes::DRAG_MOVE;
+ platform_window_->OnDragEnter(kStartDragLocation, std::move(data),
+ suggested_operation);
+ platform_window_->OnDragMotion(kStartDragLocation, suggested_operation);
+ platform_window_->OnDragDrop();
+ platform_window_->OnDragLeave();
+
+ EXPECT_EQ(1, dragdrop_delegate_->num_enters());
+ EXPECT_EQ(1, dragdrop_delegate_->num_updates());
+ EXPECT_EQ(1, dragdrop_delegate_->num_drops());
+}
+
} // namespace views