CVE-2026-13782
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc |
modified | |
forchrome/browser/ui/exclusive_access/exclusive_access_manager.cc |
modified | |
ifchrome/browser/ui/exclusive_access/exclusive_access_manager.cc |
modified |
Files Changed
chrome/browser/ui/exclusive_access/exclusive_access_controller_base.ccchrome/browser/ui/exclusive_access/exclusive_access_controller_base.hchrome/browser/ui/exclusive_access/exclusive_access_manager.ccchrome/browser/ui/exclusive_access/exclusive_access_manager.hchrome/browser/ui/exclusive_access/fullscreen_controller.cc
Patch
From 0e5984e87b2914ce3336cf20207e0724b72c0c61 Mon Sep 17 00:00:00 2001
From: Muyao Xu <muyaoxu@google.com>
Date: Mon, 01 Jun 2026 11:07:09 -0700
Subject: [PATCH] [Fullscreen] Use WeakPtr in ExclusiveAccessManager to prevent UAF
Add WeakPtr checks when iterating `exclusive_access_controllers_`
to prevent UAF if the manager is destroyed during iteration.
Bug: 516683433
Change-Id: Ic285adab7e9e922d5f0a202856796919a16b1ccf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7879474
Commit-Queue: Muyao Xu <muyaoxu@google.com>
Reviewed-by: Mike Wasserman <msw@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1639530}
---
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc b/chrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc
index c1c0da48..9103094 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc
@@ -40,8 +40,13 @@
return;
}
+ auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
ExitExclusiveAccessIfNecessary();
+ if (!weak_ptr) {
+ return;
+ }
+
// The call to exit exclusive access may result in asynchronous notification
// of state change (e.g. fullscreen change on Linux). We don't want to rely
// on it to call NotifyTabExclusiveAccessLost(), because at that point
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_controller_base.h b/chrome/browser/ui/exclusive_access/exclusive_access_controller_base.h
index b6b2b31..49addb9 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_controller_base.h
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_controller_base.h
@@ -6,6 +6,7 @@
#define CHROME_BROWSER_UI_EXCLUSIVE_ACCESS_EXCLUSIVE_ACCESS_CONTROLLER_BASE_H_
#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_bubble_type.h"
#include "content/public/browser/web_contents_observer.h"
#include "url/origin.h"
@@ -85,6 +86,8 @@
private:
const raw_ref<ExclusiveAccessControllerBase> controller_;
} web_contents_observer_{*this};
+
+ base::WeakPtrFactory<ExclusiveAccessControllerBase> weak_ptr_factory_{this};
};
#endif // CHROME_BROWSER_UI_EXCLUSIVE_ACCESS_EXCLUSIVE_ACCESS_CONTROLLER_BASE_H_
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_manager.cc b/chrome/browser/ui/exclusive_access/exclusive_access_manager.cc
index 541d4cf0..0404243 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_manager.cc
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_manager.cc
@@ -157,20 +157,32 @@
void ExclusiveAccessManager::OnTabDeactivated(WebContents* web_contents) {
+ auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
for (auto controller : exclusive_access_controllers_) {
controller->OnTabDeactivated(web_contents);
+ if (!weak_ptr) {
+ return;
+ }
}
}
void ExclusiveAccessManager::OnTabDetachedFromView(WebContents* web_contents) {
+ auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
for (auto controller : exclusive_access_controllers_) {
controller->OnTabDetachedFromView(web_contents);
+ if (!weak_ptr) {
+ return;
+ }
}
}
void ExclusiveAccessManager::OnTabClosing(WebContents* web_contents) {
+ auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
for (auto controller : exclusive_access_controllers_) {
controller->OnTabClosing(web_contents);
+ if (!weak_ptr) {
+ return;
+ }
}
}
@@ -181,6 +193,7 @@
return false;
}
+ auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
// When `features::kPressAndHoldEscToExitBrowserFullscreen` is enabled, the
// `esc_key_hold_timer_` starts on `kRawKeyDown` events, unless the key press
// event comes with a modifier key. This metrics records how often the timer
@@ -199,6 +212,9 @@
show_exit_bubble_timer_.Stop();
for (auto controller : exclusive_access_controllers_) {
controller->HandleUserReleasedEscapeEarly();
+ if (!weak_ptr) {
+ return false;
+ }
}
} else if (IsUnmodifiedEscKeyDownEvent(event) &&
!esc_key_hold_timer_.IsRunning()) {
@@ -233,6 +249,9 @@
if (controller->HandleUserPressedEscape()) {
handled = true;
}
+ if (!weak_ptr) {
+ return handled;
+ }
}
return handled;
}
@@ -242,13 +261,21 @@
}
void ExclusiveAccessManager::ExitExclusiveAccess() {
+ auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
for (auto controller : exclusive_access_controllers_) {
controller->ExitExclusiveAccessToPreviousState();
+ if (!weak_ptr) {
+ return;
+ }
}
}
void ExclusiveAccessManager::HandleUserHeldEscape() {
+ auto weak_ptr = weak_ptr_factory_.GetWeakPtr();
for (auto controller : exclusive_access_controllers_) {
controller->HandleUserHeldEscape();
+ if (!weak_ptr) {
+ return;
+ }
}
}
diff --git a/chrome/browser/ui/exclusive_access/exclusive_access_manager.h b/chrome/browser/ui/exclusive_access/exclusive_access_manager.h
index afb3665..4a726e3d 100644
--- a/chrome/browser/ui/exclusive_access/exclusive_access_manager.h
+++ b/chrome/browser/ui/exclusive_access/exclusive_access_manager.h
@@ -8,6 +8,7 @@
#include <optional>
#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_bubble_type.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_permission_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
@@ -123,6 +124,8 @@
std::optional<ui::ScopedUnownedUserData<ExclusiveAccessManager>>
scoped_unowned_user_data_;
+
+ base::WeakPtrFactory<ExclusiveAccessManager> weak_ptr_factory_{this};
};
#endif // CHROME_BROWSER_UI_EXCLUSIVE_ACCESS_EXCLUSIVE_ACCESS_MANAGER_H_
diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller.cc b/chrome/browser/ui/exclusive_access/fullscreen_controller.cc
index ca3d7de..f597f22 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller.cc
@@ -4,7 +4,6 @@
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
-#include "base/auto_reset.h"
#include "base/check.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
@@ -335,13 +334,6 @@
}
#endif // !BUILDFLAG(IS_ANDROID)
-void FullscreenController::OnTabDeactivated(
- content::WebContents* web_contents) {
- base::AutoReset<raw_ptr<content::WebContents>> auto_resetter(
- &deactivated_contents_, web_contents);
- ExclusiveAccessControllerBase::OnTabDeactivated(web_contents);
-}
-
void FullscreenController::OnTabDetachedFromView(WebContents* old_contents) {
if (!IsFullscreenWithinTab(old_contents)) {
return;
@@ -499,7 +491,7 @@
void FullscreenController::PostFullscreenChangeNotification() {
base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
FROM_HERE, base::BindOnce(&FullscreenController::NotifyFullscreenChange,
- ptr_factory_.GetWeakPtr()));
+ weak_ptr_factory_.GetWeakPtr()));
}
void FullscreenController::NotifyFullscreenChange() {
@@ -633,6 +625,7 @@
fullscreen_parameters_.reset();
Regression Test / PoC
diff --git a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
index b27c37f..02b56ce3 100644
--- a/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
+++ b/chrome/browser/ui/exclusive_access/fullscreen_controller_interactive_browsertest.cc
@@ -2027,3 +2027,28 @@
ui_test_utils::BrowserActivationWaiter(popup).WaitForActivation();
EXPECT_TRUE(ui_test_utils::IsBrowserActive(popup));
}
+
+IN_PROC_BROWSER_TEST_F(FullscreenControllerInteractiveTest,
+ ClosingTabExitsFullscreenSafely) {
+ // Add a new tab so the browser doesn't close when we close the active tab.
+ ui_test_utils::NavigateToURLWithDisposition(
+ browser(), GURL("about:blank"), WindowOpenDisposition::NEW_FOREGROUND_TAB,
+ ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+
+ WebContents* active_tab =
+ browser()->tab_strip_model()->GetActiveWebContents();
+
+ browser()
+ ->GetFeatures()
+ .exclusive_access_manager()
+ ->fullscreen_controller()
+ ->EnterFullscreenModeForTab(active_tab->GetPrimaryMainFrame(), {});
+
+ content::WebContentsDestroyedWatcher watcher(active_tab);
+
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE,
+ base::BindOnce(&chrome::CloseTab, base::Unretained(browser())));
+
+ watcher.Wait();
+}
Original Bug Report
Potential Use-After-Free in ExclusiveAccessManager and FullscreenController during OnTabClosing
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 when ExclusiveAccessManager delegates tab lifetime events to its controllers. During synchronous fullscreen state transitions or nested event processing, the parent browser widget can be destroyed, causing the premature deletion of the ExclusiveAccessManager and its controllers. Unwinding the stack or continuing the iteration loop then accesses the freed memory, leading to potential browser process memory corruption.
Affected files:
chrome/browser/ui/exclusive_access/exclusive_access_manager.ccchrome/browser/ui/exclusive_access/fullscreen_controller.ccchrome/browser/ui/exclusive_access/exclusive_access_controller_base.ccchrome/browser/ui/exclusive_access/exclusive_access_manager.h
Estimated timestamp from git blame: 2024-03-04
Root Cause & Potential Vulnerability Mechanism
ExclusiveAccessManager distributes tab-lifecycle events (such as OnTabClosing, OnTabDeactivated, etc.) by iterating over exclusive_access_controllers_ (chrome/browser/ui/exclusive_access/exclusive_access_manager.cc):
void ExclusiveAccessManager::OnTabClosing(WebContents* web_contents) {
for (auto controller : exclusive_access_controllers_) {
controller->OnTabClosing(web_contents);
}
}
exclusive_access_controllers_ is a base::flat_set<raw_ptr<ExclusiveAccessControllerBase>> containing pointers to the manager’s own sub-object members:
fullscreen_controller_keyboard_lock_controller_pointer_lock_controller_
Due to the sorted order of base::flat_set and the declaration order of the sub-objects, fullscreen_controller_ is processed first.
During FullscreenController::OnTabClosing(), the execution flows to ExitExclusiveAccessIfNecessary(), which can transition window states or call exclusive_access_manager()->context()->ExitFullscreen(). On certain platforms (specifically macOS or Windows), transitioning fullscreen states or native window configurations can spin a nested run loop or synchronously dispatch native window messages (e.g., WM_CLOSE).
If the parent widget is synchronously destroyed during this dispatch, its destructor (BrowserWidget::~BrowserWidget()) triggers pre-destruction teardown:
browser_view_->browser()->GetFeatures().TearDownPreBrowserWindowDestruction();
This invokes exclusive_access_manager_.reset(), deleting the ExclusiveAccessManager instance and all its controller sub-objects on the heap. However, the stack frames executing within FullscreenController and the range-based for loop in ExclusiveAccessManager are not guarded against this destruction. This leads to several potential cascading UAF conditions:
- UAF in Range-for Iteration: The loop iterators (
__begin,__end) point directly into the deletedbase::flat_set’s backing heap memory, resulting in an immediate UAF read or write upon the next iteration. - UAF in
FullscreenControllerTail: Upon returning from the nested native dispatch, remaining lines inExitFullscreenModeInternal()(such asextension_url_.reset()orexclusive_access_manager()->UpdateBubble(...)) execute on a freedthispointer. - UAF in
AutoResetDestructor: InFullscreenController::OnTabDeactivated(), stack unwinding triggers the destructor ofbase::AutoReset<raw_ptr<content::WebContents>>, writing to the deallocated controller instance. - Base Class Notification Hazard: In
ExclusiveAccessControllerBase::OnTabClosing(), the call toNotifyTabExclusiveAccessLost()runs after the object may have already been synchronously destroyed.
Note: Our tooling does not currently have the capability to execute code or run functional proof-of-concept tests locally, so these are potential steps and flows based on static source code analysis.
Suggested Potential Replication Flow
An attacker operating from a compromised renderer could theoretically attempt the following steps:
- Open a script-closable browser window and request tab-fullscreen using
LocalFrameHost::EnterFullscreen. - Register a task to close the parent window/widget synchronously during native window message handling (e.g., handling macOS AppKit event loops or queuing a
WM_CLOSEto be processed during state transitions on Windows). - Programmatically close the tab, invoking the
OnTabClosingevent pipeline. - As the loop processes the first element (
FullscreenController), the synchronous transition forces the destruction of the parent widget and resetsExclusiveAccessManager. - Upon stack unwinding, invalid read/write instructions execute on the freed controller members, or the loop attempts to dereference the invalidated set iterator.
Suggested Fix
To prevent destruction of the manager from invalidating active stack contexts, use a base::WeakPtr to guard the iteration and controller processing, and replace the range-based for loop with index-based iteration. For example:
void ExclusiveAccessManager::OnTabClosing(WebContents* web_contents) {
base::WeakPtr<ExclusiveAccessManager> weak_this = weak_ptr_factory_.GetWeakPtr();
for (size_t i = 0; i < exclusive_access_controllers_.size(); ++i) {
auto* controller = exclusive_access_controllers_[i];
controller->OnTabClosing(web_contents);
if (!weak_this) {
return;
}
}
}
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.