Critical chrome UAF 🔧 Commit mapped

Overview

Critical
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in WebRTC
DescriptionMemory corruption in WebRTC
ComponentWebRTC
Bug ClassUAF
Tracker574041825
Fix commite193ceb7de13 (chromium/src) +131/-21
CISA KEVNot listed
CreditedOpenAI Codex Security (kylebot)
Disclosed2026-10-20

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 the WebRtcAudioDeviceImpl that owns it; this pointer is cleared only when the device disconnects it.
`SetAudioRenderer()`
attaches a renderer: it checks preconditions under lock_, releases lock_ to call the renderer’s (potentially slow) Initialize(), then reacquires lock_ and stores renderer_. 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.

Key insight
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

  1. Renderer-process trigger a page uses WebRTC audio (e.g. a PeerConnection with an audio track and output device switching) so that WebRtcAudioDeviceImpl attach/terminate paths are exercised (inferred: standard getUserMedia/PeerConnection reachability).
  2. Begin attach a SetAudioRenderer() call passes the renderer_-null check and releases lock_ to run the renderer’s Initialize(), which performs real work (creating/starting a sink) and holds the lock open for a window.
  3. 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.
  4. Bind to dead device SetAudioRenderer() reacquires lock_ and stores renderer_ = renderer (old code’s DCHECK(!renderer_) holds, masking the bug), leaving the renderer attached to a terminated device.
  5. Dangling `source_` no future Terminate() disconnects this renderer, so its raw source_ points at a WebRtcAudioDeviceImpl that can be freed.
  6. 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

This is a use-after-free / dangling-pointer bug in the renderer process, reachable from WebRTC audio usage that any web page can initiate — no elevated privilege required. The flaw has a clear race component (a lock-release window between the signaling thread’s 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

FunctionChangeNotes
if
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc
modified
if
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
modified
MockAudioSourceInterface
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
modified
TEST_F
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
modified
WebRtcAudioRendererTrackSourceTest
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
modified
WebRtcAudioRendererTrackSourceTest
third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h
  • third_party/blink/renderer/modules/peerconnection/webrtc_audio_renderer_test.cc

Audit Directions

  • Unlocked-callback windows
    audit other WebRtcAudioDeviceImpl and WebRTC-glue methods that release lock_ to call out into a renderer/capturer and then reacquire it; any precondition asserted only before the release (especially via DCHECK) can be violated by a concurrent Terminate().
  • 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 init
    check callers that invoke SetAudioRenderer/attach-style APIs without keeping an external ref alive for the call’s duration; the scoped_refptr self(this) pattern added here is the correct guard elsewhere too.
  • Flags shared across threads
    grep for other bool state (e.g. playing_, recording_, capturer flags) historically read without lock_ from signaling/worker/main threads for the same guarded-by mismatch.
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 {
Loading diff…

Regression Test / PoC

shipped with the fix
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 {
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.