diff --git a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc index 5bc83ddd88164..06e8262d05b11 100644 --- a/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc +++ b/third_party/blink/renderer/platform/peerconnection/rtc_video_decoder_adapter_test.cc @@ -21,6 +21,7 @@ #include "base/test/mock_callback.h" #include "base/test/scoped_feature_list.h" #include "base/test/task_environment.h" +#include "base/threading/platform_thread.h" #include "base/threading/thread.h" #include "base/time/time.h" #include "build/build_config.h" @@ -866,6 +867,65 @@ TEST_F(RTCVideoDecoderAdapterTest, FallbackToSWInAV1SVC) { media_thread_.FlushForTesting(); } +// POC: Demonstrates a heap-use-after-free in RTCVideoDecoderAdapter when +// InitializeSync()'s 10-second TimedWait expires while the media thread is +// stalled. After the timeout, the adapter is destroyed and Release() posts +// "Task B" (impl.reset()) to the media thread. When the media thread drains: +// Task A (Impl::Initialize) runs -> the underlying VideoDecoder posts its +// init_cb to the media task runner (Task C), mimicking +// MojoVideoDecoder::FailInit / MediaCodecVideoDecoder behavior. +// Task B runs -> Impl and the VideoDecoder are freed. +// Task C runs -> the lambda calls video_decoder->GetDecoderType() and writes +// to *decoder_type / signals async_init_waiter_ on freed memory. +// This test takes ~12 seconds because the production timeout is hardcoded. +TEST_F(RTCVideoDecoderAdapterTest, UseAfterFreeOnInitializeSyncTimeout) { + scoped_refptr media_runner = + media_thread_.task_runner(); + + // When Impl::Initialize eventually runs on the media thread, make the mock + // decoder *post* its init_cb back to the same task runner instead of running + // it synchronously. This is what MojoVideoDecoder::FailInit() and + // MediaCodecVideoDecoder do. The posted callback (Task C) thereby outlives + // the decoder which owns nothing of it. + EXPECT_CALL(*video_decoder_, Initialize_(_, _, _, _, _, _)) + .WillOnce([media_runner](const media::VideoDecoderConfig&, bool, + media::CdmContext*, + media::VideoDecoder::InitCB& init_cb, + const media::VideoDecoder::OutputCB&, + const media::WaitingCB&) { + media_runner->PostTask( + FROM_HERE, + base::BindOnce(std::move(init_cb), + media::DecoderStatus( + media::DecoderStatus::Codes::kFailed))); + }); + + // Stall the media thread for >10s so InitializeSync's TimedWait expires + // and Release() has time to enqueue Task B behind Task A. + media_thread_.task_runner()->PostTask( + FROM_HERE, base::BindOnce([]() { + base::PlatformThread::Sleep(base::Seconds(12)); + })); + + // This blocks for ~12s: + // - webrtc thread: InitializeSync posts Task A, TimedWait(10s) expires, + // Create() resets the adapter -> ~RTCVideoDecoderAdapter -> Release() + // posts Task B and blocks on its waiter. + // - media thread wakes at t=12s: runs Task A (posts Task C), runs Task B + // (frees Impl + video_decoder_, signals Release waiter), runs Task C + // -> heap-use-after-free on freed video_decoder_ (virtual call) and on + // freed adapter members (decoder_type_, async_init_waiter_). + adapter_wrapper_ = RTCVideoDecoderAdapterWrapper::Create( + &gpu_factories_, + webrtc::SdpVideoFormat( + webrtc::CodecTypeToPayloadString(webrtc::kVideoCodecVP9)), + /*pass_resolution_monitor=*/true); + + // Drain any remaining work (Task C) so ASan observes the UAF before + // teardown. + media_thread_.FlushForTesting(); +} + TEST_F(RTCVideoDecoderAdapterTest, CanReadSharedFrameBuffer) { ASSERT_TRUE(BasicSetup());