Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Video Capture
DescriptionInsufficient validation of untrusted input in Video Capture
ComponentVideo Capture
Bug ClassLogic Error
Tracker513186670
Fix commitf5f020cf8f3e (chromium/src) +580/-41
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
for
services/video_capture/test/video_capture_service_unittest.cc
modified
if
services/video_capture/test/video_capture_service_unittest.cc
modified
TEST_F
services/video_capture/test/video_capture_service_unittest.cc
modified

Files Changed

  • services/video_capture/test/video_capture_service_unittest.cc
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());
Loading diff…

Regression Test / PoC

shipped with the fix
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)
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • services/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

  1. An attacker compromises a process with access to the video_capture.mojom.VideoSourceProvider interface. On ChromeOS, this interface is brokered to the rtanalytics daemon via the MediaPerceptionControllerClient to support media perception features.
  2. The attacker calls GetSourceInfos() to identify the device_id (typically a HAL string or system path) of a physical camera.
  3. The attacker registers a virtual device using that same device_id via AddSharedMemoryVirtualDevice().
  4. 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.
  5. The attacker injects arbitrary video frames. Because the physical camera was never opened, its hardware activity LED remains off, enabling silent spoofing.
  6. 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

  1. Collision Check: Implement validation in VirtualDeviceEnabledDeviceFactory::Add*VirtualDevice to ensure that a virtual device cannot be registered with an ID that matches an existing physical or virtual device.
  2. Update Stop Logic: Modify VirtualDeviceEnabledDeviceFactory::StopDevice to look up the ID in virtual_devices_by_id_. If a virtual device is found, call StopDevice() on the corresponding VirtualDeviceEntry to 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.

View on issue tracker