CVE-2026-95315
Overview
Background
- `WindowEventDispatcher`
- The Aura object that routes events for a root window and its host, exposing
UpdateCaptureto transition capture between windows. - `Window::ScopedDeleteBlocker`
- A RAII guard that pins an Aura
Windowalive for its scope, deferring or blocking any deletion attempted while it is held. - `OnCaptureLost`
- A window-delegate callback invoked when a window loses capture, which runs synchronously in the middle of
UpdateCaptureand can re-enter into arbitrary client code. - Re-entrant deletion
- Destruction of an object triggered synchronously from a callback that runs while an outer method is still executing on that same object’s memory.
Root Cause Analysis
The vulnerable path is WindowEventDispatcher::UpdateCapture in ui/aura/window_event_dispatcher.cc, which fires the old capture window’s OnCaptureLost while continuing to touch the dispatcher, its root window(), and its host afterward. Callers such as DesktopCaptureClient or CaptureController assume the root window and WindowTreeHost remain alive across a capture update, but OnCaptureLost can synchronously destroy the host and thus the root window and the dispatcher itself. This violates the invariant that this and the objects reachable through window() outlive the body of UpdateCapture, leaving the method operating on freed memory once control returns from the callback.
The fix constructs a Window::ScopedDeleteBlocker root_window_blocker(window()) before the capture transition, so the root window (and by extension its host and dispatcher) cannot be torn down mid-method. The added DeleteHostDuringOnCaptureLost death test confirms that a re-entrant host deletion during OnCaptureLost is now blocked rather than silently corrupting memory.
OnCaptureLost callback in the middle of UpdateCapture without pinning the root window, allowing the callback to free the very object the method continues to use; the fix pins it with a Window::ScopedDeleteBlocker for the duration of the update.Attack Path
- Establish capture
Attacker-influenced code sets capture on a target window
wunder the root window’s capture client, sowbecomes the active capture window. - Trigger a capture transition
A capture release or reassignment invokes
WindowEventDispatcher::UpdateCapture, which synchronously callsOnCaptureLoston the outgoing window’s delegate. - Free the root window from the callback
The delegate’s
OnCaptureLostdestroys the owningWindowTreeHost, which tears down the rootwindow()and theWindowEventDispatcheritself whileUpdateCaptureis still on the stack. - Use freed memory
Execution returns into
UpdateCapture, which continues to dereference the freed dispatcher and root-window state, producing a use-after-free.
Impact Assessment
WindowEventDispatcher and root-window objects, which can corrupt memory and potentially be steered toward controlled execution. The precondition is the ability to drive a capture transition whose OnCaptureLost delegate synchronously destroys the host or root window. Because the affected code is core Aura windowing infrastructure, the primitive sits in a privileged UI context rather than a sandboxed renderer.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/aura/window_event_dispatcher.cc |
modified | |
DeleteHostOnCaptureLostDelegateui/aura/window_event_dispatcher_unittest.cc |
modified | |
TEST_Fui/aura/window_event_dispatcher_unittest.cc |
modified | |
RunLoopHandlerui/aura/window_event_dispatcher_unittest.cc |
modified |
Files Changed
ui/aura/window_event_dispatcher_unittest.ccui/aura/window_event_dispatcher.cc
Audit Directions
- Callback-driven re-entrancyAudit methods that invoke delegate or client callbacks (like
OnCaptureLost) partway through and then keep usingthisor shared state, since the callback may free those objects. - Unpinned host/root-window lifetimesLook for Aura and windowing code paths that assume the
WindowTreeHostor root window survives a transition without holding aWindow::ScopedDeleteBlockeror equivalent guard. - Capture and focus transitionsReview capture, focus, and activation transition code where ownership can be relinquished mid-update, as these commonly re-enter client code capable of synchronous destruction.
Patch
From e7059223305cf6f2b182a41d763b62290e4dd350 Mon Sep 17 00:00:00 2001 From: Mitsuru Oshima <[email protected]> Date: Wed, 26 Aug 2026 18:09:42 -0700 Subject: [PATCH] aura: Block root window deletion in WindowEventDispatcher::UpdateCapture Callers of UpdateCapture (such as DesktopCaptureClient or CaptureController) expect the root window and host to remain alive during capture updates. Synchronous destruction of the host/root window during OnCaptureLost() can lead to use-after-free. This CL adds a ScopedDeleteBlocker for the root window in WindowEventDispatcher::UpdateCapture to block re-entrant deletion of the root window and its host, and adds a regression test. Fixed=517661385 TAG=agy CONV=fb9598b9-6723-4194-bd95-436415146211 Change-Id: I0bf1e30bc818515a55914160c1952715fec68191 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8294739 Commit-Queue: Mitsuru Oshima <[email protected]> Reviewed-by: Thomas Lukaszewicz <[email protected]> Cr-Commit-Position: refs/heads/main@{#1686824} --- diff --git a/ui/aura/window_event_dispatcher.cc b/ui/aura/window_event_dispatcher.cc index 5c1a14c..558785e 100644 --- a/ui/aura/window_event_dispatcher.cc +++ b/ui/aura/window_event_dispatcher.cc @@ -430,6 +430,9 @@ if (mouse_moved_handler_ && !window()->Contains(mouse_moved_handler_)) mouse_moved_handler_ = nullptr; + // Block the deletion of the root window, its host thus this dispatcher. + Window::ScopedDeleteBlocker root_window_blocker(window()); + std::unique_ptr<Window::ScopedDeleteBlocker> new_capture_blocker; if (new_capture) { new_capture_blocker = diff --git a/ui/aura/window_event_dispatcher_unittest.cc b/ui/aura/window_event_dispatcher_unittest.cc index 33e1c5e..6f88f91 100644 --- a/ui/aura/window_event_dispatcher_unittest.cc +++ b/ui/aura/window_event_dispatcher_unittest.cc @@ -22,6 +22,7 @@ #include "testing/gtest/include/gtest/gtest.h" #include "ui/aura/client/aura_constants.h" #include "ui/aura/client/capture_client.h" +#include "ui/aura/client/default_capture_client.h" #include "ui/aura/client/event_client.h" #include "ui/aura/client/focus_client.h" #include "ui/aura/env.h" @@ -2613,6 +2614,22 @@ raw_ptr<Window> other_ = nullptr; }; +class DeleteHostOnCaptureLostDelegate : public test::TestWindowDelegate { + public: + DeleteHostOnCaptureLostDelegate() = default; + ~DeleteHostOnCaptureLostDelegate() override = default; + + void set_host(std::unique_ptr<WindowTreeHost> host) { + host_ = std::move(host); + } + + // test::TestWindowDelegate: + void OnCaptureLost() override { host_.reset(); } + + private: + std::unique_ptr<WindowTreeHost> host_; +}; + } // namespace // Verifies handling loss of capture by the capture window being hidden. @@ -2654,6 +2671,31 @@ EXPECT_DEATH(client::GetCaptureClient(root_window())->SetCapture(w2), ""); } +// Verifies that deleting the host/root window during OnCaptureLost of the +// capture window causes a crash (due to ScopedDeleteBlocker). +TEST_F(WindowEventDispatcherDeathTest, DeleteHostDuringOnCaptureLost) { + DeleteHostOnCaptureLostDelegate delegate; + std::unique_ptr<WindowTreeHost> host = WindowTreeHost::Create( + ui::PlatformWindowInitProperties{gfx::Rect{100, 100}}); + host->InitHost(); + host->window()->Show(); + + client::DefaultCaptureClient capture_client(host->window()); + + Window* w = CreateNormalWindow(1, host->window(), &delegate); + delegate.set_host(std::move(host)); + + client::GetCaptureClient(w->GetRootWindow())->SetCapture(w); + EXPECT_EQ(w, + client::GetCaptureClient(w->GetRootWindow())->GetCaptureWindow()); + + // This will trigger UpdateCapture(w, nullptr). + // w's OnCaptureLost will attempt to delete the host, which should crash due + // to ScopedDeleteBlocker on the root window. + EXPECT_DEATH(client::GetCaptureClient(w->GetRootWindow())->ReleaseCapture(w), + ""); +} + namespace { class RunLoopHandler : public ui::EventHandler {
Regression Test / PoC
diff --git a/ui/aura/window_event_dispatcher_unittest.cc b/ui/aura/window_event_dispatcher_unittest.cc
index 33e1c5e..6f88f91 100644
--- a/ui/aura/window_event_dispatcher_unittest.cc
+++ b/ui/aura/window_event_dispatcher_unittest.cc
@@ -22,6 +22,7 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/client/capture_client.h"
+#include "ui/aura/client/default_capture_client.h"
#include "ui/aura/client/event_client.h"
#include "ui/aura/client/focus_client.h"
#include "ui/aura/env.h"
@@ -2613,6 +2614,22 @@
raw_ptr<Window> other_ = nullptr;
};
+class DeleteHostOnCaptureLostDelegate : public test::TestWindowDelegate {
+ public:
+ DeleteHostOnCaptureLostDelegate() = default;
+ ~DeleteHostOnCaptureLostDelegate() override = default;
+
+ void set_host(std::unique_ptr<WindowTreeHost> host) {
+ host_ = std::move(host);
+ }
+
+ // test::TestWindowDelegate:
+ void OnCaptureLost() override { host_.reset(); }
+
+ private:
+ std::unique_ptr<WindowTreeHost> host_;
+};
+
} // namespace
// Verifies handling loss of capture by the capture window being hidden.
@@ -2654,6 +2671,31 @@
EXPECT_DEATH(client::GetCaptureClient(root_window())->SetCapture(w2), "");
}
+// Verifies that deleting the host/root window during OnCaptureLost of the
+// capture window causes a crash (due to ScopedDeleteBlocker).
+TEST_F(WindowEventDispatcherDeathTest, DeleteHostDuringOnCaptureLost) {
+ DeleteHostOnCaptureLostDelegate delegate;
+ std::unique_ptr<WindowTreeHost> host = WindowTreeHost::Create(
+ ui::PlatformWindowInitProperties{gfx::Rect{100, 100}});
+ host->InitHost();
+ host->window()->Show();
+
+ client::DefaultCaptureClient capture_client(host->window());
+
+ Window* w = CreateNormalWindow(1, host->window(), &delegate);
+ delegate.set_host(std::move(host));
+
+ client::GetCaptureClient(w->GetRootWindow())->SetCapture(w);
+ EXPECT_EQ(w,
+ client::GetCaptureClient(w->GetRootWindow())->GetCaptureWindow());
+
+ // This will trigger UpdateCapture(w, nullptr).
+ // w's OnCaptureLost will attempt to delete the host, which should crash due
+ // to ScopedDeleteBlocker on the root window.
+ EXPECT_DEATH(client::GetCaptureClient(w->GetRootWindow())->ReleaseCapture(w),
+ "");
+}
+
namespace {
class RunLoopHandler : public ui::EventHandler {
Original Bug Report
Potential Use-After-Free in WindowEventDispatcher::UpdateCapture after OnCaptureLost()
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 WindowEventDispatcher::UpdateCapture. When a capture change is dispatched, the synchronous invocation of OnCaptureLost() can lead to widget destruction. If the widget is synchronously destroyed, the owning WindowEventDispatcher is deleted, resulting in UAF reads/writes upon returning to the method.
Affected files:
ui/aura/window_event_dispatcher.cc
Estimated timestamp from git blame: 2012-05-14
Root Cause Analysis
In ui/aura/window_event_dispatcher.cc, the method WindowEventDispatcher::UpdateCapture is responsible for updating mouse/pointer capture state. When transitioning capture away from an old window, the code synchronously dispatches capture loss to the old capture window’s delegate:
void WindowEventDispatcher::UpdateCapture(Window* old_capture,
Window* new_capture) {
...
if (old_capture && old_capture->GetRootWindow() == window() &&
old_capture->delegate()) {
...
DispatchDetails details = DispatchEvent(old_capture, &event);
if (details.dispatcher_destroyed)
return;
if (!details.target_destroyed)
old_capture->delegate()->OnCaptureLost(); // Synchronous Re-entrancy Point
}
if (new_capture) {
if (mouse_moved_handler_ || Env::GetInstance()->IsMouseButtonDown())
mouse_moved_handler_ = new_capture; // Potential UAF write / read
} else {
DispatchDetails details = SynthesizeMouseMoveEvent(); // Potential UAF method call
if (details.dispatcher_destroyed) return;
}
mouse_pressed_handler_ = nullptr; // Potential UAF write
}
For desktop widgets on Linux (under X11 or Wayland), old_capture->delegate()->OnCaptureLost() propagates through DesktopNativeWidgetAura::OnCaptureLost() -> Widget::OnMouseCaptureLost() -> RootView::OnMouseCaptureLost().
Views are permitted to respond to capture loss by synchronously destroying the widget (e.g., calling Widget::CloseNow()). When this occurs, it initiates a synchronous destruction chain:
Widget::CloseNow()callsDesktopNativeWidgetAura::CloseNow(), which callsDesktopWindowTreeHostPlatform::CloseNow().- This triggers the platform window to close, executing
platform_window_delegate_->OnClosed(), leading toDesktopNativeWidgetAura::OnHostClosed(). OnHostClosed()resets thehost_unique pointer (host_.reset()), triggering the destruction of theWindowTreeHostinstance.- During destruction,
WindowTreeHost::DestroyDispatcher()is invoked, resettingdispatcher_(thestd::unique_ptrthat owns theWindowEventDispatcher).
At this point, the WindowEventDispatcher instance (this) is synchronously deleted. When the execution returns from OnCaptureLost(), the stack frames unwind back into UpdateCapture(), which proceeds to access and modify member fields (mouse_moved_handler_, mouse_pressed_handler_) or invoke SynthesizeMouseMoveEvent() on the now-freed this object.
Potential Steps to Trigger
Note: These are potential steps. Our analysis is based on static code review, as our tooling does not currently have the ability to run code or execute a live proof-of-concept.
- Create a View inside a desktop widget that requests and obtains mouse capture.
- Trigger an action (or simulated event) that forces a mouse capture transfer or release.
- In the View’s
OnMouseCaptureLost()handler, synchronously callGetWidget()->CloseNow()to force widget destruction. - Observe that the platform host synchronously destroys the
WindowTreeHostand itsWindowEventDispatcherduring theOnCaptureLost()call stack. - Upon return from
OnCaptureLost(),UpdateCapture()continues execution, resulting in a Use-After-Free when accessing or writing to member variables of the deletedWindowEventDispatcher.
Impact
This is a potential Use-After-Free occurring in the unsandboxed browser process on desktop platforms.
- Memory Corruption: The assignment operators on
mouse_moved_handler_andmouse_pressed_handler_execute on a freed object structure. Since these areraw_ptrobjects, writing to them on freed memory can corrupt the allocator state. - Control Flow Hijack: When
new_captureis null,SynthesizeMouseMoveEvent()is called, accessinghost_(which is already deleted) and executing virtual functions, potentially allowing for control flow redirection if the freed memory is reclaimed and groomed.
Suggested Fix
To remediate this issue, implement a liveness check immediately following the re-entrant OnCaptureLost() call using the class’s existing weak_ptr_factory_:
base::WeakPtr<WindowEventDispatcher> weak_this = weak_ptr_factory_.GetWeakPtr();
if (!details.target_destroyed)
old_capture->delegate()->OnCaptureLost();
if (!weak_this)
return;
Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379
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.