Merge pull request #31770 from poettering/linkat-replace

introduce linkat_replace() helper, and port various things over to it
This commit is contained in:
Lennart Poettering
2024-03-14 11:03:59 +01:00
committed by GitHub
8 changed files with 190 additions and 79 deletions
+96
View File
@@ -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;
}
+4
View File
@@ -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);
+14
View File
@@ -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
+2
View File
@@ -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;
+6 -46
View File
@@ -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);