High watch CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
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.
Tracker498832921
Fix commit43bf0cbc0d5d (chromium/src) +63/-10
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-02 (per vendor advisory)
Fix landed2026-04-03 (public git)
Disclosed2026-05-05
Patch gap32 days — fix public in source before the advisory

Background

`aura::Window`
a node in Chromium’s Aura window hierarchy that holds a children_ list and notifies observers on hierarchy and root-attachment changes.
`WindowObserver`
an interface whose callbacks (e.g., OnWindowAddedToRootWindow) run during notification and may synchronously mutate the window hierarchy.
`WindowTracker`
a helper that holds windows as tracked (weak) references and clears entries automatically when a window is destroyed, allowing safe iteration while the set changes.
`DCHECK_GE` vs `CHECK_GE`
DCHECK_* is compiled out of release builds, whereas CHECK_* enforces the condition in all builds and crashes safely on violation.

Root Cause Analysis

Two related memory-safety issues are fixed. In ui/aura/window.cc, NotifyRemovingFromRootWindow, NotifyAddedToRootWindow, and NotifyWindowHierarchyChangeDown iterated over children_ with a raw Window::Windows::const_iterator; because the per-child notification can re-enter an observer that mutates children_ (e.g., removing a sibling), the iterator and the pointed-to child could be invalidated/freed mid-loop, a use-after-free.

The fix snapshots the children into a WindowTracker and drains it with tracker.Pop() inside a while (!tracker.windows().empty()) loop, so destroyed windows drop out of the tracker instead of being dereferenced through a stale iterator. Separately, in ash/wm/window_state.cc the transient-children-reparenting path validated container_id with DCHECK_GE(container_id, 0), which is absent in release builds; a negative container_id (notably -1, the Wallpaper Window) would then be passed to dst_root->GetChildById(container_id), causing incorrect reparenting and subsequent iterator invalidation per the commit message. Upgrading to CHECK_GE(container_id, 0) TIGHTENS the check so the invalid-ID path is caught in all builds before the bad reparent occurs. The shipped regression test (MutateHierarchyDuringNotifyAddedToRootWindow) confirms the iteration mechanism: an observer on w2 removes w1 during notification and the test asserts no crash.

Key insight
The core mistake was iterating a mutable child collection with a raw iterator across re-entrant observer callbacks (and letting a negative container ID slip through a release-stripped DCHECK); the fix switches to a WindowTracker-based drain loop and promotes the ID guard to a CHECK enforced in every build.

Attack Path

  1. Set up a hierarchy with an observer As the regression test shows, a parent window holds children w1, w2, w3, and a WindowObserver is attached to w2.
  2. Begin notification iteration A root-attachment or hierarchy change makes the parent iterate its children_, notifying w1, then w2.
  3. Mutate the hierarchy mid-loop w2’s observer removes w1 (or otherwise mutates children_) during its callback, which under the old code invalidates the in-progress const_iterator.
  4. Dereference freed/invalid state Continued iteration dereferences the invalidated iterator / freed child window, the use-after-free.
  5. Alternate trigger (Ash ID clash) A transient child whose parent container has ID -1 (Wallpaper Window) reaches MoveAllTransientChildrenToNewRoot, where the release-build-stripped DCHECK_GE lets the negative ID through to GetChildById, causing incorrect reparenting and iterator invalidation.

Impact Assessment

An attacker able to drive window-hierarchy changes while an observer mutates the hierarchy (or to produce the negative-container-ID condition) can trigger a use-after-free in the browser/UI process on Aura/Ash platforms, rated high severity by the vendor advisory. Preconditions are the ability to influence window observers and hierarchy mutations during notification. No in-the-wild exploitation is reported, and any escalation from the UAF to code execution is (inferred) and not demonstrated by the patch.

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

Audit Directions

  • Raw iteration across observer callbacks
    Flag any loop over a container (children_, observer-adjacent lists) that invokes callbacks able to mutate that container; prefer WindowTracker / weak snapshots or index-safe drain loops.
  • `DCHECK` guarding a memory-safety precondition
    Search for DCHECK_* protecting values (IDs, indices, sizes) that flow into pointer lookups; these are no-ops in release and should be CHECK_* when a violation enables corruption.
  • Negative/sentinel container IDs
    Audit GetChildById-style lookups and reparenting code for sentinel IDs (e.g., -1 Wallpaper Window) that could cause unintended target selection and subsequent hierarchy corruption.
From 43bf0cbc0d5d3a2183c46833dd172c016a57815b Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
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 <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
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 [email protected]

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