CVE-2026-15110
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ClosingOnFullscreenTransitionWindowchrome/browser/apps/platform_apps/app_window_browsertest.cc |
modified | |
TestAppWindowClientchrome/browser/apps/platform_apps/app_window_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/apps/platform_apps/app_window_browsertest.cc |
modified | |
ifchrome/test/BUILD.gn |
modified |
Files Changed
chrome/browser/apps/platform_apps/BUILD.gnchrome/browser/apps/platform_apps/DEPSchrome/browser/apps/platform_apps/app_browsertest_util.ccchrome/browser/apps/platform_apps/app_browsertest_util.hchrome/browser/apps/platform_apps/app_window_browsertest.ccchrome/test/BUILD.gnextensions/browser/app_window/app_window.cc
Patch
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 @@
}
Regression Test / PoC
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",
Original Bug Report
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:
- The platform-specific widget processes destruction, leading to
WidgetDelegate::DeleteDelegate(). - The delete delegate callback runs, invoking
app_window_->OnNativeClose(). AppWindow::OnNativeClose()executesdelete this;, destroying theAppWindowinstance and freeing its members, includingstd::unique_ptr<NativeAppWindow> native_app_window_.- When control flow returns to
SetNativeWindowFullscreen(),RestoreAlwaysOnTop()is invoked on the freedthispointer. - If
cached_always_on_top_is true, the code entersUpdateNativeAlwaysOnTop(), which triggers virtual method calls (e.g.,GetZOrderLevel()) through the deletednative_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.
- Launch a packaged app with the
app.windowAPI and set its window to “always on top” (setAlwaysOnTop(true)). - Programmatically trigger a transition to fullscreen (
chrome.app.window.current().fullscreen()). - While the fullscreen transition is animating (and the nested loop is active), programmatically trigger window close (e.g., via
window.close()). - 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