Repositories / jai.git
jai.git
Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git
@@ -29,14 +29,14 @@ const std::string jai_defaults = # storage /some/local/directory/${JAI_USER}/.jai -# The default mode is strict for all named jails and casual for the -# default jail. A strict jail runs under the dedicated jai UID and -# starts with an empty home directory. A casual jail runs with your -# own UID and makes your home directory copy-on-write via an overlay -# mount. Strict mode cannot grant unrestricted access to directories -# on NFS file systems. You will have to use bare mode (which gives -# you a bare home directory, but still runs with your UID) to expose -# NFS directories. Uncomment any of the following to set the mode: +# The default mode is strict. A strict jail runs under the dedicated +# jai UID and starts with an empty home directory. A casual jail runs +# with your own UID and makes your home directory copy-on-write via an +# overlay mount. Strict mode cannot grant unrestricted access to +# directories on NFS file systems. You will have to use bare mode +# (which gives you a bare home directory, but still runs with your +# UID) to expose NFS directories. Uncomment any of the following to +# set the mode, or override it in individual .jail files: # mode casual # mode bare @@ -50,7 +50,7 @@ const std::string jai_defaults = # casual, but if you define this to anything including "default", then # the default mode will be strict. -# name default +# jail default # jai launches jailed programs by running bash with the command name # in "$0" and the arguments in "@". Altering command allows you set @@ -148,3 +148,10 @@ extern const std::string default_conf = conf .defaults )"; + +extern const std::string default_jail = + R"(# Set casual mode for the default jail. + +mode casual + +)";
@@ -398,23 +398,15 @@ set_fd_acl(int fd, const char *acltext, AclType which) syserr(R"(acl_set_file("{}", DEFAULT, {}))", fdpath(fd), acltext); } -std::expected<std::string, std::system_error> -try_read_file(int dfd, path file) +std::string +read_fd(int fd) { - Fd fdholder; - 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) + if (auto sb = xfstat(fd); sb.st_size > 0x100'0000) { // Let's not go crazy with sparse files and such - err("{}: file too large", fdpath(fd)); + errno = EFBIG; + syserr("{}", fdpath(fd)); + } else if (sb.st_size > 0) ret.reserve(sb.st_size); for (;;) { @@ -423,43 +415,62 @@ try_read_file(int dfd, path file) if (n == 0) return ret; if (n < 0) + syserr("{}: read", fdpath(fd)); + ret.append(buf, size_t(n)); + } +} + +std::expected<std::string, std::system_error> +try_read_file(int dfd, path file) +{ + Fd fdholder; + 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))); - ret.append(buf, size_t(n)); + fd = *fdholder; } + return read_fd(fd); } Fd -ensure_file(int dfd, path file, std::string_view contents, int mode) +ensure_file(int dfd, path file, std::string_view contents, int mode, + bool *created) { assert(!file.empty()); - for (;;) { - if (Fd fd = openat(dfd, file.c_str(), O_RDONLY | O_CLOEXEC)) { - if (!S_ISREG(xfstat(*fd).st_mode)) - err("{}: not a regular file", fdpath(dfd, file)); - return fd; - } - if (errno != ENOENT) - syserr("{}", fdpath(dfd, file)); - - path tmp = cat(file, std::format("~{}~", getpid())); - unlinkat(dfd, tmp.c_str(), 0); - Defer cleanup{[dfd, &tmp] { unlinkat(dfd, tmp.c_str(), 0); }}; - - Fd fd = xopenat(dfd, tmp.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, - mode); - for (size_t i = 0; i < contents.size();) { - if (auto n = write(*fd, contents.data() + i, contents.size() - i); n < 0) - syserr(R"(write(O_TMPFILE for "{}"))", fdpath(dfd, file)); - else - i += n; - } - if (fsync(*fd)) - syserr("fsync(\"{}\")", fdpath(*fd)); - if (renameat(dfd, tmp.c_str(), dfd, file.c_str())) - syserr(R"(rename("{}" -> "{}") in "{}")", tmp.string(), file.string(), - fdpath(*fd)); - cleanup.release(); + + if (Fd fd = openat(dfd, file.c_str(), O_RDONLY | O_CLOEXEC)) { + if (!S_ISREG(xfstat(*fd).st_mode)) + err("{}: not a regular file", fdpath(dfd, file)); + if (created) + *created = false; return fd; } + if (errno != ENOENT) + syserr("{}", fdpath(dfd, file)); + + path tmp = cat(file, std::format("~{}~", getpid())); + unlinkat(dfd, tmp.c_str(), 0); + Defer cleanup{[dfd, &tmp] { unlinkat(dfd, tmp.c_str(), 0); }}; + + Fd fd = + xopenat(dfd, tmp.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, mode); + for (size_t i = 0; i < contents.size();) { + if (auto n = write(*fd, contents.data() + i, contents.size() - i); n < 0) + syserr(R"(write(O_TMPFILE for "{}"))", fdpath(dfd, file)); + else + i += n; + } + if (fsync(*fd)) + syserr("fsync(\"{}\")", fdpath(*fd)); + if (renameat(dfd, tmp.c_str(), dfd, file.c_str())) + syserr(R"(rename("{}" -> "{}") in "{}")", tmp.string(), file.string(), + fdpath(*fd)); + cleanup.release(); + if (created) + *created = true; + fd.reset(); // have to reopen for reading + return xopenat(dfd, file.c_str(), O_RDONLY | O_CLOEXEC); }
@@ -265,6 +265,12 @@ xfstat(int fd, path file = {}, FollowLinks follow = kFollow) return sb; } +std::string read_fd(int fd); + +// This tries to read a file. It will return an error if the file +// cannot be opened (e.g., because it does not exist), but could still +// throw if reading the actual file returns an error or allocating the +// buffer exhausts memory. std::expected<std::string, std::system_error> try_read_file(int dfd, path file = {}); @@ -277,7 +283,8 @@ read_file(int dfd, path file = {}) throw res.error(); } -Fd ensure_file(int dfd, path file, std::string_view contents, int mode = 0600); +Fd ensure_file(int dfd, path file, std::string_view contents, int mode = 0600, + bool *created = nullptr); using ACL = RaiiHelper<acl_free, acl_t>;
@@ -89,21 +89,31 @@ to expose whatever directory contains the changed files (e.g., jai allows the use of multiple home directories for different jails. To use a home directory other than the default, just give it a name -with the `-n` option and it will be created on demand. When you -specify a home directory with `-n`, strict mode becomes the default -(unless there is no unprivileged `jai` user on your system, in which -case jai falls back to bare mode). It is possible to have multiple -home overlays by specifying `-mcasual` with `-n`. +with the `-j` option and it will be created on demand. If you don't +specify `-mcasual` or `-mbare`, strict mode will become the default +for a newly created jail, but you can change this by editing the +corresponding `.jail` file in `$HOME/.jai` or wherever your storage +directory is. # CONFIGURATION -If *cmd* does not contain any slashes, configuration is taken from -`$HOME/.jai/`*cmd*`.conf`, or, if no such file exists, from -`$HOME/.jai/default.conf`. +Configuration comes from three sources: the command line, a `.conf` +configuration file (which may include other files via `conf` options), +and a `.jail` file specific to the named jail you are choosing. +Command-line options override everything, and `.jail` files override +`.conf` files. -The format of configuration files is a series of lines of the form -"*option* [*value*]". *option* can be any long command-line option -without the leading `--`, for example: +If you don't specify a `.conf` file on the command line with the `-C` +option, and if *cmd* does not contain any slashes, jai will first try +to use `$HOME/.jai/`*cmd*`.conf` if that file exists, and otherwise +will use `$HOME/.jai/default.conf` (which it will create if needed +exist). That way the `.conf` file can specify a jail name, and the +`.jail` file can set the mode of the jail. + +The format of `.conf` and `.jail` configuration files is a series of +lines of the form "*option* [*value*]" or "*option*`=`*value*. +*option* can be any long command-line option without the leading `--`, +for example: conf .defaults mode casual @@ -111,14 +121,15 @@ without the leading `--`, for example: mask Mail If you want to set an option that requires an argument to the empty -string, use an `=` sign, as in `storage=`. +string, use an `=` sign, as in `storage=` to reset the default storage +location. Within a configuration file, `conf` acts like an include directive, logically replacing the `conf` line with the contents of another -configuration file. (Relative paths are relative to `$HOME/.jai/`.) -jai creates a file `.defaults` with a sensible set of defaults you -should probably include directly or indirectly in any configuration -file. +configuration file. Relative paths are relative to `$HOME/.jai/` (or +`$JAI_CONFIG_DIR` if set). jai creates a file `.defaults` with a +sensible set of defaults you should probably include directly or +indirectly in any `.conf` configuration file you write. jai executes jailed programs with bash. The `command` directive allows you to reconfigure the environment or add command-line options @@ -129,7 +140,7 @@ following: conf default.conf mode strict dir venv - name python + jail python command source $HOME/venv/bin/activate; "$0" "$@" If you run `jai python`, this configuration file will load a virtual @@ -140,22 +151,18 @@ environment before running the command. To install claude code in a jail called `claude`: curl -fsSL https://claude.ai/install.sh | \ - jai -D -mstrict -n claude bash + jai -D -mstrict -j claude bash -To invoke claude code in that same jail, if $HOME/.local/bin is not +To invoke claude code in that same jail, if `$HOME/.local/bin` is not already on your path: - PATH=$HOME/.local/bin:$PATH jai -n claude claude + PATH=$HOME/.local/bin:$PATH jai -j claude claude To make `jai claude` use the claude jail by default: cat <<<EOF >$HOME/.jai/claude.conf conf .defaults - name claude - - # Mode already defaults to strict; change to bare if using NFS - mode strict - + jail claude command PATH=$HOME/.local/bin:$PATH "$0" "$@" EOF @@ -167,11 +174,12 @@ directory and merging them into your claude jail. # Extract cookies outside jail, merge them inside jail xauth extract - $DISPLAY | jai -C claude xauth merge - - # Copy a screen region you should be able to paste in claude + + # Now copy a screen region to paste in claude import png:- | xclip -selection clipboard -t image/png A safer way to do this is to write your screengrabs directly into the -sandbox's /tmp directory as in: +sandbox's `/tmp` directory as in: import /run/jai/$USER/tmp/claude/scrn.png @@ -188,12 +196,8 @@ directory: To do this by default when invoking `jai codex` (similar for `jai opencode`): - cat <<EOF >$HOME/.jai + cat <<EOF >$HOME/.jai/codex.conf conf .defaults - mode casual - - # no need to specify name, will be "default" by default - # name default # list additional directories to expose dir .codex @@ -233,6 +237,9 @@ opencode`): to the current working directory, while in configuration files, they are relative to your home directory. +`-x` *dir*, `--xdir` *dir* +: Reverse the effects of a previous `--dir` *dir* option. + `-D`, `--nocwd` : By default, `jai` grants access to the current working directory even if it is not specified with `-d`. This option suppresses that @@ -254,7 +261,7 @@ opencode`): directory (`$HOME/.jai/`*name*`.home`), and jailed code runs with a different user id, `jai`. Id-mapped mounts are used to map `jai` to the invoking user in granted directories. Strict mode is the - default when you name a jail (see `--name`), but not for the default + default when you name a jail (see `--jail`), but not for the default jail. Bare mode uses an empty directory like strict mode, but runs with @@ -262,18 +269,24 @@ opencode`): can be used for NFS-mounted home directories since NFS does not support id-mapped mounts. -`-n` *name*, `--name` *name* +`-j` *name*, `--jail` *name* : jai allows you to have multiple jailed home directories, which may be useful when jailing multiple tools that should not have access to - each other's API keys. This option specifies which home directory - to use. If no such jail exists yet, it will be created on demand. - When not specified, the default is just `default`. Note that each - name can be associated with both a casual home directory (accessible - at `/run/jai/$USER/`*name*`.home`, with changes in - `$HOME/.jai/`*name*`.changes`) and a strict/bare home directory (in - `$HOME/.jai/`*name*`.home`). There is no special relation between - these home directories, but casual and strict jails by the same name - do share the same `/tmp` directory. + each other's API keys. This option specifies which jail to use. If + no such jail exists yet, it will be created on demand and the mode + specified (strict by default) will become the default for that jail, + though you can change it in the file `$HOME/.jai/`*name*`.jail`. + + Note that if you switch modes, the same *name* can have both a + casual home directory (accessible at `/run/jai/$USER/`*name*`.home`, + with changes going in `$HOME/.jai/`*name*`.changes`) and a + strict/bare home directory (in `$HOME/.jai/`*name*`.home`). There + is no special relation between these two home directories, but all + jails by the name *name* share the same `/tmp` directory. + + Note that you are not allowed to use the `jail` configuration option + in a `.jail` file or any configuration file included by the `.jail` + file. `--mask` *file* : When creating an overlay home directory, create a "whiteout" file to @@ -361,6 +374,8 @@ uses `\` to escape the next character. # ENVIRONMENT +The following environment variables affect jai's operation: + `SUDO_USER`, `USER` : If jai is invoked with real UID 0 and either of these environment variables exists, it will be taken as the user whose home directory @@ -374,13 +389,18 @@ uses `\` to escape the next character. wish to put your private home directories elsewhere in order to use casual mode. -`JAI_NAME` -: Set to the name of the jai instance (specified by `-n` or `--name`) - inside the jail. +Jai sets the following environment variables inside jails: `JAI_MODE` : Set to the mode (strict, bare, or casual) inside a jail. +`JAI_JAIL` +: Set to the name of the jai instance (specified by `-j` or `--jail`) + inside the jail. + +`JAI_USER` +: Set to the name of the user who invoked jai. + # FILES In the following paths, the location `$HOME/.jai` can be changed by @@ -398,12 +418,22 @@ setting the `JAI_CONFIG_DIR` environment variable. all configuration files with the line `conf .defaults` to get the defaults. +In the following paths, the location `$HOME/.jai` is changed by the +`--storage` option. In the absence of a `--storage` option, the +location can be changed by the `JAI_CONFIG_DIR` environment variable. + +`$HOME/.jai/`*name*`.jail` +: Configuration file for jail named *name*, read after and overriding + any settings in the `.conf` file. Usually only sets `mode` for the + sandbox, but could also conceivably include `dir` and other options + except `jail`, which is disallowed. + `$HOME/.jai/default.changes`, `$HOME/.jai/`*name*`.changes` : This "upper" directory is overlaid on your home directory and contains changes that have been made inside a casual jail. Before directly changing this directory, tear down and recreate the sandboxed home directory with `jai -u`. The non-default version is - used when you specify `-n` *name* on the command line. If you + used when you specify `-j` *name* on the command line. If you specified `--storage=`*dir*, the changes directory will be in *dir* instead of `$HOME/.jai`. @@ -422,6 +452,9 @@ setting the `JAI_CONFIG_DIR` environment variable. `--storage=`*dir*, the these directories will be under *dir* instead of `$HOME/.jai`. +The following paths are always fixed, regardless of environment +variables or command-line options: + `/run/jai/$USER/default.home`, `/run/jai/$USER/`*name*`.home` : Home directories for casual jails. You can delete files with sensitive data in these jail directories to hide theme from jailed
@@ -20,11 +20,26 @@ path prog; +void +Config::parse_config_fd(int fd, Options *opts) +{ + auto ld = fdpath(fd, true); + if (auto [_it, ok] = config_loop_detect_.insert(ld); !ok) + err<Options::Error>("configuration loop"); + Defer _clear([this, ld, pch = parsing_config_file_] { + config_loop_detect_.erase(ld); + parsing_config_file_ = pch; + }); + parsing_config_file_ = true; + auto go = [&](Options *o) { o->parse_file(read_file(fd), ld); }; + go(opts ? opts : opt_parser().get()); +} + bool Config::parse_config_file(path file, Options *opts) { bool slash = std::ranges::distance(file.begin(), file.end()) > 1; - bool fromcwd = slash && !dir_relative_to_home_; + bool fromcwd = slash && !parsing_config_file_; if (struct stat sb; !slash && file.extension() != ".conf" && @@ -33,26 +48,13 @@ Config::parse_config_file(path file, Options *opts) S_ISREG(sb.st_mode)) file += ".conf"; - auto ld = (fromcwd ? cwd() : homejaipath_) / file; - if (auto [_it, ok] = config_loop_detect_.insert(ld); !ok) - err<Options::Error>("configuration loop"); - Defer _clear{[this, ld = std::move(ld), drh = dir_relative_to_home_] { - config_loop_detect_.erase(ld); - if (!drh) - dir_relative_to_home_ = false; - }}; - dir_relative_to_home_ = true; - - auto r = try_read_file(fromcwd ? AT_FDCWD : home_jai(), file); - if (!r) { - if (r.error().code() == std::errc::no_such_file_or_directory) + Fd fd = openat(fromcwd ? AT_FDCWD : home_jai(), file.c_str(), O_RDONLY); + if (!fd) { + if (errno == ENOENT) return false; - throw r.error(); + syserr("{}", file.c_str()); } - if (opts) - opts->parse_file(*r, ld.string()); - else - opt_parser()->parse_file(*r, ld.string()); + parse_config_fd(*fd, opts); return true; } @@ -146,16 +148,15 @@ Config::asuser(const Credentials *crp) } void -Config::check_user(int fd, std::string p, bool untrusted_ok) +Config::check_user(const struct stat &sb, std::string p, bool untrusted_ok) { - if (auto sb = xfstat(fd); sb.st_uid != user_cred_.uid_) { + if (sb.st_uid != user_cred_.uid_) { if (!untrusted_ok) - err("{}: owned by {} should be owned by {}", p.empty() ? fdpath(fd) : p, - sb.st_uid, user_cred_.uid_); + err("{}: owned by {} should be owned by {}", p, sb.st_uid, + user_cred_.uid_); else if (sb.st_uid != untrusted_cred_.uid_) - err("{}: owned by {} should be owned by {} or {}", - p.empty() ? fdpath(fd) : p, sb.st_uid, user_cred_.uid_, - untrusted_cred_.uid_); + err("{}: owned by {} should be owned by {} or {}", p, sb.st_uid, + user_cred_.uid_, untrusted_cred_.uid_); } } @@ -419,10 +420,7 @@ Config::make_mnt_ns() if (mode_ == kStrict && !strict_ok) err("Cannot use strict mode: invalid user {}", kUntrustedUser); - if (mode_ == kInvalidMode) - mode_ = sandbox_name_.empty() ? kCasual : strict_ok ? kStrict : kBare; - if (sandbox_name_.empty()) - sandbox_name_ = "default"; + assert(!sandbox_name_.empty()); mount_attr attr{ .attr_set = MOUNT_ATTR_NOSUID | MOUNT_ATTR_NODEV, @@ -501,7 +499,7 @@ Config::make_mnt_ns() restore_root = asuser(); Fd dst = openat(-1, d.c_str(), O_DIRECTORY | O_PATH | O_CLOEXEC); if (!dst) { - if (mode_ == kCasual || (errno != EACCES && errno != ENOENT)) + if (mode_ != kStrict || (errno != EACCES && errno != ENOENT)) syserr("{}", d.string()); restore_root.reset(); restore_root = asuser(sbcred); @@ -513,14 +511,12 @@ Config::make_mnt_ns() } xsetns(*newns, CLONE_NEWNS); - struct stat sb; - if (!storagedir_.empty() && stat(storagedir_.c_str(), &sb) == 0 && - S_ISDIR(sb.st_mode)) { - // make sure storage directory not exposed - auto restore_root = asuser(); - Fd target = xopenat(AT_FDCWD, storagedir_.c_str(), O_DIRECTORY | O_RDONLY); - check_user(*target); + auto blockdir = [this, &oldns, &newns, &sbcred](const path &p) { + assert(p.is_absolute()); + auto restore_root = asuser(sbcred); + Fd target = xopenat(AT_FDCWD, p, O_DIRECTORY | O_RDONLY); + check_user(*target, p, true); restore_root.reset(); Fd empty = xopenat(-1, kRunRoot, O_RDONLY); if (!is_dir_empty(*empty)) @@ -532,7 +528,10 @@ Config::make_mnt_ns() .propagation = MS_PRIVATE, }); xmnt_move(*source, *target); - } + }; + blockdir(storagedir_); + if (homejaipath_ != storagedir_) + blockdir(homejaipath_); return newns; } @@ -928,12 +927,8 @@ try { argv = const_cast<char **>(bashcmd.data()); } - setenv("JAI_NAME", sandbox_name_.c_str(), 1); - setenv("JAI_MODE", - mode_ == kStrict ? "strict" - : mode_ == kBare ? "bare" - : "casual", - 1); + setenv("JAI_JAIL", sandbox_name_.c_str(), 1); + setenv("JAI_MODE", std::format("{}", mode_).c_str(), 1); auto env = make_env(); execvpe(argv0, argv, const_cast<char **>(env.data())); @@ -945,7 +940,7 @@ try { } std::unique_ptr<Options> -Config::opt_parser() +Config::opt_parser(bool dotjail) { auto ret = std::make_unique<Options>(); Options &opts = *ret; @@ -953,10 +948,7 @@ Config::opt_parser() "-m", "--mode", [this](std::string_view m) { static const std::map<std::string, Mode, std::less<>> modemap{ - {"default", kInvalidMode}, - {"casual", kCasual}, - {"bare", kBare}, - {"strict", kStrict}}; + {"casual", kCasual}, {"bare", kBare}, {"strict", kStrict}}; if (auto it = modemap.find(m); it != modemap.end()) mode_ = it->second; else @@ -972,20 +964,32 @@ Config::opt_parser() "-d", "--dir", [this](path d) { grant_directories_.emplace( - canonical(dir_relative_to_home_ ? homepath_ / d : d)); + canonical(parsing_config_file_ ? homepath_ / d : d)); }, "Grant full access to DIR.", "DIR"); opts( + "-x", "--xdir", + [this](path d) { + grant_directories_.erase( + canonical(parsing_config_file_ ? homepath_ / d : d)); + }, + "undo the effects of a previous --dir option", "DIR"); + opts( "-D", "--nocwd", [this] { grant_cwd_ = false; }, "Do not grant access to the current working directory"); - opts( - "-n", "--name", - [this](path sb) { - if (!name_ok(sb)) - err<Options::Error>("{}: invalid sandbox name", sb.string()); - sandbox_name_ = sb; - }, - "Use private or overlay home directory named NAME", "NAME"); + if (!dotjail) + opts( + "-j", "--jail", + [this](path sb) { + if (!name_ok(sb)) + err<Options::Error>("{}: invalid sandbox name", sb.string()); + sandbox_name_ = sb; + }, + "Use private or overlay home directory named NAME", "NAME"); + else + opts("-j", "--jail", [](path) { + err<Options::Error>("cannot set name from a .jail file or include"); + }); opts("--conf", [this, opts = ret.get()](path file) { if (!parse_config_file(file, opts)) err<Options::Error>("{}: configuration file not found", file.string()); @@ -1038,7 +1042,7 @@ Config::opt_parser() "--storage", [this](std::string_view s) { auto sd = var_expand(s); - if (dir_relative_to_home_) + if (parsing_config_file_) storagedir_ = homepath_ / sd; else storagedir_ = sd; @@ -1126,6 +1130,7 @@ The default is CMD.conf if it exists, otherwise default.conf)", ensure_file(conf.home_jai(opt_init), ".defaults", jai_defaults, 0600); ensure_file(conf.home_jai(), "default.conf", default_conf, 0600); + ensure_file(conf.storage(), "default.jail", default_jail, 0600); if (opt_init) { std::println("You can edit the configuration defaults in {}/.defaults", @@ -1151,6 +1156,20 @@ The default is CMD.conf if it exists, otherwise default.conf)", !conf.parse_config_file(std::format("{}.conf", cmd[0]))) && !conf.parse_config_file("default.conf")) conf.parse_config_file("default.conf"); + + // Re-parse command line to override files + opts->parse_argv(argc, argv); + + bool createwarn = false; + if (conf.sandbox_name_.empty()) + conf.sandbox_name_ = "default"; + Fd dotjail = + ensure_file(conf.storage(), cat(conf.sandbox_name_, ".jail"), + std::format("mode {}\n", conf.mode_), 0600, &createwarn); + if (createwarn) + warn("created {}", fdpath(*dotjail)); + conf.parse_config_fd(*dotjail, conf.opt_parser(true).get()); + // Re-parse command line to override files opts->parse_argv(argc, argv);
@@ -85,11 +85,12 @@ constexpr const char *kRunRoot = "/run/jai"; extern const std::string jai_defaults; extern const std::string default_conf; +extern const std::string default_jail; struct Config { - enum Mode { kInvalidMode, kCasual, kBare, kStrict }; + enum Mode { kCasual, kBare, kStrict }; - Mode mode_{kInvalidMode}; + Mode mode_{kStrict}; PathSet grant_directories_; bool grant_cwd_{true}; std::set<std::string, std::less<>> env_filter_; @@ -98,7 +99,7 @@ struct Config { std::string shellcmd_; PathSet mask_files_; bool mask_warn_{}; - bool dir_relative_to_home_{}; + bool parsing_config_file_{}; std::string user_; path homepath_; @@ -127,8 +128,9 @@ struct Config { void exec(int nsfd, char **argv); void unmount(); void unmountall(); - std::unique_ptr<Options> opt_parser(); + std::unique_ptr<Options> opt_parser(bool dotjail = false); + void parse_config_fd(int fd, Options *opts = nullptr); bool parse_config_file(path file, Options *opts = nullptr); std::vector<const char *> make_env(); @@ -139,8 +141,14 @@ struct Config { [[nodiscard]] static Defer asuser(const Credentials *crp); [[nodiscard]] Defer asuser() { return asuser(&user_cred_); } - void check_user(int fd, std::string path_for_error = {}, + void check_user(const struct stat &sb, std::string path_for_error = {}, bool untrusted_ok = false); + void check_user(int fd, std::string path_for_error = {}, + bool untrusted_ok = false) + { + check_user(xfstat(fd), path_for_error.empty() ? fdpath(fd) : path_for_error, + untrusted_ok); + } Fd ensure_udir(int dfd, const path &p, mode_t perm = 0700, FollowLinks follow = kFollow) { @@ -185,3 +193,21 @@ struct Config { } } }; + +template<> struct std::formatter<Config::Mode> : std::formatter<const char *> { + using super = std::formatter<const char *>; + auto format(Config::Mode m, auto &&ctx) const + { + using enum Config::Mode; + switch (m) { + case kStrict: + return super::format("strict", ctx); + case kBare: + return super::format("bare", ctx); + case kCasual: + return super::format("casual", ctx); + default: + err<std::logic_error>("Config::Mode with bad value {}", int(m)); + } + } +};