CVE-2026-7908
Overview
Background
- `WebContentsImpl::ForSecurityDropFullscreen`
- a browser-process method that, for security, drops fullscreen on upstream opener
WebContentsand blocks them from re-entering fullscreen while the caller is active. - `FullscreenContentsSet`
- a per-
BrowserContextset ofraw_ptr<WebContentsImpl>tracking everyWebContentscurrently in fullscreen. - Nested message loop
ExitFullscreen()can pump the message loop (e.g. waiting for an OS window transition), during which unrelated IPCs such aswindow.close()run and can destroy otherWebContents.- `base::WeakPtr`
- a smart pointer that becomes null when its referent is destroyed, so it can be tested before dereference, unlike a
raw_ptrwhich 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.
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
- Enter fullscreen across openers
Attacker arranges a chain of related
WebContents(opener relationships) with one or more in fullscreen on the same display, so theFullscreenContentsSetholds multiple entries. - Trigger the security drop
An action that invokes
ForSecurityDropFullscreenbegins iterating the copied set and callsExitFullscreen()on a fullscreen contents. - Reenter via nested loop
While
ExitFullscreen()pumps a nested message loop for the window transition, attacker-controlled script fireswindow.close()(or similar) on anotherWebContentsstill queued in the iteration, destroying it. - Dangling dereference
The loop resumes and calls
is_fullscreen()on the now-freedWebContentsImpl*, a use-after-free read in the browser process. - 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
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
| Function | Change | Notes |
|---|---|---|
forcontent/browser/web_contents/web_contents_impl.cc |
modified | |
ifcontent/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 objectsReview any loop that holds
raw_ptr/raw-pointer snapshots and calls methods (likeExitFullscreen(), navigation, or sync IPC) that can pump a nested message loop and free sibling entries; preferWeakPtrsnapshots with per-iteration validity checks. - Reentrancy via `window.close()` and fullscreen transitionsAudit 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 traversalCheck other users of the fullscreen set and
GetAllOpeningWebContents()for the same assumption that the set stays stable across callbacks.
Patch
From 899e77f0d448f44b5c9065911721d6f608a675c9 Mon Sep 17 00:00:00 2001
From: Jordan Bayles <jophba@chromium.org>
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 <avi@chromium.org>
Commit-Queue: Jordan Bayles <jophba@chromium.org>
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() {
Original Bug Report
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.
- A malicious page (Page A) opens a same-origin child window (Page B).
- Page B opens another same-origin child window (Page C).
- 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. - Page A enters fullscreen mode.
- Page C calls
alert('XSS'), triggering a JavaScript dialog. - The browser process receives the IPC and calls
ForSecurityDropFullscreenon Page C’sWebContentsImpl. - The second loop calls
GetAllOpeningWebContents, capturing raw pointers to A, B, and C. - The loop processes Page A first. Since it is fullscreen,
ExitFullscreenis called, spinning a nested message loop. - While blocked in the nested loop, Page C’s JavaScript executes
pageB.close(), sending an IPC that destroys Page B. Page B is freed. - The attacker heap-sprays via IPC to reclaim Page B’s memory with a fake
WebContentsImplcontaining a fakedelegate_vtable. - The nested message loop finishes. The iteration continues to the dangling pointer for Page B.
is_fullscreenis called on the dangling pointer, triggering a virtual call to the fakedelegate_, 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.