← Chrome — 2026-W03

264b4317115819cd2105adb4de962c01f6fe4e2d  Clear some members on detach

severity medium class use-after-free/stale-member CVSS 6.0 confidence 0.4 signal score 3.8 third_party/blink/renderer AI review PLAUSIBLE
Primitive: microtasks_pauser_, prescient_networking_ and link_preview_triggerer_ were not reset during LocalFrame detach, so post-detach access could touch stale state. Fix resets them at detach time.
Triage note: Frame-teardown lifetime fix in the renderer; potential UAF/stale-use reachable via frame detach, but the concrete exploitable primitive is not provable from the diff. watch.

The bug at a glance

This is a defensive lifetime-hygiene change in LocalFrame::DetachImpl: three owned subsystem handles (microtasks_pauser_, prescient_networking_, link_preview_triggerer_) that were previously left populated across detach are now torn down at detach time alongside the existing cleanup block. The commit is security-flagged (isSecurity=true) and lands in the highest-value renderer object for cross-process lifetime bugs, the LocalFrame. However, the diff itself demonstrates no reachable use-after-free, no attacker-controlled trigger, and ships no regression test. The reset() calls guard against a latent class where a detached frame could still service a Mojo callback, microtask, or preload/navigation-prediction event and dereference state that logically should be gone. Because a concrete post-detach dereference path is not shown, and because the practical worst case for stale-but-still-live GC/Mojo handles is more often a null/DCHECK crash than exploitable corruption, this is honestly a watch-tier hardening fix rather than a proven exploitable UAF. Medium reflects the object’s importance and the maintainer’s security tag, not a demonstrated primitive.

Detach is the moment a LocalFrame stops being a valid context, but teardown in Blink is ordered and incremental. Any owned helper that survives past DetachImpl becomes a candidate for a use-after-detach: it can still be reached by an in-flight Mojo reply, a queued microtask, or a speculative networking/link-preview signal that fires on a frame the rest of the engine already considers dead. The angle for an auditor is not this specific diff but the pattern: enumerate every member of LocalFrame that owns a callback surface and confirm each is neutralized in DetachImpl, because the ones that are not are exactly the ones that produce hard-to-find stale-state bugs.

Root cause

In LocalFrame::DetachImpl, the existing cleanup block clears frame_visibility_observers_ and resets not_restored_reasons_, then asserts the view is unattached and notifies the client. The bug (b:475274575) is that three additional owned handles were not being reset in this window. microtasks_pauser_ holds a pause token over the frame’s microtask queue; if it outlives detach it keeps a scheduling handle referencing torn-down context. prescient_networking_ backs speculative connections/preresolve driven by user-intent signals; leaving it live means a prediction callback could route into a detached frame. link_preview_triggerer_ arms link-preview hover/gesture logic that likewise fires asynchronously. The fix adds microtasks_pauser_.reset(); prescient_networking_.reset(); link_preview_triggerer_.reset(); immediately after not_restored_reasons_.reset(), so all three are dropped at the same detach checkpoint as their peers. The mechanism is purely ordering: the objects were always destroyed eventually (frame GC), but not synchronously at detach, leaving a window where async work could observe a dead frame’s helper.

Key code

LocalFrame::DetachImpl now tears down three async-capable helpers at detach time

   frame_visibility_observers_.clear();
 
   not_restored_reasons_.reset();
+  microtasks_pauser_.reset();
+  prescient_networking_.reset();
+  link_preview_triggerer_.reset();
 
   DCHECK(!view_->IsAttached());
   Client()->WillBeDetached();

Patch walkthrough

  • third_party/blink/renderer/core/frame/local_frame.cc — The only edited file. Three reset() calls are inserted into the DetachImpl cleanup block, right after not_restored_reasons_.reset() and before the DCHECK(!view_->IsAttached()) / Client()->WillBeDetached() sequence. Placement is deliberate: it groups the new teardowns with the existing member-clearing so all frame-owned observer/prediction helpers are neutralized before the client is told the frame is going away. No other logic, no size or bounds change, no new caller. The subject ‘Clear some members on detach’ and the body ‘Fix a problem where some members weren\u2019t being reset at the right time’ both frame this as timing/ordering, consistent with a latent stale-state fix rather than a patched crash.

Background

LocalFrame::DetachImpl — The core Blink routine that unwinds a frame when it is removed from the tree or its document is torn down. It clears observers, notifies clients, and detaches the view. State not released here can be reached by asynchronous work that assumes a live frame.

microtasks_pauser_ — An owned handle representing a paused/held state over the frame’s microtask checkpoint. Held across detach it keeps a scheduling reference into a context that should no longer run microtasks.

prescient_networking_ — Blink’s speculative-networking helper (preconnect/preresolve driven by user-intent hints). It services asynchronous prediction signals, so a live instance on a detached frame is a candidate for a callback into dead state.

link_preview_triggerer_ — Drives the link-preview feature’s gesture/hover triggering. Like prescient networking it reacts to deferred input/gesture events, making post-detach survival a stale-access risk.

use-after-detach — A subclass of use-after-free specific to frame lifetime: the object memory may still be valid (GC-managed) but the frame it belongs to is logically dead, so operating on it violates invariants and can crash or, in the worst case, act on attacker-influenced stale context.

Vulnerability window

  1. introduction — As microtasks pausing, prescient networking, and link-preview triggering were added to LocalFrame over time, their members were not wired into the DetachImpl cleanup block that already handled peers like not_restored_reasons_ and frame_visibility_observers_.
  2. report — Internal bug b:475274575 (not a public crbug at time of commit) identified members not being reset at the right time during detach; filed/handled by frame-owner Dave Tapuska.
  3. fix — 2026-01-16, main@{#1570504}: three reset() calls added to DetachImpl; reviewed by Daniel Cheng, a Blink lifetime/security-sensitive reviewer.
  4. status — No public test, no CVE, no exploit. Treated as silent lifetime hardening; watch for whether a follow-up or a related crbug documents a concrete post-detach dereference.

Proof of concept

No PoC or test shipped with this commit, and none is reconstructable from the diff because the diff shows only the fix, not the reachable dereference. An honest investigation recipe: (1) instrument each of the three members to log/DCHECK on any use after LocalFrame::DetachImpl has run; (2) drive a page that arms each subsystem then synchronously detaches its frame\u2014e.g. an iframe that issues a speculative-navigation/link-preview gesture or holds a microtask pause, whose parent removes the iframe from the DOM (or navigates it) during the async window; (3) observe whether a queued microtask, a Mojo preconnect reply, or a link-preview timer callback lands after detach on the now-would-be-stale member. Success would be a crash or a DCHECK on pre-patch builds and clean behavior post-patch. This demonstrates reachability only; no exploit primitive is claimed and no exploit code is provided.

Exploitation

  1. reachability (unproven) — An attacker page would need to arm one of the three async helpers and then detach the owning frame within the callback/microtask window. Frame detach is script-controllable (removing an iframe, navigating), so the trigger surface exists, but this diff does not establish that any of the three actually fire post-detach.
  2. primitive (speculative) — Even granting a post-detach callback, the likely outcome is operating on GC-live-but-logically-dead state: a null deref or invariant violation (crash / DoS). Turning that into memory corruption would require an additional bug where the stale helper writes through a freed or repurposed pointer, which is not shown here.
  3. assessment — No exploitation chain is demonstrated. Classify as latent-safety hardening; the value is closing a stale-state window before someone finds a concrete callback that lands in it.

Detection & hunting

For defenders and detection engineers:

  • post-detach helper access —
  • crash clustering —

Audit directions

  • LocalFrame member inventory —
  • sibling frame types —
  • the three subsystems’ async surfaces —

Before / after

Loading diff…