← Chrome — 2026-W38

8068cdd59a8d71f791b56930f7dda8123f656a5b  [M150-LTS] Fix UAF in WebContentsImpl when dropping fullscreen for dialogs

severity high class UAF CVSS 8.0 confidence 0.82 signal score 4.0 content/browser/web_contents patch-now AI review CONFIRMED
Primitive: ForSecurityDropFullscreen() runs observers that can destroy the initiating RenderFrameHostImpl; the raw pointer is then dereferenced -> browser-process heap-UAF.
Triage note: Reachable via JS dialog / beforeunload / file chooser while fullscreen with an iframe removed during the drop; browser process. Bug:552665794. Backport M144-LTS.

The bug at a glance

This is a browser-process heap use-after-free reachable from ordinary web content, which is the highest-value class of renderer-to-browser escape primitive. The commit message and the added browsertests both confirm the freed object is a RenderFrameHostImpl (an object rich in pointers, mojo endpoints, and process/security state) and that its destruction is driven by attacker-controlled DOM manipulation (removing an iframe during the fullscreen drop). The trigger requires no compromised renderer and no special permissions: any page that enters HTML fullscreen and then initiates a JavaScript dialog, beforeunload confirm, file chooser, or view-source can reach the vulnerable code path, and a same-tab or opener-related frame can be detached synchronously while ForSecurityDropFullscreen() dispatches its exit events. A dangling RenderFrameHostImpl* in the browser process is a strong exploitation primitive (controlled reallocation of a freed C++ object with virtual dispatch and mojo state), consistent with a CVSS of 8.0 (AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H, network-adjacent to remote, low complexity, requiring the minimal user interaction of viewing a page that shows a dialog). It falls short of critical only because the free is bounded by timing/UI interaction and reliable exploitation requires heap grooming rather than a deterministic overwrite.

The interesting angle is that the dangling pointer is not created by the dialog logic itself but by a security hardening measure: ForSecurityDropFullscreen() is called precisely to protect the user (a page in fullscreen showing a dialog could spoof native UI), yet the act of leaving fullscreen synchronously runs WebContents observers and dispatches fullscreenchange/resize-style events into the page, which can re-enter script and detach the very frame that requested the dialog. The initiating RenderFrameHostImpl* is captured as a raw pointer on the stack before the drop and dereferenced after it returns. The defensive call thus opens a re-entrancy window that invalidates the caller’s own state – a classic ’the cleanup ran your destructor’ bug. A second subtlety: the four affected entry points had subtly different guards. The dialog paths additionally re-check IsActive() (not just liveness), because a frame can survive the drop but be moved to a non-active state (e.g. bfcache/pending deletion) in which continuing the dialog is unsafe, and RunFileChooser had to change its parameter type from the base RenderFrameHost* to the concrete RenderFrameHostImpl* just to obtain a GetWeakPtr(), revealing that the type erasure at the public boundary was hiding the ability to track liveness at all.

Root cause

All four methods share a pattern: capture the initiating frame as a raw pointer, call ForSecurityDropFullscreen(display::kInvalidDisplayId) to exit HTML fullscreen for the fullscreen-related set of WebContents, then continue using the frame pointer. ForSecurityDropFullscreen() is not a pure state flip – exiting fullscreen notifies WebContentsObservers and the WebContentsDelegate (delegate_->ExitFullscreenModeForTab), and dispatches the corresponding events into the renderer, which the page can observe and respond to synchronously via JavaScript. If that script removes the DOM node hosting the initiating subframe (document.querySelector(‘iframe’).remove()), the corresponding RenderFrameHostImpl is torn down before ForSecurityDropFullscreen() returns. Control then returns into WebContentsImpl, which dereferences the now-freed frame pointer: in ViewSource it called frame->ComputeIsolationInfoForNavigation(…); in RunJavaScriptDialog / RunBeforeUnloadConfirm it proceeded to run the dialog against the dead frame; in RunFileChooser it passed render_frame_host on to delegate_->RunFileChooser(…). The fix captures the frame in a base::WeakPtr BEFORE the ForSecurityDropFullscreen() call at each site: ViewSource takes weak_frame = frame->GetWeakPtr() and returns early if !weak_frame after the drop, then uses weak_frame->ComputeIsolationInfoForNavigation(…); the two dialog sites take weak_rfh = render_frame_host->GetWeakPtr() and change the post-drop guard from if (!blocker) to if (!blocker || !weak_rfh || !weak_rfh->IsActive()), aborting when the frame was freed OR is no longer active; RunFileChooser does the same but null-guarded because render_frame_host may legitimately be null there (weak_rfh = render_frame_host ? render_frame_host->GetWeakPtr() : nullptr, guard if (!blocker || (render_frame_host && (!weak_rfh || !weak_rfh->IsActive())))), and it forwards weak_rfh.get() rather than the raw pointer to delegate_->RunFileChooser. To obtain GetWeakPtr(), RunFileChooser’s signature (in both the .cc and the .h) was narrowed from RenderFrameHost* to RenderFrameHostImpl*. The WeakPtr invalidates on the frame’s destruction, so after the re-entrant teardown the guard sees a null WeakPtr and returns before any dereference, closing the UAF.

Key code

Before: raw frame pointer dereferenced after the fullscreen drop can re-enter script; After: track via WeakPtr and abort if the frame died or went inactive (RunJavaScriptDialog shown; the other three sites mirror this).

// BEFORE (RunJavaScriptDialog / RunBeforeUnloadConfirm)
  // Running a dialog causes an exit to webpage-initiated fullscreen.
  auto blocker =
      ForSecurityDropFullscreen(/*display_id=*/display::kInvalidDisplayId);
  if (!blocker) {
    return;
  }
  // ... render_frame_host used here -> UAF if drop detached the frame

// AFTER
  base::WeakPtr<RenderFrameHostImpl> weak_rfh = render_frame_host->GetWeakPtr();
  auto blocker =
      ForSecurityDropFullscreen(/*display_id=*/display::kInvalidDisplayId);
  if (!blocker || !weak_rfh || !weak_rfh->IsActive()) {
    return;
  }

// AFTER (ViewSource)
  base::WeakPtr<RenderFrameHostImpl> weak_frame = frame->GetWeakPtr();
  if (!ForSecurityDropFullscreen(display::kInvalidDisplayId)) {
    return;
  }
  if (!weak_frame) {
    return;
  }
  navigation_entry->set_isolation_info(
      weak_frame->ComputeIsolationInfoForNavigation(navigation_entry->GetURL()));

// AFTER (RunFileChooser: null-safe, forwards weak_rfh.get())
  base::WeakPtr<RenderFrameHostImpl> weak_rfh =
      render_frame_host ? render_frame_host->GetWeakPtr() : nullptr;
  auto blocker =
      ForSecurityDropFullscreen(/*display_id=*/display::kInvalidDisplayId);
  if (!blocker || (render_frame_host && (!weak_rfh || !weak_rfh->IsActive()))) {
    return;
  }
  delegate_->RunFileChooser(weak_rfh.get(), std::move(listener), params);

Patch walkthrough

  • content/browser/web_contents/web_contents_impl.cc (WebContentsImpl::ViewSource, ~line 8425) — Captures weak_frame = frame->GetWeakPtr() before ForSecurityDropFullscreen(), adds if (!weak_frame) return; after the drop, and replaces the raw frame->ComputeIsolationInfoForNavigation(…) call at ~line 8460 with weak_frame->ComputeIsolationInfoForNavigation(…). This is the site with a directly provable dereference of the freed pointer in the pre-patch code.
  • content/browser/web_contents/web_contents_impl.cc (WebContentsImpl::RunJavaScriptDialog, ~line 9265) — Adds weak_rfh = render_frame_host->GetWeakPtr() before the ForSecurityDropFullscreen() blocker call and strengthens the guard from if (!blocker) to if (!blocker || !weak_rfh || !weak_rfh->IsActive()), so a dialog is not run against a frame destroyed or made inactive by the fullscreen exit.
  • content/browser/web_contents/web_contents_impl.cc (WebContentsImpl::RunBeforeUnloadConfirm, ~line 9402) — Identical treatment to RunJavaScriptDialog: same weak_rfh capture and the same combined liveness+active guard. beforeunload confirms are a natural fullscreen-drop trigger, so this path is independently reachable.
  • content/browser/web_contents/web_contents_impl.cc (WebContentsImpl::RunFileChooser, ~line 9486/9510) — Changes the render_frame_host parameter type from RenderFrameHost* to RenderFrameHostImpl* to gain access to GetWeakPtr(); builds weak_rfh conditionally because the pointer may be null here; uses the null-aware guard if (!blocker || (render_frame_host && (!weak_rfh || !weak_rfh->IsActive()))); and passes weak_rfh.get() (revalidated) to delegate_->RunFileChooser instead of the stale raw pointer.
  • content/browser/web_contents/web_contents_impl.h (~line 1491) — Updates the RunFileChooser declaration’s second parameter from RenderFrameHost* to RenderFrameHostImpl* to match the .cc change, keeping the public interface consistent and enabling WeakPtr tracking of the concrete impl type.
  • content/browser/web_contents/web_contents_impl_browsertest.cc (~line 8831) — Adds DetachFrameOnFullscreenExitDelegate, a WebContentsDelegate that, on ExitFullscreenModeForTab, executes JS to remove the target iframe, then two IN_PROC_BROWSER_TEST_F cases (RunJavaScriptDialogFrameDetachOnFullscreenExit and RunBeforeUnloadConfirmFrameDetachOnFullscreenExit) that reproduce the detach-during-drop race and assert the child RFH’s WeakPtr becomes null without a crash – a regression test that fails (UAF) without the fix.

Background

RenderFrameHostImpl (RFHI) — The browser-process object representing a single frame (main frame or subframe) in a renderer. It owns mojo endpoints, security/isolation state, and navigation machinery. Destroying it (e.g. when its DOM iframe is removed) frees a large heap object with virtual methods, making a dangling RFHI* a high-value UAF primitive.

ForSecurityDropFullscreen() — A WebContentsImpl helper that exits HTML fullscreen across the fullscreen-related set of WebContents to prevent UI spoofing when a dialog/chooser/new-window appears. It returns a scoped ‘blocker’ (fullscreen block); crucially it synchronously notifies observers and the delegate (ExitFullscreenModeForTab) and dispatches fullscreen-exit events into renderers, which can re-enter attacker script.

base::WeakPtr / GetWeakPtr() — Chromium’s non-owning smart pointer that auto-nulls when the pointee is destroyed. Checking a WeakPtr for truthiness after a re-entrant call is the idiomatic way to detect that ’this thing may have been destroyed while I wasn’t looking’ – exactly the guard added here.

RenderFrameHostImpl::IsActive() — Returns whether a frame is the active document (not pending deletion, not in back/forward cache, etc.). The dialog sites check IsActive() in addition to WeakPtr liveness because a frame can outlive the fullscreen drop yet be transitioned to a state where running a JS dialog or beforeunload confirm against it is invalid. (partly inferred: the diff shows the IsActive() check but not the full set of non-active states.)

HTML fullscreen re-entrancy — Entering/exiting fullscreen fires fullscreenchange and layout/resize side effects observable by JavaScript. A page can register handlers that mutate the DOM synchronously during the transition, which is the mechanism by which a subframe gets detached mid-operation.

Vulnerability window

  1. Introduction — The ForSecurityDropFullscreen() security mitigation for dialogs was added around the crbug.com/728276 timeframe referenced in the comments (‘Running a dialog causes an exit to webpage-initiated fullscreen’). The raw-pointer-across-the-drop pattern predates this fix at all four sites. (inferred: exact introducing commit not in this diff.)
  2. Discovery / report — Tracked as Chromium bug 552665794. The reproduction was reduced to a WebContentsDelegate that detaches the initiating iframe during ExitFullscreenModeForTab, indicating a deliberate root-cause reproduction of a detach-during-fullscreen-drop UAF. (reporter identity not in diff; inferred.)
  3. Fix on trunk — Landed as commit 0f8885e487bd85ac7d73ffa728986f23c0079632, main@{#1690285} (CL 8324908), authored by Frank Liberato and reviewed by Ted Meyer and Dave Tapuska.
  4. M150-LTS backport — Cherry-picked to branch-heads/7871 as this commit (8068cdd59a…), CL 8377481, on 2026-09-14, with owners-override by Achuith Bhandarkar and CQ by Gyuyoung Kim – the LTS channel merge of the trunk fix.

Proof of concept

A working reproduction exists as the two added browsertests. The recipe, reconstructed from RunJavaScriptDialogFrameDetachOnFullscreenExit: (1) an opener page enters HTML fullscreen via document.body.webkitRequestFullscreen(); (2) it opens a popup (window.open) that is in the opener chain / fullscreen-related set, and the popup contains a same-process child iframe (about:blank); (3) the popup’s child frame initiates a JavaScript dialog (alert) / beforeunload confirm; (4) WebContentsImpl calls ForSecurityDropFullscreen(), which drives the opener out of fullscreen; (5) a handler observing the fullscreen exit synchronously removes the popup’s iframe (document.querySelector(‘iframe’).remove()), destroying the child RenderFrameHostImpl; (6) on return, pre-patch code dereferences the freed RFHI. The test drives step 5 via a custom WebContentsDelegate that runs the removal inside ExitFullscreenModeForTab; a pure-web page would instead attach a fullscreenchange listener that removes the iframe. The test asserts weak_child_rfh becomes nullptr, confirming the frame is freed during the call – under ASAN the pre-fix build reports heap-use-after-free at that point. No exploit code is provided or reconstructed here.

// Reconstructed trigger sketch (from the added browsertest logic).
// Opener page (fullscreen), popup with a child iframe that shows a dialog.

// --- opener.html ---
<button onclick="go()">go</button>
<script>
async function go() {
  const popup = window.open('popup.html', 'popup');
  await document.body.requestFullscreen();      // enter HTML fullscreen
  // Detach the popup's iframe the instant fullscreen is dropped.
  document.addEventListener('fullscreenchange', () => {
    if (!document.fullscreenElement) {
      popup.document.querySelector('iframe').remove(); // frees child RFHI
    }
  });
}
</script>

// --- popup.html ---
<iframe src="child.html"></iframe>

// --- child.html ---
<script>
  // Initiating a dialog triggers ForSecurityDropFullscreen() in the browser,
  // which drops the opener's fullscreen and re-enters the handler above.
  alert('x');  // or window.onbeforeunload = () => 'x'; then navigate
</script>

Exploitation

  1. Reach the sink — Get a page into HTML fullscreen and then cause one of the four entry points to run against a frame that can be detached: a JS alert/confirm/prompt (RunJavaScriptDialog), a beforeunload prompt on navigation/close (RunBeforeUnloadConfirm), an click (RunFileChooser), or view-source on a related contents (ViewSource). All are directly script-reachable from an ordinary page. (proven reachable by the diff comments and tests.)
  2. Win the re-entrancy race — Register a fullscreenchange (or delegate-level) handler that synchronously removes the DOM node hosting the initiating subframe, so the RenderFrameHostImpl is destroyed inside ForSecurityDropFullscreen() before it returns. The test proves this destruction is synchronous and deterministic under the delegate, so no timing race is even required in the tested configuration. (inferred that a pure-web fullscreenchange handler achieves the same synchronous detach.)
  3. Reallocate the freed RFHI — Between the free and the stale dereference, an attacker would groom the browser-process heap (via IPC/mojo allocations of similarly-sized objects, additional frames, or blob/string allocations) to place controlled data into the freed RenderFrameHostImpl slot. (inferred; no allocation-control primitive is shown in the diff.)
  4. Hijack the dereference — The stale pointer is used for a virtual call (ComputeIsolationInfoForNavigation in ViewSource) or passed onward to delegate/dialog logic. A corrupted vtable pointer or object fields could yield control-flow hijack or a type-confused security decision in the browser process, i.e. a sandbox-escaping primitive. (inferred; exact post-free operations vary per site and reliability depends on grooming.)
  5. Constraints — Exploitation needs the user to be on a page that enters fullscreen and shows a dialog/chooser (the UI:R / minimal interaction), and reliable control requires heap grooming rather than a deterministic write, which caps this at high rather than critical. (inferred.)

Detection & hunting

For defenders and detection engineers:

  • ASAN heap-use-after-free at the dereference sites —
  • Dangling raw_ptr / MiraclePtr reports —
  • Crash telemetry clustering —
  • Behavioral heuristic —

Audit directions

  • Other ForSecurityDropFullscreen() callers —
  • Raw pointers held across observer/delegate notifications —
  • Public-interface type erasure hiding liveness tracking —
  • Dialog/chooser/view-source lifetime on inactive frames —

Before / after

Loading diff…