CVE-2026-19172
Overview
Background
- `TransientWindowManager`
- the per-
aura::Windowobject inui/wm/core/transient_window_manager.ccthat tracks a window’s transient children and cascades visibility (show/hide) from a parent to those children. - Transient hide cascade
- when a parent window’s visibility changes, the manager iterates its transient children and calls
UpdateTransientChildVisibility(visible)on each so their visibility follows the parent’s. - `aura::WindowObserver`
- an observer interface whose callbacks (e.g.
OnWindowVisibilityChanged) fire synchronously during window state changes and may run arbitrary client code, including code that destroys other windows. - `base::WeakPtr`
- a weak reference that becomes null when its referent is destroyed, letting a method detect that
thiswas freed during a re-entrant call before continuing to touch member state.
Root Cause Analysis
The vulnerable method (inferred enclosing function TransientWindowManager::OnWindowVisibilityChanging, whose name is not present in the @@ hunk headers) begins with if (window_ != window) return;, then walks the transient children via a WindowTracker loop calling GetOrCreate(tracker.Pop())->UpdateTransientChildVisibility(visible), and afterward continues to access members of this (the show_on_parent_visible_ bookkeeping). Because UpdateTransientChildVisibility drives a visibility change on a child, that child’s registered aura::WindowObserver can run synchronously mid-cascade; as the new comment states, “Hiding transient children may delete parent as well,” and deleting the parent window destroys the parent’s TransientWindowManager — i.e. this. The invariant violated is that this must remain live for the duration of the method, but the loop hands control to observer code that can free it, so the code after the loop was a use-after-free on the manager object.
The fix captures base::WeakPtr<TransientWindowManager> weak_this = weak_factory_.GetWeakPtr() before the loop and adds if (!weak_this) { return; } immediately after it, so if any child’s observer deleted the parent during the cascade the method bails out before dereferencing freed member state. This is a defensive re-entrancy guard, not a check tightening or relaxing of any validation condition.
this (the parent’s TransientWindowManager) survives the synchronous transient-child visibility cascade, when a child’s aura::WindowObserver can delete the parent mid-loop; the fix detects this with a weak_this base::WeakPtr and returns before any post-loop member access.Attack Path
- Set up a transient parent/child with an observer
Create a parent window with a transient child whose
TransientWindowManagerhasset_parent_controls_visibility(true), and register anaura::WindowObserveron the child (asDeleteParentOnHideObserverdoes in the shipped test). - Trigger the hide cascade
Call
Hide()on the parent, which enters the manager method and begins iterating the transient children, invokingUpdateTransientChildVisibility(false)on the child. - Free the parent during the callback
The child’s visibility change fires
OnWindowVisibilityChanged(child, /*visible=*/false), whose handler synchronously destroys the parent window (the test’sparent_.reset()), destroying the parent’sTransientWindowManagerwhile its method is still on the stack. - Use-after-free on return to the loop's caller
After the loop, the pre-patch code dereferenced the now-freed
thisto read/update member state such asshow_on_parent_visible_, a use-after-free on theTransientWindowManagerobject. - Downstream escalation
Any read/write primitive, controlled reallocation of the freed manager, or code execution beyond the manager-object UAF is not shown by the diff and is
(inferred).
Impact Assessment
TransientWindowManager, matching the advisory’s “Use after free in Views.The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. Turning this UAF into an information leak, memory corruption, or code execution is (inferred) and not demonstrated by the patch, which only shows the freed-object dereference being prevented.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/wm/core/transient_window_manager.cc |
modified | |
DeleteParentOnHideObserverui/wm/core/transient_window_manager_unittest.cc |
modified | |
ifui/wm/core/transient_window_manager_unittest.cc |
modified |
Files Changed
ui/wm/core/transient_window_manager.ccui/wm/core/transient_window_manager_unittest.cc
Audit Directions
- Re-entrant observer callbacks freeing `this`Audit methods that iterate windows/children and invoke visibility, bounds, or destruction changes that fire
aura::WindowObserver/WindowObserver-style synchronous callbacks, then continue to touch member state; require aWeakPtrself-check after any such re-entrant call. - Cascade loops over trackersReview other
WindowTracker-driven loops inui/wm/andui/aura/that call into per-window managers, since the tracked collection guards the child windows but not the owning object (this) that runs the loop. - Parent lifetime assumptions in transient/anchored relationshipsLook for code paths where a child action can delete its parent (transient windows, bubbles/anchors, menus), and verify the parent object is not dereferenced after handing control to child-driven notifications.
Patch
From c17110d74d39df22037424c68fb620884d68f7e2 Mon Sep 17 00:00:00 2001 From: Mitsuru Oshima <[email protected]> Date: Mon, 27 Jul 2026 12:11:59 -0700 Subject: [PATCH] Fix Use-After-Free in TransientWindowManager hide cascade TransientWindowManager could be freed while updating child visibility during parent hide cascade. This happens if the child has an observer that synchronously deletes the parent. This adds weak ptr check inside the iteration to prevent use after free. TAG=agy CONV=6a632318-b810-4c6e-a0e2-1135bf67758d Fixed: 537838324 Change-Id: Ic5f4b3c32a03f75e95ce7c26dd2bc7f41b9ae097 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8143759 Commit-Queue: Mitsuru Oshima <[email protected]> Reviewed-by: Achuith Bhandarkar <[email protected]> Cr-Commit-Position: refs/heads/main@{#1668869} --- diff --git a/ui/wm/core/transient_window_manager.cc b/ui/wm/core/transient_window_manager.cc index ef261c6..a4b6b26 100644 --- a/ui/wm/core/transient_window_manager.cc +++ b/ui/wm/core/transient_window_manager.cc @@ -238,6 +238,9 @@ if (window_ != window) return; + // Hiding transient children may delete parent as well. + base::WeakPtr<TransientWindowManager> weak_this = weak_factory_.GetWeakPtr(); + // If the window has transient children, updates the transient children's // visiblity as well. // WindowTracker is used because child window @@ -246,6 +249,10 @@ while (!tracker.windows().empty()) GetOrCreate(tracker.Pop())->UpdateTransientChildVisibility(visible); + if (!weak_this) { + return; + } + // Remember the show request in |show_on_parent_visible_| and hide it again // if the following conditions are met // - |parent_controls_visibility| is set to true. diff --git a/ui/wm/core/transient_window_manager_unittest.cc b/ui/wm/core/transient_window_manager_unittest.cc index 28c7de54..9e901c0b 100644 --- a/ui/wm/core/transient_window_manager_unittest.cc +++ b/ui/wm/core/transient_window_manager_unittest.cc @@ -728,4 +728,60 @@ child->Show(); } +namespace { + +class DeleteParentOnHideObserver : public aura::WindowObserver { + public: + DeleteParentOnHideObserver(aura::Window* child, + std::unique_ptr<aura::Window> parent) + : child_(child), parent_(std::move(parent)) { + child_->AddObserver(this); + } + ~DeleteParentOnHideObserver() override { + if (child_) { + child_->RemoveObserver(this); + } + } + // WindowObserver: + void OnWindowVisibilityChanged(aura::Window* window, + bool visible) override { + if (window == child_ && !visible) { + parent_.reset(); + } + } + void OnWindowDestroyed(aura::Window* window) override { + if (window == child_) { + child_ = nullptr; + } + } + + private: + raw_ptr<aura::Window> child_; + std::unique_ptr<aura::Window> parent_; +}; + +} // namespace + +// Tests that there is no UAF if a window is destroyed while its transient +// children are being hidden in a cascade update. +TEST_F(TransientWindowManagerTest, + ParentDestroyedDuringTransientChildHideCascade) { + std::unique_ptr<aura::Window> parent = CreateTestWindow( + {.parent = root_window(), .bounds = {100, 100}, .window_id = 0}); + aura::Window* parent_ptr = parent.get(); + + std::unique_ptr<aura::Window> child_ptr = CreateTestWindow( + {.parent = root_window(), .bounds = {100, 100}, .window_id = 1}); + aura::Window* child = child_ptr.release(); + + TransientWindowManager::GetOrCreate(child)->set_parent_controls_visibility( + true); + AddTransientChild(parent_ptr, child); + + auto observer = + std::make_unique<DeleteParentOnHideObserver>(child, std::move(parent)); + + parent_ptr->Hide(); +} + } // namespace wm
Regression Test / PoC
diff --git a/ui/wm/core/transient_window_manager_unittest.cc b/ui/wm/core/transient_window_manager_unittest.cc
index 28c7de54..9e901c0b 100644
--- a/ui/wm/core/transient_window_manager_unittest.cc
+++ b/ui/wm/core/transient_window_manager_unittest.cc
@@ -728,4 +728,60 @@
child->Show();
}
+namespace {
+
+class DeleteParentOnHideObserver : public aura::WindowObserver {
+ public:
+ DeleteParentOnHideObserver(aura::Window* child,
+ std::unique_ptr<aura::Window> parent)
+ : child_(child), parent_(std::move(parent)) {
+ child_->AddObserver(this);
+ }
+ ~DeleteParentOnHideObserver() override {
+ if (child_) {
+ child_->RemoveObserver(this);
+ }
+ }
+ // WindowObserver:
+ void OnWindowVisibilityChanged(aura::Window* window,
+ bool visible) override {
+ if (window == child_ && !visible) {
+ parent_.reset();
+ }
+ }
+ void OnWindowDestroyed(aura::Window* window) override {
+ if (window == child_) {
+ child_ = nullptr;
+ }
+ }
+
+ private:
+ raw_ptr<aura::Window> child_;
+ std::unique_ptr<aura::Window> parent_;
+};
+
+} // namespace
+
+// Tests that there is no UAF if a window is destroyed while its transient
+// children are being hidden in a cascade update.
+TEST_F(TransientWindowManagerTest,
+ ParentDestroyedDuringTransientChildHideCascade) {
+ std::unique_ptr<aura::Window> parent = CreateTestWindow(
+ {.parent = root_window(), .bounds = {100, 100}, .window_id = 0});
+ aura::Window* parent_ptr = parent.get();
+
+ std::unique_ptr<aura::Window> child_ptr = CreateTestWindow(
+ {.parent = root_window(), .bounds = {100, 100}, .window_id = 1});
+ aura::Window* child = child_ptr.release();
+
+ TransientWindowManager::GetOrCreate(child)->set_parent_controls_visibility(
+ true);
+ AddTransientChild(parent_ptr, child);
+
+ auto observer =
+ std::make_unique<DeleteParentOnHideObserver>(child, std::move(parent));
+
+ parent_ptr->Hide();
+}
+
} // namespace wm
Original Bug Report
Potential Use-After-Free in TransientWindowManager::OnWindowVisibilityChanged
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 TransientWindowManager::OnWindowVisibilityChanged due to a lack of liveness verification after propagating visibility updates to transient children. Hiding a transient child window can trigger synchronous destruction of its parent window, which in turn deallocates the owned TransientWindowManager instance. Subsequent member reads and writes on the freed manager can result in a Use-After-Free in the browser process.
Affected files:
ui/wm/core/transient_window_manager.ccui/wm/core/transient_window_manager.h
Estimated timestamp from git blame: 2014-11-12
Root Cause Analysis
In ui/wm/core/transient_window_manager.cc, the method TransientWindowManager::OnWindowVisibilityChanged propagates visibility changes to transient children:
// ui/wm/core/transient_window_manager.cc:236-268
void TransientWindowManager::OnWindowVisibilityChanged(Window* window, bool visible) {
if (window_ != window)
return;
...
aura::WindowTracker tracker(transient_children_);
while (!tracker.windows().empty())
GetOrCreate(tracker.Pop())->UpdateTransientChildVisibility(visible); // Hides/shows children
// NO liveness check for |this| before accessing member variables
if (ignore_visibility_changed_event_ ||
!transient_parent_ || !parent_controls_visibility_) {
return;
}
...
}
TransientWindowManager is owned by aura::Window (the parent window window_) as a window property (kPropertyKey defined with DEFINE_OWNED_UI_CLASS_PROPERTY_KEY). Therefore, when the parent window_ is destroyed, its property is cleared, destroying the TransientWindowManager instance synchronously.
During the child-update loop at line 247, hiding a transient child window can trigger synchronous destruction of the parent window (for example, if a child popup/bubble loses focus or capture, triggering associated handlers that close the parent widget). When the parent window is destroyed, its TransientWindowManager (this) is freed. When control returns from the loop, line 254 unconditionally accesses members of the freed TransientWindowManager instance, leading to a Use-After-Free.
Security Impact
Since this UAF occurs in the browser process, an attacker who is able to manipulate window hierarchy and visibility transitions (e.g., from a compromised renderer process or an untrusted ChromeOS guest Wayland client) could potentially exploit this memory corruption to achieve Remote Code Execution (RCE) in the unsandboxed browser process.
Because the UAF is on the implicit this pointer, standard MiraclePtr protections do not mitigate the vulnerability (the this pointer is a bare C++ pointer on the stack, and MiraclePtr only protects raw_ptr members). This allows an attacker who reallocates the freed block to hijack control flow.
Potential Steps to Trigger
Note: These are potential steps as our tooling does not currently support running code to dynamically verify this flow.:
- Construct a transient window hierarchy containing a parent window
Pand a transient child windowCthat holds event/mouse capture. - Bind a capture-lost or gesture-cancellation handler to
C(or its widget) that closes/destroysP’s widget synchronously upon capture loss. - Hide
P, which dispatches a visibility update throughTWM(P)->OnWindowVisibilityChanged. - The loop updates child visibility by calling
C->Hide(), triggering capture release or gesture cleanup onC. - The capture-lost handler synchronously destroys the parent window
P. This executes~WindowonP, clearing properties and freeingTWM(P)whileTWM(P)::OnWindowVisibilityChangedis still on the stack. - The loop finishes and execution reaches line 254, attempting to read
ignore_visibility_changed_event_and other member variables from the freedTWM(P)memory.
Suggested Fix
To prevent this issue, verify the liveness of this before accessing any member variables after the child-visibility update loop. This can be accomplished by keeping a weak pointer to the manager:
void TransientWindowManager::OnWindowVisibilityChanged(Window* window, bool visible) {
if (window_ != window)
return;
base::WeakPtr<TransientWindowManager> weak_this = weak_factory_.GetWeakPtr();
aura::WindowTracker tracker(transient_children_);
while (!tracker.windows().empty())
GetOrCreate(tracker.Pop())->UpdateTransientChildVisibility(visible);
if (!weak_this)
return;
if (ignore_visibility_changed_event_ ||
!transient_parent_ || !parent_controls_visibility_) {
return;
}
...
}
Evaluated with Chrome root at commit: 5a99d0c5d2ec6c066f5131e7868ff637440cc3dc
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.