| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/device/usb/mojo/device_impl.h" |
| |
| #include <stddef.h> |
| |
| #include <algorithm> |
| #include <memory> |
| #include <numeric> |
| #include <optional> |
| #include <string_view> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/compiler_specific.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/metrics/histogram_functions.h" |
| #include "base/strings/strcat.h" |
| #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_device.h" |
| #include "third_party/blink/public/common/features.h" |
| |
| namespace device { |
| |
| using mojom::UsbControlTransferParamsPtr; |
| using mojom::UsbControlTransferRecipient; |
| using mojom::UsbControlTransferType; |
| using mojom::UsbIsochronousPacketPtr; |
| using mojom::UsbTransferDirection; |
| using mojom::UsbTransferStatus; |
| |
| namespace usb { |
| |
| namespace { |
| |
| constexpr size_t kUsbTransferLengthLimit = 32 * 1024 * 1024; // 32 MiB |
| |
| void OnTransferIn(mojom::UsbDevice::GenericTransferInCallback callback, |
| UsbTransferStatus status, |
| scoped_refptr<base::RefCountedBytes> buffer, |
| size_t buffer_size) { |
| auto data = buffer ? base::span(*buffer).first(buffer_size) |
| : base::span<const uint8_t>(); |
| std::move(callback).Run(mojo::ConvertTo<mojom::UsbTransferStatus>(status), |
| data); |
| } |
| |
| void OnTransferOut(mojom::UsbDevice::GenericTransferOutCallback callback, |
| UsbTransferStatus status, |
| scoped_refptr<base::RefCountedBytes> buffer, |
| size_t buffer_size) { |
| std::move(callback).Run(mojo::ConvertTo<mojom::UsbTransferStatus>(status)); |
| } |
| |
| void OnIsochronousTransferIn( |
| mojom::UsbDevice::IsochronousTransferInCallback callback, |
| scoped_refptr<base::RefCountedBytes> buffer, |
| std::vector<UsbIsochronousPacketPtr> packets) { |
| uint32_t buffer_size = std::accumulate( |
| packets.begin(), packets.end(), 0u, |
| [](const uint32_t& a, const UsbIsochronousPacketPtr& packet) { |
| return a + packet->length; |
| }); |
| auto data = buffer ? base::span(*buffer).first(buffer_size) |
| : base::span<const uint8_t>(); |
| std::move(callback).Run(data, std::move(packets)); |
| } |
| |
| void OnIsochronousTransferOut( |
| mojom::UsbDevice::IsochronousTransferOutCallback callback, |
| scoped_refptr<base::RefCountedBytes> buffer, |
| std::vector<UsbIsochronousPacketPtr> packets) { |
| std::move(callback).Run(std::move(packets)); |
| } |
| |
| // IsAndroidSecurityKeyRequest returns true if |params| is attempting to |
| // configure an Android phone to act as a security key. |
| bool IsAndroidSecurityKeyRequest( |
| const mojom::UsbControlTransferParamsPtr& params, |
| base::span<const uint8_t> data) { |
| // This matches a request to send an AOA model string: |
| // https://source.android.com/devices/accessories/aoa#attempt-to-start-in-accessory-mode |
| // |
| // The magic model is matched as a prefix because sending trailing NULs etc |
| // would be considered equivalent by Android but would not be caught by an |
| // exact match here. Android is case-sensitive thus a byte-wise match is |
| // suitable. |
| const char* magic = mojom::UsbControlTransferParams::kSecurityKeyAOAModel; |
| return params->type == mojom::UsbControlTransferType::VENDOR && |
| params->request == 52 && params->index == 1 && |
| data.size() >= strlen(magic) && |
| UNSAFE_TODO(memcmp(data.data(), magic, strlen(magic))) == 0; |
| } |
| |
| // Returns the sum of `packet_lengths`, or nullopt if the sum would overflow. |
| std::optional<uint32_t> TotalPacketLength( |
| base::span<const uint32_t> packet_lengths) { |
| uint32_t total_bytes = 0; |
| for (const uint32_t packet_length : packet_lengths) { |
| // Check for overflow. |
| if (std::numeric_limits<uint32_t>::max() - total_bytes < packet_length) { |
| return std::nullopt; |
| } |
| total_bytes += packet_length; |
| } |
| return total_bytes; |
| } |
| |
| // Helper to log blocked transfers to the correct variant. |
| void LogBlockedControlTransfer(uint8_t class_code, |
| UsbTransferDirection direction, |
| UsbControlTransferType type) { |
| std::string_view direction_str = |
| (direction == UsbTransferDirection::INBOUND) ? "Inbound" : "Outbound"; |
| std::string_view type_str; |
| switch (type) { |
| case UsbControlTransferType::STANDARD: |
| type_str = "Standard"; |
| break; |
| case UsbControlTransferType::CLASS: |
| type_str = "Class"; |
| break; |
| case UsbControlTransferType::VENDOR: |
| type_str = "Vendor"; |
| break; |
| default: |
| return; // Skip RESERVED type |
| } |
| |
| base::UmaHistogramSparse(base::StrCat({"WebUsb.ControlTransferBlocked.", |
| direction_str, ".", type_str}), |
| class_code); |
| } |
| |
| } // namespace |
| |
| // static |
| void DeviceImpl::Create(scoped_refptr<device::UsbDevice> device, |
| mojo::PendingReceiver<mojom::UsbDevice> receiver, |
| mojo::PendingRemote<mojom::UsbDeviceClient> client, |
| base::span<const uint8_t> blocked_interface_classes, |
| bool allow_security_key_requests) { |
| auto* device_impl = |
| new DeviceImpl(std::move(device), std::move(client), |
| blocked_interface_classes, allow_security_key_requests); |
| device_impl->receiver_ = mojo::MakeSelfOwnedReceiver( |
| base::WrapUnique(device_impl), std::move(receiver)); |
| } |
| |
| DeviceImpl::~DeviceImpl() { |
| CloseHandle(); |
| } |
| |
| DeviceImpl::DeviceImpl(scoped_refptr<device::UsbDevice> device, |
| mojo::PendingRemote<mojom::UsbDeviceClient> client, |
| base::span<const uint8_t> blocked_interface_classes, |
| bool allow_security_key_requests) |
| : device_(std::move(device)), |
| blocked_interface_classes_(blocked_interface_classes.begin(), |
| blocked_interface_classes.end()), |
| allow_security_key_requests_(allow_security_key_requests), |
| client_(std::move(client)) { |
| DCHECK(device_); |
| observation_.Observe(device_.get()); |
| |
| if (client_) { |
| client_.set_disconnect_handler(base::BindOnce( |
| &DeviceImpl::OnClientConnectionError, weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void DeviceImpl::CloseHandle() { |
| if (device_handle_) { |
| device_handle_->Close(); |
| if (client_) |
| client_->OnDeviceClosed(); |
| } |
| device_handle_ = nullptr; |
| } |
| |
| bool DeviceImpl::HasControlTransferPermission( |
| UsbTransferDirection direction, |
| UsbControlTransferType type, |
| UsbControlTransferRecipient recipient, |
| uint16_t index) { |
| DCHECK(device_handle_); |
| |
| // STANDARD requests to the DEVICE or OTHER recipients (e.g. GET_DESCRIPTOR) |
| // are fundamental for device discovery and management. These requests are |
| // always permitted because the USB 2.0 spec (Section 9.3) defines the usage |
| // of the `index` field (wIndex in the spec) for these types as either 0 or a |
| // Language ID. Since they are not used for interface-based routing, they |
| // are always allowed. |
| if (type == UsbControlTransferType::STANDARD && |
| (recipient == UsbControlTransferRecipient::DEVICE || |
| recipient == UsbControlTransferRecipient::OTHER)) { |
| base::UmaHistogramEnumeration( |
| "WebUsb.ControlTransferPermissionOutcome", |
| WebUsbControlTransferPermissionOutcome::kAllowed); |
| return true; |
| } |
| |
| const mojom::UsbConfigurationInfo* config = device_->GetActiveConfiguration(); |
| if (!config) { |
| base::UmaHistogramEnumeration( |
| "WebUsb.ControlTransferPermissionOutcome", |
| WebUsbControlTransferPermissionOutcome::kError_NoConfiguration); |
| return false; |
| } |
| |
| // Identify the interface targeted by this request. |
| const mojom::UsbInterfaceInfo* interface = nullptr; |
| if (recipient == UsbControlTransferRecipient::ENDPOINT) { |
| // For the ENDPOINT recipient, the low byte of `index` is the endpoint |
| // address. We look up the interface that owns this endpoint. |
| interface = device_handle_->FindInterfaceByEndpoint(index & 0xff); |
| } else { |
| // For the INTERFACE recipient, the low byte of `index` is the interface |
| // number. |
| // For DEVICE and OTHER recipients, the USB spec allows `index` to be used |
| // arbitrarily by the vendor/class. We treat the low byte of `index` as a |
| // candidate interface ID to prevent routing bypasses. |
| auto interface_it = |
| std::ranges::find(config->interfaces, index & 0xff, |
| &mojom::UsbInterfaceInfo::interface_number); |
| if (interface_it != config->interfaces.end()) { |
| interface = interface_it->get(); |
| } |
| } |
| |
| // If the request targets a protected interface class (e.g. HID, Mass |
| // Storage), it must be blocked. This prevents a site from communicating |
| // with a protected interface, |
| // 1. by explicitly targeting an INTERFACE or ENDPOINT recipient, or |
| // 2. VENDOR or CLASS requests to the DEVICE or OTHER recipient where |
| // index looks like an interface number in case the device will |
| // respond to these requests despite an incorrectly set recipient. |
| if (interface && base::FeatureList::IsEnabled( |
| features::kWebUsbProtectedClassControlTransferBlock)) { |
| for (const auto& alternate : interface->alternates) { |
| if (blocked_interface_classes_.contains(alternate->class_code)) { |
| LogBlockedControlTransfer(alternate->class_code, direction, type); |
| base::UmaHistogramEnumeration( |
| "WebUsb.ControlTransferPermissionOutcome", |
| WebUsbControlTransferPermissionOutcome::kBlocked); |
| return false; |
| } |
| } |
| } |
| |
| // For requests explicitly targeting an INTERFACE or ENDPOINT, the interface |
| // must actually exist in the current configuration. |
| if (recipient == UsbControlTransferRecipient::INTERFACE || |
| recipient == UsbControlTransferRecipient::ENDPOINT) { |
| bool has_permission = interface != nullptr; |
| if (has_permission) { |
| base::UmaHistogramEnumeration( |
| "WebUsb.ControlTransferPermissionOutcome", |
| WebUsbControlTransferPermissionOutcome::kAllowed); |
| } else { |
| base::UmaHistogramEnumeration( |
| "WebUsb.ControlTransferPermissionOutcome", |
| WebUsbControlTransferPermissionOutcome::kError_InterfaceNotFound); |
| } |
| return has_permission; |
| } |
| |
| // For DEVICE and OTHER recipients, if we reached here, it means either no |
| // interface was identified by wIndex, or the interface it identified is |
| // not protected. These requests are allowed for device-level management. |
| base::UmaHistogramEnumeration( |
| "WebUsb.ControlTransferPermissionOutcome", |
| WebUsbControlTransferPermissionOutcome::kAllowed); |
| return true; |
| } |
| |
| // static |
| void DeviceImpl::OnOpen(base::WeakPtr<DeviceImpl> self, |
| OpenCallback callback, |
| scoped_refptr<UsbDeviceHandle> handle) { |
| if (!self) { |
| if (handle) |
| handle->Close(); |
| return; |
| } |
| |
| self->opening_ = false; |
| self->device_handle_ = std::move(handle); |
| if (self->device_handle_ && self->client_) |
| self->client_->OnDeviceOpened(); |
| |
| if (self->device_handle_) { |
| std::move(callback).Run(mojom::UsbOpenDeviceResult::NewSuccess( |
| mojom::UsbOpenDeviceSuccess::OK)); |
| } else { |
| std::move(callback).Run(mojom::UsbOpenDeviceResult::NewError( |
| mojom::UsbOpenDeviceError::ACCESS_DENIED)); |
| } |
| } |
| |
| void DeviceImpl::OnPermissionGrantedForOpen(OpenCallback callback, |
| bool granted) { |
| if (granted) { |
| device_->Open(base::BindOnce( |
| &DeviceImpl::OnOpen, weak_factory_.GetWeakPtr(), std::move(callback))); |
| } else { |
| opening_ = false; |
| std::move(callback).Run(mojom::UsbOpenDeviceResult::NewError( |
| mojom::UsbOpenDeviceError::ACCESS_DENIED)); |
| } |
| } |
| |
| void DeviceImpl::Open(OpenCallback callback) { |
| if (opening_ || device_handle_) { |
| std::move(callback).Run(mojom::UsbOpenDeviceResult::NewError( |
| mojom::UsbOpenDeviceError::ALREADY_OPEN)); |
| return; |
| } |
| |
| opening_ = true; |
| |
| if (!device_->permission_granted()) { |
| device_->RequestPermission( |
| base::BindOnce(&DeviceImpl::OnPermissionGrantedForOpen, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| return; |
| } |
| |
| device_->Open(base::BindOnce(&DeviceImpl::OnOpen, weak_factory_.GetWeakPtr(), |
| std::move(callback))); |
| } |
| |
| void DeviceImpl::Close(CloseCallback callback) { |
| CloseHandle(); |
| std::move(callback).Run(); |
| } |
| |
| void DeviceImpl::SetConfiguration(uint8_t value, |
| SetConfigurationCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| device_handle_->SetConfiguration(value, std::move(callback)); |
| } |
| |
| void DeviceImpl::ClaimInterface(uint8_t interface_number, |
| ClaimInterfaceCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(mojom::UsbClaimInterfaceResult::kFailure); |
| return; |
| } |
| |
| const mojom::UsbConfigurationInfo* config = device_->GetActiveConfiguration(); |
| if (!config) { |
| std::move(callback).Run(mojom::UsbClaimInterfaceResult::kFailure); |
| return; |
| } |
| |
| auto interface_it = |
| std::ranges::find(config->interfaces, interface_number, |
| &mojom::UsbInterfaceInfo::interface_number); |
| if (interface_it == config->interfaces.end()) { |
| std::move(callback).Run(mojom::UsbClaimInterfaceResult::kFailure); |
| return; |
| } |
| |
| for (const auto& alternate : (*interface_it)->alternates) { |
| if (blocked_interface_classes_.contains(alternate->class_code)) { |
| std::move(callback).Run(mojom::UsbClaimInterfaceResult::kProtectedClass); |
| return; |
| } |
| } |
| |
| device_handle_->ClaimInterface( |
| interface_number, |
| base::BindOnce(&DeviceImpl::OnInterfaceClaimed, |
| weak_factory_.GetWeakPtr(), std::move(callback))); |
| } |
| |
| void DeviceImpl::ReleaseInterface(uint8_t interface_number, |
| ReleaseInterfaceCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| device_handle_->ReleaseInterface(interface_number, std::move(callback)); |
| } |
| |
| void DeviceImpl::SetInterfaceAlternateSetting( |
| uint8_t interface_number, |
| uint8_t alternate_setting, |
| SetInterfaceAlternateSettingCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| device_handle_->SetInterfaceAlternateSetting( |
| interface_number, alternate_setting, std::move(callback)); |
| } |
| |
| void DeviceImpl::Reset(ResetCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| device_handle_->ResetDevice(std::move(callback)); |
| } |
| |
| void DeviceImpl::ClearHalt(UsbTransferDirection direction, |
| uint8_t endpoint_number, |
| ClearHaltCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(false); |
| return; |
| } |
| |
| device_handle_->ClearHalt(direction, endpoint_number, std::move(callback)); |
| } |
| |
| void DeviceImpl::ControlTransferIn(UsbControlTransferParamsPtr params, |
| uint32_t length, |
| uint32_t timeout, |
| ControlTransferInCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(mojom::UsbTransferStatus::TRANSFER_ERROR, {}); |
| return; |
| } |
| if (ShouldRejectUsbTransferLengthAndReportBadMessage(length)) { |
| return; |
| } |
| |
| if (HasControlTransferPermission(UsbTransferDirection::INBOUND, params->type, |
| params->recipient, params->index)) { |
| auto buffer = base::MakeRefCounted<base::RefCountedBytes>(length); |
| device_handle_->ControlTransfer( |
| UsbTransferDirection::INBOUND, params->type, params->recipient, |
| params->request, params->value, params->index, buffer, timeout, |
| base::BindOnce(&OnTransferIn, std::move(callback))); |
| } else { |
| std::move(callback).Run(mojom::UsbTransferStatus::PERMISSION_DENIED, {}); |
| } |
| } |
| |
| void DeviceImpl::ControlTransferOut(UsbControlTransferParamsPtr params, |
| base::span<const uint8_t> data, |
| uint32_t timeout, |
| ControlTransferOutCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(mojom::UsbTransferStatus::TRANSFER_ERROR); |
| return; |
| } |
| if (ShouldRejectUsbTransferLengthAndReportBadMessage(data.size())) { |
| return; |
| } |
| |
| if (HasControlTransferPermission(UsbTransferDirection::OUTBOUND, params->type, |
| params->recipient, params->index) && |
| (allow_security_key_requests_ || |
| !IsAndroidSecurityKeyRequest(params, data))) { |
| auto buffer = base::MakeRefCounted<base::RefCountedBytes>(data); |
| device_handle_->ControlTransfer( |
| UsbTransferDirection::OUTBOUND, params->type, params->recipient, |
| params->request, params->value, params->index, buffer, timeout, |
| base::BindOnce(&OnTransferOut, std::move(callback))); |
| } else { |
| std::move(callback).Run(mojom::UsbTransferStatus::PERMISSION_DENIED); |
| } |
| } |
| |
| void DeviceImpl::GenericTransferIn(uint8_t endpoint_number, |
| uint32_t length, |
| uint32_t timeout, |
| GenericTransferInCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(mojom::UsbTransferStatus::TRANSFER_ERROR, {}); |
| return; |
| } |
| if (ShouldRejectUsbTransferLengthAndReportBadMessage(length)) { |
| return; |
| } |
| |
| uint8_t endpoint_address = endpoint_number | 0x80; |
| auto buffer = base::MakeRefCounted<base::RefCountedBytes>(length); |
| device_handle_->GenericTransfer( |
| UsbTransferDirection::INBOUND, endpoint_address, buffer, timeout, |
| base::BindOnce(&OnTransferIn, std::move(callback))); |
| } |
| |
| void DeviceImpl::GenericTransferOut(uint8_t endpoint_number, |
| base::span<const uint8_t> data, |
| uint32_t timeout, |
| GenericTransferOutCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(mojom::UsbTransferStatus::TRANSFER_ERROR); |
| return; |
| } |
| if (ShouldRejectUsbTransferLengthAndReportBadMessage(data.size())) { |
| return; |
| } |
| |
| uint8_t endpoint_address = endpoint_number; |
| auto buffer = base::MakeRefCounted<base::RefCountedBytes>(data); |
| device_handle_->GenericTransfer( |
| UsbTransferDirection::OUTBOUND, endpoint_address, buffer, timeout, |
| base::BindOnce(&OnTransferOut, std::move(callback))); |
| } |
| |
| void DeviceImpl::IsochronousTransferIn( |
| uint8_t endpoint_number, |
| const std::vector<uint32_t>& packet_lengths, |
| uint32_t timeout, |
| IsochronousTransferInCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run( |
| {}, BuildIsochronousPacketArray( |
| packet_lengths, mojom::UsbTransferStatus::TRANSFER_ERROR)); |
| return; |
| } |
| |
| std::optional<uint32_t> total_bytes = TotalPacketLength(packet_lengths); |
| if (!total_bytes.has_value()) { |
| mojo::ReportBadMessage("Invalid isochronous packet lengths."); |
| std::move(callback).Run( |
| {}, BuildIsochronousPacketArray( |
| packet_lengths, mojom::UsbTransferStatus::TRANSFER_ERROR)); |
| return; |
| } |
| if (ShouldRejectUsbTransferLengthAndReportBadMessage(total_bytes.value())) { |
| return; |
| } |
| |
| uint8_t endpoint_address = endpoint_number | 0x80; |
| device_handle_->IsochronousTransferIn( |
| endpoint_address, packet_lengths, timeout, |
| base::BindOnce(&OnIsochronousTransferIn, std::move(callback))); |
| } |
| |
| void DeviceImpl::IsochronousTransferOut( |
| uint8_t endpoint_number, |
| base::span<const uint8_t> data, |
| const std::vector<uint32_t>& packet_lengths, |
| uint32_t timeout, |
| IsochronousTransferOutCallback callback) { |
| if (!device_handle_) { |
| std::move(callback).Run(BuildIsochronousPacketArray( |
| packet_lengths, mojom::UsbTransferStatus::TRANSFER_ERROR)); |
| return; |
| } |
| |
| std::optional<uint32_t> total_bytes = TotalPacketLength(packet_lengths); |
| if (!total_bytes.has_value() || total_bytes.value() != data.size()) { |
| mojo::ReportBadMessage("Invalid isochronous packet lengths."); |
| std::move(callback).Run(BuildIsochronousPacketArray( |
| packet_lengths, mojom::UsbTransferStatus::TRANSFER_ERROR)); |
| return; |
| } |
| if (ShouldRejectUsbTransferLengthAndReportBadMessage(total_bytes.value())) { |
| return; |
| } |
| |
| uint8_t endpoint_address = endpoint_number; |
| auto buffer = base::MakeRefCounted<base::RefCountedBytes>(data); |
| device_handle_->IsochronousTransferOut( |
| endpoint_address, buffer, packet_lengths, timeout, |
| base::BindOnce(&OnIsochronousTransferOut, std::move(callback))); |
| } |
| |
| void DeviceImpl::OnDeviceRemoved(scoped_refptr<device::UsbDevice> device) { |
| DCHECK_EQ(device_, device); |
| receiver_->Close(); |
| } |
| |
| void DeviceImpl::OnInterfaceClaimed(ClaimInterfaceCallback callback, |
| bool success) { |
| std::move(callback).Run(success ? mojom::UsbClaimInterfaceResult::kSuccess |
| : mojom::UsbClaimInterfaceResult::kFailure); |
| } |
| |
| void DeviceImpl::OnClientConnectionError() { |
| // Close the connection with Blink when WebUsbServiceImpl notifies the |
| // permission revocation from settings UI. |
| receiver_->Close(); |
| } |
| |
| bool DeviceImpl::ShouldRejectUsbTransferLengthAndReportBadMessage( |
| size_t length) { |
| if (!base::FeatureList::IsEnabled( |
| blink::features::kWebUSBTransferSizeLimit)) { |
| return false; |
| } |
| |
| if (length <= kUsbTransferLengthLimit) { |
| return false; |
| } |
| receiver_->ReportBadMessage( |
| base::StringPrintf("Transfer size %zu is over the limit.", length)); |
| return true; |
| } |
| |
| } // namespace usb |
| } // namespace device |