[4] WebRTC DTLS UAF on RTCP-mux renegotiation
Rated High because the diff adds destructor-time unsubscription of this-capturing callbacks that the pre-fix code left subscribed; the absence of any matching UnsubscribeReceivingState API upstream confirms the dangling-closure path is real, and a notification fired after DtlsTransportInternalImpl destruction performs virtual dispatch on freed memory — a UAF primitive in a libwebrtc-hosting renderer process.
DTLSTransport is not unregistering itself from receive/send callbacks, which can trigger a UAF. We fix this by adding API to register/unregister a specific listener, and use that API in DtlsTransportInternalImpl's destructor and in DtlsTransportInternalImpl::ConnectToIceTransport. We manually validated the fix using a specific STUN server. We should upgrade our testing infra to support running the test with the STUN server.
Source/ThirdParty/libwebrtc/Source/webrtc/p2p/dtls/dtls_transport.cc
Source/ThirdParty/libwebrtc/Source/webrtc/p2p/base/packet_transport_internal.cc
Patch Details
The patch adds a new UnsubscribeReceivingState(void* tag) method on webrtc::PacketTransportInternal that delegates to receiving_state_callbacks_.RemoveReceivers(tag), mirroring the existing UnsubscribeWritableState. ~DtlsTransportInternalImpl is amended to call both UnsubscribeReceivingState(this) and UnsubscribeWritableState(this) on its underlying ICE transport, in addition to the pre-existing ResetDtlsStunPiggybackCallbacks and DeregisterReceivedPacketCallback(this) cleanup. All changes are guarded by #if WEBRTC_WEBKIT_BUILD, marking this as a WebKit downstream patch on the bundled libwebrtc.
Failure to deregister a self-referencing callback during destruction, leaving the publisher holding a dangling closure that fires on a freed observer.
Background
WebRTC negotiates media transports via SDP. By default an RTCPeerConnection audio/video m-section has two transports — one for RTP and one for RTCP — each with its own ICE+DTLS stack. RTCP-mux is an SDP-level optimization (a=rtcp-mux) that merges RTP and RTCP onto a single 5-tuple; when later negotiation enables it, the RTCP-only transport is torn down while its peer's parent transport persists.
In libwebrtc, DtlsTransportInternalImpl owns a DTLS state machine that sits on top of an IceTransportInternal (which derives from PacketTransportInternal). PacketTransportInternal exposes CallbackList-based subscription APIs (SubscribeReceivingState, SubscribeWritableState, etc.) where each callback is an absl::AnyInvocable keyed by a void* tag; calling Notify* dispatches every registered callback. absl::AnyInvocable is a move-only type-erased callable that owns its captured state — capturing this records a raw pointer to the subscriber. Callback list lifetime is independent of the lifetimes of objects whose this is captured; the publisher does not learn that a subscriber has been destroyed unless the subscriber explicitly unregisters.
Analysis
The bug is a dangling callback subscription. DtlsTransportInternalImpl::ConnectToIceTransport subscribes this as a tagged receiver on the underlying ICE transport's receiving_state_callbacks_ and writable_state_callbacks_ lists. Those entries are absl::AnyInvocable<void(PacketTransportInternal*)> closures that capture this (the DtlsTransportInternalImpl*). The pre-fix destructor only cleared the piggyback callbacks and the received-packet callback; it did not remove the receiving-state and writable-state subscriptions. The upstream PacketTransportInternal API did not even expose an Unsubscribe for ReceivingState, so there was no way to detach. After the DtlsTransportInternalImpl is destroyed, the ICE transport survives and may later invoke NotifyReceivingState/NotifyWritableState, dispatching the stale closure on freed memory. The closure invokes a member function on the dead C++ object, performing virtual dispatch on a vtable pointer that points into reclaimable storage — a textbook UAF.
The triggering scenario from web content: open an RTCPeerConnection, negotiate an offer/answer that initially does NOT include a=rtcp-mux (forcing creation of a distinct RTCP DtlsTransportInternalImpl), then renegotiate with a=rtcp-mux so the RTCP DTLS transport is destroyed while its parent ICE transport persists. Drive state transitions on the surviving ICE transport — for example via a controlled STUN/TURN peer that flaps connectivity to toggle receiving()/writable() state — so NotifyReceivingState or NotifyWritableState fires after the destructor runs. The publisher then invokes the captured absl::AnyInvocable, whose this points at freed memory, and the closure performs virtual dispatch on the dead object. The commit message states the issue was reproduced against a specific STUN server during this exact RTCP-mux renegotiation sequence and notes WebKit's CI lacks the infrastructure to drive it, which is consistent with the crash first surfacing as a real-world peer report rather than in-tree fuzzing.
The observed effect is a use-after-free with virtual dispatch on a freed DtlsTransportInternalImpl. If the freed slot is reclaimed by an attacker-shaped allocation between the free and the notification — ArrayBuffer backing stores or WebRTC packet buffers reachable from JS are the obvious candidates — the vtable load could be redirected to attacker-chosen memory, escalating to an instruction-pointer hijack primitive. Absent reliable reclamation, the immediate effect is a renderer crash reachable from web content.
libwebrtc executes inside the WebContent (or GPU/network helper) sandbox depending on build configuration. This vulnerability weakens memory safety inside the renderer-side WebRTC pipeline: the stack assumes an observer registered on PacketTransportInternal outlives its subscription window; before the fix DtlsTransportInternalImpl violated this by being destroyed while still subscribed. A web page that drives RTCPeerConnection through a non-mux → RTCP-mux renegotiation against a network peer that can keep the ICE transport eliciting state changes could plausibly obtain a UAF primitive in the process hosting libwebrtc. A separate sandbox escape is still required for full system compromise.
Upstream libwebrtc's PacketTransportInternal already provides explicit Unsubscribe* mirrors for most of its Subscribe* APIs — writable, ready-to-send, network-route-changed, sent-packet — and SubscribeReceivingState was the asymmetric outlier that this patch fills. The downstream-only nature of the fix (WEBRTC_WEBKIT_BUILD) suggests the same gap exists upstream and may bite other libwebrtc consumers.
Audit directions
- Asymmetric pub/sub APIs where
Subscribeexists without a matchingUnsubscribecreate dangling-callback UAFs whenever a subscriber can be destroyed before the publisher. Audit everyCallbackList/AnyInvocable-based subscription site in libwebrtc and WebKit's WebRTC glue for this asymmetry. Start by greppingSource/ThirdParty/libwebrtcforSubscribe[A-Z]and confirming each has a pairedUnsubscribe; flag everyAddReceiverwhose subscriber capturesthiswithout a destructor-timeRemoveReceivers. - Tear-down paths that destroy a child transport while its parent transport survives (the RTCP-mux scenario here) expose any callback or observer relationship registered during construction. Audit
RTCPeerConnection/JsepTransportController/DtlsTransport/SrtpTransportlifecycles in libwebrtc for other observer registrations that lack matching destructor cleanup. Start withSource/ThirdParty/libwebrtc/Source/webrtc/p2p/dtlsandp2p/base/dtls_*— review everySubscribe*/Register*/Connect*call site and confirm there is a symmetricUnsubscribe*/Deregister*/Disconnect*in the corresponding destructor. - WebRTC SDP renegotiation transitions (mux/unmux, BUNDLE add/remove, ICE restart, datagram/realtime transport switch) are state changes that destroy and rebuild transport objects mid-flight. Audit these as a class of state-machine-triggered lifetime bugs. Investigate whether other media-stack negotiations (BUNDLE policy changes, RTX/FEC stream changes, SCTP datachannel teardown) leave residual callback subscriptions on persistent ICE/RTP transports.
- WebKit-only patches gated behind
WEBRTC_WEBKIT_BUILDindicate places where the downstream Safari/WebKit fork knows about a bug that upstream libwebrtc does not. GrepSource/ThirdParty/libwebrtcforWEBRTC_WEBKIT_BUILDto enumerate the existing downstream divergences — these are concentrated zones where lifecycle and security assumptions differ from upstream, and worth treating as a high-priority audit surface.