Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Browser
DescriptionUse after free in Browser
ComponentBrowser
Bug ClassUAF
Tracker516683433
Fix commit0e5984e87b29 (chromium/src) +73/-16
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc
modified
for
chrome/browser/ui/exclusive_access/exclusive_access_manager.cc
modified
if
chrome/browser/ui/exclusive_access/exclusive_access_manager.cc
modified

Files Changed

  • chrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc
  • chrome/browser/ui/exclusive_access/exclusive_access_controller_base.h
  • chrome/browser/ui/exclusive_access/exclusive_access_manager.cc
  • chrome/browser/ui/exclusive_access/exclusive_access_manager.h
  • chrome/browser/ui/exclusive_access/fullscreen_controller.cc
From 0e5984e87b2914ce3336cf20207e0724b72c0c61 Mon Sep 17 00:00:00 2001
From: Muyao Xu <[email protected]>
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 <[email protected]>
Reviewed-by: Mike Wasserman <[email protected]>
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();
Loading diff…

Regression Test / PoC

shipped with the fix
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();
+}
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • chrome/browser/ui/exclusive_access/fullscreen_controller.cc
  • chrome/browser/ui/exclusive_access/exclusive_access_controller_base.cc
  • chrome/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:

  1. UAF in Range-for Iteration: The loop iterators (__begin, __end) point directly into the deleted base::flat_set’s backing heap memory, resulting in an immediate UAF read or write upon the next iteration.
  2. UAF in FullscreenController Tail: Upon returning from the nested native dispatch, remaining lines in ExitFullscreenModeInternal() (such as extension_url_.reset() or exclusive_access_manager()->UpdateBubble(...)) execute on a freed this pointer.
  3. UAF in AutoReset Destructor: In FullscreenController::OnTabDeactivated(), stack unwinding triggers the destructor of base::AutoReset<raw_ptr<content::WebContents>>, writing to the deallocated controller instance.
  4. Base Class Notification Hazard: In ExclusiveAccessControllerBase::OnTabClosing(), the call to NotifyTabExclusiveAccessLost() 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:

  1. Open a script-closable browser window and request tab-fullscreen using LocalFrameHost::EnterFullscreen.
  2. 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_CLOSE to be processed during state transitions on Windows).
  3. Programmatically close the tab, invoking the OnTabClosing event pipeline.
  4. As the loop processes the first element (FullscreenController), the synchronous transition forces the destruction of the parent widget and resets ExclusiveAccessManager.
  5. 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.

View on issue tracker