Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Fullscreen
DescriptionUse after free in Fullscreen
ComponentFullscreen
Bug ClassUAF
Tracker497436531
Fix commit899e77f0d448 (chromium/src) +33/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
for
content/browser/web_contents/web_contents_impl.cc
modified
if
content/browser/web_contents/web_contents_impl.cc
modified

Files Changed

  • content/browser/web_contents/web_contents_impl.cc
From 899e77f0d448f44b5c9065911721d6f608a675c9 Mon Sep 17 00:00:00 2001
From: Jordan Bayles <[email protected]>
Date: Fri, 10 Apr 2026 14:16:28 -0700
Subject: [PATCH] Fix Use-After-Free in WebContentsImpl::ForSecurityDropFullscreen

When `ForSecurityDropFullscreen` drops the fullscreen state of upstream
openers, it iterates over a set of `raw_ptr<WebContentsImpl>` instances.
Calling `ExitFullscreen()` on these openers can spin a nested message
loop (e.g., when waiting for the OS window transition to finish).
During this nested message loop, other `WebContentsImpl` instances in
the iteration set can be destroyed via IPCs (such as `window.close()`).

Previously, the loop continued to process the dangling `raw_ptr`s in
the local copy of the set, leading to a Use-After-Free when attempting
to query their fullscreen state via `is_fullscreen()`.

This CL mitigates the UAF by first caching the target WebContentsImpl
instances as a `std::vector<base::WeakPtr<WebContentsImpl>>`. During the
actual iteration where `ExitFullscreen()` is called, the validity of
each `WeakPtr` is checked before dereferencing, ensuring that destroyed
WebContents are safely ignored even if reentrancy occurs.

Bug: 497436531
Change-Id: I190490caeab24daa9508fa27caf8dace613e9172
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7747694
Reviewed-by: Avi Drissman <[email protected]>
Commit-Queue: Jordan Bayles <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1613095}
---

diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index b1998f14..5901f6c 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -7397,10 +7397,20 @@
   // upstream contents. Drop that WebContents out of fullscreen if it does. This
   // is theoretically quadratic-ish (fullscreen contentses x each one's opener
   // length) but neither of those is expected to ever be a large number.
-  auto fullscreen_set_copy = *FullscreenContentsSet(GetBrowserContext());
-  for (WebContentsImpl* fullscreen_contents : fullscreen_set_copy) {
-    if (is_fullscreen(fullscreen_contents, display_id)) {
-      auto opener_contentses = GetAllOpeningWebContents(fullscreen_contents);
+  std::vector<base::WeakPtr<WebContentsImpl>> fullscreen_contents_list;
+  for (WebContentsImpl* fullscreen_contents :
+       *FullscreenContentsSet(GetBrowserContext())) {
+    fullscreen_contents_list.push_back(
+        fullscreen_contents->weak_factory_.GetWeakPtr());
+  }
+
+  for (auto& fullscreen_contents : fullscreen_contents_list) {
+    if (!fullscreen_contents) {
+      continue;
+    }
+    if (is_fullscreen(fullscreen_contents.get(), display_id)) {
+      auto opener_contentses =
+          GetAllOpeningWebContents(fullscreen_contents.get());
       if (opener_contentses.count(this)) {
         fullscreen_contents->ExitFullscreen(true);
       }
@@ -7414,29 +7424,41 @@
   // any request to enter fullscreen will have the upstream of the WebContents
   // checked. (See CanEnterFullscreenMode().)
 
-  std::vector<base::WeakPtr<WebContentsImpl>> blocked_contentses;
+  std::vector<base::WeakPtr<WebContentsImpl>> blocked_contents_list;
+  std::vector<base::WeakPtr<WebContentsImpl>> openers;
+  for (WebContentsImpl* opener : GetAllOpeningWebContents(this)) {
+    openers.push_back(opener->weak_factory_.GetWeakPtr());
+  }
 
-  for (auto opener : GetAllOpeningWebContents(this)) {
-    if (is_fullscreen(opener, display_id)) {
+  for (auto& opener : openers) {
+    if (!opener) {
+      continue;
+    }
+
+    if (is_fullscreen(opener.get(), display_id)) {
       opener->ExitFullscreen(true);
     }
 
+    if (!opener) {
+      continue;
+    }
+
     // ...block the WebContents from entering fullscreen until further notice.
     ++opener->fullscreen_blocker_count_;
-    blocked_contentses.push_back(opener->weak_factory_.GetWeakPtr());
+    blocked_contents_list.push_back(opener);
   }
 
   return base::ScopedClosureRunner(base::BindOnce(
-      [](std::vector<base::WeakPtr<WebContentsImpl>> blocked_contentses) {
+      [](std::vector<base::WeakPtr<WebContentsImpl>> blocked_contents_list) {
         for (base::WeakPtr<WebContentsImpl>& web_contents :
-             blocked_contentses) {
+             blocked_contents_list) {
           if (web_contents) {
             DCHECK_GT(web_contents->fullscreen_blocker_count_, 0);
             --web_contents->fullscreen_blocker_count_;
           }
         }
       },
-      std::move(blocked_contentses)));
+      std::move(blocked_contents_list)));
 }
 
 void WebContentsImpl::ResumeLoadingCreatedWebContents() {
Loading diff…

Original Bug Report

reported by [email protected]

Potential Use-After-Free in WebContentsImpl::ForSecurityDropFullscreen

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A Use-After-Free vulnerability exists in WebContentsImpl::ForSecurityDropFullscreen due to reentrancy when exiting fullscreen mode. A nested message loop allows attacker-controlled destruction of a WebContentsImpl object held by a raw pointer, leading to a UAF virtual method call. This could potentially result in a browser process sandbox escape.

Affected files:

  • content/browser/web_contents/web_contents_impl.cc

Estimated timestamp from git blame: 2023-11-30

Vulnerability Details

There is a potential Use-After-Free (UAF) vulnerability in content/browser/web_contents/web_contents_impl.cc within the WebContentsImpl::ForSecurityDropFullscreen function. This function drops the fullscreen state when security-sensitive UI elements (like JavaScript dialogs or file choosers) are invoked, preventing UI spoofing attacks.

The function contains two loops. The second loop iterates over the opener chain of the current WebContents:

  for (auto* opener : GetAllOpeningWebContents(this)) {
    if (is_fullscreen(opener, display_id)) {
      opener->ExitFullscreen(true);
    }
    // ...
    ++opener->fullscreen_blocker_count_;
    blocked_contentses.push_back(opener->weak_factory_.GetWeakPtr());
  }

GetAllOpeningWebContents returns a base::flat_set<WebContentsImpl*>. These are raw pointers, lacking MiraclePtr (raw_ptr<T>) protection.

Inside the loop, opener->ExitFullscreen(true) is called for any fullscreen opener. On platforms with complex window management (e.g., macOS, Windows), exiting fullscreen eventually calls delegate_->ExitFullscreenModeForTab(this), which can spin a nested message loop to wait for the transition to complete.

While this nested loop runs, the browser continues to process IPCs. An attacker can use an IPC (such as window.close()) to destroy another, non-fullscreen WebContentsImpl that is further down the flat_set iteration order. Because this second WebContents is not fullscreen, it is not protected by the raw_ptr quarantine present in the first loop of the function (fullscreen_set_copy). Its memory is immediately freed.

When the nested loop completes, iteration resumes. The loop will eventually process the now-dangling raw pointer. The is_fullscreen inline lambda is invoked, which executes tab->GetDelegate()->GetFullscreenState(tab). This results in a UAF virtual method call on attacker-controlled memory, leading to potential Remote Code Execution (RCE) in the privileged Browser Process.

Suggested Attack Steps (Potential)

Note: Our tooling agent does not run live exploits; these are analytically determined steps to trigger the bug.

  1. A malicious page (Page A) opens a same-origin child window (Page B).
  2. Page B opens another same-origin child window (Page C).
  3. By relying on sequential heap allocation, the attacker ensures Page A resides at a lower memory address than Page B, meaning Page A will be processed first in the base::flat_set.
  4. Page A enters fullscreen mode.
  5. Page C calls alert('XSS'), triggering a JavaScript dialog.
  6. The browser process receives the IPC and calls ForSecurityDropFullscreen on Page C’s WebContentsImpl.
  7. The second loop calls GetAllOpeningWebContents, capturing raw pointers to A, B, and C.
  8. The loop processes Page A first. Since it is fullscreen, ExitFullscreen is called, spinning a nested message loop.
  9. While blocked in the nested loop, Page C’s JavaScript executes pageB.close(), sending an IPC that destroys Page B. Page B is freed.
  10. The attacker heap-sprays via IPC to reclaim Page B’s memory with a fake WebContentsImpl containing a fake delegate_ vtable.
  11. The nested message loop finishes. The iteration continues to the dangling pointer for Page B.
  12. is_fullscreen is called on the dangling pointer, triggering a virtual call to the fake delegate_, hijacking control flow.

Suggested Fix

Refactor GetAllOpeningWebContents to return a collection of base::WeakPtr<WebContentsImpl> or base::SafeRef<WebContentsImpl> instead of raw pointers. Alternatively, convert the raw pointers to WeakPtr locally within ForSecurityDropFullscreen before entering the loop, and check for validity (if (!opener) continue;) before dereferencing them on each iteration.

Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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