CVE-2026-13942
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forservices/video_capture/test/video_capture_service_unittest.cc |
modified | |
ifservices/video_capture/test/video_capture_service_unittest.cc |
modified | |
TEST_Fservices/video_capture/test/video_capture_service_unittest.cc |
modified |
Files Changed
services/video_capture/test/video_capture_service_unittest.cc
Patch
From f5f020cf8f3edc1924e6b2da39b1587186e55170 Mon Sep 17 00:00:00 2001
From: Markus Handell <handellm@google.com>
Date: Tue, 19 May 2026 12:56:16 -0700
Subject: [PATCH] Capture: prevent virtual devices shadowing real ones.
Bug: 513194241, 513186670
Change-Id: Ie5827d50767ae0eb7845329b8111747e65e3f2a1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7852499
Reviewed-by: Ilya Nikolaevskiy <ilnik@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Commit-Queue: Markus Handell <handellm@google.com>
Cr-Commit-Position: refs/heads/main@{#1633066}
---
diff --git a/services/video_capture/test/video_capture_service_unittest.cc b/services/video_capture/test/video_capture_service_unittest.cc
index 66110d17..d6d00d1 100644
--- a/services/video_capture/test/video_capture_service_unittest.cc
+++ b/services/video_capture/test/video_capture_service_unittest.cc
@@ -211,7 +211,6 @@
create_push_subscription_remote_callback;
EXPECT_CALL(create_push_subscription_remote_callback, Run)
- .Times(1)
.WillOnce(
[&wait_loop](mojom::CreatePushSubscriptionResultCodePtr result_code,
const media::VideoCaptureParams& param) {
@@ -231,4 +230,179 @@
wait_loop.Run();
}
+// Tests that a virtual device cannot be added with the same ID as an active
+// physical device.
+TEST_F(VideoCaptureServiceTest,
+ CannotAddVirtualDeviceWhilePhysicalDeviceActive) {
+ // Retrieve the active physical devices.
+ base::RunLoop get_infos_loop;
+ std::vector<media::VideoCaptureDeviceInfo> physical_devices;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&get_infos_loop, &physical_devices](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ physical_devices = infos;
+ get_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ get_infos_loop.Run();
+ const std::string physical_device_id =
+ physical_devices[0].descriptor.device_id;
+
+ // Start the physical device by creating a subscription.
+ mojo::Remote<mojom::VideoSource> video_source_remote;
+ video_source_provider_->GetVideoSource(
+ physical_device_id, video_source_remote.BindNewPipeAndPassReceiver());
+
+ mojo::PendingRemote<video_capture::mojom::VideoFrameHandler> subscriber;
+ MockVideoFrameHandler mock_video_frame_handler(
+ subscriber.InitWithNewPipeAndPassReceiver());
+ mojo::Remote<video_capture::mojom::PushVideoStreamSubscription> subscription;
+
+ base::RunLoop start_device_loop;
+ base::MockCallback<mojom::VideoSource::CreatePushSubscriptionCallback>
+ create_push_subscription_callback;
+ EXPECT_CALL(create_push_subscription_callback, Run)
+ .WillOnce([&start_device_loop](
+ mojom::CreatePushSubscriptionResultCodePtr result_code,
+ const media::VideoCaptureParams& param) {
+ start_device_loop.Quit();
+ });
+
+ video_source_remote->CreatePushSubscription(
+ std::move(subscriber), requestable_settings_,
+ /*force_reopen_with_new_settings=*/false,
+ subscription.BindNewPipeAndPassReceiver(),
+ create_push_subscription_callback.Get());
+ start_device_loop.Run();
+
+ // Attempt to add a virtual device with the same ID.
+ auto virtual_device_context =
+ AddSharedMemoryVirtualDevice(physical_device_id);
+
+ // Verify that the virtual device is rejected and its pipe is disconnected.
+ base::RunLoop disconnect_loop;
+ virtual_device_context->device.set_disconnect_handler(
+ disconnect_loop.QuitClosure());
+ disconnect_loop.Run();
+
+ EXPECT_FALSE(virtual_device_context->device.is_connected());
+
+ // Verify that GetSourceInfos() returns ONLY 1 entry for the device ID
+ // (registration failed).
+ base::RunLoop verify_infos_loop;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&verify_infos_loop, physical_device_id](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ size_t occurrences = 0;
+ for (const auto& info : infos) {
+ if (info.descriptor.device_id == physical_device_id) {
+ occurrences++;
+ }
+ }
+ EXPECT_EQ(1u, occurrences);
+ verify_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ verify_infos_loop.Run();
+
+ // Cleanup
+ subscription.reset();
+ video_source_remote.reset();
+ base::RunLoop cleanup_loop;
+ video_source_provider_->GetSourceInfos(base::BindOnce(
+ [](base::RunLoop* run_loop, GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ run_loop->Quit();
+ },
+ &cleanup_loop));
+ cleanup_loop.Run();
+}
+
+// Tests that a virtual device cannot be added with the same ID as an idle
+// physical device.
+TEST_F(VideoCaptureServiceTest, CannotAddVirtualDeviceWhilePhysicalDeviceIdle) {
+ // Retrieve physical device IDs.
+ base::RunLoop get_infos_loop;
+ std::vector<media::VideoCaptureDeviceInfo> physical_devices;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&get_infos_loop, &physical_devices](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ physical_devices = infos;
+ get_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ get_infos_loop.Run();
+ const std::string physical_device_id =
+ physical_devices[0].descriptor.device_id;
+
+ // Add a virtual device with the same ID.
+ auto virtual_device_context =
+ AddSharedMemoryVirtualDevice(physical_device_id);
+
+ // Verify that the virtual device is rejected and its pipe is disconnected.
+ base::RunLoop disconnect_loop;
+ virtual_device_context->device.set_disconnect_handler(
+ disconnect_loop.QuitClosure());
+ disconnect_loop.Run();
+
+ EXPECT_FALSE(virtual_device_context->device.is_connected());
+
+ // Verify that GetSourceInfos() returns ONLY 1 entry for the device ID
+ // (registration failed).
+ base::RunLoop verify_infos_loop;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&verify_infos_loop, physical_device_id](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ size_t occurrences = 0;
+ for (const auto& info : infos) {
+ if (info.descriptor.device_id == physical_device_id) {
+ occurrences++;
+ }
+ }
+ EXPECT_EQ(1u, occurrences);
+ verify_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ verify_infos_loop.Run();
+}
+
+// Tests that a texture virtual device cannot be added with the same ID as an
+// idle physical device.
+TEST_F(VideoCaptureServiceTest,
+ CannotAddTextureVirtualDeviceWhilePhysicalDeviceIdle) {
+ // Retrieve physical device IDs.
+ base::RunLoop get_infos_loop;
+ std::vector<media::VideoCaptureDeviceInfo> physical_devices;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&get_infos_loop, &physical_devices](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ physical_devices = infos;
+ get_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ get_infos_loop.Run();
+
+ ASSERT_FALSE(physical_devices.empty());
+ const std::string physical_device_id =
+ physical_devices[0].descriptor.device_id;
+
+ // Add a texture virtual device with the same ID.
+ auto texture_device = AddTextureVirtualDevice(physical_device_id);
+
+ // Verify that the texture virtual device is rejected and its pipe is
+ // disconnected.
+ mojo::Remote<mojom::TextureVirtualDevice> remote_device(
+ std::move(texture_device));
+ base::RunLoop disconnect_loop;
+ remote_device.set_disconnect_handler(disconnect_loop.QuitClosure());
Regression Test / PoC
diff --git a/services/video_capture/test/video_capture_service_unittest.cc b/services/video_capture/test/video_capture_service_unittest.cc
index 66110d17..d6d00d1 100644
--- a/services/video_capture/test/video_capture_service_unittest.cc
+++ b/services/video_capture/test/video_capture_service_unittest.cc
@@ -211,7 +211,6 @@
create_push_subscription_remote_callback;
EXPECT_CALL(create_push_subscription_remote_callback, Run)
- .Times(1)
.WillOnce(
[&wait_loop](mojom::CreatePushSubscriptionResultCodePtr result_code,
const media::VideoCaptureParams& param) {
@@ -231,4 +230,179 @@
wait_loop.Run();
}
+// Tests that a virtual device cannot be added with the same ID as an active
+// physical device.
+TEST_F(VideoCaptureServiceTest,
+ CannotAddVirtualDeviceWhilePhysicalDeviceActive) {
+ // Retrieve the active physical devices.
+ base::RunLoop get_infos_loop;
+ std::vector<media::VideoCaptureDeviceInfo> physical_devices;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&get_infos_loop, &physical_devices](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ physical_devices = infos;
+ get_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ get_infos_loop.Run();
+ const std::string physical_device_id =
+ physical_devices[0].descriptor.device_id;
+
+ // Start the physical device by creating a subscription.
+ mojo::Remote<mojom::VideoSource> video_source_remote;
+ video_source_provider_->GetVideoSource(
+ physical_device_id, video_source_remote.BindNewPipeAndPassReceiver());
+
+ mojo::PendingRemote<video_capture::mojom::VideoFrameHandler> subscriber;
+ MockVideoFrameHandler mock_video_frame_handler(
+ subscriber.InitWithNewPipeAndPassReceiver());
+ mojo::Remote<video_capture::mojom::PushVideoStreamSubscription> subscription;
+
+ base::RunLoop start_device_loop;
+ base::MockCallback<mojom::VideoSource::CreatePushSubscriptionCallback>
+ create_push_subscription_callback;
+ EXPECT_CALL(create_push_subscription_callback, Run)
+ .WillOnce([&start_device_loop](
+ mojom::CreatePushSubscriptionResultCodePtr result_code,
+ const media::VideoCaptureParams& param) {
+ start_device_loop.Quit();
+ });
+
+ video_source_remote->CreatePushSubscription(
+ std::move(subscriber), requestable_settings_,
+ /*force_reopen_with_new_settings=*/false,
+ subscription.BindNewPipeAndPassReceiver(),
+ create_push_subscription_callback.Get());
+ start_device_loop.Run();
+
+ // Attempt to add a virtual device with the same ID.
+ auto virtual_device_context =
+ AddSharedMemoryVirtualDevice(physical_device_id);
+
+ // Verify that the virtual device is rejected and its pipe is disconnected.
+ base::RunLoop disconnect_loop;
+ virtual_device_context->device.set_disconnect_handler(
+ disconnect_loop.QuitClosure());
+ disconnect_loop.Run();
+
+ EXPECT_FALSE(virtual_device_context->device.is_connected());
+
+ // Verify that GetSourceInfos() returns ONLY 1 entry for the device ID
+ // (registration failed).
+ base::RunLoop verify_infos_loop;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&verify_infos_loop, physical_device_id](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ size_t occurrences = 0;
+ for (const auto& info : infos) {
+ if (info.descriptor.device_id == physical_device_id) {
+ occurrences++;
+ }
+ }
+ EXPECT_EQ(1u, occurrences);
+ verify_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ verify_infos_loop.Run();
+
+ // Cleanup
+ subscription.reset();
+ video_source_remote.reset();
+ base::RunLoop cleanup_loop;
+ video_source_provider_->GetSourceInfos(base::BindOnce(
+ [](base::RunLoop* run_loop, GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ run_loop->Quit();
+ },
+ &cleanup_loop));
+ cleanup_loop.Run();
+}
+
+// Tests that a virtual device cannot be added with the same ID as an idle
+// physical device.
+TEST_F(VideoCaptureServiceTest, CannotAddVirtualDeviceWhilePhysicalDeviceIdle) {
+ // Retrieve physical device IDs.
+ base::RunLoop get_infos_loop;
+ std::vector<media::VideoCaptureDeviceInfo> physical_devices;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&get_infos_loop, &physical_devices](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ physical_devices = infos;
+ get_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ get_infos_loop.Run();
+ const std::string physical_device_id =
+ physical_devices[0].descriptor.device_id;
+
+ // Add a virtual device with the same ID.
+ auto virtual_device_context =
+ AddSharedMemoryVirtualDevice(physical_device_id);
+
+ // Verify that the virtual device is rejected and its pipe is disconnected.
+ base::RunLoop disconnect_loop;
+ virtual_device_context->device.set_disconnect_handler(
+ disconnect_loop.QuitClosure());
+ disconnect_loop.Run();
+
+ EXPECT_FALSE(virtual_device_context->device.is_connected());
+
+ // Verify that GetSourceInfos() returns ONLY 1 entry for the device ID
+ // (registration failed).
+ base::RunLoop verify_infos_loop;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&verify_infos_loop, physical_device_id](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ size_t occurrences = 0;
+ for (const auto& info : infos) {
+ if (info.descriptor.device_id == physical_device_id) {
+ occurrences++;
+ }
+ }
+ EXPECT_EQ(1u, occurrences);
+ verify_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ verify_infos_loop.Run();
+}
+
+// Tests that a texture virtual device cannot be added with the same ID as an
+// idle physical device.
+TEST_F(VideoCaptureServiceTest,
+ CannotAddTextureVirtualDeviceWhilePhysicalDeviceIdle) {
+ // Retrieve physical device IDs.
+ base::RunLoop get_infos_loop;
+ std::vector<media::VideoCaptureDeviceInfo> physical_devices;
+ EXPECT_CALL(device_info_receiver_, Run)
+ .WillOnce([&get_infos_loop, &physical_devices](
+ GetSourceInfosResult result,
+ const std::vector<media::VideoCaptureDeviceInfo>& infos) {
+ physical_devices = infos;
+ get_infos_loop.Quit();
+ });
+ video_source_provider_->GetSourceInfos(device_info_receiver_.Get());
+ get_infos_loop.Run();
+
+ ASSERT_FALSE(physical_devices.empty());
+ const std::string physical_device_id =
+ physical_devices[0].descriptor.device_id;
+
+ // Add a texture virtual device with the same ID.
+ auto texture_device = AddTextureVirtualDevice(physical_device_id);
+
+ // Verify that the texture virtual device is rejected and its pipe is
+ // disconnected.
+ mojo::Remote<mojom::TextureVirtualDevice> remote_device(
+ std::move(texture_device));
+ base::RunLoop disconnect_loop;
+ remote_device.set_disconnect_handler(disconnect_loop.QuitClosure());
+ disconnect_loop.Run();
+
+ EXPECT_FALSE(remote_device.is_connected());
+}
+
} // namespace video_capture
diff --git a/services/video_capture/test/virtual_device_unittest.cc b/services/video_capture/test/virtual_device_unittest.cc
index de54a5c..6c46aca 100644
--- a/services/video_capture/test/virtual_device_unittest.cc
+++ b/services/video_capture/test/virtual_device_unittest.cc
@@ -5,6 +5,7 @@
#include <algorithm>
#include "base/functional/bind.h"
+#include "base/memory/raw_ptr.h"
#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/test/mock_callback.h"
@@ -15,6 +16,7 @@
#include "services/video_capture/public/cpp/mock_video_frame_handler.h"
#include "services/video_capture/public/mojom/video_frame_handler.mojom.h"
#include "services/video_capture/shared_memory_virtual_device_mojo_adapter.h"
+#include "services/video_capture/virtual_device_enabled_device_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
using testing::_;
@@ -159,7 +161,7 @@
base::MockCallback<
mojom::SharedMemoryVirtualDevice::RequestFrameBufferCallback>
request_frame_buffer_callback;
- EXPECT_CALL(request_frame_buffer_callback, Run(_))
+ EXPECT_CALL(request_frame_buffer_callback, Run)
.Times(1)
.WillOnce([this](int32_t buffer_id) {
// Verify that the returned |buffer_id| is a known buffer ID.
@@ -174,7 +176,7 @@
base::RunLoop wait_for_stopped_loop;
{
testing::InSequence s;
- EXPECT_CALL(video_frame_handler, DoOnBufferRetired(_))
+ EXPECT_CALL(video_frame_handler, DoOnBufferRetired)
.Times(SharedMemoryVirtualDeviceMojoAdapter::
max_buffer_pool_buffer_count());
EXPECT_CALL(video_frame_handler, OnStopped())
@@ -184,4 +186,259 @@
wait_for_stopped_loop.Run();
}
+class MockDeviceFactory : public DeviceFactory {
+ public:
+ MockDeviceFactory() = default;
+ ~MockDeviceFactory() override = default;
+
+ MOCK_METHOD(void,
+ GetDeviceInfos,
+ (GetDeviceInfosCallback callback),
+ (override));
+ MOCK_METHOD(void,
+ CreateDevice,
+ (const std::string& device_id, CreateDeviceCallback callback),
+ (override));
+ MOCK_METHOD(void, StopDevice, (const std::string device_id), (override));
+ MOCK_METHOD(void,
+ AddSharedMemoryVirtualDevice,
+ (const media::VideoCaptureDeviceInfo& device_info,
+ mojo::PendingRemote<mojom::Producer> producer,
+ mojo::PendingReceiver<mojom::SharedMemoryVirtualDevice>
+ virtual_device_receiver),
+ (override));
+ MOCK_METHOD(void,
+ AddTextureVirtualDevice,
+ (const media::VideoCaptureDeviceInfo& device_info,
+ mojo::PendingReceiver<mojom::TextureVirtualDevice>
+ virtual_device_receiver),
+ (override));
+ MOCK_METHOD(void,
+ AddGpuMemoryBufferVirtualDevice,
+ (const media::VideoCaptureDeviceInfo& device_info,
+ mojo::PendingReceiver<mojom::GpuMemoryBufferVirtualDevice>
+ virtual_device_receiver),
+ (override));
+ MOCK_METHOD(void,
+ RegisterVirtualDevicesChangedObserver,
+ (mojo::PendingRemote<mojom::DevicesChangedObserver> observer,
+ bool raise_event_if_virtual_devices_already_present),
+ (override));
+#if BUILDFLAG(IS_WIN)
+ MOCK_METHOD(void, OnGpuInfoUpdate, (const CHROME_LUID& luid), (override));
+#endif
+};
+
+class VirtualDeviceEnabledDeviceFactoryTest : public ::testing::Test {
+ protected:
+ VirtualDeviceEnabledDeviceFactoryTest() {
+ auto mock_factory = std::make_unique<MockDeviceFactory>();
+ mock_factory_ = mock_factory.get();
+ factory_ = std::make_unique<VirtualDeviceEnabledDeviceFactory>(
+ std::move(mock_factory));
+ }
+
+ ~VirtualDeviceEnabledDeviceFactoryTest() override = default;
+
+ base::test::SingleThreadTaskEnvironment task_environment_;
+ std::unique_ptr<VirtualDeviceEnabledDeviceFactory> factory_;
+ raw_ptr<MockDeviceFactory> mock_factory_ = nullptr;
+};
+
+// Tests that when an underlying device creation succeeds, any old virtual
+// device connection with the same device ID is severed.
+TEST_F(VirtualDeviceEnabledDeviceFactoryTest,
+ CreateDeviceSucceedsOldVirtualDeviceConnectionSevered) {
+ const std::string device_id = "/test/device";
+
+ // Register the virtual device first by having GetDeviceInfos return empty
... (truncated)
Original Bug Report
Potential camera shadowing and DoS via ID collision in Video Capture Service
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The Video Capture Service allows virtual devices to register using IDs that collision with physical cameras. This enables a compromised privileged client to silently spoof camera feeds and causes a permanent hang when applications attempt to release the device.
Affected files:
services/video_capture/virtual_device_enabled_device_factory.ccservices/video_capture/video_source_impl.cc
Estimated timestamp from git blame: 2017-11-21
Summary
A logic vulnerability in VirtualDeviceEnabledDeviceFactory allows virtual video capture devices to shadow physical cameras by registering with a colliding device_id. Because the factory prioritizes virtual devices during device creation and lacks collision checks during registration, a compromised process with access to the VideoSourceProvider interface can silently intercept and spoof camera feeds. Furthermore, a failure to properly handle stop requests for virtual devices in the factory leads to a persistent Denial of Service (DoS) for the affected device ID.
Root Cause Analysis
1. Device Creation Prioritization
In VirtualDeviceEnabledDeviceFactory::CreateDevice(), the factory performs a lookup in its internal virtual device map before falling back to the physical device factory. If an entry is found in the virtual map, it is returned immediately, effectively shadowing any physical hardware with the same ID.
// services/video_capture/virtual_device_enabled_device_factory.cc
void VirtualDeviceEnabledDeviceFactory::CreateDevice(
const std::string& device_id,
CreateDeviceCallback callback) {
auto virtual_device_iter = virtual_devices_by_id_.find(device_id);
if (virtual_device_iter != virtual_devices_by_id_.end()) {
VirtualDeviceEntry& device_entry = virtual_device_iter->second;
DeviceInfo info{device_entry.GetDevice(), media::VideoCaptureError::kNone};
std::move(callback).Run(std::move(info));
return;
}
return device_factory_->CreateDevice(device_id, std::move(callback));
}
2. Lack of Collision Validation
Methods such as AddSharedMemoryVirtualDevice and AddTextureVirtualDevice accept a caller-supplied device_id (via VideoCaptureDeviceInfo) without validating it against the list of physical devices. This allow a client to choose a device_id that intentionally collisions with existing hardware.
3. Broken Stop Logic (Denial of Service)
The VirtualDeviceEnabledDeviceFactory::StopDevice() method only forwards stop requests to the inner physical factory. It fails to check if the ID corresponds to a virtual device:
// services/video_capture/virtual_device_enabled_device_factory.cc
void VirtualDeviceEnabledDeviceFactory::StopDevice(
const std::string device_id) {
device_factory_->StopDevice(device_id);
}
When a virtual device is in use, VideoSourceImpl::StopDeviceAsynchronously() calls this factory method and transitions to the kStoppingAsynchronously state. Because the virtual device is never signaled to stop, it never sends the required OnStopped notification to the BroadcastingReceiver. This causes VideoSourceImpl to hang in the stopping state indefinitely, preventing the camera ID from being reopened.
Potential Attack Scenario
- An attacker compromises a process with access to the
video_capture.mojom.VideoSourceProviderinterface. On ChromeOS, this interface is brokered to thertanalyticsdaemon via theMediaPerceptionControllerClientto support media perception features. - The attacker calls
GetSourceInfos()to identify thedevice_id(typically a HAL string or system path) of a physical camera. - The attacker registers a virtual device using that same
device_idviaAddSharedMemoryVirtualDevice(). - When a victim application (e.g., Google Meet) subsequently requests the camera, the Video Capture Service returns the attacker’s virtual device instead of the physical hardware.
- The attacker injects arbitrary video frames. Because the physical camera was never opened, its hardware activity LED remains off, enabling silent spoofing.
- When the victim application closes the camera, the service’s internal state hangs, leading to a persistent DoS for that device ID until the service process is restarted.
Note: These are potential steps; our current analysis is based on a review of the source code logic.
Suggested Fix
- Collision Check: Implement validation in
VirtualDeviceEnabledDeviceFactory::Add*VirtualDeviceto ensure that a virtual device cannot be registered with an ID that matches an existing physical or virtual device. - Update Stop Logic: Modify
VirtualDeviceEnabledDeviceFactory::StopDeviceto look up the ID invirtual_devices_by_id_. If a virtual device is found, callStopDevice()on the correspondingVirtualDeviceEntryto ensure the virtual adapter correctly shuts down and notifies its consumers.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.