diff options
| author | Reepca Russelstein <reepca@russelstein.xyz> | 2026-02-15 12:39:34 -0600 |
|---|---|---|
| committer | Ludovic Courtès <ludo@gnu.org> | 2026-02-27 23:54:00 +0100 |
| commit | 865cb0188c282726008c56319a853c7dd82c4057 (patch) | |
| tree | a6d6fa69b00786b734d7a2a704e19794c1984594 /nix | |
| parent | a1611ced6de6e58ec77641281a49ed368d7d8cef (diff) | |
daemon: Actually remove unreadable directories.
Fixes a regression introduced in 7173c2c0ca. Additional discussion at
https://codeberg.org/guix/guix/pulls/5977.
* nix/libutil/util.cc (_deletePathAt): chmod directory and retry open when it
fails with EACCES. Do this using an O_PATH file descriptor referenced via
/proc/self/fd whenever possible to avoid it being replaced by a
non-directory immediately before being chmod'ed.
* nix/libutil/util.hh (deletePath): document TOCTTOU race on non-linux systems
where hardlinks aren't protected.
* tests/derivations.scm ("unreadable directories in build tree can be
removed"): new test.
Fixes: guix/guix#5891
Reported-by: Liliana Marie Prikler <liliana.prikler@gmail.com>
Change-Id: I749127fe5254ebabc8387a2f0ef47e3c116bfcc5
Signed-off-by: Ludovic Courtès <ludo@gnu.org>
Merges: #6460
Diffstat (limited to 'nix')
| -rw-r--r-- | nix/libutil/util.cc | 51 | ||||
| -rw-r--r-- | nix/libutil/util.hh | 9 |
2 files changed, 57 insertions, 3 deletions
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc index ed1a371dfe1..95f293ff10f 100644 --- a/nix/libutil/util.cc +++ b/nix/libutil/util.cc @@ -380,8 +380,55 @@ static void _deletePathAt(int fd, const Path & path, const Path & fullPath, unsi O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - if(!dirfd.isOpen()) - throw SysError(std::format("opening `{}'", fullPath)); + if(!dirfd.isOpen()) { + if (errno != EACCES) + throw SysError(std::format("opening `{}'", fullPath)); + /* Target directory must not have the right permissions for us to + * access it. Try changing them. We only do this after the + * initial attempt fails because some of the ways we might try + * changing the permissions have race conditions, and we'd rather + * avoid them if we can (e.g. because we're root). */ +#ifdef O_PATH + { + AutoCloseFD pathfd = openat(fd, path.c_str(), + O_PATH | + O_DIRECTORY | + O_NOFOLLOW | + O_CLOEXEC); + if (!pathfd.isOpen()) + throw SysError(std::format("opening `{}'", fullPath)); + + /* fchmod doesn't work with O_PATH file descriptors. fchmodat + * does, but only on very recent kernels (linux 6.6). Despite + * this, regular chmod will work with a /proc/self/fd/N + * filename that names an O_PATH file descriptor. */ + string procPath = "/proc/self/fd/" + std::to_string(pathfd); + if (chmod(procPath.c_str(), S_IRUSR | S_IWUSR | S_IXUSR) != 0) { + if (errno != ENOENT) + throw SysError(std::format("chmod of `{}", procPath)); + /* Fall through */ + } else { + goto retry_open; + } + } +#endif + /* !!! If a malicious process can have replaced the directory at + PATH with a hardlink to an important file, this may change + its permissions to become overly-strict! This should only + be a concern where /proc/sys/fs/protected_hardlinks is 0, or + on systems without protected_hardlinks. */ + if (fchmodat(fd, path.c_str(), S_IRUSR | S_IWUSR | S_IXUSR, AT_SYMLINK_NOFOLLOW) != 0) + throw SysError(std::format("fchmodat of `{}'", fullPath)); + + retry_open: + dirfd = openat(fd, path.c_str(), + O_RDONLY | + O_DIRECTORY | + O_NOFOLLOW | + O_CLOEXEC); + if (!dirfd.isOpen()) + throw SysError(std::format("opening `{}'", fullPath)); + } /* st.st_mode may currently be from a different file than what we actually opened, get it straight from the file instead */ diff --git a/nix/libutil/util.hh b/nix/libutil/util.hh index 3bf2f0be754..44d579f3d6a 100644 --- a/nix/libutil/util.hh +++ b/nix/libutil/util.hh @@ -98,7 +98,14 @@ void writeLine(int fd, string s); /* Delete a path; i.e., in the case of a directory, it is deleted recursively. Don't use this at home, kids. The second variant returns the number of bytes and blocks freed, and 'linkThreshold' denotes - the number of links under which a file is accounted for in 'bytesFreed'. */ + the number of links under which a file is accounted for in 'bytesFreed'. + + Note that if a directory is unreadable, chmod will be invoked on it to make + it u+rwx. On non-linux systems with no equivalent to + /proc/sys/fs/protected_hardlinks, a TOCTTOU race may allow the directory to + be replaced with a hardlink to an important file, making that file's + permissions overly-strict. + */ void deletePath(const Path & path); void deletePath(const Path & path, unsigned long long & bytesFreed, |
