CVE-2026-79194
Overview
Background
- `IMMNotificationClient`
- A Windows Core Audio COM interface an application implements to receive notifications (e.g. default-device changes) after registering with an
IMMDeviceEnumeratorviaRegisterEndpointNotificationCallback. - COM reference counting
- The
IUnknownAddRef()/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.
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
- Detector registered with the system
AudioCapturerWin::Initializecreates aDefaultAudioDeviceChangeDetectorand registers it as anIMMNotificationClientwith the OS audio enumerator, which may take its own reference and queue notifications. - 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. - Owner frees the object during teardown
AudioCapturerWin::Deinitialize()callsdefault_device_detector_.reset(); becauseRelease()was a no-op stub, theunique_ptrdestroys the object while the system’s queued notification still points at it. - Use-after-free on callback delivery
The system delivers the pending notification to the now-freed detector, dereferencing freed memory; the shipped
OutlivesOwnerWhileSystemHoldsReferencetest 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
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifremoting/host/audio_capturer_win.cc |
modified | |
ifremoting/host/win/default_audio_device_change_detector.cc |
modified | |
DefaultAudioDeviceChangeDetectorremoting/host/win/default_audio_device_change_detector.h |
modified | |
FakeMMDeviceEnumeratorremoting/host/win/default_audio_device_change_detector_unittest.cc |
modified | |
DefaultAudioDeviceChangeDetectorTestremoting/host/win/default_audio_device_change_detector_unittest.cc |
modified | |
TEST_Fremoting/host/win/default_audio_device_change_detector_unittest.cc |
modified |
Files Changed
remoting/host/win/BUILD.gnremoting/host/win/default_audio_device_change_detector_unittest.ccremoting/host/audio_capturer_win.hremoting/host/win/default_audio_device_change_detector.hremoting/host/win/default_audio_device_change_detector.ccremoting/host/audio_capturer_win.cc
Audit Directions
- Stubbed COM lifetime methodsFlag any COM interface implementation whose
AddRef/Releasereturn a constant or are no-ops; such objects cannot honor references taken by the system or other callers. - `unique_ptr`-owned callback objectsLook 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 orderingAudit 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.
Patch
From 8c09594f13179278e04d4a558cf90c12725db66c Mon Sep 17 00:00:00 2001 From: Yuwei Huang <[email protected]> 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 <[email protected]> Auto-Submit: Yuwei Huang <[email protected]> Reviewed-by: Joe Downing <[email protected]> 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",
Regression Test / PoC
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
Original Bug Report
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.ccremoting/host/win/default_audio_device_change_detector.hremoting/host/audio_capturer_win.ccremoting/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:
- 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. - The first notification is dispatched, calling
DefaultAudioDeviceChangeDetector::OnDefaultDeviceChanged, which sets an internalchanged_flag to true. - A subsequent task (
AudioCapturerWin::DoCapture), which runs as a timer-driven task in the same STA loop, detects this flag and callsResetAndInitialize(). ResetAndInitialize()callsDeinitialize(), which destroys theDefaultAudioDeviceChangeDetectorobject viadefault_device_detector_.reset(). The destructor callsUnregisterEndpointNotificationCallbackto stop future notifications.- 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
AddRefandReleasewere stubbed out, the OS COM infrastructure did not increment a reference count to keep the C++ object alive. - 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.