Repositories / jai.git

jai.git

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

Branch

add --conf

Author
David Mazieres <dm@uun.org>
Date
2026-03-15 12:02:31 -0700
Commit
5f8b0a2a2ef84472f24005c26d25b38a7a670af3
fs.cc
index 2923ae3..6aa7aea 100644
--- a/fs.cc
+++ b/fs.cc
@@ -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));
   }
 }
fs.h
index b0a1c4d..817dff4 100644
--- a/fs.h
+++ b/fs.h
@@ -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>;
 
jai.cc
index 1fb59d7..25982a4 100644
--- a/jai.cc
+++ b/jai.cc
@@ -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();
options.h
index 86b96da..c963014 100644
--- a/options.h
+++ b/options.h
@@ -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());
       }
     }
   }