CVE-2026-9951
Overview
Background
- `transient_children_`
- a
std::vector-like container ofaura::Window*. Itsbegin()/end()are raw pointers into a heap backing store; apush_backthat exceeds capacity reallocates and frees that store. - `aura::Window::AddChild`
- reparenting primitive that synchronously dispatches observer notifications (
OnWindowHierarchyChanging,OnWindowAdded, etc.) to arbitrary registered observers before returning. - `aura::WindowTracker`
- a safe-iteration helper that stores tracked windows and automatically drops any that are destroyed;
Pop()removes and returns one, so mutation of the original vector during iteration cannot invalidate the walk.
Root Cause Analysis
In TransientWindowManager::OnWindowHierarchyChanged the pre-patch code reparented transient children with a range-based for loop: for (aura::Window* transient_child : transient_children_). The loop body calls new_parent->AddChild(transient_child), which synchronously fans out hierarchy-change notifications to observers. An observer that reacts by adding a transient child (wm::AddTransientChild → push_back on transient_children_) or removing one can mutate transient_children_ while the loop is mid-iteration. A range-based for caches the begin()/end() iterators once; because those iterators are raw pointers into the vector’s backing store, a capacity-exceeding push_back reallocates the store, frees the old buffer, and leaves the cached iterators dangling. The next loop increment/dereference touches freed heap memory — a use-after-free on the vector backing store in the browser process.
The patch replaces the range-based for with aura::WindowTracker tracker(transient_children_) and a while (!tracker.windows().empty()) { aura::Window* transient_child = tracker.Pop(); ... } loop. The tracker snapshots the window set and is robust to concurrent add/remove during the synchronous callbacks, so no stale pointer into the live transient_children_ buffer is ever dereferenced. This mirrors the already-safe iteration pattern used elsewhere in the same class (OnWindowVisibilityChanged, OnWindowDestroying).
Attack Path
- Set up transient children
Via web/extension UI surfaces an attacker causes several transient windows (popups, PiP) to be created so
transient_children_sits exactly at its allocated capacity (inferred: the exact capacity is not shown by the diff). - Register a reentrant observer Rely on an existing UI observer that reacts to hierarchy-change notifications by creating/destroying a transient child.
- Trigger reparenting
Cause an action that reaches
OnWindowHierarchyChanged(e.g. a fullscreen/reparent transition), entering the vulnerable loop. - Reallocate during callback
The in-loop
AddChildfires the observer, whichpush_backs a new transient child; capacity is exceeded, the backing store is reallocated and the old buffer freed. - Dereference dangling iterator
The loop advances over the freed buffer, reading an
aura::Window*from freed memory. - Escalate (inferred)
If the attacker reclaims the freed buffer with controlled data (e.g. IPC heap spray) and plants a fake
aura::Window, a subsequent virtual call could yield browser-process code execution. Reclaim and control are not shown by the diff.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
forui/wm/core/transient_window_manager.cc |
modified |
Files Changed
ui/wm/core/transient_window_manager.cc
Audit Directions
- Other `transient_children_` / child-list iterationsAudit the remaining loops in
transient_window_manager.cc(andTransientWindowController) that iterate child vectors while calling intoAddChild/RemoveChild/StackChild, which all dispatch synchronous observer callbacks. - Range-based `for` over observer-visible containersLook across
ui/wm,ui/aura, andui/viewsfor range-based loops over member vectors whose bodies dispatch observer notifications; these are the exact shape vulnerable to reentrant reallocation. - WindowTracker adoptionConfirm every reparent/restack path that can trigger observer reentrancy uses
WindowTracker(or an index-revalidating loop), not cached iterators.
Patch
From e086e4b3cf43f80c7bd3c24dd6d2ee886f772da0 Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Wed, 22 Apr 2026 05:10:28 -0700 Subject: [PATCH] wm: Fix UAF in TransientWindowManager::OnWindowHierarchyChanged This CL replaces a range-based for loop with aura::WindowTracker when iterating over transient_children_ in OnWindowHierarchyChanged. The previous implementation was vulnerable to iterator invalidation if synchronous observer callbacks triggered by AddChild mutated the transient_children_ vector (e.g. by adding or removing transient children). Fixed: 503873388 Change-Id: I1cf130836e7922ca5ea7ffb682051bb55653611a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7782048 Commit-Queue: Andrew Paseltiner <[email protected]> Reviewed-by: Xiaoqian Dai <[email protected]> Cr-Commit-Position: refs/heads/main@{#1618785} --- diff --git a/ui/wm/core/transient_window_manager.cc b/ui/wm/core/transient_window_manager.cc index 6bc770d..ef261c6 100644 --- a/ui/wm/core/transient_window_manager.cc +++ b/ui/wm/core/transient_window_manager.cc @@ -198,7 +198,9 @@ base::WeakAutoReset reset( weak_factory_.GetWeakPtr(), &TransientWindowManager::pause_transient_descendants_restacking_, true); - for (aura::Window* transient_child : transient_children_) { + aura::WindowTracker tracker(transient_children_); + while (!tracker.windows().empty()) { + aura::Window* transient_child = tracker.Pop(); if (transient_child->parent() == old_parent) { new_parent->AddChild(transient_child); should_restack = true;
Original Bug Report
Potential Iterator Invalidation UAF in TransientWindowManager::OnWindowHierarchyChanged
Flapjack, 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 Chrome Security team. 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) exists in TransientWindowManager::OnWindowHierarchyChanged due to iterator invalidation. Synchronous observer callbacks fired during a range-based for loop over transient_children_ can mutate the vector, invalidating iterators and potentially leading to a browser process UAF.
Affected files:
ui/wm/core/transient_window_manager.cc
Estimated timestamp from git blame: 2023-12-20
Summary
A potential Use-After-Free vulnerability was identified in TransientWindowManager::OnWindowHierarchyChanged (ui/wm/core/transient_window_manager.cc). The method iterates over the transient_children_ vector while performing operations that trigger synchronous window observer callbacks. If an observer mutates the window hierarchy, it can modify the transient_children_ vector, invalidating the loop’s iterators and causing a UAF when the loop advances.
Technical Details
In TransientWindowManager::OnWindowHierarchyChanged, the code uses a range-based for loop to reparent transient children:
// ui/wm/core/transient_window_manager.cc:201
for (aura::Window* transient_child : transient_children_) {
if (transient_child->parent() == old_parent) {
new_parent->AddChild(transient_child);
should_restack = true;
}
}
The call to new_parent->AddChild(transient_child) inside the loop is highly complex and synchronously dispatches several observer notifications, such as OnWindowHierarchyChanging and OnWindowAdded, to observers across the UI.
If any registered observer reacts to these notifications by altering the transient children of this window (e.g., by creating a new transient UI element which calls wm::AddTransientChild, or destroying an existing transient child which calls wm::RemoveTransientChild), the transient_children_ vector is mutated.
Specifically, if push_back is called and the vector’s capacity is exceeded, its backing store is reallocated and the old buffer is freed. Because the range-based for loop caches the begin() and end() iterators (which are raw pointers to the vector’s backing store), the reallocation invalidates them.
When the loop attempts to advance, it dereferences a dangling iterator, resulting in a Use-After-Free on the freed vector backing store.
Potential Attack Scenario
While our tooling agent does not have the ability to run code to confirm exploitability, an attacker might trigger this by:
- Using Web or Extension APIs to create multiple transient windows (e.g., PiP windows or popups) such that the
transient_children_vector is exactly at its allocated capacity. - Triggering a window reparenting action (e.g., entering fullscreen) that invokes
OnWindowHierarchyChanged. - Relying on an existing UI observer to react to the reparenting by synchronously adding a new transient child.
- The vector reallocation frees the backing store, which the attacker reclaims using memory manipulation primitives (e.g., IPC sprays).
- The attacker places forged
raw_ptrobjects in the reclaimed backing store. When the loop dereferences these forged pointers, it accesses attacker-controlled fakeaura::Windowobjects, potentially leading to virtual method calls and arbitrary code execution in the Browser process (Sandbox Escape).
Recommended Fix
To safely iterate over transient_children_ while permitting synchronous hierarchy modifications, aura::WindowTracker should be used. This pattern handles windows being added or removed during iteration and is already used safely elsewhere in the same class (e.g., OnWindowVisibilityChanged and OnWindowDestroying).
// Reparenting multiple sibling transient children will call back onto us...
base::WeakAutoReset reset(
weak_factory_.GetWeakPtr(),
&TransientWindowManager::pause_transient_descendants_restacking_, true);
aura::WindowTracker tracker(transient_children_);
while (!tracker.windows().empty()) {
aura::Window* transient_child = tracker.Pop();
if (transient_child->parent() == old_parent) {
new_parent->AddChild(transient_child);
should_restack = true;
}
}
Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f
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.