CVE-2026-11644
Overview
Background
- `DesktopWindowTreeHostPlatform`
- the Views desktop host object that wraps a platform window and drives window state transitions such as maximize, minimize and restore.
- `platform_window()`
- accessor for the underlying
ui::PlatformWindow(e.g.X11Window) that the host delegates state changes to, and which can synchronously call back into observers. - `weak_factory_`
- a
base::WeakPtrFactorymember used to obtain aWeakPtrtothisso a method can detect whether the host was destroyed during a reentrant call.
Root Cause Analysis
7340932fd7cf. An earlier analysis was withdrawn because it had been written against a commit that was not the security fix (fc4613ff876d).DesktopWindowTreeHostPlatform::Restore() (ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc:691-694) calls platform_window()->Restore() at :692 and then unconditionally calls Show(ui::mojom::WindowShowState::kNormal, gfx::Rect()) at :693. The call at :692 can synchronously re-enter other code (per the fix commit message and the reporter, on Linux/X11 restoring a fullscreen window can trigger a synchronous bounds change that notifies observers; the advisory says only “Use after free in Views”), and an observer may synchronously destroy the host, leaving this freed when control returns. Because the subsequent Show() at :693 dereferences members of the freed this, the result is a use-after-free. The sibling method Maximize() at :675-684 already guards against exactly this: it takes auto weak_ptr = weak_factory_.GetWeakPtr() at :676 before platform_window()->Maximize() at :677, then if (!weak_ptr) { return; } at :678-680 before touching this again. Restore() omitted this invariant. The diff restores it by inserting auto weak_ptr = weak_factory_.GetWeakPtr(); before the delegated call and if (!weak_ptr) { return; } after it, so Show() runs only if the host survived. A regression unit test (RestoreSurvivesSynchronousCloseDuringBoundsChange) drives a stub whose Restore() fires OnBoundsChanged while an observer closes the widget.Restore() delegated to the platform window and then used this again without checking whether the synchronous delegation had destroyed the host; the fix adds the same WeakPtr-liveness guard already used by Maximize() and SetFullscreen().Attack Path
- Enter fullscreen
A window belonging to the target is placed in fullscreen state, with a mechanism able to request a programmatic restore. (inferred — specific
chrome.windows/PWA entry points are from the reporter, not the shown code.) - Register a destroying observer An observer reacting to the window’s synchronous bounds/state change closes or destroys the widget the moment the restore transition fires. (inferred from the fix commit message, the reporter and the regression test’s closing observer; the X11 observer notification path is not in the shown source.)
- Trigger restore
Restore()runs, callsplatform_window()->Restore()(:692), which synchronously notifies the observer and frees the host. - Use freed host
Control returns to
Restore()and executesShow()at:693on the now-freedthis, a use-after-free in the browser process.
Impact Assessment
DesktopWindowTreeHostPlatform object in the browser process on Linux/X11, which the advisory rates critical severity; the advisory records no in-the-wild exploitation. Escalation beyond the memory-corruption primitive is not shown by the material and would be (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc |
modified | |
RestoreBoundsChangeStubWindowui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc |
modified | |
RestoreBoundsChangePlatformWindowFactoryDelegateui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc |
modified | |
RestoreBoundsChangePlatformWindowFactoryDelegateui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc |
modified |
Files Changed
ui/views/widget/desktop_aura/desktop_window_tree_host_platform.ccui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc
Audit Directions
- Delegate-then-use-thisAny method that calls into
platform_window()(or another delegate that can run observers synchronously) and then touchesthisafterward must take and re-check aWeakPtr, asMaximize()andSetFullscreen()do. - Sibling-method parityWhen one method in a class adopts a lifetime guard for a reentrant call, audit every sibling performing the same delegation for the missing guard.
- Synchronous observer callbacksTreat bounds/state-change notifications as potential reentrancy points that can destroy the notifying object mid-call.
Patch
diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
index b958f49..b953949d0 100644
--- a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
+++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
@@ -689,7 +689,11 @@
}
void DesktopWindowTreeHostPlatform::Restore() {
+ auto weak_ptr = weak_factory_.GetWeakPtr();
platform_window()->Restore();
+ if (!weak_ptr) {
+ return;
+ }
Show(ui::mojom::WindowShowState::kNormal, gfx::Rect());
}
diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc
index 5bdaf338..264680e 100644
--- a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc
+++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc
@@ -797,6 +797,64 @@
// This should not crash or trigger UAF.
host_platform->Maximize();
}
+
+class RestoreBoundsChangeStubWindow : public ui::StubWindow {
+ public:
+ RestoreBoundsChangeStubWindow(ui::PlatformWindowDelegate* delegate,
+ gfx::AcceleratedWidget widget,
+ const gfx::Rect& bounds)
+ : StubWindow(delegate, false, bounds) {
+ InitDelegateWithWidget(delegate, widget);
+ }
+
+ void Restore() override {
+ delegate()->OnBoundsChanged({/*origin_changed=*/true});
+ }
+};
+
+class RestoreBoundsChangePlatformWindowFactoryDelegate
+ : public aura::WindowTreeHostPlatform::
+ PlatformWindowFactoryDelegateForTesting {
+ public:
+ RestoreBoundsChangePlatformWindowFactoryDelegate() {
+ aura::WindowTreeHostPlatform::SetPlatformWindowFactoryDelegateForTesting(
+ this);
+ }
+ RestoreBoundsChangePlatformWindowFactoryDelegate(
+ const RestoreBoundsChangePlatformWindowFactoryDelegate&) = delete;
+ RestoreBoundsChangePlatformWindowFactoryDelegate& operator=(
+ const RestoreBoundsChangePlatformWindowFactoryDelegate&) = delete;
+ ~RestoreBoundsChangePlatformWindowFactoryDelegate() override {
+ aura::WindowTreeHostPlatform::SetPlatformWindowFactoryDelegateForTesting(
+ nullptr);
+ }
+
+ std::unique_ptr<ui::PlatformWindow> Create(
+ aura::WindowTreeHostPlatform* host) override {
+ return std::make_unique<RestoreBoundsChangeStubWindow>(
+ host, ++last_accelerated_widget_, gfx::Rect(100, 100, 100, 100));
+ }
+
+ gfx::AcceleratedWidget last_accelerated_widget_ = gfx::kNullAcceleratedWidget;
+};
+
+TEST_F(DesktopWindowTreeHostPlatformTest,
+ RestoreSurvivesSynchronousCloseDuringBoundsChange) {
+ RestoreBoundsChangePlatformWindowFactoryDelegate
+ scoped_platform_window_factory_delegate;
+
+ std::unique_ptr<Widget> widget = CreateWidgetWithNativeWidget();
+ widget->Show();
+
+ auto* host_platform = DesktopWindowTreeHostPlatform::GetHostForWidget(
+ widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget());
+ ASSERT_TRUE(host_platform);
+
+ CloseOnBoundsChangedWidgetObserver observer(widget.get());
+
+ // This should not crash or trigger UAF.
+ host_platform->Restore();
+}
#endif // !BUILDFLAG(IS_FUCHSIA)
} // namespace views
Regression Test / PoC
diff --git a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc
index 5bdaf338..264680e 100644
--- a/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc
+++ b/ui/views/widget/desktop_aura/desktop_window_tree_host_platform_unittest.cc
@@ -797,6 +797,64 @@
// This should not crash or trigger UAF.
host_platform->Maximize();
}
+
+class RestoreBoundsChangeStubWindow : public ui::StubWindow {
+ public:
+ RestoreBoundsChangeStubWindow(ui::PlatformWindowDelegate* delegate,
+ gfx::AcceleratedWidget widget,
+ const gfx::Rect& bounds)
+ : StubWindow(delegate, false, bounds) {
+ InitDelegateWithWidget(delegate, widget);
+ }
+
+ void Restore() override {
+ delegate()->OnBoundsChanged({/*origin_changed=*/true});
+ }
+};
+
+class RestoreBoundsChangePlatformWindowFactoryDelegate
+ : public aura::WindowTreeHostPlatform::
+ PlatformWindowFactoryDelegateForTesting {
+ public:
+ RestoreBoundsChangePlatformWindowFactoryDelegate() {
+ aura::WindowTreeHostPlatform::SetPlatformWindowFactoryDelegateForTesting(
+ this);
+ }
+ RestoreBoundsChangePlatformWindowFactoryDelegate(
+ const RestoreBoundsChangePlatformWindowFactoryDelegate&) = delete;
+ RestoreBoundsChangePlatformWindowFactoryDelegate& operator=(
+ const RestoreBoundsChangePlatformWindowFactoryDelegate&) = delete;
+ ~RestoreBoundsChangePlatformWindowFactoryDelegate() override {
+ aura::WindowTreeHostPlatform::SetPlatformWindowFactoryDelegateForTesting(
+ nullptr);
+ }
+
+ std::unique_ptr<ui::PlatformWindow> Create(
+ aura::WindowTreeHostPlatform* host) override {
+ return std::make_unique<RestoreBoundsChangeStubWindow>(
+ host, ++last_accelerated_widget_, gfx::Rect(100, 100, 100, 100));
+ }
+
+ gfx::AcceleratedWidget last_accelerated_widget_ = gfx::kNullAcceleratedWidget;
+};
+
+TEST_F(DesktopWindowTreeHostPlatformTest,
+ RestoreSurvivesSynchronousCloseDuringBoundsChange) {
+ RestoreBoundsChangePlatformWindowFactoryDelegate
+ scoped_platform_window_factory_delegate;
+
+ std::unique_ptr<Widget> widget = CreateWidgetWithNativeWidget();
+ widget->Show();
+
+ auto* host_platform = DesktopWindowTreeHostPlatform::GetHostForWidget(
+ widget->GetNativeWindow()->GetHost()->GetAcceleratedWidget());
+ ASSERT_TRUE(host_platform);
+
+ CloseOnBoundsChangedWidgetObserver observer(widget.get());
+
+ // This should not crash or trigger UAF.
+ host_platform->Restore();
+}
#endif // !BUILDFLAG(IS_FUCHSIA)
} // namespace views
Original Bug Report
Potential Use-after-free in DesktopWindowTreeHostPlatform::Restore on Linux/X11
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 vulnerability exists in DesktopWindowTreeHostPlatform::Restore on Linux/X11. Calling platform_window()->Restore() on a fullscreen window can trigger a synchronous bounds change that deletes the host window. Because Restore() lacks a weak-pointer guard, it proceeds to execute on the freed object.
Affected files:
ui/views/widget/desktop_aura/desktop_window_tree_host_platform.ccui/ozone/platform/x11/x11_window.cc
Estimated timestamp from git blame: 2019-09-27
Root Cause Analysis
In DesktopWindowTreeHostPlatform::Restore(), the method delegates the restoration operation to the underlying platform-specific window and then unconditionally calls Show() on this without verifying if the object survived the synchronous delegation:
// ui/views/widget/desktop_aura/desktop_window_tree_host_platform.cc
void DesktopWindowTreeHostPlatform::Restore() {
platform_window()->Restore(); // [1] May synchronously delete |this|
Show(ui::mojom::WindowShowState::kNormal, gfx::Rect()); // [2] Potential Use-after-free on freed |this|
}
Unlike sibling methods in the same class (such as SetFullscreen() and Maximize()), Restore() is missing a base::WeakPtr check to guard its lifetime.
On Linux/X11, when platform_window()->Restore() is called on a fullscreen window, the implementation (ui::X11Window::Restore()) calls SetFullscreen(false, ...) to exit fullscreen state. This method synchronously triggers platform_window_delegate_->OnBoundsChanged(...) to notify observers of layout transitions.
If an observer (e.g., reacting to bounds/layout changes) synchronously closes or destroys the widget, the entire DesktopWindowTreeHostPlatform object is deleted from the heap. When control flow returns to DesktopWindowTreeHostPlatform::Restore(), the subsequent call to Show() executes on a freed pointer.
Since this is a raw C++ pointer and the first dereferenced member fields inside Show() (such as platform_window_) are standard std::unique_ptr instances rather than raw_ptr<>, this use-after-free is not mitigated by MiraclePtr.
Suggested Steps to Trigger (Potential)
Note: These steps are theoretical and suggested based on code analysis; our tooling does not currently have the capability to execute code or verify via a live Proof-of-Concept.
- Establish an extension with the
chrome.windowspermission or an installed PWA (with experimentalwindow-managementfeatures enabled) and transition its window into fullscreen state. - Register a synchronous event observer (such as on bounds or state changes) that immediately calls close/destroy on the window when a transition occurs.
- Request a window restore operation via
chrome.windows.update(id, {state: 'normal'})orwindow.restore(). - Ensure the synchronous observer destroys the host when the bounds change is notified.
- Observe if the browser process crashes or accesses freed memory when resuming execution inside
Restore().
Suggested Fix
To remediate this issue, obtain a weak pointer before delegating to the platform window, and return early if the host has been destroyed:
void DesktopWindowTreeHostPlatform::Restore() {
auto weak_ptr = weak_factory_.GetWeakPtr();
platform_window()->Restore();
if (!weak_ptr) {
return;
}
Show(ui::mojom::WindowShowState::kNormal, gfx::Rect());
}
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
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.