diff --git a/meson.build b/meson.build index 7b5aac9..72b2910 100644 --- a/meson.build +++ b/meson.build @@ -37,10 +37,11 @@ lib_src = [ 'src/uenv/meta.cpp', 'src/uenv/parse.cpp', 'src/uenv/repository.cpp', + 'src/uenv/uenv.cpp', + 'src/util/fs.cpp', 'src/util/shell.cpp', 'src/util/strings.cpp', 'src/util/subprocess.cpp', - 'src/uenv/uenv.cpp', ] lib_inc = include_directories('src') @@ -76,6 +77,7 @@ endif unit_src = [ 'test/unit/env.cpp', 'test/unit/envvars.cpp', + 'test/unit/fs.cpp', 'test/unit/lex.cpp', 'test/unit/main.cpp', 'test/unit/parse.cpp', diff --git a/src/uenv/env.cpp b/src/uenv/env.cpp index 44d46b6..3570116 100644 --- a/src/uenv/env.cpp +++ b/src/uenv/env.cpp @@ -13,23 +13,62 @@ #include #include #include +#include +#include namespace uenv { using util::unexpected; +struct meta_info { + std::optional path; + std::optional env; +}; + +meta_info find_meta_path(const std::filesystem::path& sqfs_path) { + namespace fs = std::filesystem; + + meta_info meta; + if (fs::is_directory(sqfs_path.parent_path() / "meta")) { + meta.path = sqfs_path.parent_path() / "meta"; + spdlog::debug("find_meta_path: {} found external meta path {}", + sqfs_path.string(), meta.path->string()); + } else if (auto p = util::unsquashfs_tmp(sqfs_path, "meta")) { + meta.path = *p / "meta"; + spdlog::debug("find_meta_path: {} found internal meta path {}", + sqfs_path.string(), meta.path->string()); + } else { + spdlog::debug("find_meta_path: {} no meta path found", + sqfs_path.string()); + } + + if (meta.path) { + auto env_meta = meta.path.value() / "env.json"; + + if (fs::is_regular_file(env_meta)) { + meta.env = env_meta; + spdlog::debug("find_meta_path: {} found env meta {}", + sqfs_path.string(), meta.env->string()); + } else { + spdlog::debug("find_meta_path: {} no env meta", sqfs_path.string()); + } + } + + return meta; +} + util::expected concretise_env(const std::string& uenv_args, std::optional view_args, std::optional repo_arg) { namespace fs = std::filesystem; - // parse the uenv description that was provided as a command line argument. - // the command line argument is a comma-separated list of uenvs, where each - // uenv is either + // parse the uenv description that was provided as a command line + // argument. the command line argument is a comma-separated list of + // uenvs, where each uenv is either // - the path of a squashfs image; or - // - a uenv description of the form name[/version][:tag][@system][%uarch] - // with an optional mount point. + // - a uenv description of the form + // name[/version][:tag][@system][%uarch] with an optional mount point. const auto uenv_descriptions = uenv::parse_uenv_args(uenv_args); if (!uenv_descriptions) { return unexpected(fmt::format("invalid uenv description: {}", @@ -38,8 +77,8 @@ concretise_env(const std::string& uenv_args, // concretise the uenv descriptions by looking for the squashfs file, or // looking up the uenv descrition in a registry. - // after this loop, we have fully validated list of uenvs, mount points and - // meta data (if they have meta data). + // after this loop, we have fully validated list of uenvs, mount points + // and meta data (if they have meta data). std::unordered_map uenvs; std::set used_mounts; @@ -52,9 +91,10 @@ concretise_env(const std::string& uenv_args, // it has to be looked up in a repo. if (auto label = desc.label()) { if (!repo_arg) { - return unexpected( - "a repo needs to be provided either using the --repo flag " - "or by setting the UENV_REPO_PATH environment variable"); + return unexpected("a repo needs to be provided either " + "using the --repo flag " + "or by setting the UENV_REPO_PATH " + "environment variable"); } auto store = uenv::open_repository(*repo_arg); if (!store) { @@ -119,22 +159,22 @@ concretise_env(const std::string& uenv_args, } spdlog::info("{} squashfs image {}", desc, sqfs_path.string()); - // set the meta data path and env.json path if they exist - const auto meta_p = sqfs_path.parent_path() / "meta"; - const auto env_meta_p = meta_p / "env.json"; - const std::optional meta_path = - fs::is_directory(meta_p) ? meta_p : std::optional{}; - const std::optional env_meta_path = - fs::is_regular_file(env_meta_p) ? env_meta_p - : std::optional{}; - - // if meta/env.json exists, parse the json therein - std::string name; - std::string description; + // create a default "anonymous" name for the uenv that will be + // overwritten if meta data is provided. + std::string name = "anonymous"; + unsigned name_idx = 0; + while (uenvs.count(name)) { + name = fmt::format("anonymous{}", name_idx); + ++name_idx; + } + std::string description = ""; std::optional mount_meta; std::unordered_map views; - if (env_meta_path) { - if (const auto result = uenv::load_meta(*env_meta_path)) { + + // if meta/env.json exists, parse the json therein + auto meta = find_meta_path(sqfs_path); + if (meta.env) { + if (const auto result = uenv::load_meta(*(meta.env))) { name = std::move(result->name); description = result->description.value_or(""); mount_meta = result->mount; @@ -143,27 +183,19 @@ concretise_env(const std::string& uenv_args, mount_meta); } else { spdlog::warn("{} opening the uenv meta data {}: {}", desc, - *env_meta_path, result.error()); + meta.env->string(), result.error()); } } else { - spdlog::debug("{} no meta file found at expected location {}", desc, - meta_path); - description = ""; - // generate a unique name for the uenv - name = "anonymous"; - unsigned i = 1; - while (uenvs.count(name)) { - name = fmt::format("anonymous{}", i); - ++i; - } + spdlog::warn("{} no meta file available for {}", desc, + sqfs_path.string()); } // if an explicit mount point was provided, use that // otherwise use the mount point provided in the meta data auto mount_string = desc.mount() ? desc.mount() : mount_meta; - // handle the case where no mount point was provided by the CLI or meta - // data + // handle the case where no mount point was provided by the CLI or + // meta data if (!mount_string) { return unexpected( fmt::format("no mount point provided for {}", desc)); @@ -212,7 +244,7 @@ concretise_env(const std::string& uenv_args, } uenvs[name] = concrete_uenv{name, mount, sqfs_path, - meta_path, description, std::move(views)}; + meta.path, description, std::move(views)}; } // A dictionary with view name as a key, and a list of uenv that provide @@ -249,7 +281,8 @@ concretise_env(const std::string& uenv_args, // a list of uenv that have a view with name v.name const auto& matching_uenvs = view2uenv[view.name]; - // handle the case where no uenv name was provided, e.g. develop + // handle the case where no uenv name was provided, e.g. + // develop if (!view.uenv) { // it is ambiguous if more than one option is available if (matching_uenvs.size() > 1) { @@ -263,8 +296,8 @@ concretise_env(const std::string& uenv_args, } views.push_back({matching_uenvs[0], view.name}); } - // handle the case where both uenv and view name are provided, - // e.g. prgenv-gnu:develop + // handle the case where both uenv and view name are + // provided, e.g. prgenv-gnu:develop else { auto it = std::find_if( matching_uenvs.begin(), matching_uenvs.end(), @@ -298,8 +331,8 @@ std::unordered_map getenv(const env& environment) { // returns the value of an environment variable. // if the variable has been recorded in env_vars, that value is returned - // else the cstdlib getenv function is called to get the currently set value - // returns nullptr if the variable is not set anywhere + // else the cstdlib getenv function is called to get the currently set + // value returns nullptr if the variable is not set anywhere auto ge = [&env_vars](const std::string& name) -> const char* { if (env_vars.count(name)) { return env_vars[name].c_str(); @@ -307,10 +340,9 @@ std::unordered_map getenv(const env& environment) { return ::getenv(name.c_str()); }; - // iterate over each view in order, and set the environment variables that - // each view configures. - // the variables are not set directly, instead they are accumulated in - // env_vars. + // iterate over each view in order, and set the environment variables + // that each view configures. the variables are not set directly, + // instead they are accumulated in env_vars. for (auto& view : environment.views) { auto result = environment.uenvs.at(view.uenv) .views.at(view.name) @@ -323,11 +355,53 @@ std::unordered_map getenv(const env& environment) { return env_vars; } +// list of environment variables that are ignored in setuid applications +// the full list is defined here: +// https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/generic/unsecvars.h +std::set unsecure_envvars__{ + "GCONV_PATH", + "GETCONF_DIR", + "GLIBC_TUNABLES", + "HOSTALIASES", + "LD_AUDIT", + "LD_BIND_NOT", + "LD_BIND_NOW", + "LD_DEBUG", + "LD_DEBUG_OUTPUT", + "LD_DYNAMIC_WEAK", + "LD_LIBRARY_PATH", + "LD_ORIGIN_PATH", + "LD_PRELOAD", + "LD_PROFILE", + "LD_SHOW_AUXV", + "LD_VERBOSE", + "LD_WARN", + "LOCALDOMAIN", + "LOCPATH", + "MALLOC_ARENA_MAX", + "MALLOC_ARENA_TEST", + "MALLOC_MMAP_MAX_", + "MALLOC_MMAP_THRESHOLD_", + "MALLOC_PERTURB_", + "MALLOC_TOP_PAD_", + "MALLOC_TRACE", + "MALLOC_TRIM_THRESHOLD_", + "NIS_PATH", + "NLSPATH", + "RESOLV_HOST_CONF", + "RES_OPTIONS", + "TMPDIR", +}; + util::expected setenv(const std::unordered_map& variables, const std::string& prefix) { for (auto var : variables) { - std::string fwd_name = prefix + var.first; + // prepend prefix to unsecure environment variables + std::string fwd_name = unsecure_envvars__.contains(var.first) + ? prefix + var.first + : var.first; + fmt::println("setting {} to {}", fwd_name, var.second); if (auto rcode = ::setenv(fwd_name.c_str(), var.second.c_str(), true)) { switch (rcode) { case EINVAL: @@ -340,6 +414,7 @@ setenv(const std::unordered_map& variables, fmt::format("unknown error setting {}", fwd_name)); } } + fmt::println("set!"); } return 0; } diff --git a/src/util/fs.cpp b/src/util/fs.cpp new file mode 100644 index 0000000..ee9f4fc --- /dev/null +++ b/src/util/fs.cpp @@ -0,0 +1,103 @@ +#include +#include +#include +#include + +#include +#include +#include + +#include "expected.h" +#include "fs.h" +#include "subprocess.h" + +namespace util { + +struct temp_dir_wrap { + std::filesystem::path path; + ~temp_dir_wrap() { + if (std::filesystem::is_directory(path)) { + // ignore the error code - being unable to delete a temp path is not + // the end of the world. + std::error_code ec; + auto n = std::filesystem::remove_all(path, ec); + spdlog::debug("temp_dir_wrap: deleted {} files in {}", n, + path.string()); + } + } +}; + +// persistant storage for the temporary paths that will delete the paths on +// exit. This makes temporary paths persistent for the duration of the +// application's execution. +// Use a deque because it will not copy/move/delete its contents as it grows. +static std::deque tmp_dir_cache; + +// the temp paths are deleted automatically when tmp_dir_cache is cleaned up +// at the end of execution, except when execvp is used to replace the current +// process. +// use this function to force early clean up in such situations, so that no +// tmp paths remain after execution. +void clear_temp_dirs() { + tmp_dir_cache.clear(); +} + +std::filesystem::path make_temp_dir() { + namespace fs = std::filesystem; + auto tmp_template = + fs::temp_directory_path().string() + "/uenv-XXXXXXXXXXXX"; + std::vector base(tmp_template.data(), + tmp_template.data() + tmp_template.size() + 1); + + fs::path tmp_path = mkdtemp(base.data()); + + spdlog::debug("make_temp_dir: created {}", tmp_path.string(), + fs::is_directory(tmp_path)); + + tmp_dir_cache.emplace_back(tmp_path); + + return tmp_path; +} + +util::expected +unsquashfs_tmp(const std::filesystem::path& sqfs, + const std::filesystem::path& contents) { + namespace fs = std::filesystem; + + if (!fs::is_regular_file(sqfs)) { + return unexpected(fmt::format("unsquashfs_tmp: {} file does not exist", + sqfs.string())); + } + + auto base = make_temp_dir(); + std::vector command{"unsquashfs", "-no-exit", + "-d", base.string(), + sqfs.string(), contents.string()}; + spdlog::debug("unsquashfs_tmp: attempting to unpack {} from {}", + contents.string(), sqfs.string()); + + auto proc = run(command); + + if (!proc) { + return unexpected(fmt::format( + "unsquashfs_tmp: unable to run unsquashfs: {}", proc.error())); + } + + auto status = proc->wait(); + + spdlog::debug("unsquashfs_tmp: command '{}' retured status {}", + fmt::join(command, " "), status); + + if (status != 0) { + spdlog::warn("unsquashfs_tmp: unable to extract {} from {}", + contents.string(), sqfs.string()); + return unexpected( + fmt::format("unsquashfs_tmp: unable to extract {} from {}", + contents.string(), sqfs.string())); + } + + spdlog::info("unsquashfs_tmp: unpacked {} from {} to {}", contents.string(), + sqfs.string(), base.string()); + return base; +} +} // namespace util diff --git a/src/util/fs.h b/src/util/fs.h new file mode 100644 index 0000000..752382e --- /dev/null +++ b/src/util/fs.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#include + +namespace util { + +std::filesystem::path make_temp_dir(); + +util::expected +unsquashfs_tmp(const std::filesystem::path& sqfs, + const std::filesystem::path& contents); + +void clear_temp_dirs(); + +} // namespace util diff --git a/src/util/shell.cpp b/src/util/shell.cpp index a7ddf51..8dde7d3 100644 --- a/src/util/shell.cpp +++ b/src/util/shell.cpp @@ -10,6 +10,7 @@ #include #include +#include namespace util { @@ -37,6 +38,11 @@ util::expected current_shell() { int exec(const std::vector& args) { std::vector argv; + // clean up temporary files before calling execve, because the descructor + // that manages their deletion will not be deleted after the current process + // has been replaced. + util::clear_temp_dirs(); + // unsafe { for (auto& arg : args) { argv.push_back(const_cast(arg.c_str())); diff --git a/test/integration/cli.bats b/test/integration/cli.bats index f550717..ef64d13 100644 --- a/test/integration/cli.bats +++ b/test/integration/cli.bats @@ -11,7 +11,7 @@ function setup() { export SRC_PATH=$(realpath ../../) - export PATH="$(realpath ../../install/bin):$PATH" + export PATH="$(realpath ../../build):$PATH" unset UENV_MOUNT_LIST @@ -154,3 +154,37 @@ function teardown() { assert_failure assert_line --partial "Permission denied" } + +@test "run" { + export UENV_REPO_PATH=$REPOS/apptool + + # + # check that run looks up images in the repo and mounts at the correct location + # + run uenv run tool -- ls /user-tools + assert_success + assert_output --regexp "meta" + + run uenv run app/42.0:v1 -- ls /user-environment + assert_success + assert_output --regexp "meta" + + # + # check --view + # + run uenv run --view=tool tool -- tool + assert_success + assert_output "hello tool" + + run uenv run --view=app app/42.0:v1 -- app + assert_success + assert_output "hello app" + + # + # check --view works when reading meta data from inside a standalone sqfs file + # + + run uenv run --view=tool $SQFS_LIB/apptool/standalone/tool.squashfs -- tool + assert_success + assert_output "hello tool" +} diff --git a/test/setup/setup_repos.bash b/test/setup/setup_repos.bash index 08f6091..206846c 100755 --- a/test/setup/setup_repos.bash +++ b/test/setup/setup_repos.bash @@ -39,6 +39,11 @@ function setup_repo_apptool() { cp ${sqfs} $img_path/store.squashfs cp -R ${sources}/${name}/meta $img_path done + + img_path="$sqfs_path/standalone" + mkdir -p $img_path + cp ${sqfs} $img_path/${name}.squashfs + rm ${sqfs} sed -i "s|{${name}-sha}|${sha}|g" schema.sql diff --git a/test/unit/fs.cpp b/test/unit/fs.cpp new file mode 100644 index 0000000..0f02d09 --- /dev/null +++ b/test/unit/fs.cpp @@ -0,0 +1,53 @@ +#include + +#include +#include + +#include +#include + +namespace fs = std::filesystem; + +TEST_CASE("make_temp_dir", "[fs]") { + auto dir1 = util::make_temp_dir(); + REQUIRE(fs::is_directory(dir1)); + auto dir2 = util::make_temp_dir(); + REQUIRE(dir1 != dir2); +} + +TEST_CASE("unsquashfs", "[fs]") { + std::string sqfs = "../test/scratch/sqfs/apptool/standalone/app43.squashfs"; + { + REQUIRE(fs::is_regular_file(sqfs)); + auto meta = util::unsquashfs_tmp(sqfs, "meta"); + REQUIRE(meta); + REQUIRE(fs::is_directory(*meta)); + REQUIRE(fs::is_directory(*meta / "meta")); + REQUIRE(fs::is_regular_file(*meta / "meta" / "env.json")); + } + // unpack from a squashfs image many times to validate that the unpacked + // data is persistant. + { + const int nbuf = 128; + { + std::vector paths; + for (int i = 0; i < nbuf; ++i) { + auto meta = util::unsquashfs_tmp(sqfs, "meta/env.json"); + REQUIRE(meta); + auto file = *meta / "meta/env.json"; + REQUIRE(fs::is_regular_file(file)); + paths.push_back(file); + } + + // the generated paths should be unique + std::sort(paths.begin(), paths.end()); + auto e = std::unique(paths.begin(), paths.end()); + REQUIRE(e == paths.end()); + + // the generated paths should still exist + for (auto& file : paths) { + REQUIRE(fs::is_regular_file(file)); + } + } + } +} diff --git a/test/unit/main.cpp b/test/unit/main.cpp index 9f94c54..a086582 100644 --- a/test/unit/main.cpp +++ b/test/unit/main.cpp @@ -1,3 +1,5 @@ +#include + #define CATCH_CONFIG_MAIN #include @@ -5,6 +7,34 @@ static struct unit_init_log { unit_init_log() { - uenv::init_log(spdlog::level::trace, spdlog::level::off); + // use warn as the default log level + auto log_level = spdlog::level::warn; + bool invalid_env = false; + + // check the environment variable UENV_LOG_LEVEL + auto log_level_str = std::getenv("UENV_LOG_LEVEL"); + if (log_level_str != nullptr) { + int lvl; + auto [ptr, ec] = std::from_chars( + log_level_str, log_level_str + std::strlen(log_level_str), lvl); + + if (ec == std::errc()) { + if (lvl == 1) { + log_level = spdlog::level::info; + } else if (lvl == 2) { + log_level = spdlog::level::debug; + } else if (lvl > 2) { + log_level = spdlog::level::trace; + } + } else { + invalid_env = true; + } + } + uenv::init_log(log_level, spdlog::level::info); + if (invalid_env) { + spdlog::warn(fmt::format("UENV_LOG_LEVEL invalid value '{}' -- " + "expected a value between 0 and 3", + log_level_str)); + } } } uil{}; diff --git a/test/unit/repository.cpp b/test/unit/repository.cpp index b6e3824..1645bc4 100644 --- a/test/unit/repository.cpp +++ b/test/unit/repository.cpp @@ -16,7 +16,6 @@ TEST_CASE("read-only", "[repository]") { SKIP(fmt::format("{}", store.error())); } - fmt::println("db path: {}", store->db_path().string()); { auto results = store->query({{}, {}, {}, {}, {}}); if (!results) { @@ -29,7 +28,6 @@ TEST_CASE("read-only", "[repository]") { //} } - fmt::println(""); { auto results = store->query({"mch", {}, {}, {}, {}}); if (!results) { @@ -42,7 +40,6 @@ TEST_CASE("read-only", "[repository]") { //} } - fmt::println(""); { auto results = store->query({{}, "v7", {}, {}, {}}); if (!results) { @@ -55,7 +52,6 @@ TEST_CASE("read-only", "[repository]") { //} } - fmt::println(""); { auto results = store->query({{}, "24.7", "v1-rc1", {}, "a100"}); if (!results) { @@ -68,7 +64,6 @@ TEST_CASE("read-only", "[repository]") { //} } - fmt::println(""); { auto results = store->query({"wombat", {}, {}, {}, {}}); if (!results) {