← Chrome — 2026-W41

403a04e98d9f9d42ef0f5e41b829763e205ab154  Fix BrowserInterfaceBrokerImpl and MojoBinderPolicyApplier lifetime with kLazyBrowserInterfaceBroker

severity high class UAF confidence 0.92 signal score 6.59 content/browser/ patch-now AI review PLAUSIBLE
Primitive: dangling base::Unretained(this) deferred binder callbacks in MojoBinderPolicyApplier after broker destroyed before prerender activation -> UAF on activation
Triage note: Message: destroying broker_holder_ before activation left dangling deferred callbacks causing a UAF upon activation. Fix adds ~BrowserInterfaceBrokerImpl() calling DropDeferredBinders(), clears deferred_sync_binders_, guards broker_holder_ accesses (Bug 567216197).
Structural signals: security_assert (+3.0), null_guard (+2.59), msg_medium (+2.0), area:content/browser/ (+1.0), large_churn (-2.0)

The bug at a glance

A browser-process heap UAF reachable from an ordinary same-origin prerender. The dangling callbacks hold base::Unretained(this) pointers to a freed BrowserInterfaceBrokerImpl that live inside MojoBinderPolicyApplier’s deferred binder queues and fire at prerender activation (GrantAll), a well-defined attacker-controllable moment. Browser-process UAF with a predictable trigger; High. Gated on features::kLazyBrowserInterfaceBroker.

When kLazyBrowserInterfaceBroker is enabled, broker_holder_ (which owns BrowserInterfaceBrokerImpl) is created lazily and can be torn down (via TearDownMojoConnection on a subframe) before the prerendered page activates. BrowserInterfaceBrokerImpl had handed base::Unretained(this) BindInterface callbacks to the longer-lived MojoBinderPolicyApplier as deferred binders, on the now-false assumption that the broker always outlives the applier. The broker dies first, its deferred callbacks dangle, and they execute at activation.

Root cause

BrowserInterfaceBrokerImpl::GetInterface() routes non-associated interface requests through policy_applier_->ApplyPolicyToNonAssociatedBinder(interface_name, base::BindOnce(&BrowserInterfaceBrokerImpl::BindInterface, base::Unretained(this), …)); kDefer requests are stored in MojoBinderPolicyApplier’s deferred_binders_ / deferred_sync_binders_ and only run at GrantAll(). The old lifetime invariant breaks because mojo_binder_policy_applier_ was created inside CreateBrokerHolder() and broker_holder_ was reset on receiver rebind, so the applier (owned by RenderFrameHostImpl) survives while the broker is freed, leaving Unretained callbacks dangling. The fix adds ~BrowserInterfaceBrokerImpl() which calls policy_applier_->DropDeferredBinders(), and makes DropDeferredBinders() also clear deferred_sync_binders_. It further moves mojo_binder_policy_applier_ creation into the RenderFrameHostImpl constructor (when is_prerendering()), preserves broker_holder_ across rebinds, and reorders the member declarations so mojo_binder_policy_applier_ is destroyed after broker_holder_. Guarding broker_holder_ in RendererDidActivateForPrerendering() and DidCommitNavigation() removes the renderer-triggerable CHECK crashes.

Key code

New destructor drops the broker’s deferred callbacks from the applier; DropDeferredBinders() now also clears the sync queue.

~BrowserInterfaceBrokerImpl() override {
  if (policy_applier_) {
    policy_applier_->DropDeferredBinders();
  }
}

void MojoBinderPolicyApplier::DropDeferredBinders() {
  deferred_binders_.clear();
  deferred_sync_binders_.clear();
}

Patch walkthrough

  • content/browser/browser_interface_broker_impl.h — Adds ~BrowserInterfaceBrokerImpl() that calls policy_applier_->DropDeferredBinders() so any deferred callback bound with base::Unretained(this) is destroyed before the broker is freed.
  • content/browser/mojo_binder_policy_applier.cc — DropDeferredBinders() now also clears deferred_sync_binders_. Without this, synchronous kDefer binders holding Unretained(this) callbacks would survive broker destruction and fire at activation.
  • content/browser/renderer_host/render_frame_host_impl.cc — Moves mojo_binder_policy_applier_ creation into the RFHI constructor for prerendering frames. BindBrowserInterfaceBrokerReceiver only CreateBrokerHolder() when !broker_holder_; ResetBrowserInterfaceBrokerReceiver no longer resets broker_holder_ under the flag. DidCommitNavigation and RendererDidActivateForPrerendering now guard broker_holder_ with if() instead of CHECK().
  • content/browser/renderer_host/render_frame_host_impl.h — Reorders members: mojo_binder_policy_applier_ must be declared before broker_holder_ so it is destroyed after broker_holder_, guaranteeing the broker’s destructor sees a live applier.
  • content/browser/mojo_binder_policy_applier_unittest.cc — Extends the DropDeferredBinders test: registers a sync kDefer binder, asserts deferred_sync_binders() has one entry, then confirms DropDeferredBinders() empties both queues.
  • content/browser/preloading/prerender/prerender_browsertest.cc — Adds PrerenderLazyBrowserInterfaceBrokerBrowserTest: TearDownMojoConnectionBeforeActivation queues kDefer + sync-kDefer requests, tears down the child’s connection (freeing broker_holder_ under the flag), then activates and asserts no UAF/CHECK.

Background

BrowserInterfaceBrokerImpl — Browser-side implementation of blink.mojom.BrowserInterfaceBroker; GetInterface() dispatches renderer interface requests and, when a policy applier is attached, through policy_applier_. Owned by RenderFrameHostImpl via broker_holder_.

MojoBinderPolicyApplier — Enforces per-interface policy for prerendering frames. Stores deferred binder callbacks in deferred_binders_ and deferred_sync_binders_, running them at GrantAll().

kLazyBrowserInterfaceBroker — Feature flag that makes broker_holder_ lazily created and resettable (e.g. on TearDownMojoConnection or receiver rebind). It allows the broker to be destroyed before prerender activation, breaking the old broker-outlives-applier assumption.

base::Unretained — Binds a raw this pointer into a callback with no lifetime guarantee. BindInterface callbacks used it on the assumption the broker outlives the applier; once that fails, invoking the deferred callback is a UAF.

Prerender activation / GrantAll — When a prerendered page is activated, the applier moves to kGrantAll, flushing deferred binders — the moment dangling deferred callbacks execute.

Vulnerability window

  1. Setup — A prerendered page (flag on) issues kDefer and/or sync-kDefer interface requests; BrowserInterfaceBrokerImpl stores base::Unretained(this) callbacks in the applier’s deferred queues.
  2. Free — A subframe’s Mojo connection is torn down or the broker receiver is rebound; under the flag broker_holder_ is reset, destroying BrowserInterfaceBrokerImpl while the applier stays alive holding dangling callbacks.
  3. Trigger — The prerender is activated; GrantAll() runs the deferred binders, invoking BindInterface on the freed broker (UAF), or RendererDidActivateForPrerendering() hits CHECK(broker_holder_).
  4. Fixed — ~BrowserInterfaceBrokerImpl() drops both deferred queues on destruction, member ordering guarantees the applier is alive for that call, broker_holder_ is preserved across rebinds, and the CHECKs become guards.

Proof of concept

The diff ships a regression test (TearDownMojoConnectionBeforeActivation) reproducing the exact sequence: queue kDefer + sync-kDefer requests, tear down the child frame’s Mojo connection to free the broker under the flag, then activate. Pre-fix this executes dangling Unretained(this) BindInterface callbacks (UAF) or trips CHECK(broker_holder_). Web-facing reachability: a prerendered document can request Mojo interfaces and a subframe can be detached during the prerender, so the free-before-activation window is reachable when kLazyBrowserInterfaceBroker is enabled; a precise heap-grooming exploit is (inferred).

// prerender_browsertest.cc (parameterized on kLazyBrowserInterfaceBroker)
child_rfh->...->impl()->GetInterface(child_defer_remote.BindNewPipeAndPassReceiver());
child_rfh->...->impl()->GetInterface(
    mojo::GenericPendingReceiver("blink.mojom.NotificationService", std::move(sync_defer_pipe.handle0)));
child_rfh->TearDownMojoConnection();  // free broker before activation
main_rfh->RendererWillActivateForPrerendering();
child_rfh->RendererWillActivateForPrerendering();
main_rfh->RendererDidActivateForPrerendering();
child_rfh->RendererDidActivateForPrerendering();

Exploitation

  1. Reach — Start a same-origin prerender (speculation rules) of a page that requests deferred Mojo interfaces, including a sync kDefer interface, from a subframe.
  2. Free — Detach the subframe / tear down its Mojo connection so broker_holder_ is reset and BrowserInterfaceBrokerImpl is freed while the applier retains its Unretained(this) callbacks.
  3. Groom (inferred) — Reallocate the freed BrowserInterfaceBrokerImpl slot with attacker-controlled data so the vtable/this used by BindInterface points at crafted memory (not shown by the patch).
  4. Trigger — Activate the prerender; GrantAll() dispatches the deferred binder, calling into the freed/reallocated object. Escalation to control flow is (inferred); the patch demonstrates only the UAF and the CHECK-crash DoS.

Detection & hunting

For defenders and detection engineers:

  • Callback holds base::Unretained(this) stored in a longer-lived collection — Flag any ApplyPolicyToNonAssociatedBinder / deferred-callback pattern where base::BindOnce(…, base::Unretained(this)) is handed to an object not owned by this. Require a destructor that revokes those callbacks.
  • ASAN heap-use-after-free in BrowserInterfaceBrokerImpl::BindInterface during GrantAll — Specifically after TearDownMojoConnection/receiver rebind on a prerendering frame with kLazyBrowserInterfaceBroker on.
  • Renderer-triggerable CHECK(broker_holder_) — Browser crashes in RendererDidActivateForPrerendering()/DidCommitNavigation() CHECK(broker_holder_) indicate the same root cause reachable as a DoS.
  • Member destruction order — Audit for an object whose destructor calls into a sibling member; verify declaration order guarantees the callee outlives the caller.

Audit directions

  • MojoBinderPolicyApplier clients — Find all callers of ApplyPolicyToNonAssociatedBinder and confirm every object that enqueues Unretained callbacks revokes them in its destructor or guarantees it outlives the applier.
  • kLazyBrowserInterfaceBroker lifetimes — Review every broker_holder_.reset()/.emplace() and TearDownMojoConnection path for other state that assumed broker-outlives-applier.
  • Prerender deferred-binder queues — Audit both deferred_binders_ and deferred_sync_binders_ consumers; any new queue must be cleared by DropDeferredBinders.
  • Renderer-reachable CHECKs on broker_holder_ — Sweep RenderFrameHostImpl for CHECK(broker_holder_) on paths a renderer can drive and convert to guards where the holder can legitimately be absent.

Before / after

Loading diff…