← Chrome — 2026-W40

3635198d8f04357b26ef255ababa931cb2965b01  Convert IceTransportProxy::Delegate to GarbageCollectedMixin

severity medium class UAF confidence 0.75 signal score 7.27 third_party/blink/renderer/modules/ patch-now AI review CONFIRMED
Primitive: raw_ptr<Delegate> to non-GCed base of GCed RTCIceTransport invisible to Oilpan
Triage note: IceTransportProxy held const raw_ptr<Delegate> to RTCIceTransport (GC-managed); Oilpan can't see the edge, so the delegate can be reclaimed while proxy ICE callbacks still fire. Fix makes Delegate a GarbageCollectedMixin, stores WeakPersistent, adds CHECK guards.
Structural signals: security_assert (+6.47), area:third_party/blink/renderer/modules/ (+2.5), surgical (+1.3)

The bug at a glance

Use-after-free in the Blink WebRTC peerconnection ICE stack, reachable from any page via RTCPeerConnection. IceTransportProxy held a raw_ptr to a non-GCed base subobject of the garbage-collected RTCIceTransport; Oilpan could not see this edge, so the RTCIceTransport could be reclaimed while ICE callbacks still fired through the stale delegate. UAF of a GCed renderer object driven by network-timed ICE events; High.

The dangling edge is invisible-to-GC, not a missing null-check: raw_ptr pointed at a non-GarbageCollected base class (Delegate) of a GarbageCollected object (RTCIceTransport). Oilpan traces GCed objects through their mixin bases only if the base is a GarbageCollectedMixin; a plain base reached via raw_ptr is not a managed reference, so the collector was free to reclaim RTCIceTransport while IceTransportProxy still called into it.

Root cause

IceTransportProxy::Delegate was a plain C++ class and IceTransportProxy stored const raw_ptr delegate_, while the concrete delegate is the Oilpan-GarbageCollected RTCIceTransport. A raw_ptr to a non-GCed base subobject of a GCed object is not a tracing edge, so Oilpan treated the RTCIceTransport as unreachable and could reclaim it even though IceTransportProxy kept calling delegate_->OnGatheringStateChanged / OnCandidateGathered / OnStateChanged / OnSelectedCandidatePairChanged. The fix makes Delegate derive from GarbageCollectedMixin and changes delegate_ to WeakPersistent: the mixin makes the base a traceable/weak-trackable Oilpan edge, and WeakPersistent is automatically cleared to null when the RTCIceTransport is collected. The added CHECK(delegate_) in each callback converts a cleared weak reference into a safe abort, and RTCIceTransport::Trace is extended with IceTransportProxy::Delegate::Trace(visitor).

Key code

ice_transport_proxy.h: make Delegate a GC mixin and hold it as a WeakPersistent

  class Delegate : public GarbageCollectedMixin {
   public:
    virtual ~Delegate() = default;
...
-  const raw_ptr<Delegate> delegate_;
+  const WeakPersistent<Delegate> delegate_;

Patch walkthrough

  • third_party/blink/renderer/modules/peerconnection/adapters/ice_transport_proxy.h — Root fix. Delegate now derives from GarbageCollectedMixin, and delegate_ changes from const raw_ptr to const WeakPersistent, turning the edge into one Oilpan tracks and auto-nulls on collection.
  • third_party/blink/renderer/modules/peerconnection/adapters/ice_transport_proxy.cc — Adds CHECK(delegate_) before each delegate_-> call in OnGatheringStateChanged, OnCandidateGathered, OnStateChanged and OnSelectedCandidatePairChanged, so a weak reference cleared by GC aborts safely rather than dereferencing a freed object.
  • third_party/blink/renderer/modules/peerconnection/rtc_ice_transport.cc — RTCIceTransport::Trace now calls IceTransportProxy::Delegate::Trace(visitor), wiring the new GarbageCollectedMixin base into Oilpan’s tracing so the delegate edge is actually visited.

Background

Oilpan / GarbageCollectedMixin — Blink’s GC traces managed objects through Member/Persistent edges and through mixin base classes that derive from GarbageCollectedMixin. A plain base reached via raw_ptr is not a managed edge and is invisible to the collector.

WeakPersistent — An off-heap Oilpan handle that does not keep its target alive but is automatically reset to null when the target is collected, eliminating the dangling-pointer window a raw_ptr leaves.

IceTransportProxy::Delegate — Interface receiving ICE callbacks (gathering state, candidate gathered, transport state, selected candidate pair) on the proxy thread; implemented by the GCed RTCIceTransport.

raw_ptr — MiraclePtr-backed raw pointer. It provides PartitionAlloc UAF mitigation for PA heap objects but is not an Oilpan tracing edge, so it cannot keep or safely track a GCed object’s lifetime.

Vulnerability window

  1. Pre-patch — IceTransportProxy held raw_ptr to a non-GCed base of the GCed RTCIceTransport; Oilpan could reclaim the transport while ICE callbacks still fired through delegate_.
  2. Fix 2026-09-29 — Converts Delegate to GarbageCollectedMixin and delegate_ to WeakPersistent (CL 8454865), Bug 566019326.

Proof of concept

No test or PoC ships in the diff. Conceptually (inferred): a page creates an RTCPeerConnection and an ICE transport, then drops all JS references to the RTCIceTransport so Oilpan can reclaim it, while ICE activity (STUN/TURN gathering, candidate pairs, state transitions) continues to drive IceTransportProxy callbacks; the stale raw_ptr would then be dereferenced after free. Web-facing reachability is direct: RTCPeerConnection / ICE candidate gathering is script-triggerable, with the free/callback race timed by network-driven ICE events.

Exploitation

  1. Setup — Script creates an RTCPeerConnection, begins ICE gathering, then releases references to the RTCIceTransport so the GCed object becomes collectable while IceTransportProxy outlives it (inferred).
  2. Free — An Oilpan GC reclaims RTCIceTransport; with the old raw_ptr the delegate_ edge is not updated, leaving a dangling pointer into freed heap (inferred).
  3. Use — A subsequent ICE callback dereferences delegate_, a UAF on a GCed renderer object; grooming the freed slot could yield control-flow or type confusion (inferred sink/escalation, not shown by the patch).

Detection & hunting

For defenders and detection engineers:

  • raw_ptr pointing at a non-GCed base of a GarbageCollected object — Flag fields holding a raw pointer to an interface/base implemented by an Oilpan GCed class. The safe pattern is a Member/WeakMember/WeakPersistent to a GarbageCollectedMixin base.
  • Delegate/observer interfaces not derived from GarbageCollectedMixin but implemented by GCed classes — Any callback interface whose only implementers are GarbageCollected should itself be a GarbageCollectedMixin and be traced from the implementer’s Trace().
  • Missing Trace() of a mixin base — A GarbageCollectedMixin base must be traced from the deriving class’s Trace(); absence leaves the edge untracked.

Audit directions

  • Other adapters in modules/peerconnection/adapters — Review IceTransportHost and sibling proxy/adapter classes for raw_ptr/unretained edges into GCed Blink objects crossing the proxy/host thread boundary.
  • Cross-thread delegate/observer patterns over GCed objects — Search Blink for Delegate/Observer interfaces stored as raw_ptr where the implementer is GarbageCollected.
  • raw_ptr vs Member misuse — Audit for raw_ptr on fields whose T is (or is a base of) a GarbageCollected type; such fields should be Member/WeakMember/Persistent.

Before / after

Loading diff…