genode/repos/os/src/server/vfs/main.cc

984 lines
25 KiB
C++

/*
* \brief VFS File_system server
* \author Emery Hemingway
* \author Christian Helmuth
* \author Norman Feske
* \date 2015-08-16
*/
/*
* Copyright (C) 2015-2019 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.
*/
/* Genode includes */
#include <base/component.h>
#include <base/registry.h>
#include <base/heap.h>
#include <base/attached_ram_dataspace.h>
#include <base/attached_rom_dataspace.h>
#include <file_system_session/rpc_object.h>
#include <root/component.h>
#include <os/session_policy.h>
#include <base/allocator_guard.h>
#include <util/fifo.h>
#include <vfs/simple_env.h>
/* local includes */
#include "assert.h"
#include "node.h"
namespace Vfs_server {
using namespace File_system;
using namespace Vfs;
class Session_resources;
class Session_component;
class Vfs_env;
class Root;
typedef Genode::Fifo<Session_component> Session_queue;
typedef Genode::Entrypoint::Io_progress_handler Io_progress_handler;
/**
* Convenience utities for parsing quotas
*/
Genode::Ram_quota parse_ram_quota(char const *args) {
return Genode::Ram_quota{
Genode::Arg_string::find_arg(args, "ram_quota").ulong_value(0)}; }
Genode::Cap_quota parse_cap_quota(char const *args) {
return Genode::Cap_quota{
Genode::Arg_string::find_arg(args, "cap_quota").ulong_value(0)}; }
};
/**
* Base class to manage session quotas and allocations
*/
class Vfs_server::Session_resources
{
protected:
Genode::Ram_quota_guard _ram_guard;
Genode::Cap_quota_guard _cap_guard;
Genode::Constrained_ram_allocator _ram_alloc;
Genode::Attached_ram_dataspace _packet_ds;
Genode::Heap _alloc;
Session_resources(Genode::Ram_allocator &ram,
Genode::Region_map &region_map,
Genode::Ram_quota ram_quota,
Genode::Cap_quota cap_quota,
Genode::size_t buffer_size)
:
_ram_guard(ram_quota), _cap_guard(cap_quota),
_ram_alloc(ram, _ram_guard, _cap_guard),
_packet_ds(_ram_alloc, region_map, buffer_size),
_alloc(_ram_alloc, region_map)
{ }
};
class Vfs_server::Session_component : private Session_resources,
public ::File_system::Session_rpc_object,
private Session_queue::Element,
private Watch_node::Watch_node_response_handler
{
friend Session_queue;
private:
Vfs::File_system &_vfs;
Genode::Entrypoint &_ep;
Io_progress_handler &_io_progress_handler;
Packet_stream &_stream { *tx_sink() };
/* nodes of this session with active jobs or pending acknowledgements */
Node_queue _active_nodes { };
/* global queue of sessions with active jobs */
Session_queue &_active_sessions;
/* collection of open nodes local to this session */
Node_space _node_space { };
Genode::Signal_handler<Session_component> _packet_stream_handler {
_ep, *this, &Session_component::_handle_packet_stream };
/*
* The root node needs be allocated with the session struct
* but removeable from the id space at session destruction.
*/
Path const _root_path;
Genode::Session_label const _label;
bool const _writeable;
/****************************
** Handle to node mapping **
****************************/
/**
* Apply functor to node
*
* \throw Invalid_handle
*/
template <typename FUNC>
void _apply_node(Node_handle handle, FUNC const &fn)
{
Node_space::Id id { handle.value };
try { return _node_space.apply<Node>(id, fn); }
catch (Node_space::Unknown_id) { throw Invalid_handle(); }
}
/**
* Apply functor to typed node
*
* \throw Invalid_handle
*/
template <typename HANDLE_TYPE, typename FUNC>
auto _apply(HANDLE_TYPE handle, FUNC const &fn)
-> typename Genode::Trait::Functor<decltype(&FUNC::operator())>::Return_type
{
Node_space::Id id { handle.value };
try { return _node_space.apply<Node>(id, [&] (Node &node) {
typedef typename Node_type<HANDLE_TYPE>::Type Typed_node;
Typed_node *n = dynamic_cast<Typed_node *>(&node);
if (!n)
throw Invalid_handle();
return fn(*n);
}); } catch (Node_space::Unknown_id) { throw Invalid_handle(); }
}
/******************************
** Packet-stream processing **
******************************/
bool _try_import_jobs_from_submit_queue()
{
bool overall_progress = false;
for (;;) {
bool progress_in_iteration = false;
if (!_stream.packet_avail())
break;
/* ensure that ack for one malformed packet can be returned */
if (!_stream.ready_to_ack())
break;
Packet_descriptor packet = _stream.peek_packet();
auto drop_packet_from_submit_queue = [&] ()
{
_stream.get_packet();
overall_progress = true;
progress_in_iteration = true;
};
auto consume_and_ack_invalid_packet = [&] ()
{
drop_packet_from_submit_queue();
packet.succeeded(false);
_stream.acknowledge_packet(packet);
overall_progress = true;
progress_in_iteration = true;
};
/* test for invalid packet */
if (packet.length() > packet.size()) {
consume_and_ack_invalid_packet();
continue;
}
try {
_apply(packet.handle(), [&] (Io_node &node) {
if (!node.job_acceptable())
return;
Payload_ptr payload_ptr { _stream.packet_content(packet) };
switch (node.submit_job(packet, payload_ptr)) {
case Node::Submit_result::ACCEPTED:
if (!node.enqueued())
_active_nodes.enqueue(node);
drop_packet_from_submit_queue();
break;
case Node::Submit_result::DENIED:
consume_and_ack_invalid_packet();
break;
case Node::Submit_result::STALLED:
/* keep request packet in submit queue */
break;
}
});
}
catch (File_system::Invalid_handle) {
consume_and_ack_invalid_packet(); }
if (!progress_in_iteration)
break;
}
return overall_progress;
}
void _execute_jobs()
{
/* nodes with jobs that cannot make progress right now */
Node_queue requeued_nodes { };
_active_nodes.dequeue_all([&] (Node &node) {
if (node.job_in_progress())
node.execute_job();
requeued_nodes.enqueue(node);
});
_active_nodes = requeued_nodes;
}
bool _try_acknowledge_jobs()
{
bool overall_progress = false;
for (;;) {
if (!_stream.ready_to_ack())
break;
if (_active_nodes.empty())
break;
bool progress_in_iteration = false;
_active_nodes.dequeue([&] (Node &node) {
/*
* Deliver only one acknowledgement per iteration to
* re-check the 'ready_to_ack' condition for each
* acknowledgement.
*/
if (node.acknowledgement_pending()) {
_stream.acknowledge_packet(node.dequeue_acknowledgement());
progress_in_iteration = true;
}
/*
* If there is still another acknowledgement pending, keep
* the node enqueud to process it in the next iteration.
* This can happen if there is a READ_READY acknowledgement
* in addition to the acknowledgement of an operation.
*/
if (node.active())
_active_nodes.enqueue(node);
});
overall_progress |= progress_in_iteration;
if (!progress_in_iteration)
break;
}
return overall_progress;
}
public:
enum class Process_packets_result { NONE, PROGRESS, TOO_MUCH_PROGRESS };
/**
* Called by the global Io_progress_handler as well as the
* session-local packet-stream handler
*
* \return true if progress was made
*/
Process_packets_result process_packets()
{
bool overall_progress = false;
/*
* Upper bound for the number of iterations. When reached,
* cancel the handler and trigger the re-execution via a local
* signal. This gives the config handler and the RPC functions
* a chance to run in situations when the submit queue of the
* packet stream is always saturated.
*/
unsigned iterations = 200;
for (;;) {
if (--iterations == 0)
return Process_packets_result::TOO_MUCH_PROGRESS;
/* true if progress can be made in this iteration */
bool progress_in_iteration = false;
progress_in_iteration |= _try_import_jobs_from_submit_queue();
_execute_jobs();
progress_in_iteration |= _try_acknowledge_jobs();
if (!progress_in_iteration)
break;
overall_progress |= progress_in_iteration;
}
return overall_progress ? Process_packets_result::PROGRESS
: Process_packets_result::NONE;
}
bool no_longer_active() const
{
return Session_queue::Element::enqueued() && _active_nodes.empty();
}
bool no_longer_idle() const
{
return !Session_queue::Element::enqueued() && !_active_nodes.empty();
}
private:
/**
* Signal handler called for session-local packet-stream signals
*/
void _handle_packet_stream()
{
Process_packets_result const progress = process_packets();
if (no_longer_idle())
_active_sessions.enqueue(*this);
if (progress == Process_packets_result::TOO_MUCH_PROGRESS)
Genode::Signal_transmitter(_packet_stream_handler).submit();
/*
* The activity of the session may have an unblocking effect on
* other sessions. So we call the global 'Io_progress_handler' to
* attempt the packet processing of all active sessions.
*/
if (progress == Process_packets_result::PROGRESS)
_io_progress_handler.handle_io_progress();
}
/**
* Check if string represents a valid path (must start with '/')
*/
static void _assert_valid_path(char const *path) {
if (!path || path[0] != '/') throw Lookup_failed(); }
/**
* Check if string represents a valid name (must not contain '/')
*/
static void _assert_valid_name(char const *name)
{
if (!*name) throw Invalid_name();
for (char const *p = name; *p; ++p)
if (*p == '/')
throw Invalid_name();
}
/**
* Destroy an open node
*/
void _close(Node &node)
{
if (File *file = dynamic_cast<File*>(&node))
destroy(_alloc, file);
else if (Directory *dir = dynamic_cast<Directory*>(&node))
destroy(_alloc, dir);
else if (Symlink *link = dynamic_cast<Symlink*>(&node))
destroy(_alloc, link);
else if (Watch_node *watch = dynamic_cast<Watch_node*>(&node))
destroy(_alloc, watch);
else
destroy(_alloc, &node);
}
/**
* Watch_node::Watch_node_response_handler interface
*/
void handle_watch_node_response(Watch_node &node) override
{
if (!node.enqueued())
_active_nodes.enqueue(node);
/*
* The acknowledgement and dequeuing will be delivered by
* 'Session_component::_try_acknowledge_jobs'. Mark the session as
* active to consider it for the acknowledgement handling.
*/
if (!enqueued())
_active_sessions.enqueue(*this);
}
public:
/**
* Constructor
*/
Session_component(Genode::Env &env,
char const *label,
Genode::Ram_quota ram_quota,
Genode::Cap_quota cap_quota,
size_t tx_buf_size,
Vfs::File_system &vfs,
Session_queue &active_sessions,
Io_progress_handler &io_progress_handler,
char const *root_path,
bool writeable)
:
Session_resources(env.pd(), env.rm(), ram_quota, cap_quota, tx_buf_size),
Session_rpc_object(_packet_ds.cap(), env.rm(), env.ep().rpc_ep()),
_vfs(vfs),
_ep(env.ep()),
_io_progress_handler(io_progress_handler),
_active_sessions(active_sessions),
_root_path(root_path),
_label(label),
_writeable(writeable)
{
_tx.sigh_packet_avail(_packet_stream_handler);
_tx.sigh_ready_to_ack(_packet_stream_handler);
}
/**
* Destructor
*/
~Session_component()
{
/* flush and close the open handles */
while (_node_space.apply_any<Node>([&] (Node &node) {
_close(node); })) { }
if (enqueued())
_active_sessions.remove(*this);
}
/**
* Increase quotas
*/
void upgrade(Genode::Ram_quota ram) {
_ram_guard.upgrade(ram); }
void upgrade(Genode::Cap_quota caps) {
_cap_guard.upgrade(caps); }
/***************************
** File_system interface **
***************************/
Dir_handle dir(::File_system::Path const &path, bool create) override
{
if (create && (!_writeable))
throw Permission_denied();
char const *path_str = path.string();
if (!strcmp(path_str, "/") && create)
throw Node_already_exists();
_assert_valid_path(path_str);
Vfs_server::Path fullpath(_root_path);
if (path_str[1] != '\0')
fullpath.append(path_str);
path_str = fullpath.base();
if (!create && !_vfs.directory(path_str))
throw Lookup_failed();
typedef Directory::Session_writeable Writeable;
Directory &dir = *new (_alloc)
Directory(_node_space, _vfs, _alloc, path_str, create,
_writeable ? Writeable::WRITEABLE : Writeable::READ_ONLY);
if (create)
_io_progress_handler.handle_io_progress();
return Dir_handle(dir.id().value);
}
File_handle file(Dir_handle dir_handle, Name const &name,
Mode fs_mode, bool create) override
{
if ((create || (fs_mode & WRITE_ONLY)) && (!_writeable))
throw Permission_denied();
return _apply(dir_handle, [&] (Directory &dir) {
char const *name_str = name.string();
_assert_valid_name(name_str);
return File_handle {
dir.file(_node_space, _vfs, _alloc, name_str, fs_mode, create).value
};
});
}
Symlink_handle symlink(Dir_handle dir_handle, Name const &name, bool create) override
{
if (create && !_writeable) throw Permission_denied();
return _apply(dir_handle, [&] (Directory &dir) {
char const *name_str = name.string();
_assert_valid_name(name_str);
return Symlink_handle {
dir.symlink(_node_space, _vfs, _alloc, name_str,
_writeable ? READ_WRITE : READ_ONLY, create).value
};
});
}
Node_handle node(::File_system::Path const &path) override
{
char const *path_str = path.string();
_assert_valid_path(path_str);
/* re-root the path */
Path const sub_path(path_str + 1, _root_path.base());
path_str = sub_path.base();
if (sub_path != "/" && !_vfs.leaf_path(path_str))
throw Lookup_failed();
Node &node = *new (_alloc) Node(_node_space, path_str);
return Node_handle { node.id().value };
}
Watch_handle watch(::File_system::Path const &path) override
{
char const *path_str = path.string();
_assert_valid_path(path_str);
/* re-root the path */
Path const sub_path(path_str + 1, _root_path.base());
path_str = sub_path.base();
Vfs::Vfs_watch_handle *vfs_handle = nullptr;
typedef Directory_service::Watch_result Result;
switch (_vfs.watch(path_str, &vfs_handle, _alloc)) {
case Result::WATCH_OK: break;
case Result::WATCH_ERR_UNACCESSIBLE:
throw Lookup_failed();
case Result::WATCH_ERR_STATIC:
throw Unavailable();
case Result::WATCH_ERR_OUT_OF_RAM:
throw Out_of_ram();
case Result::WATCH_ERR_OUT_OF_CAPS:
throw Out_of_caps();
}
Node &node = *new (_alloc)
Watch_node(_node_space, path_str, *vfs_handle, *this);
return Watch_handle { node.id().value };
}
void close(Node_handle handle) override
{
/*
* Churn the packet queue so that any pending packets on this
* handle are processed.
*/
_io_progress_handler.handle_io_progress();
/*
* Closing a written file or symlink may have triggered a watch handler.
*/
bool node_modified = false;
try {
_apply_node(handle, [&] (Node &node) {
if (node.enqueued())
_active_nodes.remove(node);
node_modified = node.modified();
_close(node);
});
}
catch (::File_system::Invalid_handle) { }
if (node_modified)
_io_progress_handler.handle_io_progress();
}
Status status(Node_handle node_handle) override
{
::File_system::Status fs_stat;
_apply_node(node_handle, [&] (Node &node) {
Directory_service::Stat vfs_stat;
if (_vfs.stat(node.path(), vfs_stat) != Directory_service::STAT_OK)
throw Invalid_handle();
auto fs_node_type = [&] (Vfs::Node_type type)
{
using To = ::File_system::Node_type;
switch (type) {
case Vfs::Node_type::DIRECTORY: return To::DIRECTORY;
case Vfs::Node_type::SYMLINK: return To::SYMLINK;
case Vfs::Node_type::CONTINUOUS_FILE: return To::CONTINUOUS_FILE;
case Vfs::Node_type::TRANSACTIONAL_FILE: return To::TRANSACTIONAL_FILE;
};
return To::CONTINUOUS_FILE;
};
auto fs_node_size = [&] (Vfs::Directory_service::Stat const &vfs_stat)
{
switch (vfs_stat.type) {
case Vfs::Node_type::DIRECTORY:
return _vfs.num_dirent(node.path()) * sizeof(Directory_entry);
case Vfs::Node_type::SYMLINK:
return 0ULL;
case Vfs::Node_type::CONTINUOUS_FILE:
case Vfs::Node_type::TRANSACTIONAL_FILE:
return vfs_stat.size;
};
return 0ULL;
};
fs_stat = ::File_system::Status {
.size = fs_node_size(vfs_stat),
.type = fs_node_type(vfs_stat.type),
.rwx = {
.readable = vfs_stat.rwx.readable,
.writeable = vfs_stat.rwx.writeable && _writeable,
.executable = vfs_stat.rwx.executable },
.inode = vfs_stat.inode,
.modification_time = { vfs_stat.modification_time.value }
};
});
return fs_stat;
}
void unlink(Dir_handle dir_handle, Name const &name) override
{
if (!_writeable) throw Permission_denied();
_apply(dir_handle, [&] (Directory &dir) {
char const *name_str = name.string();
_assert_valid_name(name_str);
Path path(name_str, dir.path());
assert_unlink(_vfs.unlink(path.base()));
});
/*
* The unlinking may have triggered a directory-watch handler,
* or a watch handler of the deleted file.
*/
_io_progress_handler.handle_io_progress();
}
void truncate(File_handle file_handle, file_size_t size) override
{
_apply(file_handle, [&] (File &file) {
file.truncate(size); });
_io_progress_handler.handle_io_progress();
}
void move(Dir_handle from_dir_handle, Name const &from_name,
Dir_handle to_dir_handle, Name const &to_name) override
{
if (!_writeable)
throw Permission_denied();
char const *from_str = from_name.string();
char const *to_str = to_name.string();
_assert_valid_name(from_str);
_assert_valid_name( to_str);
_apply(from_dir_handle, [&] (Directory &from_dir) {
_apply(to_dir_handle, [&] (Directory &to_dir) {
Path from_path(from_str, from_dir.path());
Path to_path( to_str, to_dir.path());
assert_rename(_vfs.rename(from_path.base(), to_path.base()));
});
});
/* the move may have triggered a directory watch handler */
_io_progress_handler.handle_io_progress();
}
void control(Node_handle, Control) override { }
};
class Vfs_server::Root : public Genode::Root_component<Session_component>,
private Genode::Entrypoint::Io_progress_handler
{
private:
Genode::Env &_env;
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;
}
}
Genode::Signal_handler<Root> _reactivate_handler {
_env.ep(), *this, &Root::handle_io_progress };
Genode::Signal_handler<Root> _config_handler {
_env.ep(), *this, &Root::_config_update };
void _config_update()
{
_config_rom.update();
_vfs_env.root_dir().apply_config(vfs_config());
}
/**
* The VFS uses an internal heap that
* subtracts from the component quota
*/
Genode::Heap _vfs_heap { &_env.ram(), &_env.rm() };
Vfs::Simple_env _vfs_env { _env, _vfs_heap, vfs_config() };
/* sessions with active jobs */
Session_queue _active_sessions { };
/**
* Entrypoint::Io_progress_handler interface
*/
void handle_io_progress() override
{
bool yield = false;
unsigned iterations = 200;
for (;;) {
/* limit maximum number of iterations */
if (--iterations == 0) {
yield = true;
break;
}
bool progress = false;
Session_queue still_active_sessions { };
_active_sessions.dequeue_all([&] (Session_component &session) {
typedef Session_component::Process_packets_result Result;
switch (session.process_packets()) {
case Result::PROGRESS:
progress = true;
break;
case Result::TOO_MUCH_PROGRESS:
yield = true;
break;
case Result::NONE:
break;
}
if (!session.no_longer_active())
still_active_sessions.enqueue(session);
});
_active_sessions = still_active_sessions;
if (!progress)
break;
}
/*
* Submit a local signal to re-schedule another execution of
* 'handle_io_progress' if the loop was exited via 'yield'.
*/
if (yield)
Genode::Signal_transmitter(_reactivate_handler).submit();
}
protected:
Session_component *_create_session(const char *args) override
{
using namespace Genode;
Session_label const label = label_from_args(args);
Path session_root;
bool writeable = false;
/*****************
** Quota check **
*****************/
auto const initial_ram_usage = _env.pd().used_ram().value;
auto const initial_cap_usage = _env.pd().used_caps().value;
auto const ram_quota = parse_ram_quota(args).value;
auto const cap_quota = parse_cap_quota(args).value;
size_t tx_buf_size =
Arg_string::find_arg(args, "tx_buf_size").aligned_size();
if (!tx_buf_size)
throw Service_denied();
size_t session_size =
max((size_t)4096, sizeof(Session_component)) +
tx_buf_size;
if (session_size > ram_quota) {
error("insufficient 'ram_quota' from '", label, "' "
"got ", ram_quota, ", need ", session_size);
throw Insufficient_ram_quota();
}
/**************************
** Apply session policy **
**************************/
/* pull in policy changes */
_config_rom.update();
typedef String<MAX_PATH_LEN> Root_path;
Session_policy policy(label, _config_rom.xml());
/* check and apply session root offset */
if (!policy.has_attribute("root")) {
error("policy lacks 'root' attribute");
throw Service_denied();
}
Root_path const root_path = policy.attribute_value("root", Root_path());
session_root.import(root_path.string(), "/");
/*
* Determine if the session is writeable.
* Policy overrides client argument, both default to false.
*/
if (policy.attribute_value("writeable", false))
writeable = Arg_string::find_arg(args, "writeable").bool_value(false);
/* apply client's root offset. */
{
char tmp[MAX_PATH_LEN] { };
Arg_string::find_arg(args, "root").string(tmp, sizeof(tmp), "/");
if (Genode::strcmp("/", tmp, sizeof(tmp))) {
session_root.append("/");
session_root.append(tmp);
session_root.remove_trailing('/');
}
}
/* check if the session root exists */
if (!((session_root == "/")
|| _vfs_env.root_dir().directory(session_root.base()))) {
error("session root '", session_root, "' not found for '", label, "'");
throw Service_denied();
}
Session_component *session = new (md_alloc())
Session_component(_env, label.string(),
Genode::Ram_quota{ram_quota},
Genode::Cap_quota{cap_quota},
tx_buf_size, _vfs_env.root_dir(),
_active_sessions, *this,
session_root.base(), writeable);
auto ram_used = _env.pd().used_ram().value - initial_ram_usage;
auto cap_used = _env.pd().used_caps().value - initial_cap_usage;
if ((ram_used > ram_quota) || (cap_used > cap_quota)) {
if (ram_used > ram_quota)
Genode::warning("ram donation is ", ram_quota,
" but used RAM is ", ram_used, "B"
", '", label, "'");
if (cap_used > cap_quota)
Genode::warning("cap donation is ", cap_quota,
" but used caps is ", cap_used,
", '", label, "'");
}
return session;
}
/**
* Session upgrades are important for the VFS server,
* this allows sessions to open arbitrarily large amounts
* of handles without starving other sessions.
*/
void _upgrade_session(Session_component *session,
char const *args) override
{
Genode::Ram_quota more_ram = parse_ram_quota(args);
Genode::Cap_quota more_caps = parse_cap_quota(args);
if (more_ram.value > 0)
session->upgrade(more_ram);
if (more_caps.value > 0)
session->upgrade(more_caps);
}
public:
Root(Genode::Env &env, Genode::Allocator &md_alloc)
:
Root_component<Session_component>(&env.ep().rpc_ep(), &md_alloc),
_env(env)
{
_env.ep().register_io_progress_handler(*this);
_config_rom.sigh(_config_handler);
env.parent().announce(env.ep().manage(*this));
}
};
void Component::construct(Genode::Env &env)
{
static Genode::Sliced_heap sliced_heap { env.ram(), env.rm() };
static Vfs_server::Root root { env, sliced_heap };
}