diff --git a/repos/os/src/app/usb_report_filter/README b/repos/os/src/app/usb_report_filter/README new file mode 100644 index 0000000000..df3bd3ce91 --- /dev/null +++ b/repos/os/src/app/usb_report_filter/README @@ -0,0 +1,80 @@ +This directory contains a USB device report filter component. It filters the +device report coming from the USB driver by checking each device reported +against the given list of devices. Only approved devices are reported to a +consumer of the report coming from the filter component. + + +Configuration +~~~~~~~~~~~~~ + +A typical example configuration looks as follows: + +! +! +! +! +! +! + + +The component that may use the devices is identified by the 'client' node. +In addition to the 'vendor_id' and 'product_id' attribute a 'device' node +can contain a 'bus' and 'dev' attribute. If these attributes are present they +have a stronger significance than the 'vendor_id' and the 'product_id'. + +Whenever the 'usb_report_filter' component receives a new USB device report +from the driver it will generate a new driver configuration that contains +a policy entry for each matching device. After the driver's configuration has +been updated, the filter component will generate a new USB device report that +only contains the devices the component is allowed to access. + + +Example +~~~~~~~ + +In the following example we will give a VirtualBox instance access to a +Corsair Voyager USB stick: + +! +![...] +! +! +! +! + +! +! +! +! +! +! +! +! +! + +After the USB stick has been plugged in, the filter will generate the +following USB driver configuration: + +! +![...] +! +! +! +! +! +! +! +! + +After the driver has reloaded its configuration it will send a config report +that provokes the filter component to send the following USB device report to +VirtualBox: + +! +! +! + +In return, VirtualBox will try to access the USB device. Since the configuration +of the USB driver contains a matching policy entry the access attempt will +succeed. diff --git a/repos/os/src/app/usb_report_filter/main.cc b/repos/os/src/app/usb_report_filter/main.cc new file mode 100644 index 0000000000..174b4b2c2a --- /dev/null +++ b/repos/os/src/app/usb_report_filter/main.cc @@ -0,0 +1,434 @@ +/* + * \brief Component that filters USB device reports + * \author Josef Soentgen + * \date 2016-01-13 + */ + +/* + * Copyright (C) 2016 Genode Labs GmbH + * + * This file is part of the Genode OS framework, which is distributed + * under the terms of the GNU General Public License version 2. + */ + +/* Genode includes */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace Usb_filter { + + using Genode::Xml_node; + using Genode::Xml_generator; + using Genode::Attached_rom_dataspace; + using Genode::snprintf; + + struct Device_registry; + struct Main; +} + +static bool const verbose = false; + +static char const * const config_file = "usb_drv.config"; + + +class Usb_filter::Device_registry +{ + private: + + Genode::Allocator &_alloc; + Server::Entrypoint &_ep; + + Genode::Reporter _reporter { "usb_devices" }; + + Attached_rom_dataspace _devices_rom { "devices" }; + Attached_rom_dataspace _usb_drv_config_rom { "usb_drv_config" }; + + Genode::Allocator_avl _fs_packet_alloc { &_alloc }; + File_system::Connection _fs { _fs_packet_alloc, 128*1024, "usb_drv.config" }; + File_system::File_handle _file; + + struct Entry : public Genode::List::Element + { + unsigned bus; + unsigned dev; + unsigned vendor; + unsigned product; + + Entry(unsigned b, unsigned d, unsigned v, unsigned p) + : bus(b), dev(d), vendor(v), product(p) { } + }; + + Genode::List _list; + + enum { MAX_LABEL_LEN = 512 }; + typedef Genode::String Label; + Label _client_label; + + template + void _for_each_entry(FUNC const &func) const + { + Entry const *e = _list.first(); + Entry const *next = nullptr; + for (; e; e = next) { + + /* + * Obtain next element prior calling the functor because + * the functor may remove the current element from the list. + */ + next = e->next(); + + func(*e); + } + } + + static inline unsigned _get_value(Xml_node node, char const * const attr) { + return node.attribute_value(attr, 0); } + + static bool _config_has_device(Xml_node config, Entry const &entry) + { + bool result = false; + config.for_each_sub_node("device", [&] (Xml_node usb_device) { + + result |= (_get_value(usb_device, "bus") == entry.bus && + _get_value(usb_device, "dev") == entry.dev); + if (result) return; + + result |= (_get_value(usb_device, "vendor") == entry.vendor && + _get_value(usb_device, "product") == entry.product); + }); + + return result; + } + + static bool _devices_matches(Xml_node &device, Entry const & entry) + { + unsigned const bus = _get_value(device, "bus"); + unsigned const dev = _get_value(device, "dev"); + unsigned const vendor = _get_value(device, "vendor_id"); + unsigned const product = _get_value(device, "product_id"); + + return (bus == entry.bus && dev == entry.dev) || + (vendor == entry.vendor && product == entry.product); + } + + static void _gen_policy_entry(Xml_generator &xml, Xml_node &node, + Entry const &entry, char const *label) + { + xml.node("policy", [&] { + char buf[MAX_LABEL_LEN + 16]; + + unsigned const bus = _get_value(node, "bus"); + unsigned const dev = _get_value(node, "dev"); + + snprintf(buf, sizeof(buf), "%s -> usb-%d-%d", label, bus, dev); + xml.attribute("label", buf); + + snprintf(buf, sizeof(buf), "0x%4x", _get_value(node, "vendor_id")); + xml.attribute("vendor_id", buf); + + snprintf(buf, sizeof(buf), "0x%4x", _get_value(node, "product_id")); + xml.attribute("product_id", buf); + + snprintf(buf, sizeof(buf), "0x%4x", bus); + xml.attribute("bus", buf); + + snprintf(buf, sizeof(buf), "0x%4x", dev); + xml.attribute("dev", buf); + }); + } + + void _write_usb_drv_config(Xml_node & usb_devices) + { + using namespace Genode; + + try { + File_system::Dir_handle root_dir = _fs.dir("/", false); + _file = _fs.file(root_dir, config_file, File_system::READ_WRITE, false); + } catch (...) { + PERR("Could not open '%s'", config_file); + return; + } + + char old_file[1024]; + size_t n = File_system::read(_fs, _file, old_file, + sizeof(old_file)); + if (n == 0) { + PERR("Could not read '%s'", config_file); + return; + } + + Xml_node drv_config(old_file, n); + + bool const uhci_enabled = drv_config.attribute_value("uhci", false); + bool const ehci_enabled = drv_config.attribute_value("ehci", false); + bool const xhci_enabled = drv_config.attribute_value("xhci", false); + + char new_file[1024]; + Genode::Xml_generator xml(new_file, sizeof(new_file), "config", [&] { + if (uhci_enabled) xml.attribute("uhci", "yes"); + if (ehci_enabled) xml.attribute("ehci", "yes"); + if (xhci_enabled) xml.attribute("xhci", "yes"); + + /* copy other nodes */ + drv_config.for_each_sub_node([&] (Xml_node &node) { + if (!node.has_type("raw")) { + xml.append(node.addr(), node.size()); + return; + } + }); + + if (!drv_config.has_sub_node("raw")) + PINF("enable raw support in usb_drv"); + + xml.node("raw", [&] { + xml.node("report", [&] { + xml.attribute("devices", "yes"); + }); + + char const * const label = _client_label.string(); + + usb_devices.for_each_sub_node("device", [&] (Xml_node &node) { + + auto add_policy_entry = [&] (Entry const &entry) { + if (!_devices_matches(node, entry)) return; + + _gen_policy_entry(xml, node, entry, label); + }; + _for_each_entry(add_policy_entry); + }); + }); + }); + + new_file[xml.used()] = 0; + if (verbose) + PLOG("new usb_drv configuration:\n%s", new_file); + + n = File_system::write(_fs, _file, new_file, xml.used()); + if (n == 0) + PERR("Could not write '%s'", config_file); + + _fs.close(_file); + } + + Genode::Signal_rpc_member _devices_dispatcher = + { _ep, *this, &Device_registry::_handle_devices }; + + void _handle_devices(unsigned) + { + _devices_rom.update(); + + if (!_devices_rom.is_valid()) return; + + if (verbose) + PLOG("device report:\n%s", _devices_rom.local_addr()); + + Xml_node usb_devices(_devices_rom.local_addr(), _devices_rom.size()); + + _write_usb_drv_config(usb_devices); + } + + bool _check_config(Xml_node &drv_config) + { + if (!drv_config.has_sub_node("raw")) { + PERR("Could not access node"); + return false; + } + + auto check_policy = [&] (Entry const &entry) { + bool result = false; + drv_config.sub_node("raw").for_each_sub_node("policy", [&] (Xml_node &node) { + result |= (entry.bus == _get_value(node, "bus") && + entry.dev == _get_value(node, "dev")); + if (result) return; + + result |= (entry.vendor == _get_value(node, "vendor_id") && + entry.product == _get_value(node, "product_id")); + }); + + if (verbose && !result) + PWRN("No matching policy was created for device %d-%d (%x:%x)", + entry.bus, entry.dev, entry.vendor, entry.product); + }; + _for_each_entry(check_policy); + + return true; + } + + static void _gen_device_entry(Xml_generator &xml, Xml_node &node, + Entry const &entry) + { + xml.node("device", [&] { + char buf[16]; + + unsigned const bus = _get_value(node, "bus"); + unsigned const dev = _get_value(node, "dev"); + + snprintf(buf, sizeof(buf), "usb-%d-%d", bus, dev); + xml.attribute("label", buf); + + snprintf(buf, sizeof(buf), "0x%4x", _get_value(node, "vendor_id")); + xml.attribute("vendor_id", buf); + + snprintf(buf, sizeof(buf), "0x%4x", _get_value(node, "product_id")); + xml.attribute("product_id", buf); + + snprintf(buf, sizeof(buf), "0x%4x", bus); + xml.attribute("bus", buf); + + snprintf(buf, sizeof(buf), "0x%4x", dev); + xml.attribute("dev", buf); + }); + } + + void _report_usb_devices() + { + using namespace Genode; + + /* + * XXX it might happen that the device list has changed after we are + * waiting for the usb_drv_config update + */ + Xml_node usb_devices(_devices_rom.local_addr(), _devices_rom.size()); + + Reporter::Xml_generator xml(_reporter, [&] () { + usb_devices.for_each_sub_node("device", [&] (Xml_node &node) { + + auto check_entry = [&] (Entry const &entry) { + if (!_devices_matches(node, entry)) return; + + _gen_device_entry(xml, node, entry); + }; + _for_each_entry(check_entry); + }); + }); + } + + Genode::Signal_rpc_member _usb_drv_config_dispatcher = + { _ep, *this, &Device_registry::_handle_usb_drv_config }; + + void _handle_usb_drv_config(unsigned) + { + _usb_drv_config_rom.update(); + + if (!_usb_drv_config_rom.is_valid()) return; + + Xml_node config(_usb_drv_config_rom.local_addr(), + _usb_drv_config_rom.size()); + + if (!_check_config(config)) return; + + /* report devices if the USB drivers has changed its policies */ + _report_usb_devices(); + } + + bool _entry_exists(unsigned bus, unsigned dev, + unsigned vendor, unsigned product) + { + bool result = false; + auto check_exists = [&] (Entry const &entry) { + result |= (bus && dev) && + (entry.bus == bus && entry.dev == dev); + result |= (vendor && product) && + (entry.vendor == vendor && entry.product == product); + }; + + _for_each_entry(check_exists); + + return result; + } + + + public: + + /** + * Constructor + */ + Device_registry(Genode::Allocator &alloc, + Server::Entrypoint &ep) + : _alloc(alloc), _ep(ep) + { + _reporter.enabled(true); + + _devices_rom.sigh(_devices_dispatcher); + + _usb_drv_config_rom.sigh(_usb_drv_config_dispatcher); + } + + /** + * Update internal device registry + */ + void update_entries(Genode::Xml_node config) + { + auto remove_stale_entry = [&] (Entry const &entry) { + + if (_config_has_device(config, entry)) + return; + + _list.remove(const_cast(&entry)); + Genode::destroy(&_alloc, const_cast(&entry)); + }; + _for_each_entry(remove_stale_entry); + + auto add_new_entry = [&] (Xml_node const &node) { + + unsigned const bus = _get_value(node, "bus"); + unsigned const dev = _get_value(node, "dev"); + unsigned const vendor = _get_value(node, "vendor_id"); + unsigned const product = _get_value(node, "product_id"); + + if (_entry_exists(bus, dev, vendor, product)) return; + + Entry *entry = new (&_alloc) Entry(bus, dev, vendor, product); + + _list.insert(entry); + }; + config.for_each_sub_node("device", add_new_entry); + + try { + config.sub_node("client").attribute("label").value(&_client_label); + } catch (...) { + PERR("Could not update client label"); + } + } +}; + + +struct Usb_filter::Main +{ + Server::Entrypoint &ep; + + Genode::Signal_rpc_member
_config_dispatcher = + { ep, *this, &Main::_handle_config }; + + void _handle_config(unsigned) + { + Genode::config()->reload(); + device_registry.update_entries(Genode::config()->xml_node()); + } + + Device_registry device_registry { *Genode::env()->heap(), ep }; + + Main(Server::Entrypoint &ep) : ep(ep) + { + Genode::config()->sigh(_config_dispatcher); + + _handle_config(0); + } +}; + + +namespace Server { + char const *name() { return "usb_report_filter_ep"; } + size_t stack_size() { return 4*1024*sizeof(addr_t); } + void construct(Entrypoint &ep) { static Usb_filter::Main main(ep); } +} diff --git a/repos/os/src/app/usb_report_filter/target.mk b/repos/os/src/app/usb_report_filter/target.mk new file mode 100644 index 0000000000..7bf9904ec2 --- /dev/null +++ b/repos/os/src/app/usb_report_filter/target.mk @@ -0,0 +1,3 @@ +TARGET = usb_report_filter +SRC_CC = main.cc +LIBS = base config server