CVE-2026-7919
Overview
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, whereasCHECK_*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.
DCHECK); the fix switches to a WindowTracker-based drain loop and promotes the ID guard to a CHECK enforced in every build.Attack Path
- Set up a hierarchy with an observer
As the regression test shows, a parent window holds children
w1,w2,w3, and aWindowObserveris attached tow2. - Begin notification iteration
A root-attachment or hierarchy change makes the parent iterate its
children_, notifyingw1, thenw2. - Mutate the hierarchy mid-loop
w2’s observer removesw1(or otherwise mutateschildren_) during its callback, which under the old code invalidates the in-progressconst_iterator. - Dereference freed/invalid state Continued iteration dereferences the invalidated iterator / freed child window, the use-after-free.
- Alternate trigger (Ash ID clash)
A transient child whose parent container has ID
-1(Wallpaper Window) reachesMoveAllTransientChildrenToNewRoot, where the release-build-strippedDCHECK_GElets the negative ID through toGetChildById, causing incorrect reparenting and iterator invalidation.
Impact Assessment
(inferred) and not demonstrated by the patch.Changed Functions
| Function | Change | Notes |
|---|---|---|
HierarchyMutatingObserverui/aura/window_unittest.cc |
modified | |
ifui/aura/window_unittest.cc |
modified | |
TEST_Fui/aura/window_unittest.cc |
modified | |
TestLayerAnimationObserverui/aura/window_unittest.cc |
modified |
Files Changed
ash/wm/window_state.ccui/aura/window.ccui/aura/window_unittest.cc
Audit Directions
- Raw iteration across observer callbacksFlag any loop over a container (
children_, observer-adjacent lists) that invokes callbacks able to mutate that container; preferWindowTracker/ weak snapshots or index-safe drain loops. - `DCHECK` guarding a memory-safety preconditionSearch for
DCHECK_*protecting values (IDs, indices, sizes) that flow into pointer lookups; these are no-ops in release and should beCHECK_*when a violation enables corruption. - Negative/sentinel container IDsAudit
GetChildById-style lookups and reparenting code for sentinel IDs (e.g.,-1Wallpaper Window) that could cause unintended target selection and subsequent hierarchy corruption.
Patch
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()
Regression Test / PoC
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()
Original Bug Report
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.
- Setup: A compromised renderer creates an
aura::Windowhierarchy containing a parent (W_parent), a transient parent (Child1), and a transient child (W_transient). - Stacking Manipulation: The attacker carefully orders the creation so that
W_transientis located beforeChild1inW_parent’schildren_vector (e.g., by adding the transient child to the parent before the transient parent). - Trigger Move: The attacker initiates a window move across root windows (e.g., via the Multi-Screen Window Placement API or a fullscreen transition).
- Iterator Invalidation:
W_parent->NotifyAddedToRootWindow()begins iterating over its children. It skipsW_transient(index 0) and processesChild1(index 1). - Reparenting: The observer for
Child1triggersMoveAllTransientChildrenToNewRoot, which incorrectly reparentsW_transientto the Wallpaper Window due to the-1ID clash. - Exploitation:
W_transientis erased fromW_parent, shifting the vector. The iterator increments past the newend(), reads the attacker-groomed fakeWindowpointer from the heap, and executes a hijacked virtual call.
Suggested Fix
- Safe Iteration: Update
NotifyAddedToRootWindow,NotifyRemovingFromRootWindow, andNotifyWindowHierarchyChangeDowninui/aura/window.ccto useWindowTrackeror iterate over a copy of thechildren_vector. This pattern is already successfully utilized inNotifyWindowVisibilityChangedDownto prevent exactly this class of bug. - Resolve ID Clash: Address the logic bug in
MoveAllTransientChildrenToNewRoot(ash/wm/window_state.cc) so it correctly validates whetherGetChildByIdis returning the intended parent container, particularly when dealing with the default-1ID.
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.