CVE-2026-7908
Overview
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
Patch
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() {
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.