Newer
Older
minerva / Userland / Utilities / runc / main.cpp
@minerva minerva on 13 Jul 8 KB Initial commit
/*
 * Copyright (c) 2024, Liav A. <liavalb@hotmail.co.il>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/String.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/File.h>
#include <LibCore/System.h>
#include <LibMain/Main.h>
#include <sys/prctl.h>
#include <unistd.h>

#include "LayoutParsing.h"
#include "VFSRootContextLayout.h"

static ErrorOr<unsigned> create_vfs_root_context_unshare()
{
    return TRY(Core::System::unshare_create(Kernel::UnshareType::VFSRootContext, 0));
}

static ErrorOr<void> create_custom_vfs_root_context_layout(JsonArray const& layout_creation_sequence)
{
    auto vfs_root_context_index = TRY(create_vfs_root_context_unshare());
    char pattern[] = "/tmp/container_root_XXXXXX";
    auto temp_directory_path = TRY(Core::System::mkdtemp(pattern));
    auto vfs_root_context_layout = TRY(VFSRootContextLayout::create(temp_directory_path.bytes_as_string_view(), vfs_root_context_index));
    TRY(LayoutParsing::handle_creation_sequence(*vfs_root_context_layout, layout_creation_sequence));

    TRY(vfs_root_context_layout->apply_mounts_on_vfs_root_context_id());

    TRY(Core::System::rmdir(temp_directory_path));
    TRY(Core::System::unshare_attach(Kernel::UnshareType::VFSRootContext, vfs_root_context_index));

    // Finally, apply changes to current working directory.
    (void)chdir("/");
    return {};
}

static ErrorOr<void> create_and_attach_scoped_process_list_unshare()
{
    auto scoped_process_list_index = TRY(Core::System::unshare_create(Kernel::UnshareType::ScopedProcessList, 0));
    TRY(Core::System::unshare_attach(Kernel::UnshareType::ScopedProcessList, scoped_process_list_index));
    return {};
}

static ErrorOr<void> create_and_attach_hostname_context(StringView hostname)
{
    auto hostname_context_index = TRY(Core::System::unshare_create(Kernel::UnshareType::HostnameContext, 0));
    TRY(Core::System::unshare_attach(Kernel::UnshareType::HostnameContext, hostname_context_index));
    TRY(Core::System::sethostname(hostname));
    return {};
}

static ErrorOr<void> extract_values_from_file(Core::File& file, String& command, JsonArray& new_vfs_root_context_layout_creation_sequence, bool& pid_isolation, Optional<String>& hostname_context_name, bool& enforce_jail)
{
    auto file_contents = TRY(file.read_until_eof());
    auto json = TRY(JsonValue::from_string(file_contents));
    auto configuration_object = json.as_object();
    if (!configuration_object.has_bool("jail"sv))
        return Error::from_string_literal("JSON configuration invalid: Jail enforcement flag is not specified");
    if (!configuration_object.has_bool("pid-isolation"sv))
        return Error::from_string_literal("JSON configuration invalid: PID isolation flag is not specified");
    if (!configuration_object.has_string("command"sv))
        return Error::from_string_literal("JSON configuration invalid: Command is not specified");
    if (!configuration_object.has_array("layout"sv))
        return Error::from_string_literal("JSON configuration invalid: VFS root context layout is not specified");

    if (configuration_object.has_null("hostname"sv) && configuration_object.has_string("hostname"sv))
        return Error::from_string_literal("JSON configuration invalid: Can't have hostname as null and string during configuration");
    if (!configuration_object.has_null("hostname"sv) && !configuration_object.has_string("hostname"sv))
        return Error::from_string_literal("JSON configuration invalid: Hostname is not specified");

    new_vfs_root_context_layout_creation_sequence = move(configuration_object.get("layout"sv).value().as_array());
    pid_isolation = configuration_object.get("pid-isolation"sv).value().as_bool();
    enforce_jail = configuration_object.get("jail"sv).value().as_bool();

    auto possible_hostname = configuration_object.get_byte_string("hostname"sv);
    if (possible_hostname.has_value())
        hostname_context_name = TRY(String::from_byte_string(possible_hostname.value()));

    command = TRY(String::from_byte_string(configuration_object.get("command"sv).value().as_string()));
    return {};
}

static ErrorOr<void> deploy_container_based_on_config_file(StringView config_file_path)
{
    TRY(Core::System::pledge("stdio rpath wpath cpath proc mount unshare exec fattr chown"));
    auto file = TRY(Core::File::open(config_file_path, Core::File::OpenMode::Read));

    bool pid_isolation = false;
    bool enforce_jail = false;
    String command;
    Optional<String> hostname_context_name {};
    JsonArray new_vfs_root_context_layout_creation_sequence {};

    TRY(extract_values_from_file(*file, command, new_vfs_root_context_layout_creation_sequence, pid_isolation, hostname_context_name, enforce_jail));

    // NOTE: First we gather all information, then we start deploying.
    // To ensure proper functionality, we do this in the following sequence:
    // - Create PID isolation and attach the scoped process list
    // - Creating a VFS root context
    // - Populating the VFS root context with the desired layout
    // - Attach to the VFS root context
    // - Attach to the hostname context
    // - Enforce jail restrictions

    if (pid_isolation)
        TRY(create_and_attach_scoped_process_list_unshare());

    // NOTE: To be able to properly create the desired layout of the container,
    // we first mount a new filesystem instance in a temporary location, then we
    // populate its directories and mount subsequent mounts if so desired.
    // Then we copy all mounts (from the root directory of the VFS root context)
    // up to all of its subsequent mounts, completing the filesystem skeleton of the
    // container.
    if (!new_vfs_root_context_layout_creation_sequence.is_empty())
        TRY(create_custom_vfs_root_context_layout(new_vfs_root_context_layout_creation_sequence));

    // Remove the fattr & chown pledges
    TRY(Core::System::pledge("stdio rpath wpath cpath proc mount unshare exec"));

    if (hostname_context_name.has_value())
        TRY(create_and_attach_hostname_context(hostname_context_name.value().bytes_as_string_view()));

    // Remove the unshare pledge
    TRY(Core::System::pledge("stdio rpath wpath cpath proc mount exec"));

    if (enforce_jail)
        TRY(Core::System::enter_jail_mode_until_exit());

    // Remove the proc pledge
    TRY(Core::System::pledge("stdio rpath wpath cpath mount exec"));

    // FIXME: Find a better way to convert between String and Vector<StringView>
    auto splitted_command = command.bytes_as_string_view().split_view(' ');
    TRY(Core::System::exec_command(splitted_command, false));
    return {};
}

ErrorOr<int> minerva_main(Main::Arguments arguments)
{
    bool pid_isolation = false;
    bool enforce_jail = false;
    bool preserve_env = false;

    Vector<StringView> command;
    StringView config_file;

    Core::ArgsParser args_parser;
    args_parser.set_stop_on_first_non_option(true);
    args_parser.add_option(pid_isolation, "Create new process list", "pid-isolation", 'p');
    args_parser.add_option(config_file, "Use JSON-based configruation file", "configuration", 'f', "");
    args_parser.add_option(enforce_jail, "Enforce jail restrictions on container", "enforce-jail", 'j');
    args_parser.add_option(preserve_env, "Preserve user environment when running command", "preserve-env", 'E');
    args_parser.add_positional_argument(command, "Command to run at elevated privilege level", "command", Core::ArgsParser::Required::No);
    args_parser.parse(arguments);

    if (!config_file.is_null() && !config_file.is_empty()) {
        TRY(deploy_container_based_on_config_file(config_file));
        return 0;
    }

    TRY(Core::System::pledge("stdio rpath wpath cpath proc unshare exec"));

    if (command.is_empty())
        return Error::from_string_view("Can't create a container with no specified command."sv);

    if (!(pid_isolation || enforce_jail))
        return Error::from_string_view("Can't create a container with no attributes (jail/pid-isolation)."sv);

    if (pid_isolation)
        TRY(create_and_attach_scoped_process_list_unshare());
    // Remove the unshare pledge
    TRY(Core::System::pledge("stdio rpath wpath cpath proc exec"));

    if (enforce_jail)
        TRY(Core::System::enter_jail_mode_until_exit());

    // Remove the proc pledge
    TRY(Core::System::pledge("stdio rpath wpath cpath exec"));

    TRY(Core::System::exec_command(command, preserve_env));
    return 0;
}