Repositories / jai.git

jai.git

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

Branch

prevent access to storage directory when not under a masked directory

Author
David Mazieres <dm@uun.org>
Date
2026-03-18 22:07:12 -0700
Commit
d06af0d0ce04ceac9dd59a379a5c9b5ea3aacd85
fs.cc
index c6311ce..dcc140b 100644
--- a/fs.cc
+++ b/fs.cc
@@ -30,8 +30,11 @@ glob(std::string_view pattern, std::string_view target)
 std::string
 fdpath(int fd, bool must)
 {
-  if (fd < 0 || fd == AT_FDCWD)
+  if (fd < 0 || fd == AT_FDCWD) {
+    if (must)
+      err("fdpath invalid fd {}", fd);
     return ".";
+  }
   auto procfd = std::format("/proc/self/fd/{}", fd);
   std::error_code ec;
   auto res = std::filesystem::read_symlink(procfd, ec);
@@ -42,24 +45,21 @@ fdpath(int fd, bool must)
     }
     res = std::format("fd {} [can't determine path]", fd, ec.message());
   }
+  else if (must && !res.is_absolute() || !is_fd_at_path(fd, -1, res))
+    err("{} not valid complete path for fd {}", res.string(), fd);
   return res;
 }
 
 std::string
-fdpath(int fd, const path &file, bool must)
+fdpath(int fd, const path &file)
 {
   if (fd < 0 || fd == AT_FDCWD || file.is_absolute())
     return file.empty() ? "." : file.string();
   auto procfd = std::format("/proc/self/fd/{}", fd);
   std::error_code ec;
   auto res = std::filesystem::read_symlink(procfd, ec);
-  if (ec) {
-    if (must) {
-      errno = ec.value();
-      syserr("{}", procfd);
-    }
-    res = std::format("fd {} [can't determine path]", fd, ec.message());
-  }
+  if (ec)
+    res = std::format("fd {} [can't determine path]: {}", fd, ec.message());
   if (!file.empty())
     res = res / file;
   return res;
fs.h
index e50d4da..77fb653 100644
--- a/fs.h
+++ b/fs.h
@@ -68,7 +68,7 @@ subtree_rev(const is_one_of<PathSet, PathMultiset> auto &s, const path &root)
   return subtree(s, root) | std::views::reverse;
 }
 
-std::string fdpath(int fd, const path &file, bool must = false);
+std::string fdpath(int fd, const path &file);
 std::string fdpath(int fd, bool must = false);
 
 PathMultiset mountpoints(const path &mountinfo = "/proc/self/mountinfo");
jai.cc
index 737d8d1..0429487 100644
--- a/jai.cc
+++ b/jai.cc
@@ -255,10 +255,23 @@ Config::home_jai()
 int
 Config::storage()
 {
+  if (storage_fd_)
+    return *storage_fd_;
+
+  auto restore = asuser();
+
   if (storagedir_.empty())
-    return home_jai();
-  if (!storage_fd_)
+    storage_fd_ = xdup(home_jai());
+  else
     storage_fd_ = ensure_udir(AT_FDCWD, storagedir_);
+
+  path fullpath = fdpath(*storage_fd_, true);
+  if (fullpath.is_relative())
+    err("cannot find full pathname for {}", storagedir_.string());
+  if (!is_fd_at_path(*storage_fd_, -1, fullpath))
+    err("{} is no longer at {}", storagedir_.string(), fullpath.string());
+  storagedir_ = fullpath;
+
   return *storage_fd_;
 }
 
@@ -573,6 +586,28 @@ Config::make_mnt_ns()
     xmnt_move(*src, *dst);
   }
 
+  xsetns(*newns, CLONE_NEWNS);
+  struct stat sb;
+
+  if (!storagedir_.empty() && stat(storagedir_.c_str(), &sb) == 0 &&
+      S_ISDIR(sb.st_mode)) {
+    // make sure storage directory not exposed
+    auto restore_root = asuser();
+    Fd target = xopenat(AT_FDCWD, storagedir_.c_str(), O_DIRECTORY | O_RDONLY);
+    check_user(*target);
+    restore_root.reset();
+    Fd empty = xopenat(-1, kRunRoot, O_RDONLY);
+    if (!is_dir_empty(*empty))
+      err("{} should be empty in jail", kRunRoot);
+    Fd source = clone_tree(*empty);
+    xmnt_setattr(*source, mount_attr{
+                             .attr_set = MOUNT_ATTR_RDONLY | MOUNT_ATTR_NOSUID |
+                                         MOUNT_ATTR_NODEV,
+                             .propagation = MS_PRIVATE,
+                         });
+    xmnt_move(*source, *target);
+  }
+
   return newns;
 }