diff --git a/repos/os/run/clipboard.run b/repos/os/run/clipboard.run new file mode 100644 index 000000000..e6a39185a --- /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 000000000..b3257a9f7 --- /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 000000000..1ea3f3610 --- /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 000000000..f1903a7cd --- /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 000000000..d893193e3 --- /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 000000000..79d079294 --- /dev/null +++ b/repos/os/src/test/clipboard/target.mk @@ -0,0 +1,3 @@ +TARGET = test-clipboard +SRC_CC = main.cc +LIBS = base server