--- services/device/hid/hid_service_freebsd.cc.orig 2023-04-08 11:38:38 UTC +++ services/device/hid/hid_service_freebsd.cc @@ -0,0 +1,395 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "services/device/hid/hid_service_freebsd.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include "base/files/file_descriptor_watcher_posix.h" +#include "base/files/file_enumerator.h" +#include "base/files/file_util.h" +#include "base/files/file.h" +#include "base/location.h" +#include "base/logging.h" +#include "base/posix/eintr_wrapper.h" +#include "base/stl_util.h" +#include "base/strings/pattern.h" +#include "base/strings/stringprintf.h" +#include "base/strings/sys_string_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/string_split.h" +#include "base/task/single_thread_task_runner.h" +#include "base/task/thread_pool.h" +#include "base/threading/scoped_blocking_call.h" +#include "base/threading/thread_restrictions.h" +#include "components/device_event_log/device_event_log.h" +#include "services/device/hid/hid_connection_freebsd.h" + +const int kMaxPermissionChecks = 5; + +namespace device { + +struct HidServiceFreeBSD::ConnectParams { + ConnectParams(scoped_refptr device_info, + bool allow_protected_reports, + bool allow_fido_reports, + ConnectCallback callback) + : device_info(std::move(device_info)), + allow_protected_reports(allow_protected_reports), + allow_fido_reports(allow_fido_reports), + callback(std::move(callback)), + task_runner(base::SequencedTaskRunner::GetCurrentDefault()), + blocking_task_runner( + base::ThreadPool::CreateSequencedTaskRunner(kBlockingTaskTraits)) {} + ~ConnectParams() {} + + scoped_refptr device_info; + bool allow_protected_reports; + bool allow_fido_reports; + ConnectCallback callback; + scoped_refptr task_runner; + scoped_refptr blocking_task_runner; + base::ScopedFD fd; +}; + +class HidServiceFreeBSD::BlockingTaskRunnerHelper { + public: + BlockingTaskRunnerHelper(base::WeakPtr service) + : service_(std::move(service)), + task_runner_(base::SequencedTaskRunner::GetCurrentDefault()) { + DETACH_FROM_SEQUENCE(sequence_checker_); + + timer_.reset(new base::RepeatingTimer()); + devd_buffer_ = new net::IOBufferWithSize(1024); + } + + BlockingTaskRunnerHelper(const BlockingTaskRunnerHelper&) = delete; + BlockingTaskRunnerHelper& operator=(const BlockingTaskRunnerHelper&) = delete; + + ~BlockingTaskRunnerHelper() { + } + + void Start() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const base::FilePath kDevRoot("/dev"); + const std::string kUHIDPattern("/dev/uhid*"); + + base::FileEnumerator enumerator(kDevRoot, false, base::FileEnumerator::FILES); + do { + const base::FilePath next_device_path(enumerator.Next()); + const std::string next_device = next_device_path.value(); + if (next_device.empty()) + break; + + if (base::MatchPattern(next_device, kUHIDPattern)) + OnDeviceAdded(next_device.substr(5)); + } while (true); + + SetupDevdMonitor(); + + task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&HidServiceFreeBSD::FirstEnumerationComplete, service_)); + } + + bool HaveReadWritePermissions(std::string device_id) { + std::string device_node = "/dev/" + device_id; + base::internal::AssertBlockingAllowed(); + + base::FilePath device_path(device_node); + base::File device_file; + int flags = + base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_WRITE; + device_file.Initialize(device_path, flags); + if (!device_file.IsValid()) + return false; + + return true; + } + + void OnDeviceAdded(std::string device_id) { + base::ScopedBlockingCall scoped_blocking_call( + FROM_HERE, base::BlockingType::MAY_BLOCK); + std::string device_node = "/dev/" + device_id; + uint16_t vendor_id = 0xffff; + uint16_t product_id = 0xffff; + std::string product_name = ""; + std::string serial_number = ""; + + std::vector report_descriptor; + + base::internal::AssertBlockingAllowed(); + + base::FilePath device_path(device_node); + base::File device_file; + int flags = + base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_WRITE; + device_file.Initialize(device_path, flags); + if (!device_file.IsValid()) { + HID_LOG(ERROR) << "Failed to open '" << device_node + << "': " + << base::File::ErrorToString(device_file.error_details()); + return; + } + + base::ScopedFD fd; + fd.reset(device_file.TakePlatformFile()); + + struct usb_gen_descriptor ugd; + ugd.ugd_data = NULL; + ugd.ugd_maxlen = 0xffff; + int result = HANDLE_EINTR( + ioctl(fd.get(), USB_GET_REPORT_DESC, &ugd)); + + if (result < 0) { + HID_LOG(ERROR) << "Failed to get report descriptor size"; + return; + } + + report_descriptor.resize(ugd.ugd_actlen); + + ugd.ugd_data = report_descriptor.data(); + ugd.ugd_maxlen = ugd.ugd_actlen; + result = HANDLE_EINTR( + ioctl(fd.get(), USB_GET_REPORT_DESC, &ugd)); + + if (result < 0) { + HID_LOG(ERROR) << "Failed to get report descriptor"; + return; + } + + scoped_refptr device_info(new HidDeviceInfo( + device_id, + /*physical_device_id*/"", + vendor_id, + product_id, + product_name, + serial_number, + device::mojom::HidBusType::kHIDBusTypeUSB, + report_descriptor, + device_node)); + + task_runner_->PostTask(FROM_HERE, base::BindOnce(&HidServiceFreeBSD::AddDevice, + service_, device_info)); + } + + void OnDeviceRemoved(std::string device_id) { + base::ScopedBlockingCall scoped_blocking_call( + FROM_HERE, base::BlockingType::MAY_BLOCK); + task_runner_->PostTask( + FROM_HERE, base::BindOnce(&HidServiceFreeBSD::RemoveDevice, service_, + device_id)); + } + + private: + + void CheckPendingPermissionChange() { + base::internal::AssertBlockingAllowed(); + std::map::iterator it; + for (it = permissions_checks_attempts_.begin(); it != permissions_checks_attempts_.end();) { + std::string device_name = it->first; + bool keep = true; + if (HaveReadWritePermissions(device_name)) { + OnDeviceAdded(device_name); + keep = false; + } + else if (it->second-- <= 0) { + HID_LOG(ERROR) << "Still don't have write permissions to '" << device_name + << "' after " << kMaxPermissionChecks << " attempts"; + keep = false; + } + + if (keep) + ++it; + else + permissions_checks_attempts_.erase(it++); + } + + if (permissions_checks_attempts_.empty()) + timer_->Stop(); + } + + void SetupDevdMonitor() { + base::internal::AssertBlockingAllowed(); + + int devd_fd = socket(AF_UNIX, SOCK_SEQPACKET, 0); + if (devd_fd < 0) + return; + + struct sockaddr_un sa; + + sa.sun_family = AF_UNIX; + strlcpy(sa.sun_path, "/var/run/devd.seqpacket.pipe", sizeof(sa.sun_path)); + if (connect(devd_fd, (struct sockaddr *) &sa, sizeof(sa)) < 0) { + close(devd_fd); + return; + } + + devd_fd_.reset(devd_fd); + file_watcher_ = base::FileDescriptorWatcher::WatchReadable( + devd_fd_.get(), base::BindRepeating(&BlockingTaskRunnerHelper::OnDevdMessageCanBeRead, + base::Unretained(this))); + } + + void OnDevdMessageCanBeRead() { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + ssize_t bytes_read = HANDLE_EINTR(recv(devd_fd_.get(), devd_buffer_->data(), + devd_buffer_->size() - 1, MSG_WAITALL)); + if (bytes_read < 0) { + if (errno != EAGAIN) { + HID_LOG(ERROR) << "Read failed"; + file_watcher_.reset(); + } + return; + } + + devd_buffer_->data()[bytes_read] = 0; + char *data = devd_buffer_->data(); + // It may take some time for devd to change permissions + // on /dev/uhidX node. So do not fail immediately if + // open fail. Retry each second for kMaxPermissionChecks + // times before giving up entirely + if (base::StartsWith(data, "+uhid", base::CompareCase::SENSITIVE)) { + std::vector parts = base::SplitString( + data, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + if (!parts.empty()) { + std::string device_name = parts[0].substr(1); // skip '+' + if (HaveReadWritePermissions(device_name)) + OnDeviceAdded(parts[0].substr(1)); + else { + // Do not re-add to checks + if (permissions_checks_attempts_.find(device_name) == permissions_checks_attempts_.end()) { + permissions_checks_attempts_.insert(std::pair(device_name, kMaxPermissionChecks)); + timer_->Start(FROM_HERE, base::Seconds(1), + this, &BlockingTaskRunnerHelper::CheckPendingPermissionChange); + } + } + } + } + + if (base::StartsWith(data, "-uhid", base::CompareCase::SENSITIVE)) { + std::vector parts = base::SplitString( + data, " ", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); + if (!parts.empty()) { + std::string device_name = parts[0].substr(1); // skip '-' + auto it = permissions_checks_attempts_.find(device_name); + if (it != permissions_checks_attempts_.end()) { + permissions_checks_attempts_.erase(it); + if (permissions_checks_attempts_.empty()) + timer_->Stop(); + } + OnDeviceRemoved(parts[0].substr(1)); + } + } + } + + SEQUENCE_CHECKER(sequence_checker_); + + // This weak pointer is only valid when checked on this task runner. + base::WeakPtr service_; + scoped_refptr task_runner_; + std::unique_ptr file_watcher_; + std::unique_ptr timer_; + base::ScopedFD devd_fd_; + scoped_refptr devd_buffer_; + std::map permissions_checks_attempts_; +}; + +HidServiceFreeBSD::HidServiceFreeBSD() + : blocking_task_runner_( + base::ThreadPool::CreateSequencedTaskRunner(kBlockingTaskTraits)), + helper_(nullptr, base::OnTaskRunnerDeleter(blocking_task_runner_)) { + helper_.reset(new BlockingTaskRunnerHelper(weak_factory_.GetWeakPtr())); + blocking_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&BlockingTaskRunnerHelper::Start, base::Unretained(helper_.get()))); +} + +HidServiceFreeBSD::~HidServiceFreeBSD() { + blocking_task_runner_->DeleteSoon(FROM_HERE, helper_.release()); +} + +base::WeakPtr HidServiceFreeBSD::GetWeakPtr() { + return weak_factory_.GetWeakPtr(); +} + +// static +void HidServiceFreeBSD::OpenOnBlockingThread( + std::unique_ptr params) { + base::ScopedBlockingCall scoped_blocking_call( + FROM_HERE, base::BlockingType::MAY_BLOCK); + scoped_refptr task_runner = params->task_runner; + + base::FilePath device_path(params->device_info->device_node()); + base::File device_file; + int flags = + base::File::FLAG_OPEN | base::File::FLAG_READ | base::File::FLAG_WRITE; + device_file.Initialize(device_path, flags); + if (!device_file.IsValid()) { + HID_LOG(EVENT) << "Failed to open '" << params->device_info->device_node() + << "': " + << base::File::ErrorToString(device_file.error_details()); + task_runner->PostTask(FROM_HERE, + base::BindOnce(std::move(params->callback), nullptr)); + return; + } + params->fd.reset(device_file.TakePlatformFile()); + task_runner->PostTask(FROM_HERE, base::BindOnce(&HidServiceFreeBSD::FinishOpen, + std::move(params))); +} + +void HidServiceFreeBSD::Connect(const std::string& device_guid, + bool allow_protected_reports, + bool allow_fido_reports, + ConnectCallback callback) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + const auto& map_entry = devices().find(device_guid); + if (map_entry == devices().end()) { + base::SequencedTaskRunner::GetCurrentDefault()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), nullptr)); + return; + } + + scoped_refptr device_info = map_entry->second; + + auto params = std::make_unique(device_info, + allow_protected_reports, + allow_fido_reports, + std::move(callback)); + scoped_refptr blocking_task_runner = + params->blocking_task_runner; + + blocking_task_runner->PostTask( + FROM_HERE, base::BindOnce(&HidServiceFreeBSD::OpenOnBlockingThread, + std::move(params))); +} + +// static +void HidServiceFreeBSD::FinishOpen(std::unique_ptr params) { + DCHECK(params->fd.is_valid()); + + if (!base::SetNonBlocking(params->fd.get())) { + HID_PLOG(ERROR) << "Failed to set the non-blocking flag on the device fd"; + std::move(params->callback).Run(nullptr); + } + + std::move(params->callback).Run(base::MakeRefCounted( + std::move(params->device_info), + std::move(params->fd), + std::move(params->blocking_task_runner), + params->allow_protected_reports, + params->allow_fido_reports + )); +} + +} // namespace device