CVE-2026-11130
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/renderer/media/chrome_speech_recognition_client.cc |
modified |
Files Changed
chrome/renderer/media/chrome_speech_recognition_client.ccchrome/renderer/media/chrome_speech_recognition_client.h
Patch
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;
Original Bug Report
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.ccchrome/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
- Media Thread Write:
AudioRendererImpl::Initialize()runs on the media task runner and callsspeech_recognition_client_->SetOnReadyCallback(...). InChromeSpeechRecognitionClient::SetOnReadyCallback, the incoming callback is move-assigned to theon_ready_callback_member variable without any locking. - Main Thread Read/Execute:
ChromeSpeechRecognitionClient::OnRecognizerBound()is triggered as a Mojo reply callback and executes on the main thread. It checksif (on_ready_callback_)and then consumes the callback viastd::move(on_ready_callback_).Run(). - The Race Condition:
base::OnceCallbackencapsulates ascoped_refptr<internal::BindStateBase>. If the main thread attempts tostd::moveand run the callback at the exact same moment the media thread assigns a new callback to it, a data race occurs on thescoped_refptr’s internal raw pointer (ptr_). - The UAF: Because the read/write of
ptr_is not atomic, thescoped_refptr’s reference count can drop to zero on the media thread (causing theBindStateBaseto 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 thepolymorphic_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.
- An attacker hosts a webpage containing an
<audio>or<video>element with multiple audio tracks. - The user visits the page with Live Caption or Speech Recognition enabled.
- The attacker’s JavaScript initiates a tight loop that rapidly toggles the active audio track (e.g.,
audioTracks[0].enabled = falsethentrue). - Each track change forces
RendererImpl::ReinitializeAudioRenderer, scheduling a newAudioRendererImpl::Initialize()call on the media thread, which repeatedly overwriteson_ready_callback_. - Simultaneously, the browser process replies to the
BindRecognizerMojo request, schedulingOnRecognizerBoundon the main thread. - The attacker synchronizes the track toggling such that the callback assignment on the media thread collides with the callback execution on the main thread.
- The attacker utilizes JavaScript heap spraying to reclaim the freed
BindStateBasememory region and populate it with a fake object and a controlledpolymorphic_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.