Fix race condition in unsignaled stream creation when worker!=network In `WebRtcVoiceReceiveChannel::MaybeCreateDefaultReceiveStream`, concurrent incoming RTP packets for an unsignaled SSRC could queue up multiple tasks with `sink = nullptr` on the Worker thread. This was detected here: https://ci.chromium.org/ui/p/webrtc/builders/try/win_x86_more_configs/73984/overview Bug: webrtc:42222117 Change-Id: I17e3f30d8f308ce7ce32bf8c4405d9f1b925fac8 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/476641 Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org> Reviewed-by: Erik Språng <sprang@webrtc.org> Cr-Commit-Position: refs/heads/main@{#47857}
diff --git a/media/engine/webrtc_voice_engine.cc b/media/engine/webrtc_voice_engine.cc index 02d0459..7c09cb9 100644 --- a/media/engine/webrtc_voice_engine.cc +++ b/media/engine/webrtc_voice_engine.cc
@@ -2630,11 +2630,16 @@ bool WebRtcVoiceReceiveChannel::MaybeCreateDefaultReceiveStream( const RtpPacketReceived& packet) { RTC_DCHECK_RUN_ON(worker_thread_); + uint32_t ssrc = packet.Ssrc(); + + if (absl::c_linear_search(unsignaled_recv_ssrcs_, ssrc)) { + RTC_LOG(LS_INFO) << "Unsignaled stream already exists for SSRC=" << ssrc; + return true; + } + // Create an unsignaled receive stream for this previously not received // ssrc. If there already is N unsignaled receive streams, delete the // oldest. See: https://bugs.chromium.org/p/webrtc/issues/detail?id=5208 - uint32_t ssrc = packet.Ssrc(); - RTC_DCHECK(!absl::c_linear_search(unsignaled_recv_ssrcs_, ssrc)); // Add new stream. StreamParams sp = unsignaled_stream_params_;
diff --git a/pc/BUILD.gn b/pc/BUILD.gn index 867ec7f..3622e7e 100644 --- a/pc/BUILD.gn +++ b/pc/BUILD.gn
@@ -3975,6 +3975,7 @@ "../rtc_base:net_helper", "../rtc_base:random", "../rtc_base:rtc_base_tests_utils", + "../rtc_base:rtc_event", "../rtc_base:socket_address", "../rtc_base:ssl", "../rtc_base:ssl_adapter",
diff --git a/pc/peer_connection_integrationtest.cc b/pc/peer_connection_integrationtest.cc index bebbd0c..c64467e 100644 --- a/pc/peer_connection_integrationtest.cc +++ b/pc/peer_connection_integrationtest.cc
@@ -77,6 +77,7 @@ #include "pc/test/fake_periodic_video_source.h" #include "pc/test/integration_test_helpers.h" #include "pc/test/mock_peer_connection_observers.h" +#include "rtc_base/event.h" #include "rtc_base/fake_mdns_responder.h" #include "rtc_base/firewall_socket_server.h" #include "rtc_base/logging.h" @@ -2944,6 +2945,61 @@ EXPECT_EQ(RtpSourceType::SSRC, sources[0].source_type()); } +TEST_P(PeerConnectionIntegrationTest, ConcurrentUnsignaledSsrcPackets) { + ASSERT_TRUE(CreatePeerConnectionWrappers()); + ConnectFakeSignaling(); + caller()->AddAudioTrack(); + callee()->SetReceivedSdpMunger(RemoveSsrcsAndMsids); + caller()->CreateAndSetAndSignalOffer(); + ASSERT_THAT(WaitUntil([&] { return SignalingStateStable(); }, IsTrue()), + IsRtcOk()); + + // Wait until the connection is established. + ASSERT_THAT(WaitUntil([&] { return DtlsConnected(); }, IsTrue()), IsRtcOk()); + + // Drop all subsequent packets temporarily so we can coordinate the + // concurrency. + firewall()->AddRule(false); + + // Block the shared worker thread. + Event worker_blocked; + Event worker_continue; + callee()->pc_internal()->worker_thread()->PostTask( + [&worker_blocked, &worker_continue] { + worker_blocked.Set(); + worker_continue.Wait(Event::kForever); + }); + worker_blocked.Wait(Event::kForever); + + uint32_t initial_sent = virtual_socket_server()->sent_packets(); + + // Clear the firewall rules to allow packets to flow again. + // Since the worker thread is blocked, incoming RTP packets on the network + // thread will result in tasks posted to the worker thread to handle the + // unsignaled SSRC packet (specifically to create the default receive stream). + firewall()->ClearRules(); + + // Wait until at least 2 packets are sent through the virtual socket server. + ASSERT_THAT(WaitUntil( + [&] { + return virtual_socket_server()->sent_packets() - + initial_sent; + }, + ::testing::Ge(2u)), + IsRtcOk()); + + // Let the worker thread resume. It will process the queued tasks. + // The first task will create the default stream, and subsequent tasks + // for the same SSRC will discover that the stream already exists or is + // being created, and safely skip creation. + worker_continue.Set(); + + // Wait deterministically for the packets to be delivered and processed. + MediaExpectations media_expectations; + media_expectations.CalleeExpectsSomeAudio(1); + ASSERT_TRUE(ExpectNewFrames(media_expectations)); +} + TEST_P(PeerConnectionIntegrationTest, UnsignaledSsrcGetSourcesAudio) { ASSERT_TRUE(CreatePeerConnectionWrappers()); ConnectFakeSignaling();