From 9e3fa6e82773cb1d8687453ef0ba0ad9e5dda7d2 Mon Sep 17 00:00:00 2001 From: Lennart Poettering Date: Wed, 27 Dec 2017 16:20:28 +0100 Subject: [PATCH] fs-util: rework touch_file() so that it can touch socket file nodes Let's rework touch_file() so that it works correctly on sockets, fifos, and device nodes: let's open an O_PATH file descriptor first and operate based on that, if we can. This is usually the better option as it this means we can open AF_UNIX nodes in the file system, and update their timestamps and ownership correctly. It also means we can correctly touch symlinks and block/character devices without triggering their drivers. Moreover, by operating on an O_PATH fd we can make sure that we operate on the same inode the whole time, and it can't be swapped out in the middle. While we are at it, rework the call so that we try to adjust as much as we can before returning on error. This is a good idea as we call the function quite often without checking its result, and hence it's best to leave the files around in the most "correct" fashion possible. --- src/basic/fs-util.c | 55 +++++++++++++++++--------- src/test/test-fs-util.c | 87 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 19 deletions(-) diff --git a/src/basic/fs-util.c b/src/basic/fs-util.c index 4ca36faf09..16f97893c9 100644 --- a/src/basic/fs-util.c +++ b/src/basic/fs-util.c @@ -315,43 +315,60 @@ int fd_warn_permissions(const char *path, int fd) { } int touch_file(const char *path, bool parents, usec_t stamp, uid_t uid, gid_t gid, mode_t mode) { - _cleanup_close_ int fd; - int r; + char fdpath[STRLEN("/proc/self/fd/") + DECIMAL_STR_MAX(int)]; + _cleanup_close_ int fd = -1; + int r, ret = 0; assert(path); + /* Note that touch_file() does not follow symlinks: if invoked on an existing symlink, then it is the symlink + * itself which is updated, not its target + * + * Returns the first error we encounter, but tries to apply as much as possible. */ + if (parents) - mkdir_parents(path, 0755); + (void) mkdir_parents(path, 0755); - fd = open(path, O_WRONLY|O_CREAT|O_CLOEXEC|O_NOCTTY, - IN_SET(mode, 0, MODE_INVALID) ? 0644 : mode); - if (fd < 0) - return -errno; + /* Initially, we try to open the node with O_PATH, so that we get a reference to the node. This is useful in + * case the path refers to an existing device or socket node, as we can open it successfully in all cases, and + * won't trigger any driver magic or so. */ + fd = open(path, O_PATH|O_CLOEXEC|O_NOFOLLOW); + if (fd < 0) { + if (errno != ENOENT) + return -errno; - if (mode != MODE_INVALID) { - r = fchmod(fd, mode); - if (r < 0) + /* if the node doesn't exist yet, we create it, but with O_EXCL, so that we only create a regular file + * here, and nothing else */ + fd = open(path, O_WRONLY|O_CREAT|O_EXCL|O_CLOEXEC, IN_SET(mode, 0, MODE_INVALID) ? 0644 : mode); + if (fd < 0) return -errno; } - if (uid != UID_INVALID || gid != GID_INVALID) { - r = fchown(fd, uid, gid); - if (r < 0) - return -errno; - } + /* Let's make a path from the fd, and operate on that. With this logic, we can adjust the access mode, + * ownership and time of the file node in all cases, even if the fd refers to an O_PATH object — which is + * something fchown(), fchmod(), futimensat() don't allow. */ + xsprintf(fdpath, "/proc/self/fd/%i", fd); + + if (mode != MODE_INVALID) + if (chmod(fdpath, mode) < 0) + ret = -errno; + + if (uid_is_valid(uid) || gid_is_valid(gid)) + if (chown(fdpath, uid, gid) < 0 && ret >= 0) + ret = -errno; if (stamp != USEC_INFINITY) { struct timespec ts[2]; timespec_store(&ts[0], stamp); ts[1] = ts[0]; - r = futimens(fd, ts); + r = utimensat(AT_FDCWD, fdpath, ts, 0); } else - r = futimens(fd, NULL); - if (r < 0) + r = utimensat(AT_FDCWD, fdpath, NULL, 0); + if (r < 0 && ret >= 0) return -errno; - return 0; + return ret; } int touch(const char *path) { diff --git a/src/test/test-fs-util.c b/src/test/test-fs-util.c index 86d963c4c7..2ad9e71856 100644 --- a/src/test/test-fs-util.c +++ b/src/test/test-fs-util.c @@ -388,6 +388,92 @@ static void test_access_fd(void) { } } +static void test_touch_file(void) { + uid_t test_uid, test_gid; + _cleanup_(rm_rf_physical_and_freep) char *p = NULL; + struct stat st; + const char *a; + usec_t test_mtime; + + test_uid = geteuid() == 0 ? 65534 : getuid(); + test_gid = geteuid() == 0 ? 65534 : getgid(); + + test_mtime = usec_sub_unsigned(now(CLOCK_REALTIME), USEC_PER_WEEK); + + assert_se(mkdtemp_malloc("/dev/shm/touch-file-XXXXXX", &p) >= 0); + + a = strjoina(p, "/regular"); + assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0); + assert_se(lstat(a, &st) >= 0); + assert_se(st.st_uid == test_uid); + assert_se(st.st_gid == test_gid); + assert_se(S_ISREG(st.st_mode)); + assert_se((st.st_mode & 0777) == 0640); + assert_se(timespec_load(&st.st_mtim) == test_mtime); + + a = strjoina(p, "/dir"); + assert_se(mkdir(a, 0775) >= 0); + assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0); + assert_se(lstat(a, &st) >= 0); + assert_se(st.st_uid == test_uid); + assert_se(st.st_gid == test_gid); + assert_se(S_ISDIR(st.st_mode)); + assert_se((st.st_mode & 0777) == 0640); + assert_se(timespec_load(&st.st_mtim) == test_mtime); + + a = strjoina(p, "/fifo"); + assert_se(mkfifo(a, 0775) >= 0); + assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0); + assert_se(lstat(a, &st) >= 0); + assert_se(st.st_uid == test_uid); + assert_se(st.st_gid == test_gid); + assert_se(S_ISFIFO(st.st_mode)); + assert_se((st.st_mode & 0777) == 0640); + assert_se(timespec_load(&st.st_mtim) == test_mtime); + + a = strjoina(p, "/sock"); + assert_se(mknod(a, 0775 | S_IFSOCK, 0) >= 0); + assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0); + assert_se(lstat(a, &st) >= 0); + assert_se(st.st_uid == test_uid); + assert_se(st.st_gid == test_gid); + assert_se(S_ISSOCK(st.st_mode)); + assert_se((st.st_mode & 0777) == 0640); + assert_se(timespec_load(&st.st_mtim) == test_mtime); + + if (geteuid() == 0) { + a = strjoina(p, "/cdev"); + assert_se(mknod(a, 0775 | S_IFCHR, makedev(0, 0)) >= 0); + assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0); + assert_se(lstat(a, &st) >= 0); + assert_se(st.st_uid == test_uid); + assert_se(st.st_gid == test_gid); + assert_se(S_ISCHR(st.st_mode)); + assert_se((st.st_mode & 0777) == 0640); + assert_se(timespec_load(&st.st_mtim) == test_mtime); + + a = strjoina(p, "/bdev"); + assert_se(mknod(a, 0775 | S_IFBLK, makedev(0, 0)) >= 0); + assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0); + assert_se(lstat(a, &st) >= 0); + assert_se(st.st_uid == test_uid); + assert_se(st.st_gid == test_gid); + assert_se(S_ISBLK(st.st_mode)); + assert_se((st.st_mode & 0777) == 0640); + assert_se(timespec_load(&st.st_mtim) == test_mtime); + } + + a = strjoina(p, "/lnk"); + assert_se(symlink("target", a) >= 0); + assert_se(touch_file(a, false, test_mtime, test_uid, test_gid, 0640) >= 0); + assert_se(lstat(a, &st) >= 0); + assert_se(st.st_uid == test_uid); + assert_se(st.st_gid == test_gid); + assert_se(S_ISLNK(st.st_mode)); + assert_se((st.st_mode & 0777) == 0640); + assert_se(timespec_load(&st.st_mtim) == test_mtime); +} + int main(int argc, char *argv[]) { test_unlink_noerrno(); test_get_files_in_directory(); @@ -396,6 +482,7 @@ int main(int argc, char *argv[]) { test_chase_symlinks(); test_dot_or_dot_dot(); test_access_fd(); + test_touch_file(); return 0; }