High watch CVSS 9.6 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Fullscreen
DescriptionUse after free in Fullscreen
ComponentFullscreen
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker497436531
Fix commit899e77f0d448 (chromium/src) +33/-11
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-04-10 (public git)
Disclosed2026-05-05
Patch gap25 days — fix public in source before the advisory

Background

`WebContentsImpl::ForSecurityDropFullscreen`
a browser-process method that, for security, drops fullscreen on upstream opener WebContents and blocks them from re-entering fullscreen while the caller is active.
`FullscreenContentsSet`
a per-BrowserContext set of raw_ptr<WebContentsImpl> tracking every WebContents currently in fullscreen.
Nested message loop
ExitFullscreen() can pump the message loop (e.g. waiting for an OS window transition), during which unrelated IPCs such as window.close() run and can destroy other WebContents.
`base::WeakPtr`
a smart pointer that becomes null when its referent is destroyed, so it can be tested before dereference, unlike a raw_ptr which merely dangles.

Root Cause Analysis

ForSecurityDropFullscreen took a local copy of the fullscreen set as fullscreen_set_copy, a std::set of raw WebContentsImpl*, and iterated it calling is_fullscreen(), GetAllOpeningWebContents(), and ExitFullscreen() on each entry. Because ExitFullscreen() can spin a nested message loop, a reentrant IPC (e.g. window.close()) can destroy other WebContentsImpl instances that are still pending in the local copy; the copy holds plain pointers that are not updated on destruction, so the loop subsequently dereferences freed memory when it calls is_fullscreen(fullscreen_contents, ...). The invariant violated is that every pointer iterated must still refer to a live object, which no longer holds across a reentrant call.

The fix first snapshots the targets as a std::vector<base::WeakPtr<WebContentsImpl>> and, in the iteration that actually calls ExitFullscreen(), skips any entry whose WeakPtr has gone null (if (!fullscreen_contents) continue;). The second loop over GetAllOpeningWebContents(this) is likewise converted to WeakPtrs and gains an extra null re-check after ExitFullscreen() before touching fullscreen_blocker_count_, closing the same reentrancy window on the opener path.

Key insight
The code iterated a snapshot of raw pointers across an operation (ExitFullscreen()) that can reentrantly free those very objects; replacing the snapshot with WeakPtrs and null-checking before each dereference makes the stale entries detectable and safely skippable.

Attack Path

  1. Enter fullscreen across openers Attacker arranges a chain of related WebContents (opener relationships) with one or more in fullscreen on the same display, so the FullscreenContentsSet holds multiple entries.
  2. Trigger the security drop An action that invokes ForSecurityDropFullscreen begins iterating the copied set and calls ExitFullscreen() on a fullscreen contents.
  3. Reenter via nested loop While ExitFullscreen() pumps a nested message loop for the window transition, attacker-controlled script fires window.close() (or similar) on another WebContents still queued in the iteration, destroying it.
  4. Dangling dereference The loop resumes and calls is_fullscreen() on the now-freed WebContentsImpl*, a use-after-free read in the browser process.
  5. Exploitation Controlling the contents of the freed slot could turn the UAF into a more powerful primitive (inferred); the patch itself only demonstrates the dangling access.

Impact Assessment

An attacker with script in a renderer able to drive fullscreen and reentrant navigation/close can cause a use-after-free in the privileged browser process, with the precondition of multiple related fullscreen WebContents and the ability to time a destroy during the nested message loop. The vendor advisory rates this high severity (“Use after free in Fullscreen”); it does not report in-the-wild exploitation. Any escalation beyond the browser-process UAF read (memory corruption, code execution) is (inferred) and not shown by the diff.

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

Audit Directions

  • Iterate-then-callback over live objects
    Review any loop that holds raw_ptr/raw-pointer snapshots and calls methods (like ExitFullscreen(), navigation, or sync IPC) that can pump a nested message loop and free sibling entries; prefer WeakPtr snapshots with per-iteration validity checks.
  • Reentrancy via `window.close()` and fullscreen transitions
    Audit browser-process fullscreen, window, and widget code paths that wait on OS transitions, since those are the points where attacker IPCs reenter and mutate collections mid-iteration.
  • `FullscreenContentsSet` / opener-graph traversal
    Check other users of the fullscreen set and GetAllOpeningWebContents() for the same assumption that the set stays stable across callbacks.
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