Merge pull request #292 from M0nd0R/fix-mount-staging-escape

Reject path traversal in mount destinations during jail staging
This commit is contained in:
Robert Swiecki
2026-08-26 21:20:27 +02:00
5 changed files with 402 additions and 60 deletions

View File

@@ -83,6 +83,13 @@ static mount_t prepareMountPoint(const nsjail::MountPt& proto) {
} }
mpt.dst += proto.dst(); mpt.dst += proto.dst();
if (!util::isSafeContainmentPath(mpt.dst)) {
LOG_E("Mount destination escapes containment via '.'/'..'/NUL: %s", QC(mpt.dst));
/* Keep the unsafe dst so later mount steps fail closed rather than
* treating a cleared path as the jail root. */
return mpt;
}
mpt.flags = proto.rw() ? 0 : (uintptr_t)MS_RDONLY; mpt.flags = proto.rw() ? 0 : (uintptr_t)MS_RDONLY;
if (proto.is_bind()) { if (proto.is_bind()) {
mpt.flags |= MS_BIND | MS_REC | MS_PRIVATE; mpt.flags |= MS_BIND | MS_REC | MS_PRIVATE;
@@ -197,7 +204,16 @@ static bool mountWithDynamicContent(
static bool mountSinglePoint(mount_t* mpt, const char* newroot, const char* tmpdir) { static bool mountSinglePoint(mount_t* mpt, const char* newroot, const char* tmpdir) {
LOG_D("Mounting (legacy): %s", mnt::describeMountPt(*mpt->mpt).c_str()); LOG_D("Mounting (legacy): %s", mnt::describeMountPt(*mpt->mpt).c_str());
if (!util::isSafeContainmentPath(mpt->dst)) {
LOG_E("Mount destination escapes containment via '.'/'..'/NUL: %s", QC(mpt->dst));
return false;
}
const std::string dstpath = std::string(newroot) + "/" + mpt->dst; const std::string dstpath = std::string(newroot) + "/" + mpt->dst;
if (!util::isSafeContainmentPath(dstpath)) {
LOG_E("Resolved mount path escapes containment: %s", QC(dstpath));
return false;
}
std::string srcpath = mpt->src.empty() ? "none" : mpt->src; std::string srcpath = mpt->src.empty() ? "none" : mpt->src;
if (!util::createDirRecursively(dstpath.c_str())) { if (!util::createDirRecursively(dstpath.c_str())) {

View File

@@ -184,11 +184,11 @@ static bool remountWithLegacyMount(const mount_t& mpt) {
return true; return true;
} }
static bool openMountForRemount(mount_t* mpt, int root_fd, const char* rel_dst) { static bool openMountForRemount(mount_t* mpt, int parent_fd, const char* basename) {
mpt->fd = util::syscall( mpt->fd = util::syscall(
__NR_open_tree, (uintptr_t)root_fd, (uintptr_t)rel_dst, (uintptr_t)OPEN_TREE_CLOEXEC); __NR_open_tree, (uintptr_t)parent_fd, (uintptr_t)basename, (uintptr_t)OPEN_TREE_CLOEXEC);
if (mpt->fd < 0) { if (mpt->fd < 0) {
PLOG_W("open_tree(root_fd, '%s')", rel_dst); PLOG_W("open_tree(parent_fd, '%s')", basename);
return false; return false;
} }
mpt->mounted = true; mpt->mounted = true;
@@ -200,25 +200,122 @@ static bool createDirAt(int dir_fd, const char* path, mode_t mode) {
if (!path[0]) { if (!path[0]) {
return true; return true;
} }
if (!util::isSafeContainmentPath(path)) {
LOG_E("Mount destination escapes containment via '.'/'..'/NUL: '%s'", path);
return false;
}
std::string cumulative; /*
* Walk one path component at a time with O_NOFOLLOW so a previously
* planted symlink mount (-s) cannot redirect mkdirat/openat outside the
* staging root. Multi-component mkdirat() follows intermediate symlinks.
*/
int prev_fd = dir_fd;
bool close_prev = false;
for (const auto& component : util::strSplit(path, '/')) { for (const auto& component : util::strSplit(path, '/')) {
if (component.empty()) { if (component.empty()) {
continue; continue;
} }
if (component == "." || component == "..") {
if (!cumulative.empty()) { LOG_E("Mount destination escapes containment via '%s' in '%s'",
cumulative += '/'; component.c_str(), path);
if (close_prev) {
close(prev_fd);
}
return false;
} }
cumulative += component;
if (mkdirat(dir_fd, cumulative.c_str(), mode) == -1 && errno != EEXIST) { if (mkdirat(prev_fd, component.c_str(), mode) == -1 && errno != EEXIST) {
if (errno != EROFS || !util::existsAsDirAt(dir_fd, cumulative.c_str())) { if (errno != EROFS || !util::existsAsDirAt(prev_fd, component.c_str())) {
PLOG_W("mkdirat(%d, '%s')", dir_fd, cumulative.c_str()); PLOG_W("mkdirat(%d, '%s')", prev_fd, component.c_str());
if (close_prev) {
close(prev_fd);
}
return false; return false;
} }
} }
int next_fd = TEMP_FAILURE_RETRY(
openat(prev_fd, component.c_str(), O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW));
if (next_fd == -1) {
PLOG_W("openat(%d, '%s', O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW)", prev_fd,
component.c_str());
if (close_prev) {
close(prev_fd);
} }
return false;
}
if (close_prev) {
close(prev_fd);
}
prev_fd = next_fd;
close_prev = true;
}
if (close_prev) {
close(prev_fd);
}
return true;
}
/* Open the parent directory of rel_dst under root_fd without following
* symlinks; return the final basename for leaf operations. */
static bool openParentAtNoFollow(
int root_fd, const char* rel_dst, int* parent_fd_out, std::string* basename_out) {
rel_dst = util::stripLeadingSlashes(rel_dst);
if (!rel_dst[0] || strcmp(rel_dst, ".") == 0) {
*parent_fd_out = TEMP_FAILURE_RETRY(fcntl(root_fd, F_DUPFD_CLOEXEC, 0));
if (*parent_fd_out < 0) {
PLOG_W("fcntl(root_fd, F_DUPFD_CLOEXEC)");
return false;
}
*basename_out = ".";
return true;
}
const char* last_slash = strrchr(rel_dst, '/');
if (!last_slash) {
*parent_fd_out = TEMP_FAILURE_RETRY(fcntl(root_fd, F_DUPFD_CLOEXEC, 0));
if (*parent_fd_out < 0) {
PLOG_W("fcntl(root_fd, F_DUPFD_CLOEXEC)");
return false;
}
*basename_out = rel_dst;
return true;
}
std::string parent(rel_dst, last_slash - rel_dst);
*basename_out = last_slash + 1;
if (basename_out->empty() || *basename_out == "." || *basename_out == "..") {
LOG_E("Unsafe mount destination basename in '%s'", rel_dst);
return false;
}
if (!createDirAt(root_fd, parent.c_str(), 0755)) {
return false;
}
/* Re-walk parent with O_NOFOLLOW to obtain the directory fd. */
int prev_fd = root_fd;
bool close_prev = false;
for (const auto& component : util::strSplit(parent, '/')) {
if (component.empty()) {
continue;
}
int next_fd = TEMP_FAILURE_RETRY(
openat(prev_fd, component.c_str(), O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW));
if (next_fd == -1) {
PLOG_W("openat(parent walk '%s')", component.c_str());
if (close_prev) {
close(prev_fd);
}
return false;
}
if (close_prev) {
close(prev_fd);
}
prev_fd = next_fd;
close_prev = true;
}
*parent_fd_out = prev_fd;
return true; return true;
} }
@@ -313,19 +410,19 @@ static int createFilesystemMount(const mount_t& mpt) {
return mnt_fd; return mnt_fd;
} }
static bool mountSymlinkAt(mount_t* mpt, int root_fd, const char* rel_dst) { static bool mountSymlinkAt(mount_t* mpt, int parent_fd, const char* basename) {
LOG_D("Creating symlink: %s -> %s (fd-relative)", mpt->src.c_str(), rel_dst); LOG_D("Creating symlink: %s -> %s (fd-relative)", mpt->src.c_str(), basename);
if (symlinkat(mpt->src.c_str(), root_fd, rel_dst) == -1) { if (symlinkat(mpt->src.c_str(), parent_fd, basename) == -1) {
if (mpt->mpt->mandatory()) { if (mpt->mpt->mandatory()) {
PLOG_E("symlinkat('%s' -> '%s')", mpt->src.c_str(), rel_dst); PLOG_E("symlinkat('%s' -> '%s')", mpt->src.c_str(), basename);
return false; return false;
} }
PLOG_W("symlinkat('%s' -> '%s') failed (non-mandatory)", mpt->src.c_str(), rel_dst); PLOG_W("symlinkat('%s' -> '%s') failed (non-mandatory)", mpt->src.c_str(), basename);
} }
return true; return true;
} }
static bool mountDynamicContentAt(mount_t* mpt, int root_fd, const char* rel_dst) { static bool mountDynamicContentAt(mount_t* mpt, int root_fd, int parent_fd, const char* basename) {
static uint64_t counter = 0; static uint64_t counter = 0;
std::string src_rel = ".dyn." + std::to_string(++counter); std::string src_rel = ".dyn." + std::to_string(++counter);
@@ -341,7 +438,7 @@ static bool mountDynamicContentAt(mount_t* mpt, int root_fd, const char* rel_dst
close(src_fd); close(src_fd);
if (!ok) { if (!ok) {
LOG_W("Failed to write %zu bytes for dynamic content '%s'", content.length(), LOG_W("Failed to write %zu bytes for dynamic content '%s'", content.length(),
rel_dst); basename);
unlinkat(root_fd, src_rel.c_str(), 0); unlinkat(root_fd, src_rel.c_str(), 0);
return false; return false;
} }
@@ -355,12 +452,12 @@ static bool mountDynamicContentAt(mount_t* mpt, int root_fd, const char* rel_dst
} }
if (!applyMountFlags(mnt_fd, mpt->flags & ~MS_RDONLY)) { if (!applyMountFlags(mnt_fd, mpt->flags & ~MS_RDONLY)) {
LOG_W("Failed to apply mount flags to '%s'", rel_dst); LOG_W("Failed to apply mount flags to '%s'", basename);
} }
if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)root_fd, if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)parent_fd,
(uintptr_t)rel_dst, (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) { (uintptr_t)basename, (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) {
PLOG_W("move_mount('%s' -> '%s')", src_rel.c_str(), rel_dst); PLOG_W("move_mount('%s' -> '%s')", src_rel.c_str(), basename);
close(mnt_fd); close(mnt_fd);
unlinkat(root_fd, src_rel.c_str(), 0); unlinkat(root_fd, src_rel.c_str(), 0);
return false; return false;
@@ -371,16 +468,10 @@ static bool mountDynamicContentAt(mount_t* mpt, int root_fd, const char* rel_dst
PLOG_W("unlinkat(root_fd, '%s')", src_rel.c_str()); 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); return openMountForRemount(mpt, parent_fd, basename);
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) { static bool doBindMountAt(mount_t* mpt, int parent_fd, const char* basename) {
unsigned int flags = OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC; unsigned int flags = OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC;
if (mpt->flags & MS_REC) { if (mpt->flags & MS_REC) {
flags |= AT_RECURSIVE; flags |= AT_RECURSIVE;
@@ -403,63 +494,69 @@ static bool doBindMountAt(mount_t* mpt, int root_fd, const char* rel_dst) {
*/ */
if (!applyMountFlags( if (!applyMountFlags(
mnt_fd, mpt->flags & ~MS_RDONLY, true, (mpt->flags & MS_REC) != 0)) { mnt_fd, mpt->flags & ~MS_RDONLY, true, (mpt->flags & MS_REC) != 0)) {
LOG_W("Failed to apply mount flags to '%s'", rel_dst); LOG_W("Failed to apply mount flags to '%s'", basename);
} }
if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)root_fd, if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)parent_fd,
(uintptr_t)rel_dst, (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) { (uintptr_t)basename, (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) {
PLOG_W("move_mount('%s' -> '%s')", mpt->src.c_str(), rel_dst); PLOG_W("move_mount('%s' -> '%s')", mpt->src.c_str(), basename);
close(mnt_fd); close(mnt_fd);
return false; return false;
} }
close(mnt_fd); close(mnt_fd);
return openMountForRemount(mpt, root_fd, rel_dst); return openMountForRemount(mpt, parent_fd, basename);
} }
static bool mountSinglePointAt(mount_t* mpt, int root_fd) { static bool mountSinglePointAt(mount_t* mpt, int root_fd) {
LOG_D("Mounting (new API): %s", mnt::describeMountPt(*mpt->mpt).c_str()); LOG_D("Mounting (new API): %s", mnt::describeMountPt(*mpt->mpt).c_str());
const char* rel_dst = util::stripLeadingSlashes(mpt->dst.c_str()); const char* rel_dst = util::stripLeadingSlashes(mpt->dst.c_str());
/* Empty / only-slashes dst means the jail root (represented as "."). */
if (!rel_dst[0]) { if (!rel_dst[0]) {
rel_dst = "."; rel_dst = ".";
} } else if (!util::isSafeContainmentPath(rel_dst)) {
LOG_E("Mount destination escapes containment via '.'/'..'/NUL: %s", QC(mpt->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; return false;
} }
int parent_fd = -1;
std::string basename;
if (!openParentAtNoFollow(root_fd, rel_dst, &parent_fd, &basename)) {
LOG_W("Failed to resolve mount destination parents for '%s'", rel_dst);
return false;
} }
defer {
close(parent_fd);
};
if (mpt->mpt->is_symlink()) { if (mpt->mpt->is_symlink()) {
return mountSymlinkAt(mpt, root_fd, rel_dst); return mountSymlinkAt(mpt, parent_fd, basename.c_str());
} }
if (mpt->is_dir) { if (mpt->is_dir) {
if (strcmp(rel_dst, ".") != 0 && mkdirat(root_fd, rel_dst, 0711) == -1 && if (basename != "." && mkdirat(parent_fd, basename.c_str(), 0711) == -1 &&
errno != EEXIST) { errno != EEXIST) {
if (errno != EROFS || !util::existsAsDirAt(root_fd, rel_dst)) { if (errno != EROFS || !util::existsAsDirAt(parent_fd, basename.c_str())) {
PLOG_W("mkdirat(root_fd, '%s')", rel_dst); PLOG_W("mkdirat(parent_fd, '%s')", basename.c_str());
} }
} }
} else { } else {
int fd = openat(root_fd, rel_dst, O_CREAT | O_RDONLY | O_CLOEXEC, 0644); int fd = openat(parent_fd, basename.c_str(),
O_CREAT | O_RDONLY | O_CLOEXEC | O_NOFOLLOW, 0644);
if (fd >= 0) { if (fd >= 0) {
close(fd); close(fd);
} else if (errno != EROFS || !util::existsAsRegAt(root_fd, rel_dst)) { } else if (errno != EROFS || !util::existsAsRegAt(parent_fd, basename.c_str())) {
PLOG_W("openat(root_fd, '%s', O_CREAT)", rel_dst); PLOG_W("openat(parent_fd, '%s', O_CREAT|O_NOFOLLOW)", basename.c_str());
} }
} }
if (!mpt->mpt->src_content().empty()) { if (!mpt->mpt->src_content().empty()) {
return mountDynamicContentAt(mpt, root_fd, rel_dst); return mountDynamicContentAt(mpt, root_fd, parent_fd, basename.c_str());
} }
if (mpt->flags & MS_BIND) { if (mpt->flags & MS_BIND) {
return doBindMountAt(mpt, root_fd, rel_dst); return doBindMountAt(mpt, parent_fd, basename.c_str());
} }
int mnt_fd = createFilesystemMount(*mpt); int mnt_fd = createFilesystemMount(*mpt);
@@ -468,18 +565,18 @@ static bool mountSinglePointAt(mount_t* mpt, int root_fd) {
} }
if (!applyMountFlags(mnt_fd, mpt->flags & ~MS_RDONLY)) { if (!applyMountFlags(mnt_fd, mpt->flags & ~MS_RDONLY)) {
LOG_W("Failed to apply mount flags to '%s'", rel_dst); LOG_W("Failed to apply mount flags to '%s'", basename.c_str());
} }
if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)root_fd, if (util::syscall(__NR_move_mount, (uintptr_t)mnt_fd, (uintptr_t)"", (uintptr_t)parent_fd,
(uintptr_t)rel_dst, (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) { (uintptr_t)basename.c_str(), (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) {
PLOG_W("move_mount() for '%s'", rel_dst); PLOG_W("move_mount() for '%s'", basename.c_str());
close(mnt_fd); close(mnt_fd);
return false; return false;
} }
close(mnt_fd); close(mnt_fd);
return openMountForRemount(mpt, root_fd, rel_dst); return openMountForRemount(mpt, parent_fd, basename.c_str());
} }
static mount_t prepareMountPoint(const nsjail::MountPt& proto) { static mount_t prepareMountPoint(const nsjail::MountPt& proto) {
@@ -513,6 +610,13 @@ static mount_t prepareMountPoint(const nsjail::MountPt& proto) {
} }
mpt.dst += proto.dst(); mpt.dst += proto.dst();
if (!util::isSafeContainmentPath(mpt.dst)) {
LOG_E("Mount destination escapes containment via '.'/'..'/NUL: %s", QC(mpt.dst));
/* Keep the unsafe dst so later mount steps fail closed rather than
* treating a cleared path as the jail root. */
return mpt;
}
mpt.flags = proto.rw() ? 0 : (uintptr_t)MS_RDONLY; mpt.flags = proto.rw() ? 0 : (uintptr_t)MS_RDONLY;
if (proto.is_bind()) { if (proto.is_bind()) {
mpt.flags |= MS_BIND | MS_REC | MS_PRIVATE; mpt.flags |= MS_BIND | MS_REC | MS_PRIVATE;

View File

@@ -0,0 +1,191 @@
/*
* Regression tests for mount-destination path traversal hardening.
*
* Standalone harness (mirrors util::isSafeContainmentPath /
* createDirRecursively policy) so it can run without linking full nsjail.
*
* g++ -std=c++20 -O1 -o path_containment_test tests/path_containment_test.cc
* ./path_containment_test
*/
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <string>
#include <sys/stat.h>
#include <unistd.h>
#include <vector>
namespace {
std::vector<std::string> strSplit(const std::string& str, char delim) {
std::vector<std::string> vec;
std::string word;
for (char c : str) {
if (c == delim) {
vec.push_back(word);
word.clear();
} else {
word.push_back(c);
}
}
vec.push_back(word);
return vec;
}
bool isSafeContainmentPath(const std::string& path) {
if (path.empty()) {
return true;
}
if (path.find('\0') != std::string::npos) {
return false;
}
for (const auto& component : strSplit(path, '/')) {
if (component.empty()) {
continue;
}
if (component == "." || component == "..") {
return false;
}
}
return true;
}
bool createDirRecursivelySafe(const char* dir) {
if (dir[0] != '/') {
return false;
}
if (!isSafeContainmentPath(dir)) {
return false;
}
int prev_dir_fd = open("/", O_RDONLY | O_CLOEXEC | O_DIRECTORY);
if (prev_dir_fd == -1) {
return false;
}
char path[4096];
if (snprintf(path, sizeof(path), "%s", dir) >= (int)sizeof(path)) {
close(prev_dir_fd);
return false;
}
char* curr = path;
for (;;) {
while (*curr == '/') {
curr++;
}
char* next = strchr(curr, '/');
if (next == nullptr) {
close(prev_dir_fd);
return true;
}
*next = '\0';
if (mkdirat(prev_dir_fd, curr, 0755) == -1 && errno != EEXIST) {
close(prev_dir_fd);
return false;
}
int dir_fd = openat(prev_dir_fd, curr, O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
if (dir_fd == -1) {
close(prev_dir_fd);
return false;
}
close(prev_dir_fd);
prev_dir_fd = dir_fd;
curr = next + 1;
}
}
} // namespace
static void expect_safe(const char* p, bool want) {
bool got = isSafeContainmentPath(p);
if (got != want) {
fprintf(stderr, "FAIL isSafeContainmentPath(%s) = %d want %d\n", p, got, want);
exit(1);
}
}
int main() {
expect_safe("", true);
expect_safe("/", true);
expect_safe("/usr/lib", true);
expect_safe("/tmp/nsjail.root/home/user", true);
expect_safe("usr/lib", true);
expect_safe("/usr//lib", true);
expect_safe("..", false);
expect_safe("/..", false);
expect_safe("/../", false);
expect_safe("/tmp/../etc", false);
expect_safe("/tmp/nsjail.root/../../etc/passwd", false);
expect_safe("/foo/./bar", false);
expect_safe("./foo", false);
expect_safe("foo/../../bar", false);
std::string with_nul = std::string("/tmp/foo") + '\0' + "bar";
if (isSafeContainmentPath(with_nul)) {
fprintf(stderr, "FAIL NUL component accepted\n");
return 1;
}
(void)system("rm -rf /tmp/nsj_path_test /tmp/nsj_path_escaped");
mkdir("/tmp/nsj_path_test", 0755);
const char* escape =
"/tmp/nsj_path_test/root/../../nsj_path_escaped/evil_dir/leaf";
if (createDirRecursivelySafe(escape)) {
fprintf(stderr, "FAIL createDirRecursively accepted traversal path\n");
return 1;
}
if (access("/tmp/nsj_path_escaped", F_OK) == 0) {
fprintf(stderr, "FAIL escape directory was created\n");
return 1;
}
const char* ok = "/tmp/nsj_path_test/root/home/user/docs/leaf";
if (!createDirRecursivelySafe(ok)) {
fprintf(stderr, "FAIL createDirRecursively rejected safe path\n");
return 1;
}
if (access("/tmp/nsj_path_test/root/home/user/docs", F_OK) != 0) {
fprintf(stderr, "FAIL safe parents were not created\n");
return 1;
}
/* Symlink intermediate must fail closed under O_NOFOLLOW walk policy. */
(void)system("rm -rf /tmp/nsj_sym_stage /tmp/nsj_sym_escape");
mkdir("/tmp/nsj_sym_stage", 0755);
mkdir("/tmp/nsj_sym_escape", 0755);
if (symlink("/tmp/nsj_sym_escape", "/tmp/nsj_sym_stage/link") != 0) {
fprintf(stderr, "FAIL symlink setup\n");
return 1;
}
/* Mimic new-API component walk with O_NOFOLLOW */
{
int root = open("/tmp/nsj_sym_stage", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
if (root < 0) {
perror("open stage");
return 1;
}
if (mkdirat(root, "link", 0755) == -1 && errno != EEXIST) {
/* link exists as symlink; EEXIST expected */
}
int next = openat(root, "link", O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
if (next >= 0) {
fprintf(stderr, "FAIL openat(O_NOFOLLOW) followed/opened symlink\n");
close(next);
close(root);
return 1;
}
close(root);
if (access("/tmp/nsj_sym_escape/pwned", F_OK) == 0) {
fprintf(stderr, "FAIL escape created\n");
return 1;
}
}
printf("OK path_containment_test passed\n");
return 0;
}

31
util.cc
View File

@@ -233,11 +233,35 @@ bool writeBufToFile(
return true; return true;
} }
bool isSafeContainmentPath(const std::string& path) {
/* Empty / root-only paths refer to the containment root itself. */
if (path.empty()) {
return true;
}
/* Embedded NUL would truncate C-string APIs and hide trailing components. */
if (path.find('\0') != std::string::npos) {
return false;
}
for (const auto& component : strSplit(path, '/')) {
if (component.empty()) {
continue;
}
if (component == "." || component == "..") {
return false;
}
}
return true;
}
bool createDirRecursively(const char* dir) { bool createDirRecursively(const char* dir) {
if (dir[0] != '/') { if (dir[0] != '/') {
LOG_W("The directory path must start with '/': '%s' provided", dir); LOG_W("The directory path must start with '/': '%s' provided", dir);
return false; return false;
} }
if (!isSafeContainmentPath(dir)) {
LOG_W("Refusing path with '.'/'..'/NUL components: '%s'", dir);
return false;
}
int prev_dir_fd = TEMP_FAILURE_RETRY(open("/", O_RDONLY | O_CLOEXEC | O_DIRECTORY)); int prev_dir_fd = TEMP_FAILURE_RETRY(open("/", O_RDONLY | O_CLOEXEC | O_DIRECTORY));
if (prev_dir_fd == -1) { if (prev_dir_fd == -1) {
@@ -272,9 +296,12 @@ bool createDirRecursively(const char* dir) {
} }
} }
int dir_fd = TEMP_FAILURE_RETRY(openat(prev_dir_fd, curr, O_DIRECTORY | O_CLOEXEC)); /* O_NOFOLLOW: do not walk through symlinks that escape the intended tree. */
int dir_fd = TEMP_FAILURE_RETRY(
openat(prev_dir_fd, curr, O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW));
if (dir_fd == -1) { if (dir_fd == -1) {
PLOG_W("openat('%d', %s, O_DIRECTORY | O_CLOEXEC)", prev_dir_fd, QC(curr)); PLOG_W("openat('%d', %s, O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW)", prev_dir_fd,
QC(curr));
close(prev_dir_fd); close(prev_dir_fd);
return false; return false;
} }

8
util.h
View File

@@ -23,6 +23,7 @@
#define NS_UTIL_H #define NS_UTIL_H
#include <errno.h> #include <errno.h>
#include <fcntl.h>
#include <inttypes.h> #include <inttypes.h>
#include <stdbool.h> #include <stdbool.h>
#include <stdint.h> #include <stdint.h>
@@ -60,6 +61,9 @@ int recvFd(int sock);
bool writeBufToFile( bool writeBufToFile(
const char* filename, const void* buf, size_t len, int open_flags, bool log_errors = true); const char* filename, const void* buf, size_t len, int open_flags, bool log_errors = true);
bool createDirRecursively(const char* dir); bool createDirRecursively(const char* dir);
/* Reject ".", ".." and embedded NUL in mount destinations so they cannot
* escape the jail staging root via path traversal. */
bool isSafeContainmentPath(const std::string& path);
std::string* StrAppend(std::string* str, const char* format, ...) std::string* StrAppend(std::string* str, const char* format, ...)
__attribute__((format(printf, 2, 3))); __attribute__((format(printf, 2, 3)));
std::string StrPrintf(const char* format, ...) __attribute__((format(printf, 1, 2))); std::string StrPrintf(const char* format, ...) __attribute__((format(printf, 1, 2)));
@@ -105,7 +109,7 @@ inline bool existsAsDir(const char* path) {
inline bool existsAsDirAt(int dir_fd, const char* path) { inline bool existsAsDirAt(int dir_fd, const char* path) {
int saved = errno; int saved = errno;
struct stat st; struct stat st;
if (fstatat(dir_fd, path, &st, 0) == 0 && S_ISDIR(st.st_mode)) { if (fstatat(dir_fd, path, &st, AT_SYMLINK_NOFOLLOW) == 0 && S_ISDIR(st.st_mode)) {
return true; return true;
} }
errno = saved; errno = saved;
@@ -125,7 +129,7 @@ inline bool existsAsReg(const char* path) {
inline bool existsAsRegAt(int dir_fd, const char* path) { inline bool existsAsRegAt(int dir_fd, const char* path) {
int saved = errno; int saved = errno;
struct stat st; struct stat st;
if (fstatat(dir_fd, path, &st, 0) == 0 && S_ISREG(st.st_mode)) { if (fstatat(dir_fd, path, &st, AT_SYMLINK_NOFOLLOW) == 0 && S_ISREG(st.st_mode)) {
return true; return true;
} }
errno = saved; errno = saved;