Medium watch CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in UI
DescriptionUse after free in UI
ComponentUI
Sub-componentWm
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker499065126
Fix commitab2ec9cdaa26 (chromium/src) +78/-5
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-02 (per vendor advisory)
Fix landed2026-04-05 (public git)
Disclosed2026-05-05
Patch gap30 days — fix public in source before the advisory

Background

`base::AutoReset` vs `base::WeakAutoReset`
AutoReset<T> saves a variable’s value and unconditionally restores it in its destructor, while WeakAutoReset holds a base::WeakPtr to 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 its Window.
Synchronous visibility callbacks
calling window_->Hide()/Show() synchronously fires WindowObserver notifications, and an observer may destroy the window (and thus its TransientWindowManager) before control returns.
`weak_factory_`
the newly added base::WeakPtrFactory<TransientWindowManager> used to hand WeakAutoReset a revocable pointer to this.

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.

Key insight
The core mistake was using 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

  1. Set up a transient parent/child with parent-controlled visibility As the regression test shows, a hidden parent window has a transient child with set_parent_controls_visibility(true).
  2. Trigger a synchronous visibility change Calling child->Show() drives TransientWindowManager into a method that sets a flag under AutoReset and then calls window_->Hide(), firing OnWindowVisibilityChanged synchronously.
  3. Destroy the window inside the callback An observer reacts to the hide by deleting the window (delete window_.ExtractAsDangling()), which destroys the window’s TransientWindowManager while the stack frame holding the AutoReset is still unwinding.
  4. UAF write on scope exit The AutoReset destructor then writes the saved flag value back into the now-freed TransientWindowManager member, a use-after-free write.
  5. 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

An attacker able to drive aura window visibility transitions with observers that destroy windows mid-callback can cause a use-after-free write in 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

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

Audit Directions

  • `AutoReset` across reentrant callbacks
    grep for base::AutoReset whose scope spans a call that can synchronously notify observers or destroy the owning object (e.g. Hide(), Show(), NotifyObservers); these are candidates for WeakAutoReset.
  • Self-destruction during observer dispatch
    audit aura/wm code that mutates this-owned flags around WindowObserver notifications, verifying no member write occurs after the owner may have been deleted.
  • Scope-guard restores on owned members
    review RAII restorers (not just AutoReset) that write back to this members in their destructors for the same freed-owner hazard.
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
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 [email protected]

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