CVE-2026-13782
Overview
Background
- `ExclusiveAccessManager`
- the browser-process object that owns a list of exclusive-access controllers (
exclusive_access_controllers_) and fans tab/keyboard events out to each of them. - `ExclusiveAccessControllerBase`
- the base class for controllers like
FullscreenControllerthat actually exit fullscreen / pointer-lock, whoseExitExclusiveAccessIfNecessary()can trigger further teardown. - `base::WeakPtrFactory` / `base::WeakPtr`
- a Chromium idiom where an object hands out weak pointers that become null once the object is destroyed, letting callers detect a mid-call self-destruction.
- Re-entrant destruction
- a synchronous callback (here, exiting fullscreen while a tab closes) that ends up destroying the very object whose method is still executing, leaving
thisand its members dangling after the call returns.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: WeakPtr guards match diff, but the cited regression test ClosingTabExitsFullscreenSafely does not appear in the shown diff. Analysis wrongly says no severity is reported; advisory lists critical, CVSS 10.0.
Several ExclusiveAccessManager methods (OnTabDeactivated, OnTabDetachedFromView, OnTabClosing, ExitExclusiveAccess, HandleUserHeldEscape, and the escape-key handler) iterate exclusive_access_controllers_ and call a method on each controller. Calling into a controller can synchronously exit exclusive access, which — as the regression test ClosingTabExitsFullscreenSafely shows for a closing fullscreen tab — can tear down the browser feature that owns the manager, destroying this while the loop is still running. When control returned to the loop, it re-read the now-freed exclusive_access_controllers_ member and advanced the iterator, a use-after-free of the destroyed manager. The same hazard existed one level down in ExclusiveAccessControllerBase, where code kept using this after ExitExclusiveAccessIfNecessary() (which the surrounding comment notes may cause asynchronous state-change notification) could destroy the controller.
The fix captures weak_ptr_factory_.GetWeakPtr() before the loop / before the risky call and, after each callee returns, checks if (!weak_ptr) and returns early, so no freed memory is touched once the object is gone.
this, then kept dereferencing this and its members; the fix adds a WeakPtr liveness check after every such call and bails out the moment the object has been freed.Attack Path
- Enter fullscreen
A page (or the browser UI) puts the active tab into fullscreen via
FullscreenController::EnterFullscreenModeForTab, registering the fullscreen controller in the manager’sexclusive_access_controllers_, as the test’s setup does. - Close the fullscreen tab
The tab is closed (e.g.
chrome::CloseTab, driven from script/UI), causing the manager to run one of its iterating handlers such asOnTabClosingoverexclusive_access_controllers_. - Re-entrant destruction mid-iteration
The
controller->...()call exits exclusive access, which synchronously tears down the owning feature and destroys theExclusiveAccessManager(the exact teardown chain from tab close to manager destruction is not in the diff)(inferred). - Use-after-free
After the callee returns, the loop re-reads the freed
exclusive_access_controllers_and advances the iterator on the destroyedthis, the UAF the patch’s post-callif (!weak_ptr) return;guard now prevents. - Escalation
Controlling the freed manager’s memory to achieve a read/write primitive, sandbox escape, or code execution is not demonstrated by the patch
(inferred).
Impact Assessment
ExclusiveAccessManager (and potentially an ExclusiveAccessControllerBase) in the privileged browser process, reachable by getting a tab into fullscreen and then closing/detaching it so a controller callback destroys the manager during iteration. Any leveraging of this dangling access into an exploitable read/write primitive or code execution is (inferred) and not shown by the diff. The vendor advisory rates this critical severity and does not report in-the-wild exploitation; NVD scores it CVSS 10.0.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
Audit Directions
- Iterate-and-callback loops over owned collectionsAudit any browser-process loop that iterates a member container (like
exclusive_access_controllers_) while calling out to code that can destroy the container’s owner; require aWeakPtrliveness check after each call. - Synchronous teardown from exit/close pathsReview fullscreen, pointer-lock, tab-close and window-close callbacks that “exit” or “notify” and can re-enter and destroy
this; verify no member orthisaccess follows the potentially-destructive call. - Missing WeakPtrFactory on observersFlag
ExclusiveAccess-style manager/controller classes andWebContentsObservers that dereferencethisafter dispatching events but lack aweak_ptr_factory_guard, mirroring the two factories this patch introduced.
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.