← Chrome — 2026-W39

c5c4b77965e0ec76a7f385e0107d56556bb632cb  Fix missing WebContentsImpl DetachDelegate

severity low class UAF CVSS 7.0 signal score 5.3 content/browser/ AI review CONFIRMED
Structural signals: smartptr_member_added (+3.0), area:content/browser/ (+1.0), surgical (+1.3)

The bug at a glance

The commit message states that RenderWidgetHostImpl::DestroyOrDefer could trigger a UAF on delegate_ after WebContentsImpl was destroyed, because the main frame’s widget was never detached from its delegate. That is a browser-process use-after-free. It is reached on a teardown path the attacker does not obviously control, which tempers the rating, but browser-process UAFs are rated high by default.

A dangling WebContentsImpl* inside the main-frame RenderWidgetHostImpl means teardown code reads freed browser-process memory. If a page can shape timing so the widget outlives its WebContents, this is a sandbox-escape-class primitive.

Root cause

The WebContentsImpl destructor called DetachDelegate() only on widgets in created_widgets_ (popups and similar), not on the primary main frame’s RenderWidgetHost. The main-frame RWHI therefore kept a raw delegate_ pointing at the destroyed WebContentsImpl. Later FrameTree destruction ran RenderWidgetHostViewBase teardown via DestroyOrDefer, which used host()->delegate()->GetInputEventRouter() and so dereferenced the freed delegate. The fix detaches the delegate of the main-frame RWH in the destructor and changes the view code to call host()->GetInputEventRouter() instead of going through delegate() directly.

Key code

Main-frame widget is detached from the dying WebContentsImpl; the view no longer dereferences delegate() directly

+  if (GetPrimaryMainFrame() && GetPrimaryMainFrame()->GetRenderWidgetHost()) {
+    GetPrimaryMainFrame()->GetRenderWidgetHost()->DetachDelegate();
+  }
...
-  if (is_frame_sink_id_owner_ && host() && host()->delegate() &&
-      host()->delegate()->GetInputEventRouter()) {
+  if (is_frame_sink_id_owner_ && host() && host()->GetInputEventRouter()) {

Patch walkthrough

  • content/browser/web_contents/web_contents_impl.cc — In ~WebContentsImpl, after clearing created_widgets_, calls DetachDelegate() on the primary main frame’s RenderWidgetHost so its delegate_ no longer points at the WebContentsImpl being freed. This is the core lifetime fix.
  • content/browser/renderer_host/render_widget_host_view_base.cc — Destroy and the frame-sink-ID-owner registration path now go through host()->GetInputEventRouter() instead of host()->delegate()->GetInputEventRouter(). This removes the direct delegate dereference that hit freed memory. The RWHI accessor presumably null-checks delegate_ (inferred; not shown in the diff).
  • content/browser/web_contents/web_contents_impl_unittest.cc — Regression test DestroyWebContentsWithFrameSinkIdOwnerView marks the main view as a frame-sink-ID owner and destroys the WebContents. Under ASan it would catch the dangling-delegate access in DestroyOrDefer.

Background

RenderWidgetHostImpl::delegate_ — Raw pointer from a widget host back to its owning RenderWidgetHostDelegate, normally the WebContentsImpl.

DetachDelegate — Clears a widget’s delegate pointer so it stops calling into a WebContents that is going away.

RenderWidgetInputEventRouter / frame sink ID owner — Page-level router that maps FrameSinkIds to views for hit testing; views register and unregister with it during creation and destruction.

Vulnerability window

  1. Reachability — Browser process, during WebContents (tab) destruction in a stock build. The commit says pre-renderer teardown differs from normal tab destruction, which suggests specific teardown orderings expose it (inferred).
  2. Trigger — WebContentsImpl is destroyed while its main-frame RenderWidgetHost still exists. Later FrameTree teardown calls DestroyOrDefer, and the view dereferences host()->delegate().
  3. Primitive — A virtual call (GetInputEventRouter) through a freed WebContentsImpl in the browser process (inferred from the delegate dereference).
  4. Escalation — If an attacker could reclaim the freed WebContentsImpl allocation before teardown, the virtual call could be redirected. This is browser-process code execution, a sandbox escape (inferred; no such grooming is shown).

Proof of concept

Conceptually: arrange for a tab’s WebContents to be destroyed while its main-frame widget is still alive and its view is a frame-sink-ID owner. View teardown then reaches through the stale delegate pointer. The unit test reproduces this by destroying a test WebContents. No exploit code is provided.

Exploitation

  1. Teardown race — Cause or time tab closure (for example window.close or navigation-driven destruction) so the main widget outlives WebContentsImpl (inferred).
  2. Reclaim — Refill the freed WebContentsImpl slot before the delegate dereference. This is hard to control from web content (inferred).
  3. Control — A hijacked virtual dispatch in the browser process would escape the sandbox (inferred).

Detection & hunting

For defenders and detection engineers:

  • weggli ‘{ $h->delegate()->$m(); }’ content/browser/renderer_host/ — Find widget and view code that dereferences a host’s delegate during destruction paths without a liveness guarantee.
  • ASan heap-use-after-free in RenderWidgetHostViewBase::Destroy / DestroyOrDefer with free stack in ~WebContentsImpl — Crash signatures with this pair point to this bug or a variant.

Audit directions

  • WebContentsImpl destructor vs. all RenderWidgetHosts it delegates for (main frame, OOPIF widgets, inner WebContents, prerender/fenced frames) — Check that every widget whose delegate_ is this WebContentsImpl is detached, not only those tracked in created_widgets_.
  • FrameTree::Shutdown and prerender teardown — The commit notes prerender teardown differs. Check that prerendered or MPArch frame trees do not leave widgets pointing at a destroyed delegate.

Before / after

Loading diff…