1
0
mirror of https://github.com/Zygo/bees.git synced 2025-05-17 13:25:45 +02:00

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 <bees@furryterror.org>
This commit is contained in:
Zygo Blaxell 2025-01-09 02:20:11 -05:00
parent 82f1fd8054
commit 2f2a68be3d

View File

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