From d8ea7266829123630b7cc244b1da96493b0bf28b Mon Sep 17 00:00:00 2001 From: Robert Swiecki Date: Sat, 31 Jan 2026 16:05:58 +0100 Subject: [PATCH] Implement support for the new Linux mount API and refactor mount logic into dedicated files. Added the --experimental_mnt flag to allow forcing legacy or new mount behaviors. --- Makefile | 9 +- cmdline.cc | 25 ++- macros.h | 23 +++ mnt.cc | 440 +++------------------------------------ mnt.h | 12 ++ mnt_legacy.cc | 432 +++++++++++++++++++++++++++++++++++++++ mnt_legacy.h | 20 ++ mnt_newapi.cc | 555 ++++++++++++++++++++++++++++++++++++++++++++++++++ mnt_newapi.h | 21 ++ nsjail.h | 1 + util.cc | 30 +++ util.h | 2 + 12 files changed, 1144 insertions(+), 426 deletions(-) create mode 100644 mnt_legacy.cc create mode 100644 mnt_legacy.h create mode 100644 mnt_newapi.cc create mode 100644 mnt_newapi.h diff --git a/Makefile b/Makefile index f288807..184056a 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ endif BIN = nsjail LIBS = kafel/libkafel.a -SRCS_CXX = caps.cc cgroup.cc cgroup2.cc cmdline.cc config.cc contain.cc cpu.cc logs.cc mnt.cc net.cc nsjail.cc pid.cc sandbox.cc subproc.cc uts.cc user.cc util.cc +SRCS_CXX = caps.cc cgroup.cc cgroup2.cc cmdline.cc config.cc contain.cc cpu.cc logs.cc mnt.cc mnt_legacy.cc mnt_newapi.cc net.cc nsjail.cc pid.cc sandbox.cc subproc.cc uts.cc user.cc util.cc SRCS_PROTO = config.proto SRCS_PB_CXX = $(SRCS_PROTO:.proto=.pb.cc) @@ -112,14 +112,17 @@ caps.o: caps.h nsjail.h config.pb.h logs.h macros.h util.h cgroup.o: cgroup.h nsjail.h config.pb.h logs.h util.h cgroup2.o: cgroup2.h nsjail.h config.pb.h logs.h util.h cmdline.o: cmdline.h nsjail.h config.pb.h caps.h config.h logs.h macros.h -cmdline.o: mnt.h user.h util.h +cmdline.o: mnt.h mnt_newapi.h user.h util.h config.o: config.h nsjail.h config.pb.h caps.h cmdline.h logs.h macros.h config.o: mnt.h user.h util.h contain.o: contain.h nsjail.h config.pb.h caps.h cgroup.h cgroup2.h config.h contain.o: cpu.h logs.h macros.h mnt.h net.h pid.h user.h util.h uts.h cpu.o: cpu.h nsjail.h config.pb.h logs.h util.h logs.o: logs.h macros.h util.h nsjail.h config.pb.h -mnt.o: mnt.h nsjail.h config.pb.h logs.h macros.h subproc.h util.h +mnt.o: mnt.h nsjail.h config.pb.h logs.h macros.h mnt_legacy.h mnt_newapi.h +mnt.o: subproc.h util.h +mnt_legacy.o: mnt_legacy.h mnt.h nsjail.h config.pb.h logs.h macros.h util.h +mnt_newapi.o: mnt_newapi.h mnt.h nsjail.h config.pb.h logs.h util.h net.o: net.h nsjail.h config.pb.h logs.h util.h nsjail.o: nsjail.h config.pb.h cgroup2.h cmdline.h logs.h macros.h net.h nsjail.o: sandbox.h subproc.h util.h diff --git a/cmdline.cc b/cmdline.cc index 32429f1..ab30c47 100644 --- a/cmdline.cc +++ b/cmdline.cc @@ -53,6 +53,7 @@ #include "logs.h" #include "macros.h" #include "mnt.h" +#include "mnt_newapi.h" #include "user.h" #include "util.h" @@ -139,6 +140,7 @@ static const struct custom_option custom_opts[] = { { { "disable_proc", no_argument, nullptr, 0x0603 }, "Disable mounting procfs in the jail" }, { { "proc_path", required_argument, nullptr, 0x0605 }, "Path used to mount procfs (default: '/proc')" }, { { "proc_rw", no_argument, nullptr, 0x0606 }, "Is procfs mounted as R/W (default: R/O)" }, + { { "experimental_mnt", required_argument, nullptr, 0x0609 }, "Mount API to use: 'new' (fsopen/fsmount), 'old' (mount syscall), or 'default' (auto-detect based on kernel version)" }, { { "seccomp_policy", required_argument, nullptr, 'P' }, "Path to file containing seccomp-bpf policy (see kafel/)" }, { { "seccomp_string", required_argument, nullptr, 0x0901 }, "String with kafel seccomp-bpf policy (see kafel/)" }, { { "seccomp_log", no_argument, nullptr, 0x0902 }, "Use SECCOMP_FILTER_FLAG_LOG. Log all actions except SECCOMP_RET_ALLOW). Supported since kernel version 4.14" }, @@ -408,22 +410,12 @@ static bool setupMounts(nsj_t* nsj) { for (int i = nsj->njc.mount_size() - 1; i > 0; i--) { nsj->njc.mutable_mount()->SwapElements(i, i - 1); } - } else { - nsjail::MountPt* p = nsj->njc.add_mount(); - p->set_dst("/"); - p->set_fstype("tmpfs"); - p->set_rw(nsj->is_root_rw); - p->set_is_dir(true); - /* Insert at the beginning */ - for (int i = nsj->njc.mount_size() - 1; i > 0; i--) { - nsj->njc.mutable_mount()->SwapElements(i, i - 1); - } } if (!nsj->proc_path.empty()) { nsjail::MountPt* p = nsj->njc.add_mount(); p->set_dst(nsj->proc_path); p->set_fstype("proc"); - p->set_rw(nsj->njc.mount_proc()); + p->set_rw(nsj->is_proc_rw); p->set_is_dir(true); } @@ -470,6 +462,7 @@ std::unique_ptr parseArgs(int argc, char* argv[]) { nsj->orig_euid = geteuid(); nsj->seccomp_fprog.filter = NULL; nsj->seccomp_fprog.len = 0; + nsj->mnt_newapi = mnt::newapi::isAvailable(); nsj->openfds.push_back(STDIN_FILENO); nsj->openfds.push_back(STDOUT_FILENO); @@ -685,6 +678,16 @@ std::unique_ptr parseArgs(int argc, char* argv[]) { case 0x0606: nsj->is_proc_rw = true; break; + case 0x0609: + if (strcasecmp(optarg, "new") == 0) { + nsj->mnt_newapi = true; + } else if (strcasecmp(optarg, "old") == 0) { + nsj->mnt_newapi = false; + } else if (strcasecmp(optarg, "default") != 0) { + LOG_E("--experimental_mnt must be 'new', 'old', or 'default'"); + return nullptr; + } + break; case 0x0607: nsj->njc.mutable_exec_bin()->set_exec_fd(true); break; diff --git a/macros.h b/macros.h index 6ce35ba..15cc8e0 100644 --- a/macros.h +++ b/macros.h @@ -24,6 +24,8 @@ #include +#include + #if !defined(TEMP_FAILURE_RETRY) #define TEMP_FAILURE_RETRY(expression) \ (__extension__({ \ @@ -40,4 +42,25 @@ #define NS_VALSTR_STRUCT(x) {(uint64_t)x, #x} +/* go-style defer */ +template +struct Defer { + F f; + Defer(F f) : f(std::move(f)) { + } + ~Defer() noexcept { + f(); + } + + Defer(const Defer&) = delete; + Defer& operator=(const Defer&) = delete; + Defer(Defer&&) = default; + Defer& operator=(Defer&&) = default; +}; + +#define _DEFER_1(x, y) x##y +#define _DEFER_2(x, y) _DEFER_1(x, y) +#define _DEFER_3(x) _DEFER_2(x, __COUNTER__) +#define defer [[maybe_unused]] Defer _DEFER_3(_defer_) = [&]() + #endif /* NS_COMMON_H */ diff --git a/mnt.cc b/mnt.cc index 6d605d2..043477c 100644 --- a/mnt.cc +++ b/mnt.cc @@ -21,6 +21,7 @@ #include "mnt.h" +#include #include #include #include @@ -41,27 +42,17 @@ #include #include +#include #include "logs.h" #include "macros.h" +#include "mnt_legacy.h" +#include "mnt_newapi.h" #include "subproc.h" #include "util.h" namespace mnt { -struct mount_t { - std::string src; - std::string src_content; - std::string dst; - std::string fs_type; - std::string options; - uintptr_t flags; - bool is_dir; - bool is_symlink; - bool is_mandatory; - bool mounted; -}; - #if !defined(MS_NOSYMFOLLOW) #define MS_NOSYMFOLLOW 256 #endif /* if !defined(MS_NOSYMFOLLOW) */ @@ -75,11 +66,7 @@ struct mount_t { #define MS_NOUSER (1 << 31) #endif /* if !defined(MS_NOUSER) */ -#if !defined(ST_NOSYMFOLLOW) -#define ST_NOSYMFOLLOW 8192 -#endif /* if !defined(ST_NOSYMFOLLOW) */ - -static const std::string flagsToStr(unsigned long flags) { +const std::string flagsToStr(unsigned long flags) { std::string res; struct { @@ -161,339 +148,6 @@ const std::string describeMountPt(const nsjail::MountPt& mpt) { return descr; } -/* Helper for internal use with mount_t */ -static const std::string describeMountPt(const mount_t& mpt) { - std::string descr; - - descr.append(mpt.src.empty() ? "" : QC(mpt.src)) - .append(mpt.src.empty() ? "" : " -> ") - .append(QC(mpt.dst)) - .append(" flags:") - .append(flagsToStr(mpt.flags)) - .append(" type:") - .append(QC(mpt.fs_type)) - .append(" options:") - .append(QC(mpt.options)); - - if (mpt.is_dir) { - descr.append(" dir:true"); - } else { - descr.append(" dir:false"); - } - if (!mpt.is_mandatory) { - descr.append(" mandatory:false"); - } - if (!mpt.src_content.empty()) { - descr.append(" src_content_len:").append(std::to_string(mpt.src_content.length())); - } - if (mpt.is_symlink) { - descr.append(" symlink:true"); - } - - return descr; -} - -static bool isDir(const char* path) { - /* - * If the source dir is NULL, we assume it's a dir (for /proc and tmpfs) - */ - if (path == nullptr) { - return true; - } - struct stat st; - if (stat(path, &st) == -1) { - PLOG_D("stat(%s)", QC(path)); - return false; - } - if (S_ISDIR(st.st_mode)) { - return true; - } - return false; -} - -static int mountRWIfPossible(mount_t* mpt, const char* src, const char* dst) { - int res = - mount(src, dst, mpt->fs_type.c_str(), mpt->flags & ~(MS_RDONLY), mpt->options.c_str()); - if ((mpt->flags & MS_RDONLY) && res == -1 && errno == EPERM) { - LOG_W("mount(%s) src:%s dstpath:%s could not mount read-write, falling " - "back to mounting read-only directly", - describeMountPt(*mpt).c_str(), QC(src), QC(dst)); - res = mount(src, dst, mpt->fs_type.c_str(), mpt->flags, mpt->options.c_str()); - } - return res; -} - -static bool mountPt(mount_t* mpt, const char* newroot, const char* tmpdir) { - LOG_D("Mounting %s", describeMountPt(*mpt).c_str()); - - char dstpath[PATH_MAX]; - snprintf(dstpath, sizeof(dstpath), "%s/%s", newroot, mpt->dst.c_str()); - - char srcpath[PATH_MAX]; - if (!mpt->src.empty()) { - snprintf(srcpath, sizeof(srcpath), "%s", mpt->src.c_str()); - } else { - snprintf(srcpath, sizeof(srcpath), "none"); - } - - if (!util::createDirRecursively(dstpath)) { - LOG_W("Couldn't create upper directories for '%s'", dstpath); - return false; - } - - if (mpt->is_symlink) { - LOG_D("symlink(%s, %s)", util::StrQuote(srcpath).c_str(), - util::StrQuote(dstpath).c_str()); - if (symlink(srcpath, dstpath) == -1) { - if (mpt->is_mandatory) { - PLOG_E("symlink('%s', '%s')", util::StrQuote(srcpath).c_str(), - util::StrQuote(dstpath).c_str()); - return false; - } else { - PLOG_W("symlink('%s', '%s'), but it's not mandatory, continuing", - util::StrQuote(srcpath).c_str(), - util::StrQuote(dstpath).c_str()); - } - } - return true; - } - - if (mpt->is_dir) { - if (mkdir(dstpath, 0711) == -1 && errno != EEXIST) { - PLOG_W("mkdir(%s)", QC(dstpath)); - } - } else { - int fd = TEMP_FAILURE_RETRY(open(dstpath, O_CREAT | O_RDONLY | O_CLOEXEC, 0644)); - if (fd >= 0) { - close(fd); - } else { - PLOG_W("open(%s, O_CREAT|O_RDONLY|O_CLOEXEC, 0644)", QC(dstpath)); - } - } - - if (!mpt->src_content.empty()) { - static uint64_t df_counter = 0; - snprintf( - srcpath, sizeof(srcpath), "%s/dynamic_file.%" PRIu64, tmpdir, ++df_counter); - int fd = TEMP_FAILURE_RETRY( - open(srcpath, O_CREAT | O_EXCL | O_CLOEXEC | O_WRONLY, 0644)); - if (fd < 0) { - PLOG_W("open(srcpath, O_CREAT|O_EXCL|O_CLOEXEC|O_WRONLY, 0644) failed"); - return false; - } - if (!util::writeToFd(fd, mpt->src_content.data(), mpt->src_content.length())) { - LOG_W( - "Writing %zu bytes to '%s' failed", mpt->src_content.length(), srcpath); - close(fd); - return false; - } - close(fd); - mpt->flags |= (MS_BIND | MS_REC | MS_PRIVATE); - } - - /* - * Initially mount it as RW, it will be remounted later on if needed - */ - if (mountRWIfPossible(mpt, srcpath, dstpath) == -1) { - if (errno == EACCES) { - PLOG_W("mount('%s') src:'%s' dstpath:'%s' failed. " - "Try fixing this problem by applying 'chmod o+x' to the '%s' " - "directory and its ancestors", - describeMountPt(*mpt).c_str(), srcpath, dstpath, srcpath); - } else { - PLOG_W("mount('%s') src:'%s' dstpath:'%s' failed", - describeMountPt(*mpt).c_str(), srcpath, dstpath); - if (mpt->fs_type.compare("proc") == 0) { - PLOG_W("procfs can only be mounted if the original /proc doesn't " - "have any other file-systems mounted on top of it (e.g. " - "/dev/null on top of /proc/kcore)"); - } - } - return false; - } else { - mpt->mounted = true; - } - - if (!mpt->src_content.empty() && unlink(srcpath) == -1) { - PLOG_W("unlink('%s')", srcpath); - } - return true; -} - -static bool remountPt(const mount_t& mpt) { - if (!mpt.mounted) { - return true; - } - if (mpt.is_symlink) { - return true; - } - - struct statvfs vfs; - if (TEMP_FAILURE_RETRY(statvfs(mpt.dst.c_str(), &vfs)) == -1) { - PLOG_W("statvfs('%s')", mpt.dst.c_str()); - return false; - } - - struct { - const unsigned long mount_flag; - const unsigned long vfs_flag; - } static const mountPairs[] = { - {MS_NOSUID, ST_NOSUID}, - {MS_NODEV, ST_NODEV}, - {MS_NOEXEC, ST_NOEXEC}, - {MS_SYNCHRONOUS, ST_SYNCHRONOUS}, - {MS_MANDLOCK, ST_MANDLOCK}, - {MS_NOATIME, ST_NOATIME}, - {MS_NODIRATIME, ST_NODIRATIME}, - {MS_RELATIME, ST_RELATIME}, - {MS_NOSYMFOLLOW, ST_NOSYMFOLLOW}, - }; - - const unsigned long per_mountpoint_flags = - MS_LAZYTIME | MS_MANDLOCK | MS_NOATIME | MS_NODEV | MS_NODIRATIME | MS_NOEXEC | - MS_NOSUID | MS_RELATIME | MS_RDONLY | MS_SYNCHRONOUS | MS_NOSYMFOLLOW; - unsigned long new_flags = MS_REMOUNT | MS_BIND | (mpt.flags & per_mountpoint_flags); - for (const auto& i : mountPairs) { - if (vfs.f_flag & i.vfs_flag) { - new_flags |= i.mount_flag; - } - } - - LOG_D("Re-mounting '%s' (flags:%s)", mpt.dst.c_str(), flagsToStr(new_flags).c_str()); - if (mount(mpt.dst.c_str(), mpt.dst.c_str(), NULL, new_flags, 0) == -1) { - PLOG_W("mount('%s', flags:%s)", mpt.dst.c_str(), flagsToStr(new_flags).c_str()); - return false; - } - - return true; -} - -static bool mkdirAndTest(const std::string& dir) { - if (mkdir(dir.c_str(), 0755) == -1 && errno != EEXIST) { - PLOG_D("Couldn't create '%s' directory", dir.c_str()); - return false; - } - if (access(dir.c_str(), R_OK) == -1) { - PLOG_W("access('%s', R_OK)", dir.c_str()); - return false; - } - LOG_D("Created accessible directory in '%s'", dir.c_str()); - return true; -} - -static std::unique_ptr getDir(nsj_t* nsj, const char* name) { - std::unique_ptr dir(new std::string); - - dir->assign("/run/user/").append(std::to_string(nsj->orig_uid)).append("/nsjail"); - if (mkdirAndTest(*dir)) { - dir->append("/").append(name); - if (mkdirAndTest(*dir)) { - return dir; - } - } - dir->assign("/run/user/") - .append("/nsjail.") - .append(std::to_string(nsj->orig_uid)) - .append(".") - .append(name); - if (mkdirAndTest(*dir)) { - return dir; - } - dir->assign("/tmp/nsjail.").append(std::to_string(nsj->orig_uid)).append(".").append(name); - if (mkdirAndTest(*dir)) { - return dir; - } - const char* tmp = getenv("TMPDIR"); - if (tmp) { - dir->assign(tmp) - .append("/") - .append("nsjail.") - .append(std::to_string(nsj->orig_uid)) - .append(".") - .append(name); - if (mkdirAndTest(*dir)) { - return dir; - } - } - dir->assign("/dev/shm/nsjail.") - .append(std::to_string(nsj->orig_uid)) - .append(".") - .append(name); - if (mkdirAndTest(*dir)) { - return dir; - } - dir->assign("/tmp/nsjail.") - .append(std::to_string(nsj->orig_uid)) - .append(".") - .append(name) - .append(".") - .append(std::to_string(util::rnd64())); - if (mkdirAndTest(*dir)) { - return dir; - } - - LOG_E("Couldn't create tmp directory of type '%s'", QC(name)); - return nullptr; -} - -static bool addMountPt(mount_t* mnt, const std::string& src, const std::string& dst, - const std::string& fstype, const std::string& options, uintptr_t flags, isDir_t is_dir, - bool is_mandatory, const std::string& src_env, const std::string& dst_env, - const std::string& src_content, bool is_symlink) { - if (!src_env.empty()) { - const char* e = getenv(src_env.c_str()); - if (e == nullptr) { - LOG_W("No such envar:%s", QC(src_env)); - return false; - } - mnt->src = e; - } - mnt->src.append(src); - - if (!dst_env.empty()) { - const char* e = getenv(dst_env.c_str()); - if (e == nullptr) { - LOG_W("No such envar:%s", QC(dst_env)); - return false; - } - mnt->dst = e; - } - mnt->dst.append(dst); - - mnt->fs_type = fstype; - mnt->options = options; - mnt->flags = flags; - mnt->is_symlink = is_symlink; - mnt->is_mandatory = is_mandatory; - mnt->mounted = false; - mnt->src_content = src_content; - - switch (is_dir) { - case NS_DIR_YES: - mnt->is_dir = true; - break; - case NS_DIR_NO: - mnt->is_dir = false; - break; - case NS_DIR_MAYBE: { - if (!src_content.empty()) { - mnt->is_dir = false; - } else if (mnt->src.empty()) { - mnt->is_dir = true; - } else if (mnt->flags & MS_BIND) { - mnt->is_dir = mnt::isDir(mnt->src.c_str()); - } else { - mnt->is_dir = true; - } - } break; - default: - LOG_E("Unknown is_dir value: %d", is_dir); - return false; - } - - return true; -} - static bool initNoCloneNs(nsj_t* nsj) { /* * If CLONE_NEWNS is not used, we would be changing the global mount namespace, so simply @@ -514,71 +168,26 @@ static bool initNoCloneNs(nsj_t* nsj) { } static bool initCloneNs(nsj_t* nsj) { - if (chdir("/") == -1) { - PLOG_E("chdir('/')"); - return false; - } - - std::unique_ptr destdir = getDir(nsj, "root"); - if (!destdir) { - LOG_E("Couldn't obtain root mount directories"); - return false; - } - - /* Make changes to / (recursively) private, to avoid changing the global mount ns */ - if (mount("/", "/", NULL, MS_REC | MS_PRIVATE, NULL) == -1) { - PLOG_E("mount('/', '/', NULL, MS_REC|MS_PRIVATE, NULL)"); - return false; - } - if (mount(NULL, destdir->c_str(), "tmpfs", 0, "size=16777216") == -1) { - PLOG_E("mount(%s, 'tmpfs')", QC(*destdir)); - return false; - } - - std::unique_ptr tmpdir = getDir(nsj, "tmp"); - if (!tmpdir) { - LOG_E("Couldn't obtain temporary mount directories"); - return false; - } - if (mount(NULL, tmpdir->c_str(), "tmpfs", 0, "size=16777216") == -1) { - PLOG_E("mount(%s, 'tmpfs')", QC(*tmpdir)); - return false; - } - std::vector mounted_mpts; - for (const auto& p : nsj->njc.mount()) { - uintptr_t flags = (p.rw() ? 0 : (uintptr_t)MS_RDONLY); - if (p.is_bind()) { - flags |= (MS_BIND | MS_REC | MS_PRIVATE); - } - if (p.nosuid()) { - flags |= MS_NOSUID; - } - if (p.nodev()) { - flags |= MS_NODEV; - } - if (p.noexec()) { - flags |= MS_NOEXEC; + defer { + for (auto& p : mounted_mpts) { + if (p.fd >= 0) { + close(p.fd); + p.fd = -1; + } } + }; - mount_t mpt; - if (!addMountPt(&mpt, p.src(), p.dst(), p.fstype(), p.options(), flags, - p.has_is_dir() ? (p.is_dir() ? NS_DIR_YES : NS_DIR_NO) : NS_DIR_MAYBE, - p.mandatory(), p.prefix_src_env(), p.prefix_dst_env(), p.src_content(), - p.is_symlink())) { - continue; - } + std::unique_ptr destdir; - if (!mountPt(&mpt, destdir->c_str(), tmpdir->c_str()) && mpt.is_mandatory) { - LOG_E("Couldn't mount %s", QC(mpt.dst)); - return false; - } - - mounted_mpts.push_back(mpt); + if (nsj->mnt_newapi) { + destdir = newapi::buildMountTree(nsj, &mounted_mpts); + } else { + destdir = legacy::buildMountTree(nsj, &mounted_mpts); } - if (umount2(tmpdir->c_str(), MNT_DETACH) == -1) { - PLOG_E("umount2(%s, MNT_DETACH)", QC(*tmpdir)); + if (!destdir) { + LOG_E("Failed to build mount tree"); return false; } @@ -601,6 +210,7 @@ static bool initCloneNs(nsj_t* nsj) { PLOG_E("umount2('/', MNT_DETACH)"); return false; } + } else { /* * pivot_root would normally un-mount the old root, however in certain cases this @@ -641,8 +251,14 @@ static bool initCloneNs(nsj_t* nsj) { } /* Remounting R/O, if needed. Only for mount points that were actually mounted */ - for (const auto& mpt : mounted_mpts) { - if (!remountPt(mpt) && mpt.is_mandatory) { + for (auto& mpt : mounted_mpts) { + bool success; + if (nsj->mnt_newapi) { + success = newapi::remountPt(mpt); + } else { + success = legacy::remountPt(mpt); + } + if (!success && mpt.mpt->mandatory()) { return false; } } diff --git a/mnt.h b/mnt.h index 152a3da..0e903e6 100644 --- a/mnt.h +++ b/mnt.h @@ -37,8 +37,20 @@ typedef enum { NS_DIR_MAYBE, } isDir_t; +/* Shared mount point structure used by both legacy and new API */ +struct mount_t { + const nsjail::MountPt* mpt; + std::string src; + std::string dst; + uintptr_t flags; + bool is_dir; + bool mounted; + int fd; /* Used by new API for deferred remount */ +}; + bool initNs(nsj_t* nsj); const std::string describeMountPt(const nsjail::MountPt& mpt); +const std::string flagsToStr(unsigned long flags); } // namespace mnt diff --git a/mnt_legacy.cc b/mnt_legacy.cc new file mode 100644 index 0000000..dfb6876 --- /dev/null +++ b/mnt_legacy.cc @@ -0,0 +1,432 @@ +/* + + nsjail - mount namespace routines using the legacy mount(2) API + ----------------------------------------- + + Copyright 2014 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +#include "mnt_legacy.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "logs.h" +#include "macros.h" +#include "mnt.h" +#include "util.h" + +namespace mnt { +namespace legacy { +#if !defined(MS_LAZYTIME) +#define MS_LAZYTIME (1 << 25) +#endif +#if !defined(ST_NOSYMFOLLOW) +#define ST_NOSYMFOLLOW 8192 +#endif + +namespace fs = std::filesystem; + +static bool tryCreateDir(const std::string& path, bool log_errors = true) { + if (mkdir(path.c_str(), 0755) == -1 && errno != EEXIST) { + if (log_errors) { + PLOG_D("mkdir('%s')", path.c_str()); + } + return false; + } + if (access(path.c_str(), R_OK) == -1) { + if (log_errors) { + PLOG_W("access('%s', R_OK)", path.c_str()); + } + return false; + } + LOG_D("Created directory '%s'", path.c_str()); + return true; +} + +static std::string findWritableDirUnderRoot() { + std::error_code ec; + for (const auto& entry : fs::directory_iterator("/", ec)) { + auto name = entry.path().filename().string(); + if (name == "." || name == "..") { + continue; + } + if (!entry.is_directory(ec)) { + continue; + } + if (access(entry.path().c_str(), W_OK | X_OK) == 0) { + return entry.path().string(); + } + } + return ""; +} + +static std::unique_ptr findWorkDir(nsj_t* nsj, const char* purpose) { + const std::string uid = std::to_string(nsj->orig_uid); + const std::string suffix = "nsjail." + uid + "." + purpose; + + /* Try standard locations */ + std::vector candidates = { + "/run/user/" + uid + "/nsjail/" + purpose, + "/run/user/" + suffix, + "/tmp/" + suffix, + "/dev/shm/" + suffix, + }; + + if (const char* tmpdir = getenv("TMPDIR")) { + candidates.insert(candidates.begin() + 3, std::string(tmpdir) + "/" + suffix); + } + + for (const auto& path : candidates) { + size_t last_slash = path.rfind('/'); + if (last_slash != std::string::npos && last_slash > 0) { + tryCreateDir(path.substr(0, last_slash), false); + } + if (tryCreateDir(path, true)) { + return std::make_unique(path); + } + } + + std::string root_dir = findWritableDirUnderRoot(); + if (!root_dir.empty()) { + std::string candidate = root_dir + "/" + suffix; + if (tryCreateDir(candidate, false)) { + return std::make_unique(candidate); + } + } + + std::string fallback = "/tmp/" + suffix + "." + std::to_string(util::rnd64()); + if (tryCreateDir(fallback, true)) { + return std::make_unique(fallback); + } + + LOG_E("Failed to create work directory for '%s'", purpose); + return nullptr; +} + +static bool isDirectory(const char* path) { + if (!path) { + return true; + } + struct stat st; + return stat(path, &st) == 0 && S_ISDIR(st.st_mode); +} + +static mount_t prepareMountPoint(const nsjail::MountPt& proto) { + mount_t mpt = { + .mpt = &proto, + .src = "", + .dst = "", + .flags = 0, + .is_dir = true, + .mounted = false, + .fd = -1, + }; + + if (!proto.prefix_src_env().empty()) { + if (const char* env = getenv(proto.prefix_src_env().c_str())) { + mpt.src = env; + } else { + LOG_W("Environment variable not set: %s", QC(proto.prefix_src_env())); + return mpt; + } + } + mpt.src += proto.src(); + + if (!proto.prefix_dst_env().empty()) { + if (const char* env = getenv(proto.prefix_dst_env().c_str())) { + mpt.dst = env; + } else { + LOG_W("Environment variable not set: %s", QC(proto.prefix_dst_env())); + return mpt; + } + } + mpt.dst += proto.dst(); + + mpt.flags = proto.rw() ? 0 : (uintptr_t)MS_RDONLY; + if (proto.is_bind()) { + mpt.flags |= MS_BIND | MS_REC | MS_PRIVATE; + } + if (proto.nosuid()) { + mpt.flags |= MS_NOSUID; + } + if (proto.nodev()) { + mpt.flags |= MS_NODEV; + } + if (proto.noexec()) { + mpt.flags |= MS_NOEXEC; + } + + if (proto.has_is_dir()) { + mpt.is_dir = proto.is_dir(); + } else if (!proto.src_content().empty()) { + mpt.is_dir = false; + } else if (mpt.src.empty()) { + mpt.is_dir = true; + } else if (mpt.flags & MS_BIND) { + mpt.is_dir = isDirectory(mpt.src.c_str()); + } else { + mpt.is_dir = true; + } + + return mpt; +} + +static int tryMountRW(mount_t* mpt, const char* src, const char* dst) { + int res = mount(src, dst, mpt->mpt->fstype().c_str(), mpt->flags & ~MS_RDONLY, + mpt->mpt->options().c_str()); + + if (res == -1 && errno == EPERM && (mpt->flags & MS_RDONLY)) { + LOG_W("mount('%s' -> '%s'): RW failed, falling back to RO", src, dst); + res = mount( + src, dst, mpt->mpt->fstype().c_str(), mpt->flags, mpt->mpt->options().c_str()); + } + return res; +} + +static bool createMountTarget(const std::string& path, bool is_dir) { + if (is_dir) { + if (mkdir(path.c_str(), 0711) == -1 && errno != EEXIST) { + PLOG_W("mkdir('%s')", path.c_str()); + return false; + } + } else { + int fd = + TEMP_FAILURE_RETRY(open(path.c_str(), O_CREAT | O_RDONLY | O_CLOEXEC, 0644)); + if (fd == -1) { + PLOG_W("open('%s', O_CREAT)", path.c_str()); + return false; + } + close(fd); + } + return true; +} + +static bool mountSymlink(mount_t* mpt, const std::string& dstpath) { + LOG_D("Creating symlink: %s -> %s", mpt->src.c_str(), dstpath.c_str()); + if (symlink(mpt->src.c_str(), dstpath.c_str()) == -1) { + if (mpt->mpt->mandatory()) { + PLOG_E("symlink('%s' -> '%s')", mpt->src.c_str(), dstpath.c_str()); + return false; + } + PLOG_W("symlink('%s' -> '%s') failed (non-mandatory)", mpt->src.c_str(), + dstpath.c_str()); + } + return true; +} + +static bool mountWithDynamicContent( + mount_t* mpt, const std::string& dstpath, const std::string& tmpdir) { + static uint64_t counter = 0; + std::string srcpath = tmpdir + "/dynamic." + std::to_string(++counter); + defer { + unlink(srcpath.c_str()); + }; + + int fd = TEMP_FAILURE_RETRY( + open(srcpath.c_str(), O_CREAT | O_EXCL | O_CLOEXEC | O_WRONLY, 0644)); + if (fd == -1) { + PLOG_W("open('%s', O_CREAT)", srcpath.c_str()); + return false; + } + + const auto& content = mpt->mpt->src_content(); + bool write_ok = util::writeToFd(fd, content.data(), content.length()); + close(fd); + + if (!write_ok) { + LOG_W("Failed to write %zu bytes to '%s'", content.length(), srcpath.c_str()); + return false; + } + + mpt->flags |= MS_BIND | MS_REC | MS_PRIVATE; + if (tryMountRW(mpt, srcpath.c_str(), dstpath.c_str()) == -1) { + PLOG_W("mount('%s' -> '%s')", srcpath.c_str(), dstpath.c_str()); + return false; + } + + mpt->mounted = true; + return true; +} + +static bool mountSinglePoint(mount_t* mpt, const char* newroot, const char* tmpdir) { + LOG_D("Mounting (legacy): %s", mnt::describeMountPt(*mpt->mpt).c_str()); + + const std::string dstpath = std::string(newroot) + "/" + mpt->dst; + std::string srcpath = mpt->src.empty() ? "none" : mpt->src; + + if (!util::createDirRecursively(dstpath.c_str())) { + LOG_W("Failed to create parent directories for '%s'", dstpath.c_str()); + return false; + } + + if (mpt->mpt->is_symlink()) { + return mountSymlink(mpt, dstpath); + } + + if (!createMountTarget(dstpath, mpt->is_dir)) { + return false; + } + + if (!mpt->mpt->src_content().empty()) { + return mountWithDynamicContent(mpt, dstpath, tmpdir); + } + + if (tryMountRW(mpt, srcpath.c_str(), dstpath.c_str()) == -1) { + if (errno == EACCES) { + PLOG_W("mount('%s' -> '%s'): try 'chmod o+x' on source path", + srcpath.c_str(), dstpath.c_str()); + } else if (mpt->mpt->fstype() == "proc") { + PLOG_W("mount('%s' -> '%s'): procfs mount may fail if /proc has " + "overmounts (e.g., /dev/null on /proc/kcore)", + srcpath.c_str(), dstpath.c_str()); + } else { + PLOG_W("mount('%s' -> '%s')", srcpath.c_str(), dstpath.c_str()); + } + return false; + } + + mpt->mounted = true; + return true; +} + +static unsigned long computeRemountFlags(const mount_t& mpt, const struct statvfs& vfs) { + struct { + const unsigned long mount_flag; + const unsigned long vfs_flag; + } static const mountPairs[] = { + {MS_NOSUID, ST_NOSUID}, + {MS_NODEV, ST_NODEV}, + {MS_NOEXEC, ST_NOEXEC}, + {MS_SYNCHRONOUS, ST_SYNCHRONOUS}, + {MS_MANDLOCK, ST_MANDLOCK}, + {MS_NOATIME, ST_NOATIME}, + {MS_NODIRATIME, ST_NODIRATIME}, + {MS_RELATIME, ST_RELATIME}, + {MS_NOSYMFOLLOW, ST_NOSYMFOLLOW}, + }; + + const unsigned long per_mountpoint_flags = + MS_LAZYTIME | MS_MANDLOCK | MS_NOATIME | MS_NODEV | MS_NODIRATIME | MS_NOEXEC | + MS_NOSUID | MS_RELATIME | MS_RDONLY | MS_SYNCHRONOUS | MS_NOSYMFOLLOW; + + unsigned long flags = MS_REMOUNT | MS_BIND | (mpt.flags & per_mountpoint_flags); + for (const auto& i : mountPairs) { + if (vfs.f_flag & i.vfs_flag) { + flags |= i.mount_flag; + } + } + return flags; +} + +bool remountPt(mnt::mount_t& mpt) { + if (!mpt.mounted || mpt.mpt->is_symlink()) { + return true; + } + + struct statvfs vfs; + if (TEMP_FAILURE_RETRY(statvfs(mpt.dst.c_str(), &vfs)) == -1) { + PLOG_W("statvfs('%s')", mpt.dst.c_str()); + return false; + } + + unsigned long flags = computeRemountFlags(mpt, vfs); + LOG_D("Remounting '%s' with flags: %s", mpt.dst.c_str(), mnt::flagsToStr(flags).c_str()); + + if (mount(mpt.dst.c_str(), mpt.dst.c_str(), nullptr, flags, nullptr) == -1) { + PLOG_W("mount('%s', flags=%s)", mpt.dst.c_str(), mnt::flagsToStr(flags).c_str()); + return false; + } + return true; +} + +std::unique_ptr buildMountTree(nsj_t* nsj, std::vector* mounted_mpts) { + if (chdir("/") == -1) { + PLOG_E("chdir('/')"); + return nullptr; + } + + const size_t tmpfsSize = 16 * 1024 * 1024; + + auto destdir = findWorkDir(nsj, "root"); + if (!destdir) { + return nullptr; + } + + if (mount("/", "/", nullptr, MS_REC | MS_PRIVATE, nullptr) == -1) { + PLOG_E("mount('/', MS_REC|MS_PRIVATE)"); + return nullptr; + } + + if (mount(nullptr, destdir->c_str(), "tmpfs", 0, + ("size=" + std::to_string(tmpfsSize)).c_str()) == -1) { + PLOG_E("mount('%s', tmpfs)", destdir->c_str()); + return nullptr; + } + + auto tmpdir = findWorkDir(nsj, "tmp"); + if (!tmpdir) { + return nullptr; + } + + if (mount(nullptr, tmpdir->c_str(), "tmpfs", 0, + ("size=" + std::to_string(tmpfsSize)).c_str()) == -1) { + PLOG_E("mount('%s', tmpfs)", tmpdir->c_str()); + return nullptr; + } + + for (const auto& proto : nsj->njc.mount()) { + mount_t mpt = prepareMountPoint(proto); + + if (!mountSinglePoint(&mpt, destdir->c_str(), tmpdir->c_str())) { + if (mpt.mpt->mandatory()) { + LOG_E("Failed to mount mandatory point: %s", QC(mpt.dst)); + return nullptr; + } + } + mounted_mpts->push_back(mpt); + } + + if (!nsj->is_root_rw) { + if (mount(destdir->c_str(), destdir->c_str(), nullptr, MS_REMOUNT | MS_RDONLY, + nullptr) == -1) { + PLOG_E("mount('%s', MS_REMOUNT|MS_RDONLY)", destdir->c_str()); + return nullptr; + } + } + + if (umount2(tmpdir->c_str(), MNT_DETACH) == -1) { + PLOG_E("umount2('%s', MNT_DETACH)", tmpdir->c_str()); + return nullptr; + } + + return destdir; +} + +} // namespace legacy +} // namespace mnt diff --git a/mnt_legacy.h b/mnt_legacy.h new file mode 100644 index 0000000..762d4d1 --- /dev/null +++ b/mnt_legacy.h @@ -0,0 +1,20 @@ +#ifndef NS_MNT_LEGACY_H +#define NS_MNT_LEGACY_H + +#include +#include +#include + +#include "mnt.h" +#include "nsjail.h" + +namespace mnt { +namespace legacy { + +std::unique_ptr buildMountTree(nsj_t* nsj, std::vector* mounted_mpts); +bool remountPt(mnt::mount_t& mpt); + +} // namespace legacy +} // namespace mnt + +#endif /* NS_MNT_LEGACY_H */ diff --git a/mnt_newapi.cc b/mnt_newapi.cc new file mode 100644 index 0000000..5428f9a --- /dev/null +++ b/mnt_newapi.cc @@ -0,0 +1,555 @@ +/* +| +| nsjail - mount namespace routines using the new mount API (fsopen/fsmount/move_mount) +| ----------------------------------------- +| +| Copyright 2025 Google Inc. All Rights Reserved. +| +| Licensed under the Apache License, Version 2.0 (the "License"); +| you may not use this file except in compliance with the License. +| You may obtain a copy of the License at +| +| http://www.apache.org/licenses/LICENSE-2.0 +| +| Unless required by applicable law or agreed to in writing, software +| distributed under the License is distributed on an "AS IS" BASIS, +| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +| See the License for the specific language governing permissions and +| limitations under the License. +| +*/ + +#include "mnt_newapi.h" + +#include +#include +#include + +#include "logs.h" +#include "util.h" + +/* + * Compile-time feature detection for the new mount API. + * Requires kernel headers with fsopen/fsconfig/fsmount/move_mount support. + */ +#if defined(__NR_fsopen) && defined(__NR_fsconfig) && defined(__NR_fsmount) && \ + defined(__NR_move_mount) && defined(__NR_open_tree) && defined(__NR_mount_setattr) && \ + defined(FSOPEN_CLOEXEC) && defined(FSMOUNT_CLOEXEC) && defined(MOVE_MOUNT_F_EMPTY_PATH) && \ + defined(MOUNT_ATTR_RDONLY) && defined(MOUNT_ATTR_NOSUID) && defined(MOUNT_ATTR_NODEV) && \ + defined(MOUNT_ATTR_NOEXEC) && defined(AT_EMPTY_PATH) && defined(AT_RECURSIVE) +#define MNT_NEWAPI_SUPPORTED 1 +#endif + +#if !defined(MNT_NEWAPI_SUPPORTED) + +namespace mnt { +namespace newapi { + +bool isAvailable() { + LOG_W("New mount API unavailable: missing compile-time support"); + return false; +} + +bool remountPt(mnt::mount_t&) { + return false; +} + +std::unique_ptr buildMountTree(nsj_t*, std::vector*) { + return nullptr; +} + +} // namespace newapi +} // namespace mnt + +#else /* MNT_NEWAPI_SUPPORTED */ + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "macros.h" +#include "mnt.h" + +namespace mnt { +namespace newapi { + +namespace fs = std::filesystem; + +static bool applyMountFlags(int fd, uintptr_t flags) { + struct mount_attr attr = {}; + + if (flags & MS_RDONLY) { + attr.attr_set |= MOUNT_ATTR_RDONLY; + } + if (flags & MS_NOSUID) { + attr.attr_set |= MOUNT_ATTR_NOSUID; + } + if (flags & MS_NODEV) { + attr.attr_set |= MOUNT_ATTR_NODEV; + } + if (flags & MS_NOEXEC) { + attr.attr_set |= MOUNT_ATTR_NOEXEC; + } + + if (util::syscall(__NR_mount_setattr, (uintptr_t)fd, (uintptr_t)"", + (uintptr_t)AT_EMPTY_PATH, (uintptr_t)&attr, sizeof(attr)) < 0) { + PLOG_W("mount_setattr(fd=%d, flags=0x%" PRIx64 ")", fd, (uint64_t)attr.attr_set); + return false; + } + return true; +} + +static std::string findFirstDirUnderRoot() { + std::error_code ec; + for (const auto& entry : fs::directory_iterator("/", ec)) { + auto name = entry.path().filename().string(); + if (name == "." || name == "..") { + continue; + } + if (entry.is_directory(ec) && !entry.is_symlink(ec)) { + return entry.path().string(); + } + } + return ""; +} + +static bool createDirAt(int dir_fd, const char* path, mode_t mode) { + path = util::stripLeadingSlashes(path); + if (!path[0]) { + return true; + } + + std::string cumulative; + for (const auto& component : util::strSplit(path, '/')) { + if (component.empty()) { + continue; + } + + if (!cumulative.empty()) { + cumulative += '/'; + } + cumulative += component; + + if (mkdirat(dir_fd, cumulative.c_str(), mode) == -1 && errno != EEXIST) { + PLOG_W("mkdirat(%d, '%s')", dir_fd, cumulative.c_str()); + return false; + } + } + return true; +} + +static int createDetachedTmpfs(size_t size) { + int fs_fd = util::syscall(__NR_fsopen, (uintptr_t)"tmpfs", (uintptr_t)FSOPEN_CLOEXEC); + if (fs_fd < 0) { + PLOG_W("fsopen('tmpfs')"); + return -1; + } + defer { + close(fs_fd); + }; + + const std::string size_str = std::to_string(size); + if (util::syscall(__NR_fsconfig, (uintptr_t)fs_fd, (uintptr_t)FSCONFIG_SET_STRING, + (uintptr_t)"size", (uintptr_t)size_str.c_str(), (uintptr_t)0) < 0) { + PLOG_W("fsconfig(size=%s)", size_str.c_str()); + return -1; + } + + if (util::syscall(__NR_fsconfig, (uintptr_t)fs_fd, (uintptr_t)FSCONFIG_CMD_CREATE, + (uintptr_t)nullptr, (uintptr_t)nullptr, (uintptr_t)0) < 0) { + PLOG_W("fsconfig(CMD_CREATE)"); + return -1; + } + + int mnt_fd = + util::syscall(__NR_fsmount, (uintptr_t)fs_fd, (uintptr_t)FSMOUNT_CLOEXEC, (uintptr_t)0); + if (mnt_fd < 0) { + PLOG_W("fsmount('tmpfs')"); + } + return mnt_fd; +} + +static int createFilesystemMount(const mount_t& mpt) { + int fs_fd = util::syscall( + __NR_fsopen, (uintptr_t)mpt.mpt->fstype().c_str(), (uintptr_t)FSOPEN_CLOEXEC); + if (fs_fd < 0) { + PLOG_W("fsopen('%s')", mpt.mpt->fstype().c_str()); + return -1; + } + defer { + close(fs_fd); + }; + + if (!mpt.src.empty() && mpt.src != "none") { + if (util::syscall(__NR_fsconfig, (uintptr_t)fs_fd, (uintptr_t)FSCONFIG_SET_STRING, + (uintptr_t)"source", (uintptr_t)mpt.src.c_str(), (uintptr_t)0) < 0) { + PLOG_W("fsconfig(source='%s')", mpt.src.c_str()); + return -1; + } + } + + if (mpt.mpt->has_options()) { + for (const auto& opt : util::strSplit(mpt.mpt->options(), ',')) { + if (opt.empty()) { + continue; + } + if (util::syscall(__NR_fsconfig, (uintptr_t)fs_fd, + (uintptr_t)FSCONFIG_SET_FLAG, (uintptr_t)opt.c_str(), + (uintptr_t)nullptr, (uintptr_t)0) < 0) { + PLOG_W("fsconfig(flag='%s')", opt.c_str()); + return -1; + } + } + } + + if (util::syscall(__NR_fsconfig, (uintptr_t)fs_fd, (uintptr_t)FSCONFIG_CMD_CREATE, + (uintptr_t)nullptr, (uintptr_t)nullptr, (uintptr_t)0) < 0) { + PLOG_W("fsconfig(CMD_CREATE)"); + return -1; + } + + int mnt_fd = + util::syscall(__NR_fsmount, (uintptr_t)fs_fd, (uintptr_t)FSMOUNT_CLOEXEC, (uintptr_t)0); + if (mnt_fd < 0) { + PLOG_W("fsmount('%s')", mpt.mpt->fstype().c_str()); + } + return mnt_fd; +} + +static bool mountSymlinkAt(mount_t* mpt, int root_fd, const char* rel_dst) { + LOG_D("Creating symlink: %s -> %s (fd-relative)", mpt->src.c_str(), rel_dst); + if (symlinkat(mpt->src.c_str(), root_fd, rel_dst) == -1) { + if (mpt->mpt->mandatory()) { + PLOG_E("symlinkat('%s' -> '%s')", mpt->src.c_str(), rel_dst); + return false; + } + PLOG_W("symlinkat('%s' -> '%s') failed (non-mandatory)", mpt->src.c_str(), rel_dst); + } + return true; +} + +static bool mountDynamicContentAt(mount_t* mpt, int root_fd, const char* rel_dst) { + static uint64_t counter = 0; + std::string src_rel = ".dyn." + std::to_string(++counter); + + int src_fd = + openat(root_fd, src_rel.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0644); + if (src_fd < 0) { + PLOG_W("openat(root_fd, '%s', O_CREAT)", src_rel.c_str()); + return false; + } + + const auto& content = mpt->mpt->src_content(); + bool ok = util::writeToFd(src_fd, content.data(), content.length()); + close(src_fd); + if (!ok) { + LOG_W("Failed to write %zu bytes for dynamic content '%s'", content.length(), + rel_dst); + unlinkat(root_fd, src_rel.c_str(), 0); + return false; + } + + int mnt_fd = + syscall(__NR_open_tree, root_fd, src_rel.c_str(), OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC); + if (mnt_fd < 0) { + PLOG_W("open_tree('%s')", src_rel.c_str()); + unlinkat(root_fd, src_rel.c_str(), 0); + return false; + } + + if (!applyMountFlags(mnt_fd, mpt->flags & ~MS_RDONLY)) { + LOG_W("Failed to apply mount flags to '%s'", rel_dst); + } + + if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)root_fd, + (uintptr_t)rel_dst, (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) { + PLOG_W("move_mount('%s' -> '%s')", src_rel.c_str(), rel_dst); + close(mnt_fd); + unlinkat(root_fd, src_rel.c_str(), 0); + return false; + } + close(mnt_fd); + + if (unlinkat(root_fd, src_rel.c_str(), 0) == -1) { + PLOG_W("unlinkat(root_fd, '%s')", src_rel.c_str()); + } + + mpt->fd = syscall(__NR_open_tree, root_fd, rel_dst, (unsigned int)OPEN_TREE_CLOEXEC); + if (mpt->fd < 0) { + PLOG_W("open_tree(root_fd, '%s')", rel_dst); + return false; + } + mpt->mounted = true; + return true; +} + +static bool doBindMountAt(mount_t* mpt, int root_fd, const char* rel_dst) { + unsigned int flags = OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC; + if (mpt->flags & MS_REC) { + flags |= AT_RECURSIVE; + } + + LOG_D("open_tree('%s', flags=0x%x)", mpt->src.c_str(), flags); + int mnt_fd = syscall(__NR_open_tree, AT_FDCWD, mpt->src.c_str(), flags); + if (mnt_fd < 0) { + PLOG_W("open_tree('%s')", mpt->src.c_str()); + return false; + } + + /* Apply non-RO flags now; RO applied later via remount */ + if (!applyMountFlags(mnt_fd, mpt->flags & ~MS_RDONLY)) { + LOG_W("Failed to apply mount flags to '%s'", rel_dst); + } + + if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)root_fd, + (uintptr_t)rel_dst, (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) { + PLOG_W("move_mount('%s' -> '%s')", mpt->src.c_str(), rel_dst); + close(mnt_fd); + return false; + } + close(mnt_fd); + + /* Re-acquire fd for later remount (kernel 6.13+ requires this) */ + mpt->fd = syscall(__NR_open_tree, root_fd, rel_dst, (unsigned int)OPEN_TREE_CLOEXEC); + mpt->mounted = true; + return true; +} + +static bool mountSinglePointAt(mount_t* mpt, int root_fd) { + LOG_D("Mounting (new API): %s", mnt::describeMountPt(*mpt->mpt).c_str()); + + const char* rel_dst = util::stripLeadingSlashes(mpt->dst.c_str()); + if (!rel_dst[0]) { + rel_dst = "."; + } + + const char* last_slash = strrchr(rel_dst, '/'); + if (last_slash && last_slash != rel_dst) { + std::string parent(rel_dst, last_slash - rel_dst); + if (!createDirAt(root_fd, parent.c_str(), 0755)) { + LOG_W("Failed to create parent directories for '%s'", rel_dst); + return false; + } + } + + if (mpt->mpt->is_symlink()) { + return mountSymlinkAt(mpt, root_fd, rel_dst); + } + + if (mpt->is_dir) { + if (strcmp(rel_dst, ".") != 0 && mkdirat(root_fd, rel_dst, 0711) == -1 && + errno != EEXIST) { + PLOG_W("mkdirat(root_fd, '%s')", rel_dst); + } + } else { + int fd = openat(root_fd, rel_dst, O_CREAT | O_RDONLY | O_CLOEXEC, 0644); + if (fd < 0) { + PLOG_W("openat(root_fd, '%s', O_CREAT)", rel_dst); + } else { + close(fd); + } + } + + if (!mpt->mpt->src_content().empty()) { + return mountDynamicContentAt(mpt, root_fd, rel_dst); + } + + if (mpt->flags & MS_BIND) { + return doBindMountAt(mpt, root_fd, rel_dst); + } + + int mnt_fd = createFilesystemMount(*mpt); + if (mnt_fd < 0) { + return false; + } + + if (!applyMountFlags(mnt_fd, mpt->flags & ~MS_RDONLY)) { + LOG_W("Failed to apply mount flags to '%s'", rel_dst); + } + + if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)root_fd, + (uintptr_t)rel_dst, (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) { + PLOG_W("move_mount() for '%s'", rel_dst); + close(mnt_fd); + return false; + } + close(mnt_fd); + + mpt->fd = syscall(__NR_open_tree, root_fd, rel_dst, (unsigned int)OPEN_TREE_CLOEXEC); + mpt->mounted = true; + return true; +} + +static mount_t prepareMountPoint(const nsjail::MountPt& proto) { + mount_t mpt = { + .mpt = &proto, + .src = "", + .dst = "", + .flags = 0, + .is_dir = true, + .mounted = false, + .fd = -1, + }; + + if (!proto.prefix_src_env().empty()) { + if (const char* env = getenv(proto.prefix_src_env().c_str())) { + mpt.src = env; + } else { + LOG_W("Environment variable not set: %s", QC(proto.prefix_src_env())); + return mpt; + } + } + mpt.src += proto.src(); + + if (!proto.prefix_dst_env().empty()) { + if (const char* env = getenv(proto.prefix_dst_env().c_str())) { + mpt.dst = env; + } else { + LOG_W("Environment variable not set: %s", QC(proto.prefix_dst_env())); + return mpt; + } + } + mpt.dst += proto.dst(); + + mpt.flags = proto.rw() ? 0 : (uintptr_t)MS_RDONLY; + if (proto.is_bind()) { + mpt.flags |= MS_BIND | MS_REC | MS_PRIVATE; + } + if (proto.nosuid()) { + mpt.flags |= MS_NOSUID; + } + if (proto.nodev()) { + mpt.flags |= MS_NODEV; + } + if (proto.noexec()) { + mpt.flags |= MS_NOEXEC; + } + + if (proto.has_is_dir()) { + mpt.is_dir = proto.is_dir(); + } else if (!proto.src_content().empty()) { + mpt.is_dir = false; + } else if (mpt.src.empty()) { + mpt.is_dir = true; + } else if (mpt.flags & MS_BIND) { + struct stat st; + mpt.is_dir = (stat(mpt.src.c_str(), &st) == 0 && S_ISDIR(st.st_mode)); + } else { + mpt.is_dir = true; + } + + return mpt; +} + +bool isAvailable() { + if (util::kernelVersionAtLeast(6, 3, 0)) { + LOG_D("New mount API available (kernel >= 6.3)"); + return true; + } + LOG_W("New mount API unavailable (kernel < 6.3)"); + return false; +} + +bool remountPt(mnt::mount_t& mpt) { + if (!mpt.mounted || mpt.mpt->is_symlink() || mpt.fd < 0) { + return true; + } + + if (!applyMountFlags(mpt.fd, mpt.flags)) { + LOG_W("Failed to apply final flags to '%s'", mpt.dst.c_str()); + return false; + } + + close(mpt.fd); + mpt.fd = -1; + return true; +} + +std::unique_ptr buildMountTree(nsj_t* nsj, std::vector* mounted_mpts) { + if (chdir("/") == -1) { + PLOG_E("chdir('/')"); + return nullptr; + } + + /* Make root mount private recursively */ + if (mount("/", "/", nullptr, MS_REC | MS_PRIVATE, nullptr) == -1) { + PLOG_E("mount('/', MS_REC|MS_PRIVATE)"); + return nullptr; + } + + int root_mfd = createDetachedTmpfs(16 * 1024 * 1024); + if (root_mfd < 0) { + LOG_E("Failed to create root tmpfs"); + return nullptr; + } + LOG_D("Created detached root tmpfs (fd=%d)", root_mfd); + + if (!applyMountFlags(root_mfd, 0)) { + LOG_W("mount_setattr(root_mfd, 0) failed"); + } + + std::string attachdir = findFirstDirUnderRoot(); + if (attachdir.empty()) { + LOG_E("No directory found under / to mount new root"); + close(root_mfd); + return nullptr; + } + + if (util::syscall(__NR_move_mount, (uintptr_t)root_mfd, (uintptr_t)"", (uintptr_t)AT_FDCWD, + (uintptr_t)attachdir.c_str(), (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) { + PLOG_E("move_mount(root_fd -> '%s')", attachdir.c_str()); + close(root_mfd); + return nullptr; + } + close(root_mfd); + + int root_fd = + openat(AT_FDCWD, attachdir.c_str(), O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY); + if (root_fd < 0) { + PLOG_E("openat('%s')", attachdir.c_str()); + return nullptr; + } + defer { + close(root_fd); + }; + + /* Build entire mount tree using fd-relative operations */ + for (const auto& proto : nsj->njc.mount()) { + mount_t mpt = prepareMountPoint(proto); + + if (!mountSinglePointAt(&mpt, root_fd)) { + if (mpt.mpt->mandatory()) { + LOG_E("Failed to mount mandatory point: %s", QC(mpt.dst)); + return nullptr; + } + } + mounted_mpts->push_back(mpt); + } + + /* Apply RO to root if needed */ + if (!nsj->is_root_rw) { + struct mount_attr ro_attr = {}; + ro_attr.attr_set = MOUNT_ATTR_RDONLY; + if (util::syscall(__NR_mount_setattr, (uintptr_t)root_fd, (uintptr_t)"", + (uintptr_t)AT_EMPTY_PATH, (uintptr_t)&ro_attr, sizeof(ro_attr)) < 0) { + PLOG_W("mount_setattr(root_fd, MOUNT_ATTR_RDONLY)"); + LOG_W("Root filesystem may still be writable"); + } + } + + return std::make_unique(attachdir); +} + +} // namespace newapi + +} // namespace mnt + +#endif /* MNT_NEWAPI_SUPPORTED */ diff --git a/mnt_newapi.h b/mnt_newapi.h new file mode 100644 index 0000000..c6cecae --- /dev/null +++ b/mnt_newapi.h @@ -0,0 +1,21 @@ +#ifndef NS_MNT_NEWAPI_H +#define NS_MNT_NEWAPI_H + +#include +#include +#include + +#include "mnt.h" +#include "nsjail.h" + +namespace mnt { +namespace newapi { + +bool isAvailable(); +std::unique_ptr buildMountTree(nsj_t* nsj, std::vector* mounted_mpts); +bool remountPt(mnt::mount_t& mpt); + +} // namespace newapi +} // namespace mnt + +#endif /* NS_MNT_NEWAPI_H */ diff --git a/nsjail.h b/nsjail.h index df28740..edabbb0 100644 --- a/nsjail.h +++ b/nsjail.h @@ -98,6 +98,7 @@ struct nsj_t { std::string chroot; std::string proc_path; bool is_root_rw; + bool mnt_newapi; bool is_proc_rw; struct sock_fprog seccomp_fprog; }; diff --git a/util.cc b/util.cc index 3303da3..ac003ce 100644 --- a/util.cc +++ b/util.cc @@ -40,6 +40,7 @@ #include #include #include +#include #include #include @@ -433,4 +434,33 @@ bool makeRangeCOE(unsigned int first [[maybe_unused]], unsigned int last [[maybe return false; } +const char* stripLeadingSlashes(const char* path) { + while (*path == '/') { + path++; + } + return path; +} + +bool kernelVersionAtLeast(int major, int minor, int patch) { + struct utsname uts; + if (uname(&uts) == -1) { + PLOG_W("uname()"); + return false; + } + + int kmajor = 0, kminor = 0, kpatch = 0; + if (sscanf(uts.release, "%d.%d.%d", &kmajor, &kminor, &kpatch) < 2) { + LOG_W("Couldn't parse kernel version from '%s'", uts.release); + return false; + } + + if (kmajor != major) { + return kmajor > major; + } + if (kminor != minor) { + return kminor > minor; + } + return kpatch >= patch; +} + } // namespace util diff --git a/util.h b/util.h index f691351..cb6a6f3 100644 --- a/util.h +++ b/util.h @@ -75,6 +75,8 @@ long syscall(long sysno, uintptr_t a0 = 0, uintptr_t a1 = 0, uintptr_t a2 = 0, u long setrlimit(int res, const struct rlimit64& newlim); long getrlimit(int res, struct rlimit64* curlim); bool makeRangeCOE(unsigned int first, unsigned int last); +const char* stripLeadingSlashes(const char* path); +bool kernelVersionAtLeast(int major, int minor, int patch); } // namespace util