CVE-2026-10908
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_frame_host_impl.cc |
modified | |
TEST_Fcontent/browser/renderer_host/render_frame_host_impl_unittest.cc |
modified | |
DestructionDelegatecontent/browser/renderer_host/render_frame_host_impl_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/render_frame_host_impl_unittest.cc
Patch
From f2c344251df5af8f78971bb681e54b46c0513c33 Mon Sep 17 00:00:00 2001
From: Jordan Bayles <jophba@chromium.org>
Date: Fri, 24 Apr 2026 12:02:55 -0700
Subject: [PATCH] Fix Use-After-Free in RenderFrameHostImpl::ExitFullscreen()
The call to delegate_->ExitFullscreenMode() can synchronously destroy
the RenderFrameHostImpl object (e.g., if the tab is closed).
Subsequent calls in ExitFullscreen() then operate on the deleted object,
causing a Use-After-Free.
This CL adds a base::WeakPtr guard to detect if the object has been
destroyed and returns early if so. This is a variant of the fix for
EnterFullscreen in commit 91d5baaef742.
Bug: 505045913
Change-Id: Id2b34bea77bb80a3b6c11372cc8813a812e34a2e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7787660
Reviewed-by: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1620376}
---
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index b4b82c13..8b04c91 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -9322,8 +9322,14 @@
void RenderFrameHostImpl::ExitFullscreen() {
base::RecordAction(base::UserMetricsAction("ExitFullscreen_API"));
+
+ base::WeakPtr<RenderFrameHostImpl> weak_ptr = GetWeakPtr();
delegate_->ExitFullscreenMode(/*will_cause_resize=*/true);
+ if (!weak_ptr) {
+ return;
+ }
+
// The previous call might change the fullscreen state. We need to make sure
// the renderer is aware of that, which is done via the resize message.
// Typically, this will be sent as part of the call on the |delegate_| above
diff --git a/content/browser/renderer_host/render_frame_host_impl_unittest.cc b/content/browser/renderer_host/render_frame_host_impl_unittest.cc
index dbcdb2f..09e93d6 100644
--- a/content/browser/renderer_host/render_frame_host_impl_unittest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_unittest.cc
@@ -199,6 +199,27 @@
EXPECT_EQ(GURL(url::kAboutBlankURL), main_rfh()->GetLastCommittedURL());
}
+TEST_F(RenderFrameHostImplTest, ExitFullscreenDestruction) {
+ class DestructionDelegate : public WebContentsDelegate {
+ public:
+ explicit DestructionDelegate(base::OnceClosure destruction_closure)
+ : destruction_closure_(std::move(destruction_closure)) {}
+ void ExitFullscreenModeForTab(WebContents* web_contents) override {
+ std::move(destruction_closure_).Run();
+ }
+
+ private:
+ base::OnceClosure destruction_closure_;
+ };
+
+ DestructionDelegate delegate(base::BindOnce(
+ &RenderFrameHostImplTest::DeleteContents, base::Unretained(this)));
+ contents()->SetDelegate(&delegate);
+
+ // This should not crash.
+ main_test_rfh()->ExitFullscreen();
+}
+
// Ensures that IsolationInfo's SiteForCookies is empty and
// that it correctly generates a StorageKey with a kCrossSite
// AncestorChainBit when frames are nested in an A->B->A
Regression Test / PoC
diff --git a/content/browser/renderer_host/render_frame_host_impl_unittest.cc b/content/browser/renderer_host/render_frame_host_impl_unittest.cc
index dbcdb2f..09e93d6 100644
--- a/content/browser/renderer_host/render_frame_host_impl_unittest.cc
+++ b/content/browser/renderer_host/render_frame_host_impl_unittest.cc
@@ -199,6 +199,27 @@
EXPECT_EQ(GURL(url::kAboutBlankURL), main_rfh()->GetLastCommittedURL());
}
+TEST_F(RenderFrameHostImplTest, ExitFullscreenDestruction) {
+ class DestructionDelegate : public WebContentsDelegate {
+ public:
+ explicit DestructionDelegate(base::OnceClosure destruction_closure)
+ : destruction_closure_(std::move(destruction_closure)) {}
+ void ExitFullscreenModeForTab(WebContents* web_contents) override {
+ std::move(destruction_closure_).Run();
+ }
+
+ private:
+ base::OnceClosure destruction_closure_;
+ };
+
+ DestructionDelegate delegate(base::BindOnce(
+ &RenderFrameHostImplTest::DeleteContents, base::Unretained(this)));
+ contents()->SetDelegate(&delegate);
+
+ // This should not crash.
+ main_test_rfh()->ExitFullscreen();
+}
+
// Ensures that IsolationInfo's SiteForCookies is empty and
// that it correctly generates a StorageKey with a kCrossSite
// AncestorChainBit when frames are nested in an A->B->A
Original Bug Report
UAF in RenderFrameHostImpl::ExitFullscreen() — missing WeakPtr guard (variant of EnterFullscreen fix 91d5baaef742)
VULNERABILITY DETAILS
Use-after-free in RenderFrameHostImpl::ExitFullscreen() due to missing base::WeakPtr guard after delegate_->ExitFullscreenMode(). The delegate call can spin a nested message loop on Windows (via ::SetWindowPos), during which the RenderFrameHostImpl can be destroyed (e.g., via popup.close()). The code then dereferences freed this via GetOutermostMainFrame()->GetLocalRenderWidgetHost()->SynchronizeVisualProperties().
The identical sibling method EnterFullscreen() was explicitly fixed for this exact pattern in commit 91d5baaef742 (April 2, 2026), with the comment “This may spin the message loop and destroy this object.” ExitFullscreen(), located 21 lines below in the same file, was missed.
At content/browser/renderer_host/render_frame_host_impl.cc:9323:
void RenderFrameHostImpl::ExitFullscreen() {
base::RecordAction(base::UserMetricsAction("ExitFullscreen_API"));
delegate_->ExitFullscreenMode(/*will_cause_resize=*/true);
// ^^^ Can spin a nested message loop on Windows (::SetWindowPos)
// During the loop, this RenderFrameHostImpl can be destroyed
// NO WEAKPTR CHECK — `this` may already be freed
GetOutermostMainFrame() // ← UAF HERE
->GetLocalRenderWidgetHost()
->SynchronizeVisualProperties();
}
Compare with the FIXED EnterFullscreen() at line 9300:
// This may spin the message loop and destroy this object.
// See crbug.com/1506535, crbug.com/498752242.
base::WeakPtr<RenderFrameHostImpl> weak_ptr = GetWeakPtr();
delegate_->EnterFullscreenMode(this, *options);
if (!weak_ptr) {
return;
}
Impact
This is a use-after-free in the browser process (unsandboxed). The ExitFullscreen() method is a Mojo IPC handler on blink::mojom::LocalFrameHost, directly callable from any renderer. The this pointer is the literal C++ this, NOT a raw_ptr<>.
MiraclePtr Status: NOT PROTECTED — confirmed in the ASAN trace. No raw_ptr<T> access to this region was detected. This crash is exploitable.
ASAN Trace
==22092==ERROR: AddressSanitizer: heap-use-after-free on address 0x1297e5f35100
READ of size 8 at 0x1297e5f35100 thread T0
#0 content::RenderFrameHostImpl::ExitFullscreen render_frame_host_impl.cc:9345
#1 blink::mojom::LocalFrameHostStubDispatch::Accept frame.mojom.cc:9502
#2 mojo::InterfaceEndpointClient::HandleValidatedMessage
...
freed by thread T0 here:
#0 operator delete
#1 content::RenderFrameHostImpl::~RenderFrameHostImpl render_frame_host_impl.cc:2844
#2 content::RenderFrameHostManager::~RenderFrameHostManager
#3 content::FrameTreeNode::~FrameTreeNode
#4 content::FrameTree::~FrameTree
#5 content::WebContentsImpl::~WebContentsImpl
...
#12 content::WebContentsImpl::Close
#13 content::RenderFrameHostImpl::ClosePageIgnoringUnloadEvents
#14 content::RenderFrameHostImpl::RequestClose
MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.
The RFHI is freed via RequestClose (triggered by popup.close() from the opener) → WebContentsImpl::Close → ~WebContentsImpl → ~RenderFrameHostImpl. Then ExitFullscreen dereferences the freed this at GetOutermostMainFrame().
VERSION
Chrome Version: 149.0.7803.0 (built from source with ASAN) Operating System: Windows 11
Bug affects all Stable/Beta/Dev versions on Windows.
REPRODUCTION CASE
Attached: poc.html, rfhi_exit_fullscreen_runloop.patch
The race requires the RFHI to be destroyed during the ExitFullscreen call. To widen the race window for reliable reproduction, the attached patch adds a task-pumping base::RunLoop in RenderFrameHostImpl::ExitFullscreen() between the delegate call and the GetOutermostMainFrame() dereference, per VRP FAQ guidance (“Use Sleep() in privileged processes to simulate a race”).
Steps to reproduce:
- Apply
rfhi_exit_fullscreen_runloop.patchtocontent/browser/renderer_host/render_frame_host_impl.cc - Build Chrome with ASAN:
gn gen out/asan --args='is_asan=true is_debug=false symbol_level=1'thenautoninja -C out/asan chrome - Run:
out\asan\chrome.exe --no-sandbox --user-data-dir=<temp_dir> poc.html - Click “1. Start” to open a popup window
- Click inside the blue popup to enter fullscreen
- Click “2. Race” in the original window
- ASAN detects heap-use-after-free in
RenderFrameHostImpl::ExitFullscreen
Relationship to fixed bugs
| Method | File:Line | WeakPtr Guard? | Fixed In |
|---|---|---|---|
EnterFullscreen() |
render_frame_host_impl.cc:9300 | YES | 91d5baaef742 (April 2, 2026) |
ExitFullscreen() |
render_frame_host_impl.cc:9323 | NO | UNFIXED |
WebContentsImpl::ExitFullscreenMode() |
web_contents_impl.cc:4839 | YES | 40947201 (2024) |
WebContentsImpl::DidNavigateAnyFramePreCommit() |
web_contents_impl.cc:7928 | YES | 8e95aab414bf (April 15, 2026) |
Navigator::DidNavigate() |
navigator.cc:528 | YES | 8e95aab414bf (April 15, 2026) |
SUGGESTED FIX
Add a base::WeakPtr guard in ExitFullscreen(), identical to EnterFullscreen():
void RenderFrameHostImpl::ExitFullscreen() {
base::RecordAction(base::UserMetricsAction("ExitFullscreen_API"));
+ base::WeakPtr<RenderFrameHostImpl> weak_ptr = GetWeakPtr();
delegate_->ExitFullscreenMode(/*will_cause_resize=*/true);
+ if (!weak_ptr) {
+ return;
+ }
GetOutermostMainFrame()
->GetLocalRenderWidgetHost()
->SynchronizeVisualProperties();
}
CREDIT INFORMATION
Reporter credit: Mihnea Nicolau