Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in USB
DescriptionPolicy bypass in USB
ComponentUSB
Bug ClassLogic Error
Tracker513394321
Fix commit2796b8e6aee7 (chromium/src) +116/-11
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
services/device/usb/mojo/device_impl.cc
modified
TEST_F
services/device/usb/mojo/device_impl_unittest.cc
modified
MockBlockingTaskRunnerHelper
services/device/usb/usb_device_handle_usbfs_unittest.cc
modified

Files Changed

  • services/device/usb/mojo/device_impl.cc
  • services/device/usb/mojo/device_impl_unittest.cc
  • services/device/usb/usb_device_handle_usbfs.cc
  • services/device/usb/usb_device_handle_usbfs.h
  • services/device/usb/usb_device_handle_usbfs_unittest.cc
From 2796b8e6aee7866747b38cb7fadd203783b6a203 Mon Sep 17 00:00:00 2001
From: Alvin Ji <alvinji@chromium.org>
Date: Mon, 18 May 2026 20:25:36 -0700
Subject: [PATCH] usb: Block SetInterfaceAlternateSetting on protected interfaces

Fixes a WebUSB policy bypass where a compromised renderer could issue
unauthorized SET_INTERFACE commands to blocked interface classes (e.g.,
HID) on Linux-based platforms.

To prevent this, we now validate the target interface against blocked
interface classes at the Mojo layer in DeviceImpl, and enforce that the
interface must be claimed first in the UsbDeviceHandleUsbfs backend to
prevent exploiting the Linux kernel's auto-claim behavior.

Bug: 513394321
Change-Id: Ic2f851f3fc68ed714406cd09773b8dce636d39ec
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7855216
Commit-Queue: Alvin Ji <alvinji@chromium.org>
Reviewed-by: Matt Reynolds <mattreynolds@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1632597}
---

diff --git a/services/device/usb/mojo/device_impl.cc b/services/device/usb/mojo/device_impl.cc
index b3451b47..79e0e258 100644
--- a/services/device/usb/mojo/device_impl.cc
+++ b/services/device/usb/mojo/device_impl.cc
@@ -24,6 +24,7 @@
 #include "base/strings/stringprintf.h"
 #include "services/device/public/cpp/device_features.h"
 #include "services/device/public/cpp/usb/usb_utils.h"
+#include "services/device/usb/usb_descriptors.h"
 #include "services/device/usb/usb_device.h"
 #include "third_party/blink/public/common/features.h"
 
@@ -464,6 +465,24 @@
     return;
   }
 
+  const mojom::UsbConfigurationInfo* config = device_->GetActiveConfiguration();
+  if (!config) {
+    std::move(callback).Run(false);
+    return;
+  }
+
+  CombinedInterfaceInfo interface =
+      FindInterfaceInfoFromConfig(config, interface_number, alternate_setting);
+  if (!interface.IsValid()) {
+    std::move(callback).Run(false);
+    return;
+  }
+
+  if (blocked_interface_classes_.contains(interface.alternate->class_code)) {
+    std::move(callback).Run(false);
+    return;
+  }
+
   device_handle_->SetInterfaceAlternateSetting(
       interface_number, alternate_setting, std::move(callback));
 }
diff --git a/services/device/usb/mojo/device_impl_unittest.cc b/services/device/usb/mojo/device_impl_unittest.cc
index 28ef373..61989a1 100644
--- a/services/device/usb/mojo/device_impl_unittest.cc
+++ b/services/device/usb/mojo/device_impl_unittest.cc
@@ -924,23 +924,84 @@
                     .AddInterface(2, 0, 1, 2, 3)
                     .Build());
 
+  // The device must be configured because SetInterfaceAlternateSetting now
+  // retrieves the active configuration to validate the interface class code.
+  EXPECT_CALL(mock_handle(), SetConfigurationInternal(1, _));
+
+  {
+    base::test::TestFuture<bool> future;
+    device->SetConfiguration(1, future.GetCallback());
+    EXPECT_TRUE(future.Get());
+  }
+
   EXPECT_CALL(mock_handle(), SetInterfaceAlternateSettingInternal(1, 42, _));
 
   {
-    base::RunLoop loop;
-    device->SetInterfaceAlternateSetting(
-        1, 42, base::BindOnce(&ExpectResultAndThen, true, loop.QuitClosure()));
-    loop.Run();
+    base::test::TestFuture<bool> future;
+    device->SetInterfaceAlternateSetting(1, 42, future.GetCallback());
+    EXPECT_TRUE(future.Get());
   }
 
-  EXPECT_CALL(mock_handle(), SetInterfaceAlternateSettingInternal(1, 100, _));
+  EXPECT_CALL(mock_handle(), SetInterfaceAlternateSettingInternal(1, 100, _))
+      .Times(0);
 
   {
-    base::RunLoop loop;
-    device->SetInterfaceAlternateSetting(
-        1, 100,
-        base::BindOnce(&ExpectResultAndThen, false, loop.QuitClosure()));
-    loop.Run();
+    base::test::TestFuture<bool> future;
+    device->SetInterfaceAlternateSetting(1, 100, future.GetCallback());
+    EXPECT_FALSE(future.Get());
+  }
+
+  EXPECT_CALL(mock_handle(), Close());
+}
+
+TEST_F(USBDeviceImplTest, SetInterfaceAlternateSettingProtectedClassBypass) {
+  mojo::Remote<mojom::UsbDevice> device =
+      GetMockDeviceProxyWithBlockedInterfaces(base::span_from_ref(uint8_t{3}));
+
+  EXPECT_CALL(mock_device(), OpenInternal(_));
+
+  {
+    base::test::TestFuture<mojom::UsbOpenDeviceResultPtr> future;
+    device->Open(future.GetCallback());
+    EXPECT_TRUE(future.Get()->is_success());
+  }
+
+  AddMockConfig(
+      ConfigBuilder(/*configuration_value=*/1)
+          .AddInterface(/*interface_number=*/0, /*alternate_setting=*/0,
+                        /*class_code=*/0xFF, /*subclass_code=*/0,
+                        /*protocol_code=*/0)
+          .AddInterface(/*interface_number=*/1, /*alternate_setting=*/0,
+                        /*class_code=*/3, /*subclass_code=*/0,
+                        /*protocol_code=*/0)
+          .AddInterface(/*interface_number=*/1, /*alternate_setting=*/1,
+                        /*class_code=*/3, /*subclass_code=*/0,
+                        /*protocol_code=*/0)
+          .Build());
+
+  EXPECT_CALL(mock_handle(), SetConfigurationInternal(1, _));
+
+  {
+    base::test::TestFuture<bool> future;
+    device->SetConfiguration(1, future.GetCallback());
+    EXPECT_TRUE(future.Get());
+  }
+
+  EXPECT_CALL(mock_handle(), ClaimInterfaceInternal(1, _)).Times(0);
+
+  {
+    base::test::TestFuture<mojom::UsbClaimInterfaceResult> future;
+    device->ClaimInterface(1, future.GetCallback());
+    EXPECT_EQ(future.Get(), mojom::UsbClaimInterfaceResult::kProtectedClass);
+  }
+
+  EXPECT_CALL(mock_handle(), SetInterfaceAlternateSettingInternal(1, 1, _))
+      .Times(0);
+
+  {
+    base::test::TestFuture<bool> future;
+    device->SetInterfaceAlternateSetting(1, 1, future.GetCallback());
+    EXPECT_FALSE(future.Get());
   }
 
   EXPECT_CALL(mock_handle(), Close());
diff --git a/services/device/usb/usb_device_handle_usbfs.cc b/services/device/usb/usb_device_handle_usbfs.cc
index 1ce133ce..5b20401e 100644
--- a/services/device/usb/usb_device_handle_usbfs.cc
+++ b/services/device/usb/usb_device_handle_usbfs.cc
@@ -689,6 +689,13 @@
     return;
   }
 
+  if (!IsInterfaceClaimedByThis(interface_number)) {
+    USB_LOG(DEBUG) << "Interface " << interface_number << " not claimed.";
+    task_runner_->PostTask(FROM_HERE,
+                           base::BindOnce(std::move(callback), false));
+    return;
+  }
+
   // USBDEVFS_SETINTERFACE is synchronous because it issues a SET_INTERFACE
   // request to the device so it must be performed on a thread where it is okay
   // to block.
diff --git a/services/device/usb/usb_device_handle_usbfs.h b/services/device/usb/usb_device_handle_usbfs.h
index 6f689eb0..c2dbc86 100644
--- a/services/device/usb/usb_device_handle_usbfs.h
+++ b/services/device/usb/usb_device_handle_usbfs.h
@@ -211,7 +211,7 @@
   bool SetConfiguration(int configuration_value);
   virtual bool ClaimInterface(int interface_number);
   virtual bool ReleaseInterface(int interface_number);
-  bool SetInterface(int interface_number, int alternate_setting);
+  virtual bool SetInterface(int interface_number, int alternate_setting);
   bool ResetDevice();
   bool ClearHalt(uint8_t endpoint_address);
   void DiscardUrb(Transfer* transfer);
diff --git a/services/device/usb/usb_device_handle_usbfs_unittest.cc b/services/device/usb/usb_device_handle_usbfs_unittest.cc
index 8010c57..f8c40e15 100644
--- a/services/device/usb/usb_device_handle_usbfs_unittest.cc
+++ b/services/device/usb/usb_device_handle_usbfs_unittest.cc
@@ -28,6 +28,7 @@
   MockBlockingTaskRunnerHelper() {
     ON_CALL(*this, ClaimInterface).WillByDefault(testing::Return(true));
     ON_CALL(*this, ReleaseInterface).WillByDefault(testing::Return(true));
+    ON_CALL(*this, SetInterface).WillByDefault(testing::Return(true));
 #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX)
     ON_CALL(*this, DetachInterface).WillByDefault(testing::Return(true));
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/services/device/usb/mojo/device_impl_unittest.cc b/services/device/usb/mojo/device_impl_unittest.cc
index 28ef373..61989a1 100644
--- a/services/device/usb/mojo/device_impl_unittest.cc
+++ b/services/device/usb/mojo/device_impl_unittest.cc
@@ -924,23 +924,84 @@
                     .AddInterface(2, 0, 1, 2, 3)
                     .Build());
 
+  // The device must be configured because SetInterfaceAlternateSetting now
+  // retrieves the active configuration to validate the interface class code.
+  EXPECT_CALL(mock_handle(), SetConfigurationInternal(1, _));
+
+  {
+    base::test::TestFuture<bool> future;
+    device->SetConfiguration(1, future.GetCallback());
+    EXPECT_TRUE(future.Get());
+  }
+
   EXPECT_CALL(mock_handle(), SetInterfaceAlternateSettingInternal(1, 42, _));
 
   {
-    base::RunLoop loop;
-    device->SetInterfaceAlternateSetting(
-        1, 42, base::BindOnce(&ExpectResultAndThen, true, loop.QuitClosure()));
-    loop.Run();
+    base::test::TestFuture<bool> future;
+    device->SetInterfaceAlternateSetting(1, 42, future.GetCallback());
+    EXPECT_TRUE(future.Get());
   }
 
-  EXPECT_CALL(mock_handle(), SetInterfaceAlternateSettingInternal(1, 100, _));
+  EXPECT_CALL(mock_handle(), SetInterfaceAlternateSettingInternal(1, 100, _))
+      .Times(0);
 
   {
-    base::RunLoop loop;
-    device->SetInterfaceAlternateSetting(
-        1, 100,
-        base::BindOnce(&ExpectResultAndThen, false, loop.QuitClosure()));
-    loop.Run();
+    base::test::TestFuture<bool> future;
+    device->SetInterfaceAlternateSetting(1, 100, future.GetCallback());
+    EXPECT_FALSE(future.Get());
+  }
+
+  EXPECT_CALL(mock_handle(), Close());
+}
+
+TEST_F(USBDeviceImplTest, SetInterfaceAlternateSettingProtectedClassBypass) {
+  mojo::Remote<mojom::UsbDevice> device =
+      GetMockDeviceProxyWithBlockedInterfaces(base::span_from_ref(uint8_t{3}));
+
+  EXPECT_CALL(mock_device(), OpenInternal(_));
+
+  {
+    base::test::TestFuture<mojom::UsbOpenDeviceResultPtr> future;
+    device->Open(future.GetCallback());
+    EXPECT_TRUE(future.Get()->is_success());
+  }
+
+  AddMockConfig(
+      ConfigBuilder(/*configuration_value=*/1)
+          .AddInterface(/*interface_number=*/0, /*alternate_setting=*/0,
+                        /*class_code=*/0xFF, /*subclass_code=*/0,
+                        /*protocol_code=*/0)
+          .AddInterface(/*interface_number=*/1, /*alternate_setting=*/0,
+                        /*class_code=*/3, /*subclass_code=*/0,
+                        /*protocol_code=*/0)
+          .AddInterface(/*interface_number=*/1, /*alternate_setting=*/1,
+                        /*class_code=*/3, /*subclass_code=*/0,
+                        /*protocol_code=*/0)
+          .Build());
+
+  EXPECT_CALL(mock_handle(), SetConfigurationInternal(1, _));
+
+  {
+    base::test::TestFuture<bool> future;
+    device->SetConfiguration(1, future.GetCallback());
+    EXPECT_TRUE(future.Get());
+  }
+
+  EXPECT_CALL(mock_handle(), ClaimInterfaceInternal(1, _)).Times(0);
+
+  {
+    base::test::TestFuture<mojom::UsbClaimInterfaceResult> future;
+    device->ClaimInterface(1, future.GetCallback());
+    EXPECT_EQ(future.Get(), mojom::UsbClaimInterfaceResult::kProtectedClass);
+  }
+
+  EXPECT_CALL(mock_handle(), SetInterfaceAlternateSettingInternal(1, 1, _))
+      .Times(0);
+
+  {
+    base::test::TestFuture<bool> future;
+    device->SetInterfaceAlternateSetting(1, 1, future.GetCallback());
+    EXPECT_FALSE(future.Get());
   }
 
   EXPECT_CALL(mock_handle(), Close());
diff --git a/services/device/usb/usb_device_handle_usbfs_unittest.cc b/services/device/usb/usb_device_handle_usbfs_unittest.cc
index 8010c57..f8c40e15 100644
--- a/services/device/usb/usb_device_handle_usbfs_unittest.cc
+++ b/services/device/usb/usb_device_handle_usbfs_unittest.cc
@@ -28,6 +28,7 @@
   MockBlockingTaskRunnerHelper() {
     ON_CALL(*this, ClaimInterface).WillByDefault(testing::Return(true));
     ON_CALL(*this, ReleaseInterface).WillByDefault(testing::Return(true));
+    ON_CALL(*this, SetInterface).WillByDefault(testing::Return(true));
 #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX)
     ON_CALL(*this, DetachInterface).WillByDefault(testing::Return(true));
     ON_CALL(*this, ReattachInterface).WillByDefault(testing::Return(true));
@@ -44,6 +45,7 @@
               (override));
   MOCK_METHOD(bool, ClaimInterface, (int), (override));
   MOCK_METHOD(bool, ReleaseInterface, (int), (override));
+  MOCK_METHOD(bool, SetInterface, (int, int), (override));
 #if BUILDFLAG(IS_ANDROID) || BUILDFLAG(IS_LINUX)
   MOCK_METHOD(bool,
               DetachInterface,
@@ -187,6 +189,22 @@
   ASSERT_TRUE(release_interface_future2.Get());
 }
 
+TEST_F(UsbDeviceHandleUsbfsTest, SetInterfaceAlternateSettingUnclaimed) {
+  TestFuture<bool> set_interface_future;
+  handle1_->SetInterfaceAlternateSetting(1, 0,
+                                         set_interface_future.GetCallback());
+  ASSERT_FALSE(set_interface_future.Get());
+
+  TestFuture<bool> claim_interface_future;
+  handle1_->ClaimInterface(1, claim_interface_future.GetCallback());
+  ASSERT_TRUE(claim_interface_future.Get());
+
+  TestFuture<bool> set_interface_future2;
+  handle1_->SetInterfaceAlternateSetting(1, 0,
+                                         set_interface_future2.GetCallback());
+  ASSERT_TRUE(set_interface_future2.Get());
+}
+
 }  // namespace
 
 }  // namespace device
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential WebUSB Protected Interface Policy Bypass on Linux-based Platforms

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

Overview: The SetInterfaceAlternateSetting Mojo method in the USB Device service lacks a policy check for protected interface classes. On Linux, Android, and ChromeOS, this allows a compromised renderer to bypass WebUSB protections and issue SET_INTERFACE commands to blocked devices.

Affected files:

  • services/device/usb/mojo/device_impl.cc
  • services/device/usb/usb_device_handle_usbfs.cc

Estimated timestamp from git blame: 2021-03-03

Description

A vulnerability in the WebUSB implementation potentially allows a compromised renderer to bypass protected interface class restrictions (e.g., HID, Smart Card, Audio) on Linux-based platforms, including ChromeOS and Android. The issue stems from a missing policy check in the Mojo handler combined with platform-specific behavior in the Linux kernel’s usbfs driver.

The WebUSB security model relies on a blocked_interface_classes_ list to prevent access to sensitive device interfaces. While methods like ClaimInterface and ControlTransfer (via HasControlTransferPermission) correctly enforce this policy, the SetInterfaceAlternateSetting handler in services/device/usb/mojo/device_impl.cc fails to perform this check.

Furthermore, on Linux, Android, and ChromeOS, the backend implementation in services/device/usb/usb_device_handle_usbfs.cc does not verify if an interface has been claimed before attempting the operation. This is significant because the Linux kernel usbfs driver automatically claims an interface for the calling process if it is currently unclaimed when a USBDEVFS_SETINTERFACE ioctl is issued. This allows an attacker to bypass the userspace “claim” requirement, which is the primary gate where the protected class check is normally enforced.

Potential Attack Steps

  1. Renderer Compromise: An attacker first gains code execution within a Chromium renderer process.
  2. Mojo Interface Acquisition: The attacker obtains a device.mojom.UsbDevice Mojo remote for a device the user has previously granted access to (e.g., a composite device with both vendor and protected interfaces).
  3. Target Selection: The attacker identifies a protected-class interface (e.g., HID) on the device.
  4. Mojo Call Injection: The attacker directly invokes SetInterfaceAlternateSetting targeting the protected interface number, bypassing higher-level Blink checks.
  5. Policy Bypass: On Linux-based platforms, the browser process receives the call, fails to validate it against the protected class list, and issues the USBDEVFS_SETINTERFACE ioctl. The kernel then auto-claims the interface and sends the SET_INTERFACE control transfer to the physical hardware.

Impact

This could allow an attacker to bypass the kWebUsbProtectedClassControlTransferBlock policy. While the impact is limited to triggering SET_INTERFACE standard control transfers, this represents a direct violation of the WebUSB security model and allows manipulation of protected device states.

Suggested Fix

  1. In services/device/usb/mojo/device_impl.cc, update DeviceImpl::SetInterfaceAlternateSetting to validate the target interface against blocked_interface_classes_, consistent with the logic in ClaimInterface.
  2. In services/device/usb/usb_device_handle_usbfs.cc, update UsbDeviceHandleUsbfs::SetInterfaceAlternateSetting to verify that the interface is already claimed by the current handle (by checking the interfaces_ map) before proceeding with the ioctl.

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