Overview

Critical
Severity
10
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Browser
DescriptionUse after free in Browser
ComponentBrowser
Sub-componentFullscreen
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: 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.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker516683433
Fix commit0e5984e87b29 (chromium/src) +73/-16
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-26 (per vendor advisory)
Fix landed2026-06-01 (public git)
Disclosed2026-06-30
Patch gap29 days — fix public in source before the advisory

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 FullscreenController that actually exit fullscreen / pointer-lock, whose ExitExclusiveAccessIfNecessary() 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 this and its members dangling after the call returns.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
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.

Key insight
The manager and base controller invoked callbacks that could re-entrantly destroy 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

  1. Enter fullscreen A page (or the browser UI) puts the active tab into fullscreen via FullscreenController::EnterFullscreenModeForTab, registering the fullscreen controller in the manager’s exclusive_access_controllers_, as the test’s setup does.
  2. 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 as OnTabClosing over exclusive_access_controllers_.
  3. Re-entrant destruction mid-iteration The controller->...() call exits exclusive access, which synchronously tears down the owning feature and destroys the ExclusiveAccessManager (the exact teardown chain from tab close to manager destruction is not in the diff) (inferred).
  4. Use-after-free After the callee returns, the loop re-reads the freed exclusive_access_controllers_ and advances the iterator on the destroyed this, the UAF the patch’s post-call if (!weak_ptr) return; guard now prevents.
  5. 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

An attacker gains a use-after-free of the 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

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

Audit Directions

  • Iterate-and-callback loops over owned collections
    Audit 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 a WeakPtr liveness check after each call.
  • Synchronous teardown from exit/close paths
    Review fullscreen, pointer-lock, tab-close and window-close callbacks that “exit” or “notify” and can re-enter and destroy this; verify no member or this access follows the potentially-destructive call.
  • Missing WeakPtrFactory on observers
    Flag ExclusiveAccess-style manager/controller classes and WebContentsObservers that dereference this after dispatching events but lack a weak_ptr_factory_ guard, mirroring the two factories this patch introduced.
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();
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 vm...@google.com

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