summaryrefslogtreecommitdiff
path: root/nix
diff options
context:
space:
mode:
authorReepca Russelstein <reepca@russelstein.xyz>2025-03-28 05:55:51 -0500
committerJohn Kehayias <john.kehayias@protonmail.com>2025-06-24 10:07:55 -0400
commit7173c2c0cad8afc9d8d1ad26f345b5a04f47716a (patch)
tree7e630ab5e5d123494609bac10d07a7e7c3548593 /nix
parenta183afa8e251e86d9dc17e8f177deeef0c1d534d (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>
Diffstat (limited to 'nix')
-rw-r--r--nix/libutil/util.cc60
1 files changed, 43 insertions, 17 deletions
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index 74f7a97cc4f..c406325cdcb 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -323,47 +323,73 @@ void writeLine(int fd, string s)
323} 323}
324 324
325 325
326static void _deletePath(const Path & path, unsigned long long & bytesFreed, size_t linkThreshold) 326static void _deletePathAt(int fd, const Path & path, const Path & fullPath, unsigned long long & bytesFreed, size_t linkThreshold)
327{ 327{
328 checkInterrupt(); 328 checkInterrupt();
329 329
330 printMsg(lvlVomit, format("%1%") % path); 330 printMsg(lvlVomit, format("%1%") % fullPath);
331 331
332#ifdef HAVE_STATX 332#ifdef HAVE_STATX
333# define st_mode stx_mode 333# define st_mode stx_mode
334# define st_size stx_size 334# define st_size stx_size
335# define st_nlink stx_nlink 335# define st_nlink stx_nlink
336#define fstatat(fd, path, stat, flags) \
337 statx(fd, path, flags, STATX_SIZE | STATX_NLINK | STATX_MODE, stat)
338#define fstat(fd, stat) \
339 statx(fd, "", AT_EMPTY_PATH, STATX_SIZE | STATX_NLINK | STATX_MODE, stat)
336 struct statx st; 340 struct statx st;
337 if (statx(AT_FDCWD, path.c_str(),
338 AT_SYMLINK_NOFOLLOW,
339 STATX_SIZE | STATX_NLINK | STATX_MODE, &st) == -1)
340 throw SysError(format("getting status of `%1%'") % path);
341#else 341#else
342 struct stat st = lstat(path); 342 struct stat st;
343#endif 343#endif
344 if (fstatat(fd, path.c_str(), &st, AT_SYMLINK_NOFOLLOW))
345 throw SysError(format("getting status of `%1%'") % fullPath);
344 346
347 /* Note: if another process modifies what is at 'path' between now and
348 when we actually delete it, this may be inaccurate, but I know of no
349 way to check which file we actually deleted after the fact. */
345 if (!S_ISDIR(st.st_mode) && st.st_nlink <= linkThreshold) 350 if (!S_ISDIR(st.st_mode) && st.st_nlink <= linkThreshold)
346 bytesFreed += st.st_size; 351 bytesFreed += st.st_size;
347 352
348 if (S_ISDIR(st.st_mode)) { 353 if (S_ISDIR(st.st_mode)) {
349 /* Make the directory writable. */ 354 /* Note: fds required scales with depth of directory nesting */
350 if (!(st.st_mode & S_IWUSR)) { 355 AutoCloseFD dirfd = openat(fd, path.c_str(),
351 if (chmod(path.c_str(), st.st_mode | S_IWUSR) == -1) 356 O_RDONLY |
352 throw SysError(format("making `%1%' writable") % path); 357 O_DIRECTORY |
353 } 358 O_NOFOLLOW |
354 359 O_CLOEXEC);
355 for (auto & i : readDirectory(path)) 360 if(!dirfd.isOpen())
356 _deletePath(path + "/" + i.name, bytesFreed, linkThreshold); 361 throw SysError(format("opening `%1%'") % fullPath);
362
363 /* st.st_mode may currently be from a different file than what we
364 actually opened, get it straight from the file instead */
365 if(fstat(dirfd, &st))
366 throw SysError(format("re-getting status of `%1'") % fullPath);
367
368 /* Make the directory writable. */
369 if (!(st.st_mode & S_IWUSR)) {
370 if (fchmod(dirfd, st.st_mode | S_IWUSR) == -1)
371 throw SysError(format("making `%1%' writable") % fullPath);
372 }
373
374 for (auto & i : readDirectory(dirfd))
375 _deletePathAt(dirfd, i.name, path + "/" + i.name, bytesFreed, linkThreshold);
357 } 376 }
358 377
359 int ret; 378 int ret;
360 ret = S_ISDIR(st.st_mode) ? rmdir(path.c_str()) : unlink(path.c_str()); 379 ret = unlinkat(fd, path.c_str(), S_ISDIR(st.st_mode) ? AT_REMOVEDIR : 0 );
361 if (ret == -1) 380 if (ret == -1)
362 throw SysError(format("cannot unlink `%1%'") % path); 381 throw SysError(format("cannot unlink `%1%'") % fullPath);
363 382
364#undef st_mode 383#undef st_mode
365#undef st_size 384#undef st_size
366#undef st_nlink 385#undef st_nlink
386#undef fstatat
387#undef fstat
388}
389
390static void _deletePath(const Path & path, unsigned long long & bytesFreed, size_t linkThreshold)
391{
392 _deletePathAt(AT_FDCWD, path, path, bytesFreed, linkThreshold);
367} 393}
368 394
369 395