diff --git a/repos/os/recipes/src/fs_report/content.mk b/repos/os/recipes/src/fs_report/content.mk
new file mode 100644
index 0000000000..bdce6118e4
--- /dev/null
+++ b/repos/os/recipes/src/fs_report/content.mk
@@ -0,0 +1,2 @@
+SRC_DIR = include/report_rom src/server/fs_report
+include $(GENODE_DIR)/repos/base/recipes/src/content.inc
diff --git a/repos/os/recipes/src/fs_report/hash b/repos/os/recipes/src/fs_report/hash
new file mode 100644
index 0000000000..01bf328de4
--- /dev/null
+++ b/repos/os/recipes/src/fs_report/hash
@@ -0,0 +1 @@
+2017-06-09 3abb3c9c0dabc2d8281f46e5f667fb72d6906d48
diff --git a/repos/os/recipes/src/fs_report/used_apis b/repos/os/recipes/src/fs_report/used_apis
new file mode 100644
index 0000000000..0d27980fdf
--- /dev/null
+++ b/repos/os/recipes/src/fs_report/used_apis
@@ -0,0 +1,8 @@
+base
+block_session
+file_system_session
+os
+report_session
+rtc_session
+terminal_session
+vfs
diff --git a/repos/os/run/fs_report.run b/repos/os/run/fs_report.run
new file mode 100644
index 0000000000..59eab44d8d
--- /dev/null
+++ b/repos/os/run/fs_report.run
@@ -0,0 +1,103 @@
+#
+# Build
+#
+set build_components {
+ core init
+ drivers/timer
+ server/fs_report
+ server/vfs
+}
+
+build $build_components
+
+create_boot_directory
+
+#
+# Generate config
+#
+append config {
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+}
+
+install_config $config
+
+#
+# Boot modules
+#
+
+# generic modules
+set boot_modules {
+ core ld.lib.so init
+ fs_report
+ timer
+ vfs
+}
+
+build_boot_image $boot_modules
+
+append qemu_args " -nographic"
+
+run_genode_until {.*.*} 20
diff --git a/repos/os/src/server/fs_report/main.cc b/repos/os/src/server/fs_report/main.cc
new file mode 100644
index 0000000000..bbdff51914
--- /dev/null
+++ b/repos/os/src/server/fs_report/main.cc
@@ -0,0 +1,259 @@
+/*
+ * \brief Report server that writes reports to file-systems
+ * \author Emery Hemingway
+ * \date 2017-05-19
+ */
+
+/*
+ * Copyright (C) 2017 Genode Labs GmbH
+ *
+ * This file is part of the Genode OS framework, which is distributed
+ * under the terms of the GNU Affero General Public License version 3.
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Fs_report {
+ using namespace Genode;
+ using namespace Report;
+ using namespace Vfs;
+
+ class Session_component;
+ class Root;
+ struct Main;
+
+ typedef Genode::Path Path;
+
+static bool create_parent_dir(Vfs::Directory_service &vfs, Path const &child)
+{
+ typedef Vfs::Directory_service::Mkdir_result Mkdir_result;
+
+ Path parent = child;
+ parent.strip_last_element();
+
+ Mkdir_result res = vfs.mkdir(parent.base(), 0);
+ if (res == Mkdir_result::MKDIR_ERR_NO_ENTRY) {
+ if (!create_parent_dir(vfs, parent))
+ return false;
+ res = vfs.mkdir(parent.base(), 0);
+ }
+
+ switch (res) {
+ case Mkdir_result::MKDIR_OK:
+ case Mkdir_result::MKDIR_ERR_EXISTS:
+ return true;
+ default:
+ return false;
+ }
+}
+
+}
+
+class Fs_report::Session_component : public Genode::Rpc_object
+{
+ private:
+
+ Path _leaf_path;
+
+ Attached_ram_dataspace _ds;
+
+ Vfs_handle *_handle;
+ file_size _file_size = 0;
+ bool _success = true;
+
+ public:
+
+ Session_component(Genode::Env &env,
+ Genode::Allocator &alloc,
+ Vfs::File_system &vfs,
+ Genode::Session_label const &label,
+ size_t buffer_size)
+ : _ds(env.ram(), env.rm(), buffer_size)
+ {
+ typedef Vfs::Directory_service::Open_result Open_result;
+
+ Path path = path_from_label(label.string());
+ path.append(".report");
+
+ create_parent_dir(vfs, path);
+
+ Open_result res = vfs.open(
+ path.base(),
+ Directory_service::OPEN_MODE_WRONLY |
+ Directory_service::OPEN_MODE_CREATE,
+ &_handle, alloc);
+
+ if (res == Open_result::OPEN_ERR_EXISTS) {
+ res = vfs.open(
+ path.base(),
+ Directory_service::OPEN_MODE_WRONLY,
+ &_handle, alloc);
+ if (res == Open_result::OPEN_OK)
+ _handle->fs().ftruncate(_handle, 0);
+ }
+
+ if (res != Open_result::OPEN_OK) {
+ error("failed to open '", path, "'");
+ throw Service_denied();
+ }
+
+ /* get the leaf path from the leaf file-system */
+ if (char const *leaf_path = _handle->ds().leaf_path(path.base()))
+ _leaf_path.import(leaf_path);
+ }
+
+ ~Session_component()
+ {
+ if (_handle)
+ _handle->ds().close(_handle);
+ }
+
+ Dataspace_capability dataspace() override { return _ds.cap(); }
+
+ void submit(size_t const length) override
+ {
+ /* TODO: close and reopen on error */
+
+ typedef Vfs::File_io_service::Write_result Write_result;
+
+ if (_file_size != length)
+ _handle->fs().ftruncate(_handle, length);
+
+ size_t offset = 0;
+ while (offset < length) {
+ file_size n = 0;
+
+ _handle->seek(offset);
+ Write_result res = _handle->fs().write(
+ _handle, _ds.local_addr(),
+ length - offset, n);
+
+ if (res != Write_result::WRITE_OK) {
+ /* do not spam the log */
+ if (_success)
+ error("failed to write report to '", _leaf_path, "'");
+ _file_size = 0;
+ _success = false;
+ return;
+ }
+
+ offset += n;
+ }
+
+ _file_size = length;
+ _success = true;
+
+ /* flush to notify watchers */
+ _handle->ds().sync(_leaf_path.base());
+ }
+
+ void response_sigh(Genode::Signal_context_capability) override { }
+
+ size_t obtain_response() override { return 0; }
+};
+
+
+class Fs_report::Root : public Genode::Root_component
+{
+ private:
+
+ Genode::Env &_env;
+ Genode::Heap _heap { &_env.ram(), &_env.rm() };
+
+ Genode::Attached_rom_dataspace _config_rom { _env, "config" };
+
+ Genode::Xml_node vfs_config()
+ {
+ try { return _config_rom.xml().sub_node("vfs"); }
+ catch (...) {
+ Genode::error("VFS not configured");
+ _env.parent().exit(~0);
+ throw;
+ }
+ }
+
+ struct Io_dummy : Io_response_handler {
+ void handle_io_response(Vfs::Vfs_handle::Context*) override { }
+ } _io_response_handler { };
+
+ Vfs::Global_file_system_factory _global_file_system_factory { _heap };
+
+ Vfs::Dir_file_system _vfs {
+ _env, _heap, vfs_config(),
+ _io_response_handler,
+ _global_file_system_factory };
+
+ Genode::Signal_handler _config_dispatcher {
+ _env.ep(), *this, &Root::_config_update };
+
+ void _config_update()
+ {
+ _config_rom.update();
+ _vfs.apply_config(vfs_config());
+ }
+
+ protected:
+
+ Session_component *_create_session(const char *args) override
+ {
+ using namespace Genode;
+
+ /* read label from session arguments */
+ Session_label const label = label_from_args(args);
+
+ /* read RAM donation from session arguments */
+ size_t const ram_quota =
+ Arg_string::find_arg(args, "ram_quota").aligned_size();
+ /* read report buffer size from session arguments */
+ size_t const buffer_size =
+ Arg_string::find_arg(args, "buffer_size").aligned_size();
+
+ size_t session_size =
+ max((size_t)4096, sizeof(Session_component)) +
+ buffer_size;
+
+ if (session_size > ram_quota) {
+ error("insufficient 'ram_quota' from '", label, "' "
+ "got ", ram_quota, ", need ", session_size);
+ throw Insufficient_ram_quota();
+ }
+
+ return new (md_alloc())
+ Session_component(_env, _heap, _vfs, label, buffer_size);
+ }
+
+ public:
+
+ Root(Genode::Env &env, Genode::Allocator &md_alloc)
+ :
+ Genode::Root_component(env.ep(), md_alloc),
+ _env(env)
+ { }
+};
+
+
+struct Fs_report::Main
+{
+ Env &env;
+
+ Sliced_heap sliced_heap { env.ram(), env.rm() };
+
+ Root root { env, sliced_heap };
+
+ Main(Env &env) : env(env)
+ {
+ env.parent().announce(env.ep().manage(root));
+ }
+};
+
+void Component::construct(Genode::Env &env) { static Fs_report::Main main(env); }
diff --git a/repos/os/src/server/fs_report/target.mk b/repos/os/src/server/fs_report/target.mk
new file mode 100644
index 0000000000..f333b9dedb
--- /dev/null
+++ b/repos/os/src/server/fs_report/target.mk
@@ -0,0 +1,3 @@
+TARGET = fs_report
+LIBS = base vfs
+SRC_CC = main.cc