// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/usb/cros_usb_detector.h"
#include <fcntl.h>
#include <unistd.h>
#include <string>
#include <utility>
#include "ash/components/arc/arc_util.h"
#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/public/cpp/notification_utils.h"
#include "ash/webui/settings/public/constants/routes.mojom.h"
#include "base/check_deref.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/numerics/safe_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/values.h"
#include "chrome/browser/ash/arc/arc_util.h"
#include "chrome/browser/ash/bruschetta/bruschetta_util.h"
#include "chrome/browser/ash/crostini/crostini_features.h"
#include "chrome/browser/ash/crostini/crostini_manager.h"
#include "chrome/browser/ash/crostini/crostini_pref_names.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/guest_os/guest_id.h"
#include "chrome/browser/ash/guest_os/guest_os_pref_names.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_features.h"
#include "chrome/browser/ash/plugin_vm/plugin_vm_util.h"
#include "chrome/browser/notifications/system_notification_helper.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/ui/settings_window_manager_chromeos.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/ash/components/dbus/cicerone/cicerone_client.h"
#include "chromeos/ash/components/dbus/concierge/concierge_client.h"
#include "chromeos/ash/components/disks/disk.h"
#include "chromeos/ash/components/disks/disk_mount_manager.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/prefs/scoped_user_pref_update.h"
#include "components/vector_icons/vector_icons.h"
#include "content/public/browser/device_service.h"
#include "services/device/public/cpp/usb/usb_utils.h"
#include "services/device/public/mojom/usb_enumeration_options.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
namespace ash {
namespace {
constexpr uint32_t kAllInterfacesMask = ~0U;
const char16_t kParallelsShortName[] = u"Parallels";
const char16_t kParallelsName[] = u"Parallels Desktop";
// Not owned locally.
static CrosUsbDetector* g_cros_usb_detector = nullptr;
const char kNotifierUsb[] = "crosusb.connected";
std::u16string ProductLabelFromDevice(
const device::mojom::UsbDeviceInfo& device_info) {
std::u16string product_label =
l10n_util::GetStringUTF16(IDS_CROSUSB_UNKNOWN_DEVICE);
if (device_info.product_name.has_value() &&
!device_info.product_name->empty()) {
product_label = device_info.product_name.value();
} else if (device_info.manufacturer_name.has_value() &&
!device_info.manufacturer_name->empty()) {
product_label =
l10n_util::GetStringFUTF16(IDS_CROSUSB_UNKNOWN_DEVICE_FROM_MANUFACTURER,
device_info.manufacturer_name.value());
}
return product_label;
}
uint32_t ClearMatchingInterfaces(
uint32_t in_mask,
const device::mojom::UsbDeviceFilter& filter,
const device::mojom::UsbDeviceInfo& device_info) {
uint32_t mask = in_mask;
for (auto& config : device_info.configurations) {
for (auto& iface : config->interfaces) {
for (auto& alternate_info : iface->alternates) {
if (filter.has_class_code &&
alternate_info->class_code != filter.class_code) {
continue;
}
if (filter.has_subclass_code &&
alternate_info->subclass_code != filter.subclass_code) {
continue;
}
if (filter.has_protocol_code &&
alternate_info->protocol_code != filter.protocol_code) {
continue;
}
if (iface->interface_number >= 32) {
LOG(ERROR) << "Interface number too high in USB descriptor";
continue;
}
mask &= ~(1U << iface->interface_number);
}
}
}
return mask;
}
uint32_t GetUsbInterfaceBaseMask(
const device::mojom::UsbDeviceInfo& device_info) {
if (device_info.configurations.empty()) {
// No specific interfaces to clear.
return kAllInterfacesMask;
}
uint32_t mask = 0;
for (auto& config : device_info.configurations) {
for (auto& iface : config->interfaces) {
if (iface->interface_number >= 32) {
LOG(ERROR) << "Interface number too high in USB descriptor.";
continue;
}
mask |= (1U << iface->interface_number);
}
}
return mask;
}
uint32_t GetFilteredInterfacesMask(
const std::vector<device::mojom::UsbDeviceFilterPtr>& filters,
const device::mojom::UsbDeviceInfo& device_info) {
uint32_t mask = GetUsbInterfaceBaseMask(device_info);
for (const auto& filter : filters) {
mask = ClearMatchingInterfaces(mask, *filter, device_info);
}
return mask;
}
Profile* profile() {
return ProfileManager::GetActiveUserProfile();
}
crostini::CrostiniManager* manager() {
return crostini::CrostiniManager::GetForProfile(profile());
}
// Delegate for CrosUsb notification
class CrosUsbNotificationDelegate
: public message_center::NotificationDelegate {
public:
explicit CrosUsbNotificationDelegate(const std::string& notification_id,
std::string guid,
std::vector<std::string> vm_names,
std::string settings_sub_page)
: notification_id_(notification_id),
guid_(std::move(guid)),
vm_names_(std::move(vm_names)),
settings_sub_page_(std::move(settings_sub_page)),
disposition_(CrosUsbNotificationClosed::kUnknown) {}
CrosUsbNotificationDelegate(const CrosUsbNotificationDelegate&) = delete;
CrosUsbNotificationDelegate& operator=(const CrosUsbNotificationDelegate&) =
delete;
void Click(const std::optional<int>& button_index,
const std::optional<std::u16string>& reply) override {
disposition_ = CrosUsbNotificationClosed::kUnknown;
if (button_index && *button_index < static_cast<int>(vm_names_.size())) {
if (vm_names_[*button_index] == crostini::kCrostiniDefaultVmName) {
// When multi-container is enabled, show the settings page instead of
// directly attaching the device to the VM. Otherwise, the device is
// attached to the default container in the VM.
if (crostini::CrostiniFeatures::Get()->IsMultiContainerAllowed(
profile())) {
HandleShowSettings(
chromeos::settings::mojom::kCrostiniUsbPreferencesSubpagePath);
} else {
HandleConnectToGuest(crostini::DefaultContainerId());
}
} else {
HandleConnectToGuest(vm_names_[*button_index]);
}
} else {
HandleShowSettings(settings_sub_page_);
}
}
void Close(bool by_user) override {
if (by_user) {
disposition_ = CrosUsbNotificationClosed::kByUser;
}
}
private:
~CrosUsbNotificationDelegate() override = default;
void HandleConnectToGuest(const guest_os::GuestId& guest_id) {
disposition_ = CrosUsbNotificationClosed::kConnectToLinux;
CrosUsbDetector* detector = CrosUsbDetector::Get();
if (detector) {
detector->AttachUsbDeviceToGuest(guest_id, guid_, base::DoNothing());
return;
}
Close(false);
}
void HandleConnectToGuest(const std::string& vm_name) {
HandleConnectToGuest(guest_os::GuestId(vm_name, ""));
}
void HandleShowSettings(const std::string& sub_page) {
chrome::SettingsWindowManager::GetInstance()->ShowOSSettings(profile(),
sub_page);
Close(false);
}
std::string notification_id_;
std::string guid_;
std::vector<std::string> vm_names_;
std::string settings_sub_page_;
CrosUsbNotificationClosed disposition_;
base::WeakPtrFactory<CrosUsbNotificationDelegate> weak_ptr_factory_{this};
};
device::mojom::UsbDeviceFilterPtr UsbFilterByClassCode(
UsbClassCode device_class) {
auto filter = device::mojom::UsbDeviceFilter::New();
filter->has_class_code = true;
filter->class_code = device_class;
return filter;
}
device::mojom::UsbDeviceFilterPtr UsbFilterByClassAndSubclassCode(
UsbClassCode device_class,
UsbSubclassCode device_subclass) {
auto filter = device::mojom::UsbDeviceFilter::New();
filter->has_class_code = true;
filter->class_code = device_class;
filter->has_subclass_code = true;
filter->subclass_code = device_subclass;
return filter;
}
std::u16string CombineVmNames(const std::vector<std::u16string>& vm_names) {
std::u16string res;
size_t pos = 0;
while (pos < vm_names.size()) {
res.append(vm_names[pos]);
pos++;
if (pos < vm_names.size()) {
res.append(u" ");
res.append(l10n_util::GetStringUTF16(IDS_CROSUSB_NOTIFICATION_OR));
res.append(u" ");
}
}
return res;
}
// Returns true if user enables ARC on ARCVM enabled devices.
bool IsPlayStoreEnabledWithArcVmForProfile(const Profile* profile) {
return arc::IsArcPlayStoreEnabledForProfile(profile) && arc::IsArcVmEnabled();
}
void ShowNotificationForDevice(const std::string& guid,
const std::u16string& label) {
message_center::RichNotificationData rich_notification_data;
std::vector<std::string> vm_names;
std::string settings_sub_page;
std::u16string vm_name;
std::u16string vm_name_button_text;
std::vector<std::u16string> vm_names_in_notification;
rich_notification_data.small_image = gfx::Image(
gfx::CreateVectorIcon(vector_icons::kUsbIcon, 64, gfx::kGoogleBlue800));
rich_notification_data.accent_color_id = cros_tokens::kCrosSysPrimary;
if (crostini::CrostiniFeatures::Get()->IsEnabled(profile())) {
vm_name = l10n_util::GetStringUTF16(IDS_CROSTINI_LINUX);
rich_notification_data.buttons.emplace_back(
message_center::ButtonInfo(l10n_util::GetStringFUTF16(
IDS_CROSUSB_NOTIFICATION_BUTTON_CONNECT_TO_VM, vm_name)));
vm_names.emplace_back(crostini::kCrostiniDefaultVmName);
vm_names_in_notification.emplace_back(vm_name);
settings_sub_page =
chromeos::settings::mojom::kCrostiniUsbPreferencesSubpagePath;
}
if (plugin_vm::PluginVmFeatures::Get()->IsEnabled(profile())) {
vm_name = kParallelsName;
vm_name_button_text = kParallelsShortName;
rich_notification_data.buttons.emplace_back(
message_center::ButtonInfo(l10n_util::GetStringFUTF16(
IDS_CROSUSB_NOTIFICATION_BUTTON_CONNECT_TO_VM,
vm_name_button_text)));
vm_names.emplace_back(plugin_vm::kPluginVmName);
vm_names_in_notification.emplace_back(vm_name);
settings_sub_page =
chromeos::settings::mojom::kPluginVmUsbPreferencesSubpagePath;
}
if (IsPlayStoreEnabledWithArcVmForProfile(profile())) {
vm_name = l10n_util::GetStringUTF16(IDS_CROSUSB_NOTIFICATION_ARCVM);
vm_name_button_text =
l10n_util::GetStringUTF16(IDS_CROSUSB_NOTIFICATION_ARCVM_BUTTON);
rich_notification_data.buttons.emplace_back(
message_center::ButtonInfo(l10n_util::GetStringFUTF16(
IDS_CROSUSB_NOTIFICATION_BUTTON_CONNECT_TO_VM,
vm_name_button_text)));
vm_names.emplace_back(arc::kArcVmName);
vm_names_in_notification.emplace_back(vm_name);
settings_sub_page =
chromeos::settings::mojom::kArcVmUsbPreferencesSubpagePath;
}
if (bruschetta::IsInstalled(profile(), bruschetta::GetBruschettaAlphaId())) {
vm_name = bruschetta::GetOverallVmName(profile());
rich_notification_data.buttons.emplace_back(
message_center::ButtonInfo(l10n_util::GetStringFUTF16(
IDS_CROSUSB_NOTIFICATION_BUTTON_CONNECT_TO_VM, vm_name)));
vm_names.emplace_back(bruschetta::kBruschettaVmName);
vm_names_in_notification.emplace_back(vm_name);
settings_sub_page =
chromeos::settings::mojom::kBruschettaUsbPreferencesSubpagePath;
}
DCHECK(vm_names_in_notification.size());
std::u16string message = l10n_util::GetStringFUTF16(
IDS_CROSUSB_DEVICE_DETECTED_NOTIFICATION, label,
CombineVmNames(vm_names_in_notification));
if (vm_names.size() > 1) {
settings_sub_page = std::string();
}
std::string notification_id = CrosUsbDetector::MakeNotificationId(guid);
message_center::Notification notification(
message_center::NOTIFICATION_TYPE_MULTIPLE, notification_id,
l10n_util::GetStringUTF16(IDS_CROSUSB_DEVICE_DETECTED_NOTIFICATION_TITLE),
message, ui::ImageModel(), std::u16string(), GURL(),
message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
kNotifierUsb,
NotificationCatalogName::kCrosUSBDetector),
rich_notification_data,
base::MakeRefCounted<CrosUsbNotificationDelegate>(
notification_id, guid, std::move(vm_names),
std::move(settings_sub_page)));
SystemNotificationHelper::GetInstance()->Display(notification);
}
class FilesystemUnmounter : public base::RefCounted<FilesystemUnmounter> {
public:
static void UnmountPaths(const std::set<std::string>& paths,
base::OnceCallback<void(bool success)> callback);
private:
friend class base::RefCounted<FilesystemUnmounter>;
explicit FilesystemUnmounter(base::OnceCallback<void(bool success)> callback)
: callback_(std::move(callback)) {}
~FilesystemUnmounter() { std::move(callback_).Run(success_); }
void OnUnmountPath(MountError mount_error);
bool success_ = true;
base::OnceCallback<void(bool success)> callback_;
};
void FilesystemUnmounter::UnmountPaths(
const std::set<std::string>& paths,
base::OnceCallback<void(bool success)> callback) {
scoped_refptr<FilesystemUnmounter> unmounter =
new FilesystemUnmounter(std::move(callback));
// When the last UnmountPath() calls completes, the ref count reaches zero
// and the destructor fires the callback. We can't use base::BarrierClosure()
// because we need to aggregate the MountError results.
for (const std::string& path : paths) {
disks::DiskMountManager::GetInstance()->UnmountPath(
path, base::BindOnce(&FilesystemUnmounter::OnUnmountPath, unmounter));
}
}
void FilesystemUnmounter::OnUnmountPath(MountError mount_error) {
if (mount_error != MountError::kSuccess) {
LOG(ERROR) << "Error unmounting USB drive: " << mount_error;
success_ = false;
}
}
} // namespace
CrosUsbDeviceInfo::CrosUsbDeviceInfo(
std::string guid,
std::u16string label,
std::optional<guest_os::GuestId> shared_guest_id,
uint16_t vendor_id,
uint16_t product_id,
std::string serial_number,
bool prompt_before_sharing)
: guid(guid),
label(label),
shared_guest_id(shared_guest_id),
vendor_id(vendor_id),
product_id(product_id),
serial_number(serial_number),
prompt_before_sharing(prompt_before_sharing) {}
CrosUsbDeviceInfo::CrosUsbDeviceInfo(const CrosUsbDeviceInfo&) = default;
CrosUsbDeviceInfo::~CrosUsbDeviceInfo() = default;
std::string CrosUsbDetector::MakeNotificationId(const std::string& guid) {
return "cros:" + guid;
}
CrosUsbDetector::DeviceClaim::DeviceClaim() = default;
CrosUsbDetector::DeviceClaim::~DeviceClaim() = default;
// static
CrosUsbDetector* CrosUsbDetector::Get() {
return g_cros_usb_detector;
}
CrosUsbDetector::CrosUsbDetector() {
DCHECK(!g_cros_usb_detector);
g_cros_usb_detector = this;
// If *ALL* interfaces of a device match the below list, no notification will
// be shown.
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_CDC_DATA));
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_HID));
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_PHYSICAL));
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_AUDIO));
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_STILL_IMAGE));
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_MASS_STORAGE));
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_VIDEO));
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_BILLBOARD));
guest_os_usb_int_all_filter_.emplace_back(
UsbFilterByClassCode(USB_CLASS_PERSONAL_HEALTHCARE));
// If *ANY* interfaces of a device match the below list, no notification will
// be shown.
guest_os_usb_int_any_filter_.emplace_back(UsbFilterByClassAndSubclassCode(
USB_CLASS_COMM, USB_COMM_SUBCLASS_ETHERNET));
CiceroneClient::Get()->AddObserver(this);
ConciergeClient::Get()->AddVmObserver(this);
VmPluginDispatcherClient::Get()->AddObserver(this);
disks::DiskMountManager::GetInstance()->AddObserver(this);
}
CrosUsbDetector::~CrosUsbDetector() {
DCHECK_EQ(this, g_cros_usb_detector);
disks::DiskMountManager::GetInstance()->RemoveObserver(this);
CiceroneClient::Get()->RemoveObserver(this);
ConciergeClient::Get()->RemoveVmObserver(this);
VmPluginDispatcherClient::Get()->RemoveObserver(this);
g_cros_usb_detector = nullptr;
}
void CrosUsbDetector::SetDeviceManagerForTesting(
mojo::PendingRemote<device::mojom::UsbDeviceManager> device_manager) {
DCHECK(!device_manager_) << "device_manager_ was already initialized";
device_manager_.Bind(std::move(device_manager));
}
void CrosUsbDetector::AddUsbDeviceObserver(CrosUsbDeviceObserver* observer) {
usb_device_observers_.AddObserver(observer);
}
void CrosUsbDetector::RemoveUsbDeviceObserver(CrosUsbDeviceObserver* observer) {
usb_device_observers_.RemoveObserver(observer);
}
void CrosUsbDetector::SignalUsbDeviceObservers() {
for (auto& observer : usb_device_observers_) {
observer.OnUsbDevicesChanged();
}
}
std::vector<CrosUsbDeviceInfo> CrosUsbDetector::GetShareableDevices() const {
std::vector<CrosUsbDeviceInfo> result;
for (const auto& it : usb_devices_) {
const UsbDevice& device = it.second;
std::string serial_number =
device.info->serial_number.has_value()
? base::UTF16ToASCII(device.info->serial_number.value()).c_str()
: "";
result.emplace_back(
device.info->guid, device.label, device.shared_guest_id,
device.info->vendor_id, device.info->product_id, serial_number,
/*prompt_before_sharing=*/
device.shared_guest_id.has_value() || !device.mount_points.empty());
}
return result;
}
CrosUsbDetector::UsbDevice::UsbDevice() = default;
CrosUsbDetector::UsbDevice::UsbDevice(UsbDevice&&) = default;
CrosUsbDetector::UsbDevice::~UsbDevice() = default;
void CrosUsbDetector::ConnectToDeviceManager() {
// Tests may set a fake manager.
if (!device_manager_) {
content::GetDeviceService().BindUsbDeviceManager(
device_manager_.BindNewPipeAndPassReceiver());
}
DCHECK(device_manager_);
device_manager_.set_disconnect_handler(
base::BindOnce(&CrosUsbDetector::OnDeviceManagerConnectionError,
weak_ptr_factory_.GetWeakPtr()));
// Listen for added/removed device events.
DCHECK(!client_receiver_.is_bound());
device_manager_->EnumerateDevicesAndSetClient(
client_receiver_.BindNewEndpointAndPassRemote(),
base::BindOnce(&CrosUsbDetector::OnListAttachedDevices,
weak_ptr_factory_.GetWeakPtr()));
}
bool CrosUsbDetector::ShouldShowNotification(const UsbDevice& device) {
PrefService* prefs = profile()->GetPrefs();
if (!prefs->GetBoolean(ash::prefs::kUsbDetectorNotificationEnabled) ||
!prefs->GetBoolean(guest_os::prefs::kGuestOsUSBNotificationEnabled)) {
return false;
}
if (!crostini::CrostiniFeatures::Get()->IsEnabled(profile()) &&
!plugin_vm::PluginVmFeatures::Get()->IsEnabled(profile()) &&
!IsPlayStoreEnabledWithArcVmForProfile(profile()) &&
!bruschetta::IsInstalled(profile(), bruschetta::GetBruschettaAlphaId())) {
return false;
}
bool all_filter_cleared =
GetFilteredInterfacesMask(guest_os_usb_int_all_filter_, *device.info) !=
0;
bool any_filter_cleared =
GetFilteredInterfacesMask(guest_os_usb_int_any_filter_, *device.info) ==
GetUsbInterfaceBaseMask(*device.info);
return all_filter_cleared && any_filter_cleared;
}
void CrosUsbDetector::OnContainerStarted(
const vm_tools::cicerone::ContainerStartedSignal& signal) {
const auto guest_id =
guest_os::GuestId(signal.vm_name(), signal.container_name());
for (auto& it : usb_devices_) {
auto& device = it.second;
if (device.shared_guest_id == guest_id && device.guest_port.has_value()) {
VLOG(1) << "Connecting " << device.label << " to " << guest_id.vm_name
<< ":" << guest_id.container_name;
AttachUsbDeviceToContainer(guest_id, *device.guest_port,
device.info->guid, base::DoNothing());
}
}
}
void CrosUsbDetector::OnLxdContainerDeleted(
const vm_tools::cicerone::LxdContainerDeletedSignal& signal) {
if (signal.status() ==
vm_tools::cicerone::LxdContainerDeletedSignal_Status_DELETED) {
const auto guest_id =
guest_os::GuestId(signal.vm_name(), signal.container_name());
for (auto& it : usb_devices_) {
auto& device = it.second;
if (device.shared_guest_id == guest_id) {
VLOG(1) << "Detaching " << device.label << " from deleted container "
<< guest_id.vm_name << ":" << guest_id.container_name;
DetachUsbDeviceFromVm(guest_id.vm_name, device.info->guid,
base::DoNothing());
}
}
}
}
void CrosUsbDetector::OnVmStarted(
const vm_tools::concierge::VmStartedSignal& signal) {
ConnectSharedDevicesOnVmStartup(signal.name());
}
void CrosUsbDetector::OnVmStopped(
const vm_tools::concierge::VmStoppedSignal& signal) {
DisconnectSharedDevicesOnVmShutdown(signal.name());
}
void CrosUsbDetector::OnVmToolsStateChanged(
const vm_tools::plugin_dispatcher::VmToolsStateChangedSignal& signal) {}
void CrosUsbDetector::OnVmStateChanged(
const vm_tools::plugin_dispatcher::VmStateChangedSignal& signal) {
if (signal.vm_state() ==
vm_tools::plugin_dispatcher::VmState::VM_STATE_RUNNING) {
ConnectSharedDevicesOnVmStartup(signal.vm_name());
} else if (signal.vm_state() ==
vm_tools::plugin_dispatcher::VmState::VM_STATE_STOPPED) {
DisconnectSharedDevicesOnVmShutdown(signal.vm_name());
}
}
void CrosUsbDetector::OnMountEvent(
disks::DiskMountManager::MountEvent event,
MountError error_code,
const disks::DiskMountManager::MountPoint& mount_info) {
if (mount_info.mount_type != MountType::kDevice ||
error_code != MountError::kSuccess) {
return;
}
const auto* disk =
disks::DiskMountManager::GetInstance()->FindDiskBySourcePath(
mount_info.source_path);
// This can be null if a drive is physically removed.
if (!disk) {
return;
}
for (auto& it : usb_devices_) {
UsbDevice& device = it.second;
if (disk->bus_number() ==
base::checked_cast<int64_t>(device.info->bus_number) &&
disk->device_number() ==
base::checked_cast<int64_t>(device.info->port_number)) {
bool was_empty = device.mount_points.empty();
if (event == disks::DiskMountManager::MOUNTING) {
device.mount_points.insert(mount_info.mount_path);
} else {
device.mount_points.erase(mount_info.mount_path);
}
if (!device.is_unmounting && was_empty != device.mount_points.empty()) {
SignalUsbDeviceObservers();
}
return;
}
}
}
std::string UsbDeviceIdentifier(device::mojom::UsbDeviceInfoPtr& device_info) {
std::string serial_number =
device_info->serial_number.has_value()
? base::UTF16ToASCII(device_info->serial_number.value()).c_str()
: "";
return base::StringPrintf("%d:%d:%s", device_info->vendor_id,
device_info->product_id, serial_number.c_str());
}
void CrosUsbDetector::OnDeviceChecked(
device::mojom::UsbDeviceInfoPtr device_info,
bool hide_notification,
bool allowed) {
if (!allowed) {
LOG(WARNING) << "Device not allowed by Permission Broker. vendor: 0x"
<< std::hex << device_info->vendor_id << " product: 0x"
<< device_info->product_id;
return;
}
UsbDevice new_device;
new_device.label = ProductLabelFromDevice(*device_info);
// Storage devices already plugged in at log-in time will already be mounted.
for (const auto& disk : disks::DiskMountManager::GetInstance()->disks()) {
if (disk->bus_number() ==
base::checked_cast<int64_t>(device_info->bus_number) &&
disk->device_number() ==
base::checked_cast<int64_t>(device_info->port_number) &&
disk->is_mounted()) {
new_device.mount_points.insert(disk->mount_path());
}
}
// Copy fields prior to moving |device_info| and |new_device|.
std::string guid = device_info->guid;
std::u16string label = new_device.label;
// If device exists in persistent passthrough dict, skip notifications and
// connect it to the appropriate guest.
PrefService* prefs = profile()->GetPrefs();
const base::Value::Dict& persistent_passthrough_devices =
prefs->GetDict(guest_os::prefs::kGuestOsUSBPersistentPassthroughDevices);
const std::string* device = persistent_passthrough_devices.FindString(
UsbDeviceIdentifier(device_info));
new_device.info = std::move(device_info);
auto result = usb_devices_.emplace(guid, std::move(new_device));
if (!result.second) {
LOG(ERROR) << "Ignoring USB device " << label << " as guid already exists.";
return;
}
SignalUsbDeviceObservers();
if (device) {
const std::string& device_ref = CHECK_DEREF(device);
std::optional<guest_os::GuestId> guest_id =
guest_os::Deserialize(device_ref);
if (guest_id.has_value()) {
AttachUsbDeviceToGuest(guest_id.value(), guid, base::DoNothing());
return;
}
}
// Some devices should not trigger the notification.
if (hide_notification || !ShouldShowNotification(result.first->second)) {
VLOG(1) << "Not showing USB notification for " << label;
return;
}
ShowNotificationForDevice(guid, label);
}
void CrosUsbDetector::OnDeviceAdded(device::mojom::UsbDeviceInfoPtr device) {
CrosUsbDetector::OnDeviceAdded(std::move(device), false);
}
void CrosUsbDetector::OnDeviceAdded(device::mojom::UsbDeviceInfoPtr device_info,
bool hide_notification) {
std::string guid = device_info->guid;
device_manager_->CheckAccess(
guid, base::BindOnce(&CrosUsbDetector::OnDeviceChecked,
weak_ptr_factory_.GetWeakPtr(),
std::move(device_info), hide_notification));
}
void CrosUsbDetector::OnDeviceRemoved(
device::mojom::UsbDeviceInfoPtr device_info) {
SystemNotificationHelper::GetInstance()->Close(
CrosUsbDetector::MakeNotificationId(device_info->guid));
std::string guid = device_info->guid;
auto it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(ERROR) << "Unknown USB device removed: "
<< ProductLabelFromDevice(*device_info);
return;
}
if (it->second.shared_guest_id.has_value()) {
DetachUsbDeviceFromVm(it->second.shared_guest_id->vm_name, guid,
base::DoNothing());
}
usb_devices_.erase(it);
SignalUsbDeviceObservers();
}
void CrosUsbDetector::OnDeviceManagerConnectionError() {
device_manager_.reset();
client_receiver_.reset();
ConnectToDeviceManager();
}
void CrosUsbDetector::ConnectSharedDevicesOnVmStartup(
const std::string& vm_name) {
// Reattach shared devices when the VM becomes available.
for (auto& it : usb_devices_) {
auto& device = it.second;
if (device.shared_guest_id.has_value() &&
device.shared_guest_id->vm_name == vm_name) {
VLOG(1) << "Connecting " << device.label << " to " << vm_name;
// Clear any older guest_port setting.
device.guest_port = std::nullopt;
AttachUsbDeviceToGuest(*device.shared_guest_id, device.info->guid,
base::DoNothing());
}
}
}
void CrosUsbDetector::DisconnectSharedDevicesOnVmShutdown(
const std::string& vm_name) {
// Clear guest_port on shared devices when the VM shuts down.
for (auto& it : usb_devices_) {
auto& device = it.second;
if (device.shared_guest_id.has_value() &&
device.shared_guest_id->vm_name == vm_name) {
VLOG(1) << device.label << " is disconnected from " << vm_name;
device.guest_port = std::nullopt;
}
}
}
void CrosUsbDetector::AttachUsbDeviceToGuest(
const guest_os::GuestId& guest_id,
const std::string& guid,
base::OnceCallback<void(bool success)> callback) {
const auto& it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(WARNING) << "Attempted to attach device that does not exist: " << guid;
std::move(callback).Run(false);
return;
}
auto& device = it->second;
// If we tried to share a device to a VM that wasn't started,
// |shared_guest_id| would be set but |guest_port| would be empty. Once the VM
// is started, we go through this flow again.
if (device.guest_port.has_value()) {
if (device.shared_guest_id == guest_id) {
LOG(WARNING) << "Device " << device.label << " is already shared with vm "
<< guest_id.vm_name;
std::move(callback).Run(true);
return;
} else if (device.shared_guest_id->vm_name == guest_id.vm_name &&
device.shared_guest_id->container_name !=
guest_id.container_name) {
// The device is already shared with VM but in wrong container. In case
// the new container is stopped, detach it from the old container first,
// so that it can be attached later.
DetachUsbDeviceFromContainer(
guest_id.vm_name, *device.guest_port, device.info->guid,
base::BindOnce(&CrosUsbDetector::ContainerAttachAfterDetach,
weak_ptr_factory_.GetWeakPtr(), guest_id,
*device.guest_port, guid, std::move(callback)));
return;
}
}
UnmountFilesystems(guest_id, guid, std::move(callback));
}
void CrosUsbDetector::DetachUsbDeviceFromVm(
const std::string& vm_name,
const std::string& guid,
base::OnceCallback<void(bool success)> callback) {
const auto& it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(WARNING) << "Attempted to detach device that does not exist: " << guid;
std::move(callback).Run(/*success=*/true);
return;
}
UsbDevice& device = it->second;
if (!device.shared_guest_id.has_value() ||
device.shared_guest_id->vm_name != vm_name) {
LOG(WARNING) << "Failed to detach " << guid << " from " << vm_name
<< ". It appears to be shared with "
<< (device.shared_guest_id.has_value()
? device.shared_guest_id->vm_name
: "[not shared]")
<< " at port "
<< (device.guest_port
? base::NumberToString(*device.guest_port)
: "[not attached]")
<< ".";
std::move(callback).Run(/*success=*/false);
return;
}
if (!device.guest_port) {
// The VM hasn't been started yet, attaching is in progress, or attaching
// failed.
// TODO(timloh): Check what happens if attaching to a different VM races
// with an in progress attach.
RelinquishDeviceClaim(guid);
device.shared_guest_id = std::nullopt;
SignalUsbDeviceObservers();
std::move(callback).Run(/*success=*/true);
return;
}
vm_tools::concierge::DetachUsbDeviceRequest request;
request.set_vm_name(vm_name);
request.set_owner_id(crostini::CryptohomeIdForProfile(profile()));
request.set_guest_port(*device.guest_port);
ConciergeClient::Get()->DetachUsbDevice(
std::move(request),
base::BindOnce(&CrosUsbDetector::OnUsbDeviceDetachFinished,
weak_ptr_factory_.GetWeakPtr(), vm_name, guid,
std::move(callback)));
}
void CrosUsbDetector::OnListAttachedDevices(
std::vector<device::mojom::UsbDeviceInfoPtr> devices) {
for (device::mojom::UsbDeviceInfoPtr& device_info : devices) {
CrosUsbDetector::OnDeviceAdded(std::move(device_info),
/*hide_notification*/ true);
}
}
void CrosUsbDetector::UnmountFilesystems(
const guest_os::GuestId& guest_id,
const std::string& guid,
base::OnceCallback<void(bool success)> callback) {
auto it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(ERROR) << "Couldn't find device " << guid;
std::move(callback).Run(false);
return;
}
it->second.is_unmounting = true;
FilesystemUnmounter::UnmountPaths(
it->second.mount_points,
base::BindOnce(&CrosUsbDetector::OnUnmountFilesystems,
weak_ptr_factory_.GetWeakPtr(), guest_id, guid,
std::move(callback)));
}
void CrosUsbDetector::OnUnmountFilesystems(
const guest_os::GuestId& guest_id,
const std::string& guid,
base::OnceCallback<void(bool success)> callback,
bool unmount_success) {
auto it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(ERROR) << "Couldn't find device " << guid;
std::move(callback).Run(false);
return;
}
UsbDevice& device = it->second;
device.is_unmounting = false;
if (!unmount_success) {
// FilesystemUnmounter already logged the error.
std::move(callback).Run(false);
return;
}
// Detach first if device is attached elsewhere
if (device.guest_port.has_value()) {
DetachUsbDeviceFromVm(device.shared_guest_id->vm_name, guid,
base::BindOnce(&CrosUsbDetector::AttachAfterDetach,
weak_ptr_factory_.GetWeakPtr(),
guest_id, guid, std::move(callback)));
} else {
// The device isn't attached.
AttachAfterDetach(guest_id, guid, std::move(callback),
/*detach_success=*/true);
}
}
void CrosUsbDetector::AttachAfterDetach(
const guest_os::GuestId& guest_id,
const std::string& guid,
base::OnceCallback<void(bool success)> callback,
bool detach_success) {
if (!detach_success) {
LOG(ERROR) << "Failed to detatch before attach";
std::move(callback).Run(false);
return;
}
auto it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(ERROR) << "No device info for " << guid;
std::move(callback).Run(false);
return;
}
auto& device = it->second;
// Mark the USB device shared so that it will be shared when the VM starts
// if it isn't started yet. This also ensures the UI will show the device as
// shared. The guest_port will be set later.
device.shared_guest_id = guest_id;
auto claim_it = devices_claimed_.find(guid);
if (claim_it != devices_claimed_.end()) {
if (claim_it->second.device_file.is_valid()) {
// We take a dup here which will be closed if DoVmAttach fails.
base::ScopedFD device_fd(dup(claim_it->second.device_file.get()));
DoVmAttach(guest_id, device.info.Clone(), std::move(device_fd),
std::move(callback));
} else {
LOG(WARNING) << "Device " << guid << " already claimed and awaiting fd.";
std::move(callback).Run(false);
}
return;
}
VLOG(1) << "Opening " << guid;
base::ScopedFD read_end, write_end;
if (!base::CreatePipe(&read_end, &write_end, /*non_blocking=*/true)) {
LOG(ERROR) << "Couldn't create pipe for " << guid;
std::move(callback).Run(false);
return;
}
VLOG(1) << "Saving lifeline_fd " << write_end.get();
devices_claimed_[guid].lifeline_file = std::move(write_end);
// Open a file descriptor to pass to CrostiniManager & Concierge.
device_manager_->OpenFileDescriptor(
guid, kAllInterfacesMask, mojo::PlatformHandle(std::move(read_end)),
base::BindOnce(&CrosUsbDetector::OnAttachUsbDeviceOpened,
weak_ptr_factory_.GetWeakPtr(), guest_id,
device.info.Clone(), std::move(callback)));
// Close any associated notifications (the user isn't using them). This
// destroys the CrosUsbNotificationDelegate and vm_name and guid args may be
// invalid after Close.
SystemNotificationHelper::GetInstance()->Close(
CrosUsbDetector::MakeNotificationId(guid));
}
void CrosUsbDetector::OnAttachUsbDeviceOpened(
const guest_os::GuestId& guest_id,
device::mojom::UsbDeviceInfoPtr device_info,
base::OnceCallback<void(bool success)> callback,
base::File file) {
if (!file.IsValid()) {
LOG(ERROR) << "Permission broker refused access to USB device";
std::move(callback).Run(/*success=*/false);
return;
}
devices_claimed_[device_info->guid].device_file =
base::ScopedFD(file.Duplicate().TakePlatformFile());
if (!manager()) {
LOG(ERROR) << "Attaching device without Crostini manager instance";
std::move(callback).Run(/*success=*/false);
return;
}
DoVmAttach(guest_id, device_info.Clone(),
base::ScopedFD(file.TakePlatformFile()), std::move(callback));
}
void CrosUsbDetector::DoVmAttach(
const guest_os::GuestId& guest_id,
device::mojom::UsbDeviceInfoPtr device_info,
base::ScopedFD fd,
base::OnceCallback<void(bool success)> callback) {
vm_tools::concierge::AttachUsbDeviceRequest request;
request.set_vm_name(guest_id.vm_name);
request.set_owner_id(crostini::CryptohomeIdForProfile(profile()));
request.set_bus_number(device_info->bus_number);
request.set_port_number(device_info->port_number);
request.set_vendor_id(device_info->vendor_id);
request.set_product_id(device_info->product_id);
ConciergeClient::Get()->AttachUsbDevice(
std::move(fd), std::move(request),
base::BindOnce(&CrosUsbDetector::OnUsbDeviceAttachFinished,
weak_ptr_factory_.GetWeakPtr(), guest_id,
std::move(device_info), std::move(callback)));
}
void CrosUsbDetector::OnUsbDeviceAttachFinished(
const guest_os::GuestId& guest_id,
device::mojom::UsbDeviceInfoPtr device_info,
base::OnceCallback<void(bool success)> callback,
std::optional<vm_tools::concierge::AttachUsbDeviceResponse> response) {
bool success = true;
if (!response) {
LOG(ERROR) << "Failed to attach USB device, empty dbus response";
success = false;
} else if (!response->success()) {
LOG(ERROR) << "Failed to attach USB device, " << response->reason();
success = false;
}
if (success) {
auto it = usb_devices_.find(device_info->guid);
if (it == usb_devices_.end()) {
LOG(WARNING) << "Dbus response indicates successful attach but device "
<< "info was missing for " << device_info->guid;
success = false;
} else {
it->second.shared_guest_id = guest_id;
it->second.guest_port = response->guest_port();
}
}
PrefService* prefs = profile()->GetPrefs();
if (success &&
prefs->GetBoolean(
guest_os::prefs::kGuestOsUSBPersistentPassthroughEnabled)) {
ScopedDictPrefUpdate update(
prefs, guest_os::prefs::kGuestOsUSBPersistentPassthroughDevices);
base::Value::Dict& devices = update.Get();
std::string device_identifier = UsbDeviceIdentifier(device_info);
// there are 3 possible scenarios here:
// 1 - device was not in list. in this case we definitely want to add it.
// 2 - device was in list for a different guest. in this case we want to
// override the previous state.
// 3 - device was in list, with the current guest. we already have to
// serialize the guest_id to check, so not much more different in
// comparing vs writing the same thing back again.
devices.Set(device_identifier, guest_id.Serialize());
}
if (success && !guest_id.container_name.empty()) {
AttachUsbDeviceToContainer(guest_id, response->guest_port(),
device_info->guid, std::move(callback));
} else {
SignalUsbDeviceObservers();
std::move(callback).Run(success);
}
}
void CrosUsbDetector::AttachUsbDeviceToContainer(
const guest_os::GuestId& guest_id,
uint8_t guest_port,
const std::string& guid,
base::OnceCallback<void(bool success)> callback) {
vm_tools::cicerone::AttachUsbToContainerRequest request;
request.set_vm_name(guest_id.vm_name);
request.set_container_name(guest_id.container_name);
request.set_owner_id(crostini::CryptohomeIdForProfile(profile()));
request.set_port_num(static_cast<int32_t>(guest_port));
CiceroneClient::Get()->AttachUsbToContainer(
std::move(request),
base::BindOnce(&CrosUsbDetector::OnContainerAttachFinished,
weak_ptr_factory_.GetWeakPtr(), guest_id, guid,
std::move(callback)));
}
void CrosUsbDetector::OnContainerAttachFinished(
const guest_os::GuestId& guest_id,
const std::string& guid,
base::OnceCallback<void(bool success)> callback,
std::optional<vm_tools::cicerone::AttachUsbToContainerResponse> response) {
bool success = true;
if (!response) {
LOG(ERROR) << "Failed to attach USB device, empty dbus response";
success = false;
} else if (response->status() !=
vm_tools::cicerone::AttachUsbToContainerResponse_Status_OK) {
LOG(ERROR) << "Failed to attach USB device, " << response->failure_reason();
success = false;
}
if (success) {
const auto& it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(WARNING) << "Dbus response indicates successful attach but device "
<< "info was missing for " << guid;
success = false;
} else {
it->second.shared_guest_id = guest_id;
}
}
SignalUsbDeviceObservers();
std::move(callback).Run(success);
}
void CrosUsbDetector::DetachUsbDeviceFromContainer(
const std::string& vm_name,
uint8_t guest_port,
const std::string& guid,
base::OnceCallback<void(bool success)> callback) {
vm_tools::cicerone::DetachUsbFromContainerRequest request;
request.set_vm_name(vm_name);
request.set_owner_id(crostini::CryptohomeIdForProfile(profile()));
request.set_port_num(static_cast<int32_t>(guest_port));
CiceroneClient::Get()->DetachUsbFromContainer(
std::move(request),
base::BindOnce(&CrosUsbDetector::OnContainerDetachFinished,
weak_ptr_factory_.GetWeakPtr(), vm_name, guid,
std::move(callback)));
}
void CrosUsbDetector::OnContainerDetachFinished(
const std::string& vm_name,
const std::string& guid,
base::OnceCallback<void(bool success)> callback,
std::optional<vm_tools::cicerone::DetachUsbFromContainerResponse>
response) {
bool success = true;
if (!response) {
LOG(ERROR) << "Failed to attach USB device, empty dbus response";
success = false;
} else if (response->status() !=
vm_tools::cicerone::DetachUsbFromContainerResponse_Status_OK) {
LOG(ERROR) << "Failed to attach USB device, " << response->failure_reason();
success = false;
}
if (success) {
const auto& it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(WARNING) << "Dbus response indicates successful detach but device "
<< "info was missing for " << guid;
success = false;
} else {
it->second.shared_guest_id->container_name = "";
}
}
SignalUsbDeviceObservers();
std::move(callback).Run(success);
}
void CrosUsbDetector::ContainerAttachAfterDetach(
const guest_os::GuestId& guest_id,
uint8_t guest_port,
const std::string& guid,
base::OnceCallback<void(bool success)> callback,
bool detach_success) {
if (!detach_success) {
LOG(ERROR) << "Failed to detach from container before attach";
std::move(callback).Run(false);
return;
}
const auto& it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(ERROR) << "No device info for " << guid;
std::move(callback).Run(false);
return;
}
auto& device = it->second;
if (device.shared_guest_id->vm_name != guest_id.vm_name) {
LOG(ERROR) << "Unexpected VM name for device " << guid;
std::move(callback).Run(false);
return;
} else if (device.guest_port != guest_port) {
LOG(ERROR) << "Unexpected guest port for device " << guid;
std::move(callback).Run(false);
return;
}
// Set the container name so if the container is stopped, the device will be
// attached after the container starts.
device.shared_guest_id->container_name = guest_id.container_name;
AttachUsbDeviceToContainer(guest_id, guest_port, guid, std::move(callback));
}
void CrosUsbDetector::OnUsbDeviceDetachFinished(
const std::string& vm_name,
const std::string& guid,
base::OnceCallback<void(bool success)> callback,
std::optional<vm_tools::concierge::DetachUsbDeviceResponse> response) {
bool success = true;
if (!response) {
LOG(ERROR) << "Failed to detach USB device, empty dbus response";
success = false;
} else if (!response->success()) {
LOG(ERROR) << "Failed to detach USB device, " << response->reason();
success = false;
}
auto it = usb_devices_.find(guid);
if (it == usb_devices_.end()) {
LOG(WARNING) << "Dbus response indicates successful detach but device info "
<< "was missing for " << guid;
} else {
it->second.shared_guest_id = std::nullopt;
it->second.guest_port = std::nullopt;
}
RelinquishDeviceClaim(guid);
SignalUsbDeviceObservers();
std::move(callback).Run(success);
}
void CrosUsbDetector::RelinquishDeviceClaim(const std::string& guid) {
auto it = devices_claimed_.find(guid);
if (it != devices_claimed_.end()) {
VLOG(1) << "Closing lifeline_fd " << it->second.lifeline_file.get();
devices_claimed_.erase(it);
} else {
LOG(ERROR) << "Relinquishing device with no prior claim: " << guid;
}
}
} // namespace ash