CVE-2026-7991
Overview
Background
- `base::AutoReset` vs `base::WeakAutoReset`
AutoReset<T>saves a variable’s value and unconditionally restores it in its destructor, whileWeakAutoResetholds abase::WeakPtrto the owning object and restores the member only if that object is still alive when the scope ends.- `TransientWindowManager`
- an aura helper (one per
Window) that manages transient parent/child relationships, stacking, and visibility propagation; its lifetime is tied to itsWindow. - Synchronous visibility callbacks
- calling
window_->Hide()/Show()synchronously firesWindowObservernotifications, and an observer may destroy the window (and thus itsTransientWindowManager) before control returns. - `weak_factory_`
- the newly added
base::WeakPtrFactory<TransientWindowManager>used to handWeakAutoReseta revocable pointer tothis.
Root Cause Analysis
Several TransientWindowManager methods used stack-scoped base::AutoReset objects to temporarily flip member flags such as stacking_target_, pause_transient_descendants_restacking_, and ignore_visibility_changed_event_, then ran code that synchronously changes window visibility (e.g. window_->Hide()). Because a synchronous WindowObserver callback during that visibility change can delete the window and destroy its TransientWindowManager, the AutoReset destructor would run after the manager’s memory was freed and write the saved value back into the freed member — a use-after-free write (exactly what the DeleteDuringHideUAF regression test reproduces by deleting child inside OnWindowVisibilityChanged).
The fix replaces each AutoReset with a base::WeakAutoReset constructed from weak_factory_.GetWeakPtr(), so the destructor first checks whether the manager is still alive and skips the restore if it was destroyed. This preserves the invariant that the flag is only written back to a live object.
base::AutoReset, whose destructor unconditionally writes back to a member that may have been freed by a reentrant window-destruction callback; base::WeakAutoReset fixes it by gating the restore on a WeakPtr liveness check of the TransientWindowManager.Attack Path
- Set up a transient parent/child with parent-controlled visibility
As the regression test shows, a hidden
parentwindow has a transientchildwithset_parent_controls_visibility(true). - Trigger a synchronous visibility change
Calling
child->Show()drivesTransientWindowManagerinto a method that sets a flag underAutoResetand then callswindow_->Hide(), firingOnWindowVisibilityChangedsynchronously. - Destroy the window inside the callback
An observer reacts to the hide by deleting the window (
delete window_.ExtractAsDangling()), which destroys the window’sTransientWindowManagerwhile the stack frame holding theAutoResetis still unwinding. - UAF write on scope exit
The
AutoResetdestructor then writes the saved flag value back into the now-freedTransientWindowManagermember, a use-after-free write. - Memory-corruption consequence (inferred)
Any controlled exploitation of that freed-memory write beyond the crash is not demonstrated by the diff and is
(inferred).
Impact Assessment
TransientWindowManager, consistent with the advisory’s Use after free in UI at medium severity. The code runs in the browser process (window management), and triggering it requires arranging transient-window visibility changes with a reentrant destroy. The advisory does not report in-the-wild exploitation, and escalation beyond the UAF write is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
forui/wm/core/transient_window_manager.cc |
modified | |
ifui/wm/core/transient_window_manager.cc |
modified | |
DeleteOnHideObserverui/wm/core/transient_window_manager_unittest.cc |
modified | |
ifui/wm/core/transient_window_manager_unittest.cc |
modified | |
TransientWindowManagerTestui/wm/core/transient_window_manager_unittest.cc |
modified | |
TransientWindowManagerTestui/wm/core/transient_window_manager_unittest.cc |
modified | |
TEST_Fui/wm/core/transient_window_manager_unittest.cc |
modified |
Files Changed
ui/wm/core/transient_window_manager.ccui/wm/core/transient_window_manager.hui/wm/core/transient_window_manager_unittest.cc
Audit Directions
- `AutoReset` across reentrant callbacksgrep for
base::AutoResetwhose scope spans a call that can synchronously notify observers or destroy the owning object (e.g.Hide(),Show(),NotifyObservers); these are candidates forWeakAutoReset. - Self-destruction during observer dispatchaudit aura/wm code that mutates
this-owned flags aroundWindowObservernotifications, verifying no member write occurs after the owner may have been deleted. - Scope-guard restores on owned membersreview RAII restorers (not just
AutoReset) that write back tothismembers in their destructors for the same freed-owner hazard.
Patch
From ab2ec9cdaa26aa92b075cf08b8ac67fb85b7526b Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Sun, 05 Apr 2026 14:26:46 -0700 Subject: [PATCH] [wm] Fix UAF in TransientWindowManager using WeakAutoReset TransientWindowManager used base::AutoReset to temporarily set flags. However, synchronous callbacks during window visibility changes (like window_->Hide()) can lead to the destruction of the window and its associated TransientWindowManager. base::AutoReset's destructor would then attempt to write to the freed memory. This CL replaces base::AutoReset with base::WeakAutoReset in TransientWindowManager to safely check if the manager is still alive before restoring the flags. A regression test 'DeleteDuringHideUAF' is added to transient_window_manager_unittest.cc. Fixed: 499065126 Bug: 40062312 Change-Id: Ie9bf06d992b7703be5df9cd1b3239624d258eb5f Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7726852 Commit-Queue: Andrew Paseltiner <[email protected]> Reviewed-by: Xiaoqian Dai <[email protected]> Cr-Commit-Position: refs/heads/main@{#1610199} --- diff --git a/ui/wm/core/transient_window_manager.cc b/ui/wm/core/transient_window_manager.cc index dee2eb3..6bc770d 100644 --- a/ui/wm/core/transient_window_manager.cc +++ b/ui/wm/core/transient_window_manager.cc @@ -9,6 +9,7 @@ #include "base/auto_reset.h" #include "base/memory/ptr_util.h" +#include "base/memory/weak_auto_reset.h" #include "base/observer_list.h" #include "ui/aura/client/transient_window_client.h" #include "ui/aura/client/transient_window_client_observer.h" @@ -172,8 +173,9 @@ if (child_window != window_ && HasTransientAncestor(child_window, window_)) { TransientWindowManager* descendant_manager = GetOrCreate(child_window); - base::AutoReset<raw_ptr<Window>> resetter( - &descendant_manager->stacking_target_, window_); + base::WeakAutoReset resetter(descendant_manager->weak_factory_.GetWeakPtr(), + &TransientWindowManager::stacking_target_, + window_); parent->StackChildAbove(child_window, window_); } } @@ -193,7 +195,9 @@ // Reparenting multiple sibling transient children will call back onto us // (the transient parent) in [2] below, to restack all our descendants. We // should pause restacking until we're done with all the reparenting. - base::AutoReset<bool> reset(&pause_transient_descendants_restacking_, true); + base::WeakAutoReset reset( + weak_factory_.GetWeakPtr(), + &TransientWindowManager::pause_transient_descendants_restacking_, true); for (aura::Window* transient_child : transient_children_) { if (transient_child->parent() == old_parent) { new_parent->AddChild(transient_child); @@ -214,7 +218,9 @@ void TransientWindowManager::UpdateTransientChildVisibility( bool parent_visible) { - base::AutoReset<bool> reset(&ignore_visibility_changed_event_, true); + base::WeakAutoReset reset( + weak_factory_.GetWeakPtr(), + &TransientWindowManager::ignore_visibility_changed_event_, true); if (!parent_visible) { show_on_parent_visible_ = window_->TargetVisibility(); window_->Hide(); @@ -249,7 +255,9 @@ } if (!transient_parent_->TargetVisibility() && visible) { - base::AutoReset<bool> reset(&ignore_visibility_changed_event_, true); + base::WeakAutoReset reset( + weak_factory_.GetWeakPtr(), + &TransientWindowManager::ignore_visibility_changed_event_, true); show_on_parent_visible_ = true; window_->Hide(); } else if (!visible) { diff --git a/ui/wm/core/transient_window_manager.h b/ui/wm/core/transient_window_manager.h index 06dc34f..416eff03 100644 --- a/ui/wm/core/transient_window_manager.h +++ b/ui/wm/core/transient_window_manager.h @@ -9,6 +9,7 @@ #include "base/component_export.h" #include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" #include "base/observer_list.h" #include "ui/aura/window_observer.h" @@ -118,6 +119,8 @@ bool pause_transient_descendants_restacking_ = false; base::ObserverList<TransientWindowObserver>::Unchecked observers_; + + base::WeakPtrFactory<TransientWindowManager> weak_factory_{this}; }; } // namespace wm diff --git a/ui/wm/core/transient_window_manager_unittest.cc b/ui/wm/core/transient_window_manager_unittest.cc index 5701732a..28c7de54 100644 --- a/ui/wm/core/transient_window_manager_unittest.cc +++ b/ui/wm/core/transient_window_manager_unittest.cc @@ -79,6 +79,38 @@ std::unique_ptr<Window> owned_window_; }; +class DeleteOnHideObserver : public aura::WindowObserver { + public: + explicit DeleteOnHideObserver(aura::Window* window) : window_(window) { + window_->AddObserver(this); + } + + DeleteOnHideObserver(const DeleteOnHideObserver&) = delete; + DeleteOnHideObserver& operator=(const DeleteOnHideObserver&) = delete; + + ~DeleteOnHideObserver() override { + if (window_) { + window_->RemoveObserver(this); + } + } + + void OnWindowVisibilityChanged(aura::Window* window, bool visible) override { + if (window == window_ && !visible) { + window_->RemoveObserver(this); + delete window_.ExtractAsDangling(); + } + } + + void OnWindowDestroyed(aura::Window* window) override { + if (window == window_) { + window_ = nullptr; + } + } + + private: + raw_ptr<aura::Window> window_; +}; + class TransientWindowManagerTest : public aura::test::AuraTestBase { public: TransientWindowManagerTest() {} @@ -666,4 +698,34 @@ t2->RemoveObserver(&observer); } +// Tests that there is no UAF if a window is destroyed during Hide(). +// (crbug.com/40062312) +TEST_F(TransientWindowManagerTest, DeleteDuringHideUAF) { + std::unique_ptr<aura::Window> parent(CreateTestWindow( + {.parent = root_window(), .bounds = {100, 100}, .window_id = 0})); + parent->Hide(); + + std::unique_ptr<aura::Window> child_ptr = CreateTestWindow( + {.parent = root_window(), .bounds = {100, 100}, .window_id = 1}); + aura::Window* child = child_ptr.get(); + + TransientWindowManager::GetOrCreate(child)->set_parent_controls_visibility( + true); + AddTransientChild(parent.get(), child); + + // This observer will delete 'child' when it's hidden. + auto observer = std::make_unique<DeleteOnHideObserver>(child); + + // Release the ownership of 'child' so that the observer can safely delete it. + [[maybe_unused]] aura::Window* released_child = child_ptr.release(); + + // This should trigger OnWindowVisibilityChanged on 'child'. + // Since 'parent' is hidden and parent_controls_visibility is true, + // it will call window_->Hide() internally. + // The observer will then delete 'child', which deletes its + // TransientWindowManager. + // WeakAutoReset will then safely check if the manager is alive. + child->Show(); +} + } // 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 5701732a..28c7de54 100644
--- a/ui/wm/core/transient_window_manager_unittest.cc
+++ b/ui/wm/core/transient_window_manager_unittest.cc
@@ -79,6 +79,38 @@
std::unique_ptr<Window> owned_window_;
};
+class DeleteOnHideObserver : public aura::WindowObserver {
+ public:
+ explicit DeleteOnHideObserver(aura::Window* window) : window_(window) {
+ window_->AddObserver(this);
+ }
+
+ DeleteOnHideObserver(const DeleteOnHideObserver&) = delete;
+ DeleteOnHideObserver& operator=(const DeleteOnHideObserver&) = delete;
+
+ ~DeleteOnHideObserver() override {
+ if (window_) {
+ window_->RemoveObserver(this);
+ }
+ }
+
+ void OnWindowVisibilityChanged(aura::Window* window, bool visible) override {
+ if (window == window_ && !visible) {
+ window_->RemoveObserver(this);
+ delete window_.ExtractAsDangling();
+ }
+ }
+
+ void OnWindowDestroyed(aura::Window* window) override {
+ if (window == window_) {
+ window_ = nullptr;
+ }
+ }
+
+ private:
+ raw_ptr<aura::Window> window_;
+};
+
class TransientWindowManagerTest : public aura::test::AuraTestBase {
public:
TransientWindowManagerTest() {}
@@ -666,4 +698,34 @@
t2->RemoveObserver(&observer);
}
+// Tests that there is no UAF if a window is destroyed during Hide().
+// (crbug.com/40062312)
+TEST_F(TransientWindowManagerTest, DeleteDuringHideUAF) {
+ std::unique_ptr<aura::Window> parent(CreateTestWindow(
+ {.parent = root_window(), .bounds = {100, 100}, .window_id = 0}));
+ parent->Hide();
+
+ std::unique_ptr<aura::Window> child_ptr = CreateTestWindow(
+ {.parent = root_window(), .bounds = {100, 100}, .window_id = 1});
+ aura::Window* child = child_ptr.get();
+
+ TransientWindowManager::GetOrCreate(child)->set_parent_controls_visibility(
+ true);
+ AddTransientChild(parent.get(), child);
+
+ // This observer will delete 'child' when it's hidden.
+ auto observer = std::make_unique<DeleteOnHideObserver>(child);
+
+ // Release the ownership of 'child' so that the observer can safely delete it.
+ [[maybe_unused]] aura::Window* released_child = child_ptr.release();
+
+ // This should trigger OnWindowVisibilityChanged on 'child'.
+ // Since 'parent' is hidden and parent_controls_visibility is true,
+ // it will call window_->Hide() internally.
+ // The observer will then delete 'child', which deletes its
+ // TransientWindowManager.
+ // WeakAutoReset will then safely check if the manager is alive.
+ child->Show();
+}
+
} // namespace wm
Original Bug Report
Potential Use-After-Free Write in TransientWindowManager via AutoReset
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: A potential 1-byte Use-After-Free (UAF) write of 0x00 exists in TransientWindowManager within the browser process. It occurs if a window is synchronously destroyed during a nested window_->Hide() call. base::AutoReset attempts to restore a boolean flag on the freed manager, bypassing MiraclePtr protections.
Affected files:
ui/wm/core/transient_window_manager.cc
Estimated timestamp from git blame: 2014-10-07
Description
A potential Use-After-Free (UAF) vulnerability exists in the browser process’s windowing system (Aura/Ash) within wm::TransientWindowManager::OnWindowVisibilityChanged. The issue is triggered if an aura::Window is synchronously destroyed during a visibility change, leading to a 1-byte write of 0x00 to freed memory.
When a transient child window is shown while its parent is hidden, the manager temporarily sets a flag and re-hides the child window. This is implemented using base::AutoReset in ui/wm/core/transient_window_manager.cc:
if (!transient_parent_->TargetVisibility() && visible) {
base::AutoReset<bool> reset(&ignore_visibility_changed_event_, true);
show_on_parent_visible_ = true;
window_->Hide();
}
The base::AutoReset object stores a raw pointer to the ignore_visibility_changed_event_ member. Crucially, base::AutoReset annotates its internal pointer with RAW_PTR_EXCLUSION for performance reasons. This means the pointer bypasses PartitionAlloc’s MiraclePtr (BackupRefPtr) protections.
When window_->Hide() is called, it triggers OnWindowVisibilityChanged on all observers of the window. The Aura framework explicitly supports observers synchronously destroying the window during this callback (handled safely internally via aura::WindowTracker). If the window is destroyed, aura::Window::~Window() clears the window’s property map (ClearProperties()). Because TransientWindowManager is an owned property (DEFINE_OWNED_UI_CLASS_PROPERTY_KEY), it is synchronously deleted, and its heap memory is freed.
When window_->Hide() returns, the AutoReset object goes out of scope. Its destructor blindly writes the original value (false, which is 0x00) back to the now-freed TransientWindowManager memory allocation.
Potential Attacker Steps
Note: These are suggested/potential steps based on code analysis. Our tooling agent does not yet have the ability to run code to verify a full exploit chain.
An attacker with initial execution in a compromised guest or client (such as an ARC++ app or a Wayland client via Exo) could potentially trigger this by:
- Creating two windows via the windowing system:
Window P(Parent) andWindow C(Child). - Hiding
Window P. - Requesting that
Window Cbe made a transient child ofWindow Pwithparent_controls_visibilityenabled. - Registering a UI state or Exo client callback that synchronously destroys
Window Cwhen it receives a “window hidden” event. - Issuing a request to
Show()Window C.
The Show() request will enter OnWindowVisibilityChanged, trigger the AutoReset, and immediately call Hide(). The attacker’s callback then destroys the window, freeing the manager. The AutoReset destructor then writes 0x00 into the freed chunk. By using heap grooming techniques, the attacker could reallocate an object of the same size into that chunk before the UAF write occurs, corrupting a critical field and potentially leading to a sandbox escape.
Suggested Fix
Remove the use of base::AutoReset in this function, as the this pointer’s lifetime is not guaranteed across the window_->Hide() call.
Instead, use a base::WeakPtr to safely check if the TransientWindowManager has survived the Hide() call before restoring the variable:
if (!transient_parent_->TargetVisibility() && visible) {
ignore_visibility_changed_event_ = true;
show_on_parent_visible_ = true;
auto weak_this = weak_factory_.GetWeakPtr();
window_->Hide();
if (weak_this) {
ignore_visibility_changed_event_ = false;
}
}
(Note: TransientWindowManager will need a base::WeakPtrFactory added to its class definition). Alternatively, use base::WeakAutoReset if a weak pointer factory is added.
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.