mirror of
https://github.com/NixOS/nix.git
synced 2025-11-27 12:41:00 +01:00
libutil/file-descriptor: Add safer utilities for opening files relative to dirFd
Implements a safe no symlink following primitive operation for opening file descriptors. This is unix-only for the time being, since windows doesn't really suffer from symlink races, since they are admin-only. Tested with enosys --syscall openat2 as well.
This commit is contained in:
parent
5d066386b5
commit
77990e7cca
3 changed files with 217 additions and 0 deletions
|
|
@ -1,3 +1,4 @@
|
|||
#include "nix/util/fs-sink.hh"
|
||||
#include "nix/util/util.hh"
|
||||
#include "nix/util/types.hh"
|
||||
#include "nix/util/file-system.hh"
|
||||
|
|
@ -318,4 +319,53 @@ TEST(DirectoryIterator, nonexistent)
|
|||
ASSERT_THROW(DirectoryIterator("/schnitzel/darmstadt/pommes"), SysError);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------------
|
||||
* openFileEnsureBeneathNoSymlinks
|
||||
* --------------------------------------------------------------------------*/
|
||||
|
||||
#ifndef _WIN32
|
||||
|
||||
TEST(openFileEnsureBeneathNoSymlinks, works)
|
||||
{
|
||||
std::filesystem::path tmpDir = nix::createTempDir();
|
||||
nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true);
|
||||
|
||||
{
|
||||
RestoreSink sink(/*startFsync=*/false);
|
||||
sink.dstPath = tmpDir;
|
||||
sink.dirFd = openDirectory(tmpDir);
|
||||
sink.createDirectory(CanonPath("a"));
|
||||
sink.createDirectory(CanonPath("c"));
|
||||
sink.createDirectory(CanonPath("c/d"));
|
||||
sink.createRegularFile(CanonPath("c/d/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); });
|
||||
sink.createSymlink(CanonPath("a/absolute_symlink"), tmpDir.string());
|
||||
sink.createSymlink(CanonPath("a/relative_symlink"), "../.");
|
||||
sink.createSymlink(CanonPath("a/broken_symlink"), "./nonexistent");
|
||||
}
|
||||
|
||||
AutoCloseFD dirFd = openDirectory(tmpDir);
|
||||
|
||||
using namespace nix::unix;
|
||||
|
||||
auto open = [&](std::string_view path, int flags, mode_t mode = 0) {
|
||||
return openFileEnsureBeneathNoSymlinks(dirFd.get(), CanonPath(path), flags, mode);
|
||||
};
|
||||
|
||||
EXPECT_THROW(open("a/absolute_symlink", O_RDONLY), SymlinkNotAllowed);
|
||||
EXPECT_THROW(open("a/relative_symlink", O_RDONLY), SymlinkNotAllowed);
|
||||
EXPECT_THROW(open("a/absolute_symlink/a", O_RDONLY), SymlinkNotAllowed);
|
||||
EXPECT_THROW(open("a/absolute_symlink/c/d", O_RDONLY), SymlinkNotAllowed);
|
||||
EXPECT_THROW(open("a/relative_symlink/c", O_RDONLY), SymlinkNotAllowed);
|
||||
EXPECT_EQ(open("a/broken_symlink", O_CREAT | O_WRONLY | O_EXCL, 0666), INVALID_DESCRIPTOR);
|
||||
/* Sanity check, no symlink shenanigans and behaves the same as regular openat with O_EXCL | O_CREAT. */
|
||||
EXPECT_EQ(errno, EEXIST);
|
||||
EXPECT_THROW(open("a/absolute_symlink/broken_symlink", O_CREAT | O_WRONLY | O_EXCL, 0666), SymlinkNotAllowed);
|
||||
EXPECT_EQ(open("c/d/regular/a", O_RDONLY), INVALID_DESCRIPTOR);
|
||||
EXPECT_EQ(open("c/d/regular", O_RDONLY | O_DIRECTORY), INVALID_DESCRIPTOR);
|
||||
EXPECT_TRUE(AutoCloseFD{open("c/d/regular", O_RDONLY)});
|
||||
EXPECT_TRUE(AutoCloseFD{open("a/regular", O_CREAT | O_WRONLY | O_EXCL, 0666)});
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace nix
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
///@file
|
||||
|
||||
#include "nix/util/canon-path.hh"
|
||||
#include "nix/util/types.hh"
|
||||
#include "nix/util/error.hh"
|
||||
|
||||
|
|
@ -203,6 +204,26 @@ void closeOnExec(Descriptor fd);
|
|||
} // namespace unix
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
namespace linux {
|
||||
|
||||
/**
|
||||
* Wrapper around Linux's openat2 syscall introduced in Linux 5.6.
|
||||
*
|
||||
* @see https://man7.org/linux/man-pages/man2/openat2.2.html
|
||||
* @see https://man7.org/linux/man-pages/man2/open_how.2type.html
|
||||
v*
|
||||
* @param flags O_* flags
|
||||
* @param mode Mode for O_{CREAT,TMPFILE}
|
||||
* @param resolve RESOLVE_* flags
|
||||
*
|
||||
* @return nullopt if openat2 is not supported by the kernel.
|
||||
*/
|
||||
std::optional<Descriptor> openat2(Descriptor dirFd, const char * path, uint64_t flags, uint64_t mode, uint64_t resolve);
|
||||
|
||||
} // namespace linux
|
||||
#endif
|
||||
|
||||
#if defined(_WIN32) && _WIN32_WINNT >= 0x0600
|
||||
namespace windows {
|
||||
|
||||
|
|
@ -212,6 +233,43 @@ std::wstring handleToFileName(Descriptor handle);
|
|||
} // namespace windows
|
||||
#endif
|
||||
|
||||
#ifndef _WIN32
|
||||
namespace unix {
|
||||
|
||||
struct SymlinkNotAllowed : public Error
|
||||
{
|
||||
CanonPath path;
|
||||
|
||||
SymlinkNotAllowed(CanonPath path)
|
||||
/* Can't provide better error message, since the parent directory is only known to the caller. */
|
||||
: Error("relative path '%s' points to a symlink, which is not allowed", path.rel())
|
||||
, path(std::move(path))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Safe(r) function to open \param path file relative to \param dirFd, while
|
||||
* disallowing escaping from a directory and resolving any symlinks in the
|
||||
* process.
|
||||
*
|
||||
* @note When not on Linux or when openat2 is not available this is implemented
|
||||
* via openat single path component traversal. Uses RESOLVE_BENEATH with openat2
|
||||
* or O_RESOLVE_BENEATH.
|
||||
*
|
||||
* @note Since this is Unix-only path is specified as CanonPath, which models
|
||||
* Unix-style paths and ensures that there are no .. or . components.
|
||||
*
|
||||
* @param flags O_* flags
|
||||
* @param mode Mode for O_{CREAT,TMPFILE}
|
||||
*
|
||||
* @throws SymlinkNotAllowed if any path components
|
||||
*/
|
||||
Descriptor openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode = 0);
|
||||
|
||||
} // namespace unix
|
||||
#endif
|
||||
|
||||
MakeError(EndOfFile, Error);
|
||||
|
||||
} // namespace nix
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#include "nix/util/canon-path.hh"
|
||||
#include "nix/util/file-system.hh"
|
||||
#include "nix/util/signals.hh"
|
||||
#include "nix/util/finally.hh"
|
||||
|
|
@ -7,6 +8,14 @@
|
|||
#include <unistd.h>
|
||||
#include <poll.h>
|
||||
|
||||
#if defined(__linux__) && defined(__NR_openat2)
|
||||
# define HAVE_OPENAT2 1
|
||||
# include <sys/syscall.h>
|
||||
# include <linux/openat2.h>
|
||||
#else
|
||||
# define HAVE_OPENAT2 0
|
||||
#endif
|
||||
|
||||
#include "util-config-private.hh"
|
||||
#include "util-unix-config-private.hh"
|
||||
|
||||
|
|
@ -223,4 +232,104 @@ void unix::closeOnExec(int fd)
|
|||
throw SysError("setting close-on-exec flag");
|
||||
}
|
||||
|
||||
#ifdef __linux__
|
||||
|
||||
namespace linux {
|
||||
|
||||
std::optional<Descriptor> openat2(Descriptor dirFd, const char * path, uint64_t flags, uint64_t mode, uint64_t resolve)
|
||||
{
|
||||
# if HAVE_OPENAT2
|
||||
/* Cache the result of whether openat2 is not supported. */
|
||||
static std::atomic_flag unsupported{};
|
||||
|
||||
if (!unsupported.test()) {
|
||||
/* No glibc wrapper yet, but there's a patch:
|
||||
* https://patchwork.sourceware.org/project/glibc/patch/20251029200519.3203914-1-adhemerval.zanella@linaro.org/
|
||||
*/
|
||||
auto how = ::open_how{.flags = flags, .mode = mode, .resolve = resolve};
|
||||
auto res = ::syscall(__NR_openat2, dirFd, path, &how, sizeof(how));
|
||||
/* Cache that the syscall is not supported. */
|
||||
if (res < 0 && errno == ENOSYS) {
|
||||
unsupported.test_and_set();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
# endif
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace linux
|
||||
|
||||
#endif
|
||||
|
||||
static Descriptor
|
||||
openFileEnsureBeneathNoSymlinksIterative(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode)
|
||||
{
|
||||
AutoCloseFD parentFd;
|
||||
auto nrComponents = std::ranges::distance(path);
|
||||
auto components = std::views::take(path, nrComponents - 1); /* Everything but last component */
|
||||
auto getParentFd = [&]() { return parentFd ? parentFd.get() : dirFd; };
|
||||
|
||||
/* This rather convoluted loop is necessary to avoid TOCTOU when validating that
|
||||
no inner path component is a symlink. */
|
||||
for (auto it = components.begin(); it != components.end(); ++it) {
|
||||
auto component = std::string(*it); /* Copy into a string to make NUL terminated. */
|
||||
assert(component != ".." && !component.starts_with('/')); /* In case invariant is broken somehow.. */
|
||||
|
||||
AutoCloseFD parentFd2 = ::openat(
|
||||
getParentFd(), /* First iteration uses dirFd. */
|
||||
component.c_str(),
|
||||
O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC
|
||||
#ifdef __linux__
|
||||
| O_PATH /* Linux-specific optimization. Files are open only for path resolution purposes. */
|
||||
#endif
|
||||
#ifdef __FreeBSD__
|
||||
| O_RESOLVE_BENEATH /* Further guard against any possible SNAFUs. */
|
||||
#endif
|
||||
);
|
||||
|
||||
if (!parentFd2) {
|
||||
/* Construct the CanonPath for error message. */
|
||||
auto path2 = std::ranges::fold_left(components.begin(), ++it, CanonPath::root, [](auto lhs, auto rhs) {
|
||||
lhs.push(rhs);
|
||||
return lhs;
|
||||
});
|
||||
|
||||
if (errno == ENOTDIR) /* Path component might be a symlink. */ {
|
||||
struct ::stat st;
|
||||
if (::fstatat(getParentFd(), component.c_str(), &st, AT_SYMLINK_NOFOLLOW) == 0 && S_ISLNK(st.st_mode))
|
||||
throw unix::SymlinkNotAllowed(path2);
|
||||
errno = ENOTDIR; /* Restore the errno. */
|
||||
} else if (errno == ELOOP) {
|
||||
throw unix::SymlinkNotAllowed(path2);
|
||||
}
|
||||
|
||||
return INVALID_DESCRIPTOR;
|
||||
}
|
||||
|
||||
parentFd = std::move(parentFd2);
|
||||
}
|
||||
|
||||
auto res = ::openat(getParentFd(), std::string(path.baseName().value()).c_str(), flags | O_NOFOLLOW, mode);
|
||||
if (res < 0 && errno == ELOOP)
|
||||
throw unix::SymlinkNotAllowed(path);
|
||||
return res;
|
||||
}
|
||||
|
||||
Descriptor unix::openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode)
|
||||
{
|
||||
#ifdef __linux__
|
||||
auto maybeFd = linux::openat2(
|
||||
dirFd, path.rel_c_str(), flags, static_cast<uint64_t>(mode), RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS);
|
||||
if (maybeFd) {
|
||||
if (*maybeFd < 0 && errno == ELOOP)
|
||||
throw unix::SymlinkNotAllowed(path);
|
||||
return *maybeFd;
|
||||
}
|
||||
#endif
|
||||
return openFileEnsureBeneathNoSymlinksIterative(dirFd, path, flags, mode);
|
||||
}
|
||||
|
||||
} // namespace nix
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue