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.

This commit is contained in:
Robert Swiecki
2026-01-31 16:05:58 +01:00
parent 841232d052
commit d8ea726682
12 changed files with 1144 additions and 426 deletions

View File

@@ -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

View File

@@ -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<nsj_t> 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<nsj_t> 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;

View File

@@ -24,6 +24,8 @@
#include <unistd.h>
#include <utility>
#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 <typename F>
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 */

442
mnt.cc
View File

@@ -21,6 +21,7 @@
#include "mnt.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
@@ -41,27 +42,17 @@
#include <memory>
#include <string>
#include <vector>
#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<std::string> getDir(nsj_t* nsj, const char* name) {
std::unique_ptr<std::string> 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<std::string> 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<std::string> 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<mount_t> 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);
defer {
for (auto& p : mounted_mpts) {
if (p.fd >= 0) {
close(p.fd);
p.fd = -1;
}
if (p.nosuid()) {
flags |= MS_NOSUID;
}
if (p.nodev()) {
flags |= MS_NODEV;
}
if (p.noexec()) {
flags |= MS_NOEXEC;
};
std::unique_ptr<std::string> destdir;
if (nsj->mnt_newapi) {
destdir = newapi::buildMountTree(nsj, &mounted_mpts);
} else {
destdir = legacy::buildMountTree(nsj, &mounted_mpts);
}
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;
}
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 (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;
}
}

12
mnt.h
View File

@@ -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

432
mnt_legacy.cc Normal file
View File

@@ -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 <errno.h>
#include <fcntl.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <cstdint>
#include <cstdio>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#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<std::string> 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<std::string> 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<std::string>(path);
}
}
std::string root_dir = findWritableDirUnderRoot();
if (!root_dir.empty()) {
std::string candidate = root_dir + "/" + suffix;
if (tryCreateDir(candidate, false)) {
return std::make_unique<std::string>(candidate);
}
}
std::string fallback = "/tmp/" + suffix + "." + std::to_string(util::rnd64());
if (tryCreateDir(fallback, true)) {
return std::make_unique<std::string>(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<std::string> buildMountTree(nsj_t* nsj, std::vector<mnt::mount_t>* 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

20
mnt_legacy.h Normal file
View File

@@ -0,0 +1,20 @@
#ifndef NS_MNT_LEGACY_H
#define NS_MNT_LEGACY_H
#include <memory>
#include <string>
#include <vector>
#include "mnt.h"
#include "nsjail.h"
namespace mnt {
namespace legacy {
std::unique_ptr<std::string> buildMountTree(nsj_t* nsj, std::vector<mnt::mount_t>* mounted_mpts);
bool remountPt(mnt::mount_t& mpt);
} // namespace legacy
} // namespace mnt
#endif /* NS_MNT_LEGACY_H */

555
mnt_newapi.cc Normal file
View File

@@ -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 <fcntl.h>
#include <linux/mount.h>
#include <sys/syscall.h>
#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<std::string> buildMountTree(nsj_t*, std::vector<mnt::mount_t>*) {
return nullptr;
}
} // namespace newapi
} // namespace mnt
#else /* MNT_NEWAPI_SUPPORTED */
#include <dirent.h>
#include <errno.h>
#include <inttypes.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstdint>
#include <filesystem>
#include <string>
#include <vector>
#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<std::string> buildMountTree(nsj_t* nsj, std::vector<mnt::mount_t>* 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<std::string>(attachdir);
}
} // namespace newapi
} // namespace mnt
#endif /* MNT_NEWAPI_SUPPORTED */

21
mnt_newapi.h Normal file
View File

@@ -0,0 +1,21 @@
#ifndef NS_MNT_NEWAPI_H
#define NS_MNT_NEWAPI_H
#include <memory>
#include <string>
#include <vector>
#include "mnt.h"
#include "nsjail.h"
namespace mnt {
namespace newapi {
bool isAvailable();
std::unique_ptr<std::string> buildMountTree(nsj_t* nsj, std::vector<mnt::mount_t>* mounted_mpts);
bool remountPt(mnt::mount_t& mpt);
} // namespace newapi
} // namespace mnt
#endif /* NS_MNT_NEWAPI_H */

View File

@@ -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;
};

30
util.cc
View File

@@ -40,6 +40,7 @@
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <time.h>
#include <unistd.h>
@@ -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

2
util.h
View File

@@ -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