← All issues

[9] RealtimeIncoming{Audio,Video}Source removes sink after derived members destroyed

Severity: Medium | Component: WebCore WebRTC media pipeline | 02e76c6

Medium으로 분류된 이유는, 이 diff가 race window UAF를 수정하기 때문입니다. libwebrtc media thread callback이 이미 소멸된 derived 멤버를 역참조할 수 있는 상황이 해당됩니다. Web content에서 이 window에 도달하려면, preventSourceFromEnding observer가 정상적인 requestToEnd 경로를 차단하는 상태에서 RTCPeerConnection teardown을 유도해야 합니다.

WebRTC audio/video callback이 객체 소멸 중에 이미 소멸된 멤버 변수에 접근하면서 crash가 발생했습니다. Derived 클래스(예: RealtimeIncomingAudioSourceCocoa)의 컴파일러 생성 destructor는 base 클래스 destructor를 호출하기 전에 derived 멤버들을 먼저 소멸시킵니다. 그런데 audio/video track sink를 제거하는 stop() 호출은 base 클래스 destructor 안에 위치해 있었습니다. 이번 수정으로 derived destructor가 멤버 소멸 이전에 stop()을 호출하여 sink를 먼저 제거하도록 변경되었습니다.

Source/WebCore/platform/mediastream/RealtimeIncomingAudioSource.cpp

RealtimeIncomingAudioSource::~RealtimeIncomingAudioSource()
{
- stop();
+ // Subclass는 각자의 destructor에서 stop()을 호출해야 합니다.
+ // derived 멤버가 소멸되기 전에 audio track sink를 반드시 제거해야 하기 때문입니다.
+ // 그렇지 않으면 audio thread의 OnData callback이 이미 소멸된 멤버에 접근할 수 있습니다.
+ ASSERT(!isProducingData());
m_audioTrack->UnregisterObserver(this);
}

Source/WebCore/platform/mediastream/cocoa/RealtimeIncomingAudioSourceCocoa.cpp

+RealtimeIncomingAudioSourceCocoa::~RealtimeIncomingAudioSourceCocoa()
+{
+ stop();
+}

Source/WebCore/platform/mediastream/cocoa/RealtimeIncomingVideoSourceCocoa.mm

+RealtimeIncomingVideoSourceCocoa::~RealtimeIncomingVideoSourceCocoa()
+{
+ stop();
+}

네 개의 concrete subclass(RealtimeIncomingAudioSourceCocoa, RealtimeIncomingVideoSourceCocoa, RealtimeIncomingAudioSourceLibWebRTC, RealtimeIncomingVideoSourceLibWebRTC) 모두에 명시적 destructor가 추가되었습니다. 각 destructor는 C++가 derived 멤버를 소멸시키기 전에 stop()을 호출하여 WebRTC audio/video track sink를 먼저 제거합니다. Base destructor에서는 더 이상 stop()을 호출하지 않으며, m_audioTrack->UnregisterObserver(this) / m_videoTrack->UnregisterObserver(this) 호출만 남아 있습니다. 아울러 새로운 계약 조건을 문서화하고 검증하기 위해 ASSERT(!isProducingData())가 추가되었습니다.

C++ 소멸 순서에서 발생하는 use-after-free: 멀티스레드 callback이 접근하는 derived 멤버가 이미 소멸된 이후에 base 클래스 destructor가 해당 callback의 등록을 해제하는 패턴.

Remote peer가 RTCPeerConnection을 통해 audio나 video를 전송하면, libwebrtc는 각 remote track을 webrtc::AudioTrackInterface / webrtc::VideoTrackInterface로 노출합니다. WebCore는 각 track을 RealtimeIncoming*Source로 감싸며, 이 클래스는 libwebrtc의 sink interface(AudioTrackSinkInterface::OnData, VideoSinkInterface::OnFrame)를 구현합니다. WebKit 객체는 startProducingData()에서 AddSink/AddOrUpdateSink를 통해 sink로 자신을 등록하고, 소멸 전에 stopProducingData()RemoveSink를 통해 반드시 등록을 해제해야 합니다. libwebrtc는 OnData/OnFrame을 WebCore main thread가 아닌 자체 media thread에서 호출합니다. libwebrtc 내부의 RemoveSinksink_lock_을 획득하여 callback dispatch와의 직렬화를 보장합니다.

Derived 클래스 D : B의 소멸 순서는 ~DD의 멤버 소멸 → ~B 순입니다. 따라서 derived 상태가 살아있는 동안 반드시 완료되어야 하는 작업은 ~D 안에서 처리해야 합니다. RTCPeerConnection::doClose()는 정상적인 teardown 경로로, 각 source에 requestToEnd()를 호출합니다. 그러나 RealtimeMediaSourceObserverpreventSourceFromEnding()이 true를 반환하면 requestToEnd()가 차단될 수 있습니다. 이 경우 isProducingData()가 여전히 true인 상태로 소멸에 도달하는 상황이 가능합니다.

RealtimeIncomingAudioSourceCocoa가 소멸될 때, 컴파일러는 먼저 derived destructor를 실행한 뒤 derived 데이터 멤버(m_audioBufferList, pixel buffer pool)를 소멸시키고, 마지막으로 base destructor를 호출합니다. WebRTC sink를 제거하는 stop()은 base destructor 안에 위치해 있었습니다. 즉, WebRTC track이 부분적으로 소멸된 subclass를 가리키는 pointer를 아직 보유하고 있는 상태에서, media thread의 OnData 또는 OnFrame callback이 이미 실행 중이거나 새로 진입할 수 있었습니다. 이때 callback은 C++ runtime이 이미 해제한 m_audioBufferList 같은 멤버를 역참조하게 됩니다.

libwebrtc 내부 RemoteAudioSourcesink_lock_을 통해 RemoveSink와 진행 중인 callback 사이의 동기화는 보장됩니다. 다만 이 동기화가 이루어지는 시점이 너무 늦습니다. derived 상태가 이미 소멸된 이후이기 때문입니다. RTCPeerConnection::doClose()의 정상 경로는 requestToEnd()를 통해 source를 먼저 중단시키지만, RealtimeMediaSourceObserverpreventSourceFromEnding()이 true를 반환하면 이 경로가 차단될 수 있습니다. 결과적으로 isProducingData()가 여전히 true인 상태에서 소멸에 도달하는 상황이 가능합니다. 이번 수정은 stop()을 각 derived destructor 안으로 이동시켜, 모든 derived 멤버가 살아있는 동안 sink 등록을 해제하고 대기 중인 callback이 완료되도록 보장합니다. Base destructor는 이제 ASSERT(!isProducingData())를 통해 invariant를 검증하는 역할만 담당합니다.

이 vulnerability는 WebRTC remote media 수신 경로의 memory safety를 약화시킵니다. 위반된 invariant는 다음과 같습니다. libwebrtc sink interface를 구현하는 객체는 모든 pending sink callback보다 오래 살아있어야 하며, callback이 읽는 subclass 상태가 소멸되기 전에 RemoveSink가 완료되어야 합니다. Remote peer(또는 RTCPeerConnection lifecycle을 제어하는 공격자의 JS)는 RealtimeIncoming*Source의 소멸과 여전히 수신 중인 audio/video callback 사이에 race를 유발할 수 있습니다. 이 경우 WebRTC media thread에서 부분적으로 소멸된 C++ 객체에 대한 read 및 write가 발생합니다. 해당 메모리 영역을 공격자가 영향을 미치는 allocation으로 재사용할 수 있다면, WebContent sandbox 내에서 UAF primitive가 노출됩니다. libwebrtc 내부의 sink lock 동기화는 필요 조건이지만 충분 조건은 아닙니다. 이 동기화는 callback과 RemoveSink 사이의 race만 보호하며, RemoveSink보다 먼저 실행되는 derived destructor와의 race는 방어하지 못합니다.