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