403a04e98d9f9d42ef0f5e41b829763e205ab154 Fix BrowserInterfaceBrokerImpl and MojoBinderPolicyApplier lifetime with kLazyBrowserInterfaceBroker
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).
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
- 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.
- 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.
- Trigger — The prerender is activated; GrantAll() runs the deferred binders, invoking BindInterface on the freed broker (UAF), or RendererDidActivateForPrerendering() hits CHECK(broker_holder_).
- 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
- Reach — Start a same-origin prerender (speculation rules) of a page that requests deferred Mojo interfaces, including a sync kDefer interface, from a subframe.
- 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.
- 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).
- 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.