Overview

Critical
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Ozone
DescriptionUse after free in Ozone
ComponentOzone
Sub-componentHeadless
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.
Tracker516674532
Fix commit583fe61da24f (chromium/src) +202/-0
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-05-26 (per vendor advisory)
Fix landed2026-05-28 (public git)
Disclosed2026-06-08
Patch gap11 days — fix public in source before the advisory

Background

`ui::HeadlessWindow`
The Ozone headless platform window implementation (ui/ozone/platform/headless/headless_window.cc), running in the browser process and driving window-state changes for headless Chrome.
`PlatformWindowDelegate`
The delegate interface (OnWindowStateChanged, etc.) that HeadlessWindow calls synchronously when window state changes; observers/embedders can react during these callbacks, including by destroying the widget.
Reentrant deletion
A delegate callout can synchronously close the widget and free the calling ui::HeadlessWindow, so any member access after the callout dereferences freed memory.
`base::WeakPtr`
The self-invalidation guard added by the fix; GetWeakPtr() captured before a callout and tested afterward tells the method whether this still exists.

Root Cause Analysis

ui::HeadlessWindow mutates window state through helpers (ZoomWindowBounds, RestoreWindowBounds, UpdateWindowState) that synchronously reach PlatformWindowDelegate callbacks such as OnWindowStateChanged. These state-change entry points (ToggleFullscreen, Maximize, Minimize, Restore) assumed this remained valid across the callout and continued to touch members (window_state_, bounds_) and issue further UpdateWindowState calls afterward. However, an observer can react to the callback by synchronously closing the widget, which destroys the ui::HeadlessWindow while its method is still executing on the stack. The subsequent member accesses then operate on a freed object, producing a use-after-free in the browser process.

The fix captures auto weak_ptr = GetWeakPtr(); before each delegate-reaching call and returns early with if (!weak_ptr) return; once this has been invalidated.

Key insight
Synchronous delegate callbacks during state transitions create a reentrancy window in which the delegate can delete the very object mid-method; the object never guarded its own lifetime across the callout, so any post-callback member access was unsafe.

Attack Path

  1. Trigger state change Content or automation drives a headless window state transition (fullscreen/maximize/minimize/restore) that enters one of the affected ui::HeadlessWindow methods.
  2. Synchronous callout The method calls a helper (UpdateWindowState/ZoomWindowBounds/RestoreWindowBounds) that synchronously invokes a PlatformWindowDelegate callback.
  3. Reentrant close An observer handling that callback synchronously closes the widget, deleting the ui::HeadlessWindow while its method remains on the stack.
  4. Use freed memory Control returns into the original method, which accesses window_state_/bounds_ or issues another UpdateWindowState on the freed instance, yielding a browser-process UAF.

Impact Assessment

Use-after-free of a ui::HeadlessWindow in the browser process of headless Chrome, exploitable for memory corruption and potentially code execution at browser privilege. Reachability is limited to the headless Ozone platform, so exposure is confined to headless deployments (automation, rendering, CI) rather than the standard desktop browser. Realistic triggering requires an embedder/observer that synchronously closes the widget during a state-change callback, so this reads as defensive hardening against a latent reentrancy hazard.

Changed Functions

FunctionChangeNotes
if
ui/ozone/BUILD.gn
modified
source_set
ui/ozone/platform/headless/BUILD.gn
modified
if
ui/ozone/platform/headless/headless_window.cc
modified

Files Changed

  • ui/ozone/BUILD.gn
  • ui/ozone/platform/headless/BUILD.gn
  • ui/ozone/platform/headless/headless_window.cc
  • ui/ozone/platform/headless/headless_window.h
  • ui/ozone/platform/headless/headless_window_unittest.cc

Audit Directions

  • Reentrant self-deletion across synchronous delegate callbacks
    Audit other ui::PlatformWindow/Ozone platform implementations (x11, wayland, drm) where state-change methods call PlatformWindowDelegate synchronously, then touch members; verify each guards this with a WeakPtr or documented non-reentrancy.
  • Post-callback member access without lifetime guard
    Search for methods that read/write members (window_state_, bounds_) or reissue calls after invoking any observer/delegate callout; each such site needs a GetWeakPtr() check or a copied-out local.
  • Observer-driven `CloseWidget`/destroy during `OnWindowStateChanged`
    Trace callers/observers that can synchronously close or delete a window inside window-state notifications, confirming callers tolerate deletion mid-stack.
From 583fe61da24f865e1da49dc1ad1b297c7c9cf6d4 Mon Sep 17 00:00:00 2001
From: Peter Kvitek <[email protected]>
Date: Thu, 28 May 2026 12:14:28 -0700
Subject: [PATCH] [headless][linux] Safeguard headless window code against potential UAF

ui::HeadlessWindow synchronously callsbacks delegate methods during state-changing operations. If an observer synchronously closes the widget during these callbacks, the associated ui::HeadlessWindow instance is deleted while its methods are still executing on the stack.

This CL adds weak ptr checks to avoid ui::HeadlessWindow code execution after a delegate callback that invalidates 'this'.

Bug: 516674532
Change-Id: I788ba9b19c733c7af17010744bd66b25783ddf9c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7879467
Commit-Queue: Peter Kvitek <[email protected]>
Reviewed-by: Colin Blundell <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1637863}
---

diff --git a/ui/ozone/BUILD.gn b/ui/ozone/BUILD.gn
index 7bf64c4..0bd2b4e8 100644
--- a/ui/ozone/BUILD.gn
+++ b/ui/ozone/BUILD.gn
@@ -37,6 +37,7 @@
 if (ozone_platform_headless) {
   ozone_platforms += [ "headless" ]
   ozone_platform_deps += [ "platform/headless" ]
+  ozone_platform_test_deps += [ "platform/headless:headless_unittests" ]
 }
 
 if (ozone_platform_drm) {
@@ -356,6 +357,7 @@
   visibility = [
     ":*",
     "platform/flatland:flatland_unittests",
+    "platform/headless:headless_unittests",
     "platform/wayland:test_support",
     "platform/wayland:wayland_unittests",
     "platform/x11:x11_unittests",
diff --git a/ui/ozone/platform/headless/BUILD.gn b/ui/ozone/platform/headless/BUILD.gn
index 65005840..dc07e2b6 100644
--- a/ui/ozone/platform/headless/BUILD.gn
+++ b/ui/ozone/platform/headless/BUILD.gn
@@ -59,3 +59,18 @@
     deps += [ "//gpu/vulkan" ]
   }
 }
+
+source_set("headless_unittests") {
+  testonly = true
+  sources = [ "headless_window_unittest.cc" ]
+  deps = [
+    ":headless",
+    "//base",
+    "//base/test:test_support",
+    "//testing/gtest",
+    "//ui/display:test_support",
+    "//ui/events:test_support",
+    "//ui/ozone:platform",
+    "//ui/ozone:test_support",
+  ]
+}
diff --git a/ui/ozone/platform/headless/headless_window.cc b/ui/ozone/platform/headless/headless_window.cc
index d11ccc5..a0061fa 100644
--- a/ui/ozone/platform/headless/headless_window.cc
+++ b/ui/ozone/platform/headless/headless_window.cc
@@ -78,18 +78,31 @@
     return;
   }
 
+  // PlatformWindowDelegate callbacks can destroy the window, invalidating
+  // `this`. Keep a weak pointer and check it after each callback, here and
+  // below.
+  auto weak_ptr = GetWeakPtr();
+
   if (fullscreen) {
     if (window_state_ != PlatformWindowState::kMaximized &&
         window_state_ != PlatformWindowState::kFullScreen) {
       restored_bounds_ = bounds_;
     }
     ZoomWindowBounds();
+    if (!weak_ptr) {
+      return;
+    }
+
     UpdateWindowState(PlatformWindowState::kFullScreen);
   } else {
     if (window_state_ != PlatformWindowState::kFullScreen) {
       return;
     }
     RestoreWindowBounds();
+    if (!weak_ptr) {
+      return;
+    }
+
     UpdateWindowState(PlatformWindowState::kNormal);
   }
 }
@@ -101,21 +114,37 @@
 
   if (window_state_ != PlatformWindowState::kMaximized &&
       window_state_ != PlatformWindowState::kFullScreen) {
+    auto weak_ptr = GetWeakPtr();
+
     restored_bounds_ = bounds_;
     ZoomWindowBounds();
+    if (!weak_ptr) {
+      return;
+    }
+
     UpdateWindowState(PlatformWindowState::kMaximized);
   }
 }
 
 void HeadlessWindow::Minimize() {
   if (window_state_ != PlatformWindowState::kMinimized) {
+    auto weak_ptr = GetWeakPtr();
+
     // Minimized window retains its size and position, however, it's made
     // hidden by Aura.
     if (window_state_ == PlatformWindowState::kMaximized ||
         window_state_ == PlatformWindowState::kFullScreen) {
       RestoreWindowBounds();
+      if (!weak_ptr) {
+        return;
+      }
     }
+
     UpdateWindowState(PlatformWindowState::kMinimized);
+    if (!weak_ptr) {
+      return;
+    }
+
     // Minimized windows are inactive. Aura activates minimized windows
     // when restoring. If we don't deactivate the window here, the subsequent
     // activation will be optimized away, causing https://crbug.com/358998544.
@@ -125,7 +154,13 @@
 
 void HeadlessWindow::Restore() {
   if (window_state_ != PlatformWindowState::kNormal) {
+    auto weak_ptr = GetWeakPtr();
+
     RestoreWindowBounds();
+    if (!weak_ptr) {
+      return;
+    }
+
     UpdateWindowState(PlatformWindowState::kNormal);
   }
 }
diff --git a/ui/ozone/platform/headless/headless_window.h b/ui/ozone/platform/headless/headless_window.h
index 646e398..a7dc0249 100644
--- a/ui/ozone/platform/headless/headless_window.h
+++ b/ui/ozone/platform/headless/headless_window.h
@@ -9,6 +9,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/memory/scoped_refptr.h"
+#include "base/memory/weak_ptr.h"
 #include "ui/gfx/geometry/point.h"
 #include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/image/image_skia.h"
@@ -73,6 +74,10 @@
  protected:
   PlatformWindowDelegate* delegate() { return delegate_; }
 
+  base::WeakPtr<HeadlessWindow> GetWeakPtr() {
+    return weak_ptr_factory_.GetWeakPtr();
+  }
+
  private:
   enum class ActivationState {
     kUnknown,
@@ -90,6 +95,8 @@
   std::optional<gfx::Rect> restored_bounds_;
   PlatformWindowState window_state_ = PlatformWindowState::kUnknown;
   ActivationState activation_state_ = ActivationState::kUnknown;
+
+  base::WeakPtrFactory<HeadlessWindow> weak_ptr_factory_{this};
 };
 
 }  // namespace ui
diff --git a/ui/ozone/platform/headless/headless_window_unittest.cc b/ui/ozone/platform/headless/headless_window_unittest.cc
new file mode 100644
index 0000000..0881a98
--- /dev/null
+++ b/ui/ozone/platform/headless/headless_window_unittest.cc
@@ -0,0 +1,143 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/ozone/platform/headless/headless_window.h"
+
+#include <memory>
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/display/test/test_screen.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/ozone/platform/headless/headless_window_manager.h"
+#include "ui/platform_window/platform_window_delegate.h"
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ui/ozone/platform/headless/headless_window_unittest.cc b/ui/ozone/platform/headless/headless_window_unittest.cc
new file mode 100644
index 0000000..0881a98
--- /dev/null
+++ b/ui/ozone/platform/headless/headless_window_unittest.cc
@@ -0,0 +1,143 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "ui/ozone/platform/headless/headless_window.h"
+
+#include <memory>
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "ui/display/test/test_screen.h"
+#include "ui/gfx/geometry/rect.h"
+#include "ui/ozone/platform/headless/headless_window_manager.h"
+#include "ui/platform_window/platform_window_delegate.h"
+
+namespace ui {
+namespace {
+
+// A PlatformWindowDelegate that simulates the production behaviour of
+// DesktopWindowTreeHostPlatform: when the platform window calls back into the
+// delegate (OnBoundsChanged / OnWindowStateChanged), an observer may
+// synchronously call Widget::CloseNow(), which ends up calling
+// HeadlessWindow::Close() -> delegate_->OnClosed() ->
+// DWTHP::OnClosed() -> SetPlatformWindow(nullptr) -> ~HeadlessWindow().
+//
+// This delegate models that by resetting its owned unique_ptr<PlatformWindow>
+// from inside the chosen callback.
+class DestroyingDelegate : public PlatformWindowDelegate {
+ public:
+  enum class DestroyOn {
+    kNone,
+    kBoundsChanged,
+    kWindowStateChanged,
+  };
+
+  explicit DestroyingDelegate(DestroyOn destroy_on) : destroy_on_(destroy_on) {}
+  ~DestroyingDelegate() override = default;
+
+  void set_destroy_on(DestroyOn destroy_on) { destroy_on_ = destroy_on; }
+
+  void SetWindow(std::unique_ptr<PlatformWindow> window) {
+    window_ = std::move(window);
+  }
+
+  // PlatformWindowDelegate:
+  void OnBoundsChanged(const BoundsChange& change) override {
+    if (destroy_on_ == DestroyOn::kBoundsChanged) {
+      window_.reset();
+    }
+  }
+  void OnDamageRect(const gfx::Rect& damaged_region) override {}
+  void DispatchEvent(Event* event) override {}
+  void OnCloseRequest() override {}
+  void OnClosed() override { window_.reset(); }
+  void OnWindowStateChanged(PlatformWindowState old_state,
+                            PlatformWindowState new_state) override {
+    if (destroy_on_ == DestroyOn::kWindowStateChanged) {
+      window_.reset();
+    }
+  }
+  void OnLostCapture() override {}
+  void OnAcceleratedWidgetAvailable(gfx::AcceleratedWidget widget) override {}
+  void OnWillDestroyAcceleratedWidget() override {}
+  void OnAcceleratedWidgetDestroyed() override {}
+  void OnActivationChanged(bool active) override {}
+  void OnCursorUpdate() override {}
+  bool CanMaximize() const override { return true; }
+  bool CanFullscreen() const override { return true; }
+  gfx::Rect ConvertRectToPixels(const gfx::Rect& rect_in_dip) const override {
+    return rect_in_dip;
+  }
+  gfx::Rect ConvertRectToDIP(const gfx::Rect& rect_in_pixels) const override {
+    return rect_in_pixels;
+  }
+
+ private:
+  DestroyOn destroy_on_ = DestroyOn::kNone;
+  std::unique_ptr<PlatformWindow> window_;
+};
+
+class HeadlessWindowCrashTest : public ::testing::Test {
+ public:
+  HeadlessWindowCrashTest() = default;
+  ~HeadlessWindowCrashTest() override = default;
+
+ protected:
+  display::test::TestScreen test_screen_{/*create_display=*/true,
+                                         /*register_screen=*/true};
+  HeadlessWindowManager manager_;
+};
+
+TEST_F(HeadlessWindowCrashTest, DestroyViaObserverInSetFullscreen) {
+  DestroyingDelegate delegate(DestroyingDelegate::DestroyOn::kBoundsChanged);
+
+  auto window = std::make_unique<HeadlessWindow>(&delegate, &manager_,
+                                                 gfx::Rect(0, 0, 100, 100));
+  HeadlessWindow* headless_window = window.get();
+  delegate.SetWindow(std::move(window));
+
+  headless_window->SetFullscreen(/*fullscreen=*/true,
+                                 /*target_display_id=*/-1);
+}
+
+TEST_F(HeadlessWindowCrashTest, DestroyViaObserverInMaximize) {
+  DestroyingDelegate delegate(
+      DestroyingDelegate::DestroyOn::kWindowStateChanged);
+
+  auto window = std::make_unique<HeadlessWindow>(&delegate, &manager_,
+                                                 gfx::Rect(0, 0, 100, 100));
+  HeadlessWindow* headless_window = window.get();
+  delegate.SetWindow(std::move(window));
+
+  headless_window->Maximize();
+}
+
+TEST_F(HeadlessWindowCrashTest, DestroyViaObserverInMinimize) {
+  DestroyingDelegate delegate(
+      DestroyingDelegate::DestroyOn::kWindowStateChanged);
+
+  auto window = std::make_unique<HeadlessWindow>(&delegate, &manager_,
+                                                 gfx::Rect(0, 0, 100, 100));
+  HeadlessWindow* headless_window = window.get();
+  delegate.SetWindow(std::move(window));
+
+  headless_window->Minimize();
+}
+
+TEST_F(HeadlessWindowCrashTest, DestroyViaObserverInRestore) {
+  DestroyingDelegate delegate(DestroyingDelegate::DestroyOn::kNone);
+
+  auto window = std::make_unique<HeadlessWindow>(&delegate, &manager_,
+                                                 gfx::Rect(0, 0, 100, 100));
+  HeadlessWindow* headless_window = window.get();
+  delegate.SetWindow(std::move(window));
+
+  headless_window->Maximize();
+
+  delegate.set_destroy_on(DestroyingDelegate::DestroyOn::kBoundsChanged);
+
+  headless_window->Restore();
+}
+
+}  // namespace
+}  // namespace ui
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in ui::HeadlessWindow via Synchronous Deletion during State Changes

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 ui::HeadlessWindow due to synchronous delegate callbacks during state-changing operations. If an observer synchronously closes the widget during these callbacks, the underlying HeadlessWindow is deleted while its methods are still executing on the stack. Subsequent execution leads to UAF member accesses and a virtual function call in the browser process.

Affected files:

  • ui/ozone/platform/headless/headless_window.cc
  • ui/ozone/platform/headless/headless_window.h

Estimated timestamp from git blame: 2023-09-29

Root Cause

In ui/ozone/platform/headless/headless_window.cc, multiple state-changing operations such as SetFullscreen(), Maximize(), Minimize(), and Restore() invoke delegate callbacks synchronously (e.g., delegate_->OnBoundsChanged() via UpdateBounds()). These callbacks propagate up to observers (such as WidgetObserver or WindowTreeHostObserver), which can synchronously close and destroy the widget and window via Widget::CloseNow() under certain application states.

Because HeadlessWindow lacks any base::WeakPtrFactory or liveness checks, it is vulnerable to being deleted while its methods are still active on the call stack.

Potential Call Stack and Destruction Sequence

When a widget is closed during a bounds-changed or state-changed notification, the destruction sequence proceeds as follows:

  1. ui::HeadlessWindow::SetFullscreen() calls ZoomWindowBounds(), which calls UpdateBounds(), which synchronously invokes delegate_->OnBoundsChanged().
  2. This propagates through WindowTreeHostPlatform::OnBoundsChanged() and DesktopNativeWidgetAura::OnHostResized(), calling Widget::OnNativeWidgetSizeChanged().
  3. A registered observer synchronously calls Widget::CloseNow().
  4. This delegates to DesktopWindowTreeHostPlatform::CloseNow(), which calls platform_window()->Close().
  5. HeadlessWindow::Close() synchronously calls delegate_->OnClosed(), resulting in a call to SetPlatformWindow(nullptr) inside DesktopWindowTreeHostPlatform::OnClosed().
  6. SetPlatformWindow(nullptr) resets the unique pointer owning the HeadlessWindow, synchronously destroying it.
  7. The call stack unwinds back to HeadlessWindow::SetFullscreen(), which proceeds to execute UpdateWindowState(PlatformWindowState::kFullScreen) on the now-freed this pointer.
  8. Inside UpdateWindowState(), the code attempts a write to window_state_ and a virtual call on delegate_ (which is also a dangling pointer or reclaimed memory).

Suggested / Potential Steps to Reproduce

Note: These are potential steps based on source code analysis; our tooling does not currently support running code to verify this dynamically.

  1. Run Headless Chrome (--headless) on Linux with Ozone.
  2. Trigger a state change (such as fullscreen via element.requestFullscreen()) from a renderer process or headless test script.
  3. Register an observer (e.g., a custom WidgetObserver or layout observer) that responds to the resulting bounds change by synchronously calling Widget::CloseNow().
  4. Observe the Use-After-Free condition when control returns to the call stack of HeadlessWindow::SetFullscreen() and attempts to access window_state_ or invoke delegate_->OnWindowStateChanged().

Proposed Fix

Add a base::WeakPtrFactory<HeadlessWindow> member to HeadlessWindow. Guard any call sites that invoke delegate methods synchronously by taking a weak pointer first, and verifying its liveness before accessing any member variables or calling subsequent helper functions.

For example, in ui/ozone/platform/headless/headless_window.cc:

void HeadlessWindow::SetFullscreen(bool fullscreen, int64_t target_display_id) {
  DCHECK_EQ(target_display_id, display::kInvalidDisplayId);
  if (!delegate_->CanFullscreen()) {
    return;
  }

  auto weak_this = weak_ptr_factory_.GetWeakPtr();
  if (fullscreen) {
    if (window_state_ != PlatformWindowState::kMaximized &&
        window_state_ != PlatformWindowState::kFullScreen) {
      restored_bounds_ = bounds_;
    }
    ZoomWindowBounds();
    if (!weak_this) {
      return;
    }
    UpdateWindowState(PlatformWindowState::kFullScreen);
  } else {
    if (window_state_ != PlatformWindowState::kFullScreen) {
      return;
    }
    RestoreWindowBounds();
    if (!weak_this) {
      return;
    }
    UpdateWindowState(PlatformWindowState::kNormal);
  }
}

Evaluated with Chrome root at commit: a2bea94528f4bd6cc57739c43fa3bb890b8367d3


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.

View on issue tracker