Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in UI
DescriptionUse after free in UI
ComponentUI
Bug ClassUAF
Tracker499065126
Fix commitab2ec9cdaa26 (chromium/src) +78/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
for
ui/wm/core/transient_window_manager.cc
modified
if
ui/wm/core/transient_window_manager.cc
modified
DeleteOnHideObserver
ui/wm/core/transient_window_manager_unittest.cc
modified
if
ui/wm/core/transient_window_manager_unittest.cc
modified
TransientWindowManagerTest
ui/wm/core/transient_window_manager_unittest.cc
modified
TransientWindowManagerTest
ui/wm/core/transient_window_manager_unittest.cc
modified
TEST_F
ui/wm/core/transient_window_manager_unittest.cc
modified

Files Changed

  • ui/wm/core/transient_window_manager.cc
  • ui/wm/core/transient_window_manager.h
  • ui/wm/core/transient_window_manager_unittest.cc
From ab2ec9cdaa26aa92b075cf08b8ac67fb85b7526b Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
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 <apaseltiner@chromium.org>
Reviewed-by: Xiaoqian Dai <xdai@chromium.org>
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by vm...@google.com

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:

  1. Creating two windows via the windowing system: Window P (Parent) and Window C (Child).
  2. Hiding Window P.
  3. Requesting that Window C be made a transient child of Window P with parent_controls_visibility enabled.
  4. Registering a UI state or Exo client callback that synchronously destroys Window C when it receives a “window hidden” event.
  5. 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.

View on issue tracker