os: add component for filtering USB devices report

The 'usb_report_filter' component takes the devices report from the
USB driver and generates a new devices report after checking each
entry against its device white-list. Before emitting the new report
it changes the configuration of the USB driver to contain the
required policy entries.

See 'repos/os/src/app/usb_report_filter/README' for more details.

Issue #1863.
This commit is contained in:
Josef Söntgen 2016-01-14 13:43:13 +01:00 committed by Christian Helmuth
parent 716eab21e3
commit e233fe0b71
3 changed files with 517 additions and 0 deletions

View File

@ -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:
!<config>
! <client label="component_xyz"/>
! <device vendor_id="0x13fe" product_id="0x5200"/>
! <device vendor_id="0x148f" product_id="0x2573"/>
! <device vendor_id="0x04f9" product_id="0x0051"/>
! <device vendor_id="0x1b1c" product_id="0x1a09"/>
</config>
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:
!<start name="usb_report_filter">
![...]
! <config>
! <client label="vbox"
! <device vendor_id="0x1b1c" product_id="0x1a09"/>
! </config>
!</start>
!<start name="report_rom">
! <resource name="RAM" quantum="1M"/>
! <provides> <service name="Report"/> <service name="ROM"/> </provides>
! <config>
! <policy label="usb_report_filter -> devices" report="usb_drv -> devices"/>
! <policy label="usb_report_filter -> usb_drv_config" report="usb_drv -> config"/>
! <policy label="vbox -> usb_devices" report="usb_report_filter -> usb_devices"/>
! </config>
!</start>
After the USB stick has been plugged in, the filter will generate the
following USB driver configuration:
!<start name="usb_drv">
![...]
! <config uhci="yes" ehci="yes" xhci="yes">
! <hid/>
! <raw>
! <report devices="yes"/>
! <policy label="vbox -> usb-1-3" vendor_id="0x1b1c" product_id="0x1a09" bus="0x0001" device="0x0003"/>
! </raw>
! </config>
!</start>
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:
!<devices>
! <device label="usb-1-3" vendor_id="0x1b1c" product_id="0x1a09" bus="0x0001" device="0x0003"/>
!</device>
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.

View File

@ -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 <base/allocator_avl.h>
#include <os/attached_rom_dataspace.h>
#include <os/config.h>
#include <os/reporter.h>
#include <os/server.h>
#include <util/list.h>
#include <util/string.h>
#include <util/xml_generator.h>
#include <util/xml_node.h>
#include <file_system_session/connection.h>
#include <file_system/util.h>
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<Entry>::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<Entry> _list;
enum { MAX_LABEL_LEN = 512 };
typedef Genode::String<MAX_LABEL_LEN> Label;
Label _client_label;
template <typename FUNC>
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<unsigned long>(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<bool>("uhci", false);
bool const ehci_enabled = drv_config.attribute_value<bool>("ehci", false);
bool const xhci_enabled = drv_config.attribute_value<bool>("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<Device_registry> _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<char>());
Xml_node usb_devices(_devices_rom.local_addr<char>(), _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 <raw> 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<char>(), _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<Device_registry> _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<char>(),
_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 *>(&entry));
Genode::destroy(&_alloc, const_cast<Entry *>(&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<Main> _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); }
}

View File

@ -0,0 +1,3 @@
TARGET = usb_report_filter
SRC_CC = main.cc
LIBS = base config server