CVE-2026-113303
Overview
Background
- `WebRtcAudioDeviceImpl`
- Blink’s implementation of WebRTC’s
AudioDeviceModule(third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc/.h). It mediates between WebRTC’s voice engine and Chrome’s audio renderer, and is reference-counted (webrtc::RefCountedObject). - `WebRtcAudioRenderer` / `source_`
- the audio renderer that plays out WebRTC audio. On attachment it holds a raw (non-owning)
source_back-pointer to theWebRtcAudioDeviceImplthat owns it; this pointer is cleared only when the device disconnects it. - `SetAudioRenderer()`
- attaches a renderer: it checks preconditions under
lock_, releaseslock_to call the renderer’s (potentially slow)Initialize(), then reacquireslock_and storesrenderer_. Called on the main renderer thread. - `Terminate()`
- tears the device down on the signaling thread: stops recording/playout, moves
renderer_out to disconnect it, and marks the device uninitialized. “Calling Terminate() multiple times is OK.” - `initialized_` / `lock_`
- a flag meaning the device is live. Before the patch it was not guarded by
lock_and was read/written across the main, signaling, and worker threads.
Root Cause Analysis
SetAudioRenderer() guarded attachment with only if (renderer_) return false;, never checking whether the device was still alive. Two unsafe orderings follow. First, if Terminate() already ran, the device is terminated but renderer_ is null, so a later SetAudioRenderer() happily calls renderer->Initialize() and stores renderer_ = renderer on a dead device. Second — the race — because SetAudioRenderer() releases lock_ across the unlocked Initialize() call, Terminate() can run on the signaling thread during that window: Terminate() moves out renderer_ (which is still null at that instant) and marks the device uninitialized, then SetAudioRenderer() reacquires lock_ and blindly does DCHECK(!renderer_); renderer_ = renderer. In both cases the renderer ends up attached to a terminated device while retaining its raw source_ back-pointer, and because the device considers itself already terminated, no subsequent Terminate() will ever disconnect that renderer — the renderer is left with a dangling source_ to a device that can be destroyed, a use-after-free. Compounding this, initialized_ was read and written without lock_, so the cross-thread checks were also a data race.
The patch makes initialized_ GUARDED_BY(lock_) and takes lock_ everywhere it is touched (Init, Terminate, Initialized, PlayoutIsAvailable, PlayoutIsInitialized, and the DCHECKs in Start/Stop paths), and has Terminate() set initialized_ = false inside the same locked block that moves out renderer_. renderer() now returns the scoped_refptr by value under lock_ instead of returning a raw reference. Crucially, SetAudioRenderer() now (a) checks if (!initialized_ || renderer_) return false; up front, (b) takes a scoped_refptr<WebRtcAudioDeviceImpl> self(this) to keep the device alive across the unlocked Initialize(), and (c) after reacquiring lock_ rechecks if (initialized_ && !renderer_) before storing renderer_; if Terminate() intervened, it instead calls renderer->DisconnectSource() (clearing the dangling back-pointer — done first so Stop() won’t re-enter RemoveAudioRenderer()) and then drives the renderer back to the uninitialized state via MediaStreamAudioRenderer::Start()/Stop() and returns false.
SetAudioRenderer() only checked “is a renderer already set?” and never “is the device still alive?”, and it did so around an unlocked Initialize() whose lock-release window let Terminate() slip in — so a renderer could be bound to (or left pointing at) a terminated device whose source_ back-pointer would never be cleared, yielding a dangling pointer / use-after-free.Attack Path
- Renderer-process trigger
a page uses WebRTC audio (e.g. a
PeerConnectionwith an audio track and output device switching) so thatWebRtcAudioDeviceImplattach/terminate paths are exercised (inferred: standard getUserMedia/PeerConnection reachability). - Begin attach
a
SetAudioRenderer()call passes therenderer_-null check and releaseslock_to run the renderer’sInitialize(), which performs real work (creating/starting a sink) and holds the lock open for a window. - Terminate concurrently
the device is terminated on the signaling thread during that window (or was terminated before the call), moving out
renderer_and marking the device dead without ever seeing the in-flight renderer. - Bind to dead device
SetAudioRenderer()reacquireslock_and storesrenderer_ = renderer(old code’sDCHECK(!renderer_)holds, masking the bug), leaving the renderer attached to a terminated device. - Dangling `source_`
no future
Terminate()disconnects this renderer, so its rawsource_points at aWebRtcAudioDeviceImplthat can be freed. - Primitive
use-after-free when the renderer later dereferences
source_(playout callbacks, device switch, teardown), reusing freed heap memory in the renderer process.
Impact Assessment
Terminate() and the main thread’s SetAudioRenderer()), but the memory-safety sink is the renderer’s raw source_ pointer to a terminated/freed device, which is why the advisory’s “memory corruption” is most precisely a UAF. Exploitation requires winning the Terminate()-during-Initialize() race (or the simpler already-terminated ordering) and then landing a controlled allocation in the freed device slot before the renderer next dereferences source_; the diff proves reachability and the dangling-pointer condition but does not demonstrate a concrete write primitive, so exploitable-UAF strength beyond crash is (inferred). The added self reference and the disconnect/stop fallback both eliminate the dangling pointer and the premature-free window.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc |
modified | |
ifthird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc |
modified | |
MockAudioSourceInterfacethird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc |
modified | |
WebRtcAudioRendererTrackSourceTestthird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc |
modified | |
WebRtcAudioRendererTrackSourceTestthird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.ccthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.ccthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.hthird_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
Audit Directions
- Unlocked-callback windowsaudit other
WebRtcAudioDeviceImpland WebRTC-glue methods that releaselock_to call out into a renderer/capturer and then reacquire it; any precondition asserted only before the release (especially viaDCHECK) can be violated by a concurrentTerminate(). - Raw back-pointers (`source_`)look for other objects holding a raw pointer to a ref-counted owner that is cleared only on an explicit disconnect path; verify every teardown ordering clears it, including the “owner already torn down” case.
- Lifetime across async initcheck callers that invoke
SetAudioRenderer/attach-style APIs without keeping an external ref alive for the call’s duration; thescoped_refptr self(this)pattern added here is the correct guard elsewhere too. - Flags shared across threadsgrep for other bool state (e.g.
playing_,recording_, capturer flags) historically read withoutlock_from signaling/worker/main threads for the same guarded-by mismatch.
Patch
From e193ceb7de134efa938172d45c38daec31658357 Mon Sep 17 00:00:00 2001 From: Guido Urdaneta <[email protected]> Date: Fri, 16 Oct 2026 07:12:49 -0700 Subject: [PATCH] Prevent attaching WebRtcAudioRenderer to terminated audio device WebRtcAudioDeviceImpl::SetAudioRenderer() previously checked only whether `renderer_` was null before calling WebRtcAudioRenderer::Initialize() and registering the renderer. If the audio device had already been terminated via WebRtcAudioDeviceImpl::Terminate(), or if Terminate() ran on the signaling thread while Initialize() was executing with `lock_` released, the renderer retained a raw `source_` pointer to the terminated device that would not be cleared on subsequent Terminate() calls. Fix this by: - Guarding `initialized_` with `lock_` across threads and returning `renderer_` by value under `lock_` in WebRtcAudioDeviceImpl::renderer(). - Checking `initialized_` before and after the unlocked WebRtcAudioRenderer::Initialize() call in SetAudioRenderer(), while retaining a reference to `this` across Initialize(). - Calling DisconnectSource() and stopping the renderer if Terminate() intervened during Initialize(). Fixed: 574041825 Change-Id: I36c202aae365df56f7f2a8180233179f6c9b88c7 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8591863 Reviewed-by: Johannes Kron <[email protected]> Commit-Queue: Guido Urdaneta <[email protected]> Cr-Commit-Position: refs/heads/main@{#1720672} --- diff --git a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc index 30d7398..c3ae12b 100644 --- a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc +++ b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc @@ -141,6 +141,12 @@ } } +scoped_refptr<blink::WebRtcAudioRenderer> WebRtcAudioDeviceImpl::renderer() + const { + base::AutoLock auto_lock(lock_); + return renderer_; +} + void WebRtcAudioDeviceImpl::RemoveAudioRenderer( blink::WebRtcAudioRenderer* renderer) { DCHECK_CALLED_ON_VALID_THREAD(main_thread_checker_); @@ -198,6 +204,7 @@ // We need to return a success to continue the initialization of WebRtc VoE // because failure on the capturer_ initialization should not prevent WebRTC // from working. See issue http://crbug.com/144421 for details. + base::AutoLock auto_lock(lock_); initialized_ = true; return 0; @@ -208,8 +215,12 @@ DCHECK_CALLED_ON_VALID_THREAD(signaling_thread_checker_); // Calling Terminate() multiple times in a row is OK. - if (!initialized_) - return 0; + { + base::AutoLock auto_lock(lock_); + if (!initialized_) { + return 0; + } + } StopRecording(); StopPlayout(); @@ -221,6 +232,7 @@ base::AutoLock auto_lock(lock_); renderer_to_disconnect = std::move(renderer_); capturers_.clear(); + initialized_ = false; } if (renderer_to_disconnect) { @@ -239,23 +251,25 @@ } } - initialized_ = false; return 0; } bool WebRtcAudioDeviceImpl::Initialized() const { DCHECK_CALLED_ON_VALID_THREAD(signaling_thread_checker_); + base::AutoLock auto_lock(lock_); return initialized_; } int32_t WebRtcAudioDeviceImpl::PlayoutIsAvailable(bool* available) { DCHECK_CALLED_ON_VALID_THREAD(signaling_thread_checker_); + base::AutoLock auto_lock(lock_); *available = initialized_; return 0; } bool WebRtcAudioDeviceImpl::PlayoutIsInitialized() const { DCHECK_CALLED_ON_VALID_THREAD(signaling_thread_checker_); + base::AutoLock auto_lock(lock_); return initialized_; } @@ -291,7 +305,6 @@ int32_t WebRtcAudioDeviceImpl::StopPlayout() { DVLOG(1) << "WebRtcAudioDeviceImpl::StopPlayout()"; - DCHECK(initialized_); // Can be called both from the worker thread (e.g. when called from webrtc) // or the signaling thread (e.g. when we call it ourselves internally). // The order in this check is important so that we won't incorrectly @@ -301,6 +314,7 @@ worker_thread_checker_.CalledOnValidThread()); #endif base::AutoLock auto_lock(lock_); + DCHECK(initialized_); // webrtc::VoiceEngine assumes that it is OK to call Stop() multiple times. playing_ = false; return 0; @@ -314,9 +328,9 @@ int32_t WebRtcAudioDeviceImpl::StartRecording() { DCHECK_CALLED_ON_VALID_THREAD(worker_thread_checker_); - DCHECK(initialized_); SendLogMessage(base::StringPrintf("%s()", __func__)); base::AutoLock auto_lock(lock_); + DCHECK(initialized_); base::AutoLock callback_lock(audio_transport_callback_lock_); if (!audio_transport_callback_) { LOG(ERROR) << "Audio transport is missing"; @@ -329,7 +343,6 @@ } int32_t WebRtcAudioDeviceImpl::StopRecording() { - DCHECK(initialized_); // Can be called both from the worker thread (e.g. when called from webrtc) // or the signaling thread (e.g. when we call it ourselves internally). // The order in this check is important so that we won't incorrectly @@ -340,6 +353,7 @@ #endif SendLogMessage(base::StringPrintf("%s()", __func__)); base::AutoLock auto_lock(lock_); + DCHECK(initialized_); recording_ = false; return 0; } @@ -365,11 +379,17 @@ DCHECK(renderer); SendLogMessage(base::StringPrintf("%s()", __func__)); + // Defense-in-depth: retain |this| across the unlocked initialization interval + // in case a caller does not keep an external reference alive for the duration + // of the call. + scoped_refptr<WebRtcAudioDeviceImpl> self(this); + // Here we acquire |lock_| in order to protect the internal state. { base::AutoLock auto_lock(lock_); - if (renderer_) + if (!initialized_ || renderer_) { return false; + } } // We release |lock_| here because invoking |renderer|->Initialize while @@ -390,12 +410,28 @@ // it attach later to the new thread. DETACH_FROM_THREAD(audio_renderer_thread_checker_); - // We acquire |lock_| again and assert our precondition, since we are - // accessing the internal state again. - base::AutoLock auto_lock(lock_); - DCHECK(!renderer_); - renderer_ = renderer; - return true; + // We acquire |lock_| again and recheck |initialized_| in case Terminate() + // ran while |lock_| was released. + { + base::AutoLock auto_lock(lock_); + if (initialized_ && !renderer_) { + renderer_ = renderer; + return true; + } + } + + // Terminate() ran while |lock_| was released. Disconnect the renderer from + // |this| and tear it down: after a successful Initialize() the renderer is + // paused with a started sink, and it must be back in the uninitialized state + // before it can be destroyed. Start()/Stop() are private on + // WebRtcAudioRenderer, so go via the MediaStreamAudioRenderer interface. + // DisconnectSource() must come first so that Stop() does not call back into + // RemoveAudioRenderer() on |this|. + renderer->DisconnectSource(); + MediaStreamAudioRenderer* media_stream_renderer = renderer; + media_stream_renderer->Start(); + media_stream_renderer->Stop(); + return false; } void WebRtcAudioDeviceImpl::AddAudioCapturer( diff --git a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h index 3a34e2c6..879510d 100644 --- a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h +++ b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h @@ -94,7 +94,9 @@ int32_t PlayoutDelay(uint16_t* delay_ms) const override; public: - // Sets the |renderer_|, returns false if |renderer_| already exists. + // Sets the |renderer_|, returns false if |renderer_| already exists, if the + // device is not initialized, or if Terminate() ran while initializing + // |renderer| (in which case |renderer| has been disconnected and stopped). // Called on the main renderer thread. bool SetAudioRenderer(blink::WebRtcAudioRenderer* renderer); @@ -112,9 +114,7 @@ // function will not be able to pick an appropriate device and return 0. base::UnguessableToken GetAuthorizedDeviceSessionIdForAudioRenderer(); - const scoped_refptr<blink::WebRtcAudioRenderer>& renderer() const { - return renderer_; - } + scoped_refptr<blink::WebRtcAudioRenderer> renderer() const; // blink::WebRtcAudioRendererSource implementation. @@ -182,11 +182,11 @@ // Cached value of the current audio delay on the output/renderer side. base::TimeDelta output_delay_ GUARDED_BY(lock_); - // Protects |renderer_|, |playout_sinks_|, |output_delay_|, |playing_|, - // and |recording_|. + // Protects |renderer_|, |playout_sinks_|, |output_delay_|, |initialized_|, + // |playing_|, and |recording_|. mutable base::Lock lock_; - bool initialized_; + bool initialized_ GUARDED_BY(lock_); bool playing_ GUARDED_BY(lock_); bool recording_ GUARDED_BY(lock_); diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc index 2a33019..c2d81e2 100644 --- a/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc +++ b/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc @@ -129,6 +129,10 @@ if (params.device_id != kInvalidOutputDeviceId) { EXPECT_CALL(*mock_sink_.get(), Start()); EXPECT_CALL(*mock_sink_.get(), Play()); + if (expect_stop_on_next_sink_) { + EXPECT_CALL(*mock_sink_.get(), Stop()); + expect_stop_on_next_sink_ = false; + } } else { EXPECT_CALL(*mock_sink_.get(), Stop()); } @@ -136,6 +140,10 @@ return mock_sink_; } + void set_expect_stop_on_next_sink(bool expect_stop) { + expect_stop_on_next_sink_ = expect_stop; + } + MOCK_METHOD3(MockNewAudioRendererSink, void(blink::WebAudioDeviceSourceType, blink::WebLocalFrame*, @@ -154,6 +162,7 @@ private: scoped_refptr<media::MockAudioRendererSink> mock_sink_; + bool expect_stop_on_next_sink_ = false; }; class MockAudioSourceInterface : public webrtc::AudioSourceInterface { @@ -619,6 +628,70 @@ renderer_proxy->Stop(); } +TEST_F(WebRtcAudioRendererTest, SetAudioRendererRejectedAfterDeviceTerminate) { + scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device( + new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>()); + webrtc::AudioDeviceModule* adm = audio_device.get(); + + auto renderer = base::MakeRefCounted<WebRtcAudioRenderer>( + scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor_, + *web_local_frame_, base::UnguessableToken::Create(), + kDefaultOutputDeviceId, base::RepeatingCallback<void()>()); + + EXPECT_CALL(*audio_device_factory_platform_, + MockNewAudioRendererSink(_, _, _)) + .Times(0); + + // Rejects attachment before Init(). + EXPECT_FALSE(audio_device->SetAudioRenderer(renderer.get())); + EXPECT_EQ(audio_device->renderer(), nullptr); + + adm->Init(); + adm->Terminate(); + + // Rejects attachment after Terminate(). + EXPECT_FALSE(audio_device->SetAudioRenderer(renderer.get())); + EXPECT_EQ(audio_device->renderer(), nullptr); +} + +TEST_F(WebRtcAudioRendererTest, + SetAudioRendererDisconnectedIfDeviceTerminatesDuringInitialize) { + scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device( + new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>()); + webrtc::AudioDeviceModule* adm = audio_device.get(); + adm->Init(); + + auto renderer = base::MakeRefCounted<WebRtcAudioRenderer>( + scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor_, + *web_local_frame_, base::UnguessableToken::Create(), + kDefaultOutputDeviceId, base::RepeatingCallback<void()>()); + + audio_device_factory_platform_->set_expect_stop_on_next_sink(true); + EXPECT_CALL(*audio_device_factory_platform_, + MockNewAudioRendererSink(blink::WebAudioDeviceSourceType::kWebRtc, + web_local_frame_.get(), _)) + .WillOnce(InvokeWithoutArgs([&]() { + // Simulate Terminate() and dropping the last external reference while + // SetAudioRenderer() has released its lock to run Initialize(). + adm->Terminate(); + adm = nullptr; + audio_device = nullptr; + })); + + // Hold a raw pointer only for the call; SetAudioRenderer() must keep `this` + // alive across Initialize(), detect that Terminate() intervened, and + // disconnect and stop the renderer. + blink::WebRtcAudioDeviceImpl* raw_audio_device = audio_device.get(); + EXPECT_FALSE(raw_audio_device->SetAudioRenderer(renderer.get())); + + auto renderer_proxy = + renderer->CreateSharedAudioRendererProxy(stream_descriptor_); + base::test::TestFuture<media::OutputDeviceStatus> future; + renderer_proxy->SwitchOutputDevice(kOtherOutputDeviceId, + future.GetCallback()); + EXPECT_EQ(future.Get(), media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL); +} + class WebRtcAudioRendererTrackSourceTest : public WebRtcAudioRendererTest { public: WebRtcAudioRendererTrackSourceTest() { diff --git a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc index 0ae6e543..a56ec84 100644 --- a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc +++ b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc @@ -239,7 +239,6 @@ base::RepeatingCallback<void()>(), run_loop_.QuitClosure()); test_audio_device_ = new webrtc::RefCountedObject<WebRtcAudioDeviceImpl>(); - EXPECT_TRUE(test_audio_device_->SetAudioRenderer(renderer_.get())); worker_thread_ = std::make_unique<base::Thread>("WebRTC_Worker"); worker_thread_->Start(); @@ -254,6 +253,8 @@ base::Unretained(test_audio_device_.get()), base::Unretained(&init_loop))); init_loop.Run(); + + EXPECT_TRUE(test_audio_device_->SetAudioRenderer(renderer_.get())); } ~WebRtcAudioDeviceImplReleaseTest() override {
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc b/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
index 2a33019..c2d81e2 100644
--- a/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
+++ b/third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
@@ -129,6 +129,10 @@
if (params.device_id != kInvalidOutputDeviceId) {
EXPECT_CALL(*mock_sink_.get(), Start());
EXPECT_CALL(*mock_sink_.get(), Play());
+ if (expect_stop_on_next_sink_) {
+ EXPECT_CALL(*mock_sink_.get(), Stop());
+ expect_stop_on_next_sink_ = false;
+ }
} else {
EXPECT_CALL(*mock_sink_.get(), Stop());
}
@@ -136,6 +140,10 @@
return mock_sink_;
}
+ void set_expect_stop_on_next_sink(bool expect_stop) {
+ expect_stop_on_next_sink_ = expect_stop;
+ }
+
MOCK_METHOD3(MockNewAudioRendererSink,
void(blink::WebAudioDeviceSourceType,
blink::WebLocalFrame*,
@@ -154,6 +162,7 @@
private:
scoped_refptr<media::MockAudioRendererSink> mock_sink_;
+ bool expect_stop_on_next_sink_ = false;
};
class MockAudioSourceInterface : public webrtc::AudioSourceInterface {
@@ -619,6 +628,70 @@
renderer_proxy->Stop();
}
+TEST_F(WebRtcAudioRendererTest, SetAudioRendererRejectedAfterDeviceTerminate) {
+ scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device(
+ new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>());
+ webrtc::AudioDeviceModule* adm = audio_device.get();
+
+ auto renderer = base::MakeRefCounted<WebRtcAudioRenderer>(
+ scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor_,
+ *web_local_frame_, base::UnguessableToken::Create(),
+ kDefaultOutputDeviceId, base::RepeatingCallback<void()>());
+
+ EXPECT_CALL(*audio_device_factory_platform_,
+ MockNewAudioRendererSink(_, _, _))
+ .Times(0);
+
+ // Rejects attachment before Init().
+ EXPECT_FALSE(audio_device->SetAudioRenderer(renderer.get()));
+ EXPECT_EQ(audio_device->renderer(), nullptr);
+
+ adm->Init();
+ adm->Terminate();
+
+ // Rejects attachment after Terminate().
+ EXPECT_FALSE(audio_device->SetAudioRenderer(renderer.get()));
+ EXPECT_EQ(audio_device->renderer(), nullptr);
+}
+
+TEST_F(WebRtcAudioRendererTest,
+ SetAudioRendererDisconnectedIfDeviceTerminatesDuringInitialize) {
+ scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device(
+ new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>());
+ webrtc::AudioDeviceModule* adm = audio_device.get();
+ adm->Init();
+
+ auto renderer = base::MakeRefCounted<WebRtcAudioRenderer>(
+ scheduler::GetSingleThreadTaskRunnerForTesting(), stream_descriptor_,
+ *web_local_frame_, base::UnguessableToken::Create(),
+ kDefaultOutputDeviceId, base::RepeatingCallback<void()>());
+
+ audio_device_factory_platform_->set_expect_stop_on_next_sink(true);
+ EXPECT_CALL(*audio_device_factory_platform_,
+ MockNewAudioRendererSink(blink::WebAudioDeviceSourceType::kWebRtc,
+ web_local_frame_.get(), _))
+ .WillOnce(InvokeWithoutArgs([&]() {
+ // Simulate Terminate() and dropping the last external reference while
+ // SetAudioRenderer() has released its lock to run Initialize().
+ adm->Terminate();
+ adm = nullptr;
+ audio_device = nullptr;
+ }));
+
+ // Hold a raw pointer only for the call; SetAudioRenderer() must keep `this`
+ // alive across Initialize(), detect that Terminate() intervened, and
+ // disconnect and stop the renderer.
+ blink::WebRtcAudioDeviceImpl* raw_audio_device = audio_device.get();
+ EXPECT_FALSE(raw_audio_device->SetAudioRenderer(renderer.get()));
+
+ auto renderer_proxy =
+ renderer->CreateSharedAudioRendererProxy(stream_descriptor_);
+ base::test::TestFuture<media::OutputDeviceStatus> future;
+ renderer_proxy->SwitchOutputDevice(kOtherOutputDeviceId,
+ future.GetCallback());
+ EXPECT_EQ(future.Get(), media::OUTPUT_DEVICE_STATUS_ERROR_INTERNAL);
+}
+
class WebRtcAudioRendererTrackSourceTest : public WebRtcAudioRendererTest {
public:
WebRtcAudioRendererTrackSourceTest() {
diff --git a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
index 0ae6e543..a56ec84 100644
--- a/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
+++ b/third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
@@ -239,7 +239,6 @@
base::RepeatingCallback<void()>(), run_loop_.QuitClosure());
test_audio_device_ = new webrtc::RefCountedObject<WebRtcAudioDeviceImpl>();
- EXPECT_TRUE(test_audio_device_->SetAudioRenderer(renderer_.get()));
worker_thread_ = std::make_unique<base::Thread>("WebRTC_Worker");
worker_thread_->Start();
@@ -254,6 +253,8 @@
base::Unretained(test_audio_device_.get()),
base::Unretained(&init_loop)));
init_loop.Run();
+
+ EXPECT_TRUE(test_audio_device_->SetAudioRenderer(renderer_.get()));
}
~WebRtcAudioDeviceImplReleaseTest() override {