From: Lennart Poettering Date: Tue, 12 Dec 2023 10:27:55 +0000 (+0100) Subject: fs-util: add new helper linkat_replace() X-Git-Tag: v256-rc1~531^2~2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=1f27e7b724bb7a4fb460760c57ccba051336a028;p=thirdparty%2Fsystemd.git fs-util: add new helper linkat_replace() --- diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index 999713d243b..7f0b5814be5 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -1256,3 +1256,79 @@ int link_fd(int fd, int newdirfd, const char *newpath) { return RET_NERRNO(linkat(fd, "", newdirfd, newpath, AT_EMPTY_PATH)); } + +int linkat_replace(int olddirfd, const char *oldpath, int newdirfd, const char *newpath) { + _cleanup_close_ int old_fd = -EBADF; + int r; + + assert(olddirfd >= 0 || olddirfd == AT_FDCWD); + assert(newdirfd >= 0 || newdirfd == AT_FDCWD); + assert(!isempty(newpath)); /* source path is optional, but the target path is not */ + + /* Like linkat() but replaces the target if needed. Is a NOP if source and target already share the + * same inode. */ + + if (olddirfd == AT_FDCWD && isempty(oldpath)) /* Refuse operating on the cwd (which is a dir, and dirs can't be hardlinked) */ + return -EISDIR; + + if (path_implies_directory(oldpath)) /* Refuse these definite directories early */ + return -EISDIR; + + if (path_implies_directory(newpath)) + return -EISDIR; + + /* First, try to link this directly */ + if (oldpath) + r = RET_NERRNO(linkat(olddirfd, oldpath, newdirfd, newpath, 0)); + else + r = link_fd(olddirfd, newdirfd, newpath); + if (r >= 0) + return 0; + if (r != -EEXIST) + return r; + + old_fd = xopenat(olddirfd, oldpath, O_PATH|O_CLOEXEC); + if (old_fd < 0) + return old_fd; + + struct stat old_st; + if (fstat(old_fd, &old_st) < 0) + return -errno; + + if (S_ISDIR(old_st.st_mode)) /* Don't bother if we are operating on a directory */ + return -EISDIR; + + struct stat new_st; + if (fstatat(newdirfd, newpath, &new_st, AT_SYMLINK_NOFOLLOW) < 0) + return -errno; + + if (S_ISDIR(new_st.st_mode)) /* Refuse replacing directories */ + return -EEXIST; + + if (stat_inode_same(&old_st, &new_st)) /* Already the same inode? Then shortcut this */ + return 0; + + _cleanup_free_ char *tmp_path = NULL; + r = tempfn_random(newpath, /* extra= */ NULL, &tmp_path); + if (r < 0) + return r; + + r = link_fd(old_fd, newdirfd, tmp_path); + if (r < 0) { + if (!ERRNO_IS_PRIVILEGE(r)) + return r; + + /* If that didn't work due to permissions then go via the path of the dentry */ + r = RET_NERRNO(linkat(olddirfd, oldpath, newdirfd, tmp_path, 0)); + if (r < 0) + return r; + } + + r = RET_NERRNO(renameat(newdirfd, tmp_path, newdirfd, newpath)); + if (r < 0) { + (void) unlinkat(newdirfd, tmp_path, /* flags= */ 0); + return r; + } + + return 0; +} diff --git a/src/basic/fs-util.h b/src/basic/fs-util.h index 86fc6711586..58a7d0a7459 100644 --- a/src/basic/fs-util.h +++ b/src/basic/fs-util.h @@ -148,3 +148,5 @@ static inline int xopenat_lock(int dir_fd, const char *path, int open_flags, Loc } int link_fd(int fd, int newdirfd, const char *newpath); + +int linkat_replace(int olddirfd, const char *oldpath, int newdirfd, const char *newpath); diff --git a/src/test/test-fs-util.c b/src/test/test-fs-util.c index b32feffd303..b27c79fdc93 100644 --- a/src/test/test-fs-util.c +++ b/src/test/test-fs-util.c @@ -753,6 +753,36 @@ TEST(xopenat_lock_full) { assert_se(xopenat_lock_full(tfd, "def", O_DIRECTORY, 0, 0755, LOCK_POSIX, LOCK_EX) == -EBADF); } +TEST(linkat_replace) { + _cleanup_(rm_rf_physical_and_freep) char *t = NULL; + _cleanup_close_ int tfd = -EBADF; + + assert_se((tfd = mkdtemp_open(NULL, 0, &t)) >= 0); + + _cleanup_close_ int fd1 = openat(tfd, "foo", O_CREAT|O_RDWR|O_CLOEXEC, 0600); + assert_se(fd1 >= 0); + + assert_se(linkat_replace(tfd, "foo", tfd, "bar") >= 0); + assert_se(linkat_replace(tfd, "foo", tfd, "bar") >= 0); + + _cleanup_close_ int fd1_check = openat(tfd, "bar", O_RDWR|O_CLOEXEC); + assert_se(fd1_check >= 0); + + assert_se(inode_same_at(fd1, NULL, fd1_check, NULL, AT_EMPTY_PATH) > 0); + + _cleanup_close_ int fd2 = openat(tfd, "baz", O_CREAT|O_RDWR|O_CLOEXEC, 0600); + assert_se(fd2 >= 0); + + assert_se(inode_same_at(fd1, NULL, fd2, NULL, AT_EMPTY_PATH) == 0); + + assert_se(linkat_replace(tfd, "foo", tfd, "baz") >= 0); + + _cleanup_close_ int fd2_check = openat(tfd, "baz", O_RDWR|O_CLOEXEC); + + assert_se(inode_same_at(fd2, NULL, fd2_check, NULL, AT_EMPTY_PATH) == 0); + assert_se(inode_same_at(fd1, NULL, fd2_check, NULL, AT_EMPTY_PATH) > 0); +} + static int intro(void) { arg_test_dir = saved_argv[1]; return EXIT_SUCCESS;