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
164 changes: 0 additions & 164 deletions src/libutil-tests/file-descriptor.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@

#include "nix/util/file-descriptor.hh"
#include "nix/util/serialise.hh"
#include "nix/util/file-system.hh"
#include "nix/util/fs-sink.hh"

#include <cstring>

Expand Down Expand Up @@ -245,166 +243,4 @@ TEST(BufferedSourceReadLine, BufferExhaustedThenEof)
EXPECT_EQ(source.readLine(/*eofOk=*/true), "");
}

/* ----------------------------------------------------------------------------
* readLinkAt
* --------------------------------------------------------------------------*/

TEST(readLinkAt, works)
{
std::filesystem::path tmpDir = nix::createTempDir();
nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true);

constexpr size_t maxPathLength =
#ifdef _WIN32
260
#else
PATH_MAX
#endif
;
std::string mediumTarget(maxPathLength / 2, 'x');
std::string longTarget(maxPathLength - 1, 'y');

{
RestoreSink sink(/*startFsync=*/false);
sink.dstPath = tmpDir;
sink.dirFd = openDirectory(tmpDir);
sink.createSymlink(CanonPath("link"), "target");
sink.createSymlink(CanonPath("relative"), "../relative/path");
sink.createSymlink(CanonPath("absolute"), "/absolute/path");
sink.createSymlink(CanonPath("medium"), mediumTarget);
sink.createSymlink(CanonPath("long"), longTarget);
sink.createDirectory(CanonPath("a"));
sink.createDirectory(CanonPath("a/b"));
sink.createSymlink(CanonPath("a/b/link"), "nested_target");
sink.createRegularFile(CanonPath("regular"), [](CreateRegularFileSink &) {});
sink.createDirectory(CanonPath("dir"));
}

AutoCloseFD dirFd = openDirectory(tmpDir);

EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("link")), OS_STR("target"));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("relative")), OS_STR("../relative/path"));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("absolute")), OS_STR("/absolute/path"));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("medium")), string_to_os_string(mediumTarget));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("long")), string_to_os_string(longTarget));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("a/b/link")), OS_STR("nested_target"));

AutoCloseFD subDirFd = openDirectory(tmpDir / "a");
EXPECT_EQ(readLinkAt(subDirFd.get(), CanonPath("b/link")), OS_STR("nested_target"));

// Test error cases - expect SystemError on both platforms
EXPECT_THROW(readLinkAt(dirFd.get(), CanonPath("regular")), SystemError);
EXPECT_THROW(readLinkAt(dirFd.get(), CanonPath("dir")), SystemError);
EXPECT_THROW(readLinkAt(dirFd.get(), CanonPath("nonexistent")), SystemError);
}

/* ----------------------------------------------------------------------------
* openFileEnsureBeneathNoSymlinks
* --------------------------------------------------------------------------*/

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");
sink.createDirectory(CanonPath("a/b"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {
dirSink.createDirectory(CanonPath("d"));
dirSink.createSymlink(CanonPath("c"), "./d");
});
// FIXME: This still follows symlinks on Unix (incorrectly succeeds)
sink.createDirectory(CanonPath("a/b/c/e"));
// Test that symlinks in intermediate path are detected during nested operations
ASSERT_THROW(
sink.createDirectory(
CanonPath("a/b/c/f"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {}),
SymlinkNotAllowed);
ASSERT_THROW(
sink.createRegularFile(
CanonPath("a/b/c/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); }),
SymlinkNotAllowed);
}

AutoCloseFD dirFd = openDirectory(tmpDir);

// Helper to open files with platform-specific arguments
auto openRead = [&](std::string_view path) -> Descriptor {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
#ifdef _WIN32
FILE_READ_DATA | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
0
#else
O_RDONLY,
0
#endif
);
};

auto openReadDir = [&](std::string_view path) -> Descriptor {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
#ifdef _WIN32
FILE_READ_ATTRIBUTES | SYNCHRONIZE,
FILE_DIRECTORY_FILE
#else
O_RDONLY | O_DIRECTORY,
0
#endif
);
};

auto openCreateExclusive = [&](std::string_view path) -> Descriptor {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
#ifdef _WIN32
FILE_WRITE_DATA | SYNCHRONIZE,
0,
FILE_CREATE // Create new file, fail if exists (equivalent to O_CREAT | O_EXCL)
#else
O_CREAT | O_WRONLY | O_EXCL,
0666
#endif
);
};

// Test that symlinks are detected and rejected
EXPECT_THROW(openRead("a/absolute_symlink"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/relative_symlink"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/absolute_symlink/a"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/absolute_symlink/c/d"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/relative_symlink/c"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/b/c/d"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/broken_symlink"), SymlinkNotAllowed);

#if !defined(_WIN32) && !defined(__CYGWIN__)
// This returns ELOOP on cygwin when O_NOFOLLOW is used
EXPECT_EQ(openCreateExclusive("a/broken_symlink"), INVALID_DESCRIPTOR);
/* Sanity check, no symlink shenanigans and behaves the same as regular openat with O_EXCL | O_CREAT. */
EXPECT_EQ(errno, EEXIST);
#endif
EXPECT_THROW(openCreateExclusive("a/absolute_symlink/broken_symlink"), SymlinkNotAllowed);

// Test invalid paths
EXPECT_EQ(openRead("c/d/regular/a"), INVALID_DESCRIPTOR);
EXPECT_EQ(openReadDir("c/d/regular"), INVALID_DESCRIPTOR);

// Test valid paths work
EXPECT_TRUE(AutoCloseFD{openRead("c/d/regular")});
EXPECT_TRUE(AutoCloseFD{openCreateExclusive("a/regular")});
}

} // namespace nix
172 changes: 172 additions & 0 deletions src/libutil-tests/file-system-at.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
#include <gtest/gtest.h>
#include <gmock/gmock.h>

#include "nix/util/file-system-at.hh"
#include "nix/util/file-system.hh"
#include "nix/util/fs-sink.hh"

namespace nix {

/* ----------------------------------------------------------------------------
* readLinkAt
* --------------------------------------------------------------------------*/

TEST(readLinkAt, works)
{
std::filesystem::path tmpDir = nix::createTempDir();
nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true);

constexpr size_t maxPathLength =
#ifdef _WIN32
260
#else
PATH_MAX
#endif
;
std::string mediumTarget(maxPathLength / 2, 'x');
std::string longTarget(maxPathLength - 1, 'y');

{
RestoreSink sink(/*startFsync=*/false);
sink.dstPath = tmpDir;
sink.dirFd = openDirectory(tmpDir);
sink.createSymlink(CanonPath("link"), "target");
sink.createSymlink(CanonPath("relative"), "../relative/path");
sink.createSymlink(CanonPath("absolute"), "/absolute/path");
sink.createSymlink(CanonPath("medium"), mediumTarget);
sink.createSymlink(CanonPath("long"), longTarget);
sink.createDirectory(CanonPath("a"));
sink.createDirectory(CanonPath("a/b"));
sink.createSymlink(CanonPath("a/b/link"), "nested_target");
sink.createRegularFile(CanonPath("regular"), [](CreateRegularFileSink &) {});
sink.createDirectory(CanonPath("dir"));
}

AutoCloseFD dirFd = openDirectory(tmpDir);

EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("link")), OS_STR("target"));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("relative")), OS_STR("../relative/path"));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("absolute")), OS_STR("/absolute/path"));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("medium")), string_to_os_string(mediumTarget));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("long")), string_to_os_string(longTarget));
EXPECT_EQ(readLinkAt(dirFd.get(), CanonPath("a/b/link")), OS_STR("nested_target"));

AutoCloseFD subDirFd = openDirectory(tmpDir / "a");
EXPECT_EQ(readLinkAt(subDirFd.get(), CanonPath("b/link")), OS_STR("nested_target"));

// Test error cases - expect SystemError on both platforms
EXPECT_THROW(readLinkAt(dirFd.get(), CanonPath("regular")), SystemError);
EXPECT_THROW(readLinkAt(dirFd.get(), CanonPath("dir")), SystemError);
EXPECT_THROW(readLinkAt(dirFd.get(), CanonPath("nonexistent")), SystemError);
}

/* ----------------------------------------------------------------------------
* openFileEnsureBeneathNoSymlinks
* --------------------------------------------------------------------------*/

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");
sink.createDirectory(CanonPath("a/b"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {
dirSink.createDirectory(CanonPath("d"));
dirSink.createSymlink(CanonPath("c"), "./d");
});
// FIXME: This still follows symlinks on Unix (incorrectly succeeds)
sink.createDirectory(CanonPath("a/b/c/e"));
// Test that symlinks in intermediate path are detected during nested operations
ASSERT_THROW(
sink.createDirectory(
CanonPath("a/b/c/f"), [](FileSystemObjectSink & dirSink, const CanonPath & relPath) {}),
SymlinkNotAllowed);
ASSERT_THROW(
sink.createRegularFile(
CanonPath("a/b/c/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); }),
SymlinkNotAllowed);
}

AutoCloseFD dirFd = openDirectory(tmpDir);

// Helper to open files with platform-specific arguments
auto openRead = [&](std::string_view path) -> Descriptor {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
#ifdef _WIN32
FILE_READ_DATA | FILE_READ_ATTRIBUTES | SYNCHRONIZE,
0
#else
O_RDONLY,
0
#endif
);
};

auto openReadDir = [&](std::string_view path) -> Descriptor {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
#ifdef _WIN32
FILE_READ_ATTRIBUTES | SYNCHRONIZE,
FILE_DIRECTORY_FILE
#else
O_RDONLY | O_DIRECTORY,
0
#endif
);
};

auto openCreateExclusive = [&](std::string_view path) -> Descriptor {
return openFileEnsureBeneathNoSymlinks(
dirFd.get(),
CanonPath(path),
#ifdef _WIN32
FILE_WRITE_DATA | SYNCHRONIZE,
0,
FILE_CREATE // Create new file, fail if exists (equivalent to O_CREAT | O_EXCL)
#else
O_CREAT | O_WRONLY | O_EXCL,
0666
#endif
);
};

// Test that symlinks are detected and rejected
EXPECT_THROW(openRead("a/absolute_symlink"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/relative_symlink"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/absolute_symlink/a"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/absolute_symlink/c/d"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/relative_symlink/c"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/b/c/d"), SymlinkNotAllowed);
EXPECT_THROW(openRead("a/broken_symlink"), SymlinkNotAllowed);

#if !defined(_WIN32) && !defined(__CYGWIN__)
// This returns ELOOP on cygwin when O_NOFOLLOW is used
EXPECT_EQ(openCreateExclusive("a/broken_symlink"), INVALID_DESCRIPTOR);
/* Sanity check, no symlink shenanigans and behaves the same as regular openat with O_EXCL | O_CREAT. */
EXPECT_EQ(errno, EEXIST);
#endif
EXPECT_THROW(openCreateExclusive("a/absolute_symlink/broken_symlink"), SymlinkNotAllowed);

// Test invalid paths
EXPECT_EQ(openRead("c/d/regular/a"), INVALID_DESCRIPTOR);
EXPECT_EQ(openReadDir("c/d/regular"), INVALID_DESCRIPTOR);

// Test valid paths work
EXPECT_TRUE(AutoCloseFD{openRead("c/d/regular")});
EXPECT_TRUE(AutoCloseFD{openCreateExclusive("a/regular")});
}

} // namespace nix
1 change: 1 addition & 0 deletions src/libutil-tests/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ sources = files(
'executable-path.cc',
'file-content-address.cc',
'file-descriptor.cc',
'file-system-at.cc',
'file-system.cc',
'git.cc',
'hash.cc',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#include <gtest/gtest.h>

#include "nix/util/file-descriptor.hh"
#include "nix/util/file-system-at.hh"
#include "nix/util/file-system.hh"
#include "nix/util/fs-sink.hh"
#include "nix/util/processes.hh"
Expand Down
2 changes: 1 addition & 1 deletion src/libutil-tests/unix/meson.build
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
sources += files(
'file-descriptor.cc',
'file-system-at.cc',
)
1 change: 1 addition & 0 deletions src/libutil/fs-sink.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

#include "nix/util/error.hh"
#include "nix/util/config-global.hh"
#include "nix/util/file-system-at.hh"
#include "nix/util/fs-sink.hh"

#ifdef _WIN32
Expand Down
Loading
Loading