← All issues

[4] WebRTC DTLS UAF on RTCP-mux renegotiation

Severity: High | Component: libwebrtc DTLS transport | c458fe1

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

DtlsTransportInternalImpl::~DtlsTransportInternalImpl() {
ice_transport()->ResetDtlsStunPiggybackCallbacks();
ice_transport()->DeregisterReceivedPacketCallback(this);
+#if WEBRTC_WEBKIT_BUILD
+ ice_transport()->UnsubscribeReceivingState(this);
+ ice_transport()->UnsubscribeWritableState(this);
+#endif
}

Source/ThirdParty/libwebrtc/Source/webrtc/p2p/base/packet_transport_internal.cc

+#if WEBRTC_WEBKIT_BUILD
+void PacketTransportInternal::UnsubscribeReceivingState(void* tag)
+{
+ receiving_state_callbacks_.RemoveReceivers(tag);
+}
+#endif

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.

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.

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.

🔒

Multiple reusable audit patterns identified for pub/sub lifetime bugs across libwebrtc, with concrete starting points for variant discovery.

Subscribe to read more

🔒

Multiple reusable audit patterns identified for pub/sub lifetime bugs across libwebrtc, with concrete starting points for variant discovery.

Subscribe to read more