Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in GetUserMedia
DescriptionInappropriate implementation in GetUserMedia
ComponentGetUserMedia
Bug ClassLogic Error
Tracker523505418
Fix commit1787d529449a (chromium/src) +62/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-08

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/renderer_host/media/audio_input_device_manager_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/media/audio_input_device_manager.cc
  • content/browser/renderer_host/media/audio_input_device_manager.h
  • content/browser/renderer_host/media/audio_input_device_manager_unittest.cc
From 1787d529449a9e5c1d4aa4505f34fe98f6307c20 Mon Sep 17 00:00:00 2001
From: Tove Petersson <[email protected]>
Date: Mon, 29 Jun 2026 07:31:33 -0700
Subject: [PATCH] Abort pending AudioInputDeviceManager open when closed early

AudioInputDeviceManager::Open() returns a session id immediately and
queries the audio system asynchronously. If Close() is called for that
session before the query completes, the device is not yet in |devices_|
so Close() returns without doing anything, and the later
OpenedOnIOThread() callback still registers the device, leaving a
session that MediaStreamManager no longer tracks.

Track sessions whose open is still in flight and drop the result of
OpenedOnIOThread() when Close() has already been called for the session.

Bug: 523505418
Change-Id: Ib64b19cbf2fb3077bae7107b11d0570d4551efde
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8004471
Commit-Queue: Tove Petersson <[email protected]>
Reviewed-by: Guido Urdaneta <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1654054}
---

diff --git a/content/browser/renderer_host/media/audio_input_device_manager.cc b/content/browser/renderer_host/media/audio_input_device_manager.cc
index 2723decb..3d82f8e96 100644
--- a/content/browser/renderer_host/media/audio_input_device_manager.cc
+++ b/content/browser/renderer_host/media/audio_input_device_manager.cc
@@ -110,6 +110,7 @@
   // Generate a new id for this device.
   auto session_id = base::UnguessableToken::Create();
   SendAudioLogMessage(GetOpenLogString(session_id, device));
+  pending_open_sessions_.insert(session_id);
 
   // base::Unretained(this) is safe, because AudioInputDeviceManager is
   // destroyed not earlier than on the IO message loop destruction.
@@ -143,8 +144,12 @@
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
   SendAudioLogMessage("Close({session_id=" + session_id.ToString() + "})");
   auto device = GetDevice(session_id);
-  if (device == devices_.end())
+  if (device == devices_.end()) {
+    // The asynchronous device query started by Open() may not have completed
+    // yet. Drop the session so that OpenedOnIOThread() does not register it.
+    pending_open_sessions_.erase(session_id);
     return;
+  }
   const blink::mojom::MediaStreamType stream_type = device->type;
   devices_.erase(device);
 
@@ -161,10 +166,16 @@
     const std::optional<media::AudioParameters>& input_params,
     const std::optional<std::string>& matched_output_device_id) {
   DCHECK_CURRENTLY_ON(BrowserThread::IO);
-  DCHECK(GetDevice(session_id) == devices_.end());
   DCHECK(!input_params || input_params->IsValid());
   DCHECK(!matched_output_device_id || !matched_output_device_id->empty());
 
+  if (!pending_open_sessions_.erase(session_id)) {
+    // The session was closed while the device query was in flight.
+    return;
+  }
+
+  DCHECK(GetDevice(session_id) == devices_.end());
+
   SendAudioLogMessage("Opened({session_id=" + session_id.ToString() + "})");
   blink::MediaStreamDevice media_stream_device(device.type, device.id,
                                                device.name);
diff --git a/content/browser/renderer_host/media/audio_input_device_manager.h b/content/browser/renderer_host/media/audio_input_device_manager.h
index a632e8b..4459034 100644
--- a/content/browser/renderer_host/media/audio_input_device_manager.h
+++ b/content/browser/renderer_host/media/audio_input_device_manager.h
@@ -16,6 +16,7 @@
 #include <string>
 #include <vector>
 
+#include "base/containers/flat_set.h"
 #include "base/memory/raw_ptr.h"
 #include "base/observer_list.h"
 #include "base/threading/thread.h"
@@ -74,6 +75,12 @@
   base::ObserverList<MediaStreamProviderListener>::Unchecked listeners_;
   blink::MediaStreamDevices devices_;
 
+  // Sessions for which Open() has been called and the asynchronous device
+  // query is still in flight. A session is removed from this set either when
+  // OpenedOnIOThread() runs or when Close() is called for it before that
+  // happens, in which case OpenedOnIOThread() will discard the result.
+  base::flat_set<base::UnguessableToken> pending_open_sessions_;
+
   const raw_ptr<media::AudioSystem> audio_system_;
 };
 
diff --git a/content/browser/renderer_host/media/audio_input_device_manager_unittest.cc b/content/browser/renderer_host/media/audio_input_device_manager_unittest.cc
index 231e9b7d..21978ff 100644
--- a/content/browser/renderer_host/media/audio_input_device_manager_unittest.cc
+++ b/content/browser/renderer_host/media/audio_input_device_manager_unittest.cc
@@ -28,6 +28,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/mediastream/media_stream_request.h"
 
+using testing::_;
 using testing::InSequence;
 
 namespace content {
@@ -378,4 +379,45 @@
   }
 }
 
+// Closes a session while its asynchronous open is still pending. The pending
+// open should be aborted and the session should never be registered.
+TEST_F(AudioInputDeviceManagerNoDevicesTest, CloseWhileOpenIsPending) {
+  ASSERT_FALSE(devices_.empty());
+
+  base::UnguessableToken session_id = manager_->Open(devices_.front());
+  manager_->Close(session_id);
+
+  EXPECT_CALL(*audio_input_listener_, Opened(_, session_id)).Times(0);
+  WaitForOpenCompletion();
+
+  EXPECT_EQ(nullptr, manager_->GetOpenedDeviceById(session_id));
+}
+
+// Closes one of two pending sessions. The other session should still open
+// normally.
+TEST_F(AudioInputDeviceManagerNoDevicesTest,
+       CloseOneOfMultiplePendingSessions) {
+  ASSERT_GE(devices_.size(), 2u);
+
+  base::UnguessableToken first_session_id = manager_->Open(devices_[0]);
+  base::UnguessableToken second_session_id = manager_->Open(devices_[1]);
+  manager_->Close(first_session_id);
+
+  EXPECT_CALL(*audio_input_listener_, Opened(_, first_session_id)).Times(0);
+  EXPECT_CALL(*audio_input_listener_,
+              Opened(devices_[1].type, second_session_id))
+      .Times(1);
+  WaitForOpenCompletion();
+
+  EXPECT_EQ(nullptr, manager_->GetOpenedDeviceById(first_session_id));
+  EXPECT_NE(nullptr, manager_->GetOpenedDeviceById(second_session_id));
+
+  base::RunLoop run_loop;
+  EXPECT_CALL(*audio_input_listener_,
+              Closed(devices_[1].type, second_session_id))
+      .WillOnce(testing::InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
+  manager_->Close(second_session_id);
+  run_loop.Run();
+}
+
 }  // namespace content
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/media/audio_input_device_manager_unittest.cc b/content/browser/renderer_host/media/audio_input_device_manager_unittest.cc
index 231e9b7d..21978ff 100644
--- a/content/browser/renderer_host/media/audio_input_device_manager_unittest.cc
+++ b/content/browser/renderer_host/media/audio_input_device_manager_unittest.cc
@@ -28,6 +28,7 @@
 #include "testing/gtest/include/gtest/gtest.h"
 #include "third_party/blink/public/common/mediastream/media_stream_request.h"
 
+using testing::_;
 using testing::InSequence;
 
 namespace content {
@@ -378,4 +379,45 @@
   }
 }
 
+// Closes a session while its asynchronous open is still pending. The pending
+// open should be aborted and the session should never be registered.
+TEST_F(AudioInputDeviceManagerNoDevicesTest, CloseWhileOpenIsPending) {
+  ASSERT_FALSE(devices_.empty());
+
+  base::UnguessableToken session_id = manager_->Open(devices_.front());
+  manager_->Close(session_id);
+
+  EXPECT_CALL(*audio_input_listener_, Opened(_, session_id)).Times(0);
+  WaitForOpenCompletion();
+
+  EXPECT_EQ(nullptr, manager_->GetOpenedDeviceById(session_id));
+}
+
+// Closes one of two pending sessions. The other session should still open
+// normally.
+TEST_F(AudioInputDeviceManagerNoDevicesTest,
+       CloseOneOfMultiplePendingSessions) {
+  ASSERT_GE(devices_.size(), 2u);
+
+  base::UnguessableToken first_session_id = manager_->Open(devices_[0]);
+  base::UnguessableToken second_session_id = manager_->Open(devices_[1]);
+  manager_->Close(first_session_id);
+
+  EXPECT_CALL(*audio_input_listener_, Opened(_, first_session_id)).Times(0);
+  EXPECT_CALL(*audio_input_listener_,
+              Opened(devices_[1].type, second_session_id))
+      .Times(1);
+  WaitForOpenCompletion();
+
+  EXPECT_EQ(nullptr, manager_->GetOpenedDeviceById(first_session_id));
+  EXPECT_NE(nullptr, manager_->GetOpenedDeviceById(second_session_id));
+
+  base::RunLoop run_loop;
+  EXPECT_CALL(*audio_input_listener_,
+              Closed(devices_[1].type, second_session_id))
+      .WillOnce(testing::InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
+  manager_->Close(second_session_id);
+  run_loop.Run();
+}
+
 }  // namespace content
Loading diff…

Original Bug Report

reported by [email protected]

Unauthorized silent audio capture via MediaStreamManager race condition

Flapjack, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A potential race condition in AudioInputDeviceManager and MediaStreamManager allows a compromised renderer to bypass audio capture security controls. By cancelling a stream request immediately after creating it, a renderer can leak a valid session ID and create an orphaned session. This session can then be used to silently record audio without user notification or permission checks.

Affected files:

  • content/browser/renderer_host/media/audio_input_device_manager.cc
  • content/browser/renderer_host/media/audio_input_device_manager.h

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

A potential race condition in the browser’s handling of audio stream generation allows a compromised renderer to bypass security controls and persistently record audio without any user-facing indicators (like the Omnibox red dot). By cancelling a GenerateStreams request precisely while the browser is querying the OS for audio device parameters, the renderer can induce the browser to leak a valid session_id while failing to clean up the pending session. The renderer can subsequently use this leaked session_id to establish an unauthorized audio stream.

Vulnerability Details

When a renderer requests an audio stream, the browser initiates a sequence that involves an asynchronous call to the underlying audio system. A compromised renderer can race a CancelRequest against this asynchronous operation to leave the system in a vulnerable state.

Suggested steps to trigger this potential vulnerability:

  1. Initiate Request: A compromised renderer sends a GenerateStreams IPC to the browser.
  2. Browser Processing: MediaStreamManager::HandleAccessRequestResponse calls AudioInputDeviceManager::Open. This generates a new session_id and starts an asynchronous OS query (audio_system_->GetInputDeviceInfo), passing AudioInputDeviceManager::OpenedOnIOThread as the callback.
  3. Early Return: AudioInputDeviceManager::Open immediately returns the session_id to MediaStreamManager before the async query completes. The device state is set to MEDIA_REQUEST_STATE_OPENING.
  4. Race Condition Trigger: The compromised renderer immediately sends a CancelRequest IPC.
  5. Failed Cleanup: MediaStreamManager::CancelRequest calls AudioInputDeviceManager::Close(session_id). However, because the async query hasn’t finished, the session is not yet in the devices_ vector. Close returns early without cleaning up or invalidating the pending callback (audio_input_device_manager.cc:145-147).
  6. ID Leak: MediaStreamManager executes the device_stopped_callback to notify the renderer that the stream is closing. This intentionally sends the valid session_id back to the renderer via the MediaStreamDeviceObserver::OnDeviceStopped IPC (media_stream_manager.cc:2078).
  7. Request Deletion: MediaStreamManager deletes the request from its requests_ map, tearing down any associated UI proxies. The browser believes the request is fully cancelled.
  8. Zombie Session: Sometime later, the async audio query completes. AudioInputDeviceManager::OpenedOnIOThread executes and unconditionally adds the session to its devices_ vector (audio_input_device_manager.cc:178). The session is now active but untracked by MediaStreamManager.
  9. Stream Creation: The compromised renderer uses the leaked session_id to call RenderFrameAudioInputStreamFactory::CreateStream.
  10. Validation Bypass: CreateStream attempts to validate the request via MediaStreamManager::ValidateAudioSession. This relies on ValidateSession, which looks up the session_id in the requests_ map. Because the request was deleted in Step 7, it is not found. Due to a logic flaw intended to handle benign race conditions (if (!request) { return true; }), validation incorrectly passes (media_stream_manager.cc:2378-2380).
  11. Silent Recording: CreateStream finds the orphaned device in AudioInputDeviceManager and opens the stream. Because the request was deleted, no UI proxy exists, meaning no recording indicators are shown to the user, and revoking permissions has no effect.

Suggested Fix

There are multiple ways to address this issue:

  1. Track Pending Sessions in AIDM: AudioInputDeviceManager should keep track of pending asynchronous open requests. If Close is called for a pending session_id, it should record the cancellation and ensure OpenedOnIOThread does not add the device to the devices_ list when the callback eventually fires.
  2. Fix Session Validation: The return true logic in MediaStreamManager::ValidateSession is dangerous. If a session is truly untracked, it should return false. However, this requires careful handling to avoid breaking legitimate race conditions where a stream is closing.
  3. Prevent ID Leakage: If a request is cancelled while in MEDIA_REQUEST_STATE_OPENING, do not execute the device_stopped_callback, or do not include the valid session_id in the payload, as the device was never fully handed to the renderer.

The most robust fix is likely #1: ensuring AudioInputDeviceManager correctly aborts pending sessions when Close is called.

Evaluated with Chrome root at commit: 65b3256311f3ab6fb9870eaa522de7e6dd2663bb


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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