From 2f2a68be3db5c323323e49c17395029a2ddc2d9a Mon Sep 17 00:00:00 2001 From: Zygo Blaxell Date: Thu, 9 Jan 2025 02:20:11 -0500 Subject: [PATCH] roots: use openat2 instead of openat when available This increases resistance to symlink and mount attacks. Previously, bees could follow a symlink or a mount point in a directory component of a subvol or file name. Once the file is opened, the open file descriptor would be checked to see if its subvol and inode matches the expected file in the target filesystem. Files that fail to match would be immediately closed. With openat2 resolve flags, symlinks and mount points terminate path resolution in the kernel. Paths that lead through symlinks or onto mount points cannot be opened at all. Fall back to openat() if openat2() returns ENOSYS, so bees will still run on kernels before v5.6. Signed-off-by: Zygo Blaxell --- src/bees-roots.cc | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/src/bees-roots.cc b/src/bees-roots.cc index 3a6cac5..92b525f 100644 --- a/src/bees-roots.cc +++ b/src/bees-roots.cc @@ -3,6 +3,7 @@ #include "crucible/btrfs-tree.h" #include "crucible/cache.h" #include "crucible/ntoa.h" +#include "crucible/openat2.h" #include "crucible/string.h" #include "crucible/table.h" #include "crucible/task.h" @@ -1758,6 +1759,32 @@ BeesRoots::stop_wait() BEESLOGDEBUG("BeesRoots stopped"); } +static +Fd +bees_openat(int const parent_fd, const char *const pathname, uint64_t const flags) +{ + // Never O_CREAT so we don't need a mode argument + THROW_CHECK1(invalid_argument, flags, (flags & O_CREAT) == 0); + + // Try openat2 if the kernel has it + static bool can_openat2 = true; + if (can_openat2) { + open_how how { + .flags = flags, + .resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS | RESOLVE_NO_XDEV, + }; + const auto rv = openat2(parent_fd, pathname, &how, sizeof(open_how)); + if (rv == -1 && errno == ENOSYS) { + can_openat2 = false; + } else { + return Fd(rv); + } + } + + // No kernel support, use openat instead + return Fd(openat(parent_fd, pathname, flags)); +} + Fd BeesRoots::open_root_nocache(uint64_t rootid) { @@ -1820,7 +1847,7 @@ BeesRoots::open_root_nocache(uint64_t rootid) } // Theoretically there is only one, so don't bother looping. BEESTRACE("dirid " << dirid << " path " << ino.m_paths.at(0)); - parent_fd = openat(parent_fd, ino.m_paths.at(0).c_str(), FLAGS_OPEN_DIR); + parent_fd = bees_openat(parent_fd, ino.m_paths.at(0).c_str(), FLAGS_OPEN_DIR); if (!parent_fd) { BEESLOGTRACE("no parent_fd from dirid"); BEESCOUNT(root_parent_path_open_fail); @@ -1829,7 +1856,7 @@ BeesRoots::open_root_nocache(uint64_t rootid) } // BEESLOG("openat(" << name_fd(parent_fd) << ", " << name << ")"); BEESTRACE("openat(" << name_fd(parent_fd) << ", " << name << ")"); - Fd rv = openat(parent_fd, name.c_str(), FLAGS_OPEN_DIR); + Fd rv = bees_openat(parent_fd, name.c_str(), FLAGS_OPEN_DIR); if (!rv) { BEESLOGTRACE("open failed for name " << name << ": " << strerror(errno)); BEESCOUNT(root_open_fail); @@ -1975,7 +2002,7 @@ BeesRoots::open_root_ino_nocache(uint64_t root, uint64_t ino) // opening in write mode, and if we do open in write mode, // we can't exec the file while we have it open. const char *fp_cstr = file_path.c_str(); - rv = openat(root_fd, fp_cstr, FLAGS_OPEN_FILE); + rv = bees_openat(root_fd, fp_cstr, FLAGS_OPEN_FILE); if (!rv) { // errno == ENOENT is the most common error case. // No need to report it.