Repositories / jai.git

jai.git

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

Branch

create .defaults file

Author
David Mazieres <dm@uun.org>
Date
2026-03-18 21:02:29 -0700
Commit
7efbc5677e9d3606a49c200c1f5f764d7ddbbe7d
default_conf.cc
index beef2cc..31b62fb 100644
--- a/default_conf.cc
+++ b/default_conf.cc
@@ -1,45 +1,69 @@
 
 #include "jai.h"
 
-const std::string default_conf =
-    R"(# Note: instead of copying this file to create a custom configuration
-# for certain commands, you can include this file by referernce with a
-# line "conf default.conf" in your new configuration file.
-
-# The default mode is strict for all named sandboxes and casual for
-# the default sandbox.  A strict sandbox runs under the dedicated jai
-# UID and starts with an empty home directory.  A casual sandbox runs
-# with your own UID and makes your home directory copy-on-write via an
-# overlay mount.  To change the default, you can uncomment one of the
-# following:
+const std::string jai_defaults =
+    R"(# This file contains generic defaults built into jai.  It is intended
+# to be included by other configuration files with a line:
+#
+#     conf .defaults
+#
+# You can override settings in this file by editing it directly or by
+# adding configuration directives to default.conf or other <name>.conf
+# files after the `conf .defaults` line.  (Later lines override
+# previous ones in configuration files.)
+#
+# If you delete this file, jai will re-create it the next time it
+# runs.  You can also see the default contents of this file by running
+#
+#     jai --print-defaults
+
+
+# By default, jai stores private home directory state in $HOME/.jai
+# (or in $JAI_CONFIG_DIR, if set).  However, jai works best if the
+# storage directory is not on NFS.  If your home directory is on NFS,
+# use a `storage` directive to specify a local storage location.
+
+# storage /some/local/directory
+
+# 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:
 
 # mode casual
 # mode bare
 # mode strict
 
-# You can use use "name NAME" to specify different sandboxes.  For
-# casual sandboxes, the sandboxed home directory will be in
-# /run/jai/$USER/NAME.home, and changed files will be in
-# $HOME/.jai/NAME.changes.  For strict sandboxes, the home directory
-# will be $HOME/.jai/NAME.home.  If you leave it undefined, the name
-# will be "default" and the mode will default to casual, but if you
-# define this to anything including "default", then the mode will be
-# strict.
+# You can use use "name NAME" to specify different jails.  For casual
+# jails, the home directory will be in /run/jai/$USER/NAME.home, and
+# changed files will be in $HOME/.jai/NAME.changes.  For strict jails,
+# the home directory will be $HOME/.jai/NAME.home.  If you leave name
+# undefined, the name will be "default" and the mode will default to
+# casual, but if you define this to anything including "default", then
+# the default mode will be strict.
 
 # name default
 
-# jai launches programs in a sandbox by running bash with the command
-# name in "$0" and the arguments in "@".  Altering command allow you
-# to set enfironment variables or add command-line arguments.
+# jai launches jailed programs by running bash with the command name
+# in "$0" and the arguments in "@".  Altering command allows you set
+# environment variables dynamically or add command-line arguments,
+# which is more useful to do in a non-default configuration file.
 
-command "$0" "$@"
+# command "$0" "$@"
 
 # Masked files are deleted when an overlayfs is first created, but
 # have no effect on existing overlays or on strict/bare jails.  To
 # delete files from an existing overlay, delete them under
 # /run/jai/$USER/default.home.  Otherwise, to apply new mask
 # directives after editing this file, you can run "jai -u" to unmount
-# any existing overlays.
+# any existing overlays.  If you want to avoid masing any of these
+# files in one particular configuration, you can use a directive such
+# as `unmask .aws` to undo the effects from a previously included
+# default file.
 
 mask .jai
 mask .ssh
@@ -62,9 +86,11 @@ mask .config/BraveSoftware
 mask .bash_history
 mask .zsh_history
 
-# The following environment variables will be removed from sandboxed
+# The following environment variables will be removed from jail
 # environments.  You can use * as a wildcard to match any variables
-# matching the pattern.
+# matching the pattern.  If you want to undo any of these unsetenv
+# commands in a particular config file, you can use setenv to reverse
+# the effects of unsetenv.
 
 unsetenv AZURE_CLIENT_ID
 unsetenv AZURE_TENANT_ID
@@ -97,3 +123,7 @@ unsetenv *_SOCKET
 unsetenv *_SOCKET_PATH
 unsetenv *_TOKEN
 )";
+
+extern const std::string default_conf =
+  R"(conf .defaults
+)";
fs.cc
index fb7c0bc..c6311ce 100644
--- a/fs.cc
+++ b/fs.cc
@@ -341,15 +341,24 @@ open_flags_to_string(int flags)
       {O_TMPFILE, "O_TMPFILE"},
   });
   static constexpr auto known_flags = std::to_array<Flag>({
-      {O_WRONLY, "O_WRONLY"},       {O_RDWR, "O_RDWR"},
-      {O_CREAT, "O_CREAT"},         {O_EXCL, "O_EXCL"},
-      {O_NOCTTY, "O_NOCTTY"},       {O_TRUNC, "O_TRUNC"},
-      {O_APPEND, "O_APPEND"},       {O_NONBLOCK, "O_NONBLOCK"},
-      {O_DSYNC, "O_DSYNC"},         {O_ASYNC, "O_ASYNC"},
-      {O_DIRECT, "O_DIRECT"},       {O_LARGEFILE, "O_LARGEFILE"},
-      {O_DIRECTORY, "O_DIRECTORY"}, {O_NOFOLLOW, "O_NOFOLLOW"},
-      {O_NOATIME, "O_NOATIME"},     {O_CLOEXEC, "O_CLOEXEC"},
-      {O_SYNC, "O_SYNC"},           {O_PATH, "O_PATH"},
+      {O_WRONLY, "O_WRONLY"},
+      {O_RDWR, "O_RDWR"},
+      {O_CREAT, "O_CREAT"},
+      {O_EXCL, "O_EXCL"},
+      {O_NOCTTY, "O_NOCTTY"},
+      {O_TRUNC, "O_TRUNC"},
+      {O_APPEND, "O_APPEND"},
+      {O_NONBLOCK, "O_NONBLOCK"},
+      {O_DSYNC, "O_DSYNC"},
+      {O_ASYNC, "O_ASYNC"},
+      {O_DIRECT, "O_DIRECT"},
+      {O_LARGEFILE, "O_LARGEFILE"},
+      {O_DIRECTORY, "O_DIRECTORY"},
+      {O_NOFOLLOW, "O_NOFOLLOW"},
+      {O_NOATIME, "O_NOATIME"},
+      {O_CLOEXEC, "O_CLOEXEC"},
+      {O_SYNC, "O_SYNC"},
+      {O_PATH, "O_PATH"},
   });
 
   std::string result;
@@ -426,3 +435,36 @@ try_read_file(int dfd, path file)
     ret.append(buf, size_t(n));
   }
 }
+
+Fd
+ensure_file(int dfd, path file, std::string_view contents, int mode)
+{
+  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));
+
+    auto pp = file.parent_path();
+    if (pp.empty())
+      pp = ".";
+    Fd fd = xopenat(dfd, pp, O_TMPFILE | O_RDWR | O_CLOEXEC);
+    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 (linkat(*fd, "", dfd, file.c_str(), AT_EMPTY_PATH) == 0)
+      return fd;
+    if (errno != EEXIST)
+      syserr(R"(linkat(O_TMPFILE -> "{}"))", fdpath(dfd, file));
+    if (!S_ISREG(xfstat(dfd, file).st_mode))
+      err("{}: not a regular file", fdpath(dfd, file));
+  }
+}
fs.h
index 09bfa49..e50d4da 100644
--- a/fs.h
+++ b/fs.h
@@ -239,11 +239,16 @@ xpipe()
 }
 
 inline struct stat
-xfstat(int fd)
+xfstat(int fd, path file = {}, FollowLinks follow = kFollow)
 {
   struct stat sb;
-  if (fstat(fd, &sb))
-    syserr(R"(fstat("{}"))", fdpath(fd));
+  if (file.empty()) {
+    if (fstat(fd, &sb))
+      syserr(R"(fstat("{}"))", fdpath(fd));
+  }
+  else if (fstatat(fd, file.c_str(), &sb,
+                   follow == kFollow ? 0 : AT_SYMLINK_NOFOLLOW))
+    syserr(R"({}stat("{}"))", follow == kFollow ? "" : "l", fdpath(fd, file));
   return sb;
 }
 
@@ -259,6 +264,8 @@ read_file(int dfd, path file = {})
     throw res.error();
 }
 
+Fd ensure_file(int dfd, path file, std::string_view contents, int mode = 0600);
+
 using ACL = RaiiHelper<acl_free, acl_t>;
 
 enum AclType {
jai.1.md
index 0fef7eb..c8cb99e 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -242,6 +242,9 @@ environment before running the command.
   you must use this option if you have added new files to be masked,
   as masking only takes effect at the time an overlay home is created.
 
+`--print-defaults`
+: Prints the default contents for `$HOME/.jai/.defaults`.
+
 `--version`
 : Prints the version number and copyright.
 
@@ -277,12 +280,21 @@ setting the `JAI_CONFIG_DIR` environment variable.
   file for *cmd*, then *cmd*`.conf` is used.  Otherwise `default.conf`
   is used.
 
+`$HOME/.jai/.defaults`
+: Reasonable system defaults to be included in `defaults.conf` or
+  *cmd*`.conf`.  This file is created automatically by jai.  The file
+  has no effect if you don't include it, but you should probably begin
+  all configuration files with the line `conf .defaults` to get the
+  defaults.
+
 `$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 jail.  If you make
-  changes in this directory, you may need to 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.
+  contains changes that have been made inside a casual jail.  If you
+  make changes in this directory, you may need to 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 specified `--storage=`*dir*, the changes
+  directory will be in *dir* instead of `$HOME/.jai`.
 
 `$HOME/.jai/default.work`, `$HOME/.jai/`*name*`.work`
 : This "work" directory is required by overlayfs, but does not contain
@@ -290,15 +302,22 @@ setting the `JAI_CONFIG_DIR` environment variable.
   system may create files in here that you cannot delete.  If you are
   trying to blow away an overlay directory to start from scratch and
   cannot delete this directory, try running `jay -u` which will clean
-  things up.
+  things up.  If you specified `--storage=`*dir*, or used a symbolic
+  link for your changes directory, then the work directory will always
+  be next to the changes directory wherever that lives.
+
+`$HOME/.jai/default.home`, `$HOME/.jai/`*name*`.home`
+: Private home directory for bare and strict jails.  If you specified
+  `--storage=`*dir*, the these directories will be in *dir* instead.
 
 `/run/jai/$USER/default.home`, `/run/jai/$USER/`*name*`.home`
-: Sandboxed home directories for jails.  You can delete files with
-  sensitive data in these sandboxed directories to hide theme from
-  jailed processes, or see the `--mask` option.
+: Home directories for casual jails.  You can delete files with
+  sensitive data in these jail directories to hide theme from jailed
+  processes, or see the `--mask` option.
 
 `/run/jai/$USER/tmp/default`, `/run/jai/$USER/tmp/`*name*
-: Private `/tmp` and `/var/tmp` directory made available in the jail.
+: Private `/tmp` and `/var/tmp` directory (they are the same) in
+  jails.
 
 # BUGS
 
jai.cc
index 3ba0af4..737d8d1 100644
--- a/jai.cc
+++ b/jai.cc
@@ -67,7 +67,6 @@ struct Config {
   void unmount();
   void unmountall();
   std::unique_ptr<Options> opt_parser();
-  void make_default_conf();
 
   bool parse_config_file(path file, Options *opts = nullptr);
   std::vector<const char *> make_env();
@@ -327,6 +326,8 @@ Config::make_blacklist(int dfd, path name)
 {
   Fd blacklistfd = ensure_dir(dfd, name.c_str(), 0700, kFollow);
   check_user(*blacklistfd);
+  if (is_mountpoint(*blacklistfd))
+    err("{}: directory must not be a mountpoint", fdpath(*blacklistfd));
 
   for (path p : mask_files_) {
     try {
@@ -666,19 +667,6 @@ Config::unmountall()
   unlinkat(run_jai(), user_.c_str(), AT_REMOVEDIR);
 }
 
-void
-Config::make_default_conf()
-{
-  auto fd = xopenat(home_jai(), ".", O_RDWR | O_TMPFILE | O_CLOEXEC, 0600);
-  errno = EAGAIN;
-  if (write(*fd, default_conf.data(), default_conf.size()) !=
-      default_conf.size())
-    syserr("write(O_TMPFILE for default.conf)");
-  if (linkat(*fd, "", home_jai(), "default.conf", AT_EMPTY_PATH) &&
-      errno != EEXIST)
-    syserr("linkat({})", fdpath(home_jai(), "default.conf"));
-}
-
 extern "C" char **environ;
 
 std::vector<const char *>
@@ -1009,12 +997,12 @@ The default is CMD.conf if it exists, otherwise default.conf)",
   (*opts)("--help", [] { usage(0); });
   (*opts)("--version", version, "Print copyright and version then exit");
   (*opts)(
-      "--print-default-conf",
+      "--print-defaults",
       [] {
-        write(1, default_conf.data(), default_conf.size());
+        write(1, jai_defaults.data(), jai_defaults.size());
         exit(0);
       },
-      "Show contents of the default configuration file");
+      "Show default contents of $JAI_CONFIG_DIR/.defaults");
   option_help = opts->help();
 
   std::vector<char *> cmd;
@@ -1037,16 +1025,17 @@ The default is CMD.conf if it exists, otherwise default.conf)",
     return;
   }
 
+  ensure_file(conf.home_jai(), ".defaults", jai_defaults, 0600);
+  ensure_file(conf.home_jai(), "default.conf", jai_defaults, 0600);
+
   if (!opt_C.empty()) {
     if (!conf.parse_config_file(opt_C))
       err("{}: no such configuration file", opt_C.string());
   }
   else if ((cmd.empty() || !conf.name_ok(cmd[0]) ||
             !conf.parse_config_file(std::format("{}.conf", cmd[0]))) &&
-           !conf.parse_config_file("default.conf")) {
-    conf.make_default_conf();
+           !conf.parse_config_file("default.conf"))
     conf.parse_config_file("default.conf");
-  }
   // Re-parse command line to override files
   opts->parse_argv(argc, argv);
 
jai.h
index 37a9364..956f210 100644
--- a/jai.h
+++ b/jai.h
@@ -27,4 +27,5 @@ xfork(std::uint64_t flags = 0)
     }                                                                          \
   } while (0)
 
+extern const std::string jai_defaults;
 extern const std::string default_conf;