Merge pull request #31770 from poettering/linkat-replace
introduce linkat_replace() helper, and port various things over to it
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+14
-33
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user