diff --git a/call/BUILD.gn b/call/BUILD.gn index a8ad708290..e1484d32dc 100644 --- a/call/BUILD.gn +++ b/call/BUILD.gn @@ -483,6 +483,37 @@ rtc_library("fake_network") { ] } +# POC: heap-use-after-free in RtpStreamsSynchronizer::syncable_audio_ +# after Call::OnUpdateSyncGroup. This target is intentionally placed +# outside the `rtc_include_tests` / `!build_with_chromium` guards so it +# can be built inside a Chromium ASAN tree. +rtc_executable("sync_group_uaf_poctest") { + testonly = true + sources = [ "sync_group_uaf_poctest.cc" ] + deps = [ + ":call", + ":call_interfaces", + ":video_receive_stream_api", + "../api:make_ref_counted", + "../api:scoped_refptr", + "../api/audio_codecs:audio_codecs_api", + "../api/test/video:function_video_factory", + "../api/units:time_delta", + "../api/video_codecs:video_codecs_api", + "../modules/audio_device:mock_audio_device", + "../modules/audio_mixer:audio_mixer_impl", + "../test:audio_codec_mocks", + "../test:create_test_environment", + "../test:fake_video_codecs", + "../test:mock_transport", + "../test:run_loop", + "../test:test_support", + "../test:video_test_common", + "//third_party/webrtc_overrides:metrics", + "//third_party/webrtc_overrides:system_time", + ] +} + if (rtc_include_tests) { if (!build_with_chromium) { rtc_library("call_tests") { diff --git a/call/sync_group_uaf_poctest.cc b/call/sync_group_uaf_poctest.cc new file mode 100644 index 0000000000..6fdfb93106 --- /dev/null +++ b/call/sync_group_uaf_poctest.cc @@ -0,0 +1,134 @@ +/* + * POC: Stale Syncable* in RtpStreamsSynchronizer after + * Call::OnUpdateSyncGroup leads to UAF (virtual call) on freed + * AudioReceiveStreamImpl. + * + * This test exercises the production sequence that + * WebRtcVoiceReceiveChannel::AddRecvStream performs when an unsignaled + * audio stream is "promoted" to a signaled stream with a *different* + * msid (sync_group): + * + * Call::CreateVideoReceiveStream(V, sync_group="A") + * Call::CreateAudioReceiveStream(X, sync_group="A") + * -> ConfigureSync("A") -> V.SetSync(X) + * RtpStreamsSynchronizer::syncable_audio_ = X + * 1 Hz UpdateDelay() repeating task started. + * Call::OnUpdateSyncGroup(X, "B") + * -> X.sync_group becomes "B" + * -> ConfigureSync("B") iterates only video streams in "B" + * ==> V (still group "A") is skipped; syncable_audio_ stays X. + * Call::DestroyAudioReceiveStream(X) + * -> ConfigureSync(X.sync_group()) == ConfigureSync("B") + * ==> V skipped again. + * -> delete X. <-- syncable_audio_ now dangling. + * + * RtpStreamsSynchronizer::UpdateDelay() + * -> syncable_audio_->GetInfo() <-- UAF, virtual call on + * freed AudioReceiveStreamImpl. + * + * Under ASAN this produces a heap-use-after-free at + * rtp_streams_synchronizer2.cc:107. + */ + +#include + +#include "api/audio_codecs/audio_decoder_factory.h" +#include "api/make_ref_counted.h" +#include "api/scoped_refptr.h" +#include "api/test/video/function_video_decoder_factory.h" +#include "api/units/time_delta.h" +#include "api/video_codecs/sdp_video_format.h" +#include "call/audio_receive_stream.h" +#include "call/audio_state.h" +#include "call/call.h" +#include "call/call_config.h" +#include "call/video_receive_stream.h" +#include "modules/audio_device/include/mock_audio_device.h" +#include "modules/audio_mixer/audio_mixer_impl.h" +#include "test/create_test_environment.h" +#include "test/fake_decoder.h" +#include "test/fake_videorenderer.h" +#include "test/gtest.h" +#include "test/mock_audio_decoder_factory.h" +#include "test/mock_transport.h" +#include "test/run_loop.h" + +namespace webrtc { +namespace { + +TEST(SyncGroupUafPoc, OnUpdateSyncGroupLeavesStaleSyncableAudio) { + test::RunLoop loop; + + AudioState::Config audio_state_config; + audio_state_config.audio_mixer = AudioMixerImpl::Create(); + audio_state_config.audio_processing = nullptr; + audio_state_config.audio_device_module = + test::MockAudioDeviceModule::CreateNice(); + + CallConfig config(CreateTestEnvironment()); + config.audio_state = AudioState::Create(audio_state_config); + std::unique_ptr call = Call::Create(std::move(config)); + + ::testing::NiceMock transport; + scoped_refptr audio_decoder_factory = + MockAudioDecoderFactory::CreateEmptyFactory(); + test::FunctionVideoDecoderFactory video_decoder_factory( + []() { return std::make_unique(); }); + test::FakeVideoRenderer renderer; + + // 1) Video receive stream V in sync_group "A". + VideoReceiveStreamInterface::Config video_config(&transport); + video_config.rtp.remote_ssrc = 0x1111; + video_config.sync_group = "A"; + video_config.decoders.emplace_back(SdpVideoFormat("VP8"), + /*payload_type=*/96); + video_config.decoder_factory = &video_decoder_factory; + video_config.renderer = &renderer; + VideoReceiveStreamInterface* video_stream = + call->CreateVideoReceiveStream(std::move(video_config)); + ASSERT_NE(video_stream, nullptr); + + // 2) Audio receive stream X in sync_group "A". + // Call::CreateAudioReceiveStream -> ConfigureSync("A") sets + // V's RtpStreamsSynchronizer::syncable_audio_ = X and starts the + // 1 Hz UpdateDelay() repeating task on this RunLoop's task queue. + AudioReceiveStreamInterface::Config audio_config; + audio_config.rtp.remote_ssrc = 0x2222; + audio_config.rtcp_send_transport = &transport; + audio_config.decoder_factory = audio_decoder_factory; + audio_config.sync_group = "A"; + AudioReceiveStreamInterface* audio_stream = + call->CreateAudioReceiveStream(audio_config); + ASSERT_NE(audio_stream, nullptr); + + // 3) Promote X to sync_group "B" -- this is exactly what + // WebRtcVoiceReceiveChannel::AddRecvStream does at + // media/engine/webrtc_voice_engine.cc when an unsignaled SSRC is + // later signaled with a different msid. ConfigureSync("B") only + // touches video streams whose sync_group()=="B"; V (group "A") is + // skipped and keeps its raw Syncable* pointing at X. + call->OnUpdateSyncGroup(*audio_stream, "B"); + + // 4) Destroy X. Call::DestroyAudioReceiveStream calls + // ConfigureSync(X.sync_group()) == ConfigureSync("B"), which again + // skips V, then `delete X`. V's syncable_audio_ is now dangling. + call->DestroyAudioReceiveStream(audio_stream); + + // 5) Let the 1 Hz repeating task fire. RtpStreamsSynchronizer:: + // UpdateDelay() runs `syncable_audio_->GetInfo()` -- a virtual call + // through the freed AudioReceiveStreamImpl's vtable. + // + // *** Under ASAN this is a heap-use-after-free. *** + loop.RunFor(TimeDelta::Millis(1500)); + + // (Only reached without ASAN / if the bug is fixed.) + call->DestroyVideoReceiveStream(video_stream); +} + +} // namespace +} // namespace webrtc + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}