Overview

Critical
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Aura
DescriptionUse after free in Aura
ComponentAura
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.
Tracker516427761
Fix commitb1a95e642207 (chromium/src) +65/-4
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-25 (per vendor advisory)
Fix landed2026-07-22 (public git)
Disclosed2026-08-25
Patch gap34 days — fix public in source before the advisory

Background

`aura::Window`
The core Aura UI object representing a node in the window hierarchy tree, carrying parent/child links and a list of observers.
`ScopedDeleteBlocker`
A stack-scoped helper that increments a target Window’s delete_block_count_ in its constructor and decrements it in its destructor, turning any re-entrant delete of that window into a CHECK crash instead of a silent free.
`WindowObserver::OnWindowHierarchyChanging`
An observer callback fired during the “changing” phase of a reparent operation, before the hierarchy change is complete, into which arbitrary embedder code runs.
`HierarchyChangeParams`
The struct passed through the hierarchy-change notifications, holding raw pointers to target, receiver, old_parent, and new_parent.

Root Cause Analysis

When a window is reparented, Window::NotifyWindowHierarchyChange walked the subtree and delivered OnWindowHierarchyChanging to every observer, but its only guard was a single ScopedDeleteBlocker blocker(this) protecting the notifying window — it left params.old_parent, params.new_parent, and each per-node receiver unprotected. Because NotifyWindowHierarchyChangeDown recurses through the subtree and dispatches to receivers, a WindowObserver reached during that walk could synchronously delete its observed window (a receiver, or either parent) from inside OnWindowHierarchyChanging, freeing an object whose address the notification machinery still held in HierarchyChangeParams and continued to dereference.

The fix moves the receiver guard into Window::NotifyWindowHierarchyChangeAtReceiver as ScopedDeleteBlocker blocker(this) so each node is protected while it is being notified, and adds old_blocker/new_blocker in NotifyWindowHierarchyChange covering both parents across the whole subtree walk. To let those parent pointers (which may legitimately be null, e.g. an initial parent) be wrapped unconditionally, ScopedDeleteBlocker is made null-safe so a nullptr window is a no-op rather than a null-pointer crash. Together these convert a re-entrant use-after-free into a deterministic CHECK failure, which the new DeleteReceiverInOnWindowHierarchyChanging death test locks in.

Key insight
The single mistake was scoping deletion protection to only the notifying window while leaving the receiver and both parents deletable during re-entrant observer callbacks; the fix extends ScopedDeleteBlocker coverage to the receiver and to old_parent/new_parent (made null-safe) so no participant in the notification can be freed mid-dispatch.

Attack Path

  1. Install a malicious observer Embedder-reachable code registers a WindowObserver on a descendant window that will act as a receiver during an upcoming reparent, mirroring the test’s DeleteOnHierarchyChangingObserver on grandchild.
  2. Trigger the reparent A page or UI action causes a window hierarchy change (e.g. AddChild), invoking NotifyWindowHierarchyChange and the recursive NotifyWindowHierarchyChangeDown walk over the subtree.
  3. Delete during the callback When the observed receiver is notified, the observer’s OnWindowHierarchyChanging synchronously deletes that window while it is unprotected, freeing it out from under the in-flight notification.
  4. Dangling dereference The notification loop and HierarchyChangeParams continue to use the freed receiver (or freed old_parent/new_parent) pointer, yielding a use-after-free the attacker can shape into a controlled crash or corruption.

Impact Assessment

An attacker who can drive a window reparent and run observer code gains a use-after-free of an aura::Window in the browser process, where Aura runs, enabling controlled freeing and reuse of window memory during hierarchy notifications. Exploitation requires the ability to install or reach a WindowObserver and induce a hierarchy change that notifies it, after which re-entrant deletion of the receiver or a parent triggers the flaw. Post-fix, such a deletion is caught as a CHECK/EXPECT_DEATH rather than being exploitable.

Changed Functions

FunctionChangeNotes
switch
ui/aura/window.cc
modified
ScopedDeleteBlocker
ui/aura/window.h
modified
if
ui/aura/window.h
modified
DeleteOnHierarchyChangingObserver
ui/aura/window_unittest.cc
modified
TEST_F
ui/aura/window_unittest.cc
modified
DeleteOnVisibilityChangedObserver
ui/aura/window_unittest.cc
modified

Files Changed

  • ui/aura/window_unittest.cc
  • ui/aura/window.cc
  • ui/aura/window.h

Audit Directions

  • Partial delete-blocker coverage
    Audit every notification or observer-dispatch path that uses ScopedDeleteBlocker and confirm it guards all pointers it keeps live (receiver, parents, siblings), not just the notifying this.
  • Re-entrant deletion in observer callbacks
    Review other WindowObserver callbacks such as visibility, bounds, and removal notifications for paths that dereference pointers after an embedder callback that could delete them.
  • Raw pointers in params structs
    Scrutinize structs like HierarchyChangeParams that carry raw Window* fields across recursive dispatch, ensuring each referenced object’s lifetime is pinned for the full duration of the walk.
From b1a95e642207618fbc409a73ab5ad0d8b4ce42ac Mon Sep 17 00:00:00 2001
From: Mitsuru Oshima <[email protected]>
Date: Tue, 21 Jul 2026 23:08:37 -0700
Subject: [PATCH] Block the deletion of new/old_parent, and receiver window

during the window hierachy change notifications.

Fixed: 516427761
Test: covered by unittest
Change-Id: I72aaf6c940ed8883eecaa3efac3db3fe0d20d333
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8127340
Commit-Queue: Mitsuru Oshima <[email protected]>
Reviewed-by: Achuith Bhandarkar <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1666030}
---
diff --git a/ui/aura/window.cc b/ui/aura/window.cc
index fa0951a..80574027 100644
--- a/ui/aura/window.cc
+++ b/ui/aura/window.cc
@@ -1501,7 +1501,12 @@
 
 void Window::NotifyWindowHierarchyChange(
     const WindowObserver::HierarchyChangeParams& params) {
-  ScopedDeleteBlocker blocker(this);
+
+  // Block deletion of old_parent and new_parent across all target
+  // sub-tree callbacks to ensure neither parent is destroyed before
+  // NotifyWindowHierarchyChangeUp() runs.
+  ScopedDeleteBlocker old_blocker(params.old_parent);
+  ScopedDeleteBlocker new_blocker(params.new_parent);
 
   params.target->NotifyWindowHierarchyChangeDown(params);
   switch (params.phase) {
@@ -1533,6 +1538,8 @@
 
 void Window::NotifyWindowHierarchyChangeAtReceiver(
     const WindowObserver::HierarchyChangeParams& params) {
+  ScopedDeleteBlocker blocker(this);
+
   WindowObserver::HierarchyChangeParams local_params = params;
   local_params.receiver = this;
 
diff --git a/ui/aura/window.h b/ui/aura/window.h
index ae8efc79..f743126 100644
--- a/ui/aura/window.h
+++ b/ui/aura/window.h
@@ -127,15 +127,22 @@
   // A helper class to ensure that the Window is not deleted while it is
   // notifying observers or doing other operations where re-entrant deletion
   // would be problematic. Attempting to delete the Window while a
-  // ScopedDeleteBlocker is active will cause a crash.
+  // ScopedDeleteBlocker is active will cause a crash. This is no-op if
+  // if nullptr is passed.
   class ScopedDeleteBlocker {
    public:
     explicit ScopedDeleteBlocker(aura::Window* window) : window_(window) {
-      window_->delete_block_count_++;
+      if (window_) {
+        window_->delete_block_count_++;
+      }
     }
     ScopedDeleteBlocker(const ScopedDeleteBlocker&) = delete;
     ScopedDeleteBlocker& operator=(const ScopedDeleteBlocker&) = delete;
-    ~ScopedDeleteBlocker() { window_->delete_block_count_--; }
+    ~ScopedDeleteBlocker() {
+      if (window_) {
+        window_->delete_block_count_--;
+      }
+    }
     raw_ptr<aura::Window> window_;
   };
 
diff --git a/ui/aura/window_unittest.cc b/ui/aura/window_unittest.cc
index 1324b2d..4c4538b 100644
--- a/ui/aura/window_unittest.cc
+++ b/ui/aura/window_unittest.cc
@@ -3595,6 +3595,53 @@
   EXPECT_TRUE(weak_window);
 }
 
+// WindowObserver implementation that deletes the observed window in
+// OnWindowHierarchyChanging().
+class DeleteOnHierarchyChangingObserver : public WindowObserver {
+ public:
+  explicit DeleteOnHierarchyChangingObserver(Window* window) : window_(window) {
+    window_->AddObserver(this);
+  }
+
+  DeleteOnHierarchyChangingObserver(const DeleteOnHierarchyChangingObserver&) =
+      delete;
+  DeleteOnHierarchyChangingObserver& operator=(
+      const DeleteOnHierarchyChangingObserver&) = delete;
+
+  ~DeleteOnHierarchyChangingObserver() override {
+    CHECK(window_);
+    window_->RemoveObserver(this);
+  }
+
+  // WindowObserver:
+  void OnWindowHierarchyChanging(const HierarchyChangeParams& params) override {
+    Window* window = window_;
+    window_ = nullptr;
+    window->RemoveObserver(this);
+    // This will fail with CHECK.
+    delete window;
+  }
+
+ private:
+  raw_ptr<Window> window_;
+};
+
+TEST_F(WindowDeathTest, DeleteReceiverInOnWindowHierarchyChanging) {
+  std::unique_ptr<Window> parent = std::make_unique<Window>(nullptr);
+  parent->Init(ui::LAYER_NOT_DRAWN);
+  std::unique_ptr<Window> child = std::make_unique<Window>(nullptr);
+  child->Init(ui::LAYER_NOT_DRAWN);
+  Window* grandchild = new Window(nullptr);
+  grandchild->Init(ui::LAYER_NOT_DRAWN);
+  child->AddChild(grandchild);
+
+  // |grandchild| is notified as a receiver while recursing down through the
+  // subtree rooted at |child|; deleting it from within the notification must
+  // not be allowed.
+  DeleteOnHierarchyChangingObserver observer(grandchild);
+  EXPECT_DEATH(parent->AddChild(child.get()), "");
+}
+
 // WindowObserver implementation that deletes a window in
 // OnWindowVisibilityChanged().
 class DeleteOnVisibilityChangedObserver : public WindowObserver {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/aura/window_unittest.cc b/ui/aura/window_unittest.cc
index 1324b2d..4c4538b 100644
--- a/ui/aura/window_unittest.cc
+++ b/ui/aura/window_unittest.cc
@@ -3595,6 +3595,53 @@
   EXPECT_TRUE(weak_window);
 }
 
+// WindowObserver implementation that deletes the observed window in
+// OnWindowHierarchyChanging().
+class DeleteOnHierarchyChangingObserver : public WindowObserver {
+ public:
+  explicit DeleteOnHierarchyChangingObserver(Window* window) : window_(window) {
+    window_->AddObserver(this);
+  }
+
+  DeleteOnHierarchyChangingObserver(const DeleteOnHierarchyChangingObserver&) =
+      delete;
+  DeleteOnHierarchyChangingObserver& operator=(
+      const DeleteOnHierarchyChangingObserver&) = delete;
+
+  ~DeleteOnHierarchyChangingObserver() override {
+    CHECK(window_);
+    window_->RemoveObserver(this);
+  }
+
+  // WindowObserver:
+  void OnWindowHierarchyChanging(const HierarchyChangeParams& params) override {
+    Window* window = window_;
+    window_ = nullptr;
+    window->RemoveObserver(this);
+    // This will fail with CHECK.
+    delete window;
+  }
+
+ private:
+  raw_ptr<Window> window_;
+};
+
+TEST_F(WindowDeathTest, DeleteReceiverInOnWindowHierarchyChanging) {
+  std::unique_ptr<Window> parent = std::make_unique<Window>(nullptr);
+  parent->Init(ui::LAYER_NOT_DRAWN);
+  std::unique_ptr<Window> child = std::make_unique<Window>(nullptr);
+  child->Init(ui::LAYER_NOT_DRAWN);
+  Window* grandchild = new Window(nullptr);
+  grandchild->Init(ui::LAYER_NOT_DRAWN);
+  child->AddChild(grandchild);
+
+  // |grandchild| is notified as a receiver while recursing down through the
+  // subtree rooted at |child|; deleting it from within the notification must
+  // not be allowed.
+  DeleteOnHierarchyChangingObserver observer(grandchild);
+  EXPECT_DEATH(parent->AddChild(child.get()), "");
+}
+
 // WindowObserver implementation that deletes a window in
 // OnWindowVisibilityChanged().
 class DeleteOnVisibilityChangedObserver : public WindowObserver {
Loading diff…

Original Bug Report

reported by [email protected]

Potential UAF in aura::Window notification traversal due to synchronous window destruction

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential Use-After-Free (UAF) vulnerability exists in the browser process within aura::Window notification methods. Observer callbacks can synchronously destroy the Window object before the methods attempt to read its children vector to instantiate a WindowTracker. This allows reading freed heap memory and can lead to a wild memory write in the browser process.

Affected files:

  • ui/aura/window.cc

Estimated timestamp from git blame: 2026-04-03

Root Cause

In ui/aura/window.cc, three recursive traversal functions are potentially vulnerable to a Use-After-Free (UAF) on the this pointer:

  • Window::NotifyRemovingFromRootWindow (at ui/aura/window.cc:1224)
  • Window::NotifyAddedToRootWindow (at ui/aura/window.cc:1236)
  • Window::NotifyWindowHierarchyChangeDown (at ui/aura/window.cc:1263)

These functions fail to establish a WindowTracker to track the liveness of this prior to executing their observer loops (unlike the fully hardened sibling NotifyWindowVisibilityChangedDown at ui/aura/window.cc:1314). If an observer callback synchronously deletes the window this, returning from the observer loop to initialize WindowTracker tracker(children_) results in a UAF read of the freed children_ member vector.

Potential Trigger Steps

Please note that these are potential trigger sequences identified via static code flow analysis; our automated tooling does not currently have the capability to execute code or provide a dynamic Proof of Concept.

  1. A window state modification (e.g., closing a panel or removing a tab) invokes Window::RemoveChildImpl on W’s parent (ui/aura/window.cc:1138).
  2. RemoveChildImpl detects that the root window is changing and invokes child->NotifyRemovingFromRootWindow(new_root_window) on W (ui/aura/window.cc:1146).
  3. Inside Window::NotifyRemovingFromRootWindow (ui/aura/window.cc:1224), the method begins iterating through observers_ and invokes observer.OnWindowRemovingFromRootWindow(this, new_root).
  4. One of the registered observers is the root window’s WindowEventDispatcher (ui/aura/window_event_dispatcher.cc). This invokes OnWindowHidden (ui/aura/window_event_dispatcher.cc:362), which calls invisible->CleanupGestureState().
  5. Window::CleanupGestureState() invokes env->gesture_recognizer()->CancelActiveTouches(this) (ui/aura/window.cc:1356).
  6. The GestureRecognizer cancels active touches and dispatches a touch-cancel event (ui::EventType::kTouchCancelled) to W’s delegate.
  7. The delegate synchronously deletes the window (e.g., calling delete window), deallocating W and its member vector children_.
  8. Control returns to Window::NotifyRemovingFromRootWindow. The range-based loop exits, and the instruction WindowTracker tracker(children_) attempts to access the deallocated children_ member vector of this, triggering the Use-After-Free.

Impact

This vulnerability runs in the unsandboxed Browser process. Furthermore, because this involves an implicit this dereference of an in-object member vector rather than a raw pointer field, MiraclePtr does not mitigate this access. If the freed heap slot is reclaimed before the vector constructor executes, an attacker can leverage this to trigger a wild memory write when WindowTracker::Add invokes window->AddObserver(this) on a reclaimed pointer.

Ensure that a WindowTracker is instantiated to track the liveness of this before executing observer loops in all three affected functions, and abort further processing if this is deleted. For example, implement the following pattern used in NotifyWindowVisibilityChangedDown:

void Window::NotifyRemovingFromRootWindow(Window* new_root) {
  if (frame_sink_id_.is_valid())
    UnregisterFrameSinkId();

  WindowTracker this_tracker;
  this_tracker.Add(this);

  for (WindowObserver& observer : observers_)
    observer.OnWindowRemovingFromRootWindow(this, new_root);

  if (!this_tracker.Contains(this))
    return;

  WindowTracker tracker(children_);
  while (!tracker.windows().empty()) {
    tracker.Pop()->NotifyRemovingFromRootWindow(new_root);
  }
}

Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker