Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Media
DescriptionUse after free in Media
ComponentMedia
Bug ClassUAF
Tracker501546443
Fix commita65f69b772c0 (chromium/src) +22/-10
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/renderer/media/chrome_speech_recognition_client.cc
modified

Files Changed

  • chrome/renderer/media/chrome_speech_recognition_client.cc
  • chrome/renderer/media/chrome_speech_recognition_client.h
From a65f69b772c0cec8f2f8a337ad40a890589f50bf Mon Sep 17 00:00:00 2001
From: Evan Liu <evliu@google.com>
Date: Mon, 13 Apr 2026 17:44:17 -0700
Subject: [PATCH] Fix potential Use-After-Free data race in ChromeSpeechRecognitionClient

This CL fixes a cross-thread data race in ChromeSpeechRecognitionClient
where `on_ready_callback_` was being concurrently modified on the media
thread (via SetOnReadyCallback) and executed on the main thread
(via OnRecognizerBound) without synchronization.

To fix this, `on_ready_callback_` is now guarded by the existing
`is_recognizer_bound_lock_`. The callback is moved into a local
variable while holding the lock and then executed outside the lock to
prevent deadlocks.

Fixed: 501546443
Change-Id: I39a1bcc70d6a71edff5f88bd1d3155fe76563341
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7759475
Commit-Queue: Evan Liu <evliu@google.com>
Reviewed-by: Tommy Steimel <steimel@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1614101}
---

diff --git a/chrome/renderer/media/chrome_speech_recognition_client.cc b/chrome/renderer/media/chrome_speech_recognition_client.cc
index 7332f21..e34a8f2 100644
--- a/chrome/renderer/media/chrome_speech_recognition_client.cc
+++ b/chrome/renderer/media/chrome_speech_recognition_client.cc
@@ -102,11 +102,20 @@
 // existing callback.
 void ChromeSpeechRecognitionClient::SetOnReadyCallback(
     SpeechRecognitionClient::OnReadyCallback callback) {
-  on_ready_callback_ = std::move(callback);
+  SpeechRecognitionClient::OnReadyCallback callback_to_run;
+  {
+    base::AutoLock auto_lock(is_recognizer_bound_lock_);
+    on_ready_callback_ = std::move(callback);
 
-  // Immediately run the callback if speech recognition is already available.
-  if (IsSpeechRecognitionAvailable() && on_ready_callback_)
-    std::move(on_ready_callback_).Run();
+    // Immediately run the callback if speech recognition is already available.
+    if (is_recognizer_bound_ && on_ready_callback_) {
+      callback_to_run = std::move(on_ready_callback_);
+    }
+  }
+
+  if (callback_to_run) {
+    std::move(callback_to_run).Run();
+  }
 }
 
 void ChromeSpeechRecognitionClient::Reconfigure(
@@ -118,15 +127,17 @@
 
 void ChromeSpeechRecognitionClient::OnRecognizerBound(
     bool is_multichannel_supported) {
+  SpeechRecognitionClient::OnReadyCallback callback_to_run;
   {
     base::AutoLock auto_lock(is_recognizer_bound_lock_);
     is_recognizer_bound_ = true;
+    is_multichannel_supported_ = is_multichannel_supported;
+    callback_to_run = std::move(on_ready_callback_);
   }
 
-  is_multichannel_supported_ = is_multichannel_supported;
-
-  if (on_ready_callback_)
-    std::move(on_ready_callback_).Run();
+  if (callback_to_run) {
+    std::move(callback_to_run).Run();
+  }
 }
 
 void ChromeSpeechRecognitionClient::SpeechRecognitionAvailabilityChanged(
diff --git a/chrome/renderer/media/chrome_speech_recognition_client.h b/chrome/renderer/media/chrome_speech_recognition_client.h
index 20d2497..8d6ca69 100644
--- a/chrome/renderer/media/chrome_speech_recognition_client.h
+++ b/chrome/renderer/media/chrome_speech_recognition_client.h
@@ -105,8 +105,6 @@
 
   ChromeSpeechRecognitionClient::InitializeCallback initialize_callback_;
 
-  media::SpeechRecognitionClient::OnReadyCallback on_ready_callback_;
-
   base::RepeatingClosure reset_callback_;
 
   // Sends audio to the speech recognition thread on the renderer thread.
@@ -136,6 +134,9 @@
   // rendering threads concurrently.
   mutable base::Lock is_recognizer_bound_lock_;
 
+  media::SpeechRecognitionClient::OnReadyCallback on_ready_callback_
+      GUARDED_BY(is_recognizer_bound_lock_);
+
   // A flag indicating whether the speech recognition service supports
   // multichannel audio.
   bool is_multichannel_supported_ = false;
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential Use-After-Free via data race in ChromeSpeechRecognitionClient

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 Chrome Security team.

Overview: A cross-thread data race occurs in ChromeSpeechRecognitionClient due to unsynchronized access to the on_ready_callback_ member across the main and media threads. This race can cause a Use-After-Free of the callback’s internal BindStateBase object. A malicious webpage could potentially trigger this by rapidly toggling audio tracks to achieve renderer Remote Code Execution.

Affected files:

  • chrome/renderer/media/chrome_speech_recognition_client.cc
  • chrome/renderer/media/chrome_speech_recognition_client.h

Estimated timestamp from git blame: 2023-05-17

Summary

A cross-thread data race exists in ChromeSpeechRecognitionClient regarding the on_ready_callback_ member variable. This base::OnceCallback is concurrently modified on the media thread and executed on the main thread without synchronization. This race can lead to the premature destruction of the callback’s underlying BindStateBase object, resulting in a potential Use-After-Free (UAF).

Technical Details

  1. Media Thread Write: AudioRendererImpl::Initialize() runs on the media task runner and calls speech_recognition_client_->SetOnReadyCallback(...). In ChromeSpeechRecognitionClient::SetOnReadyCallback, the incoming callback is move-assigned to the on_ready_callback_ member variable without any locking.
  2. Main Thread Read/Execute: ChromeSpeechRecognitionClient::OnRecognizerBound() is triggered as a Mojo reply callback and executes on the main thread. It checks if (on_ready_callback_) and then consumes the callback via std::move(on_ready_callback_).Run().
  3. The Race Condition: base::OnceCallback encapsulates a scoped_refptr<internal::BindStateBase>. If the main thread attempts to std::move and run the callback at the exact same moment the media thread assigns a new callback to it, a data race occurs on the scoped_refptr’s internal raw pointer (ptr_).
  4. The UAF: Because the read/write of ptr_ is not atomic, the scoped_refptr’s reference count can drop to zero on the media thread (causing the BindStateBase to be deleted and freed), while the main thread still holds a dangling copy of the pointer. When the main thread subsequently attempts to invoke the callback, it dereferences this freed pointer to call the polymorphic_invoke_ function pointer.

Because scoped_refptr uses RAW_PTR_EXCLUSION for its internal pointer, this object is not protected by MiraclePtr (BackupRefPtr). Exploitation of this UAF allows direct control over an instruction pointer.

Potential Trigger Steps

Note: These are suggested steps; our tooling agent does not have the ability to run or verify arbitrary code execution.

  1. An attacker hosts a webpage containing an <audio> or <video> element with multiple audio tracks.
  2. The user visits the page with Live Caption or Speech Recognition enabled.
  3. The attacker’s JavaScript initiates a tight loop that rapidly toggles the active audio track (e.g., audioTracks[0].enabled = false then true).
  4. Each track change forces RendererImpl::ReinitializeAudioRenderer, scheduling a new AudioRendererImpl::Initialize() call on the media thread, which repeatedly overwrites on_ready_callback_.
  5. Simultaneously, the browser process replies to the BindRecognizer Mojo request, scheduling OnRecognizerBound on the main thread.
  6. The attacker synchronizes the track toggling such that the callback assignment on the media thread collides with the callback execution on the main thread.
  7. The attacker utilizes JavaScript heap spraying to reclaim the freed BindStateBase memory region and populate it with a fake object and a controlled polymorphic_invoke_ pointer, hijacking control flow when the main thread executes the callback.

Suggested Fix

Access to on_ready_callback_ must be synchronized. A straightforward fix is to introduce a base::Lock (or reuse the existing is_recognizer_bound_lock_) to protect all reads, writes, and moves of on_ready_callback_.

For example, in OnRecognizerBound:

SpeechRecognitionClient::OnReadyCallback callback_to_run;
{
  base::AutoLock auto_lock(is_recognizer_bound_lock_);
  is_recognizer_bound_ = true;
  callback_to_run = std::move(on_ready_callback_);
}

is_multichannel_supported_ = is_multichannel_supported;

if (callback_to_run)
  std::move(callback_to_run).Run();

And similarly wrap the assignment in SetOnReadyCallback within the same lock.

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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