CVE-2026-7956
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
EXPECT_TRUEcontent/browser/security_exploit_browsertest.cc |
modified | |
BeginNavigationTransitionReplacercontent/browser/security_exploit_browsertest.cc |
modified |
Files Changed
content/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/render_frame_host_manager.hcontent/browser/security_exploit_browsertest.cc
Patch
From 1ccd3deae8ccccf326c8c67f646cf3101ff114c0 Mon Sep 17 00:00:00 2001 From: Alex Moshchuk <[email protected]> Date: Fri, 27 Mar 2026 19:47:14 -0700 Subject: [PATCH] Disallow spoofed unload ACKs during inner delegate attachment. This CL prevents misbehaving renderers that are attaching an inner WebContents for MimeHandlerView (such as when navigating to a PDF), from incorrectly triggering RenderFrameDeleted from OnUnloadACK() while not actually waiting for an unload ACK. This can happen in the async time window where MimeHandlerView is "preparing to attach", invoked via RenderFrameHostImpl::PrepareForInnerWebContentsAttach(), which tries to run beforeunload handlers before proceeding with the actual attachment via WebContents::AttachInnerWebContents(). To do this, OnUnloadACK() checks that the inner delegate has already been attached, rather than still preparing to attach, at the time it's received. This should always be the case in normal cases, as the attachment process sends the Unload IPC in SwapOuterDelegateFrame() (to swap the placeholder RenderFrame with a proxy and make it non-live) and then calls set_attach_inner_delegate_complete() [1] in the same task. Another approach that was considered here was to check `is_waiting_for_unload_ack_` in OnUnloadACK() before proceeding with RenderFrameDeleted() for inner delegate cases, rather than after. This didn't work out, as it also required updating RenderFrameHostImpl::SwapOuterDelegateFrame() to set `is_waiting_for_unload_ack_` to true, which confuses other unload-related code (e.g., to later try to take placeholder frames for an inner WebContents off the RFH pending delete list, which we don't use for placeholder frames). [1] https://source.chromium.org/chromium/chromium/src/+/main:content/browser/web_contents/web_contents_impl.cc;l=3373;drc=e28fa5b90bf167ca4f5edaa753106753e1e59a4e Bug: 496463315 Change-Id: I0bdf4dadfc8b642af39e51a4c8fee9dd3cb8f045 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7707137 Reviewed-by: Charlie Reis <[email protected]> Reviewed-by: Kevin McNee <[email protected]> Commit-Queue: Alex Moshchuk <[email protected]> Cr-Commit-Position: refs/heads/main@{#1606603} --- diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index 8ae223b..914b061 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -7061,10 +7061,19 @@ // it makes its renderer send this message. `owner_` is non null since this // attachment can only happen for subframes and pending deletion is the only // case where subframes may have a null `owner_`. + // + // Note that for MimeHandlerView specifically, the unload ACK can only be + // legitimately received after the inner delegate has already been attached by + // `RFH::SwapOuterDelegateFrame()`, and should be ignored if it's received + // during an earlier MimeHandlerView-specific preparation phase invoked via + // `RFH::PrepareForInnerContentsAttach()` (because not ignoring it would later + // disrupt the attachment, e.g. by causing the Unload IPC not to be sent). + // Hence, it's important to check for `is_inner_delegate_attached()` rather + // than `is_attaching_inner_delegate()`. RenderFrameHostOwner* owner = IsPendingDeletion() ? GetFrameTreeNodeForUnload() : owner_; if (!is_main_frame() && - owner->GetRenderFrameHostManager().is_attaching_inner_delegate()) { + owner->GetRenderFrameHostManager().is_inner_delegate_attached()) { // This RFH was unloaded while attaching an inner delegate. The RFH // will stay around but it will no longer be associated with a RenderFrame. RenderFrameDeleted(); diff --git a/content/browser/renderer_host/render_frame_host_manager.h b/content/browser/renderer_host/render_frame_host_manager.h index aba08454..517ecfc 100644 --- a/content/browser/renderer_host/render_frame_host_manager.h +++ b/content/browser/renderer_host/render_frame_host_manager.h @@ -719,6 +719,12 @@ return attach_to_inner_delegate_state_ != AttachToInnerDelegateState::NONE; } + // Returns true if an inner delegate has been fully attached. + bool is_inner_delegate_attached() const { + return attach_to_inner_delegate_state_ == + AttachToInnerDelegateState::ATTACHED; + } + // Called by the delegate at the end of the attaching process. void set_attach_inner_delegate_complete() { attach_to_inner_delegate_state_ = AttachToInnerDelegateState::ATTACHED; diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc index fbbcfca9..b500c32 100644 --- a/content/browser/security_exploit_browsertest.cc +++ b/content/browser/security_exploit_browsertest.cc @@ -24,6 +24,7 @@ #include "base/test/bind.h" #include "base/test/gtest_util.h" #include "base/test/scoped_feature_list.h" +#include "base/test/test_future.h" #include "base/unguessable_token.h" #include "build/build_config.h" #include "content/browser/attribution_reporting/attribution_manager.h" @@ -1859,6 +1860,75 @@ kill_waiter.Wait()); } +// Inner delegate attachment for MimeHandlerView has an extra phase in the +// beginning, invoked via RenderFrameHost::PrepareForInnerContentsAttach(), +// which currently invokes beforeunload handlers prior to proceeding with +// normal attachment in AttachInnerWebContents()/AttachGuestPage() in a later +// task. Ensure that a misbehaving renderer doesn't trigger RenderFrameDeleted() +// via an unload ACK in that first PrepareForInnerContentsAttach() phase, where +// we aren't actually expecting that unload ACK. +IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, + SpoofUnloadACKDuringInnerDelegateAttach) { + // Start on a page with a blank iframe, simulating the normal starting point + // of attaching an inner delegate (e.g., for MimeHandlerView) to a placeholder + // subframe. + GURL main_url( + embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html")); + EXPECT_TRUE(NavigateToURL(shell(), main_url)); + + WebContentsImpl* web_contents = + static_cast<WebContentsImpl*>(shell()->web_contents()); + RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame(); + RenderFrameHostImpl* subframe = main_frame->child_at(0)->current_frame_host(); + + // Add a beforeunload handler to the blank subframe. This shouldn't ever + // happen during the normal inner delegate attachment flow, but a compromised + // renderer can still do it. This is not technically necessary for the + // renderer to send a spoofed unlock ACK while we're in this phase, but it + // makes the time window where this ACK can lead to problems larger and more + // practical. + // TODO(crbug.com/40249634): Make it impossible for placeholder + // frames to trigger beforeunload during inner delegate attachment. + subframe->DisableBeforeUnloadHangMonitorForTesting(); + EXPECT_TRUE(ExecJs(subframe, "window.onbeforeunload = function() {};")); + + // Prepare for inner web contents attach (this simulates MimeHandlerView + // creating the PDF inner delegate). This currently triggers the beforeunload + // phase. + base::test::TestFuture<RenderFrameHost*> future; + subframe->PrepareForInnerWebContentsAttach(future.GetCallback()); + + // Verify that the subframe is waiting for a beforeunload ACK and also + // attaching an inner delegate. + EXPECT_TRUE(subframe->is_waiting_for_beforeunload_completion()); + EXPECT_TRUE(subframe->frame_tree_node() + ->render_manager() + ->is_attaching_inner_delegate()); + + RenderFrameHostWrapper observer(subframe); + + // Spoof DidUnloadRenderFrame IPC while waiting for beforeunload ACK. + subframe->OnUnloadACK(); + + // Ensure the subframe is still live, and its RenderFrame is not deleted. + ASSERT_FALSE(observer.IsRenderFrameDeleted()); + + // Check that we're still in the inner delegate attachment phase. + EXPECT_TRUE(subframe->frame_tree_node() + ->render_manager() + ->is_attaching_inner_delegate()); + + // The RFH shouldn't change when inner delegate attachment is ready to + // proceed. + EXPECT_EQ(subframe, future.Get()); + + // Make sure proceeding with the inner WebContents attachment succeeds. + EXPECT_TRUE(CreateAndAttachInnerContents(subframe)); + EXPECT_TRUE(subframe->frame_tree_node() + ->render_manager() + ->is_inner_delegate_attached()); +} + class BeginNavigationTransitionReplacer : public FrameHostInterceptor { public: BeginNavigationTransitionReplacer(WebContents* web_contents,
Regression Test / PoC
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index fbbcfca9..b500c32 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -24,6 +24,7 @@
#include "base/test/bind.h"
#include "base/test/gtest_util.h"
#include "base/test/scoped_feature_list.h"
+#include "base/test/test_future.h"
#include "base/unguessable_token.h"
#include "build/build_config.h"
#include "content/browser/attribution_reporting/attribution_manager.h"
@@ -1859,6 +1860,75 @@
kill_waiter.Wait());
}
+// Inner delegate attachment for MimeHandlerView has an extra phase in the
+// beginning, invoked via RenderFrameHost::PrepareForInnerContentsAttach(),
+// which currently invokes beforeunload handlers prior to proceeding with
+// normal attachment in AttachInnerWebContents()/AttachGuestPage() in a later
+// task. Ensure that a misbehaving renderer doesn't trigger RenderFrameDeleted()
+// via an unload ACK in that first PrepareForInnerContentsAttach() phase, where
+// we aren't actually expecting that unload ACK.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+ SpoofUnloadACKDuringInnerDelegateAttach) {
+ // Start on a page with a blank iframe, simulating the normal starting point
+ // of attaching an inner delegate (e.g., for MimeHandlerView) to a placeholder
+ // subframe.
+ GURL main_url(
+ embedded_test_server()->GetURL("a.com", "/page_with_blank_iframe.html"));
+ EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+ WebContentsImpl* web_contents =
+ static_cast<WebContentsImpl*>(shell()->web_contents());
+ RenderFrameHostImpl* main_frame = web_contents->GetPrimaryMainFrame();
+ RenderFrameHostImpl* subframe = main_frame->child_at(0)->current_frame_host();
+
+ // Add a beforeunload handler to the blank subframe. This shouldn't ever
+ // happen during the normal inner delegate attachment flow, but a compromised
+ // renderer can still do it. This is not technically necessary for the
+ // renderer to send a spoofed unlock ACK while we're in this phase, but it
+ // makes the time window where this ACK can lead to problems larger and more
+ // practical.
+ // TODO(crbug.com/40249634): Make it impossible for placeholder
+ // frames to trigger beforeunload during inner delegate attachment.
+ subframe->DisableBeforeUnloadHangMonitorForTesting();
+ EXPECT_TRUE(ExecJs(subframe, "window.onbeforeunload = function() {};"));
+
+ // Prepare for inner web contents attach (this simulates MimeHandlerView
+ // creating the PDF inner delegate). This currently triggers the beforeunload
+ // phase.
+ base::test::TestFuture<RenderFrameHost*> future;
+ subframe->PrepareForInnerWebContentsAttach(future.GetCallback());
+
+ // Verify that the subframe is waiting for a beforeunload ACK and also
+ // attaching an inner delegate.
+ EXPECT_TRUE(subframe->is_waiting_for_beforeunload_completion());
+ EXPECT_TRUE(subframe->frame_tree_node()
+ ->render_manager()
+ ->is_attaching_inner_delegate());
+
+ RenderFrameHostWrapper observer(subframe);
+
+ // Spoof DidUnloadRenderFrame IPC while waiting for beforeunload ACK.
+ subframe->OnUnloadACK();
+
+ // Ensure the subframe is still live, and its RenderFrame is not deleted.
+ ASSERT_FALSE(observer.IsRenderFrameDeleted());
+
+ // Check that we're still in the inner delegate attachment phase.
+ EXPECT_TRUE(subframe->frame_tree_node()
+ ->render_manager()
+ ->is_attaching_inner_delegate());
+
+ // The RFH shouldn't change when inner delegate attachment is ready to
+ // proceed.
+ EXPECT_EQ(subframe, future.Get());
+
+ // Make sure proceeding with the inner WebContents attachment succeeds.
+ EXPECT_TRUE(CreateAndAttachInnerContents(subframe));
+ EXPECT_TRUE(subframe->frame_tree_node()
+ ->render_manager()
+ ->is_inner_delegate_attached());
+}
+
class BeginNavigationTransitionReplacer : public FrameHostInterceptor {
public:
BeginNavigationTransitionReplacer(WebContents* web_contents,
Original Bug Report
Sandbox Escape: Premature RenderFrameDeleted via spoofed DidUnloadRenderFrame during inner delegate attach
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A logic flaw in RenderFrameHostImpl::OnUnloadACK allows a compromised renderer to spoof a DidUnloadRenderFrame IPC while the browser is waiting for a BeforeUnload ACK. This bypasses the unload acknowledgment check and prematurely calls RenderFrameDeleted() on an active frame. Observers free associated state while the frame remains in the tree, leading to a potential Use-After-Free (UAF) in the browser process.
Affected files:
content/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/agent_scheduling_group_host.cccontent/browser/renderer_host/render_frame_host_manager.cc
Estimated timestamp from git blame: 2025-10-27
Summary
A static analysis of content/browser/renderer_host/render_frame_host_impl.cc reveals a potential vulnerability in RenderFrameHostImpl::OnUnloadACK. The function handles the acknowledgement of a frame unloading. However, the logic contains a critical ordering flaw: it checks if the frame is attaching an inner delegate (e.g., loading a PDF via MimeHandlerView) and immediately calls RenderFrameDeleted() before verifying if the browser is actually waiting for an Unload ACK.
A compromised renderer can exploit this by sending a spoofed DidUnloadRenderFrame IPC while the browser is only waiting for a BeforeUnload ACK. This triggers RenderFrameDeleted() prematurely, causing WebContentsObservers to free per-frame state. Because the BeforeUnload timeout is still running and the frame is not detached from the FrameTree, subsequent iterations over the frame tree (e.g., via ForEachRenderFrameHost) will yield this zombie frame, potentially causing observers to access freed state, leading to a Use-After-Free (UAF) in the highly privileged Browser process.
Technical Details
In RenderFrameHostImpl::OnUnloadACK, the code evaluates whether to delete the frame during inner delegate attachment:
void RenderFrameHostImpl::OnUnloadACK() {
// ...
RenderFrameHostOwner* owner =
IsPendingDeletion() ? GetFrameTreeNodeForUnload() : owner_;
if (!is_main_frame() &&
owner->GetRenderFrameHostManager().is_attaching_inner_delegate()) {
// This RFH was unloaded while attaching an inner delegate.
RenderFrameDeleted();
return;
}
// Ignore spurious unload ack.
if (!is_waiting_for_unload_ack_) {
return;
}
// ...
}
The is_attaching_inner_delegate() check occurs before the is_waiting_for_unload_ack_ check.
When a subframe navigates to a resource requiring an inner delegate (like a PDF), RenderFrameHostManager::PrepareForInnerDelegateAttach sets the state to PREPARE_FRAME (making is_attaching_inner_delegate() return true). If the subframe has a beforeunload handler, the browser dispatches a BeforeUnload IPC and starts a timeout. Crucially, during this phase, the browser is waiting for a BeforeUnload ACK, so is_waiting_for_unload_ack_ is false.
A compromised renderer can send a DidUnloadRenderFrame IPC for this subframe via AgentSchedulingGroupHost. The browser receives it, calls OnUnloadACK(), hits the is_attaching_inner_delegate() bypass, and prematurely executes RenderFrameDeleted().
Potential Exploitation Steps
(Note: These are suggested steps based on static analysis; a working Proof of Concept has not yet been developed.)
- Attacker Setup: A compromised renderer creates an
<iframe>and installs abeforeunloadevent handler on it. - Trigger Attachment: The renderer navigates the iframe to a PDF file, triggering
MimeHandlerViewcreation. - Browser State Change: The browser intercepts the request, the renderer loads the placeholder, and signals
ReadyToCreateMimeHandlerView. The browser callsPrepareForInnerDelegateAttach, settingattach_to_inner_delegate_state_toPREPARE_FRAME. - Dispatch BeforeUnload: Because of the
beforeunloadhandler, the browser dispatches aBeforeUnloadIPC to the renderer and starts a 1-second timeout timer. The browser’sis_waiting_for_unload_ack_state remainsfalse. - Spoof IPC: The compromised renderer intentionally ignores the
BeforeUnloadIPC. Instead, it sends a spoofedDidUnloadRenderFrameIPC for the iframe. - Premature Deletion: The browser receives the spoofed IPC and calls
RenderFrameHostImpl::OnUnloadACK. The logic flaw bypasses theis_waiting_for_unload_ack_check, directly executingRenderFrameDeleted(). - Observer State Freed:
RenderFrameDeleted()notifies allWebContentsObservers (e.g., Extensions, Autofill), which proceed to free memory and state mappings associated with thisRenderFrameHost*. - Use-After-Free: The frame remains in the
FrameTree(thebeforeunloadtimeout is still running). When a downstream browser component iterates the frame tree (e.g.,WebContentsImpl::ForEachRenderFrameHost), it yields this zombieRenderFrameHost*. Observers accessing their now-freed state mappings for this pointer will trigger a Use-After-Free (Sandbox Escape).
Suggested Fix
The is_waiting_for_unload_ack_ check should be moved up in RenderFrameHostImpl::OnUnloadACK so that spurious Unload ACKs are rejected before evaluating the inner delegate attachment state.
void RenderFrameHostImpl::OnUnloadACK() {
// ... (existing early returns)
// Ignore spurious unload ack MUST happen first.
if (!is_waiting_for_unload_ack_) {
return;
}
RenderFrameHostOwner* owner =
IsPendingDeletion() ? GetFrameTreeNodeForUnload() : owner_;
if (!is_main_frame() &&
owner->GetRenderFrameHostManager().is_attaching_inner_delegate()) {
RenderFrameDeleted();
return;
}
// ...
}
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
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. Please feel free to reach out to me if you have concerns or feedback.