3635198d8f04357b26ef255ababa931cb2965b01 Convert IceTransportProxy::Delegate to GarbageCollectedMixin
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.
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
- 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_.
- 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
- 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).
- 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).
- 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.