Repositories / jai.git

jai.git

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

Branch

add bare mode

Author
David Mazieres <dm@uun.org>
Date
2026-03-16 22:06:17 -0700
Commit
eedee3ab2ca25073d1864fdee81233b52a9bdb22
README.md
index 89cbe66..6d5fd56 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 ![](./logo.svg "JAI logo")
 
-# JAI - An ultra lightweight jail for AI CLIs on linux 6.13 and later
+# JAI - An ultra lightweight jail for AI CLIs on modern linux
 
 `jai` strives to be the easiest container in the world to
 configure--so easy that you never again need to run a code assistant
@@ -39,6 +39,11 @@ following:
 
 * Per-command configuration files.
 
+jai emphasizes security over portability.  It heavily leverages modern
+linux APIs to isolate processes and avoid time-of-check-to-time-of-use
+race conditions.  It will not work with kernels older than 6.13 or
+operating systems other than linux.
+
 See the [man page](jai.1.md) for more documentation.
 
 See the [INSTALL](INSTALL) file for installation instructions.
default_conf.cc
index 0bf8c7c..a974103 100644
--- a/default_conf.cc
+++ b/default_conf.cc
@@ -13,8 +13,9 @@ const std::string default_conf =
 # overlay mount.  To change the default, you can uncomment one of the
 # following:
 
-# casual
-# strict
+# 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
@@ -38,10 +39,11 @@ const std::string default_conf =
 command "$0" "$@"; exit $?
 
 # Masked files are deleted when an 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.
-# Otherwise, to apply new mask directives you can run "jai -u" to
-# unmount any existing overlays.
+# 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.
 
 mask .jai
 mask .ssh
jai.1.md
index 29d819d..8cf56e3 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -31,7 +31,7 @@ everything else.  This is known as _casual mode_, because *cmd* can
 read most sensitive files on the system, so jai prevents *cmd* from
 clobbering all your files but doesn't provide any confidentiality.
 
-If you run `jai --strict` *cmd* [*arg*]...", then *cmd* will be run
+If you run `jai -mstrict` *cmd* [*arg*]...", then *cmd* will be run
 with an empty home directory as an unprivileged user id, but with the
 current working directory mapped to its place and fully exposed.
 While the rest of the system outside the user's home directory is
@@ -43,6 +43,8 @@ Before using `jai`, if your home directory is on NFS, make
 `$HOME/.jai` a symbolic link to a directory you own on a local file
 system that supports extended attributes.  Otherwise, overlay mounts
 may not work and you may only be able to use strict mode (see below).
+Note that strict mode will not work with a home directory on NFS, but
+you can use bare mode.
 
 If you want to grant access to directories other than the current
 working directory, you can specify addition directories with the `-d`
@@ -60,7 +62,7 @@ jai allows the use of multiple sandboxed home directories.  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.  However, you
-can have multiple home overlays by specifying `--casual` with `-n`.
+can have multiple home overlays by specifying `-mcasual` with `-n`.
 
 # CONFIGURATION
 
@@ -120,7 +122,7 @@ virtual environment before running the command.
   is read at the exact point of the `conf` directive, so that it
   overrides previous lines and is overridden by subsequent lines.
 
-`-d` *dir*, `--dir `*dir*
+`-d` *dir*, `--dir` *dir*
 : Grant full access to directory *dir* and everything below in the
   jail.  You must own the directory.  You can supply this option
   multiple times.  Note that on the command line, relative paths are
@@ -134,9 +136,9 @@ virtual environment before running the command.
   home directory will be copy-on-write and nothing will be directly
   exported.
 
-`--casual`
-: Enables casual mode, in which the user's home directory is made
-  available as an overlay mount.  Casual mode protects against
+`-m casual`|`bare`|`strict`, `--mode casual`|`bare`|`strict`
+: Set the execution mode.  In casual mode, the user's home directory
+  is made available as an overlay mount.  Casual mode protects against
   destruction of files outside of granted directories, but does not
   protect confidentiality:  sandboxed code can read most files
   accessible to the user.  You can hide specific files with the
@@ -144,15 +146,19 @@ virtual environment before running the command.
   but because casual mode makes everything readable by default, it
   cannot protect all sensitive files.
 
-`--strict`
-: Enables strict mode.  In strict mode, the user's home directory is
-  replaced by an empty directory, and sandboxed 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
+    In strict mode, the user's home directory is replaced by an empty
+  directory (`$HOME/.jai/`*name*`.home`), and sandboxed 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 sandbox (see `--name`), but not for the
   default sandbox.
 
-`-n` *name*, `--name `*name*
+    Bare mode uses an empty directory like strict mode, but runs with
+  the invoking user's credentials.  It is inferior to strict mode, but
+  can be used for NFS-mounted home directories, since NFS does not
+  support id-mapped mounts.
+
+`-n` *name*, `--name` *name*
 : jai allows you to have multiple sandboxed home directories, which
   may be useful when sandboxing multiple tools that should not have
   access to each other's API keys.  This option specifies which home
jai.cc
index eab8db0..3c58862 100644
--- a/jai.cc
+++ b/jai.cc
@@ -17,10 +17,11 @@
 path prog;
 
 constexpr const char *kUnstrustedUser = UNTRUSTED_USER;
+constexpr const char *kUntrustedGecos = "JAI sandbox untrusted user";
 constexpr const char *kRunRoot = "/run/jai";
 
 struct Config {
-  enum Mode { kInvalidMode, kCasual, kStrict };
+  enum Mode { kInvalidMode, kCasual, kBare, kStrict };
 
   Mode mode_{kInvalidMode};
   PathSet grant_directories_;
@@ -179,18 +180,14 @@ Config::init_credentials()
   untrusted_cred_ = user_cred_ = Credentials::get_user(pw);
 
   if (PwEnt u = PwEnt::get_nam(kUnstrustedUser)) {
-    if (u->pw_uid && !strcmp(u->pw_gecos, "JAI sandbox untrusted user") &&
+    if (u->pw_uid && !strcmp(u->pw_gecos, kUntrustedGecos) &&
         !strcmp(u->pw_dir, "/"))
       untrusted_cred_ = Credentials::get_user(u);
     else
-      warn(R"(Ignoring user {} because uid is 0, home dir is not "/" or
-GECOS field is not "JAI sandbox untrusted user")",
-           kUnstrustedUser);
+      warn(R"(Ignoring user {} because uid is 0, home dir is not "/", or
+GECOS field is not "{}")",
+           kUnstrustedUser, kUntrustedGecos);
   }
-  else
-    warn(R"(Could not find credentials for untrusted {} user.
-Try running "sudo systemd-sysusers".)",
-         kUnstrustedUser);
 
   // Paranoia about ptrace, because we will drop privileges to access
   // the file system as the user.
@@ -409,6 +406,9 @@ Config::make_mnt_ns()
   Fd oldns = xopenat(-1, "/proc/self/ns/mnt", O_RDONLY | O_CLOEXEC);
   Defer _restore_ns{[fd = *oldns] { xsetns(fd, CLONE_NEWNS); }};
 
+  if (mode_ == kStrict && untrusted_cred_ == user_cred_)
+    err("Cannot use strict mode: invalid user {}", kUnstrustedUser);
+
   if (mode_ == kInvalidMode)
     mode_ = sandbox_name_.empty() ? kCasual : kStrict;
   if (sandbox_name_.empty())
@@ -426,10 +426,12 @@ Config::make_mnt_ns()
   if (mode_ == kCasual)
     home = clone_tree(*make_home_overlay());
   else {
-    sbcred = &untrusted_cred_;
-    mapns = make_idmap_ns();
-    attr.attr_set |= MOUNT_ATTR_IDMAP;
-    attr.userns_fd = *mapns;
+    if (mode_ == kStrict) {
+      sbcred = &untrusted_cred_;
+      mapns = make_idmap_ns();
+      attr.attr_set |= MOUNT_ATTR_IDMAP;
+      attr.userns_fd = *mapns;
+    }
     home = clone_tree(*ensure_udir(home_jai(), cat(sandbox_name_, ".home")));
   }
   xmnt_setattr(*tmp, attr);
@@ -474,6 +476,7 @@ Config::make_mnt_ns()
     xsetns(*oldns, CLONE_NEWNS);
     auto restore_root = asuser();
     Fd src = xopenat(-1, d, O_DIRECTORY | O_PATH | O_CLOEXEC);
+    check_user(*src, d);
     restore_root.reset();
     src = clone_tree(*src); // Should it be recursive?
     xmnt_setattr(*src, attr);
@@ -661,7 +664,7 @@ Config::exec(int nsfd, char **argv)
     _exit(1);
   }
 
-  if (mode_ == kCasual)
+  if (mode_ == kCasual || mode_ == kBare)
     user_cred_.make_real();
   else
     untrusted_cred_.make_real();
@@ -692,12 +695,24 @@ Config::opt_parser()
   auto ret = std::make_unique<Options>();
   Options &opts = *ret;
   opts(
-      "--casual", [this] { mode_ = kCasual; },
-      "Enable casual mode (copy-on-write overlay home directory)");
-  opts(
-      "--strict", [this] { mode_ = kStrict; },
-      std::format("Enable strict mode (run with uid {} and empty home)",
-                  kUnstrustedUser));
+      "-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}};
+        if (auto it = modemap.find(m); it != modemap.end())
+          mode_ = it->second;
+        else
+          err<Options::Error>(R"(invalid mode {})", m);
+      },
+      std::format(R"(Set execution mode to one of the following:
+    casual - run as invoking UID with overlay home directory
+    bare - run as invoking UID with bare home directory
+    strict - run as UID {} with bare home directory)",
+                  kUnstrustedUser),
+      "casual|bare|strict");
   opts(
       "-d", "--dir",
       [this](path d) {