8000 mnt: fail closed on untrusted workdir candidates by krishna28238-arch · Pull Request #381 · google/nsjail · GitHub
[go: up one dir, main page]

Skip to content
  • Open
    Show file tree
    Hide file tree
    Changes from all commits
    Commits
    File filter

    Filter by extension

    Filter by extension

    Conversations
    Failed to load comments.
    Loading
    Jump to
    Jump to file
    Failed to load files.
    Loading
    Diff view
    Diff view
    27 changes: 22 additions & 5 deletions mnt.cc
    Original file line number Diff line number Diff line change
    Expand Up @@ -174,7 +174,17 @@ static std::string findWritableDirUnderRoot() {
    return "";
    }

    std::unique_ptr<std::string> findWorkDir(nsj_t* nsj, const char* purpose) {
    /*
    * Picks a directory the staging tmpfs trees are attached to. Every candidate is
    * validated with util::openVerifiedDir(): a pre-existing entry is accepted only
    * when it is a real directory owned by the current euid, and the returned fd
    * refers to that verified inode. Callers attach their mounts through this fd
    * (or /proc/self/fd/<fd>) instead of re-resolving the path, so an entry planted
    * in a shared parent such as /tmp cannot redirect the jail root. On success
    * *fd_out holds the directory fd, which stays open for the caller to close.
    */
    std::unique_ptr<std::string> findWorkDir(nsj_t* nsj, const char* purpose, int* fd_out) {
    *fd_out = -1;
    const std::string uid = std::to_string(nsj->orig_uid);
    const std::string suffix = "nsjail." + uid + "." + purpose;

    Expand All @@ -195,21 +205,28 @@ std::unique_ptr<std::string> findWorkDir(nsj_t* nsj, const char* purpose) {
    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);
    int fd = util::openVerifiedDir(path.c_str(), 0755);
    if (fd == -1) {
    continue;
    }
    *fd_out = fd;
    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)) {
    int fd = util::openVerifiedDir(candidate.c_str(), 0755);
    if (fd != -1) {
    *fd_out = fd;
    return std::make_unique<std::string>(candidate);
    }
    }

    std::string fallback = "/tmp/" + suffix + "." + std::to_string(util::rnd64());
    if (tryCreateDir(fallback, true)) {
    int fd = util::openVerifiedDir(fallback.c_str(), 0755);
    if (fd != -1) {
    *fd_out = fd;
    return std::make_unique<std::string>(fallback);
    }

    Expand Down
    2 changes: 1 addition & 1 deletion mnt.h
    Original file line number Diff line number Diff line change
    Expand Up @@ -71,7 +71,7 @@ struct mount_t {
    };

    bool initNs(nsj_t* nsj);
    std::unique_ptr<std::string> findWorkDir(nsj_t* nsj, const char* purpose);
    std::unique_ptr<std::string> findWorkDir(nsj_t* nsj, const char* purpose, int* fd_out);
    const std::string describeMountPt(const nsjail::MountPt& mpt);
    const std::string flagsToStr(unsigned long flags);

    Expand Down
    26 changes: 22 additions & 4 deletions mnt_legacy.cc
    Original file line number Diff line number Diff line change
    Expand Up @@ -392,28 +392,46 @@ std::unique_ptr<std::string> buildMountTree(nsj_t* nsj, std::vector<mnt::mount_t

    const size_t tmpfsSize = 16 * 1024 * 1024;

    auto destdir = mnt::findWorkDir(nsj, "root");
    int destdir_fd = -1;
    auto destdir = mnt::findWorkDir(nsj, "root", &destdir_fd);
    if (!destdir) {
    return nullptr;
    }
    defer {
    close(destdir_fd);
    };

    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,
    /*
    * Attach the staging tmpfs trees through /proc/self/fd handles instead of
    * re-resolving the paths: findWorkDir() verified that the destinations are
    * euid-owned real directories, and the procfs handle pins that inode even if
    * a shared parent is mutated afterwards.
    */
    char destdir_procfd[64];
    snprintf(destdir_procfd, sizeof(destdir_procfd), "/proc/self/fd/%d", destdir_fd);
    if (mount(nullptr, destdir_procfd, "tmpfs", 0,
    ("size=" + std::to_string(tmpfsSize)).c_str()) == -1) {
    PLOG_E("mount('%s', tmpfs)", destdir->c_str());
    return nullptr;
    }

    auto tmpdir = mnt::findWorkDir(nsj, "tmp");
    int tmpdir_fd = -1;
    auto tmpdir = mnt::findWorkDir(nsj, "tmp", &tmpdir_fd);
    if (!tmpdir) {
    return nullptr;
    }
    defer {
    close(tmpdir_fd);
    };

    if (mount(nullptr, tmpdir->c_str(), "tmpfs", 0,
    char tmpdir_procfd[64];
    snprintf(tmpdir_procfd, sizeof(tmpdir_procfd), "/proc/self/fd/%d", tmpdir_fd);
    if (mount(nullptr, tmpdir_procfd, "tmpfs", 0,
    ("size=" + std::to_string(tmpfsSize)).c_str()) == -1) {
    PLOG_E("mount('%s', tmpfs)", tmpdir->c_str());
    return nullptr;
    Expand Down
    22 changes: 16 additions & 6 deletions mnt_newapi.cc
    Original file line number Diff line number Diff line change
    Expand Up @@ -732,27 +732,37 @@ std::unique_ptr<std::string> buildMountTree(nsj_t* nsj, std::vector<mnt::mount_t
    LOG_W("mount_setattr(root_mfd, 0) failed");
    }

    auto destdir = mnt::findWorkDir(nsj, "root");
    int destdir_fd = -1;
    auto destdir = mnt::findWorkDir(nsj, "root", &destdir_fd);
    if (!destdir) {
    close(root_mfd);
    return nullptr;
    }
    defer {
    close(destdir_fd);
    };

    /*
    * Attach the new root to the filesystem *before* populating it.
    * Attempting to populate a detached mount tree via move_mount() can fail with EINVAL
    * on some kernels (e.g. 6.12) if the target is not yet attached.
    */
    if (util::syscall(__NR_move_mount, (uintptr_t)root_mfd, (uintptr_t)"", (uintptr_t)AT_FDCWD,
    (uintptr_t)destdir->c_str(), (uintptr_t)MOVE_MOUNT_F_EMPTY_PATH) < 0) {
    /*
    * The attach targets the fd returned by findWorkDir() instead of the path:
    * that fd is pinned to the verified inode, so an entry swapped into a shared
    * parent after the verification cannot redirect the jail root.
    */
    if (util::syscall(__NR_move_mount, (uintptr_t)root_mfd, (uintptr_t)"",
    (uintptr_t)destdir_fd, (uintptr_t)"",
    (uintptr_t)(MOVE_MOUNT_F_EMPTY_PATH | MOVE_MOUNT_T_EMPTY_PATH)) < 0) {
    PLOG_E("move_mount(root_mfd -> '%s')", destdir->c_str());
    close(root_mfd);
    return nullptr;
    }
    close(root_mfd);

    int root_fd =
    openat(AT_FDCWD, destdir->c_str(), O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY);
    int root_fd = openat(
    AT_FDCWD, destdir->c_str(), O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY | O_NOFOLLOW);
    if (root_fd < 0) {
    PLOG_E("openat('%s')", destdir->c_str());
    return nullptr;
    Expand Down Expand Up @@ -805,7 +815,7 @@ std::unique_ptr<std::string> buildMountTree(nsj_t* nsj, std::vector<mnt::mount_t
    */
    if (!util::stripLeadingSlashes(mpt.dst.c_str())[0]) {
    int nroot = openat(AT_FDCWD, destdir->c_str(),
    O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY);
    O_RDONLY | O_CLOEXEC | O_PATH | O_DIRECTORY | O_NOFOLLOW);
    if (nroot < 0) {
    PLOG_E("openat('%s') to refresh root fd", destdir->c_str());
    return nullptr;
    Expand Down
    39 changes: 39 additions & 0 deletions util.cc
    Original file line number Diff line number Diff line change
    Expand Up @@ -311,6 +311,45 @@ bool createDirRecursively(const char* dir) {
    }
    }

    /*
    * Creates (or reuses) the directory at 'path' and hands back an O_DIRECTORY fd for
    * it. A pre-existing entry is only accepted when it is a real directory
    * (O_NOFOLLOW) owned by the current euid, so that a symlink or a foreign-owned
    * directory planted in a shared parent such as /tmp can never be selected as a
    * staging or work directory. Returns -1 on refusal.
    */
    int openVerifiedDir(const char* path, mode_t mode) {
    if (mkdir(path, mode) == -1 && errno != EEXIST) {
    if (errno != EROFS || !existsAsDir(path)) {
    PLOG_D("mkdir('%s')", path);
    return -1;
    }
    }

    int fd = TEMP_FAILURE_RETRY(open(path, O_RDONLY | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW));
    if (fd == -1) {
    PLOG_W("open('%s', O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW)", path);
    return -1;
    }

    struct stat st;
    if (TEMP_FAILURE_RETRY(fstat(fd, &st)) == -1) {
    PLOG_W("fstat('%s')", path);
    close(fd);
    return -1;
    }

    if (st.st_uid != geteuid()) {
    LOG_W("Refusing pre-existing directory '%s' owned by uid=%ju (we are uid=%ju)",
    path, (uintmax_t)st.st_uid, (uintmax_t)geteuid());
    close(fd);
    return -1;
    }

    LOG_D("Verified directory '%s' (fd=%d)", path, fd);
    return fd;
    }

    std::string* StrAppend(std::string* str, const char* format, ...) {
    char* strp;

    Expand Down
    1 change: 1 addition & 0 deletions util.h
    Original file line number Diff line number Diff line change
    Expand Up @@ -61,6 +61,7 @@ int recvFd(int sock);
    bool writeBufToFile(
    const char* filename, const void* buf, size_t len, int open_flags, bool log_errors = true);
    bool createDirRecursively(const char* dir);
    int openVerifiedDir(const char* path, mode_t mode);
    /* Reject ".", ".." and embedded NUL in mount destinations so they cannot
    * escape the jail staging root via path traversal. */
    bool isSafeContainmentPath(const std::string& path);
    Expand Down
    Loading
    0