diff --git a/src/libutil-tests/file-system.cc b/src/libutil-tests/file-system.cc index 49c123e79a9..bced4dbd92f 100644 --- a/src/libutil-tests/file-system.cc +++ b/src/libutil-tests/file-system.cc @@ -1,5 +1,5 @@ -#include "nix/util/fs-sink.hh" #include "nix/util/util.hh" +#include "nix/util/serialise.hh" #include "nix/util/types.hh" #include "nix/util/file-system.hh" #include "nix/util/processes.hh" @@ -319,68 +319,6 @@ 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); - ASSERT_THROW( - sink.createRegularFile( - CanonPath("a/b/c/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); }), - 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 - /* ---------------------------------------------------------------------------- * createAnonymousTempFile * --------------------------------------------------------------------------*/ diff --git a/src/libutil-tests/meson.build b/src/libutil-tests/meson.build index 7c3890be90f..9e7318bee85 100644 --- a/src/libutil-tests/meson.build +++ b/src/libutil-tests/meson.build @@ -87,6 +87,10 @@ sources = files( 'xml-writer.cc', ) +if host_machine.system() != 'windows' + subdir('unix') +endif + include_dirs = [ include_directories('.') ] diff --git a/src/libutil-tests/package.nix b/src/libutil-tests/package.nix index c06de6894af..f24d69243b8 100644 --- a/src/libutil-tests/package.nix +++ b/src/libutil-tests/package.nix @@ -32,6 +32,7 @@ mkMesonExecutable (finalAttrs: { ../../.version ./.version ./meson.build + ./unix/meson.build # ./meson.options (fileset.fileFilter (file: file.hasExt "cc") ./.) (fileset.fileFilter (file: file.hasExt "hh") ./.) diff --git a/src/libutil-tests/unix/file-descriptor.cc b/src/libutil-tests/unix/file-descriptor.cc new file mode 100644 index 00000000000..b5e3c50b4e4 --- /dev/null +++ b/src/libutil-tests/unix/file-descriptor.cc @@ -0,0 +1,194 @@ +#include + +#include "nix/util/file-descriptor.hh" +#include "nix/util/file-system.hh" +#include "nix/util/fs-sink.hh" +#include "nix/util/processes.hh" + +#ifdef __linux__ +# include "nix/util/linux-namespaces.hh" + +# include +#endif + +#include + +namespace nix { + +using namespace nix::unix; + +/* ---------------------------------------------------------------------------- + * 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"); + }); + 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); + ASSERT_THROW( + sink.createRegularFile( + CanonPath("a/b/c/regular"), [](CreateRegularFileSink & crf) { crf("some contents"); }), + 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)}); +} + +/* ---------------------------------------------------------------------------- + * fchmodatTryNoFollow + * --------------------------------------------------------------------------*/ + +TEST(fchmodatTryNoFollow, 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.createRegularFile(CanonPath("file"), [](CreateRegularFileSink & crf) {}); + sink.createDirectory(CanonPath("dir")); + sink.createSymlink(CanonPath("filelink"), "file"); + sink.createSymlink(CanonPath("dirlink"), "dir"); + } + + ASSERT_EQ(chmod((tmpDir / "file").c_str(), 0644), 0); + ASSERT_EQ(chmod((tmpDir / "dir").c_str(), 0755), 0); + + AutoCloseFD dirFd = openDirectory(tmpDir); + ASSERT_TRUE(dirFd); + + struct ::stat st; + + /* Check that symlinks are not followed and targets are not changed. */ + + EXPECT_NO_THROW( + try { fchmodatTryNoFollow(dirFd.get(), CanonPath("filelink"), 0777); } catch (SysError & e) { + if (e.errNo != EOPNOTSUPP) + throw; + }); + ASSERT_EQ(stat((tmpDir / "file").c_str(), &st), 0); + EXPECT_EQ(st.st_mode & 0777, 0644); + + EXPECT_NO_THROW( + try { fchmodatTryNoFollow(dirFd.get(), CanonPath("dirlink"), 0777); } catch (SysError & e) { + if (e.errNo != EOPNOTSUPP) + throw; + }); + ASSERT_EQ(stat((tmpDir / "dir").c_str(), &st), 0); + EXPECT_EQ(st.st_mode & 0777, 0755); + + /* Check fchmodatTryNoFollow works on regular files and directories. */ + + EXPECT_NO_THROW(fchmodatTryNoFollow(dirFd.get(), CanonPath("file"), 0600)); + ASSERT_EQ(stat((tmpDir / "file").c_str(), &st), 0); + EXPECT_EQ(st.st_mode & 0777, 0600); + + EXPECT_NO_THROW((fchmodatTryNoFollow(dirFd.get(), CanonPath("dir"), 0700), 0)); + ASSERT_EQ(stat((tmpDir / "dir").c_str(), &st), 0); + EXPECT_EQ(st.st_mode & 0777, 0700); +} + +#ifdef __linux__ + +TEST(fchmodatTryNoFollow, fallbackWithoutProc) +{ + if (!userNamespacesSupported()) + GTEST_SKIP() << "User namespaces not supported"; + + std::filesystem::path tmpDir = nix::createTempDir(); + nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true); + + { + RestoreSink sink(/*startFsync=*/false); + sink.dstPath = tmpDir; + sink.dirFd = openDirectory(tmpDir); + sink.createRegularFile(CanonPath("file"), [](CreateRegularFileSink & crf) {}); + sink.createSymlink(CanonPath("link"), "file"); + } + + ASSERT_EQ(chmod((tmpDir / "file").c_str(), 0644), 0); + + Pid pid = startProcess( + [&] { + if (unshare(CLONE_NEWNS) == -1) + _exit(1); + + if (mount(0, "/", 0, MS_PRIVATE | MS_REC, 0) == -1) + _exit(1); + + if (mount("tmpfs", "/proc", "tmpfs", 0, 0) == -1) + _exit(1); + + AutoCloseFD dirFd = openDirectory(tmpDir); + if (!dirFd) + exit(1); + + try { + fchmodatTryNoFollow(dirFd.get(), CanonPath("file"), 0600); + } catch (SysError & e) { + _exit(1); + } + + try { + fchmodatTryNoFollow(dirFd.get(), CanonPath("link"), 0777); + } catch (SysError & e) { + if (e.errNo == EOPNOTSUPP) + _exit(0); /* Success. */ + } + + _exit(1); /* Didn't throw the expected exception. */ + }, + {.cloneFlags = CLONE_NEWUSER}); + + int status = pid.wait(); + ASSERT_TRUE(statusOk(status)); + + struct ::stat st; + ASSERT_EQ(stat((tmpDir / "file").c_str(), &st), 0); + EXPECT_EQ(st.st_mode & 0777, 0600); +} +#endif + +} // namespace nix diff --git a/src/libutil-tests/unix/meson.build b/src/libutil-tests/unix/meson.build new file mode 100644 index 00000000000..2861148a255 --- /dev/null +++ b/src/libutil-tests/unix/meson.build @@ -0,0 +1,3 @@ +sources += files( + 'file-descriptor.cc', +) diff --git a/src/libutil/include/nix/util/file-descriptor.hh b/src/libutil/include/nix/util/file-descriptor.hh index 1ff46ade460..cfb0fb8ee13 100644 --- a/src/libutil/include/nix/util/file-descriptor.hh +++ b/src/libutil/include/nix/util/file-descriptor.hh @@ -257,6 +257,17 @@ namespace unix { */ Descriptor openFileEnsureBeneathNoSymlinks(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode = 0); +/** + * Try to change the mode of file named by \ref path relative to the parent directory denoted by \ref dirFd. + * + * @note When on linux without fchmodat2 support and without procfs mounted falls back to fchmodat without + * AT_SYMLINK_NOFOLLOW, since it's the best we can do without failing. + * + * @pre path.isRoot() is false + * @throws SysError if any operation fails + */ +void fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t mode); + } // namespace unix #endif diff --git a/src/libutil/unix/file-descriptor.cc b/src/libutil/unix/file-descriptor.cc index bdb8054eb5a..99ff9011dba 100644 --- a/src/libutil/unix/file-descriptor.cc +++ b/src/libutil/unix/file-descriptor.cc @@ -17,6 +17,12 @@ # define HAVE_OPENAT2 0 #endif +#if defined(__linux__) && defined(__NR_fchmodat2) +# define HAVE_FCHMODAT2 1 +#else +# define HAVE_FCHMODAT2 0 +#endif + #include "util-config-private.hh" #include "util-unix-config-private.hh" @@ -265,6 +271,75 @@ std::optional openat2(Descriptor dirFd, const char * path, uint64_t #endif +void unix::fchmodatTryNoFollow(Descriptor dirFd, const CanonPath & path, mode_t mode) +{ + assert(!path.isRoot()); + +#if HAVE_FCHMODAT2 + /* Cache whether fchmodat2 is not supported. */ + static std::atomic_flag fchmodat2Unsupported{}; + if (!fchmodat2Unsupported.test()) { + /* Try with fchmodat2 first. */ + auto res = ::syscall(__NR_fchmodat2, dirFd, path.rel_c_str(), mode, AT_SYMLINK_NOFOLLOW); + /* Cache that the syscall is not supported. */ + if (res < 0) { + if (errno == ENOSYS) + fchmodat2Unsupported.test_and_set(); + else + throw SysError("fchmodat2 '%s' relative to parent directory", path.rel()); + } else + return; + } +#endif + +#ifdef __linux__ + AutoCloseFD pathFd = ::openat(dirFd, path.rel_c_str(), O_PATH | O_NOFOLLOW | O_CLOEXEC); + if (!pathFd) + throw SysError( + "opening '%s' relative to parent directory to get an O_PATH file descriptor (fchmodat2 is unsupported)", + path.rel()); + + struct ::stat st; + /* Possible since https://github.com/torvalds/linux/commit/55815f70147dcfa3ead5738fd56d3574e2e3c1c2 (3.6) */ + if (::fstat(pathFd.get(), &st) == -1) + throw SysError("statting '%s' relative to parent directory via O_PATH file descriptor", path.rel()); + + if (S_ISLNK(st.st_mode)) + throw SysError(EOPNOTSUPP, "can't change mode of symlink '%s' relative to parent directory", path.rel()); + + static std::atomic_flag dontHaveProc{}; + if (!dontHaveProc.test()) { + static const CanonPath selfProcFd = CanonPath("/proc/self/fd"); + + auto selfProcFdPath = selfProcFd / std::to_string(pathFd.get()); + if (int res = ::chmod(selfProcFdPath.c_str(), mode); res == -1) { + if (errno == ENOENT) + dontHaveProc.test_and_set(); + else + throw SysError("chmod '%s' ('%s' relative to parent directory)", selfProcFdPath, path); + } else + return; + } + + static std::atomic warned = false; + warnOnce(warned, "kernel doesn't support fchmodat2 and procfs isn't mounted, falling back to fchmodat"); +#endif + + int res = ::fchmodat( + dirFd, + path.rel_c_str(), + mode, +#if defined(__APPLE__) || defined(__FreeBSD__) + AT_SYMLINK_NOFOLLOW +#else + 0 +#endif + ); + + if (res == -1) + throw SysError("fchmodat '%s' relative to parent directory", path.rel()); +} + static Descriptor openFileEnsureBeneathNoSymlinksIterative(Descriptor dirFd, const CanonPath & path, int flags, mode_t mode) { diff --git a/src/libutil/unix/file-system.cc b/src/libutil/unix/file-system.cc index 72f18cfd5df..692f7fd075d 100644 --- a/src/libutil/unix/file-system.cc +++ b/src/libutil/unix/file-system.cc @@ -139,10 +139,15 @@ static void _deletePath( if (S_ISDIR(st.st_mode)) { /* Make the directory accessible. */ const auto PERM_MASK = S_IRUSR | S_IWUSR | S_IXUSR; - if ((st.st_mode & PERM_MASK) != PERM_MASK) { - if (fchmodat(parentfd, name.c_str(), st.st_mode | PERM_MASK, 0) == -1) - throw SysError("chmod %1%", path); - } + if ((st.st_mode & PERM_MASK) != PERM_MASK) + try { + unix::fchmodatTryNoFollow(parentfd, CanonPath(name), st.st_mode | PERM_MASK); + } catch (SysError & e) { + e.addTrace({}, "while making directory %1% accessible for deletion", path); + if (e.errNo == EOPNOTSUPP) + e.addTrace({}, "%1% is now a symlink, expected directory", path); + throw; + } int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW); if (fd == -1)