Skip to content
Merged
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
59 changes: 59 additions & 0 deletions src/libutil-tests/file-system.cc
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -318,4 +319,62 @@ 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);
using namespace nix::unix;

{
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");
sink.createDirectory(CanonPath("a/b"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {
dirSink.createDirectory(CanonPath("d"));
dirSink.createSymlink(CanonPath("c"), "./d");
});
sink.createDirectory(CanonPath("a/b/c/e")); // FIXME: This still follows symlinks
ASSERT_THROW(
sink.createDirectory(
CanonPath("a/b/c/f"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {}),
SymlinkNotAllowed);
}

AutoCloseFD dirFd = openDirectory(tmpDir);

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_THROW(open("a/b/c/d", 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
3 changes: 2 additions & 1 deletion src/libutil/fs-sink.cc
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ void RestoreSink::createDirectory(const CanonPath & path, DirectoryCreatedCallba

RestoreSink dirSink{startFsync};
dirSink.dstPath = append(dstPath, path);
dirSink.dirFd = ::openat(dirFd.get(), path.rel_c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
dirSink.dirFd =
unix::openFileEnsureBeneathNoSymlinks(dirFd.get(), path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);

if (!dirSink.dirFd)
throw SysError("opening directory '%s'", dirSink.dstPath.string());
Expand Down
58 changes: 58 additions & 0 deletions src/libutil/include/nix/util/file-descriptor.hh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
///@file

#include "nix/util/canon-path.hh"
#include "nix/util/types.hh"
#include "nix/util/error.hh"

Expand Down Expand Up @@ -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 {

Expand All @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

N.B. we might want to reuse this for other things, in future PR.

/* 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
109 changes: 109 additions & 0 deletions src/libutil/unix/file-descriptor.cc
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"

Expand Down Expand Up @@ -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