CVE-2026-7987
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc |
modified | |
WebRtcAudioDeviceImplTestthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_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.hthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc
Patch
From a0f3b94ca37f9a9a626bc5f04c7030f074ead7ba Mon Sep 17 00:00:00 2001 From: Tomas Gunnarsson <[email protected]> Date: Thu, 02 Apr 2026 02:55:47 -0700 Subject: [PATCH] Protect audio_transport_callback_ with a lock. This change introduces a new lock, `audio_transport_callback_lock_`, to guard the `audio_transport_callback_` member. This ensures that the callback is not accessed by the audio render thread while it is being modified or reset on the signaling thread, preventing potential use-after-free issues. Bug: 498696266 Change-Id: I4e8a93bf7f231580fac42a49f189dc25b445adb3 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7725063 Commit-Queue: Tomas Gunnarsson <[email protected]> Reviewed-by: Tony Herre <[email protected]> Cr-Commit-Position: refs/heads/main@{#1609071} --- 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 fca8b59..dfb5d189 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 @@ -93,7 +93,6 @@ audio_bus->Zero(); return; } - DCHECK(audio_transport_callback_); // Store the reported audio delay locally. output_delay_ = audio_delay; } @@ -114,9 +113,14 @@ TRACE_EVENT_BEGIN1("audio", "VoE::PullRenderData", "frames", frames_per_10_ms); - audio_transport_callback_->PullRenderData( - kBytesPerSample * 8, sample_rate, audio_bus->channels(), frames_per_10_ms, - audio_data, &elapsed_time_ms, &ntp_time_ms); + { + base::AutoLock callback_lock(audio_transport_callback_lock_); + if (audio_transport_callback_) { + audio_transport_callback_->PullRenderData( + kBytesPerSample * 8, sample_rate, audio_bus->channels(), + frames_per_10_ms, audio_data, &elapsed_time_ms, &ntp_time_ms); + } + } TRACE_EVENT_END2("audio", "VoE::PullRenderData", "elapsed_time_ms", elapsed_time_ms, "ntp_time_ms", ntp_time_ms); if (elapsed_time_ms >= 0) { @@ -179,6 +183,7 @@ DCHECK_CALLED_ON_VALID_THREAD(signaling_thread_checker_); SendLogMessage(base::StringPrintf("%s()", __func__)); base::AutoLock lock(lock_); + base::AutoLock callback_lock(audio_transport_callback_lock_); DCHECK_EQ(!audio_transport_callback_, !!audio_callback); audio_transport_callback_ = audio_callback; return 0; @@ -252,6 +257,7 @@ DVLOG(1) << "WebRtcAudioDeviceImpl::StartPlayout()"; DCHECK_CALLED_ON_VALID_THREAD(worker_thread_checker_); base::AutoLock auto_lock(lock_); + base::AutoLock callback_lock(audio_transport_callback_lock_); if (!audio_transport_callback_) { LOG(ERROR) << "Audio transport is missing"; return 0; @@ -291,6 +297,7 @@ DCHECK(initialized_); SendLogMessage(base::StringPrintf("%s()", __func__)); base::AutoLock auto_lock(lock_); + base::AutoLock callback_lock(audio_transport_callback_lock_); if (!audio_transport_callback_) { LOG(ERROR) << "Audio transport is missing"; return -1; 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 30d437f..3a34e2c6 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 @@ -169,10 +169,15 @@ // before it goes away. PlayoutDataSinkList playout_sinks_ GUARDED_BY(lock_); + // Protects |audio_transport_callback_| from being modified while it is in use + // by the audio render thread. + mutable base::Lock audio_transport_callback_lock_; + // Weak reference to the audio callback. // The webrtc client defines |audio_transport_callback_| by calling // RegisterAudioCallback(). - raw_ptr<webrtc::AudioTransport, DanglingUntriaged> audio_transport_callback_; + raw_ptr<webrtc::AudioTransport> audio_transport_callback_ + GUARDED_BY(audio_transport_callback_lock_); // Cached value of the current audio delay on the output/renderer side. base::TimeDelta output_delay_ GUARDED_BY(lock_); 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 42996437..ca22e7d4 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 @@ -74,9 +74,9 @@ class WebRtcAudioDeviceImplTest : public testing::Test { public: WebRtcAudioDeviceImplTest() - : audio_device_( - new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>()), - audio_transport_(new MockAudioTransport()) { + : audio_transport_(new MockAudioTransport()), + audio_device_( + new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>()) { audio_device_module()->Init(); audio_device_module()->RegisterAudioCallback(audio_transport_.get()); } @@ -89,8 +89,8 @@ } test::TaskEnvironment task_environment_; - scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device_; std::unique_ptr<MockAudioTransport> audio_transport_; + scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device_; }; // Verify that stats are accumulated during calls to RenderData and are
Regression Test / PoC
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 42996437..ca22e7d4 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
@@ -74,9 +74,9 @@
class WebRtcAudioDeviceImplTest : public testing::Test {
public:
WebRtcAudioDeviceImplTest()
- : audio_device_(
- new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>()),
- audio_transport_(new MockAudioTransport()) {
+ : audio_transport_(new MockAudioTransport()),
+ audio_device_(
+ new webrtc::RefCountedObject<blink::WebRtcAudioDeviceImpl>()) {
audio_device_module()->Init();
audio_device_module()->RegisterAudioCallback(audio_transport_.get());
}
@@ -89,8 +89,8 @@
}
test::TaskEnvironment task_environment_;
- scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device_;
std::unique_ptr<MockAudioTransport> audio_transport_;
+ scoped_refptr<blink::WebRtcAudioDeviceImpl> audio_device_;
};
// Verify that stats are accumulated during calls to RenderData and are
Original Bug Report
Potential Use-after-free in WebRtcAudioDeviceImpl::RenderData during WebRTC teardown
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 security team.
Overview: A race condition in WebRtcAudioDeviceImpl::RenderData allows a potential use-after-free of the AudioTransport callback object. The lock protecting the callback pointer is released before the virtual method PullRenderData is called, allowing the underlying object to be destroyed on another thread.
Affected files:
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.ccthird_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.hthird_party/webrtc/media/engine/webrtc_voice_engine.ccthird_party/webrtc/audio/audio_state.h
Estimated timestamp from git blame: 2025-09-30
Description
A potential use-after-free (UAF) vulnerability exists in the WebRTC audio rendering path in Blink. The issue stems from a race condition in WebRtcAudioDeviceImpl::RenderData where a pointer to a webrtc::AudioTransport object is extracted and used after the object it points to can be freed during the teardown of a WebRTC session.
In third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc, the RenderData() method (which runs on the high-priority real-time audio render thread) acquires a lock to check the state of the audio device. However, it releases this lock before invoking the virtual method PullRenderData() on the audio_transport_callback_ member. Concurrently, when a session is closed (e.g., via pc.close() in JavaScript), WebRtcVoiceEngine::Terminate() is called on the WebRTC worker thread, which clears this callback and subsequently destroys the object it points to.
Vulnerability Details
In WebRtcAudioDeviceImpl::RenderData (lines 56-117):
65: base::AutoLock auto_lock(lock_);
... // Checks playing state and total playout delay
96: DCHECK(audio_transport_callback_);
99: } // <-- lock_ released here
...
117: audio_transport_callback_->PullRenderData( // <-- UAF: raw_ptr evaluated without lock
The audio_transport_callback_ member is declared as a raw_ptr<webrtc::AudioTransport, DanglingUntriaged>.
During teardown, WebRtcVoiceEngine::Terminate() in third_party/webrtc/media/engine/webrtc_voice_engine.cc executes the following sequence on the WebRTC worker thread:
568: adm()->StopPlayout();
569: adm()->StopRecording();
570: adm()->RegisterAudioCallback(nullptr); // Sets audio_transport_callback_ = nullptr under lock
571: adm()->Terminate();
The AudioTransportImpl object is owned by the AudioState object as a member variable. Once the last reference to the AudioState scoped_refptr is dropped (which occurs following Terminate() and the destruction of the Call object), the AudioState and its member AudioTransportImpl are destroyed.
Exploitation and MiraclePtr Bypass
The critical race window exists between the extraction of the raw C++ pointer via operator->() at line 117 and the completion of the PullRenderData() virtual method call.
If the real-time audio render thread is preempted immediately after evaluating the raw_ptr but before (or during) the virtual function execution, the worker thread can concurrently execute the teardown sequence. The worker thread calls RegisterAudioCallback(nullptr), which overwrites the raw_ptr. This crucial step decrements the MiraclePtr (BackupRefPtr) reference count for the AudioTransportImpl allocation down to zero.
Subsequently, the worker thread drops the last reference to the AudioState object, causing its destruction. Because the MiraclePtr refcount is already zero, PartitionAlloc physically frees the memory back to the heap, completely bypassing the quarantine mechanism.
The attacker can then rapidly reallocate the newly freed heap chunk using WebAssembly or WebAudio heap spraying on another thread. When the preempted audio render thread resumes, it uses the previously extracted dangling raw pointer to invoke the virtual method PullRenderData() on the attacker-controlled memory chunk, leading to a virtual call hijack and Remote Code Execution (RCE) within the renderer sandbox.
Suggested Attacker Steps
Note: These are potential steps; our tooling agent cannot yet run code to verify them.
- Host a malicious webpage that executes JavaScript to create a WebRTC connection (
new RTCPeerConnection()) and add an audio track to initiate the audio rendering pipeline. - The browser’s native audio subsystem invokes
WebRtcAudioDeviceImpl::RenderData()on a high-priority real-time thread. - The attacker’s JavaScript asynchronously calls
pc.close()to abruptly terminate the WebRTC session, triggeringWebRtcVoiceEngine::Terminate()on the WebRTC worker thread. - The audio render thread evaluates
audio_transport_callback_->and extracts the raw pointer, but is then preempted by the OS scheduler. - The worker thread executes
Terminate(), setting the callback tonullptr(dropping the MiraclePtr refcount to 0) and destroying theAudioStateobject, physically freeing theAudioTransportImplmemory. - The attacker uses a concurrent worker thread to rapidly allocate memory (heap spraying), successfully reallocating the exact same memory chunk with attacker-controlled data (including a fake vtable).
- The audio render thread resumes, dereferences the dangling raw pointer, and executes the attacker-controlled virtual method, resulting in RCE.
Suggested Fix
Due to the real-time constraints of the audio render thread, extending the base::AutoLock auto_lock(lock_) scope to cover the entire RenderData() method (including the PullRenderData call) could introduce unacceptable latency or deadlocks if the WebRTC worker thread is heavily loaded. A better approach might involve holding a strong reference (scoped_refptr) to the AudioState (or the underlying transport) locally within the RenderData scope to ensure the object’s lifetime extends until the method completes, or utilizing a thread-safe mechanism to ensure the worker thread waits for the audio thread to exit its processing loop before destroying the object.
Evaluated with Chrome root at commit: e9e0fcbb690b1a8c1a26c81c2a9ea23d6e178368
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.