diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index 5bc7d2f95b..7f0b5814be 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -1236,3 +1236,99 @@ int xopenat_lock_full( return TAKE_FD(fd); } + +int link_fd(int fd, int newdirfd, const char *newpath) { + int r; + + assert(fd >= 0); + assert(newdirfd >= 0 || newdirfd == AT_FDCWD); + assert(newpath); + + /* Try linking via /proc/self/fd/ first. */ + r = RET_NERRNO(linkat(AT_FDCWD, FORMAT_PROC_FD_PATH(fd), newdirfd, newpath, AT_SYMLINK_FOLLOW)); + if (r != -ENOENT) + return r; + + /* Fall back to symlinking via AT_EMPTY_PATH as fallback (this requires CAP_DAC_READ_SEARCH and a + * more recent kernel, but does not require /proc/ mounted) */ + if (proc_mounted() != 0) + return r; + + 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 6a1e2e76d1..58a7d0a745 100644 --- a/src/basic/fs-util.h +++ b/src/basic/fs-util.h @@ -146,3 +146,7 @@ int xopenat_lock_full(int dir_fd, const char *path, int open_flags, XOpenFlags x static inline int xopenat_lock(int dir_fd, const char *path, int open_flags, LockType locktype, int operation) { return xopenat_lock_full(dir_fd, path, open_flags, 0, 0, locktype, operation); } + +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/basic/path-util.c b/src/basic/path-util.c index 6810bf66aa..05a21f8f8e 100644 --- a/src/basic/path-util.c +++ b/src/basic/path-util.c @@ -1336,6 +1336,20 @@ bool dot_or_dot_dot(const char *path) { return path[2] == 0; } +bool path_implies_directory(const char *path) { + + /* Sometimes, if we look at a path we already know it must refer to a directory, because it is + * suffixed with a slash, or its last component is "." or ".." */ + + if (!path) + return false; + + if (dot_or_dot_dot(path)) + return true; + + return ENDSWITH_SET(path, "/", "/.", "/.."); +} + bool empty_or_root(const char *path) { /* For operations relative to some root directory, returns true if the specified root directory is diff --git a/src/basic/path-util.h b/src/basic/path-util.h index 39b6714525..052bbd7031 100644 --- a/src/basic/path-util.h +++ b/src/basic/path-util.h @@ -201,6 +201,8 @@ bool valid_device_allow_pattern(const char *path); bool dot_or_dot_dot(const char *path); +bool path_implies_directory(const char *path); + static inline const char *skip_dev_prefix(const char *p) { const char *e; diff --git a/src/basic/tmpfile-util.c b/src/basic/tmpfile-util.c index e77ca94248..3a3f7dcc0f 100644 --- a/src/basic/tmpfile-util.c +++ b/src/basic/tmpfile-util.c @@ -330,28 +330,7 @@ int fopen_tmpfile_linkable(const char *target, int flags, char **ret_path, FILE return 0; } -static int link_fd(int fd, int newdirfd, const char *newpath) { - int r; - - assert(fd >= 0); - assert(newdirfd >= 0 || newdirfd == AT_FDCWD); - assert(newpath); - - /* Try symlinking via /proc/fd/ first. */ - r = RET_NERRNO(linkat(AT_FDCWD, FORMAT_PROC_FD_PATH(fd), newdirfd, newpath, AT_SYMLINK_FOLLOW)); - if (r != -ENOENT) - return r; - - /* Fall back to symlinking via AT_EMPTY_PATH as fallback (this requires CAP_DAC_READ_SEARCH and a - * more recent kernel, but does not require /proc/ mounted) */ - if (proc_mounted() != 0) - return r; - - return RET_NERRNO(linkat(fd, "", newdirfd, newpath, AT_EMPTY_PATH)); -} - int link_tmpfile_at(int fd, int dir_fd, const char *path, const char *target, LinkTmpfileFlags flags) { - _cleanup_free_ char *tmp = NULL; int r; assert(fd >= 0); @@ -370,33 +349,14 @@ int link_tmpfile_at(int fd, int dir_fd, const char *path, const char *target, Li r = RET_NERRNO(renameat(dir_fd, path, dir_fd, target)); else r = rename_noreplace(dir_fd, path, dir_fd, target); - if (r < 0) - return r; } else { - - r = link_fd(fd, dir_fd, target); - if (r != -EEXIST || !FLAGS_SET(flags, LINK_TMPFILE_REPLACE)) - return r; - - /* So the target already exists and we were asked to replace it. That sucks a bit, since the kernel's - * linkat() logic does not allow that. We work-around this by linking the file to a random name - * first, and then renaming that to the final name. This reintroduces the race O_TMPFILE kinda is - * trying to fix, but at least the vulnerability window (i.e. where the file is linked into the file - * system under a temporary name) is very short. */ - - r = tempfn_random(target, NULL, &tmp); - if (r < 0) - return r; - - if (link_fd(fd, dir_fd, tmp) < 0) - return -EEXIST; /* propagate original error */ - - r = RET_NERRNO(renameat(dir_fd, tmp, dir_fd, target)); - if (r < 0) { - (void) unlinkat(dir_fd, tmp, 0); - return r; - } + if (FLAGS_SET(flags, LINK_TMPFILE_REPLACE)) + r = linkat_replace(fd, /* oldpath= */ NULL, dir_fd, target); + else + r = link_fd(fd, dir_fd, target); } + if (r < 0) + return r; if (FLAGS_SET(flags, LINK_TMPFILE_SYNC)) { r = fsync_full(fd); diff --git a/src/shared/creds-util.c b/src/shared/creds-util.c index 2d5846a06f..ba8aee3355 100644 --- a/src/shared/creds-util.c +++ b/src/shared/creds-util.c @@ -28,6 +28,7 @@ #include "random-util.h" #include "sparse-endian.h" #include "stat-util.h" +#include "tmpfile-util.h" #include "tpm2-util.h" #include "user-util.h" #include "varlink.h" @@ -377,22 +378,9 @@ static int make_credential_host_secret( assert(dfd >= 0); assert(fn); - /* For non-root users creating a temporary file using the openat(2) over "." will fail later, in the - * linkat(2) step at the end. The reason is that linkat(2) requires the CAP_DAC_READ_SEARCH - * capability when it uses the AT_EMPTY_PATH flag. */ - if (have_effective_cap(CAP_DAC_READ_SEARCH) > 0) { - fd = openat(dfd, ".", O_CLOEXEC|O_WRONLY|O_TMPFILE, 0400); - if (fd < 0) - log_debug_errno(errno, "Failed to create temporary credential file with O_TMPFILE, proceeding without: %m"); - } - if (fd < 0) { - if (asprintf(&t, "credential.secret.%016" PRIx64, random_u64()) < 0) - return -ENOMEM; - - fd = openat(dfd, t, O_CLOEXEC|O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0400); - if (fd < 0) - return -errno; - } + fd = open_tmpfile_linkable_at(dfd, fn, O_CLOEXEC|O_WRONLY, &t); + if (fd < 0) + return log_debug_errno(fd, "Failed to create temporary file for credential host secret: %m"); r = chattr_secret(fd, 0); if (r < 0) @@ -412,6 +400,11 @@ static int make_credential_host_secret( if (r < 0) goto fail; + if (fchmod(fd, 0400) < 0) { + r = -errno; + goto fail; + } + if (fsync(fd) < 0) { r = -errno; goto fail; @@ -419,19 +412,9 @@ static int make_credential_host_secret( warn_not_encrypted(fd, flags, dirname, fn); - if (t) { - r = rename_noreplace(dfd, t, dfd, fn); - if (r < 0) - goto fail; - - t = mfree(t); - } else if (linkat(fd, "", dfd, fn, AT_EMPTY_PATH) < 0) { - r = -errno; - goto fail; - } - - if (fsync(dfd) < 0) { - r = -errno; + r = link_tmpfile_at(fd, dfd, t, fn, LINK_TMPFILE_SYNC); + if (r < 0) { + log_debug_errno(r, "Failed to link host key into place: %m"); goto fail; } @@ -439,10 +422,8 @@ static int make_credential_host_secret( void *copy; copy = memdup(buf.data, sizeof(buf.data)); - if (!copy) { - r = -ENOMEM; - goto fail; - } + if (!copy) + return -ENOMEM; *ret = IOVEC_MAKE(copy, sizeof(buf.data)); } diff --git a/src/test/test-fs-util.c b/src/test/test-fs-util.c index b32feffd30..b27c79fdc9 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; diff --git a/src/test/test-path-util.c b/src/test/test-path-util.c index f5a425689a..ca11bf3a29 100644 --- a/src/test/test-path-util.c +++ b/src/test/test-path-util.c @@ -1305,4 +1305,28 @@ TEST(print_MAX) { assert_cc(FILENAME_MAX == PATH_MAX); } +TEST(path_implies_directory) { + assert_se(!path_implies_directory(NULL)); + assert_se(!path_implies_directory("")); + assert_se(path_implies_directory("/")); + assert_se(path_implies_directory("////")); + assert_se(path_implies_directory("////.///")); + assert_se(path_implies_directory("////./")); + assert_se(path_implies_directory("////.")); + assert_se(path_implies_directory(".")); + assert_se(path_implies_directory("./")); + assert_se(path_implies_directory("/.")); + assert_se(path_implies_directory("..")); + assert_se(path_implies_directory("../")); + assert_se(path_implies_directory("/..")); + assert_se(!path_implies_directory("a")); + assert_se(!path_implies_directory("ab")); + assert_se(path_implies_directory("ab/")); + assert_se(!path_implies_directory("ab/a")); + assert_se(path_implies_directory("ab/a/")); + assert_se(path_implies_directory("ab/a/..")); + assert_se(path_implies_directory("ab/a/.")); + assert_se(path_implies_directory("ab/a//")); +} + DEFINE_TEST_MAIN(LOG_DEBUG);