Repositories / jai.git

jai.git

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

Branch

create private password file so ~jai is user home directory

Author
David Mazieres <dm@uun.org>
Date
2026-03-17 22:41:44 -0700
Commit
efad562e9d4f8ce542b3ca873eb824ba72a4f1d9
cred.h
index eedeb01..c04c255 100644
--- a/cred.h
+++ b/cred.h
@@ -45,10 +45,10 @@ template<typename Ent, auto IdFn, auto NamFn> struct DbEnt {
   const Ent *operator->() const { return p_; }
   Ent *get() const { return p_; }
 
-  static DbEnt get_id(ugid_t n) { return get(IdFn, n); }
-  static DbEnt get_nam(const char *n) { return get(NamFn, n); }
+  static DbEnt get_id(ugid_t n) { return find(IdFn, n); }
+  static DbEnt get_nam(const char *n) { return find(NamFn, n); }
 
-  static DbEnt get(auto fn, auto key)
+  static DbEnt find(auto fn, auto key)
   {
     DbEnt ret;
     ret.buf_.resize(std::max(128uz, ret.buf_.capacity()));
@@ -58,8 +58,10 @@ template<typename Ent, auto IdFn, auto NamFn> struct DbEnt {
         return ret ? std::move(ret) : DbEnt{};
       else if (r == ERANGE)
         ret.buf_.resize(2 * ret.buf_.size());
+      else if (r == ENOENT)
+        return DbEnt{};
       else
-        errno = r, syserr("DbEnt<{}>::get", typeid(Ent).name());
+        errno = r, syserr("DbEnt<{}>::find", typeid(Ent).name());
     }
   }
 };
jai.cc
index 42bd06a..1a09ae1 100644
--- a/jai.cc
+++ b/jai.cc
@@ -12,13 +12,14 @@
 #include <print>
 
 #include <acl/libacl.h>
+#include <pwd.h>
 #include <sys/prctl.h>
 #include <sys/types.h>
 #include <sys/wait.h>
 
 path prog;
 
-constexpr const char *kUnstrustedUser = UNTRUSTED_USER;
+constexpr const char *kUntrustedUser = UNTRUSTED_USER;
 constexpr const char *kUntrustedGecos = "JAI sandbox untrusted user";
 constexpr const char *kRunRoot = "/run/jai";
 
@@ -40,6 +41,7 @@ struct Config {
   path sandbox_name_;
   Credentials user_cred_;
   Credentials untrusted_cred_;
+  path shell_;
   mode_t old_umask_ = 0755;
 
   Fd home_fd_;
@@ -93,6 +95,7 @@ struct Config {
   Fd make_blacklist(int dfd, path name);
   Fd make_home_overlay();
   Fd make_private_tmp();
+  Fd make_private_passwd();
 
   static bool name_ok(path p)
   {
@@ -182,16 +185,17 @@ Config::init_credentials()
 
   user_ = pw->pw_name;
   homepath_ = pw->pw_dir;
+  shell_ = pw->pw_shell;
   untrusted_cred_ = user_cred_ = Credentials::get_user(pw);
 
-  if (PwEnt u = PwEnt::get_nam(kUnstrustedUser)) {
+  if (PwEnt u = PwEnt::get_nam(kUntrustedUser)) {
     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 "{}")",
-           kUnstrustedUser, kUntrustedGecos);
+           kUntrustedUser, kUntrustedGecos);
   }
 
   // Paranoia about ptrace, because we will drop privileges to access
@@ -369,6 +373,47 @@ Config::make_private_tmp()
 }
 
 Fd
+Config::make_private_passwd()
+{
+  if (Fd fd = openat(run_jai_user(), "passwd", O_RDONLY | O_CLOEXEC))
+    return fd;
+  if (errno != ENOENT)
+    syserr("{}", fdpath(run_jai_user(), "passwd"));
+
+  RaiiHelper<fclose> r, w;
+
+  Fd wfd = xopenat(run_jai_user(), ".", O_RDWR | O_TMPFILE | O_CLOEXEC, 0444);
+  if (!(w = fdopen(*wfd, "w")))
+    syserr("fdopen({})", fdpath(*wfd));
+  wfd.release();
+
+  auto restore = asuser();
+  r = fopen("/etc/passwd", "r");
+  if (!r)
+    syserr("/etc/passwd");
+  fcntl(fileno(r), F_SETFD, 1);
+  restore.reset();
+
+  while (auto pw = PwEnt::find(fgetpwent_r, *r)) {
+    if (!strcmp(pw->pw_name, kUntrustedUser)) {
+      pw.get()->pw_dir = const_cast<char *>(homepath_.c_str());
+      pw.get()->pw_shell = const_cast<char *>(shell_.c_str());
+    }
+    if (putpwent(pw.get(), *w))
+      syserr("putpwent");
+  }
+  if (fflush(*w))
+    syserr("fflush");
+  if (linkat(fileno(*w), "", run_jai_user(), "passwd", AT_EMPTY_PATH) &&
+      errno != EEXIST)
+    syserr("linkat({})", fdpath(run_jai_user(), "passwd"));
+  r.reset();
+  w.reset();
+
+  return xopenat(run_jai_user(), "passwd", O_RDONLY);
+}
+
+Fd
 Config::make_idmap_ns()
 {
   pid_t pid{-1};
@@ -411,11 +456,12 @@ 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);
+  bool strict_ok = untrusted_cred_ != user_cred_;
+  if (mode_ == kStrict && !strict_ok)
+    err("Cannot use strict mode: invalid user {}", kUntrustedUser);
 
   if (mode_ == kInvalidMode)
-    mode_ = sandbox_name_.empty() ? kCasual : kStrict;
+    mode_ = sandbox_name_.empty() ? kCasual : strict_ok ? kStrict : kBare;
   if (sandbox_name_.empty())
     sandbox_name_ = "default";
 
@@ -425,6 +471,10 @@ Config::make_mnt_ns()
   };
   Fd tmp = clone_tree(*mp_holder_.emplace_back(make_private_tmp()));
 
+  Fd passwd;
+  if (mode_ == kStrict)
+    passwd = clone_tree(*make_private_passwd());
+
   Fd home;
   Fd mapns;
   Credentials *sbcred = &user_cred_;
@@ -441,6 +491,8 @@ Config::make_mnt_ns()
   }
   xmnt_setattr(*tmp, attr);
   xmnt_setattr(*home, attr);
+  if (passwd)
+    xmnt_setattr(*passwd, attr);
 
   if (unshare(CLONE_NEWNS))
     syserr("unshare(CLONE_NEWNS)");
@@ -458,6 +510,8 @@ Config::make_mnt_ns()
   xmnt_move(*tmp, -1, "/tmp");
   xmnt_move(*clone_tree(-1, "/tmp"), -1, "/var/tmp", 0);
   xmnt_move(*home, -1, homepath_);
+  if (passwd)
+    xmnt_move(*passwd, -1, "/etc/passwd");
 
   if (grant_cwd_) {
     if (!grant_directories_.contains(cwd())) {
@@ -787,7 +841,7 @@ Config::opt_parser()
     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),
+                  kUntrustedUser),
       "casual|bare|strict");
   opts(
       "-d", "--dir",