High watch CVSS 8.1 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Chromoting
DescriptionUse after free in Chromoting
ComponentChromoting
Sub-componentRemoting
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker515470739
Fix commit8c09594f1317 (chromium/src) +193/-35
CWECWE-416 (Use-after-free)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-21 (per vendor advisory)
Fix landed2026-07-24 (public git)
Disclosed2026-08-25
Patch gap32 days — fix public in source before the advisory

Background

`IMMNotificationClient`
A Windows Core Audio COM interface an application implements to receive notifications (e.g. default-device changes) after registering with an IMMDeviceEnumerator via RegisterEndpointNotificationCallback.
COM reference counting
The IUnknown AddRef()/Release() protocol by which the system and callers keep a COM object alive; when correctly implemented, an object is destroyed only after the last reference is released.
`Microsoft::WRL::RuntimeClass` / `ComPtr` / `Make`
WRL helpers that provide a standard ref-counted COM implementation, a smart pointer that maintains references, and a factory that allocates such objects.
`DefaultAudioDeviceChangeDetector`
The remoting host class that watches for default-audio-device changes; it is registered with the enumerator and receives callbacks like OnDefaultDeviceChanged.

Root Cause Analysis

DefaultAudioDeviceChangeDetector implemented IMMNotificationClient but stubbed its lifetime methods: AddRef() and Release() returned a constant 1 and never affected the object’s lifetime, and QueryInterface was hand-rolled. The object was owned by a std::unique_ptr<DefaultAudioDeviceChangeDetector> in AudioCapturerWin, and unregistration happened in the destructor. Because the stubbed ref-counting could not keep the object alive, when AudioCapturerWin::Deinitialize() reset the unique_ptr the detector was freed even though the audio system might still hold a reference to it and have a notification (e.g. OnDefaultDeviceChanged) pending; delivering that callback to the freed object is a use-after-free.

The fix converts the class to a Microsoft::WRL::RuntimeClass<...ClassicCom..., IMMNotificationClient> so it gets real COM ref-counting, holds it via Microsoft::WRL::ComPtr created with Microsoft::WRL::Make, and moves unregistration into an explicit Unregister() that the owner calls before releasing its reference — so the enumerator drops its reference and any still-pending system reference keeps the object alive until it is actually done.

Key insight
The core mistake was faking COM lifetime management (AddRef/Release hardwired to 1) while a unique_ptr unilaterally freed the object, so the audio system’s outstanding reference could outlive the allocation; the fix restores genuine COM ref-counting plus explicit Unregister(), guaranteeing the object survives until every reference — including the system’s — is released.

Attack Path

  1. Detector registered with the system AudioCapturerWin::Initialize creates a DefaultAudioDeviceChangeDetector and registers it as an IMMNotificationClient with the OS audio enumerator, which may take its own reference and queue notifications.
  2. System queues a late notification A default-audio-device change (which can be induced by device/endpoint state changes) causes the OS to hold a reference and prepare to invoke a callback such as OnDefaultDeviceChanged.
  3. Owner frees the object during teardown AudioCapturerWin::Deinitialize() calls default_device_detector_.reset(); because Release() was a no-op stub, the unique_ptr destroys the object while the system’s queued notification still points at it.
  4. Use-after-free on callback delivery The system delivers the pending notification to the now-freed detector, dereferencing freed memory; the shipped OutlivesOwnerWhileSystemHoldsReference test reproduces exactly this ordering. Any further control of the freed slot to escalate to corruption or execution is not shown by the diff. (inferred)

Impact Assessment

An attacker able to trigger audio default-device change notifications while the remoting host tears down its audio capturer can cause a use-after-free in the Chromoting host process, potentially leading to memory corruption or code execution in that process. The advisory classifies this as a use-after-free in Chromoting with no exploited-in-the-wild status stated; escalation beyond the freed-object callback demonstrated by the test is (inferred).

Changed Functions

FunctionChangeNotes
if
remoting/host/audio_capturer_win.cc
modified
if
remoting/host/win/default_audio_device_change_detector.cc
modified
DefaultAudioDeviceChangeDetector
remoting/host/win/default_audio_device_change_detector.h
modified
FakeMMDeviceEnumerator
remoting/host/win/default_audio_device_change_detector_unittest.cc
modified
DefaultAudioDeviceChangeDetectorTest
remoting/host/win/default_audio_device_change_detector_unittest.cc
modified
TEST_F
remoting/host/win/default_audio_device_change_detector_unittest.cc
modified

Files Changed

  • remoting/host/win/BUILD.gn
  • remoting/host/win/default_audio_device_change_detector_unittest.cc
  • remoting/host/audio_capturer_win.h
  • remoting/host/win/default_audio_device_change_detector.h
  • remoting/host/win/default_audio_device_change_detector.cc
  • remoting/host/audio_capturer_win.cc

Audit Directions

  • Stubbed COM lifetime methods
    Flag any COM interface implementation whose AddRef/Release return a constant or are no-ops; such objects cannot honor references taken by the system or other callers.
  • `unique_ptr`-owned callback objects
    Look for objects registered with the OS or another subsystem as a callback/observer while owned by a unique_ptr (or otherwise freed unilaterally), where teardown can race a pending in-flight notification.
  • Unregister-before-free ordering
    Audit destructors that both unregister a callback and free the object; unregistration must complete (and any outstanding references must be accounted for) before the memory is released, ideally via explicit Unregister() plus ref-counting.
From 8c09594f13179278e04d4a558cf90c12725db66c Mon Sep 17 00:00:00 2001
From: Yuwei Huang <yuweih@chromium.org>
Date: Thu, 23 Jul 2026 17:44:10 -0700
Subject: [PATCH] [remoting] Use COM ref-counting for audio device change detector

DefaultAudioDeviceChangeDetector implements IMMNotificationClient but
stubbed AddRef()/Release() and was owned by a unique_ptr, so system
references could not keep it alive while notifications were pending
after AudioCapturerWin released it.

Convert the class to a Microsoft::WRL::RuntimeClass with ClassicCom
flags so it gets standard COM reference counting, hold it via ComPtr in
AudioCapturerWin, and move unregistration to an explicit Unregister()
method that the owner calls before releasing its reference. Add unit
tests covering registration, GetAndReset, and late notification safety.

Bug: 515470739
Change-Id: I8b249149e6e54b85daf058d675bca29082e6d416
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8140581
Commit-Queue: Yuwei Huang <yuweih@chromium.org>
Auto-Submit: Yuwei Huang <yuweih@chromium.org>
Reviewed-by: Joe Downing <joedow@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1667543}
---
diff --git a/remoting/host/audio_capturer_win.cc b/remoting/host/audio_capturer_win.cc
index 252412b2..2a17bdea 100644
--- a/remoting/host/audio_capturer_win.cc
+++ b/remoting/host/audio_capturer_win.cc
@@ -87,7 +87,10 @@
 void AudioCapturerWin::Deinitialize() {
   DCHECK(thread_checker_.CalledOnValidThread());
   wave_format_ex_.Reset(nullptr);
-  default_device_detector_.reset();
+  if (default_device_detector_) {
+    default_device_detector_->Unregister();
+  }
+  default_device_detector_.Reset();
   audio_capture_client_.Reset();
   if (audio_client_) {
     audio_client_->Stop();
@@ -113,7 +116,8 @@
   }
 
   default_device_detector_ =
-      std::make_unique<DefaultAudioDeviceChangeDetector>(mm_device_enumerator);
+      Microsoft::WRL::Make<DefaultAudioDeviceChangeDetector>(
+          mm_device_enumerator);
 
   // Get the audio endpoint.
   hr = mm_device_enumerator->GetDefaultAudioEndpoint(eRender, eConsole,
diff --git a/remoting/host/audio_capturer_win.h b/remoting/host/audio_capturer_win.h
index 168b922..396b2b5d 100644
--- a/remoting/host/audio_capturer_win.h
+++ b/remoting/host/audio_capturer_win.h
@@ -77,7 +77,8 @@
   Microsoft::WRL::ComPtr<IAudioClient> audio_client_;
   Microsoft::WRL::ComPtr<IMMDevice> mm_device_;
 
-  std::unique_ptr<DefaultAudioDeviceChangeDetector> default_device_detector_;
+  Microsoft::WRL::ComPtr<DefaultAudioDeviceChangeDetector>
+      default_device_detector_;
 
   HRESULT last_capture_error_;
 
diff --git a/remoting/host/win/default_audio_device_change_detector.cc b/remoting/host/win/default_audio_device_change_detector.cc
index dbd76f5..4115415 100644
--- a/remoting/host/win/default_audio_device_change_detector.cc
+++ b/remoting/host/win/default_audio_device_change_detector.cc
@@ -4,8 +4,7 @@
 
 #include "remoting/host/win/default_audio_device_change_detector.h"
 
-#include <unknwn.h>
-
+#include "base/check.h"
 #include "base/logging.h"
 
 namespace remoting {
@@ -21,10 +20,18 @@
     LOG(WARNING) << "Failed to register IMMNotificationClient, we may not be "
                     "able to detect the new default audio device. Error "
                  << hr;
+    return;
   }
+  registered_ = true;
 }
 
-DefaultAudioDeviceChangeDetector::~DefaultAudioDeviceChangeDetector() {
+DefaultAudioDeviceChangeDetector::~DefaultAudioDeviceChangeDetector() = default;
+
+void DefaultAudioDeviceChangeDetector::Unregister() {
+  if (!registered_) {
+    return;
+  }
+  registered_ = false;
   enumerator_->UnregisterEndpointNotificationCallback(this);
 }
 
@@ -49,16 +56,6 @@
   return S_OK;
 }
 
-HRESULT DefaultAudioDeviceChangeDetector::QueryInterface(REFIID iid,
-                                                         void** object) {
-  if (iid == IID_IUnknown || iid == __uuidof(IMMNotificationClient)) {
-    *object = static_cast<IMMNotificationClient*>(this);
-    return S_OK;
-  }
-  *object = nullptr;
-  return E_NOINTERFACE;
-}
-
 HRESULT DefaultAudioDeviceChangeDetector::OnDeviceAdded(LPCWSTR pwstrDeviceId) {
   return S_OK;
 }
@@ -80,12 +77,4 @@
   return S_OK;
 }
 
-ULONG DefaultAudioDeviceChangeDetector::AddRef() {
-  return 1;
-}
-
-ULONG DefaultAudioDeviceChangeDetector::Release() {
-  return 1;
-}
-
 }  // namespace remoting
diff --git a/remoting/host/win/default_audio_device_change_detector.h b/remoting/host/win/default_audio_device_change_detector.h
index b8cb7d8..d04dfab 100644
--- a/remoting/host/win/default_audio_device_change_detector.h
+++ b/remoting/host/win/default_audio_device_change_detector.h
@@ -6,23 +6,39 @@
 #define REMOTING_HOST_WIN_DEFAULT_AUDIO_DEVICE_CHANGE_DETECTOR_H_
 
 #include <mmdeviceapi.h>
+
 #include <wrl/client.h>
+#include <wrl/implements.h>
 
 #include "base/synchronization/lock.h"
 
 namespace remoting {
 
 // An IMMNotificationClient implementation to detect the change of the default
-// audio output device on the system. It registers itself into the input
-// IMMDeviceEnumerator in constructor and unregisters in destructor.
-// This class does not use the default ref-counting memory management method
-// provided by IUnknown: calling DefaultAudioDeviceChangeDetector::Release()
-// won't delete the object.
-class DefaultAudioDeviceChangeDetector final : public IMMNotificationClient {
+// audio output device on the system. It registers itself with the input
+// IMMDeviceEnumerator in the constructor. The owner must call Unregister()
+// before releasing its reference so that the enumerator releases its own
+// reference to this object. Instances must be created with
+// Microsoft::WRL::Make and held in a Microsoft::WRL::ComPtr so that the
+// instance is kept alive while the system holds outstanding references.
+class DefaultAudioDeviceChangeDetector final
+    : public Microsoft::WRL::RuntimeClass<
+          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+          IMMNotificationClient> {
  public:
   explicit DefaultAudioDeviceChangeDetector(
       const Microsoft::WRL::ComPtr<IMMDeviceEnumerator>& enumerator);
-  ~DefaultAudioDeviceChangeDetector();
+
+  DefaultAudioDeviceChangeDetector(const DefaultAudioDeviceChangeDetector&) =
+      delete;
+  DefaultAudioDeviceChangeDetector& operator=(
+      const DefaultAudioDeviceChangeDetector&) = delete;
+
+  ~DefaultAudioDeviceChangeDetector() override;
+
+  // Unregisters this object from the IMMDeviceEnumerator. Must be called by
+  // the owner before releasing its reference.
+  void Unregister();
 
   bool GetAndReset();
 
@@ -32,8 +48,6 @@
                                            ERole role,
                                            LPCWSTR pwstrDefaultDevice) override;
 
-  HRESULT __stdcall QueryInterface(REFIID iid, void** object) override;
-
   // No-ops overrides.
   HRESULT __stdcall OnDeviceAdded(LPCWSTR pwstrDeviceId) override;
   HRESULT __stdcall OnDeviceRemoved(LPCWSTR pwstrDeviceId) override;
@@ -41,10 +55,9 @@
                                          DWORD dwNewState) override;
   HRESULT __stdcall OnPropertyValueChanged(LPCWSTR pwstrDeviceId,
                                            const PROPERTYKEY key) override;
-  ULONG __stdcall AddRef() override;
-  ULONG __stdcall Release() override;
 
   const Microsoft::WRL::ComPtr<IMMDeviceEnumerator> enumerator_;
+  bool registered_ = false;
   bool changed_ = false;
   base::Lock lock_;
 };
diff --git a/remoting/host/win/default_audio_device_change_detector_unittest.cc b/remoting/host/win/default_audio_device_change_detector_unittest.cc
new file mode 100644
index 0000000..be9c675
--- /dev/null
+++ b/remoting/host/win/default_audio_device_change_detector_unittest.cc
@@ -0,0 +1,150 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/host/win/default_audio_device_change_detector.h"
+
+#include <mmdeviceapi.h>
+
+#include <wrl/client.h>
+#include <wrl/implements.h>
+
+#include "base/win/scoped_com_initializer.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace remoting {
+
+namespace {
+
+class FakeMMDeviceEnumerator final
+    : public Microsoft::WRL::RuntimeClass<
+          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+          IMMDeviceEnumerator> {
+ public:
+  FakeMMDeviceEnumerator() = default;
+
+  FakeMMDeviceEnumerator(const FakeMMDeviceEnumerator&) = delete;
+  FakeMMDeviceEnumerator& operator=(const FakeMMDeviceEnumerator&) = delete;
+
+  ~FakeMMDeviceEnumerator() override = default;
+
+  Microsoft::WRL::ComPtr<IMMNotificationClient> client() const {
+    return client_;
+  }
+
+  // IMMDeviceEnumerator implementation.
+  HRESULT __stdcall EnumAudioEndpoints(
+      EDataFlow dataFlow,
+      DWORD dwStateMask,
+      IMMDeviceCollection** ppDevices) override;
+  HRESULT __stdcall GetDefaultAudioEndpoint(EDataFlow dataFlow,
+                                            ERole role,
+                                            IMMDevice** ppEndpoint) override;
+  HRESULT __stdcall GetDevice(LPCWSTR pwstrId, IMMDevice** ppDevice) override;
+  HRESULT __stdcall RegisterEndpointNotificationCallback(
+      IMMNotificationClient* pClient) override;
+  HRESULT __stdcall UnregisterEndpointNotificationCallback(
+      IMMNotificationClient* pClient) override;
+
+ private:
+  Microsoft::WRL::ComPtr<IMMNotificationClient> client_;
+};
+
+HRESULT FakeMMDeviceEnumerator::EnumAudioEndpoints(
+    EDataFlow dataFlow,
+    DWORD dwStateMask,
+    IMMDeviceCollection** ppDevices) {
+  return E_NOTIMPL;
+}
+
+HRESULT FakeMMDeviceEnumerator::GetDefaultAudioEndpoint(
+    EDataFlow dataFlow,
+    ERole role,
+    IMMDevice** ppEndpoint) {
+  return E_NOTIMPL;
+}
+
+HRESULT FakeMMDeviceEnumerator::GetDevice(LPCWSTR pwstrId,
+                                          IMMDevice** ppDevice) {
+  return E_NOTIMPL;
+}
+
+HRESULT FakeMMDeviceEnumerator::RegisterEndpointNotificationCallback(
+    IMMNotificationClient* pClient) {
+  client_ = pClient;
+  return S_OK;
+}
+
+HRESULT FakeMMDeviceEnumerator::UnregisterEndpointNotificationCallback(
+    IMMNotificationClient* pClient) {
+  if (client_.Get() != pClient) {
+    return E_INVALIDARG;
+  }
+  client_.Reset();
+  return S_OK;
+}
+
+}  // namespace
+
+class DefaultAudioDeviceChangeDetectorTest : public testing::Test {
+ public:
+  DefaultAudioDeviceChangeDetectorTest() = default;
+  ~DefaultAudioDeviceChangeDetectorTest() override = default;
+
+ protected:
+  base::win::ScopedCOMInitializer com_init_;
+};
+
+TEST_F(DefaultAudioDeviceChangeDetectorTest, RegistersAndUnregisters) {
+  auto enumerator = Microsoft::WRL::Make<FakeMMDeviceEnumerator>();
+  ASSERT_FALSE(enumerator->client());
+
+  auto detector =
+      Microsoft::WRL::Make<DefaultAudioDeviceChangeDetector>(enumerator);
+  EXPECT_EQ(enumerator->client().Get(),
+            static_cast<IMMNotificationClient*>(detector.Get()));
+
+  detector->Unregister();
+  EXPECT_FALSE(enumerator->client());
+
+  // A second call is a no-op.
+  detector->Unregister();
+}
+
+TEST_F(DefaultAudioDeviceChangeDetectorTest, GetAndReset) {
+  auto enumerator = Microsoft::WRL::Make<FakeMMDeviceEnumerator>();
+  auto detector =
+      Microsoft::WRL::Make<DefaultAudioDeviceChangeDetector>(enumerator);
+
+  EXPECT_FALSE(detector->GetAndReset());
+
+  enumerator->client()->OnDefaultDeviceChanged(eRender, eConsole, nullptr);
+  EXPECT_TRUE(detector->GetAndReset());
+  EXPECT_FALSE(detector->GetAndReset());
+
+  detector->Unregister();
+}
+
+TEST_F(DefaultAudioDeviceChangeDetectorTest,
+       OutlivesOwnerWhileSystemHoldsReference) {
+  auto enumerator = Microsoft::WRL::Make<FakeMMDeviceEnumerator>();
+  auto detector =
+      Microsoft::WRL::Make<DefaultAudioDeviceChangeDetector>(enumerator);
+
+  // Simulate the system queuing a notification, which takes an additional
+  // reference on the registered client.
+  Microsoft::WRL::ComPtr<IMMNotificationClient> pending = enumerator->client();
+  ASSERT_TRUE(pending);
+
+  // The owner unregisters the detector and releases its reference, as
+  // AudioCapturerWin::Deinitialize() does.
+  detector->Unregister();
+  detector.Reset();
+
+  // The detector must remain alive while `pending` holds a reference, so a
+  // late notification is delivered to a valid object.
+  EXPECT_EQ(pending->OnDefaultDeviceChanged(eRender, eConsole, nullptr), S_OK);
+  pending.Reset();
+}
+
+}  // namespace remoting
diff --git a/remoting/host/win/BUILD.gn b/remoting/host/win/BUILD.gn
index 65ea109..c5aca2b 100644
--- a/remoting/host/win/BUILD.gn
+++ b/remoting/host/win/BUILD.gn
@@ -235,6 +235,7 @@
   testonly = true
 
   sources = [
+    "default_audio_device_change_detector_unittest.cc",
     "desktop_event_handler_unittest.cc",
     "event_trace_data.cc",
     "event_trace_data.h",
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/remoting/host/win/default_audio_device_change_detector_unittest.cc b/remoting/host/win/default_audio_device_change_detector_unittest.cc
new file mode 100644
index 0000000..be9c675
--- /dev/null
+++ b/remoting/host/win/default_audio_device_change_detector_unittest.cc
@@ -0,0 +1,150 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "remoting/host/win/default_audio_device_change_detector.h"
+
+#include <mmdeviceapi.h>
+
+#include <wrl/client.h>
+#include <wrl/implements.h>
+
+#include "base/win/scoped_com_initializer.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace remoting {
+
+namespace {
+
+class FakeMMDeviceEnumerator final
+    : public Microsoft::WRL::RuntimeClass<
+          Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+          IMMDeviceEnumerator> {
+ public:
+  FakeMMDeviceEnumerator() = default;
+
+  FakeMMDeviceEnumerator(const FakeMMDeviceEnumerator&) = delete;
+  FakeMMDeviceEnumerator& operator=(const FakeMMDeviceEnumerator&) = delete;
+
+  ~FakeMMDeviceEnumerator() override = default;
+
+  Microsoft::WRL::ComPtr<IMMNotificationClient> client() const {
+    return client_;
+  }
+
+  // IMMDeviceEnumerator implementation.
+  HRESULT __stdcall EnumAudioEndpoints(
+      EDataFlow dataFlow,
+      DWORD dwStateMask,
+      IMMDeviceCollection** ppDevices) override;
+  HRESULT __stdcall GetDefaultAudioEndpoint(EDataFlow dataFlow,
+                                            ERole role,
+                                            IMMDevice** ppEndpoint) override;
+  HRESULT __stdcall GetDevice(LPCWSTR pwstrId, IMMDevice** ppDevice) override;
+  HRESULT __stdcall RegisterEndpointNotificationCallback(
+      IMMNotificationClient* pClient) override;
+  HRESULT __stdcall UnregisterEndpointNotificationCallback(
+      IMMNotificationClient* pClient) override;
+
+ private:
+  Microsoft::WRL::ComPtr<IMMNotificationClient> client_;
+};
+
+HRESULT FakeMMDeviceEnumerator::EnumAudioEndpoints(
+    EDataFlow dataFlow,
+    DWORD dwStateMask,
+    IMMDeviceCollection** ppDevices) {
+  return E_NOTIMPL;
+}
+
+HRESULT FakeMMDeviceEnumerator::GetDefaultAudioEndpoint(
+    EDataFlow dataFlow,
+    ERole role,
+    IMMDevice** ppEndpoint) {
+  return E_NOTIMPL;
+}
+
+HRESULT FakeMMDeviceEnumerator::GetDevice(LPCWSTR pwstrId,
+                                          IMMDevice** ppDevice) {
+  return E_NOTIMPL;
+}
+
+HRESULT FakeMMDeviceEnumerator::RegisterEndpointNotificationCallback(
+    IMMNotificationClient* pClient) {
+  client_ = pClient;
+  return S_OK;
+}
+
+HRESULT FakeMMDeviceEnumerator::UnregisterEndpointNotificationCallback(
+    IMMNotificationClient* pClient) {
+  if (client_.Get() != pClient) {
+    return E_INVALIDARG;
+  }
+  client_.Reset();
+  return S_OK;
+}
+
+}  // namespace
+
+class DefaultAudioDeviceChangeDetectorTest : public testing::Test {
+ public:
+  DefaultAudioDeviceChangeDetectorTest() = default;
+  ~DefaultAudioDeviceChangeDetectorTest() override = default;
+
+ protected:
+  base::win::ScopedCOMInitializer com_init_;
+};
+
+TEST_F(DefaultAudioDeviceChangeDetectorTest, RegistersAndUnregisters) {
+  auto enumerator = Microsoft::WRL::Make<FakeMMDeviceEnumerator>();
+  ASSERT_FALSE(enumerator->client());
+
+  auto detector =
+      Microsoft::WRL::Make<DefaultAudioDeviceChangeDetector>(enumerator);
+  EXPECT_EQ(enumerator->client().Get(),
+            static_cast<IMMNotificationClient*>(detector.Get()));
+
+  detector->Unregister();
+  EXPECT_FALSE(enumerator->client());
+
+  // A second call is a no-op.
+  detector->Unregister();
+}
+
+TEST_F(DefaultAudioDeviceChangeDetectorTest, GetAndReset) {
+  auto enumerator = Microsoft::WRL::Make<FakeMMDeviceEnumerator>();
+  auto detector =
+      Microsoft::WRL::Make<DefaultAudioDeviceChangeDetector>(enumerator);
+
+  EXPECT_FALSE(detector->GetAndReset());
+
+  enumerator->client()->OnDefaultDeviceChanged(eRender, eConsole, nullptr);
+  EXPECT_TRUE(detector->GetAndReset());
+  EXPECT_FALSE(detector->GetAndReset());
+
+  detector->Unregister();
+}
+
+TEST_F(DefaultAudioDeviceChangeDetectorTest,
+       OutlivesOwnerWhileSystemHoldsReference) {
+  auto enumerator = Microsoft::WRL::Make<FakeMMDeviceEnumerator>();
+  auto detector =
+      Microsoft::WRL::Make<DefaultAudioDeviceChangeDetector>(enumerator);
+
+  // Simulate the system queuing a notification, which takes an additional
+  // reference on the registered client.
+  Microsoft::WRL::ComPtr<IMMNotificationClient> pending = enumerator->client();
+  ASSERT_TRUE(pending);
+
+  // The owner unregisters the detector and releases its reference, as
+  // AudioCapturerWin::Deinitialize() does.
+  detector->Unregister();
+  detector.Reset();
+
+  // The detector must remain alive while `pending` holds a reference, so a
+  // late notification is delivered to a valid object.
+  EXPECT_EQ(pending->OnDefaultDeviceChanged(eRender, eConsole, nullptr), S_OK);
+  pending.Reset();
+}
+
+}  // namespace remoting
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential UAF in Chromoting Host via Incorrect COM Lifetime in DefaultAudioDeviceChangeDetector

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 use-after-free (UAF) vulnerability exists in the Chromoting host on Windows due to incorrect COM lifetime management of the DefaultAudioDeviceChangeDetector object. The object stubs its reference counting and is destroyed while notifications may still be pending in the STA message queue, potentially leading to remote code execution in the highly privileged remoting process.

Affected files:

  • remoting/host/win/default_audio_device_change_detector.cc
  • remoting/host/win/default_audio_device_change_detector.h
  • remoting/host/audio_capturer_win.cc
  • remoting/host/audio_capturer_win.h

Estimated timestamp from git blame: 2017-04-24

Summary

A potential Use-After-Free (UAF) vulnerability exists in the Chromoting (Remote Desktop) host process on Windows due to incorrect lifetime management of the DefaultAudioDeviceChangeDetector COM object. The class implements the IMMNotificationClient interface but stubs its COM AddRef() and Release() methods to return 1. This bypasses COM’s reference-counting lifetime management, leaving the object’s actual lifetime to be managed by a std::unique_ptr in AudioCapturerWin. This can lead to a UAF if OS notifications are queued in the thread’s message loop after the object is destroyed.

Technical Details

The DefaultAudioDeviceChangeDetector class registers itself for notifications via IMMDeviceEnumerator::RegisterEndpointNotificationCallback. The Chromoting audio thread is initialized as a Single-Threaded Apartment (STA) via AutoThread::COM_INIT_STA. In an STA, COM callbacks from background threads (like the Windows Audio endpoint builder) are marshaled as messages to the thread’s message queue.

The race condition occurs as follows:

  1. An audio device change event occurs. Windows frequently sends multiple notifications for a single event (e.g., OnDefaultDeviceChanged, OnDeviceStateChanged), which are marshaled and queued sequentially in the STA message loop.
  2. The first notification is dispatched, calling DefaultAudioDeviceChangeDetector::OnDefaultDeviceChanged, which sets an internal changed_ flag to true.
  3. A subsequent task (AudioCapturerWin::DoCapture), which runs as a timer-driven task in the same STA loop, detects this flag and calls ResetAndInitialize().
  4. ResetAndInitialize() calls Deinitialize(), which destroys the DefaultAudioDeviceChangeDetector object via default_device_detector_.reset(). The destructor calls UnregisterEndpointNotificationCallback to stop future notifications.
  5. However, if additional notification messages for the same object instance were already marshaled and present in the STA message queue, they remain in the queue. Because AddRef and Release were stubbed out, the OS COM infrastructure did not increment a reference count to keep the C++ object alive.
  6. When the STA message loop resumes and attempts to dispatch the remaining pending COM notification messages, it invokes a virtual method on the now-freed memory address of the detector instance, resulting in a Use-After-Free.

Impact

This is a potential high-severity vulnerability because the remoting_host.exe and remoting_desktop.exe processes are unsandboxed and typically run with SYSTEM privileges on Windows. Exploitation is feasible via vtable hijacking when the memory is reallocated during the subsequent Initialize() call (which performs multiple COM allocations that can reclaim the freed memory region). This could allow an attacker to achieve Remote Code Execution (RCE) with SYSTEM privileges.

Suggested Fix

The DefaultAudioDeviceChangeDetector should properly implement COM reference counting (e.g., by inheriting from Microsoft::WRL::RuntimeClass and using RuntimeClassFlags<ClassicCom>). The AudioCapturerWin should hold a Microsoft::WRL::ComPtr to the detector rather than a std::unique_ptr. This ensures that the object remains alive as long as the OS holds a reference to it in the message queue, even if AudioCapturerWin no longer needs it.

Evaluated with Chrome root at commit: a9e0d24757518a6f49f01bf5068f2cadfc6092f4


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