Repositories / jai.git

jai.git

Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git

Branch

don't expand environment variables in command-line options

Author
David Mazieres <dm@uun.org>
Date
2026-03-27 12:52:12 -0700
Commit
e66cedc4ef9b5fa8b3b2b52061660a9813ec20f5
jai.1.md
index fdb1941..ab4c111 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -165,11 +165,11 @@ environment before running the command.  For more complicated setup
 logic, you can use `setenv` to set the `BASH_ENV` environment variable
 to an initialization script to be sourced in non-interactive session.
 
-The `--dir`, `--xdir`, `--mask`, `--unmask`, `--setenv`, and
-`--storage` options will perform environment variable substitution for
-variable names contained within `${`...`}`, but the braces are
-required, unlike in the shell.  You can quote a literal `$` by
-preceding it with a backslash `\`.
+The `dir`, `xdir`, `mask`, `unmask`, `setenv`, and `storage` options,
+and `conf` options in configuration files will perform environment
+variable substitution for variable names contained within `${`...`}`.
+Note the braces are required, unlike in the shell.  You can quote a
+literal `$` or `\` by preceding it with a backslash `\`.
 
 # EXAMPLES
 
@@ -385,6 +385,9 @@ opencode`):
   otherwise `$HOME/.jai`.  However, if your home directory is on NFS
   you may wish to use storage on a local file system, as NFS does not
   support the extended attributes required by overlay file systems.
+  You can of course install a symbolic link for each individual home
+  directory, but `--storage` allows you to relocate the base directory
+  where all jails are located.
 
 `--command` *bash-command*
 : jai launches the jailed program you specify by running "`/bin/bash
@@ -487,8 +490,9 @@ location can be changed by the `JAI_CONFIG_DIR` environment variable.
   directly changing this directory, tear down and recreate the
   sandboxed home directory with `jai -u`.  The non-default version is
   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`.
+  specified `--storage=`*dir*, the changes directory will looked up
+  under *dir* instead of `$HOME/.jai` (though in either case may be a
+  symbolic link elsewhere).
 
 `$HOME/.jai/default.work`, `$HOME/.jai/`*name*`.work`
 : This "work" directory is required by overlayfs, but does not contain
jai.cc
index 22ddb75..e29842b 100644
--- a/jai.cc
+++ b/jai.cc
@@ -26,9 +26,9 @@ 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_] {
+  Defer _clear([this, ld, pcf = parsing_config_file_] {
     config_loop_detect_.erase(ld);
-    parsing_config_file_ = pch;
+    parsing_config_file_ = pcf;
   });
   parsing_config_file_ = true;
   auto go = [&](Options *o) { o->parse_file(read_file(fd), ld); };
@@ -1036,7 +1036,8 @@ Config::opt_parser(bool dotjail)
     opts("-j", "--jail", [](path) {
       err<Options::Error>("cannot set name from a .jail file or include");
     });
-  opts("--conf", [this, opts = ret.get()](path file) {
+  opts("--conf", [this, opts = ret.get()](std::string_view arg) {
+    path file(expand(arg));
     if (!parse_config_file(file, opts))
       err<Options::Error>("{}: configuration file not found", file.string());
   });
jai.h
index 83a63cd..9cda1cd 100644
--- a/jai.h
+++ b/jai.h
@@ -16,18 +16,15 @@
 
 extern "C" char **environ;
 
-inline const char *
-env_or_empty(std::string_view var)
-{
+inline const char *env_or_empty(std::string_view var) {
   const char *p = getenv(std::string(var).c_str());
   return p ? p : "";
 }
 
 // Calls exp("VAR"sv) to expand strings like "123${VAR}456".
-template<typename Exp = decltype(env_or_empty)>
-std::string
-var_expand(std::string_view in, Exp &&exp = env_or_empty)
-    requires requires(std::string r) { r += exp(in); }
+template <typename Exp = decltype(env_or_empty)>
+std::string var_expand(std::string_view in, Exp &&exp = env_or_empty)
+  requires requires(std::string r) { r += exp(in); }
 {
   std::string ret;
   for (std::size_t i = 0, e = in.size(); i < e;)
@@ -37,24 +34,19 @@ var_expand(std::string_view in, Exp &&exp = env_or_empty)
              in.substr(i, 2) == "${" && (j = in.find('}', i + 2)) != in.npos) {
       ret += exp(in.substr(i + 2, j - i - 2));
       i = j + 1;
-    }
-    else
+    } else
       ret += in[i++];
   return ret;
 }
 
-inline pid_t
-xfork(std::uint64_t flags = 0)
-{
+inline pid_t xfork(std::uint64_t flags = 0) {
   clone_args ca{.flags = flags, .exit_signal = SIGCHLD};
   if (auto ret = syscall(SYS_clone3, &ca, sizeof(ca)); ret != -1)
     return ret;
   syserr("clone3");
 }
 
-inline sigset_t
-sigsingleton(int sig)
-{
+inline sigset_t sigsingleton(int sig) {
   sigset_t ret;
   sigemptyset(&ret);
   sigaddset(&ret, sig);
@@ -135,14 +127,12 @@ struct Config {
   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)
-  {
+                  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)
-  {
+                 FollowLinks follow = kFollow) {
     auto _restore = asuser();
     Fd fd = ensure_dir(dfd, p, perm, follow);
     check_user(*fd);
@@ -154,8 +144,7 @@ struct Config {
   int storage();
   int run_jai();
   int run_jai_user();
-  const path &cwd()
-  {
+  const path &cwd() {
     if (cwd_.empty()) {
       auto restore = asuser();
       cwd_ = canonical(std::filesystem::current_path());
@@ -169,25 +158,24 @@ struct Config {
   Fd make_private_run();
   Fd make_private_passwd();
 
-  const char *env_lookup(std::string_view var)
-  {
+  const char *env_lookup(std::string_view var) {
     if (auto it = setenv_.find(var); it != setenv_.end())
       if (auto pos = it->second.find('='); pos != it->second.npos)
         return it->second.c_str() + pos + 1;
     return env_or_empty(var);
   }
-  std::string expand(std::string_view in)
-  {
-    return var_expand(in, [this](std::string_view v) { return env_lookup(v); });
+  std::string expand(std::string_view in) {
+    return parsing_config_file_
+               ? var_expand(
+                     in, [this](std::string_view v) { return env_lookup(v); })
+               : std::string(in);
   }
 
-  static bool name_ok(path p)
-  {
+  static bool name_ok(path p) {
     return p.is_relative() && std::ranges::distance(p.begin(), p.end()) == 1 &&
            *p.c_str() != '.';
   }
-  void mask_warn()
-  {
+  void mask_warn() {
     if (mask_warn_) {
       warn(R"(--mask ignored because {5}/{0}/{1}.home already mounted.
 {2:>{3}}  Run "{4} -u" to unmount overlays.)",
@@ -198,10 +186,9 @@ struct Config {
   }
 };
 
-template<> struct std::formatter<Config::Mode> : std::formatter<const char *> {
+template <> struct std::formatter<Config::Mode> : std::formatter<const char *> {
   using super = std::formatter<const char *>;
-  auto format(Config::Mode m, auto &&ctx) const
-  {
+  auto format(Config::Mode m, auto &&ctx) const {
     using enum Config::Mode;
     switch (m) {
     case kStrict:
tests/basic.sh
index 41a61b2..1242783 100755
--- a/tests/basic.sh
+++ b/tests/basic.sh
@@ -11,8 +11,8 @@ assert_path_exists "$CONFIG_DIR/default.jail"
 
 capture run_jai --version
 assert_status 0
-assert_contains "$CAPTURE_STDOUT" "jai 0.1"
-assert_contains "$CAPTURE_STDOUT" "https://github.com/stanford-scs/jai"
+assert_contains "$CAPTURE_STDOUT" "Untrusted user for strict mode:"
+assert_contains "$CAPTURE_STDOUT" "This program comes with NO WARRANTY"
 
 capture run_jai --print-defaults
 assert_status 0
tests/config-precedence.sh
index 73ec90b..cc4f6a6 100755
--- a/tests/config-precedence.sh
+++ b/tests/config-precedence.sh
@@ -43,3 +43,17 @@ EOF
 capture_in_dir "$WORKDIR" run_jai_with_env SRC_VALUE=expanded -C expand
 assert_status 0
 assert_output_line "EXPANDED=expanded"
+
+cat >"$CONFIG_DIR/expand-include.conf" <<'EOF'
+conf .defaults
+conf ${INCLUDE_FILE}
+command /usr/bin/env
+EOF
+
+capture_in_dir "$WORKDIR" run_jai_with_env INCLUDE_FILE=shared.conf -C expand-include
+assert_status 0
+assert_output_line "FROM_SHARED=shared"
+
+capture_in_dir "$WORKDIR" run_jai_with_env INCLUDE_FILE=expand-include.conf -C '${INCLUDE_FILE}'
+assert_status 1
+assert_contains "$CAPTURE_STDERR" '${INCLUDE_FILE}: no such configuration file'