Repositories / jai.git
jai.git
Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git
@@ -66,7 +66,7 @@ fdpath(int fd, const path &file, bool must) return res; } -PathSet +PathMultiset mountpoints(const path &mountinfo) { RaiiHelper<mnt_unref_table> t = mnt_new_table(); @@ -79,7 +79,7 @@ mountpoints(const path &mountinfo) if (!i) err("mnt_new_iter(MNT_ITER_FORWARD) failed"); - PathSet res; + PathMultiset res; libmnt_fs *mp = nullptr; while (!mnt_table_next_fs(t, i, &mp)) if (const char *target = mnt_fs_get_target(mp)) @@ -155,10 +155,9 @@ recursive_umount(const path &tree, bool detach) auto dirs = subtree_rev(mps, tree); for (const auto &dir : dirs) { if (umount2(dir.c_str(), UMOUNT_NOFOLLOW)) { - std::println(stderr, R"(umount("{}"): {})", dir.string(), - strerror(errno)); + warn(R"(umount("{}"): {})", dir.string(), strerror(errno)); if (detach && umount2(dir.c_str(), UMOUNT_NOFOLLOW | MNT_DETACH) == 0) - std::println(stderr, "did lazy unmount of {}\n", dir.string()); + warn("did lazy unmount of {}\n", dir.string()); } } } @@ -353,11 +352,18 @@ set_fd_acl(int fd, const char *acltext, AclType which) syserr(R"(acl_set_file("{}", DEFAULT, {}))", fdpath(fd), acltext); } -std::string -read_file(int dfd, path file) +std::expected<std::string, std::system_error> +try_read_file(int dfd, path file) { Fd fdholder; - int fd = file.empty() ? dfd : *(fdholder = xopenat(fd, file, O_RDONLY)); + int fd = dfd; + if (!file.empty()) { + fdholder = openat(fd, file.c_str(), O_RDONLY, O_CLOEXEC); + if (!fdholder) + return std::unexpected( + std::system_error(errno, std::system_category(), fdpath(fd, file))); + fd = *fdholder; + } std::string ret; if (auto sb = xfstat(fd); sb.st_size > 0x100'0000) @@ -371,7 +377,8 @@ read_file(int dfd, path file) if (n == 0) return ret; if (n < 0) - syserr("read {}", fdpath(fd)); + return std::unexpected( + std::system_error(errno, std::system_category(), fdpath(fd, file))); ret.append(buf, size_t(n)); } }
@@ -42,12 +42,13 @@ struct PathLess { } }; -using PathSet = std::multiset<path, PathLess>; +using PathSet = std::set<path, PathLess>; +using PathMultiset = std::multiset<path, PathLess>; // Return a range for a subtree rooted at root. root itself will be // returned only if it does not contain a trailing slash. inline auto -subtree(const PathSet &s, const path &root) +subtree(const is_one_of<PathSet, PathMultiset> auto &s, const path &root) { if (root.relative_path().empty()) return std::ranges::subrange(s.begin(), s.end()); @@ -62,7 +63,7 @@ subtree(const PathSet &s, const path &root) // Return a subtree in reverse order (suitable for unmounting). inline auto -subtree_rev(const PathSet &s, const path &root) +subtree_rev(const is_one_of<PathSet, PathMultiset> auto &s, const path &root) { return subtree(s, root) | std::views::reverse; } @@ -70,7 +71,7 @@ subtree_rev(const PathSet &s, const path &root) std::string fdpath(int fd, const path &file, bool must = false); std::string fdpath(int fd, bool must = false); -PathSet mountpoints(const path &mountinfo = "/proc/self/mountinfo"); +PathMultiset mountpoints(const path &mountinfo = "/proc/self/mountinfo"); // source (if non-NULL) is the source printed in /proc/self/mountinfo Fd xfsopen(const char *fsname, const char *source = nullptr); @@ -245,7 +246,17 @@ xfstat(int fd) return sb; } -std::string read_file(int dfd, path file = {}); +std::expected<std::string, std::system_error> try_read_file(int dfd, + path file = {}); + +inline std::string +read_file(int dfd, path file = {}) +{ + if (auto res = try_read_file(dfd, file)) + return std::move(*res); + else + throw res.error(); +} using ACL = RaiiHelper<acl_free, acl_t>;
@@ -41,14 +41,18 @@ struct Config { Fd run_jai_fd_; Fd run_jai_user_fd_; + PathSet config_loop_detect_; + void init_credentials(); Fd make_idmap_ns(); Fd make_mnt_ns(); void exec(int nsfd, char **argv); void unmount(); void unmountall(); - Options opt_parser(); + std::unique_ptr<Options> opt_parser(); void make_default_conf(); + + bool parse_config_file(path file, Options *opts = nullptr); void sanitize_env(); [[nodiscard]] static Defer asuser(const Credentials *crp); @@ -88,6 +92,29 @@ struct Config { } }; +bool +Config::parse_config_file(path file, Options *opts) +{ + auto ld = homepath_ / file; + if (auto [_it, ok] = config_loop_detect_.insert(ld); !ok) { + warn("{}: configuration loop", file.string()); + return false; + } + Defer _clear{[this, ld = std::move(ld)] { config_loop_detect_.erase(ld); }}; + + auto r = try_read_file(home_jai(), file); + if (!r) { + if (r.error().code() == std::errc::no_such_file_or_directory) + return false; + throw r.error(); + } + if (opts) + opts->parse_file(*r, fdpath(home_jai(), file)); + else + opt_parser()->parse_file(*r, fdpath(home_jai(), file)); + return true; +} + static std::expected<Fd, Defer> lock_or_validate_file(int dfd, const path &file, int flags, auto &&validate, path lockfile = {}) requires requires { @@ -139,15 +166,14 @@ Config::init_credentials() !strcmp(u->pw_dir, "/")) untrusted_cred_ = Credentials::get_user(u); else - std::println(stderr, - R"(Ignoring user {} because uid is 0, home dir is not "/" or + warn(R"(Ignoring user {} because uid is 0, home dir is not "/" or GECOS field is not "JAI sandbox untrusted user")", - kUnstrustedUser); + kUnstrustedUser); } else - std::println(stderr, R"(Could not find credentials for untrusted {} user. + warn(R"(Could not find credentials for untrusted {} user. Try running "sudo systemd-sysusers".)", - kUnstrustedUser); + kUnstrustedUser); // Paranoia about ptrace, because we will drop privileges to access // the file system as the user. @@ -704,7 +730,7 @@ Config::exec(int nsfd, char **argv) bashcmd.push_back("init"); bashcmd.push_back("-c"); bashcmd.push_back(shellcmd_.c_str()); - while(*argv) + while (*argv) bashcmd.push_back(*(argv++)); bashcmd.push_back(nullptr); argv = const_cast<char **>(bashcmd.data()); @@ -714,10 +740,11 @@ Config::exec(int nsfd, char **argv) _exit(1); } -Options +std::unique_ptr<Options> Config::opt_parser() { - Options opts; + auto ret = std::make_unique<Options>(); + Options &opts = *ret; opts( "-d", "--dir", [this](path d) { grant_directories_.emplace(canonical(d)); }, @@ -733,6 +760,10 @@ Config::opt_parser() sandbox_name_ = sb; }, "Use private or overlay home directory NAME", "NAME"); + opts("--conf", [this, opts = ret.get()](path file) { + if (!parse_config_file(file, opts)) + warn("{}: configuration file not found", file.string()); + }); opts( "--strict", [this] { mode_ = kStrict; }, std::format("Enable strict mode (run with uid {} and empty home)", @@ -747,7 +778,7 @@ Config::opt_parser() opts( "--command", [this](std::string cmd) { shellcmd_ = std::move(cmd); }, R"(Bash command line to execute program (default: "$0" "$@"))", "CMD"); - return opts; + return ret; } std::string option_help; @@ -782,17 +813,24 @@ do_main(int argc, char **argv) bool opt_u{}; std::vector<path> opt_d; + path opt_C = ""; - Options opts = conf.opt_parser(); + auto opts = conf.opt_parser(); // A few options not available in config files - opts("-u", [&] { opt_u = true; }, "Unmount sandboxed file systems"); - opts("--help", [] { usage(1); }); - opts("--version", version, "Print copyright and version then exit"); - option_help = opts.help(); + (*opts)("-u", [&] { opt_u = true; }, "Unmount sandboxed file systems"); + // Override inline conf to make CLI idempotent + (*opts)( + "-C", "--conf", [&](path p) { opt_C = p; }, + R"(Use FILE as configuration file (relative to ~/.jai). +Default: CMD.conf or default.conf if CMD.conf does not exist)", + "FILE"); + (*opts)("--help", [] { usage(1); }); + (*opts)("--version", version, "Print copyright and version then exit"); + option_help = opts->help(); std::vector<char *> cmd; try { - cmd.assign_range(opts.parse_argv(argc, argv)); + cmd.assign_range(opts->parse_argv(argc, argv)); } catch (Options::Error &e) { std::println("{}", e.what()); usage(2); @@ -808,28 +846,12 @@ do_main(int argc, char **argv) return; } - auto tryparse = [&opts, &conf, argc, argv](path p) { - if (!conf.name_ok(p)) - return false; - p += ".conf"; - if (Fd cf = openat(conf.home_jai(), p.c_str(), O_RDONLY)) { - try { - conf.opt_parser().parse_file(read_file(*cf)); - // Re-parse argv so it takes precedence - opts.parse_argv(argc, argv); - return true; - } catch (const Options::Error &e) { - err<Options::Error>("{}:{}", fdpath(conf.home_jai(), p), e.what()); - } - } - if (errno != ENOENT) - syserr("{}", fdpath(conf.home_jai(), p)); - return false; - }; - - if (!(!cmd.empty() && tryparse(cmd[0])) && !tryparse("default")) { + if (opt_C.empty() && !cmd.empty() && conf.name_ok(cmd[0])) + opt_C = std::format("{}.conf", cmd[0]); + if ((opt_C.empty() || !conf.parse_config_file(opt_C)) && + !conf.parse_config_file("default.conf")) { conf.make_default_conf(); - tryparse("default"); + conf.parse_config_file("default.conf"); } restore.reset();
@@ -308,7 +308,7 @@ public: return parse_argspan(std::span{argv + 1, argv + argc}); } - void parse_file(std::string_view text) + void parse_file(std::string_view text, std::string_view errpath = {}) { static constexpr std::string_view ws = " \t\r"; static constexpr std::string_view wsnl = " \t\r\n"; @@ -371,8 +371,10 @@ public: optarg.resize(optarg.size() - 1); parse_argspan(std::span{&optarg, 1}); } catch (const Error &e) { + if (errpath.empty()) + throw; auto nnl = std::count(text.begin(), text.begin() + clamp(pos), '\n'); - err<Error>("{}: {}", nnl + 1, e.what()); + err<Error>("{}:{}: {}", errpath, nnl + 1, e.what()); } } }