summaryrefslogtreecommitdiff
path: root/nix
diff options
context:
space:
mode:
authorReepca Russelstein <reepca@russelstein.xyz>2026-02-15 12:39:34 -0600
committerLudovic Courtès <ludo@gnu.org>2026-02-27 23:54:00 +0100
commit865cb0188c282726008c56319a853c7dd82c4057 (patch)
treea6d6fa69b00786b734d7a2a704e19794c1984594 /nix
parenta1611ced6de6e58ec77641281a49ed368d7d8cef (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.cc51
-rw-r--r--nix/libutil/util.hh9
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,