From 75da37f792faac38bad81882707379e3902d1932 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Tue, 6 Jan 2026 18:46:34 +0300 Subject: [PATCH 1/2] libutil-tests: Move unix-specific tests for file descriptors to unix/file-descriptor.cc --- src/libutil-tests/file-system.cc | 64 +-------------------- src/libutil-tests/meson.build | 4 ++ src/libutil-tests/unix/file-descriptor.cc | 69 +++++++++++++++++++++++ src/libutil-tests/unix/meson.build | 3 + 4 files changed, 77 insertions(+), 63 deletions(-) create mode 100644 src/libutil-tests/unix/file-descriptor.cc create mode 100644 src/libutil-tests/unix/meson.build 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/unix/file-descriptor.cc b/src/libutil-tests/unix/file-descriptor.cc new file mode 100644 index 00000000000..36a1c6118bb --- /dev/null +++ b/src/libutil-tests/unix/file-descriptor.cc @@ -0,0 +1,69 @@ +#include + +#include "nix/util/file-descriptor.hh" +#include "nix/util/file-system.hh" +#include "nix/util/fs-sink.hh" + +#include + +namespace nix { + +/* ---------------------------------------------------------------------------- + * openFileEnsureBeneathNoSymlinks + * --------------------------------------------------------------------------*/ + +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)}); +} + +} // 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', +) From 9a637523172696c3847cb245e66d87a0f0c7c57a Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Tue, 6 Jan 2026 19:32:59 +0300 Subject: [PATCH 2/2] libutil: Implement unix::fchmodatTryNoFollow Using fchmodat after a fstatat in deletePath has a slight TOCTOU window. We can plug it by using fchmodat (the libc wrapper with AT_SYMLINK_NOFOLLOW), but it tries fchmodat2 and falls back to the O_PATH trick while failing when procfs isn't mounted. We can do a bit better than that and also cache whether syscalls are unsupported to avoid the repeated context switching that glibc would impose. Also tests the fallback path. It's only for kernels older than 6.6 and when procfs isn't accessible that we fall back to the racy fchmodat without AT_SYMLINK_NOFOLLOW. What previously used to be: openat(AT_FDCWD, "/tmp/store-race/nix/var/nix/builds", O_RDONLY) = 11 newfstatat(11, "nix-2704212-84654554", {st_mode=S_IFDIR|000, st_size=3, ...}, AT_SYMLINK_NOFOLLOW) = 0 fchmodat(11, "nix-2704212-84654554", 040700) = 0 Is now a TOCTOU-free sequence of syscalls: openat(AT_FDCWD, "/tmp/store-race/nix/var/nix/builds", O_RDONLY) = 11 newfstatat(11, "nix-2704953-1733606057", {st_mode=S_IFDIR|000, st_size=3, ...}, AT_SYMLINK_NOFOLLOW) = 0 fchmodat2(11, "nix-2704953-1733606057", 040700, AT_SYMLINK_NOFOLLOW) = 0 Or if the fchmodat2 is not supported: openat(11, "nix-2705443-3010460784", O_RDONLY|O_NOFOLLOW|O_CLOEXEC|O_PATH) = 12 fstat(12, {st_mode=S_IFDIR|000, st_size=3, ...}) = 0 chmod("/proc/self/fd/12", 040700) = 0 openat(11, "nix-2705443-3010460784", O_RDONLY|O_NOFOLLOW|O_DIRECTORY) = 12 This prevents a potentially arbitrary chmod that follows symlinks, though the race window is very small. Also in the case that fchmodat2 isn't supported we could instead open the /proc/self/fd/N path instead of using openat, but that's pretty much equivalent. We only care about ensuring that the thing we chmodded wasn't a symlink since fchmodat follows symlinks and the support for AT_SYMLINK_NOFOLLOW in libc for that is pretty spotty on Linux. E.g. glibc fails if the AT_SYMLINK_NOFOLLOW is specified and procfs isn't available even on regular files. The patch also includes a test that uses a user namespace on Linux to test this exact scenario (though it's rather exotic). --- src/libutil-tests/package.nix | 1 + src/libutil-tests/unix/file-descriptor.cc | 127 +++++++++++++++++- .../include/nix/util/file-descriptor.hh | 11 ++ src/libutil/unix/file-descriptor.cc | 75 +++++++++++ src/libutil/unix/file-system.cc | 13 +- 5 files changed, 222 insertions(+), 5 deletions(-) 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 index 36a1c6118bb..b5e3c50b4e4 100644 --- a/src/libutil-tests/unix/file-descriptor.cc +++ b/src/libutil-tests/unix/file-descriptor.cc @@ -3,11 +3,20 @@ #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 * --------------------------------------------------------------------------*/ @@ -16,7 +25,6 @@ TEST(openFileEnsureBeneathNoSymlinks, works) { std::filesystem::path tmpDir = nix::createTempDir(); nix::AutoDelete delTmpDir(tmpDir, /*recursive=*/true); - using namespace nix::unix; { RestoreSink sink(/*startFsync=*/false); @@ -66,4 +74,121 @@ TEST(openFileEnsureBeneathNoSymlinks, works) 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/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)