diff options
author | Reepca Russelstein <reepca@russelstein.xyz> | 2025-03-28 05:55:51 -0500 |
---|---|---|
committer | John Kehayias <john.kehayias@protonmail.com> | 2025-06-24 10:07:55 -0400 |
commit | 7173c2c0cad8afc9d8d1ad26f345b5a04f47716a (patch) | |
tree | 7e630ab5e5d123494609bac10d07a7e7c3548593 | |
parent | a183afa8e251e86d9dc17e8f177deeef0c1d534d (diff) |
daemon: Implement ‘deletePath’ in terms of the *at functions.
deletePath needs to be able to operate securely in unfriendly environments,
where adversaries may be concurrently modifying the files being operated on.
For example, directories that we are currently recursing through may be
replaced with symbolic links.
We err on the side of early failure here: if a file or directory is
concurrently modified in a way that causes one of the system calls to fail, we
throw an exception immediately instead of trying to adapt to the change.
Note that we use fstat instead of fstatat for verifying the directory's
st_mode field because AT_EMPTY_PATH is linux-specific.
* nix/libutil/util.cc (_deletePathAt): new procedure.
(_deletePath): use it.
Change-Id: I7ccfe6f1f74dbab95617b24034494e0f63030582
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
Signed-off-by: John Kehayias <john.kehayias@protonmail.com>
-rw-r--r-- | nix/libutil/util.cc | 58 |
1 files changed, 42 insertions, 16 deletions
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc index 74f7a97cc4..c406325cdc 100644 --- a/nix/libutil/util.cc +++ b/nix/libutil/util.cc @@ -323,47 +323,73 @@ void writeLine(int fd, string s) } -static void _deletePath(const Path & path, unsigned long long & bytesFreed, size_t linkThreshold) +static void _deletePathAt(int fd, const Path & path, const Path & fullPath, unsigned long long & bytesFreed, size_t linkThreshold) { checkInterrupt(); - printMsg(lvlVomit, format("%1%") % path); + printMsg(lvlVomit, format("%1%") % fullPath); #ifdef HAVE_STATX # define st_mode stx_mode # define st_size stx_size # define st_nlink stx_nlink +#define fstatat(fd, path, stat, flags) \ + statx(fd, path, flags, STATX_SIZE | STATX_NLINK | STATX_MODE, stat) +#define fstat(fd, stat) \ + statx(fd, "", AT_EMPTY_PATH, STATX_SIZE | STATX_NLINK | STATX_MODE, stat) struct statx st; - if (statx(AT_FDCWD, path.c_str(), - AT_SYMLINK_NOFOLLOW, - STATX_SIZE | STATX_NLINK | STATX_MODE, &st) == -1) - throw SysError(format("getting status of `%1%'") % path); #else - struct stat st = lstat(path); + struct stat st; #endif + if (fstatat(fd, path.c_str(), &st, AT_SYMLINK_NOFOLLOW)) + throw SysError(format("getting status of `%1%'") % fullPath); + /* Note: if another process modifies what is at 'path' between now and + when we actually delete it, this may be inaccurate, but I know of no + way to check which file we actually deleted after the fact. */ if (!S_ISDIR(st.st_mode) && st.st_nlink <= linkThreshold) bytesFreed += st.st_size; if (S_ISDIR(st.st_mode)) { - /* Make the directory writable. */ - if (!(st.st_mode & S_IWUSR)) { - if (chmod(path.c_str(), st.st_mode | S_IWUSR) == -1) - throw SysError(format("making `%1%' writable") % path); - } + /* Note: fds required scales with depth of directory nesting */ + AutoCloseFD dirfd = openat(fd, path.c_str(), + O_RDONLY | + O_DIRECTORY | + O_NOFOLLOW | + O_CLOEXEC); + if(!dirfd.isOpen()) + throw SysError(format("opening `%1%'") % fullPath); + + /* st.st_mode may currently be from a different file than what we + actually opened, get it straight from the file instead */ + if(fstat(dirfd, &st)) + throw SysError(format("re-getting status of `%1'") % fullPath); - for (auto & i : readDirectory(path)) - _deletePath(path + "/" + i.name, bytesFreed, linkThreshold); + /* Make the directory writable. */ + if (!(st.st_mode & S_IWUSR)) { + if (fchmod(dirfd, st.st_mode | S_IWUSR) == -1) + throw SysError(format("making `%1%' writable") % fullPath); + } + + for (auto & i : readDirectory(dirfd)) + _deletePathAt(dirfd, i.name, path + "/" + i.name, bytesFreed, linkThreshold); } int ret; - ret = S_ISDIR(st.st_mode) ? rmdir(path.c_str()) : unlink(path.c_str()); + ret = unlinkat(fd, path.c_str(), S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0 ); if (ret == -1) - throw SysError(format("cannot unlink `%1%'") % path); + throw SysError(format("cannot unlink `%1%'") % fullPath); #undef st_mode #undef st_size #undef st_nlink +#undef fstatat +#undef fstat +} + +static void _deletePath(const Path & path, unsigned long long & bytesFreed, size_t linkThreshold) +{ + _deletePathAt(AT_FDCWD, path, path, bytesFreed, linkThreshold); } |