Overview

High
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Views
DescriptionUse after free in Views
ComponentViews
Sub-componentOzone
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker532916987
Fix commit3d5bba92b661 (chromium/src) +62/-6
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:L/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-07-09 (per vendor advisory)
Fix landed2026-09-02 (public git)
Disclosed2026-09-08
Patch gap6 days — fix public in source before the advisory

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.cc that consults ui::DataTransferPolicyController and, 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 the DesktopDragDropClientOzone (this).
`base::WeakPtr` / `weak_factory_`
a weak reference that becomes null when its owning object is destroyed, letting a method detect that this was 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.

Key insight
The single mistake is re-entrancy: a synchronous drop callback can free 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

  1. 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 DesktopDragDropClientOzone acquires a target and data_to_drop_ is set. (inferred: the exact UI/content that initiates the drag is not shown in the diff.)
  2. Trigger the drop OnDragDrop obtains drop_cb from delegate_->GetDropCallback(*event) and calls DropIfAllowed(...), which per the commit message can run the drop callback synchronously.
  3. 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 test DestroyClientDuringDrop reproduces this by having the delegate’s PerformDrop run on_drop_callback_ = ResetClient, which calls client_.reset() mid-drop.
  4. Use freed `this` Control returns into OnDragDrop, which (pre-patch) called observers_.Notify(&DragDropClientObserver::OnDragCompleted, *event) and ResetDragDropTarget(/*send_exit=*/false) on the freed object — the use-after-free.
  5. 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

An attacker who can cause a drop handler to destroy the drag target during a synchronous drop gains a use-after-free of 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

FunctionChangeNotes
if
ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc
modified
if
ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
modified
TEST_F
ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone_unittest.cc
modified

Files Changed

  • ui/views/widget/desktop_aura/desktop_drag_drop_client_ozone.cc
  • ui/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 a WeakPtr liveness check or reorder the member use before the re-entrant call.
  • Observer notifications ordered after a destructive sink
    Look for observers_.Notify(...) or state resets placed after a call that may destroy the notifier; the fix here worked partly by moving OnDragCompleted ahead of DropIfAllowed, so verify notify-before-mutate ordering elsewhere in drag/drop and Views lifetime code.
  • Trailing cleanup on drag/window teardown paths
    Review other DragDropClient / Ozone / desktop_aura handlers (OnDragLeave, DragCancel, ResetDragDropTarget callers) where a target view or window can be destroyed mid-operation, ensuring post-callback cleanup is gated on object liveness.
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
Loading diff…

Regression Test / PoC

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

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.