Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Aura
DescriptionUse after free in Aura
ComponentAura
Bug ClassUAF
Tracker498832921
Fix commit43bf0cbc0d5d (chromium/src) +63/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
HierarchyMutatingObserver
ui/aura/window_unittest.cc
modified
if
ui/aura/window_unittest.cc
modified
TEST_F
ui/aura/window_unittest.cc
modified
TestLayerAnimationObserver
ui/aura/window_unittest.cc
modified

Files Changed

  • ash/wm/window_state.cc
  • ui/aura/window.cc
  • ui/aura/window_unittest.cc
From 43bf0cbc0d5d3a2183c46833dd172c016a57815b Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Fri, 03 Apr 2026 13:21:20 -0700
Subject: [PATCH] Fix iterator invalidation in aura::Window and Ash ID clash

This CL addresses two related issues that could lead to memory safety
vulnerabilities:

1. Update aura::Window notification loops to use WindowTracker.
   NotifyRemovingFromRootWindow, NotifyAddedToRootWindow, and
   NotifyWindowHierarchyChangeDown now safely handle potential hierarchy
   mutations during child iteration.

2. Upgrade ID validation in MoveAllTransientChildrenToNewRoot.
   Upgraded the ID validation from DCHECK_GE to CHECK_GE to ensure that
   container_id is non-negative in all builds. This prevents incorrect
   reparenting to the Wallpaper Window (which often has an ID of -1)
   and subsequent iterator invalidation.

Fixed: 498832921
Change-Id: Ie38a4f864ffdc0baa6c59d83c3f4e59babbe2528
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7726886
Reviewed-by: Mitsuru Oshima <oshima@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609938}
---

diff --git a/ash/wm/window_state.cc b/ash/wm/window_state.cc
index d34bdeb..70c2f2c 100644
--- a/ash/wm/window_state.cc
+++ b/ash/wm/window_state.cc
@@ -40,6 +40,7 @@
 #include "ash/wm/wm_event.h"
 #include "ash/wm/wm_metrics.h"
 #include "base/check_is_test.h"
+#include "base/check_op.h"
 #include "base/containers/adapters.h"
 #include "base/containers/fixed_flat_map.h"
 #include "base/debug/crash_logging.h"
@@ -265,7 +266,7 @@
     if (!transient_child->parent())
       continue;
     const int container_id = transient_child->parent()->GetId();
-    DCHECK_GE(container_id, 0);
+    CHECK_GE(container_id, 0);
     aura::Window* container = dst_root->GetChildById(container_id);
     if (container->Contains(transient_child))
       continue;
diff --git a/ui/aura/window.cc b/ui/aura/window.cc
index 832b4fc..1437ffcb 100644
--- a/ui/aura/window.cc
+++ b/ui/aura/window.cc
@@ -1226,9 +1226,10 @@
     UnregisterFrameSinkId();
   for (WindowObserver& observer : observers_)
     observer.OnWindowRemovingFromRootWindow(this, new_root);
-  for (Window::Windows::const_iterator it = children_.begin();
-       it != children_.end(); ++it) {
-    (*it)->NotifyRemovingFromRootWindow(new_root);
+
+  WindowTracker tracker(children_);
+  while (!tracker.windows().empty()) {
+    tracker.Pop()->NotifyRemovingFromRootWindow(new_root);
   }
 }
 
@@ -1237,9 +1238,10 @@
     RegisterFrameSinkId();
   for (WindowObserver& observer : observers_)
     observer.OnWindowAddedToRootWindow(this);
-  for (Window::Windows::const_iterator it = children_.begin();
-       it != children_.end(); ++it) {
-    (*it)->NotifyAddedToRootWindow();
+
+  WindowTracker tracker(children_);
+  while (!tracker.windows().empty()) {
+    tracker.Pop()->NotifyAddedToRootWindow();
   }
 }
 
@@ -1261,9 +1263,9 @@
 void Window::NotifyWindowHierarchyChangeDown(
     const WindowObserver::HierarchyChangeParams& params) {
   NotifyWindowHierarchyChangeAtReceiver(params);
-  for (Window::Windows::const_iterator it = children_.begin();
-       it != children_.end(); ++it) {
-    (*it)->NotifyWindowHierarchyChangeDown(params);
+  WindowTracker tracker(children_);
+  while (!tracker.windows().empty()) {
+    tracker.Pop()->NotifyWindowHierarchyChangeDown(params);
   }
 }
 
diff --git a/ui/aura/window_unittest.cc b/ui/aura/window_unittest.cc
index e57db79..c59b14ef 100644
--- a/ui/aura/window_unittest.cc
+++ b/ui/aura/window_unittest.cc
@@ -13,6 +13,7 @@
 
 #include "base/compiler_specific.h"
 #include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
@@ -3592,6 +3593,55 @@
   }
 }
 
+namespace {
+
+class HierarchyMutatingObserver : public WindowObserver {
+ public:
+  explicit HierarchyMutatingObserver(Window* window_to_remove)
+      : window_to_remove_(window_to_remove) {}
+
+  HierarchyMutatingObserver(const HierarchyMutatingObserver&) = delete;
+  HierarchyMutatingObserver& operator=(const HierarchyMutatingObserver&) =
+      delete;
+
+  void OnWindowAddedToRootWindow(Window* window) override {
+    if (window_to_remove_) {
+      window_to_remove_->parent()->RemoveChild(window_to_remove_);
+      window_to_remove_ = nullptr;
+    }
+  }
+
+ private:
+  raw_ptr<Window> window_to_remove_;
+};
+
+}  // namespace
+
+// Tests that modifying the hierarchy during NotifyAddedToRootWindow doesn't
+// cause a crash.
+TEST_F(WindowTest, MutateHierarchyDuringNotifyAddedToRootWindow) {
+  std::unique_ptr<Window> parent_window(CreateTestWindow({.window_id = 0}));
+  std::unique_ptr<Window> w1(CreateTestWindow({.window_id = 1}));
+  std::unique_ptr<Window> w2(CreateTestWindow({.window_id = 2}));
+  std::unique_ptr<Window> w3(CreateTestWindow({.window_id = 3}));
+
+  parent_window->AddChild(w1.get());
+  parent_window->AddChild(w2.get());
+  parent_window->AddChild(w3.get());
+
+  // Add an observer to w2 that removes w1.
+  // When parent_window is added to the root, it will notify its children: w1,
+  // w2, w3.
+  // 1. Notify w1.
+  // 2. Notify w2. w2's observer removes w1.
+  // 3. Notify w3.
+  HierarchyMutatingObserver observer(w1.get());
+  base::ScopedObservation<Window, WindowObserver> observation(&observer);
+  observation.Observe(w2.get());
+
+  root_window()->AddChild(parent_window.get());
+}
+
 class TestLayerAnimationObserver : public ui::LayerAnimationObserver {
  public:
   TestLayerAnimationObserver()
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/aura/window_unittest.cc b/ui/aura/window_unittest.cc
index e57db79..c59b14ef 100644
--- a/ui/aura/window_unittest.cc
+++ b/ui/aura/window_unittest.cc
@@ -13,6 +13,7 @@
 
 #include "base/compiler_specific.h"
 #include "base/memory/raw_ptr.h"
+#include "base/scoped_observation.h"
 #include "base/strings/string_number_conversions.h"
 #include "base/strings/string_util.h"
 #include "base/strings/stringprintf.h"
@@ -3592,6 +3593,55 @@
   }
 }
 
+namespace {
+
+class HierarchyMutatingObserver : public WindowObserver {
+ public:
+  explicit HierarchyMutatingObserver(Window* window_to_remove)
+      : window_to_remove_(window_to_remove) {}
+
+  HierarchyMutatingObserver(const HierarchyMutatingObserver&) = delete;
+  HierarchyMutatingObserver& operator=(const HierarchyMutatingObserver&) =
+      delete;
+
+  void OnWindowAddedToRootWindow(Window* window) override {
+    if (window_to_remove_) {
+      window_to_remove_->parent()->RemoveChild(window_to_remove_);
+      window_to_remove_ = nullptr;
+    }
+  }
+
+ private:
+  raw_ptr<Window> window_to_remove_;
+};
+
+}  // namespace
+
+// Tests that modifying the hierarchy during NotifyAddedToRootWindow doesn't
+// cause a crash.
+TEST_F(WindowTest, MutateHierarchyDuringNotifyAddedToRootWindow) {
+  std::unique_ptr<Window> parent_window(CreateTestWindow({.window_id = 0}));
+  std::unique_ptr<Window> w1(CreateTestWindow({.window_id = 1}));
+  std::unique_ptr<Window> w2(CreateTestWindow({.window_id = 2}));
+  std::unique_ptr<Window> w3(CreateTestWindow({.window_id = 3}));
+
+  parent_window->AddChild(w1.get());
+  parent_window->AddChild(w2.get());
+  parent_window->AddChild(w3.get());
+
+  // Add an observer to w2 that removes w1.
+  // When parent_window is added to the root, it will notify its children: w1,
+  // w2, w3.
+  // 1. Notify w1.
+  // 2. Notify w2. w2's observer removes w1.
+  // 3. Notify w3.
+  HierarchyMutatingObserver observer(w1.get());
+  base::ScopedObservation<Window, WindowObserver> observation(&observer);
+  observation.Observe(w2.get());
+
+  root_window()->AddChild(parent_window.get());
+}
+
 class TestLayerAnimationObserver : public ui::LayerAnimationObserver {
  public:
   TestLayerAnimationObserver()
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential OOB Read in aura::Window notifications via iterator invalidation

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 without the security team.

Overview: Methods in aura::Window iterate over child windows using raw iterators while firing observer callbacks. If a callback modifies the window hierarchy, such as ChromeOS transient window reparenting, the iterator is invalidated. This causes a potential out-of-bounds read on the vector’s backing buffer, leading to a sandbox escape and RCE in the browser process.

Affected files:

  • ui/aura/window.cc

Estimated timestamp from git blame: 2025-08-19

Overview

There is a potential Out-Of-Bounds (OOB) read and Use-After-Free (UAF) vulnerability in the window hierarchy notification loops of aura::Window (ui/aura/window.cc). Methods like NotifyAddedToRootWindow, NotifyRemovingFromRootWindow, and NotifyWindowHierarchyChangeDown iterate over the children_ vector using a raw const_iterator while synchronously firing observer callbacks.

If an observer alters the window hierarchy during these loops (e.g., adding or removing a sibling window), the children_ vector is mutated, invalidating the iterator.

Vulnerability Details

In ChromeOS (Ash), a highly exploitable path exists via ash::WindowState::OnWindowAddedToRootWindow. When a window is moved across root windows (e.g., moving to a different display), NotifyAddedToRootWindow is called recursively. When the notification reaches a transient parent window, the WindowState observer triggers MoveAllTransientChildrenToNewRoot.

This function attempts to move the window’s transient children to the new root window by finding the corresponding container ID. Because typical renderer-backed windows retain the default ID of -1 (kInitialId), the search dst_root->GetChildById(-1) incorrectly resolves to the first window with ID -1, which is typically the system’s Wallpaper Window.

The logic then calls WallpaperWindow->AddChild(transient_child). If transient_child was a direct sibling of the transient parent, this reparenting implicitly calls RemoveChild on the parent whose children_ vector is currently being iterated.

When an element before the current iterator is removed from a std::vector, the remaining elements shift left and the vector’s size decreases. The iterator now points to the new end(). At the end of the loop body, ++it increments the iterator past the new end(). The loop condition it != children_.end() evaluates to true, and the loop continues, dereferencing an out-of-bounds pointer.

Impact

The OOB read accesses adjacent heap memory outside the vector’s backing buffer. An attacker can groom the heap to place a fake Window object in this space. The loop executes a virtual method call (*it)->NotifyAddedToRootWindow(), allowing the attacker to hijack control flow, resulting in potential Remote Code Execution (RCE) and Sandbox Escape in the highly privileged Browser process. BRP (MiraclePtr) does not mitigate this, as the UAF/OOB occurs on the vector’s backing buffer, not the pointed-to object.

Potential Attacker Steps

Note: These are suggested/potential steps derived from code analysis; our tooling agent does not currently have the capability to run code or provide a working Proof of Concept.

  1. Setup: A compromised renderer creates an aura::Window hierarchy containing a parent (W_parent), a transient parent (Child1), and a transient child (W_transient).
  2. Stacking Manipulation: The attacker carefully orders the creation so that W_transient is located before Child1 in W_parent’s children_ vector (e.g., by adding the transient child to the parent before the transient parent).
  3. Trigger Move: The attacker initiates a window move across root windows (e.g., via the Multi-Screen Window Placement API or a fullscreen transition).
  4. Iterator Invalidation: W_parent->NotifyAddedToRootWindow() begins iterating over its children. It skips W_transient (index 0) and processes Child1 (index 1).
  5. Reparenting: The observer for Child1 triggers MoveAllTransientChildrenToNewRoot, which incorrectly reparents W_transient to the Wallpaper Window due to the -1 ID clash.
  6. Exploitation: W_transient is erased from W_parent, shifting the vector. The iterator increments past the new end(), reads the attacker-groomed fake Window pointer from the heap, and executes a hijacked virtual call.

Suggested Fix

  1. Safe Iteration: Update NotifyAddedToRootWindow, NotifyRemovingFromRootWindow, and NotifyWindowHierarchyChangeDown in ui/aura/window.cc to use WindowTracker or iterate over a copy of the children_ vector. This pattern is already successfully utilized in NotifyWindowVisibilityChangedDown to prevent exactly this class of bug.
  2. Resolve ID Clash: Address the logic bug in MoveAllTransientChildrenToNewRoot (ash/wm/window_state.cc) so it correctly validates whether GetChildById is returning the intended parent container, particularly when dealing with the default -1 ID.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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