diff --git a/repos/os/run/clipboard.run b/repos/os/run/clipboard.run
new file mode 100644
index 0000000000..e6a39185a4
--- /dev/null
+++ b/repos/os/run/clipboard.run
@@ -0,0 +1,105 @@
+#
+# Build
+#
+
+set build_components {
+ core init drivers/timer
+ server/clipboard server/report_rom test/clipboard drivers/timer
+}
+
+build $build_components
+
+create_boot_directory
+
+#
+# Generate config
+#
+
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+install_config $config
+
+#
+# Boot modules
+#
+
+set boot_modules { core init timer report_rom clipboard test-clipboard }
+
+build_boot_image $boot_modules
+
+append qemu_args " -nographic "
+
+run_genode_until {.*-- state WAIT_FOR_SUCCESS --.*\n} 40
+
+
diff --git a/repos/os/src/server/clipboard/README b/repos/os/src/server/clipboard/README
new file mode 100644
index 0000000000..b3257a9f77
--- /dev/null
+++ b/repos/os/src/server/clipboard/README
@@ -0,0 +1,4 @@
+The "clipboard" component is both a report service and a ROM service. The
+clients of the report service can issue new clipboard content, which is then
+propagated to the clients of the ROM service according to a configurable
+information-flow policy.
diff --git a/repos/os/src/server/clipboard/main.cc b/repos/os/src/server/clipboard/main.cc
new file mode 100644
index 0000000000..1ea3f3610e
--- /dev/null
+++ b/repos/os/src/server/clipboard/main.cc
@@ -0,0 +1,219 @@
+/*
+ * \brief Clipboard used for copy and paste between domains
+ * \author Norman Feske
+ * \date 2015-09-23
+ */
+
+/*
+ * Copyright (C) 2015 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
+
+namespace Server { struct Main; }
+
+
+/**
+ * The clipboard uses a single ROM module for all clients
+ */
+struct Rom::Registry : Rom::Registry_for_reader, Rom::Registry_for_writer
+{
+ Module module;
+
+ /**
+ * Rom::Registry_for_writer interface
+ */
+ Module &lookup(Writer &, Module::Name const &) override { return module; }
+ void release(Writer &, Module &) override { }
+
+ /**
+ * Rom::Registry_for_reader interface
+ */
+ Module &lookup(Reader &reader, Module::Name const &) override
+ {
+ module._register(reader);
+ return module;
+ }
+
+ void release(Reader &reader, Readable_module &) override
+ {
+ module._unregister(reader);
+ }
+
+ /**
+ * Constructor
+ */
+ Registry(Module::Read_policy const &read_policy,
+ Module::Write_policy const &write_policy)
+ :
+ module("clipboard", read_policy, write_policy)
+ { }
+};
+
+
+struct Server::Main : Rom::Module::Read_policy, Rom::Module::Write_policy
+{
+ Entrypoint &_ep;
+
+ Genode::Sliced_heap _sliced_heap = { Genode::env()->ram_session(),
+ Genode::env()->rm_session() };
+ bool _verbose_config()
+ {
+ char const *attr = "verbose";
+ return Genode::config()->xml_node().has_attribute(attr)
+ && Genode::config()->xml_node().attribute(attr).has_value("yes");
+ }
+
+ bool verbose = _verbose_config();
+
+ typedef Genode::String<100> Domain;
+
+ Genode::Attached_rom_dataspace _focus_ds { "focus" };
+
+ Genode::Signal_rpc_member _focus_dispatcher =
+ { _ep, *this, &Main::_handle_focus };
+
+ Domain _focused_domain;
+
+ /**
+ * Handle the change of the current nitpicker focus
+ *
+ * We only accept reports from the currently focused domain.
+ */
+ void _handle_focus(unsigned)
+ {
+ _focus_ds.update();
+
+ _focused_domain = Domain();
+
+ try {
+ Genode::Xml_node focus(_focus_ds.local_addr(), _focus_ds.size());
+
+ if (focus.attribute("active").has_value("yes"))
+ _focused_domain = focus.attribute_value("domain", Domain());
+
+ } catch (...) { }
+ }
+
+ Domain _domain(Genode::Session_label const &label) const
+ {
+ using namespace Genode;
+
+ try {
+ return Session_policy(label).attribute_value("domain", Domain());
+ } catch (Session_policy::No_policy_defined) { }
+
+ return Domain();
+ }
+
+ Domain _domain(Rom::Reader const &reader) const
+ {
+ Rom::Session_component const &rom_session =
+ static_cast(reader);
+
+ return _domain(rom_session.label());
+ }
+
+ Domain _domain(Rom::Writer const &writer) const
+ {
+ Report::Session_component const &report_session =
+ static_cast(writer);
+
+ return _domain(report_session.label());
+ }
+
+ bool _flow_defined(Domain const &from, Domain const &to) const
+ {
+ if (!from.valid() || !to.valid())
+ return false;
+
+ /*
+ * Search config for flow node with matching 'from' and 'to'
+ * attributes.
+ */
+ bool result = false;
+ try {
+
+ auto match_flow = [&] (Genode::Xml_node flow) {
+ if (flow.attribute_value("from", Domain()) == from
+ && flow.attribute_value("to", Domain()) == to)
+ result = true; };
+
+ Genode::config()->xml_node().for_each_sub_node("flow", match_flow);
+
+ } catch (Genode::Xml_node::Nonexistent_sub_node) { }
+
+ return result;
+ }
+
+ /**
+ * Rom::Module::Read_policy interface
+ */
+ bool read_permitted(Rom::Module const &module,
+ Rom::Writer const &writer,
+ Rom::Reader const &reader) const override
+ {
+ Domain const from_domain = _domain(writer);
+ Domain const to_domain = _domain(reader);
+
+ if (from_domain == to_domain)
+ return true;
+
+ if (_flow_defined(from_domain, to_domain))
+ return true;
+
+ return false;
+ }
+
+
+ /**
+ * Rom::Module::Write_policy interface
+ */
+ bool write_permitted(Rom::Module const &module,
+ Rom::Writer const &writer) const override
+ {
+ if (_focused_domain.valid() && _domain(writer) == _focused_domain)
+ return true;
+
+ PWRN("unexpected attempt by '%s' to write to '%s'",
+ writer.label().string(), module.name().string());
+
+ return false;
+ }
+
+ Rom::Registry _rom_registry { *this, *this };
+
+ Report::Root report_root = { _ep, _sliced_heap, _rom_registry, verbose };
+ Rom ::Root rom_root = { _ep, _sliced_heap, _rom_registry };
+
+ Main(Entrypoint &ep) : _ep(ep)
+ {
+ _focus_ds.sigh(_focus_dispatcher);
+
+ Genode::env()->parent()->announce(_ep.manage(report_root));
+ Genode::env()->parent()->announce(_ep.manage(rom_root));
+ }
+};
+
+
+namespace Server {
+
+ char const *name() { return "report_rom_ep"; }
+
+ size_t stack_size() { return 4*1024*sizeof(long); }
+
+ void construct(Entrypoint &ep)
+ {
+ static Main main(ep);
+ }
+}
diff --git a/repos/os/src/server/clipboard/target.mk b/repos/os/src/server/clipboard/target.mk
new file mode 100644
index 0000000000..f1903a7cd0
--- /dev/null
+++ b/repos/os/src/server/clipboard/target.mk
@@ -0,0 +1,4 @@
+TARGET = clipboard
+SRC_CC = main.cc
+LIBS = base server config
+INC_DIR += $(PRG_DIR)
diff --git a/repos/os/src/test/clipboard/main.cc b/repos/os/src/test/clipboard/main.cc
new file mode 100644
index 0000000000..d893193e39
--- /dev/null
+++ b/repos/os/src/test/clipboard/main.cc
@@ -0,0 +1,429 @@
+/*
+ * \brief Clipboard test
+ * \author Norman Feske
+ * \date 2015-09-23
+ */
+
+/*
+ * Copyright (C) 2015 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
+
+
+class Nitpicker
+{
+ private:
+
+ Timer::Session &_timer;
+
+ Genode::Reporter _focus_reporter { "focus" };
+
+ void _focus(char const *domain, bool active)
+ {
+ Genode::Reporter::Xml_generator xml(_focus_reporter, [&] () {
+ xml.attribute("domain", domain);
+ xml.attribute("active", active ? "yes" : "no");
+ });
+
+ /*
+ * Trigger a state change after a while. We wait a bit after
+ * reporting a new focus to give the new state some time to
+ * propagate through the report-rom server to the clipboard.
+ */
+ _timer.trigger_once(250*1000);
+ }
+
+ public:
+
+ Nitpicker(Timer::Session &timer)
+ :
+ _timer(timer)
+ {
+ _focus_reporter.enabled(true);
+ }
+
+ void focus_active (char const *domain) { _focus(domain, true); }
+ void focus_inactive(char const *domain) { _focus(domain, false); }
+};
+
+
+/**
+ * Callback called each time when a subsystem makes progress
+ *
+ * This function drives the state machine of the test program.
+ */
+struct Handle_step_fn
+{
+ virtual void handle_step(unsigned) = 0;
+};
+
+
+class Subsystem
+{
+ private:
+
+ Server::Entrypoint &_ep;
+
+ typedef Genode::String<100> Label;
+
+ Label _name;
+
+ Handle_step_fn &_handle_step_fn;
+
+ bool _expect_import = true;
+
+ Label _session_label()
+ {
+ char buf[Label::capacity()];
+ Genode::snprintf(buf, sizeof(buf), "%s -> clipboard", _name.string());
+ return Label(buf);
+ }
+
+ Genode::Attached_rom_dataspace _import_rom;
+
+ char const *_import_content = nullptr;
+
+ Report::Connection _export_report { _session_label().string() };
+
+ Genode::Attached_dataspace _export_report_ds { _export_report.dataspace() };
+
+ static void _log_lines(char const *string, Genode::size_t len)
+ {
+ Genode::print_lines<200>(string, len,
+ [&] (char const *line) { PLOG(" %s", line); });
+ }
+
+ void _handle_import(unsigned)
+ {
+ if (!_expect_import) {
+ class Unexpected_clipboard_import { };
+ throw Unexpected_clipboard_import();
+ }
+
+ PLOG("\n%s: import new content:", _name.string());
+
+ _import_rom.update();
+ _import_content = _import_rom.local_addr();
+ _log_lines(_import_content, _import_rom.size());
+
+ /* trigger next step */
+ _handle_step_fn.handle_step(0);
+ }
+
+ Genode::Signal_rpc_member _import_dispatcher =
+ { _ep, *this, &Subsystem::_handle_import };
+
+ static void _strip_outer_whitespace(char const **str_ptr, Genode::size_t &len)
+ {
+ char const *str = *str_ptr;
+
+ /* strip leading whitespace */
+ for (; Genode::is_whitespace(*str); str++, len--);
+
+ /* strip trailing whitespace */
+ for (; len > 1 && Genode::is_whitespace(str[len - 1]); len--);
+
+ *str_ptr = str;
+ }
+
+ /**
+ * Return currently present imported text
+ *
+ * \throw Xml_node::Nonexistent_sub_node
+ */
+ Genode::Xml_node _imported_text() const
+ {
+ if (!_import_content)
+ throw Genode::Xml_node::Nonexistent_sub_node();
+
+ Genode::Xml_node clipboard(_import_content,
+ _import_rom.size());
+
+ return clipboard.sub_node("text");
+ }
+
+ public:
+
+ Subsystem(Server::Entrypoint &ep, char const *name,
+ Handle_step_fn &handle_step_fn)
+ :
+ _ep(ep),
+ _name(name),
+ _handle_step_fn(handle_step_fn),
+ _import_rom(_session_label().string())
+ {
+ _import_rom.sigh(_import_dispatcher);
+ }
+
+ void copy(char const *str)
+ {
+ Genode::Xml_generator xml(_export_report_ds.local_addr(),
+ _export_report_ds.size(),
+ "clipboard", [&] ()
+ {
+ xml.attribute("origin", _name.string());
+ xml.node("text", [&] () {
+ xml.append(str, Genode::strlen(str));
+ });
+ });
+
+ PLOG("\n%s: export content:", _name.string());
+ _log_lines(_export_report_ds.local_addr(), xml.used());
+
+ _export_report.submit(xml.used());
+ }
+
+ bool has_content(char const *str) const
+ {
+ using namespace Genode;
+ try {
+ typedef Genode::String<100> String;
+
+ String const expected(str);
+ String const imported = _imported_text().decoded_content();
+
+ return expected == imported;
+ }
+ catch (...) { }
+ return false;
+ }
+
+ bool is_cleared() const
+ {
+ try {
+ _imported_text();
+ return false;
+ } catch (...) { }
+ return true;
+ }
+
+ /**
+ * Configure assertion for situation where no imports are expected
+ */
+ void expect_import(bool expect) { _expect_import = expect; }
+};
+
+
+namespace Server { struct Main; }
+
+
+struct Server::Main : Handle_step_fn
+{
+ Entrypoint &_ep;
+
+ enum State {
+ INIT,
+ FOCUSED_HOBBY_DOMAIN,
+ EXPECT_CAT_PICTURE,
+ FOCUSED_ADMIN_DOMAIN,
+ EXPECT_PRIVATE_KEY,
+ BLOCKED_REPETITION,
+ FOCUSED_WORK_DOMAIN,
+ EXPECT_CONTRACT,
+ FOCUS_BECOMES_INACTIVE,
+ BLOCKED_WHEN_INACTIVE,
+ FOCUSED_HOBBY_DOMAIN_AGAIN,
+ WAIT_FOR_SUCCESS
+ };
+
+ State _state = INIT;
+
+ static char const *_state_name(State state)
+ {
+ switch (state) {
+ case INIT: return "INIT";
+ case FOCUSED_HOBBY_DOMAIN: return "FOCUSED_HOBBY_DOMAIN";
+ case EXPECT_CAT_PICTURE: return "EXPECT_CAT_PICTURE";
+ case FOCUSED_ADMIN_DOMAIN: return "FOCUSED_ADMIN_DOMAIN";
+ case EXPECT_PRIVATE_KEY: return "EXPECT_PRIVATE_KEY";
+ case BLOCKED_REPETITION: return "BLOCKED_REPETITION";
+ case FOCUSED_WORK_DOMAIN: return "FOCUSED_WORK_DOMAIN";
+ case EXPECT_CONTRACT: return "EXPECT_CONTRACT";
+ case FOCUS_BECOMES_INACTIVE: return "FOCUS_BECOMES_INACTIVE";
+ case BLOCKED_WHEN_INACTIVE: return "BLOCKED_WHEN_INACTIVE";
+ case FOCUSED_HOBBY_DOMAIN_AGAIN: return "FOCUSED_HOBBY_DOMAIN_AGAIN";
+ case WAIT_FOR_SUCCESS: return "WAIT_FOR_SUCCESS";
+ }
+ return "";
+ }
+
+ void _enter_state(State state)
+ {
+ PINF("\n-> entering state %s", _state_name(state));
+ _state = state;
+ }
+
+ void handle_step(unsigned cnt) override
+ {
+ PLOG("\n -- state %s --", _state_name(_state));
+
+ char const * const cat_picture = "cat picture";
+ char const * const private_key = "private key";
+ char const * const another_private_key = "another private key";
+ char const * const contract = "contract";
+ char const * const garbage = "garbage";
+
+ char const * const hobby_domain = "hobby";
+ char const * const work_domain = "work";
+ char const * const admin_domain = "admin";
+
+ switch (_state) {
+
+ case INIT:
+ _nitpicker.focus_active(hobby_domain);
+ _enter_state(FOCUSED_HOBBY_DOMAIN);
+ return;
+
+ case FOCUSED_HOBBY_DOMAIN:
+ _hobby.copy(cat_picture);
+ _enter_state(EXPECT_CAT_PICTURE);
+ return;
+
+ case EXPECT_CAT_PICTURE:
+
+ if (!_hobby.has_content(cat_picture)
+ || !_work .has_content(cat_picture)
+ || !_admin.has_content(cat_picture))
+ return;
+
+ _nitpicker.focus_active(admin_domain);
+ _enter_state(FOCUSED_ADMIN_DOMAIN);
+ return;
+
+ case FOCUSED_ADMIN_DOMAIN:
+ _admin.copy(private_key);
+ _enter_state(EXPECT_PRIVATE_KEY);
+ return;
+
+ case EXPECT_PRIVATE_KEY:
+
+ if (!_hobby.is_cleared()
+ || !_work .is_cleared()
+ || !_admin.has_content(private_key))
+ return;
+
+ /*
+ * Issue a copy operation that leaves the hobby and work
+ * domains unchanged. The unchanged domains are not expected
+ * to receive any notification. Otherwise, such notifications
+ * could be misused as a covert channel.
+ */
+ _work .expect_import(false);
+ _hobby.expect_import(false);
+ _admin.copy(another_private_key);
+
+ _timer.trigger_once(500*1000);
+ _enter_state(BLOCKED_REPETITION);
+ return;
+
+ case BLOCKED_REPETITION:
+
+ /*
+ * Let the work and hobby domains accept new imports.
+ */
+ _work .expect_import(true);
+ _hobby.expect_import(true);
+
+ _nitpicker.focus_active(work_domain);
+ _enter_state(FOCUSED_WORK_DOMAIN);
+ return;
+
+ case FOCUSED_WORK_DOMAIN:
+ _work.copy(contract);
+ _enter_state(EXPECT_CONTRACT);
+ return;
+
+ case EXPECT_CONTRACT:
+
+ if (!_hobby.is_cleared()
+ || !_work .has_content(contract)
+ || !_admin.has_content(contract))
+ return;
+
+ _nitpicker.focus_inactive(work_domain);
+ _enter_state(FOCUS_BECOMES_INACTIVE);
+ return;
+
+ case FOCUS_BECOMES_INACTIVE:
+
+ /*
+ * With the focus becoming inactive, we do not expect the
+ * delivery of any new clipboard content.
+ */
+ _work .expect_import(false);
+ _admin.expect_import(false);
+ _hobby.expect_import(false);
+ _work.copy(garbage);
+
+ /*
+ * Since no state changes are triggered from the outside,
+ * we schedule a timeout to proceed.
+ */
+ _timer.trigger_once(500*1000);
+ _enter_state(BLOCKED_WHEN_INACTIVE);
+ return;
+
+ case BLOCKED_WHEN_INACTIVE:
+ _nitpicker.focus_active(hobby_domain);
+ _enter_state(FOCUSED_HOBBY_DOMAIN_AGAIN);
+ return;
+
+ case FOCUSED_HOBBY_DOMAIN_AGAIN:
+ /*
+ * Let the work domain try to issue a copy operation while the
+ * hobby domain is focused. The clipboard is expected to block
+ * this report.
+ */
+ _work.copy(garbage);
+ _timer.trigger_once(500*1000);
+ _enter_state(WAIT_FOR_SUCCESS);
+ return;
+
+ case WAIT_FOR_SUCCESS:
+ break;
+ }
+ }
+
+ Genode::Signal_rpc_member _step_dispatcher =
+ { _ep, *this, &Main::handle_step };
+
+ Subsystem _admin { _ep, "noux", *this };
+ Subsystem _hobby { _ep, "linux", *this };
+ Subsystem _work { _ep, "win7", *this };
+
+ Timer::Connection _timer;
+
+ Nitpicker _nitpicker { _timer };
+
+ Main(Entrypoint &ep) : _ep(ep)
+ {
+ _timer.sigh(_step_dispatcher);
+
+ /* trigger first step */
+ handle_step(0);
+ }
+};
+
+
+namespace Server {
+
+ char const *name() { return "ep"; }
+
+ size_t stack_size() { return 4*1024*sizeof(long); }
+
+ void construct(Entrypoint &ep)
+ {
+ static Main main(ep);
+ }
+}
diff --git a/repos/os/src/test/clipboard/target.mk b/repos/os/src/test/clipboard/target.mk
new file mode 100644
index 0000000000..79d0792943
--- /dev/null
+++ b/repos/os/src/test/clipboard/target.mk
@@ -0,0 +1,3 @@
+TARGET = test-clipboard
+SRC_CC = main.cc
+LIBS = base server