← 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 같은 멤버를 역참조하게 됩니다.

🔒

Detailed C++ destruction-order and cross-thread-callback lifetime analysis, including how a normal teardown path can be subverted and what primitives the resulting race could yield

더 확인하려면 구독해 주세요

🔒

Four reusable audit patterns identified for cross-thread callback lifetime bugs across the WebRTC media pipeline and related WebCore subsystems, with concrete grep targets

더 확인하려면 구독해 주세요