mirror of
https://github.com/SerenityOS/serenity
synced 2026-04-25 17:15:42 +02:00
Immutable mounts are mounts that can't be changed in any aspect, if the VFSRootContext that hold them is used by a process. This includes two operations on a mount: 1. Trying to remove the mount from the mount table. 2. Trying to change the flags of the mount. The condition of a VFSRootContext being held by a process or not is crucial, as the intention is to allow removal of mounts that marked as immutable if the VFSRootContext is not being used anymore (for example, if the container that was created with such context stopped). Marking mounts as immutable on the first VFS root context essentially ensures they will never be modified because there will be a process using that context (which is the "main" VFS root context in the system runtime). It should be noted that setting a mount as immutable can be done in creation time of the mount by passing the MS_IMMUTABLE flag, or by doing a remount with MS_IMMUTABLE flag.
365 lines
16 KiB
C++
365 lines
16 KiB
C++
/*
|
|
* Copyright (c) 2024, Liav A. <liavalb@hotmail.co.il>
|
|
*
|
|
* SPDX-License-Identifier: BSD-2-Clause
|
|
*/
|
|
|
|
#include <Kernel/FileSystem/RAMFS/FileSystem.h>
|
|
#include <Kernel/FileSystem/VFSRootContext.h>
|
|
#include <Kernel/FileSystem/VirtualFileSystem.h>
|
|
|
|
namespace Kernel {
|
|
|
|
static RawPtr<VFSRootContext> s_empty_context;
|
|
static RawPtr<Custody> s_empty_context_custody;
|
|
static Atomic<u64> s_vfs_root_context_id = 0;
|
|
|
|
UNMAP_AFTER_INIT void VFSRootContext::initialize_empty_ramfs_root_context_for_kernel_processes()
|
|
{
|
|
s_empty_context = &MUST(VFSRootContext::create_with_empty_ramfs()).leak_ref();
|
|
// NOTE: This custody is immutable, so we expose it also outside of the SpinlockProtected
|
|
// template so it could be accessed immediately.
|
|
s_empty_context_custody = &s_empty_context->root_custody().with([](auto& custody) -> NonnullRefPtr<Custody> { return *custody; }).leak_ref();
|
|
|
|
// NOTE: We remove the context from the vfs root contexts list because
|
|
// we leaked a ref, and this context is artificially created only for
|
|
// kernel processes.
|
|
dbgln("VFSRootContext({}): Context is artificially made, detach from global list", s_empty_context->id());
|
|
VFSRootContext::all_root_contexts_list().with([&](auto& list) {
|
|
list.remove(*s_empty_context);
|
|
});
|
|
}
|
|
|
|
VFSRootContext::VFSRootContext(NonnullRefPtr<Custody> custody)
|
|
: m_root_custody(custody)
|
|
, m_id(s_vfs_root_context_id.fetch_add(1))
|
|
{
|
|
}
|
|
|
|
VFSRootContext const& VFSRootContext::empty_context_for_kernel_processes()
|
|
{
|
|
VERIFY(s_empty_context);
|
|
return *s_empty_context;
|
|
}
|
|
|
|
Custody const& VFSRootContext::empty_context_custody_for_kernel_processes()
|
|
{
|
|
VERIFY(s_empty_context_custody);
|
|
return *s_empty_context_custody;
|
|
}
|
|
|
|
ErrorOr<void> VFSRootContext::for_each_mount(Function<ErrorOr<void>(Mount const&)> callback) const
|
|
{
|
|
return m_details.with([&](auto& details) -> ErrorOr<void> {
|
|
for (auto& mount : details.mounts)
|
|
TRY(callback(mount));
|
|
return {};
|
|
});
|
|
}
|
|
|
|
void VFSRootContext::add_to_mounts_list_and_increment_fs_mounted_count(DoBindMount do_bind_mount, IntrusiveList<&Mount::m_vfs_list_node>& mounts_list, NonnullOwnPtr<Mount> new_mount)
|
|
{
|
|
new_mount->guest_fs().mounted_count().with([&](auto& mounted_count) {
|
|
// NOTE: We increment the mounted counter for the given filesystem regardless of the mount type,
|
|
// as a bind mount also counts as a normal mount from the perspective of unmount(),
|
|
// so we need to keep track of it in order for prepare_to_clear_last_mount() to work properly.
|
|
mounted_count++;
|
|
|
|
// NOTE: Add the filesystem to the file systems list if it's not a bind mount (this
|
|
// condition is VERIFYed within the if-case) and this is the first time this FileSystem is mounted.
|
|
// This is symmetric with VirtualFileSystem::unmount()'s `remove()` calls (which remove
|
|
// the FileSystem once it is no longer mounted).
|
|
if (mounted_count == 1) {
|
|
// NOTE: If the mounted_count is 1, and we try to do a bind-mount on an inode
|
|
// from this filesystem this means we have a bug because it's expected that
|
|
// we will always have an already-mounted filesystem when creating a new bind-mount.
|
|
//
|
|
// Even in the odd case of mounting a new filesystem, creating a new bind mount
|
|
// from a source Inode within the same filesystem and then removing the original mountpoint
|
|
// we should still maintain a mounted_count > 1 if somehow new bind mounts from the filesystem inodes
|
|
// appear.
|
|
VERIFY(do_bind_mount != DoBindMount::Yes);
|
|
|
|
FileSystem::all_file_systems_list().with([&](auto& fs_list) {
|
|
fs_list.append(new_mount->guest_fs());
|
|
});
|
|
}
|
|
});
|
|
|
|
// NOTE: Leak the mount pointer so it can be added to the mount list, but it won't be
|
|
// deleted after being added.
|
|
mounts_list.append(*new_mount.leak_ptr());
|
|
}
|
|
|
|
ErrorOr<NonnullRefPtr<VFSRootContext>> VFSRootContext::create_with_empty_ramfs()
|
|
{
|
|
auto fs = TRY(RAMFS::try_create({}));
|
|
TRY(fs->initialize());
|
|
auto root_custody = TRY(Custody::try_create(nullptr, ""sv, fs->root_inode(), 0));
|
|
auto context = TRY(adopt_nonnull_ref_or_enomem(new (nothrow) VFSRootContext(root_custody)));
|
|
auto new_mount = TRY(adopt_nonnull_own_or_enomem(new (nothrow) Mount(fs->root_inode(), 0)));
|
|
TRY(context->m_details.with([&](auto& details) -> ErrorOr<void> {
|
|
dbgln("VFSRootContext({}): Root (\"/\") FileSystemID {}, Mounting {} at inode {} with flags {}",
|
|
context->id(),
|
|
fs->fsid(),
|
|
fs->class_name(),
|
|
root_custody->inode().identifier(),
|
|
0);
|
|
add_to_mounts_list_and_increment_fs_mounted_count(DoBindMount::No, details.mounts, move(new_mount));
|
|
return {};
|
|
}));
|
|
|
|
// Finally, add the context to the global list so it can be used.
|
|
VFSRootContext::all_root_contexts_list().with([&](auto& list) {
|
|
list.append(*context);
|
|
});
|
|
return context;
|
|
}
|
|
|
|
ErrorOr<void> VFSRootContext::pivot_root(FileBackedFileSystem::List& file_backed_file_systems_list, FileSystem& fs, NonnullOwnPtr<Mount> new_mount, NonnullRefPtr<Custody> root_mount_point, int root_mount_flags)
|
|
{
|
|
return m_details.with([&](auto& details) -> ErrorOr<void> {
|
|
return fs.mounted_count().with([&](auto& mounted_count) -> ErrorOr<void> {
|
|
// NOTE: If the mounted count is 0, then this filesystem is about to be
|
|
// deleted, so this must be a kernel bug as we don't include such filesystem
|
|
// in the VirtualFileSystem s_details->file_backed_file_systems_list list anymore.
|
|
VERIFY(mounted_count > 0);
|
|
|
|
// NOTE: The mounts table should not be empty as it always need
|
|
// to have at least one mount!
|
|
VERIFY(!details.mounts.is_empty());
|
|
|
|
// NOTE: If we have many mounts in the table, then simply don't allow
|
|
// userspace to override them but instead require to unmount everything except
|
|
// the root mount first.
|
|
if (details.mounts.size_slow() != 1)
|
|
return EPERM;
|
|
|
|
auto& mount = *details.mounts.first();
|
|
TRY(VirtualFileSystem::remove_mount(mount, file_backed_file_systems_list));
|
|
VERIFY(details.mounts.is_empty());
|
|
|
|
dbgln("VFSRootContext({}): Root mount set to FileSystemID {}, Mounting {} at inode {} with flags {}",
|
|
id(),
|
|
new_mount->guest_fs().fsid(),
|
|
new_mount->guest_fs().class_name(),
|
|
root_mount_point->inode().identifier(),
|
|
root_mount_flags);
|
|
|
|
// NOTE: Leak the mount pointer so it can be added to the mount list, but it won't be
|
|
// deleted after being added.
|
|
details.mounts.append(*new_mount.leak_ptr());
|
|
|
|
// NOTE: We essentially do the same thing like VFSRootContext::add_to_mounts_list_and_increment_fs_mounted_count function
|
|
// but because we already locked the spinlock of the attach count, we can't call that function here.
|
|
mounted_count++;
|
|
// NOTE: Now fill the root custody with a valid custody for the new root mount.
|
|
m_root_custody.with([&root_mount_point](auto& custody) {
|
|
custody = move(root_mount_point);
|
|
});
|
|
return {};
|
|
});
|
|
});
|
|
}
|
|
|
|
ErrorOr<void> VFSRootContext::do_full_teardown(Badge<PowerStateSwitchTask>)
|
|
{
|
|
// NOTE: We are going to tear down the entire VFS root context from its mounts.
|
|
// To do this properly, we swap out the original root custody with the empty
|
|
// root custody for vfs root context of kernel processes.
|
|
m_root_custody.with([](auto& custody) {
|
|
custody = VFSRootContext::empty_context_custody_for_kernel_processes();
|
|
});
|
|
|
|
auto unmount_was_successful = true;
|
|
while (unmount_was_successful) {
|
|
unmount_was_successful = false;
|
|
Vector<Mount&, 16> mounts;
|
|
TRY(m_details.with([&mounts](auto& details) -> ErrorOr<void> {
|
|
for (auto& mount : details.mounts) {
|
|
TRY(mounts.try_append(const_cast<Mount&>(mount)));
|
|
}
|
|
return {};
|
|
}));
|
|
if (mounts.is_empty())
|
|
break;
|
|
auto const remaining_mounts = mounts.size();
|
|
|
|
while (!mounts.is_empty()) {
|
|
auto& mount = mounts.take_last();
|
|
TRY(mount.guest_fs().flush_writes());
|
|
|
|
auto mount_path = TRY(mount.absolute_path());
|
|
auto& mount_inode = mount.guest();
|
|
auto const result = VirtualFileSystem::unmount(*this, mount_inode, mount_path->view());
|
|
if (result.is_error()) {
|
|
dbgln("Error during unmount of {}: {}", mount_path, result.error());
|
|
// FIXME: For unknown reasons the root FS stays busy even after everything else has shut down and was unmounted.
|
|
// Until we find the underlying issue, allow an unclean shutdown here.
|
|
if (remaining_mounts <= 1)
|
|
dbgln("BUG! One mount remaining; the root file system may not be unmountable at all. Shutting down anyways.");
|
|
} else {
|
|
unmount_was_successful = true;
|
|
}
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
ErrorOr<void> VFSRootContext::unmount(FileBackedFileSystem::List& file_backed_file_systems_list, Inode& guest_inode, StringView custody_path)
|
|
{
|
|
return m_details.with([&](auto& details) -> ErrorOr<void> {
|
|
bool did_unmount = false;
|
|
for (auto& mount : details.mounts) {
|
|
if (&mount.guest() != &guest_inode)
|
|
continue;
|
|
auto mountpoint_path = TRY(mount.absolute_path());
|
|
if (custody_path != mountpoint_path->view())
|
|
continue;
|
|
|
|
TRY(VFSRootContext::validate_mount_not_immutable_while_being_used(details, mount));
|
|
dbgln("VFSRootContext({}): Unmounting {}...", id(), custody_path);
|
|
TRY(VirtualFileSystem::remove_mount(mount, file_backed_file_systems_list));
|
|
did_unmount = true;
|
|
break;
|
|
}
|
|
if (!did_unmount) {
|
|
dbgln("VFSRootContext: Nothing mounted on inode {}", guest_inode.identifier());
|
|
return ENODEV;
|
|
}
|
|
|
|
// NOTE: The VFSRootContext mount table is not empty and we
|
|
// successfully deleted the desired mount from it, so return
|
|
// a success now.
|
|
if (!details.mounts.is_empty())
|
|
return {};
|
|
|
|
// NOTE: If the mount table is empty, then the VFSRootContext
|
|
// is no longer in valid state (each VFSRootContext at least should
|
|
// have a root mount), so remove it now.
|
|
VFSRootContext::all_root_contexts_list().with([this](auto& list) {
|
|
dbgln("VFSRootContext: Nothing mounted in VFSRootContext({}), removing it", id());
|
|
list.remove(*this);
|
|
});
|
|
return {};
|
|
});
|
|
}
|
|
|
|
void VFSRootContext::detach(Badge<Process>)
|
|
{
|
|
m_details.with([](auto& details) {
|
|
VERIFY(details.attached_by_process.was_set());
|
|
VERIFY(details.attach_count > 0);
|
|
details.attach_count--;
|
|
});
|
|
}
|
|
|
|
void VFSRootContext::attach(Badge<Process>)
|
|
{
|
|
m_details.with([](auto& details) {
|
|
details.attached_by_process.set();
|
|
details.attach_count++;
|
|
});
|
|
}
|
|
|
|
bool VFSRootContext::mount_point_exists_at_custody(Custody& mount_point)
|
|
{
|
|
return m_details.with([&](auto& details) -> bool {
|
|
return any_of(details.mounts, [&mount_point](auto const& existing_mount) {
|
|
return existing_mount.host_custody() && VirtualFileSystem::check_matching_absolute_path_hierarchy(*existing_mount.host_custody(), mount_point);
|
|
});
|
|
});
|
|
}
|
|
|
|
ErrorOr<void> VFSRootContext::do_on_mount_for_host_custody(ValidateImmutableFlag validate_immutable_flag, Custody const& current_custody, Function<void(Mount&)> callback) const
|
|
{
|
|
VERIFY(validate_immutable_flag == ValidateImmutableFlag::Yes || validate_immutable_flag == ValidateImmutableFlag::No);
|
|
return m_details.with([&](auto& details) -> ErrorOr<void> {
|
|
// NOTE: We either search for the root mount or for a mount that has a parent custody!
|
|
if (!current_custody.parent()) {
|
|
for (auto& mount : details.mounts) {
|
|
if (!mount.host_custody()) {
|
|
if (validate_immutable_flag == ValidateImmutableFlag::Yes)
|
|
TRY(VFSRootContext::validate_mount_not_immutable_while_being_used(details, mount));
|
|
|
|
callback(mount);
|
|
return {};
|
|
}
|
|
}
|
|
// NOTE: There must be a root mount entry, so fail if we don't find it.
|
|
VERIFY_NOT_REACHED();
|
|
} else {
|
|
for (auto& mount : details.mounts) {
|
|
if (mount.host_custody() && VirtualFileSystem::check_matching_absolute_path_hierarchy(*mount.host_custody(), current_custody)) {
|
|
if (validate_immutable_flag == ValidateImmutableFlag::Yes)
|
|
TRY(VFSRootContext::validate_mount_not_immutable_while_being_used(details, mount));
|
|
|
|
callback(mount);
|
|
return {};
|
|
}
|
|
}
|
|
}
|
|
return Error::from_errno(ENODEV);
|
|
});
|
|
}
|
|
|
|
ErrorOr<void> VFSRootContext::apply_to_mount_for_host_custody(Custody const& current_custody, Function<void(Mount&)> callback)
|
|
{
|
|
return do_on_mount_for_host_custody(ValidateImmutableFlag::Yes, current_custody, move(callback));
|
|
}
|
|
|
|
ErrorOr<VFSRootContext::CurrentMountState> VFSRootContext::current_mount_state_for_host_custody(Custody const& current_custody) const
|
|
{
|
|
RefPtr<FileSystem> guest_fs;
|
|
RefPtr<Inode> guest;
|
|
int mount_flags_for_child { 0 };
|
|
TRY(do_on_mount_for_host_custody(ValidateImmutableFlag::No, current_custody, [&guest, &guest_fs, &mount_flags_for_child](auto const& mount) {
|
|
guest = mount.guest();
|
|
guest_fs = mount.guest_fs();
|
|
mount_flags_for_child = mount.flags();
|
|
}));
|
|
|
|
return VFSRootContext::CurrentMountState {
|
|
Mount::Details { guest_fs.release_nonnull(), guest.release_nonnull() },
|
|
mount_flags_for_child
|
|
};
|
|
}
|
|
|
|
ErrorOr<void> VFSRootContext::add_new_mount(DoBindMount do_bind_mount, Inode& source, Custody& mount_point, int flags)
|
|
{
|
|
auto new_mount = TRY(adopt_nonnull_own_or_enomem(new (nothrow) Mount(source, mount_point, flags)));
|
|
return m_details.with([&](auto& details) -> ErrorOr<void> {
|
|
// NOTE: The VFSRootContext should be attached to the list if there's
|
|
// at least one mount in the mount table.
|
|
// We also should have at least one mount in the table because
|
|
// this method shouldn't be called for new contexts when adding
|
|
// their root mounts.
|
|
VERIFY(!details.mounts.is_empty());
|
|
VFSRootContext::all_root_contexts_list().with([&](auto& list) {
|
|
VERIFY(list.contains(*this));
|
|
});
|
|
|
|
VERIFY(&new_mount->guest_fs() == &source.fs());
|
|
if (do_bind_mount == DoBindMount::No) {
|
|
VERIFY(&source == &source.fs().root_inode());
|
|
dbgln("VFSRootContext({}): FileSystemID {}, Mounting {} at inode {} with flags {}",
|
|
id(),
|
|
source.fs().fsid(),
|
|
source.fs().class_name(),
|
|
mount_point.inode().identifier(),
|
|
flags);
|
|
} else {
|
|
dbgln("VFSRootContext({}): Bind-mounting inode {} at inode {}", id(), source.identifier(), mount_point.inode().identifier());
|
|
}
|
|
|
|
if (mount_point_exists_at_custody(mount_point)) {
|
|
dbgln("VFSRootContext({}): Mounting unsuccessful - inode {} is already a mount-point.", id(), mount_point.inode().identifier());
|
|
return EBUSY;
|
|
}
|
|
add_to_mounts_list_and_increment_fs_mounted_count(do_bind_mount, details.mounts, move(new_mount));
|
|
return {};
|
|
});
|
|
}
|
|
|
|
}
|