mirror of
https://github.com/SerenityOS/serenity
synced 2026-04-26 01:25:22 +02:00
That functionality seems to be too much complicated. We shouldn't overengineer how the copy_mount syscall works, so instead of allowing replacement of the root filesystem, let's make the unshare file descriptor to be configured via a special ioctl call before we initialize a new VFSRootContext object. The special ioctl can either set a new root filesystem for the upcoming VFSRootContext object, or remove it (by passing fd of -1). If there's no specified root filesystem, a new RAMFS instance will be created automatically when invoking the unshare_create syscall. This also simplifies the code in the boot process, hence making it much more readable. It should be noted, that we assumed during pivot_root that the first mountpoint in a context is the root mountpoint, which is probably a fair assumption, but we don't assume this anywhere else in the VFSRootContext code. If this functionality ever comes back, we should ensure that we make some effort to not assume this again.
350 lines
15 KiB
C++
350 lines
15 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>
|
|
|
|
extern SetOnce g_not_in_early_boot;
|
|
|
|
namespace Kernel {
|
|
|
|
static RawPtr<VFSRootContext> s_empty_context;
|
|
static RawPtr<Custody> s_empty_context_custody;
|
|
static Atomic<u64> s_vfs_root_context_id = 0;
|
|
|
|
ErrorOr<NonnullRefPtr<VFSRootContext>> VFSRootContext::create_with_empty_ramfs(AddToGlobalContextList add_to_global_context_list)
|
|
{
|
|
// NOTE: It's not expected that this will be called for creating a
|
|
// context that will not be added into the global list after some
|
|
// point during the boot process.
|
|
if (g_not_in_early_boot.was_set())
|
|
VERIFY(add_to_global_context_list == AddToGlobalContextList::Yes);
|
|
|
|
auto fs = TRY(RAMFS::try_create({}));
|
|
TRY(fs->initialize());
|
|
return create_with_filesystem(add_to_global_context_list, fs);
|
|
}
|
|
|
|
ErrorOr<NonnullRefPtr<VFSRootContext>> VFSRootContext::create_with_filesystem(AddToGlobalContextList add_to_global_context_list, FileSystem& fs)
|
|
{
|
|
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> {
|
|
if (add_to_global_context_list == AddToGlobalContextList::Yes) {
|
|
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 {};
|
|
}));
|
|
|
|
if (add_to_global_context_list == AddToGlobalContextList::Yes) {
|
|
// 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;
|
|
}
|
|
|
|
UNMAP_AFTER_INIT void VFSRootContext::initialize_empty_ramfs_root_context_for_kernel_processes()
|
|
{
|
|
// NOTE: We don't add the context to the vfs root contexts list and
|
|
// we leak a ref, as this context is artificially created only for
|
|
// kernel processes.
|
|
s_empty_context = &MUST(VFSRootContext::create_with_empty_ramfs(AddToGlobalContextList::No)).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();
|
|
}
|
|
|
|
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<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, Details& details)
|
|
{
|
|
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<NonnullRefPtr<FileSystem>> VFSRootContext::mount_point_to_guest_filesystem(Custody const& custody)
|
|
{
|
|
return m_details.with([&](auto& details) -> ErrorOr<NonnullRefPtr<FileSystem>> {
|
|
// NOTE: We either search for the root mount or for a mount that has a parent custody!
|
|
if (!custody.parent()) {
|
|
for (auto& mount : details.mounts) {
|
|
if (!mount.host_custody())
|
|
return mount.guest_fs();
|
|
}
|
|
// 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(), custody))
|
|
return mount.guest_fs();
|
|
}
|
|
}
|
|
return ENODEV;
|
|
});
|
|
}
|
|
|
|
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, details)) {
|
|
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 {};
|
|
});
|
|
}
|
|
|
|
}
|