/* * \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 #include #include #include #include #include #include #include #include #include #include /* 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_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 ®ion_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 _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 void _apply_node(Node_handle handle, FUNC const &fn) { Node_space::Id id { handle.value }; try { return _node_space.apply(id, fn); } catch (Node_space::Unknown_id) { throw Invalid_handle(); } } /** * Apply functor to typed node * * \throw Invalid_handle */ template auto _apply(HANDLE_TYPE handle, FUNC const &fn) -> typename Genode::Trait::Functor::Return_type { Node_space::Id id { handle.value }; try { return _node_space.apply(id, [&] (Node &node) { typedef typename Node_type::Type Typed_node; Typed_node *n = dynamic_cast(&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(&node)) destroy(_alloc, file); else if (Directory *dir = dynamic_cast(&node)) destroy(_alloc, dir); else if (Symlink *link = dynamic_cast(&node)) destroy(_alloc, link); else if (Watch_node *watch = dynamic_cast(&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) { _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, 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 _reactivate_handler { _env.ep(), *this, &Root::handle_io_progress }; Genode::Signal_handler _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 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(&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 }; }