Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Extensions
DescriptionUse after free in Extensions
ComponentExtensions
Bug ClassUAF
Tracker516948486
Fix commitc73802e7eca0 (chromium/src) +88/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-08

Changed Functions

FunctionChangeNotes
ClosingOnFullscreenTransitionWindow
chrome/browser/apps/platform_apps/app_window_browsertest.cc
modified
TestAppWindowClient
chrome/browser/apps/platform_apps/app_window_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/apps/platform_apps/app_window_browsertest.cc
modified
if
chrome/test/BUILD.gn
modified

Files Changed

  • chrome/browser/apps/platform_apps/BUILD.gn
  • chrome/browser/apps/platform_apps/DEPS
  • chrome/browser/apps/platform_apps/app_browsertest_util.cc
  • chrome/browser/apps/platform_apps/app_browsertest_util.h
  • chrome/browser/apps/platform_apps/app_window_browsertest.cc
  • chrome/test/BUILD.gn
  • extensions/browser/app_window/app_window.cc
From c73802e7eca02587d75cc2db56a1fc851ba5d76f Mon Sep 17 00:00:00 2001
From: Andrea Orru <andreaorru@chromium.org>
Date: Tue, 16 Jun 2026 12:05:39 -0700
Subject: [PATCH] [Extensions] Fix UAF in AppWindow::SetNativeWindowFullscreen

If the window is destroyed during a nested message loop spun by the
platform fullscreen transition (e.g. on macOS), the AppWindow instance
is deleted, resulting in a UAF when execution returns to
SetNativeWindowFullscreen() and calls RestoreAlwaysOnTop().

This change adds a dedicated weak pointer factory to AppWindow and
checks liveness before executing any logic following the platform
SetFullscreen call. We avoid reusing image_loader_ptr_factory_ as it
gets explicitly invalidated during favicon downloading.

Fixed: 516948486
Change-Id: I371d778366eaaa9dac5e565b6ec00ee666833e03
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7902487
Reviewed-by: Toni Barzic <tbarzic@chromium.org>
Commit-Queue: Andrea Orru <andreaorru@chromium.org>
Reviewed-by: Solomon Kinard <solomonkinard@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1647781}
---

diff --git a/chrome/browser/apps/platform_apps/BUILD.gn b/chrome/browser/apps/platform_apps/BUILD.gn
index 636775b..38690dc 100644
--- a/chrome/browser/apps/platform_apps/BUILD.gn
+++ b/chrome/browser/apps/platform_apps/BUILD.gn
@@ -176,6 +176,7 @@
     "//components/media_router/browser",
     "//content/test:test_support",
     "//extensions/browser:test_support",
+    "//extensions/components/native_app_window",
     "//testing/gmock",
     "//testing/gtest",
   ]
diff --git a/chrome/browser/apps/platform_apps/DEPS b/chrome/browser/apps/platform_apps/DEPS
new file mode 100644
index 0000000..47ca473
--- /dev/null
+++ b/chrome/browser/apps/platform_apps/DEPS
@@ -0,0 +1,5 @@
+specific_include_rules = {
+  "app_window_browsertest.cc": [
+    "+extensions/components/native_app_window/native_app_window_views.h",
+  ],
+}
diff --git a/chrome/browser/apps/platform_apps/app_browsertest_util.cc b/chrome/browser/apps/platform_apps/app_browsertest_util.cc
index ea3a9535..9671a84 100644
--- a/chrome/browser/apps/platform_apps/app_browsertest_util.cc
+++ b/chrome/browser/apps/platform_apps/app_browsertest_util.cc
@@ -284,6 +284,11 @@
                                           bounds);
 }
 
+void PlatformAppBrowserTest::SetNativeWindowFullscreenForTesting(
+    AppWindow* window) {
+  window->SetNativeWindowFullscreen();
+}
+
 AppWindow* PlatformAppBrowserTest::CreateTestAppWindow(
     const std::string& window_create_options) {
   ExtensionTestMessageListener launched_listener("launched",
diff --git a/chrome/browser/apps/platform_apps/app_browsertest_util.h b/chrome/browser/apps/platform_apps/app_browsertest_util.h
index 5cff6b8..b732814a 100644
--- a/chrome/browser/apps/platform_apps/app_browsertest_util.h
+++ b/chrome/browser/apps/platform_apps/app_browsertest_util.h
@@ -123,6 +123,9 @@
       const gfx::Size& minimum_size,
       gfx::Rect* bounds);
 
+  // Call SetNativeWindowFullscreen of |window|.
+  void SetNativeWindowFullscreenForTesting(AppWindow* window);
+
   // Load a simple test app and create a window. The window must be closed by
   // the caller in order to terminate the test - use CloseAppWindow().
   // |window_create_options| are the options that will be passed to
diff --git a/chrome/browser/apps/platform_apps/app_window_browsertest.cc b/chrome/browser/apps/platform_apps/app_window_browsertest.cc
index 1393919..8adb63c0 100644
--- a/chrome/browser/apps/platform_apps/app_window_browsertest.cc
+++ b/chrome/browser/apps/platform_apps/app_window_browsertest.cc
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "extensions/browser/app_window/app_window.h"
+
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
 #include "build/build_config.h"
@@ -9,15 +11,18 @@
 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
 #include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/apps/chrome_app_window_client.h"
 #include "chrome/browser/ui/browser.h"
 #include "components/services/app_service/public/cpp/app_launch_params.h"
 #include "components/services/app_service/public/cpp/app_launch_util.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/test_utils.h"
 #include "extensions/browser/app_window/app_window_geometry_cache.h"
+#include "extensions/browser/app_window/native_app_window.h"
 #include "extensions/common/constants.h"
 #include "extensions/common/extension.h"
 #include "extensions/common/extension_id.h"
+#include "extensions/components/native_app_window/native_app_window_views.h"
 #include "extensions/test/extension_test_message_listener.h"
 #include "extensions/test/result_catcher.h"
 #include "ui/display/display_switches.h"
@@ -273,3 +278,63 @@
       RunAppWindowAPITestAndWaitForRoundTrip("testVisibleOnAllWorkspaces"))
       << message_;
 }
+
+namespace {
+
+class ClosingOnFullscreenTransitionWindow
+    : public native_app_window::NativeAppWindowViews {
+ public:
+  ClosingOnFullscreenTransitionWindow() = default;
+  ~ClosingOnFullscreenTransitionWindow() override = default;
+
+  void SetFullscreen(int fullscreen_types) override {
+    // Simulate window closure during fullscreen transition (e.g. as can happen
+    // on macOS when spinning a nested run loop).
+    widget()->CloseNow();
+  }
+};
+
+class TestAppWindowClient : public ChromeAppWindowClient {
+ public:
+  TestAppWindowClient() = default;
+  ~TestAppWindowClient() override = default;
+
+  std::unique_ptr<extensions::NativeAppWindow> CreateNativeAppWindow(
+      extensions::AppWindow* window,
+      extensions::AppWindow::CreateParams* params) override {
+    auto native_window =
+        std::make_unique<ClosingOnFullscreenTransitionWindow>();
+    native_window->Init(window, *params);
+    return native_window;
+  }
+};
+
+}  // namespace
+
+// Regression test for crbug.com/516948486.
+IN_PROC_BROWSER_TEST_F(AppWindowAPITest, UafInSetNativeWindowFullscreen) {
+  const extensions::Extension* extension = LoadExtension(
+      test_data_dir_.AppendASCII("platform_apps").AppendASCII("window_api"));
+  ASSERT_TRUE(extension);
+
+  TestAppWindowClient test_client;
+  // `AppWindowClient::Set()` has a DCHECK verifying that we don't overwrite a
+  // non-null client with another non-null client. We must clear the existing
+  // client (set during browser startup) before setting our test client.
+  extensions::AppWindowClient::Set(nullptr);
+  extensions::AppWindowClient::Set(&test_client);
+
+  extensions::AppWindow* window = CreateAppWindowFromParams(
+      browser()->profile(), extension, extensions::AppWindow::CreateParams());
+  ASSERT_TRUE(window);
+
+  // Trigger fullscreen transition. In our placeholder `SetFullscreen()`,
+  // `OnNativeClose()` will be called, deleting `window`. Without the weak
+  // pointer check in `SetNativeWindowFullscreen()`, this call would result in a
+  // Use-After-Free when `RestoreAlwaysOnTop()` is reached.
+  SetNativeWindowFullscreenForTesting(window);
+
+  // Clear our test client before restoring the production client.
+  extensions::AppWindowClient::Set(nullptr);
+  extensions::AppWindowClient::Set(ChromeAppWindowClient::GetInstance());
+}
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index a1d0222..95b196e 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -4426,6 +4426,7 @@
         "//chrome/browser/web_applications/extensions",
         "//chrome/browser/web_applications/isolated_web_apps/window_management",
         "//extensions/browser:webstore_installer",
+        "//extensions/components/native_app_window",
       ]
       if (is_chromeos) {
         deps += [ "//chrome/browser/apps:browser_tests" ]
@@ -5280,7 +5281,6 @@
         "//components/web_resource",
         "//components/webapps/isolated_web_apps",
         "//extensions/browser:test_support",
-        "//extensions/components/native_app_window",
         "//mojo/core/embedder",
         "//mojo/public/cpp/test_support:test_utils",
         "//remoting/host/chromeos:features",
diff --git a/extensions/browser/app_window/app_window.cc b/extensions/browser/app_window/app_window.cc
index b704f7e..9ca9cf69 100644
--- a/extensions/browser/app_window/app_window.cc
+++ b/extensions/browser/app_window/app_window.cc
@@ -853,7 +853,14 @@
 }
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/apps/platform_apps/app_browsertest_util.cc b/chrome/browser/apps/platform_apps/app_browsertest_util.cc
index ea3a9535..9671a84 100644
--- a/chrome/browser/apps/platform_apps/app_browsertest_util.cc
+++ b/chrome/browser/apps/platform_apps/app_browsertest_util.cc
@@ -284,6 +284,11 @@
                                           bounds);
 }
 
+void PlatformAppBrowserTest::SetNativeWindowFullscreenForTesting(
+    AppWindow* window) {
+  window->SetNativeWindowFullscreen();
+}
+
 AppWindow* PlatformAppBrowserTest::CreateTestAppWindow(
     const std::string& window_create_options) {
   ExtensionTestMessageListener launched_listener("launched",
diff --git a/chrome/browser/apps/platform_apps/app_browsertest_util.h b/chrome/browser/apps/platform_apps/app_browsertest_util.h
index 5cff6b8..b732814a 100644
--- a/chrome/browser/apps/platform_apps/app_browsertest_util.h
+++ b/chrome/browser/apps/platform_apps/app_browsertest_util.h
@@ -123,6 +123,9 @@
       const gfx::Size& minimum_size,
       gfx::Rect* bounds);
 
+  // Call SetNativeWindowFullscreen of |window|.
+  void SetNativeWindowFullscreenForTesting(AppWindow* window);
+
   // Load a simple test app and create a window. The window must be closed by
   // the caller in order to terminate the test - use CloseAppWindow().
   // |window_create_options| are the options that will be passed to
diff --git a/chrome/browser/apps/platform_apps/app_window_browsertest.cc b/chrome/browser/apps/platform_apps/app_window_browsertest.cc
index 1393919..8adb63c0 100644
--- a/chrome/browser/apps/platform_apps/app_window_browsertest.cc
+++ b/chrome/browser/apps/platform_apps/app_window_browsertest.cc
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include "extensions/browser/app_window/app_window.h"
+
 #include "base/memory/raw_ptr.h"
 #include "base/run_loop.h"
 #include "build/build_config.h"
@@ -9,15 +11,18 @@
 #include "chrome/browser/apps/app_service/app_service_proxy_factory.h"
 #include "chrome/browser/apps/platform_apps/app_browsertest_util.h"
 #include "chrome/browser/profiles/profile.h"
+#include "chrome/browser/ui/apps/chrome_app_window_client.h"
 #include "chrome/browser/ui/browser.h"
 #include "components/services/app_service/public/cpp/app_launch_params.h"
 #include "components/services/app_service/public/cpp/app_launch_util.h"
 #include "content/public/test/browser_test.h"
 #include "content/public/test/test_utils.h"
 #include "extensions/browser/app_window/app_window_geometry_cache.h"
+#include "extensions/browser/app_window/native_app_window.h"
 #include "extensions/common/constants.h"
 #include "extensions/common/extension.h"
 #include "extensions/common/extension_id.h"
+#include "extensions/components/native_app_window/native_app_window_views.h"
 #include "extensions/test/extension_test_message_listener.h"
 #include "extensions/test/result_catcher.h"
 #include "ui/display/display_switches.h"
@@ -273,3 +278,63 @@
       RunAppWindowAPITestAndWaitForRoundTrip("testVisibleOnAllWorkspaces"))
       << message_;
 }
+
+namespace {
+
+class ClosingOnFullscreenTransitionWindow
+    : public native_app_window::NativeAppWindowViews {
+ public:
+  ClosingOnFullscreenTransitionWindow() = default;
+  ~ClosingOnFullscreenTransitionWindow() override = default;
+
+  void SetFullscreen(int fullscreen_types) override {
+    // Simulate window closure during fullscreen transition (e.g. as can happen
+    // on macOS when spinning a nested run loop).
+    widget()->CloseNow();
+  }
+};
+
+class TestAppWindowClient : public ChromeAppWindowClient {
+ public:
+  TestAppWindowClient() = default;
+  ~TestAppWindowClient() override = default;
+
+  std::unique_ptr<extensions::NativeAppWindow> CreateNativeAppWindow(
+      extensions::AppWindow* window,
+      extensions::AppWindow::CreateParams* params) override {
+    auto native_window =
+        std::make_unique<ClosingOnFullscreenTransitionWindow>();
+    native_window->Init(window, *params);
+    return native_window;
+  }
+};
+
+}  // namespace
+
+// Regression test for crbug.com/516948486.
+IN_PROC_BROWSER_TEST_F(AppWindowAPITest, UafInSetNativeWindowFullscreen) {
+  const extensions::Extension* extension = LoadExtension(
+      test_data_dir_.AppendASCII("platform_apps").AppendASCII("window_api"));
+  ASSERT_TRUE(extension);
+
+  TestAppWindowClient test_client;
+  // `AppWindowClient::Set()` has a DCHECK verifying that we don't overwrite a
+  // non-null client with another non-null client. We must clear the existing
+  // client (set during browser startup) before setting our test client.
+  extensions::AppWindowClient::Set(nullptr);
+  extensions::AppWindowClient::Set(&test_client);
+
+  extensions::AppWindow* window = CreateAppWindowFromParams(
+      browser()->profile(), extension, extensions::AppWindow::CreateParams());
+  ASSERT_TRUE(window);
+
+  // Trigger fullscreen transition. In our placeholder `SetFullscreen()`,
+  // `OnNativeClose()` will be called, deleting `window`. Without the weak
+  // pointer check in `SetNativeWindowFullscreen()`, this call would result in a
+  // Use-After-Free when `RestoreAlwaysOnTop()` is reached.
+  SetNativeWindowFullscreenForTesting(window);
+
+  // Clear our test client before restoring the production client.
+  extensions::AppWindowClient::Set(nullptr);
+  extensions::AppWindowClient::Set(ChromeAppWindowClient::GetInstance());
+}
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index a1d0222..95b196e 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -4426,6 +4426,7 @@
         "//chrome/browser/web_applications/extensions",
         "//chrome/browser/web_applications/isolated_web_apps/window_management",
         "//extensions/browser:webstore_installer",
+        "//extensions/components/native_app_window",
       ]
       if (is_chromeos) {
         deps += [ "//chrome/browser/apps:browser_tests" ]
@@ -5280,7 +5281,6 @@
         "//components/web_resource",
         "//components/webapps/isolated_web_apps",
         "//extensions/browser:test_support",
-        "//extensions/components/native_app_window",
         "//mojo/core/embedder",
         "//mojo/public/cpp/test_support:test_utils",
         "//remoting/host/chromeos:features",
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free in AppWindow::SetNativeWindowFullscreen during nested-loop destruction

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 the browser process due to a lack of liveness verification in extensions::AppWindow::SetNativeWindowFullscreen after completing a fullscreen transition. If the window is destroyed during the nested message loop spun by the platform fullscreen call, the AppWindow instance is deleted, resulting in a UAF when execution returns. This could potentially allow an attacker to trigger code execution or a browser crash.

Affected files:

  • extensions/browser/app_window/app_window.cc

Estimated timestamp from git blame: 2013-11-25

Root Cause Analysis

In extensions/browser/app_window/app_window.cc, the method AppWindow::SetNativeWindowFullscreen invokes the platform-specific fullscreen transition and subsequently calls RestoreAlwaysOnTop() without any liveness checks:

void AppWindow::SetNativeWindowFullscreen() {
  native_app_window_->SetFullscreen(fullscreen_types_);

  RestoreAlwaysOnTop();  // Potential Use-After-Free (UAF)
}

On platforms like macOS, the underlying call to native_app_window_->SetFullscreen() can spin a nested UI message loop to run the native transition animation synchronously (e.g., inside -[NSWindow toggleFullScreen:]). While multiple underlying layers in the widget hierarchy are protected against crashes during nested loop destruction, extensions::AppWindow (which acts as the WebContentsDelegate) does not perform any liveness checks upon returning from this transition.

Potential Destruction Sequence

If a window closure is queued or triggered during the nested loop inside the synchronous platform fullscreen call:

  1. The platform-specific widget processes destruction, leading to WidgetDelegate::DeleteDelegate().
  2. The delete delegate callback runs, invoking app_window_->OnNativeClose().
  3. AppWindow::OnNativeClose() executes delete this;, destroying the AppWindow instance and freeing its members, including std::unique_ptr<NativeAppWindow> native_app_window_.
  4. When control flow returns to SetNativeWindowFullscreen(), RestoreAlwaysOnTop() is invoked on the freed this pointer.
  5. If cached_always_on_top_ is true, the code enters UpdateNativeAlwaysOnTop(), which triggers virtual method calls (e.g., GetZOrderLevel()) through the deleted native_app_window_ pointer, potentially leading to a virtual table dispatch on freed memory.

Suggested Steps to Reproduce (Potential Flow)

Note: These steps are based on static code analysis and have not been validated with a running proof-of-concept.

  1. Launch a packaged app with the app.window API and set its window to “always on top” (setAlwaysOnTop(true)).
  2. Programmatically trigger a transition to fullscreen (chrome.app.window.current().fullscreen()).
  3. While the fullscreen transition is animating (and the nested loop is active), programmatically trigger window close (e.g., via window.close()).
  4. Observe if the process crashes or exhibits undefined behavior due to the Use-After-Free condition when RestoreAlwaysOnTop() is reached.

Proposed Fix

Use a weak pointer to check if the AppWindow instance is still alive before executing any logic following the platform fullscreen transition:

void AppWindow::SetNativeWindowFullscreen() {
  base::WeakPtr<AppWindow> weak_this = image_loader_ptr_factory_.GetWeakPtr();
  native_app_window_->SetFullscreen(fullscreen_types_);
  if (!weak_this)
    return;

  RestoreAlwaysOnTop();
}

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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.

Raised in root component due to access or custom field issues on 1456886

View on issue tracker