CVE-2026-7356
Overview
Background
- `WebContentsImpl`
- The browser-process object representing a tab’s contents; it owns much of the per-tab navigation and fullscreen state.
- `RenderFrameHostImpl`
- The browser-process representation of a frame, created/destroyed as navigations commit.
- `base::WeakPtr`
- A non-owning pointer that becomes null when its target is destroyed, letting callers detect that an object was freed during a re-entrant call.
- Nested message loop during fullscreen exit
ExitFullscreen()and related pre-commit tasks can spin a nested run loop, allowing other tasks (including ones that destroy theWebContentsImplorRenderFrameHostImpl) to run before control returns.
Root Cause Analysis
During a cross-document navigation, Navigator::DidNavigate (named in the commit message) calls delegate_->DidNavigateAnyFramePreCommit(...), and WebContentsImpl::DidNavigateAnyFramePreCommit (also named in the commit message) calls ExitFullscreen(false) on the fullscreen-exit path. Both of those calls can pump a nested message loop, during which the RenderFrameHostImpl (in Navigator::DidNavigate) or the WebContentsImpl itself (in DidNavigateAnyFramePreCommit) may be destroyed. The pre-patch code resumed executing after those calls — dereferencing render_frame_host/params and this (e.g. CancelKeyboardLock(keyboard_lock_widget_)) — assuming the objects still existed, which is the violated lifetime invariant.
The fix captures a base::WeakPtr before each re-entrant call (weak_rfh = render_frame_host->GetWeakPtr() and weak_this = weak_factory_.GetWeakPtr()) and returns early if the weak pointer is null afterward, so no code runs against a freed RenderFrameHostImpl or WebContentsImpl.
this/render_frame_host after calls (ExitFullscreen, pre-commit tasks) that can spin a nested loop and destroy those objects; the fix adds base::WeakPtr liveness checks after each such call and returns early when the object is gone.Attack Path
- Enter fullscreen Web content puts the tab into fullscreen so the fullscreen-exit path is active.
- Start a cross-document navigation
Trigger a cross-document navigation whose pre-commit processing reaches
DidNavigateAnyFramePreCommitwithshould_exit_fullscreentrue, invokingExitFullscreen(false). - Destroy the object in the nested loop
Arrange for the nested message loop spun by fullscreen exit / pre-commit tasks to run a task that destroys the
WebContentsImplor the committingRenderFrameHostImpl(e.g. closing the tab/frame during the transition). - Use-after-free on return
Control returns to the pre-patch code, which dereferences the freed
this/render_frame_host(e.g.CancelKeyboardLock, or continuedNavigator::DidNavigatework), the UAF the advisory reports.
Impact Assessment
WebContentsImpl or RenderFrameHostImpl in the browser process, which the vendor advisory rates high severity. Preconditions are entering fullscreen and timing a navigation plus an object-destroying event so destruction happens inside the nested message loop. The advisory does not report in-the-wild exploitation, and escalation of this browser-process UAF to code execution or sandbox-related compromise is (inferred), not demonstrated by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/navigator.cc |
modified | |
ifcontent/browser/web_contents/web_contents_impl.cc |
modified |
Files Changed
content/browser/renderer_host/navigator.cccontent/browser/web_contents/web_contents_impl.cc
Audit Directions
- Re-entrant calls that can destroy `this`Audit browser-process methods that continue touching member state after calling functions (fullscreen, modal dialogs, event dispatch) known to spin nested run loops; each needs a
WeakPtr/liveness guard before resuming. - Navigation-commit lifetimeReview commit-time code paths in
NavigatorandWebContentsImplthat assume theRenderFrameHostImplor delegate survives delegate callbacks during navigation. - Fullscreen / keyboard-lock teardown orderingLook for other call sites pairing
ExitFullscreenwith follow-up cleanup (CancelKeyboardLock, widget teardown) that may dereference objects freed by the fullscreen transition.
Patch
From 8e95aab414bfce3dd56947c3464b5d8465e4b2bb Mon Sep 17 00:00:00 2001
From: Jordan Bayles <jophba@chromium.org>
Date: Wed, 15 Apr 2026 15:26:47 -0700
Subject: [PATCH] Fix UAF in fullscreen exit during cross-document navigation
This change addresses a Use-After-Free (UAF) vulnerability that occurs
when exiting fullscreen during a cross-document navigation, by
introducing `base::WeakPtr` checks in
`WebContentsImpl::DidNavigateAnyFramePreCommit` and
`Navigator::DidNavigate` to ensure that early destruction of
`WebContentsImpl` or `RenderFrameHostImpl` during nested message loops
does not result in dereferencing freed memory.
Bug: 497769116
Change-Id: Ife5e726df78beb2f1d51d8d782f3b87b53d65fdb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7750093
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1615462}
---
diff --git a/content/browser/renderer_host/navigator.cc b/content/browser/renderer_host/navigator.cc
index 1819408..8fcff3f 100644
--- a/content/browser/renderer_host/navigator.cc
+++ b/content/browser/renderer_host/navigator.cc
@@ -525,9 +525,16 @@
#endif // BUILDFLAG(IS_ANDROID)
// Run tasks that must execute just before the commit.
+ base::WeakPtr<RenderFrameHostImpl> weak_rfh = render_frame_host->GetWeakPtr();
delegate_->DidNavigateAnyFramePreCommit(navigation_request.get(),
was_within_same_document);
+ // NOTE: the pre commit tasks may result in the destruction of the render
+ // frame host, in which case we should exit this method early.
+ if (!weak_rfh) {
+ return;
+ }
+
if (ui::PageTransitionIsMainFrame(params.transition)) {
delegate_->DidNavigateMainFramePreCommit(navigation_request.get(),
was_within_same_document);
diff --git a/content/browser/web_contents/web_contents_impl.cc b/content/browser/web_contents/web_contents_impl.cc
index 736e23bf..76c3b0c5 100644
--- a/content/browser/web_contents/web_contents_impl.cc
+++ b/content/browser/web_contents/web_contents_impl.cc
@@ -7925,7 +7925,13 @@
}
if (should_exit_fullscreen) {
+ base::WeakPtr<WebContentsImpl> weak_this = weak_factory_.GetWeakPtr();
ExitFullscreen(false);
+
+ // If `this` gets destructed due to ExitFullscreen(), we need to exit early.
+ if (!weak_this) {
+ return;
+ }
CancelKeyboardLock(keyboard_lock_widget_);
}
}
Original Bug Report
Use-After-Free in Navigator::DidNavigate via ExitFullscreenModeForTab
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A potential Use-After-Free exists when exiting fullscreen during a cross-document navigation. This can spin a nested message loop, during which the WebContents may be destroyed, leading to virtual function calls on a dangling this pointer and bypassing MiraclePtr.
Affected files:
content/browser/renderer_host/navigator.cccontent/browser/web_contents/web_contents_impl.cc
Estimated timestamp from git blame: 2026-03-06
Description
A potential Use-After-Free (UAF) vulnerability exists in the browser process, specifically in how cross-document navigations handle exiting fullscreen mode.
When a navigation commits, Navigator::DidNavigate invokes delegate_->DidNavigateAnyFramePreCommit(...) (content/browser/renderer_host/navigator.cc). The delegate is typically WebContentsImpl.
If the page is currently in fullscreen, WebContentsImpl::DidNavigateAnyFramePreCommit will attempt to exit fullscreen by calling ExitFullscreen(false), which eventually calls delegate_->ExitFullscreenModeForTab(this). As noted by inline comments in WebContentsImpl::ExitFullscreenMode (crbug.com/1506535), this delegate call may spin a nested message loop (e.g., on macOS for window transitions).
If the WebContentsImpl object is destroyed while this nested message loop is spinning (for example, if the fullscreen window is a popup and its opener closes it), the this pointers currently on the stack in Navigator::DidNavigate and WebContentsImpl::DidNavigateAnyFramePreCommit become dangling.
When the nested loop finishes, ExitFullscreenMode safely detects the destruction via a WeakPtr and returns early. However, the stack unwinds to DidNavigateAnyFramePreCommit, which immediately calls CancelKeyboardLock(keyboard_lock_widget_). This accesses the freed this pointer to read the keyboard_lock_widget_ member and perform a virtual method dispatch, leading to a UAF and potential vtable hijack.
Furthermore, the stack will unwind to Navigator::DidNavigate, which subsequently checks if the transition is a main frame and calls delegate_->DidNavigateMainFramePreCommit(...), reading the delegate_ from freed memory and performing another virtual call.
MiraclePtr (BRP) Bypass
This vulnerability naturally bypasses MiraclePtr (BackupRefPtr). WebContentsImpl embeds FrameTree directly, which in turn embeds Navigator directly. Therefore, all three reside in the exact same heap allocation. Navigator holds a raw_ptr<NavigatorDelegate> delegate_ which points to the WebContentsImpl.
When the WebContentsImpl is destroyed, the Navigator and its raw_ptr member are destroyed along with it. The raw_ptr destructor drops the BRP reference count before the memory is released to the allocator. Since the stack pointers waiting on the call stack are raw C++ pointers, the reference count drops to zero, and the memory is freed without being quarantined. An attacker can then use heap spraying to reclaim the memory with controlled data before the execution resumes.
Suggested Steps to Trigger
Note: These are potential steps as we have not verified them with a working Proof of Concept.
- An attacker creates a malicious webpage that opens a popup window.
- The user interacts with the popup, satisfying the user gesture requirement. The popup requests and enters fullscreen mode.
- The popup initiates a cross-document navigation to a different site.
- The navigation reaches the commit phase in the browser process, eventually calling
WebContentsImpl::DidNavigateAnyFramePreCommit, which spins a nested message loop to exit fullscreen. - While the browser is spinning the message loop, the attacker’s opener window calls
popup.close(), queuing a task that synchronously destroys the popup’sWebContentsImpl. - The opener window performs a heap spray via IPCs, allocating objects of the same size as
WebContentsImplto reclaim the freed memory with attacker-controlled bytes. - The nested message loop completes, and the stack unwinds back to
DidNavigateAnyFramePreCommitandNavigator::DidNavigate. - The code dereferences the dangling
thispointer (now pointing to sprayed memory) to call a virtual function, leading to arbitrary code execution in the browser process.
Proposed Fix
In WebContentsImpl::DidNavigateAnyFramePreCommit, acquire a base::WeakPtr to this before calling ExitFullscreen(false). If the WeakPtr is invalidated after the call returns, return early.
Similarly, in Navigator::DidNavigate, acquire a base::WeakPtr to the relevant object (e.g., via the RenderFrameHost or FrameTree) before the call to delegate_->DidNavigateAnyFramePreCommit. Check its validity before proceeding to call delegate_->DidNavigateMainFramePreCommit. Alternatively, delay the fullscreen exit logic and keyboard lock cancellation until after the synchronous commit sequence is complete.
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from 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.