CVE-2026-78903
Overview
Background
- Inner delegate / placeholder frame
- When an inner
WebContentsor GuestView (e.g. a MimeHandlerView guest, as the regression test simulates) is attached into a page, an existing subframe’sRenderFrameHostImplbecomes a placeholder whoseinner_tree_main_frame_tree_node_id_points at the inner frame tree’s main frame. - `SwapOuterDelegateFrame()`
- The
RenderFrameHostImplmethod that runs at attach time, swaps the placeholder for a proxy and sends the renderer an Unload IPC for the placeholder’s local frame. - `OnUnloadACK()` / `DidUnloadRenderFrame`
- The renderer’s acknowledgement that the unload finished; before this patch, the placeholder’s child-frame cleanup (
ResetChildren()) ran only here, so it depended on the renderer replying. - `FrameTree::NodeIterator`
- Iterates the frame tree and, for a placeholder, treats the inner tree’s main frame as the only child, so any local
children_the placeholder still holds are skipped during cross-tree iteration.
Root Cause Analysis
Before the patch, RenderFrameHostImpl::SwapOuterDelegateFrame() set up the inner-delegate attachment and sent an Unload IPC, but it left the placeholder’s existing child frames in place until OnUnloadACK() called ResetChildren(). That only happens after the renderer sends DidUnloadRenderFrame. Until then, which is indefinitely if a misbehaving renderer withholds the ACK, the placeholder could hold a non-null inner_tree_main_frame_tree_node_id_ and a non-empty children_ at the same time. The commit message says FrameTree::NodeIterator does not handle that state: those children are skipped by cross-tree iteration. In the same window, OnCreateChildFrame() would still accept new children for the placeholder because its render frame was still considered live.
The fix moves the cleanup into the browser. SwapOuterDelegateFrame() now calls ResetChildren() before sending the Unload IPC, and OnCreateChildFrame() returns early, dropping the request, when inner_tree_main_frame_tree_node_id_ is set.
As a result, the invariant “a delegate placeholder has no local children” no longer depends on the renderer.
Attack Path
- Child frame under the future placeholder A subframe that will become the inner-delegate placeholder gets a child iframe. The regression test does this with script and notes that “a compromised renderer could create this child.”
- Inner delegate attaches
The embedder attaches an inner
WebContents/GuestView to that subframe (e.g. MimeHandlerView), which callsSwapOuterDelegateFrame()and sends Unload. Whether the attacker can trigger this depends on embedder features such as PDF/MimeHandlerView or guest views. - Renderer withholds the unload ACK
A compromised renderer never sends
DidUnloadRenderFrame, so before the fixResetChildren()inOnUnloadACK()never runs and the stale children remain. It can also sendOnCreateChildFramemessages to add new children to the placeholder. - Inconsistent frame tree
The placeholder now has both an inner-tree main frame and hidden local children. Browser-side code that walks frames via
FrameTree::NodeIteratorskips those children, so per-frame bookkeeping (e.g. site-isolation, lifecycle or cleanup logic) can miss frames that are still live (inferred).
Impact Assessment
OnCreateChildFrame IPCs, plus an embedder flow that attaches an inner delegate (GuestView, MimeHandlerView, or the fenced frame mentioned in the code comment). The advisory classifies it as “Incomplete cleanup in SiteIsolation”. The likely consequence is that live child frames escape browser-side frame-tree iteration, which could weaken site-isolation or lifecycle enforcement or lead to stale-object bugs (inferred). The patch does not show a specific memory-safety sink. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 3.1.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_frame_host_impl.cc |
modified | |
BindRepeatingcontent/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/security_exploit_browsertest.cc
Audit Directions
- Browser invariants gated on renderer ACKsLook for browser-side cleanup that runs only in handlers for renderer replies (unload/detach/swap ACKs). A compromised renderer can withhold these indefinitely, so security-relevant state should be fixed up synchronously in the browser.
- Renderer IPCs accepted in transitional statesCheck whether
OnCreateChildFrame, navigation, and other frame-creating IPCs are rejected when the target RFH is a placeholder, pending deletion, or otherwise in a special state, not only whenIsRenderFrameLive()is false. - Iterators with special-cased topologyReview
FrameTree::NodeIteratorand similar walkers that substitute or skip nodes (inner trees, fenced frames, portals/guests) for states where “hidden” nodes are still live and so escape policy checks or teardown.
Patch
From ccc0ccf66880d0306d268da01f2d88a1f0aa1f00 Mon Sep 17 00:00:00 2001
From: Alex Moshchuk <alexmos@chromium.org>
Date: Fri, 17 Jul 2026 09:12:18 -0700
Subject: [PATCH] Clear inner-delegate placeholder children at attach time
When attaching an inner WebContents/GuestView to a placeholder
RenderFrameHost, SwapOuterDelegateFrame() sends an Unload IPC but defers
the placeholder's child-frame cleanup to OnUnloadACK(), which only runs
once the renderer sends DidUnloadRenderFrame. Until then the placeholder
can hold both inner_tree_main_frame_tree_node_id_ and non-empty
children_, which FrameTree::NodeIterator does not handle (the children
are skipped by cross-tree iteration).
Make the cleanup browser-side:
- SwapOuterDelegateFrame() now calls ResetChildren() before
sending the Unload IPC, so existing children are cleared at
attach time without waiting for the renderer.
- OnCreateChildFrame() now drops the message if
inner_tree_main_frame_tree_node_id_ is set, so the placeholder
cannot acquire new children once it has become a delegate node
for an inner frame tree.
The existing ResetChildren() in OnUnloadACK() is retained as a no-op
safety net.
This is a followup to a similar issue in crbug.com/517241992.
Most of this CL has been adapted from the AI-generated fix at
https://crbug.com/518078552#comment4.
Bug: 518078552, 517241992
Change-Id: I66871ede1d5b8312c68393d09d7ff4ac2e6c0d37
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8112019
Commit-Queue: Alex Moshchuk <alexmos@chromium.org>
Reviewed-by: Kevin McNee <mcnee@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1663944}
---
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 27e1428..cf8354a 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -5188,6 +5188,16 @@
}
}
+ // The placeholder frame for an inner frame tree (e.g., an attached
+ // GuestView, fenced frame) must never have its own local children, since
+ // the inner tree's main frame is treated as this frame's only child during
+ // FrameTree iteration. This message could only have been sent before the
+ // renderer processed the corresponding Unload IPC, so just drop it. See
+ // https://crbug.com/518078552.
+ if (inner_tree_main_frame_tree_node_id_) {
+ return;
+ }
+
// `new_routing_id`, `frame_token`, `devtools_frame_token` and
// `document_token` were generated on the browser's IO thread and not taken
// from the renderer process.
@@ -6879,6 +6889,12 @@
void RenderFrameHostImpl::SwapOuterDelegateFrame(
RenderFrameProxyHost* proxy,
const base::UnguessableToken& devtools_frame_token) {
+ // The placeholder frame for an inner frame tree must never have its own
+ // local children. Clear them here, before sending the Unload IPC, so that
+ // the cleanup does not depend on the renderer sending a DidUnloadRenderFrame
+ // ACK. See https://crbug.com/518078552.
+ ResetChildren();
+
// Note: At this point the placeholder iframe for embedding the guest has
// been initialized with a devtools_frame_token that is different from the
// guest's main frame (that is about to be attached to it). When we swap
@@ -7182,7 +7198,8 @@
// stay around but it will no longer be associated with a RenderFrame.
// Ensure there are no lingering child frames before marking the frame
// deleted - this matters for compromised renderers (see
- // https://crbug.com/517241992).
+ // https://crbug.com/517241992). This is a defense-in-depth call that
+ // mirrors a similar call in SwapOuterDelegateFrame().
ResetChildren();
RenderFrameDeleted();
return;
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index 7a4372a9..1cafdb7 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -80,6 +80,8 @@
#include "device/gamepad/public/mojom/gamepad.mojom.h"
#include "ipc/constants.mojom.h"
#include "mojo/core/embedder/embedder.h"
+#include "mojo/public/cpp/bindings/associated_receiver.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
@@ -2963,6 +2965,82 @@
EXPECT_EQ(0U, subframe->child_count());
}
+// Test that the placeholder frame for an inner delegate has its children
+// cleared synchronously at attach time, without depending on the renderer's
+// DidUnloadRenderFrame ACK (which a misbehaving renderer may withhold), and
+// that the placeholder cannot acquire new children after the inner delegate is
+// attached. See https://crbug.com/518078552.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+ NoChildrenOnInnerDelegatePlaceholderAfterAttach) {
+ // 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 child frame to the subframe so that we can test if it gets properly
+ // detached. A compromised renderer could create this child.
+ EXPECT_TRUE(ExecJs(subframe,
+ "let f = document.createElement('iframe'); "
+ "document.body.appendChild(f);"));
+ ASSERT_EQ(1U, subframe->child_count());
+ RenderFrameHostImpl* grandchild = subframe->child_at(0)->current_frame_host();
+ RenderFrameHostWrapper grandchild_observer(grandchild);
+
+ // Swallow the unload ACK so that any cleanup observed below is solely due to
+ // browser-side attach logic and not the renderer's DidUnloadRenderFrame ACK.
+ subframe->SetUnloadACKCallbackForTesting(
+ base::BindRepeating([]() { return true; }));
+
+ // Attach an inner delegate. This synchronously calls SwapOuterDelegateFrame()
+ // which sends the Unload IPC, and it sets is_inner_delegate_attached().
+ EXPECT_TRUE(CreateAndAttachInnerContents(subframe));
+ EXPECT_TRUE(subframe->frame_tree_node()
+ ->render_manager()
+ ->is_inner_delegate_attached());
+ EXPECT_TRUE(subframe->inner_tree_main_frame_tree_node_id());
+
+ // The placeholder frame must have no children immediately after attach,
+ // independently of whether the renderer ACKs the unload.
+ EXPECT_TRUE(grandchild_observer.IsRenderFrameDeleted());
+ EXPECT_EQ(0U, subframe->child_count());
+
+ // Simulate the renderer attempting to create a new child on the placeholder
+ // frame after the inner delegate has been attached. The placeholder's render
+ // frame is still considered created in the browser (since the unload ACK was
+ // swallowed above), so OnCreateChildFrame()'s lifecycle checks alone would
+ // not reject this; the dedicated inner-delegate check should drop it.
+ ASSERT_TRUE(subframe->IsRenderFrameLive());
+ mojo::AssociatedRemote<mojom::Frame> frame_remote;
+ std::ignore = frame_remote.BindNewEndpointAndPassDedicatedReceiver();
+ mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker> bib_remote;
+ mojo::AssociatedReceiver<blink::mojom::AssociatedInterfaceProvider>
+ associated_interface_provider_receiver(nullptr);
+ std::ignore = associated_interface_provider_receiver
+ .BindNewEndpointAndPassDedicatedRemote();
+ subframe->OnCreateChildFrame(
+ subframe->GetProcess()->GetNextRoutingID(), frame_remote.Unbind(),
+ bib_remote.InitWithNewPipeAndPassReceiver(),
+ blink::mojom::PolicyContainerBindParams::New(
+ mojo::PendingAssociatedRemote<blink::mojom::PolicyContainerHost>()
+ .InitWithNewEndpointAndPassReceiver()),
+ associated_interface_provider_receiver.Unbind(),
+ blink::mojom::TreeScopeType::kDocument, "", "uniqueName1",
+ /*is_created_by_script=*/false, blink::LocalFrameToken(),
+ base::UnguessableToken::Create(), blink::DocumentToken(),
+ blink::FramePolicy(), blink::mojom::FrameOwnerProperties(),
+ blink::FrameOwnerElementType::kIframe, ukm::kInvalidSourceId);
+
+ // The placeholder frame must still have no children.
+ EXPECT_EQ(0U, subframe->child_count());
+}
+
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 7a4372a9..1cafdb7 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -80,6 +80,8 @@
#include "device/gamepad/public/mojom/gamepad.mojom.h"
#include "ipc/constants.mojom.h"
#include "mojo/core/embedder/embedder.h"
+#include "mojo/public/cpp/bindings/associated_receiver.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
#include "mojo/public/cpp/bindings/pending_associated_remote.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
@@ -2963,6 +2965,82 @@
EXPECT_EQ(0U, subframe->child_count());
}
+// Test that the placeholder frame for an inner delegate has its children
+// cleared synchronously at attach time, without depending on the renderer's
+// DidUnloadRenderFrame ACK (which a misbehaving renderer may withhold), and
+// that the placeholder cannot acquire new children after the inner delegate is
+// attached. See https://crbug.com/518078552.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+ NoChildrenOnInnerDelegatePlaceholderAfterAttach) {
+ // 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 child frame to the subframe so that we can test if it gets properly
+ // detached. A compromised renderer could create this child.
+ EXPECT_TRUE(ExecJs(subframe,
+ "let f = document.createElement('iframe'); "
+ "document.body.appendChild(f);"));
+ ASSERT_EQ(1U, subframe->child_count());
+ RenderFrameHostImpl* grandchild = subframe->child_at(0)->current_frame_host();
+ RenderFrameHostWrapper grandchild_observer(grandchild);
+
+ // Swallow the unload ACK so that any cleanup observed below is solely due to
+ // browser-side attach logic and not the renderer's DidUnloadRenderFrame ACK.
+ subframe->SetUnloadACKCallbackForTesting(
+ base::BindRepeating([]() { return true; }));
+
+ // Attach an inner delegate. This synchronously calls SwapOuterDelegateFrame()
+ // which sends the Unload IPC, and it sets is_inner_delegate_attached().
+ EXPECT_TRUE(CreateAndAttachInnerContents(subframe));
+ EXPECT_TRUE(subframe->frame_tree_node()
+ ->render_manager()
+ ->is_inner_delegate_attached());
+ EXPECT_TRUE(subframe->inner_tree_main_frame_tree_node_id());
+
+ // The placeholder frame must have no children immediately after attach,
+ // independently of whether the renderer ACKs the unload.
+ EXPECT_TRUE(grandchild_observer.IsRenderFrameDeleted());
+ EXPECT_EQ(0U, subframe->child_count());
+
+ // Simulate the renderer attempting to create a new child on the placeholder
+ // frame after the inner delegate has been attached. The placeholder's render
+ // frame is still considered created in the browser (since the unload ACK was
+ // swallowed above), so OnCreateChildFrame()'s lifecycle checks alone would
+ // not reject this; the dedicated inner-delegate check should drop it.
+ ASSERT_TRUE(subframe->IsRenderFrameLive());
+ mojo::AssociatedRemote<mojom::Frame> frame_remote;
+ std::ignore = frame_remote.BindNewEndpointAndPassDedicatedReceiver();
+ mojo::PendingRemote<blink::mojom::BrowserInterfaceBroker> bib_remote;
+ mojo::AssociatedReceiver<blink::mojom::AssociatedInterfaceProvider>
+ associated_interface_provider_receiver(nullptr);
+ std::ignore = associated_interface_provider_receiver
+ .BindNewEndpointAndPassDedicatedRemote();
+ subframe->OnCreateChildFrame(
+ subframe->GetProcess()->GetNextRoutingID(), frame_remote.Unbind(),
+ bib_remote.InitWithNewPipeAndPassReceiver(),
+ blink::mojom::PolicyContainerBindParams::New(
+ mojo::PendingAssociatedRemote<blink::mojom::PolicyContainerHost>()
+ .InitWithNewEndpointAndPassReceiver()),
+ associated_interface_provider_receiver.Unbind(),
+ blink::mojom::TreeScopeType::kDocument, "", "uniqueName1",
+ /*is_created_by_script=*/false, blink::LocalFrameToken(),
+ base::UnguessableToken::Create(), blink::DocumentToken(),
+ blink::FramePolicy(), blink::mojom::FrameOwnerProperties(),
+ blink::FrameOwnerElementType::kIframe, ukm::kInvalidSourceId);
+
+ // The placeholder frame must still have no children.
+ EXPECT_EQ(0U, subframe->child_count());
+}
+
class BeginNavigationTransitionReplacer : public FrameHostInterceptor {
public:
BeginNavigationTransitionReplacer(WebContents* web_contents,
Original Bug Report
FrameTree invariant bypass in SwapOuterDelegateFrame allows creation of hidden subtrees
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential logic issue in the outer delegate frame swapping mechanism allows a compromised renderer to bypass the child frame cleanup mechanism. By withholding the Unload ACK, a placeholder frame’s Mojo endpoints remain active and bound while the frame is in an inconsistent state. An attacker can then spawn child frames under this placeholder, creating a hidden subtree that is skipped during standard frame tree walks.
Affected files:
content/browser/renderer_host/render_frame_host_impl.cccontent/browser/web_contents/web_contents_impl.cccontent/browser/renderer_host/agent_scheduling_group_host.cccontent/browser/renderer_host/frame_tree.cc
Estimated timestamp from git blame: 2019-02-26
Root Cause Analysis
The cleanup logic designed to prevent lingering child frames during delegate attachment is normally triggered upon receiving the unload acknowledgement. Specifically, ResetChildren() is invoked within the inner-delegate branch of RenderFrameHostImpl::OnUnloadACK():
// content/browser/renderer_host/render_frame_host_impl.cc
RenderFrameHostOwner* owner =
IsPendingDeletion() ? GetFrameTreeNodeForUnload() : owner_;
if (!is_main_frame() &&
owner->GetRenderFrameHostManager().is_inner_delegate_attached()) {
ResetChildren();
RenderFrameDeleted();
return;
}
However, when a placeholder frame is swapped via RenderFrameHostImpl::SwapOuterDelegateFrame(), the browser sends an unload request to the renderer but does not configure the standard browser-side bookkeeping to enforce a timeout or transition the frame state:
// content/browser/renderer_host/render_frame_host_impl.cc
void RenderFrameHostImpl::SwapOuterDelegateFrame(...) {
...
GetMojomFrameInRenderer()->Unload(/*is_loading=*/false, ...); // Fire-and-forget
// Does NOT set is_waiting_for_unload_ack_ = true
// Does NOT start unload_event_monitor_timeout_
// Does NOT set the lifecycle state to kRunningUnloadHandlers
...
}
Because the unload ACK is expected exclusively via the renderer-controlled IPC DidUnloadRenderFrame without any expected-state or timeout enforcement, a compromised renderer can simply withhold the ACK indefinitely. This leaves the placeholder RenderFrameHost with active Mojo endpoints bound while its state remains LifecycleStateImpl::kActive and RenderFrameState::kCreated.
Potential Trigger Path
Note: The following steps are potential/suggested steps to trigger this behavior, as our current evaluation is based on static analysis of the codebase.
- A compromised renderer creates a placeholder iframe (e.g.,
<embed type="application/pdf" src="...">during MimeHandlerView flow), triggeringPrepareForInnerWebContentsAttach->AttachToOuterWebContentsFrame->AttachInnerWebContentsImplin the browser. AttachInnerWebContentsImplsetsinner_tree_main_frame_tree_node_id_on the placeholder frame, callsSwapOuterDelegateFrame(which sends theUnloadIPC to the renderer without setting up a timeout or changing the lifecycle state), and completes attachment.- The compromised renderer intentionally ignores the
UnloadIPC and never sendsAgentSchedulingGroupHost::DidUnloadRenderFrame. - Since the Mojo interfaces (
mojom::FrameHostandblink::mojom::LocalFrameHost) on the placeholder frame remain bound, the compromised renderer callsCreateChildFrameon the placeholder’s pipe. - In the browser,
RenderFrameHostImpl::OnCreateChildFramechecksis_render_frame_created()(which is stillkCreated) andIsInactiveAndDisallowActivation(which returnsfalsebecauselifecycle_state_remainskActive). The browser accepts the request and adds a child frame (gc1) to the placeholder’s children list.
Resulting Inconsistent State and Impact
This sequence puts the browser into a state where:
placeholder->lifecycle_state_ == kActiveplaceholder->children_is non-empty (containinggc1).is_inner_delegate_attached()evaluates to true.
This violates the core design invariant:
child_count() > 0 => inner_tree_main_frame_tree_node_id().is_null()
(which is explicitly asserted in test environments, such as in content/browser/renderer_host/scroll_into_view_browsertest.cc:460-462).
Security Consequences:
Standard frame walks (such as ForEachRenderFrameHost and NodesIncludingInnerTreeNodes) walk the tree using include_delegate_nodes_for_inner_frame_trees = false. When the FrameTree::NodeIterator encounters the placeholder frame node, it skips the placeholder entirely and descends directly into the inner tree’s main frame:
// content/browser/renderer_host/frame_tree.cc:95-98
if (should_descend_into_inner_trees_ && inner_tree_main_ftn) {
if (include_delegate_nodes_for_inner_frame_trees_)
queue_.push_back(child); // Skipped
queue_.push_back(inner_tree_main_ftn);
}
Because the placeholder node is never queued, its children (the newly created frame gc1) are never visited. This creates a hidden active frame subtree that is invisible to security policy enforcements, origin checks, and lifecycle audits, yet remains fully active with live, renderer-controlled Mojo connections.
Suggested Remediation
To prevent the creation of children under placeholder frames, the browser should ensure that once SwapOuterDelegateFrame is invoked, the placeholder frame is immediately restricted from creating child frames or processing further state-modifying IPCs.
- Disable Mojo Receivers Early: In
SwapOuterDelegateFrame, immediately close or unbind the frame-creation and frame-navigation related Mojo receivers (such aslocal_frame_host_receiver_), rather than waiting for the asynchronous unload ACK. - Explicit Validation: Modify
RenderFrameHostImpl::OnCreateChildFrameto explicitly reject child frame creation if an inner delegate is attached or in the process of being attached (e.g.,is_inner_delegate_attached()is true oris_attaching_inner_delegate()is true).
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.