blob: 1e6b7b869668b14786f5831f813247d73bdbbf9a [file] [log] [blame]
// Copyright 2023 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "permission_broker/allow_external_tagged_usb_device_rule.h"
#include <libudev.h>
#include <cstring>
#include <optional>
#include "base/logging.h"
#include "featured/feature_library.h"
#include "permission_broker/rule.h"
#include "permission_broker/rule_utils.h"
#include "permission_broker/udev_scopers.h"
namespace permission_broker {
CrosUsbLocationProperty AncestorsLocation(udev_device* device) {
bool internal_ancestors = false;
bool external_ancestors = false;
for (udev_device* ancestor = udev_device_get_parent(device);
ancestor != nullptr; ancestor = udev_device_get_parent(ancestor)) {
const char* subsystem = udev_device_get_subsystem(ancestor);
if (strcmp(subsystem, "usb")) {
break;
}
auto location = GetCrosUsbLocationProperty(ancestor);
if (!location.has_value()) {
continue;
}
if (location == CrosUsbLocationProperty::kExternal) {
external_ancestors = true;
} else if (location == CrosUsbLocationProperty::kInternal) {
// TODO(b/267951284) - should we track this, and see if we get false
// positives?
internal_ancestors = true;
}
}
if (internal_ancestors)
return CrosUsbLocationProperty::kInternal;
else if (external_ancestors)
return CrosUsbLocationProperty::kExternal;
else
return CrosUsbLocationProperty::kUnknown;
}
AllowExternalTaggedUsbDeviceRule::AllowExternalTaggedUsbDeviceRule()
: Rule("AllowExternalTaggedUsbDeviceRule"),
// If unable to load form-factor, assume most conservative case.
running_on_chromebox_(GetFormFactor() == FormFactor::kChromebox ||
GetFormFactor() == FormFactor::kUnknown) {}
AllowExternalTaggedUsbDeviceRule::~AllowExternalTaggedUsbDeviceRule() = default;
Rule::Result ProcessUsbDevice(udev_device* device,
CrosUsbLocationProperty location) {
// Safety check, if we have an internal node in the device hierarchy we
// should DENY this device, even if the device thinks it is external.
CrosUsbLocationProperty ancestors_location = AncestorsLocation(device);
if (ancestors_location == CrosUsbLocationProperty::kInternal) {
return Rule::DENY;
}
// Loop through the child nodes (interfaces, really) to ascertain if any of
// them have an attached driver - meaning they are 'claimed' by the host
// kernel.
udev* udev = udev_device_get_udev(device);
ScopedUdevEnumeratePtr enumerate(udev_enumerate_new(udev));
udev_enumerate_add_match_subsystem(enumerate.get(), "usb");
udev_enumerate_add_match_parent(enumerate.get(), device);
udev_enumerate_scan_devices(enumerate.get());
bool found_claimed_interface = false;
bool found_unclaimed_interface = false;
struct udev_list_entry* child = nullptr;
udev_list_entry_foreach(child,
udev_enumerate_get_list_entry(enumerate.get())) {
const char* entry_path = udev_list_entry_get_name(child);
// udev_enumerate_add_match_parent includes the parent entry, skip it.
if (!strcmp(udev_device_get_syspath(device), entry_path)) {
continue;
}
ScopedUdevDevicePtr child(udev_device_new_from_syspath(udev, entry_path));
const char* child_type = udev_device_get_devtype(child.get());
// Safety check - child nodes of a USB device should only be interfaces.
if (!child_type || strcmp(child_type, "usb_interface") != 0) {
LOG(WARNING) << "Found a child interface '" << entry_path
<< "' with unexpected type: "
<< (child_type ? child_type : "(null)");
return Rule::DENY;
}
const char* driver = udev_device_get_driver(child.get());
if (driver) {
found_claimed_interface = true;
} else {
found_unclaimed_interface = true;
}
}
// The basic logic for what decision this rule will reach:
// - If no claimed interfaces exist for the device in question, we will likely
// allow (pending connection to an external port).
// - If there are claimed interfaces but no unclaimed ones, we allow the
// device to be used on successfully detaching kernel drivers.
// - If there are both claimed and unclaimed interfaces, we allow the device
// to be used if privileges on the device are dropped.
Rule::Result allow_variant;
if (found_claimed_interface) {
allow_variant = found_unclaimed_interface ? Rule::ALLOW_WITH_LOCKDOWN
: Rule::ALLOW_WITH_DETACH;
} else {
allow_variant = Rule::ALLOW;
}
// The top level ALLOW/DENY decision hinges on the internal/external property
// of the device in question, and we also want to check for devices that
// mistakenly identify as internal when they are really not.
if (location == CrosUsbLocationProperty::kExternal) {
return allow_variant;
} else if ((location == CrosUsbLocationProperty::kInternal ||
location == CrosUsbLocationProperty::kUnknown) &&
ancestors_location == CrosUsbLocationProperty::kExternal) {
// device erroneously reported that it is not external, but has an external
// ancestor.
return allow_variant;
} else if (location == CrosUsbLocationProperty::kInternal) {
return Rule::DENY;
}
return Rule::IGNORE;
}
Rule::Result AllowExternalTaggedUsbDeviceRule::ProcessDevice(
udev_device* device) {
const char* device_syspath = udev_device_get_syspath(device);
if (!device_syspath) {
return DENY;
}
const char* const subsystem = udev_device_get_subsystem(device);
if (!subsystem || strcmp(subsystem, "usb")) {
return IGNORE;
}
auto maybe_location = GetCrosUsbLocationProperty(device);
if (!maybe_location.has_value()) {
return IGNORE;
}
auto features_lib = feature::PlatformFeatures::Get();
if (!features_lib) {
LOG(ERROR) << "Unable to get PlatformFeatures library, will not enable "
"permissive features";
return IGNORE;
}
if (!features_lib->IsEnabledBlocking(
RuleUtils::kEnablePermissiveUsbPassthrough) ||
running_on_chromebox_) {
return IGNORE;
}
return ProcessUsbDevice(device, maybe_location.value());
}
} // namespace permission_broker