From f274a7273ac4930a74ad6172152e1e2f824cedf0 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Wed, 17 Dec 2025 23:47:40 +0300 Subject: [PATCH] libutil: Implement deletePath on windows via std::filesystem::remove_all It doesn't track the number of bytes deleted, but since this code is security critical also we can split unix and windows implementations. If the need arises we can implement a smarter recursive deletion function ourselves in the future. Review with --color-moved. --- src/libutil/file-system.cc | 150 ---------------------------- src/libutil/unix/file-system.cc | 152 +++++++++++++++++++++++++++++ src/libutil/windows/file-system.cc | 14 +++ 3 files changed, 166 insertions(+), 150 deletions(-) diff --git a/src/libutil/file-system.cc b/src/libutil/file-system.cc index 000259777..e06b022fc 100644 --- a/src/libutil/file-system.cc +++ b/src/libutil/file-system.cc @@ -375,14 +375,6 @@ void syncParent(const Path & path) fd.fsync(); } -#ifdef __FreeBSD__ -# define MOUNTEDPATHS_PARAM , std::set & mountedPaths -# define MOUNTEDPATHS_ARG , mountedPaths -#else -# define MOUNTEDPATHS_PARAM -# define MOUNTEDPATHS_ARG -#endif - void recursiveSync(const Path & path) { /* If it's a file or symlink, just fsync and return. */ @@ -427,129 +419,6 @@ void recursiveSync(const Path & path) } } -static void _deletePath( - Descriptor parentfd, - const std::filesystem::path & path, - uint64_t & bytesFreed, - std::exception_ptr & ex MOUNTEDPATHS_PARAM) -{ -#ifndef _WIN32 - checkInterrupt(); - -# ifdef __FreeBSD__ - // In case of emergency (unmount fails for some reason) not recurse into mountpoints. - // This prevents us from tearing up the nullfs-mounted nix store. - if (mountedPaths.find(path) != mountedPaths.end()) { - return; - } -# endif - - std::string name(path.filename()); - assert(name != "." && name != ".." && !name.empty()); - - struct stat st; - if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) { - if (errno == ENOENT) - return; - throw SysError("getting status of %1%", path); - } - - if (!S_ISDIR(st.st_mode)) { - /* We are about to delete a file. Will it likely free space? */ - - switch (st.st_nlink) { - /* Yes: last link. */ - case 1: - bytesFreed += st.st_size; - break; - /* Maybe: yes, if 'auto-optimise-store' or manual optimisation - was performed. Instead of checking for real let's assume - it's an optimised file and space will be freed. - - In worst case we will double count on freed space for files - with exactly two hardlinks for unoptimised packages. - */ - case 2: - bytesFreed += st.st_size; - break; - /* No: 3+ links. */ - default: - break; - } - } - - 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); - } - - int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW); - if (fd == -1) - throw SysError("opening directory %1%", path); - AutoCloseDir dir(fdopendir(fd)); - if (!dir) - throw SysError("opening directory %1%", path); - - struct dirent * dirent; - while (errno = 0, dirent = readdir(dir.get())) { /* sic */ - checkInterrupt(); - std::string childName = dirent->d_name; - if (childName == "." || childName == "..") - continue; - _deletePath(dirfd(dir.get()), path / childName, bytesFreed, ex MOUNTEDPATHS_ARG); - } - if (errno) - throw SysError("reading directory %1%", path); - } - - int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0; - if (unlinkat(parentfd, name.c_str(), flags) == -1) { - if (errno == ENOENT) - return; - try { - throw SysError("cannot unlink %1%", path); - } catch (...) { - if (!ex) - ex = std::current_exception(); - else - ignoreExceptionExceptInterrupt(); - } - } -#else - // TODO implement - throw UnimplementedError("_deletePath"); -#endif -} - -static void _deletePath(const std::filesystem::path & path, uint64_t & bytesFreed MOUNTEDPATHS_PARAM) -{ - assert(path.is_absolute()); - assert(path.parent_path() != path); - - AutoCloseFD dirfd = toDescriptor(open(path.parent_path().string().c_str(), O_RDONLY)); - if (!dirfd) { - if (errno == ENOENT) - return; - throw SysError("opening directory %s", path.parent_path()); - } - - std::exception_ptr ex; - - _deletePath(dirfd.get(), path, bytesFreed, ex MOUNTEDPATHS_ARG); - - if (ex) - std::rethrow_exception(ex); -} - -void deletePath(const std::filesystem::path & path) -{ - uint64_t dummy; - deletePath(path, dummy); -} - void createDir(const Path & path, mode_t mode) { if (mkdir( @@ -572,25 +441,6 @@ void createDirs(const std::filesystem::path & path) } } -void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed) -{ - // Activity act(*logger, lvlDebug, "recursively deleting path '%1%'", path); -#ifdef __FreeBSD__ - std::set mountedPaths; - struct statfs * mntbuf; - int count; - if ((count = getmntinfo(&mntbuf, MNT_WAIT)) < 0) { - throw SysError("getmntinfo"); - } - - for (int i = 0; i < count; i++) { - mountedPaths.emplace(mntbuf[i].f_mntonname); - } -#endif - bytesFreed = 0; - _deletePath(path, bytesFreed MOUNTEDPATHS_ARG); -} - ////////////////////////////////////////////////////////////////////// AutoDelete::AutoDelete() diff --git a/src/libutil/unix/file-system.cc b/src/libutil/unix/file-system.cc index 6069c2d36..7086cb258 100644 --- a/src/libutil/unix/file-system.cc +++ b/src/libutil/unix/file-system.cc @@ -10,6 +10,8 @@ #include "nix/util/file-system.hh" #include "nix/util/environment-variables.hh" +#include "nix/util/signals.hh" +#include "nix/util/util.hh" #include "util-unix-config-private.hh" @@ -72,4 +74,154 @@ void setWriteTime( #endif } +#ifdef __FreeBSD__ +# define MOUNTEDPATHS_PARAM , std::set & mountedPaths +# define MOUNTEDPATHS_ARG , mountedPaths +#else +# define MOUNTEDPATHS_PARAM +# define MOUNTEDPATHS_ARG +#endif + +static void _deletePath( + Descriptor parentfd, + const std::filesystem::path & path, + uint64_t & bytesFreed, + std::exception_ptr & ex MOUNTEDPATHS_PARAM) +{ +#ifndef _WIN32 + checkInterrupt(); + +# ifdef __FreeBSD__ + // In case of emergency (unmount fails for some reason) not recurse into mountpoints. + // This prevents us from tearing up the nullfs-mounted nix store. + if (mountedPaths.find(path) != mountedPaths.end()) { + return; + } +# endif + + std::string name(path.filename()); + assert(name != "." && name != ".." && !name.empty()); + + struct stat st; + if (fstatat(parentfd, name.c_str(), &st, AT_SYMLINK_NOFOLLOW) == -1) { + if (errno == ENOENT) + return; + throw SysError("getting status of %1%", path); + } + + if (!S_ISDIR(st.st_mode)) { + /* We are about to delete a file. Will it likely free space? */ + + switch (st.st_nlink) { + /* Yes: last link. */ + case 1: + bytesFreed += st.st_size; + break; + /* Maybe: yes, if 'auto-optimise-store' or manual optimisation + was performed. Instead of checking for real let's assume + it's an optimised file and space will be freed. + + In worst case we will double count on freed space for files + with exactly two hardlinks for unoptimised packages. + */ + case 2: + bytesFreed += st.st_size; + break; + /* No: 3+ links. */ + default: + break; + } + } + + 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); + } + + int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW); + if (fd == -1) + throw SysError("opening directory %1%", path); + AutoCloseDir dir(fdopendir(fd)); + if (!dir) + throw SysError("opening directory %1%", path); + + struct dirent * dirent; + while (errno = 0, dirent = readdir(dir.get())) { /* sic */ + checkInterrupt(); + std::string childName = dirent->d_name; + if (childName == "." || childName == "..") + continue; + _deletePath(dirfd(dir.get()), path / childName, bytesFreed, ex MOUNTEDPATHS_ARG); + } + if (errno) + throw SysError("reading directory %1%", path); + } + + int flags = S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0; + if (unlinkat(parentfd, name.c_str(), flags) == -1) { + if (errno == ENOENT) + return; + try { + throw SysError("cannot unlink %1%", path); + } catch (...) { + if (!ex) + ex = std::current_exception(); + else + ignoreExceptionExceptInterrupt(); + } + } +#else + // TODO implement + throw UnimplementedError("_deletePath"); +#endif +} + +static void _deletePath(const std::filesystem::path & path, uint64_t & bytesFreed MOUNTEDPATHS_PARAM) +{ + assert(path.is_absolute()); + assert(path.parent_path() != path); + + AutoCloseFD dirfd = toDescriptor(open(path.parent_path().string().c_str(), O_RDONLY)); + if (!dirfd) { + if (errno == ENOENT) + return; + throw SysError("opening directory %s", path.parent_path()); + } + + std::exception_ptr ex; + + _deletePath(dirfd.get(), path, bytesFreed, ex MOUNTEDPATHS_ARG); + + if (ex) + std::rethrow_exception(ex); +} + +void deletePath(const std::filesystem::path & path) +{ + uint64_t dummy; + deletePath(path, dummy); +} + +void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed) +{ + // Activity act(*logger, lvlDebug, "recursively deleting path '%1%'", path); +#ifdef __FreeBSD__ + std::set mountedPaths; + struct statfs * mntbuf; + int count; + if ((count = getmntinfo(&mntbuf, MNT_WAIT)) < 0) { + throw SysError("getmntinfo"); + } + + for (int i = 0; i < count; i++) { + mountedPaths.emplace(mntbuf[i].f_mntonname); + } +#endif + bytesFreed = 0; + _deletePath(path, bytesFreed MOUNTEDPATHS_ARG); +} + } // namespace nix diff --git a/src/libutil/windows/file-system.cc b/src/libutil/windows/file-system.cc index 0c021777b..79931d591 100644 --- a/src/libutil/windows/file-system.cc +++ b/src/libutil/windows/file-system.cc @@ -39,4 +39,18 @@ std::filesystem::path defaultTempDir() return std::filesystem::path(buf); } +void deletePath(const std::filesystem::path & path) +{ + std::error_code ec; + std::filesystem::remove_all(path, ec); + if (ec && ec != std::errc::no_such_file_or_directory) + throw SysError(ec.default_error_condition().value(), "recursively deleting %1%", path); +} + +void deletePath(const std::filesystem::path & path, uint64_t & bytesFreed) +{ + bytesFreed = 0; + deletePath(path); +} + } // namespace nix