Repositories / jai.git

jai.git

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

Branch

add mask option

Author
David Mazieres <dm@uun.org>
Date
2026-03-15 18:03:18 -0700
Commit
484bdcf06a381c40a75244a0124972a2825d03cf
fs.cc
index 6aa7aea..01bd2bf 100644
--- a/fs.cc
+++ b/fs.cc
@@ -226,6 +226,33 @@ ensure_dir(int dfd, const path &p, mode_t perm, FollowLinks follow,
   return fd;
 }
 
+void
+make_whiteout(int dfd, const path &inp)
+{
+  auto p = inp.lexically_normal();
+  if (!p.has_filename())
+    p = p.parent_path();
+  if (p.is_absolute() || p.empty() || *p.begin() == "..")
+    err<std::logic_error>(R"(make_whiteout: "{}" is not a relative path)",
+                          inp.string());
+
+  Fd dirholder;
+  if (p.has_parent_path()) {
+    dirholder = ensure_dir(dfd, p.parent_path(), 0700, kNoFollow, false);
+    dfd = *dirholder;
+    p = p.filename();
+  }
+
+  auto olduid = geteuid();
+  seteuid(0);
+  int err = 0;
+  if (mknodat(dfd, p.filename().c_str(), S_IFCHR, 0))
+    err = errno;
+  seteuid(olduid);
+  if ((errno = err))
+    syserr("mknod {}/.jai c 0 0", fdpath(dfd, p));
+}
+
 bool
 is_mountpoint(int dfd, const path &file, FollowLinks follow)
 {
fs.h
index 817dff4..7c0b72d 100644
--- a/fs.h
+++ b/fs.h
@@ -133,6 +133,8 @@ bool is_dir_empty(int dirfd);
 Fd ensure_dir(int dfd, const path &p, mode_t perm, FollowLinks follow,
               bool okay_if_other_owner = false);
 
+void make_whiteout(int dfd, const path &p);
+
 bool is_mountpoint(int dfd, const path &file = {},
                    FollowLinks follow = kNoFollow);
 
jai.cc
index 270c3b2..81df0dc 100644
--- a/jai.cc
+++ b/jai.cc
@@ -28,6 +28,8 @@ struct Config {
   std::set<std::string> env_filter_;
   path cwd_;
   std::string shellcmd_;
+  PathSet mask_files_;
+  bool mask_warn_{false};
 
   std::string user_;
   path homepath_;
@@ -90,6 +92,14 @@ struct Config {
     return p.is_relative() && std::ranges::distance(p.begin(), p.end()) == 1 &&
            *p.c_str() != '.';
   }
+  void mask_warn()
+  {
+    if (mask_warn_) {
+      warn("--mask did nothing because ~/.jai/{}.changes already existed",
+           sandbox_name_.string());
+      mask_warn_ = false;
+    }
+  }
 };
 
 bool
@@ -278,44 +288,19 @@ Config::home()
   return *home_fd_;
 }
 
-const auto default_blacklist = std::to_array<const char *>({
-    ".jai",
-    ".ssh",
-    ".gnupg",
-    ".local/share/keyrings",
-    ".netrc",
-    ".git-credentials",
-    ".aws",
-    ".azure",
-    ".config/gcloud",
-    ".config/gh",
-    ".config/Keybase",
-    ".config/kube",
-    ".docker",
-    ".password-store",
-    ".mozilla",
-    ".config/chromium",
-    ".config/google-chrome",
-    ".config/BraveSoftware",
-    ".bash_history",
-    ".zsh_history",
-});
-
 Fd
 Config::make_blacklist(int dfd, path name)
 {
   Fd blacklistfd = ensure_dir(dfd, name.c_str(), 0700, kFollow);
   check_user(*blacklistfd);
-  if (!is_dir_empty(*blacklistfd))
+  if (!is_dir_empty(*blacklistfd)) {
+    mask_warn();
     return blacklistfd;
+  }
 
-  for (path p : default_blacklist) {
+  for (path p : mask_files_) {
     try {
-      auto subdir = p.relative_path().parent_path();
-      xopenat(subdir.empty()
-                  ? *blacklistfd
-                  : *ensure_dir(*blacklistfd, subdir, 0700, kNoFollow),
-              p.filename(), O_CREAT | O_WRONLY | O_CLOEXEC, 0600);
+      make_whiteout(*blacklistfd, p);
     } catch (const std::exception &e) {
       warn("{}", e.what());
     }
@@ -331,8 +316,10 @@ Config::make_home_overlay()
   auto r = lock_or_validate_file(
       run_jai_user(), sb, O_RDONLY | O_DIRECTORY,
       [](int fd) { return is_mountpoint(fd); }, ".lock");
-  if (r)
+  if (r) {
+    mask_warn();
     return std::move(*r);
+  }
 
   Fd sandboxed_home = ensure_dir(run_jai_user(), sb, 0755, kFollow, true);
   if (is_mountpoint(*sandboxed_home))
@@ -630,17 +617,60 @@ auto env_blacklist = std::to_array<const char *>({
     "*_TOKEN",
 });
 
+const auto default_masklist = std::to_array<const char *>({
+    ".jai",
+    ".ssh",
+    ".gnupg",
+    ".local/share/keyrings",
+    ".netrc",
+    ".git-credentials",
+    ".aws",
+    ".azure",
+    ".config/gcloud",
+    ".config/gh",
+    ".config/Keybase",
+    ".config/kube",
+    ".docker",
+    ".password-store",
+    ".mozilla",
+    ".config/chromium",
+    ".config/google-chrome",
+    ".config/BraveSoftware",
+    ".bash_history",
+    ".zsh_history",
+});
+
 void
 Config::make_default_conf()
 {
   auto fd = xopenat(home_jai(), ".", O_RDWR | O_TMPFILE | O_CLOEXEC, 0600);
-  std::string text;
+  std::string text =
+      R"(# The executed process will have PID 1, which can confuse some
+# programs.  Adding "; exit $?" after the command and arguments will
+# cause bash to fork and stay around, so command "$0" has PID 2.
+
+command "$0" "$@"; exit $?
+
+# Masked file wills be deleted when a new overlayfs is first created,
+# but have no effect on existing overlays.  To delete files from an
+# existing overlay, delete them under /run/jai/$USER/default.home.  If
+# you want to start over with a fresh overlay, you can run "jai -u" to
+# unmount any existing overlays, then remove the directory
+# $HOME/.jai/$USER/default.changes.  The next time you run jai, it
+# will create a new overlay masking all of the files below.
+
+)";
+  for (auto p : default_masklist)
+    text += std::format("mask {}\n", p);
+
+  text += R"(
+# The following environment variables will be removed from sandboxed
+# environments.  You can use * as a wildcard to match any variables
+# matching the pattern.
+
+)";
   for (auto e : env_blacklist)
     text += std::format("unsetenv {}\n", e);
-  // The executed process will have PID 1, which can confuse some programs,
-  // so add "; exit $?" to force bash to stay around and be PID 1.
-  text += R"(command "$0" "$@"; exit $?)"
-          "\n";
 
   errno = EAGAIN;
   if (write(*fd, text.data(), text.size()) != text.size())
@@ -760,6 +790,14 @@ Config::opt_parser()
         sandbox_name_ = sb;
       },
       "Use private or overlay home directory NAME", "NAME");
+  opts(
+      "--mask",
+      [this](path p) {
+        if (p.is_absolute())
+          err<Options::Error>("{}: cannot mask an absolute path", p.string());
+        mask_files_.emplace(std::move(p));
+      },
+      "Erase $HOME/FILE when first creating overlay home", "FILE");
   opts("--conf", [this, opts = ret.get()](path file) {
     if (!parse_config_file(file, opts))
       err<Options::Error>("{}: configuration file not found", file.string());
@@ -836,6 +874,8 @@ default: CMD.conf or default.conf if CMD.conf does not exist)",
     std::println("{}", e.what());
     usage(2);
   }
+  if (!conf.mask_files_.empty())
+    conf.mask_warn_ = true;
 
   if (opt_u) {
     if (!conf.grant_cwd_ || !conf.grant_directories_.empty() || !cmd.empty()) {