Medium patch-now CVSS 8.8 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in WebRTC
DescriptionUse after free in WebRTC
ComponentWebRTC
Bug ClassUAF
Action tierpatch-now — corruption primitive, web-reachable in default config (unverified)
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker498696266
Fix commita0f3b94ca37f (chromium/src) +21/-9
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-01 (per vendor advisory)
Fix landed2026-04-02 (public git)
Disclosed2026-05-05
Patch gap33 days — fix public in source before the advisory

Background

`WebRtcAudioDeviceImpl`
Blink’s WebRTC audio device (webrtc_audio_device_impl.cc/.h) bridging the audio render thread and WebRTC’s AudioTransport callback.
`audio_transport_callback_`
a raw_ptr<webrtc::AudioTransport> the render thread calls (PullRenderData) to obtain audio, set/cleared on the signaling thread via RegisterAudioCallback.
Cross-thread access
the pointer is read/used on the real-time audio render thread but mutated on the signaling thread, so it needs synchronization to avoid racing a reset against a use.
`DanglingUntriaged` / `raw_ptr`
a MiraclePtr annotation marking a raw pointer previously known to dangle; its presence signals a known lifetime hazard on this member.

Root Cause Analysis

audio_transport_callback_ was read and dereferenced on the audio render thread (audio_transport_callback_->PullRenderData(...)) while it could be concurrently modified or reset to a new/null value on the signaling thread via RegisterAudioCallback, with no lock protecting the member — the pre-patch render path merely DCHECKed the pointer non-null and then called through it. This data race means the render thread could dereference the pointer after the signaling thread reset it (and after the underlying AudioTransport was torn down), a use-after-free; the member was even annotated raw_ptr<webrtc::AudioTransport, DanglingUntriaged>, marking a known dangling hazard.

The fix introduces a dedicated base::Lock audio_transport_callback_lock_, marks the member GUARDED_BY(audio_transport_callback_lock_), and drops the DanglingUntriaged annotation. The render path now takes base::AutoLock callback_lock(audio_transport_callback_lock_) and only calls PullRenderData inside if (audio_transport_callback_), while RegisterAudioCallback, StartPlayout, and the recording-start path all acquire the same lock before touching the member. Holding one lock across both the use and the reset serializes them, so the render thread can never dereference a callback that is being (or has been) cleared.

Key insight
A cross-thread raw_ptr callback was used on the render thread without synchronization against its reset on the signaling thread; the fix adds audio_transport_callback_lock_ guarding every read and write and null-checks the pointer under the lock, eliminating the race-induced use-after-free.

Attack Path

  1. Establish a WebRTC audio session A page sets up WebRTC audio so the render thread is actively pulling data via audio_transport_callback_->PullRenderData.
  2. Reset the callback concurrently On the signaling thread, RegisterAudioCallback resets/clears audio_transport_callback_ (e.g. during teardown or renegotiation) while the render thread is mid-use.
  3. Race the unlocked access With no shared lock, the render thread reads the stale pointer value after the reset and tearing down of the underlying AudioTransport.
  4. Use-after-free The render thread dereferences the freed AudioTransport in PullRenderData, the use-after-free named by the advisory (exploitation requires winning the thread race; the diff shows the missing synchronization, not a concrete primitive).

Impact Assessment

A page driving WebRTC audio can race callback teardown against the audio render thread to dereference a freed AudioTransport, yielding a use-after-free in the renderer process; because it depends on a thread timing window, triggering is non-deterministic. The vendor advisory rates this medium severity (“Use after free in WebRTC”) and does not report in-the-wild exploitation. Any escalation beyond the memory-safety fault is (inferred) and not demonstrated by the diff.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.cc
modified
WebRtcAudioDeviceImplTest
third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_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.h
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl_test.cc

Audit Directions

  • Cross-thread raw pointers to external objects
    Audit members read on a real-time/render thread but set on another thread; each needs a lock or atomic plus a null-check at the use site.
  • `DanglingUntriaged` annotations
    Treat raw_ptr<..., DanglingUntriaged> as a to-do list of known lifetime hazards and verify each has real synchronization or ownership, not just a DCHECK.
  • `DCHECK`-only non-null guards before dereference
    Replace release-stripped DCHECK(ptr) guards preceding a cross-thread dereference with a lock-held runtime null-check, since the DCHECK neither synchronizes nor protects release builds.
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
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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.cc
  • third_party/blink/renderer/modules/webrtc/webrtc_audio_device_impl.h
  • third_party/webrtc/media/engine/webrtc_voice_engine.cc
  • third_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.

  1. 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.
  2. The browser’s native audio subsystem invokes WebRtcAudioDeviceImpl::RenderData() on a high-priority real-time thread.
  3. The attacker’s JavaScript asynchronously calls pc.close() to abruptly terminate the WebRTC session, triggering WebRtcVoiceEngine::Terminate() on the WebRTC worker thread.
  4. The audio render thread evaluates audio_transport_callback_-> and extracts the raw pointer, but is then preempted by the OS scheduler.
  5. The worker thread executes Terminate(), setting the callback to nullptr (dropping the MiraclePtr refcount to 0) and destroying the AudioState object, physically freeing the AudioTransportImpl memory.
  6. 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).
  7. 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.

View on issue tracker