CVE-2026-7911
Overview
Background
- `aura::Window`
- a node in the Aura window hierarchy; its destructor
Window::~Window()tears down children, observers, and occlusion tracking. - `is_destroying_`
- a member flag set true at the start of
~Window()to mark that the object is already being destroyed. - Re-entrant destruction
- a code path that causes
~Window()to be entered a second time for the same object while the first destruction is still in progress, which underlies the use-after-free / double-free. - `CHECK` vs `base::NotFatalUntil::M149`
CHECK(cond)aborts immediately whencondis false, whileCHECK(cond, base::NotFatalUntil::M149)only reports (non-fatally, e.g. a dump) and continues executing until milestone M149, after which it becomes fatal.
Root Cause Analysis
Window::~Window() begins with a guard against re-entrant destruction: it checks !is_destroying_, then sets is_destroying_ = true. Before the patch the guard was CHECK(!is_destroying_, base::NotFatalUntil::M149), meaning that if the destructor was re-entered for an already-destroying Window, the condition was false but the process did not abort; it recorded a non-fatal report and fell through, continuing to destroy an object that is already mid-teardown, which is the use-after-free / double-free condition.
The patch changes the guard to a plain CHECK(!is_destroying_), so re-entrant destruction now terminates the process immediately instead of proceeding into undefined behavior. This is a hardening change that converts a silently-tolerated memory-safety violation into a deterministic, safe crash; the diff does not alter the code path that causes the re-entrancy itself. The removed comment (TODO(crbug.com/461127606): Crash on re-entrant destruction) and the added TODO(crbug.com/497548912) confirm the intent is to keep crashing on this condition from M149 onward rather than to fix a specific caller.
NotFatalUntil::M149) check, so a violated invariant let ~Window() continue into use-after-free territory; promoting it to a hard CHECK makes the violation abort safely instead of corrupting memory.Attack Path
- Reach re-entrancy
Some observer or teardown callback invoked during
Windowdestruction triggers a second destruction of the sameWindow(the specific trigger is not in this diff). - Guard passes silently (pre-patch)
On the second entry
is_destroying_is already true, butCHECK(!is_destroying_, base::NotFatalUntil::M149)does not abort, so execution continues. - Use-after-free / double-free The destructor re-runs teardown on an object already being freed, producing the memory-safety violation the advisory classifies as a UAF.
- Post-patch outcome
With the plain
CHECK, step 2 now aborts the process immediately, turning the exploitable condition into a controlled crash; deeper primitives are(inferred)and not demonstrated by the diff.
Impact Assessment
aura::Window destruction in the browser process, reachable only when a destruction-time callback re-enters the destructor; the advisory rates it high (“Use after free in Aura”) and does not report in-the-wild exploitation. This patch is a defensive hardening that makes the invariant violation fatal rather than a fix of the root re-entrancy cause, so after it the condition yields a safe crash instead of memory corruption. Any read/write primitive or escalation from the underlying UAF is (inferred); the diff only demonstrates the guard upgrade, not a corruption or code-execution sink.Files Changed
ui/aura/window.cc
Audit Directions
- `NotFatalUntil` guards on memory-safety invariantsGrep for
base::NotFatalUntilon checks that gate re-entrancy, destruction, or liveness; each is a place where a real violation is currently tolerated and may be exploitable until the milestone lands. - Re-entrant destructorsAudit destructors that notify observers or run teardown callbacks (
aura::Window, widgets, hosts) for paths that can trigger a second destruction of the same object; prefer structural prevention over a crash-only guard. - Root-cause follow-upBecause this is a “crash on the symptom” fix, track the referenced bugs (461127606 / 497548912) for the actual caller that produces re-entrancy and audit similar owner/observer lifetimes.
Patch
From 54847382f578feb9ac9756600e04f9b1eb55b58d Mon Sep 17 00:00:00 2001
From: Takashi Sakamoto <tasak@google.com>
Date: Mon, 30 Mar 2026 18:01:06 -0700
Subject: [PATCH] Enable CHECK(!is_destroying_) on M149 or newer.
Bug: 497548912
Change-Id: Ie2ac006f546152668524afa8b7e489c5334703c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7710793
Reviewed-by: Mitsuru Oshima <oshima@chromium.org>
Commit-Queue: Takashi Sakamoto <tasak@google.com>
Cr-Commit-Position: refs/heads/main@{#1607525}
---
diff --git a/ui/aura/window.cc b/ui/aura/window.cc
index c1db2c9..832b4fc 100644
--- a/ui/aura/window.cc
+++ b/ui/aura/window.cc
@@ -187,7 +187,9 @@
Window::~Window() {
// TODO(crbug.com/461127606): Crash on re-entrant destruction.
- CHECK(!is_destroying_, base::NotFatalUntil::M149);
+ // TODO(crbug.com/497548912): Continue crashing on re-entrant destruction
+ // on Chrome M149 or newer.
+ CHECK(!is_destroying_);
is_destroying_ = true;
WindowOcclusionTracker::ScopedPause pause_occlusion_tracking;
Original Bug Report
Potential UAF and RCE via re-entrant aura::Window destruction on Windows
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential Use-After-Free vulnerability exists in aura::Window on Windows due to a non-fatal re-entrancy guard in its destructor. A nested message loop during destruction can allow a second, re-entrant destruction of the same window, bypassing MiraclePtr and leading to a highly reliable UAF. An attacker could potentially exploit this to achieve Remote Code Execution in the browser process.
Affected files:
ui/aura/window.cccontent/browser/renderer_host/render_widget_host_view_aura.ccui/wm/core/transient_window_manager.cccontent/browser/renderer_host/legacy_render_widget_host_win.cc
Estimated timestamp from git blame: 2026-01-15
Vulnerability Description
A potential Use-After-Free (UAF) and double-free vulnerability exists in aura::Window::~Window() on Windows. The destructor guards against re-entrant destruction with CHECK(!is_destroying_, base::NotFatalUntil::M149). Because this check is non-fatal in builds prior to M149, it triggers a DumpWithoutCrashing but allows execution to continue.
During window destruction, aura::Window notifies its delegate. For a RenderWidgetHostViewAura (RWHVA), this notification eventually calls LegacyRenderWidgetHostHWND::Destroy(), which invokes the Windows API ::DestroyWindow(). This API synchronously pumps the thread’s message loop, which can process pending IPCs.
If the destruction of the window’s parent is triggered during this nested message loop, the parent will iterate through its transient children and call delete on the child window again. This re-enters the ~Window() destructor.
MiraclePtr (BRP) Bypass
When the inner (re-entrant) destructor executes, it successfully removes the window from its parent’s children_ list, its transient parent’s transient_children_ list, and its delegate’s window_ member. Since all of these are raw_ptrs, clearing them drops the MiraclePtr reference count to zero.
When the inner destructor finishes, operator delete genuinely frees the memory to the allocator (bypassing the BackupRefPtr quarantine). When the nested message loop unwinds, the outer destructor resumes execution using its implicit this pointer—which is an unprotected raw C++ pointer—pointing to the now-freed memory. The outer destructor then iterates over the observers_ list, providing a reliable primitive for virtual call hijacking.
Suggested Attacker Steps
Note: These are potential steps based on static analysis; our tooling does not yet have the ability to run code to produce a working Proof of Concept.
- A compromised renderer process creates a popup window, which is initialized as a transient child of the main renderer window.
- The renderer sends an IPC to close the popup, triggering
delete window_;and entering the “outer”aura::Window::~Window(). - The outer destructor notifies the delegate, leading to a call to
::DestroyWindow()on Windows, which pumps the message loop. - During this pump, the attacker triggers the destruction of the main window (e.g., by crashing the renderer or via another IPC).
- The main window tears down and calls
deleteon its transient children, including the popup. This enters the “inner”~Window()destructor. - The non-fatal
CHECKis bypassed. The inner destructor clears allraw_ptrreferences to the popup and frees the memory. - The attacker sprays the heap via IPCs processed in the still-running message loop, reallocating the freed
aura::Windowmemory with a forgedbase::ObserverList. - The nested message loop unwinds, and the outer destructor resumes. It iterates the attacker-controlled
observers_list and callsobserver.OnWindowDestroying(this). - The attacker hijacks the virtual method call to achieve Remote Code Execution (RCE) in the browser process.
Suggested Fix
The re-entrancy guard in aura::Window::~Window() must be made strictly fatal to prevent the inner destructor from proceeding.
Change:
CHECK(!is_destroying_, base::NotFatalUntil::M149);
To:
CHECK(!is_destroying_);
Alternatively, if telemetry is still required without crashing the whole browser in release builds immediately, use base::ImmediateCrash() or base::debug::Alias to halt the specific execution path once the dump is generated, preventing the destructor from continuing.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.