CVE-2026-10948
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcall/call.cc |
modified | |
TESTcall/call_unittest.cc |
modified |
Files Changed
call/call.cccall/call_unittest.cc
Patch
From b5bd19b75c8c417d9e4d5a25ce55eea6b17b9c94 Mon Sep 17 00:00:00 2001 From: Erik Språng <[email protected]> Date: Tue, 21 Apr 2026 15:12:32 +0200 Subject: [PATCH] Remove audio stream from old sync group on update. If the sync group for an audio receive stream is updated, it must be removed from the old sync group as well so that the old group does not have an invalid reference. Bug: chromium:504599749 Change-Id: I548e194d4f8ac6461388d639836e1f62f6b7f7e7 Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/465663 Reviewed-by: Tomas Gunnarsson <[email protected]> Commit-Queue: Tomas Gunnarsson <[email protected]> Cr-Commit-Position: refs/heads/main@{#47504} --- diff --git a/call/call.cc b/call/call.cc index 0efef93..39905c0 100644 --- a/call/call.cc +++ b/call/call.cc @@ -1229,7 +1229,11 @@ RTC_DCHECK_RUN_ON(worker_thread_); webrtc::AudioReceiveStreamImpl& receive_stream = static_cast<webrtc::AudioReceiveStreamImpl&>(stream); + std::string old_sync_group(receive_stream.sync_group()); receive_stream.SetSyncGroup(sync_group); + if (old_sync_group != sync_group) { + ConfigureSync(old_sync_group); + } ConfigureSync(sync_group); } diff --git a/call/call_unittest.cc b/call/call_unittest.cc index 1a4f1e5..e024898 100644 --- a/call/call_unittest.cc +++ b/call/call_unittest.cc @@ -28,6 +28,7 @@ #include "api/test/mock_audio_mixer.h" #include "api/test/mock_video_decoder_factory.h" #include "api/test/video/function_video_encoder_factory.h" +#include "api/units/time_delta.h" #include "api/units/timestamp.h" #include "api/video/builtin_video_bitrate_allocator_factory.h" #include "api/video_codecs/sdp_video_format.h" @@ -55,6 +56,7 @@ #include "test/mock_audio_decoder_factory.h" #include "test/mock_transport.h" #include "test/run_loop.h" +#include "test/time_controller/simulated_time_controller.h" #include "video/config/video_encoder_config.h" namespace webrtc { @@ -614,4 +616,53 @@ un_demuxable_packet_handler_.AsStdFunction()); } +TEST(CallTest, HandlesAudioSyncGroupUpdate) { + // Set up a call with an audio stream and a video stream in the same sync + // group. + GlobalSimulatedTimeController time_controller(Timestamp::Seconds(1)); + Environment env = CreateTestEnvironment({.time = &time_controller}); + + AudioState::Config audio_state_config; + audio_state_config.audio_mixer = make_ref_counted<MockAudioMixer>(); + audio_state_config.audio_device_module = + make_ref_counted<MockAudioDeviceModule>(); + CallConfig config(env); + config.audio_state = AudioState::Create(audio_state_config); + std::unique_ptr<Call> call(Call::Create(std::move(config))); + + AudioReceiveStreamInterface::Config audio_config; + MockTransport rtcp_send_transport; + audio_config.rtp.remote_ssrc = 42; + audio_config.rtcp_send_transport = &rtcp_send_transport; + audio_config.decoder_factory = make_ref_counted<MockAudioDecoderFactory>(); + audio_config.sync_group = "group1"; + AudioReceiveStreamInterface* audio_stream = + call->CreateAudioReceiveStream(audio_config); + ASSERT_NE(audio_stream, nullptr); + + MockTransport video_rtcp_transport; + test::FakeVideoRenderer video_renderer; + VideoReceiveStreamInterface::Config video_config(&video_rtcp_transport); + video_config.rtp.remote_ssrc = 43; + video_config.sync_group = "group1"; + video_config.renderer = &video_renderer; + MockVideoDecoderFactory video_decoder_factory; + video_config.decoder_factory = &video_decoder_factory; + VideoReceiveStreamInterface::Decoder decoder; + decoder.payload_type = 96; + decoder.video_format = SdpVideoFormat("VP8"); + video_config.decoders.push_back(decoder); + VideoReceiveStreamInterface* video_stream = + call->CreateVideoReceiveStream(std::move(video_config)); + ASSERT_NE(video_stream, nullptr); + + // Move the audio stream to a new sync group and then remove it. + call->OnUpdateSyncGroup(*audio_stream, "group2"); + call->DestroyAudioReceiveStream(audio_stream); + + // The video stream should not be affected. + time_controller.AdvanceTime(TimeDelta::Seconds(2)); + call->DestroyVideoReceiveStream(video_stream); +} + } // namespace webrtc
Regression Test / PoC
diff --git a/call/call_unittest.cc b/call/call_unittest.cc
index 1a4f1e5..e024898 100644
--- a/call/call_unittest.cc
+++ b/call/call_unittest.cc
@@ -28,6 +28,7 @@
#include "api/test/mock_audio_mixer.h"
#include "api/test/mock_video_decoder_factory.h"
#include "api/test/video/function_video_encoder_factory.h"
+#include "api/units/time_delta.h"
#include "api/units/timestamp.h"
#include "api/video/builtin_video_bitrate_allocator_factory.h"
#include "api/video_codecs/sdp_video_format.h"
@@ -55,6 +56,7 @@
#include "test/mock_audio_decoder_factory.h"
#include "test/mock_transport.h"
#include "test/run_loop.h"
+#include "test/time_controller/simulated_time_controller.h"
#include "video/config/video_encoder_config.h"
namespace webrtc {
@@ -614,4 +616,53 @@
un_demuxable_packet_handler_.AsStdFunction());
}
+TEST(CallTest, HandlesAudioSyncGroupUpdate) {
+ // Set up a call with an audio stream and a video stream in the same sync
+ // group.
+ GlobalSimulatedTimeController time_controller(Timestamp::Seconds(1));
+ Environment env = CreateTestEnvironment({.time = &time_controller});
+
+ AudioState::Config audio_state_config;
+ audio_state_config.audio_mixer = make_ref_counted<MockAudioMixer>();
+ audio_state_config.audio_device_module =
+ make_ref_counted<MockAudioDeviceModule>();
+ CallConfig config(env);
+ config.audio_state = AudioState::Create(audio_state_config);
+ std::unique_ptr<Call> call(Call::Create(std::move(config)));
+
+ AudioReceiveStreamInterface::Config audio_config;
+ MockTransport rtcp_send_transport;
+ audio_config.rtp.remote_ssrc = 42;
+ audio_config.rtcp_send_transport = &rtcp_send_transport;
+ audio_config.decoder_factory = make_ref_counted<MockAudioDecoderFactory>();
+ audio_config.sync_group = "group1";
+ AudioReceiveStreamInterface* audio_stream =
+ call->CreateAudioReceiveStream(audio_config);
+ ASSERT_NE(audio_stream, nullptr);
+
+ MockTransport video_rtcp_transport;
+ test::FakeVideoRenderer video_renderer;
+ VideoReceiveStreamInterface::Config video_config(&video_rtcp_transport);
+ video_config.rtp.remote_ssrc = 43;
+ video_config.sync_group = "group1";
+ video_config.renderer = &video_renderer;
+ MockVideoDecoderFactory video_decoder_factory;
+ video_config.decoder_factory = &video_decoder_factory;
+ VideoReceiveStreamInterface::Decoder decoder;
+ decoder.payload_type = 96;
+ decoder.video_format = SdpVideoFormat("VP8");
+ video_config.decoders.push_back(decoder);
+ VideoReceiveStreamInterface* video_stream =
+ call->CreateVideoReceiveStream(std::move(video_config));
+ ASSERT_NE(video_stream, nullptr);
+
+ // Move the audio stream to a new sync group and then remove it.
+ call->OnUpdateSyncGroup(*audio_stream, "group2");
+ call->DestroyAudioReceiveStream(audio_stream);
+
+ // The video stream should not be affected.
+ time_controller.AdvanceTime(TimeDelta::Seconds(2));
+ call->DestroyVideoReceiveStream(video_stream);
+}
+
} // namespace webrtc
Original Bug Report
Potential UAF in WebRTC RtpStreamsSynchronizer due to stale sync group pointer
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability exists in WebRTC’s A/V synchronization logic. Changing an audio stream’s sync group fails to clear the reference held by video streams in the old sync group. A subsequent stream destruction leaves a dangling raw pointer that is dereferenced by a periodic task, potentially leading to Remote Code Execution.
Affected files:
third_party/webrtc/call/call.ccthird_party/webrtc/video/rtp_streams_synchronizer2.ccthird_party/webrtc/video/rtp_streams_synchronizer2.hthird_party/webrtc/pc/channel.ccthird_party/webrtc/media/engine/webrtc_voice_engine.ccthird_party/webrtc/audio/audio_receive_stream.hthird_party/webrtc/video/video_receive_stream2.cc
Estimated timestamp from git blame: 2024-09-02
Root Cause
A potential Use-After-Free (UAF) vulnerability exists in the WebRTC implementation within Chrome’s renderer process. The issue arises from how Call::OnUpdateSyncGroup and Call::DestroyAudioReceiveStream handle the transition of an AudioReceiveStreamImpl between synchronization groups.
When an audio stream’s sync group is changed, Call::OnUpdateSyncGroup updates the stream’s internal configuration and calls ConfigureSync for the new group only. ConfigureSync then iterates through all video streams, but only updates those whose sync_group() matches the provided argument.
// third_party/webrtc/call/call.cc
void Call::OnUpdateSyncGroup(webrtc::AudioReceiveStreamInterface& stream,
absl::string_view sync_group) {
webrtc::AudioReceiveStreamImpl& receive_stream =
static_cast<webrtc::AudioReceiveStreamImpl&>(stream);
receive_stream.SetSyncGroup(sync_group); // Updates config_.sync_group
ConfigureSync(sync_group); // Reconfigures NEW group only
}
If a VideoReceiveStream2 was previously synchronized with the audio stream in its old group, its internal reference (RtpStreamsSynchronizer::syncable_audio_) remains pointing to the audio stream because ConfigureSync is never called for the old group.
When the audio stream is later destroyed, Call::DestroyAudioReceiveStream again uses the updated sync_group() value, skipping the stale reference in the video stream once more. This leaves a dangling bare C++ pointer in RtpStreamsSynchronizer, which is not protected by MiraclePtr:
// third_party/webrtc/video/rtp_streams_synchronizer2.h
Syncable* syncable_audio_ RTC_GUARDED_BY(main_checker_) = nullptr;
This dangling pointer is dereferenced via a virtual call on the worker thread by a periodic 1 Hz task:
// third_party/webrtc/video/rtp_streams_synchronizer2.cc
std::optional<Syncable::Info> audio_info = syncable_audio_->GetInfo(); // Virtual call
Potential Exploit Scenario
An attacker can trigger this state by carefully maneuvering the stream through an unsignaled state to bypass standard teardown routines. These are suggested steps to trigger the vulnerability:
- The attacker establishes a WebRTC
PeerConnection. - The attacker sends a crafted SDP offer configuring an audio stream with sync group “group1” but omitting any signaled SSRCs (an unsignaled stream).
WebRtcVoiceReceiveChannel::AddRecvStreamcaches this configuration. - The attacker negotiates a signaled video stream associated with the same sync group “group1”.
- The attacker sends an RTP audio packet with a new SSRC.
MaybeCreateDefaultReceiveStreamhandles the packet, uses the cached parameters to spawn anAudioReceiveStreamImpl, and syncs it with the video stream. - The attacker sends a new valid SDP offer containing the now-signaled audio stream with a new sync group (“group2”), alongside a dummy unsignaled audio stream. The dummy stream bypasses
ResetUnsignaledRecvStream()(which would otherwise destroy the old stream safely). - The stream is promoted.
Call::OnUpdateSyncGroupis called with “group2”, leaving the video stream in “group1” with a stale pointer to the audio stream. - The attacker sends a final SDP offer removing the audio stream. The audio stream is deleted, but the video stream still holds the pointer.
- The attacker performs heap spraying in the renderer to reclaim the freed memory with a controlled vtable.
- The 1 Hz
repeating_task_inRtpStreamsSynchronizerexecutes a virtual function call on the attacker-controlled vtable, leading to Control Flow Hijacking and Remote Code Execution in the renderer process.
Suggested Fix
Modify Call::OnUpdateSyncGroup to clear the synchronization state of the old sync group before setting the new one. Call should either track the previous sync group and explicitly call ConfigureSync(old_sync_group), or it should iterate through all video_receive_streams_ and set syncable_audio_ to nullptr for any video stream that currently points to the audio stream being updated, regardless of its sync_group string.
Evaluated with Chrome root at commit: 7353d249d9cacf9c7218e1d7b8a39cf39c72d646
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.