chiark / gitweb /
fs-util: rework touch_file() so that it can touch socket file nodes
authorLennart Poettering <lennart@poettering.net>
Wed, 27 Dec 2017 15:20:28 +0000 (16:20 +0100)
committerSven Eden <yamakuzure@gmx.net>
Wed, 30 May 2018 05:49:55 +0000 (07:49 +0200)
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
src/test/test-fs-util.c

index 354c97d8bdc3fb53cd69e3fe69f60454a87212f0..0cd339d299d31f0d2cf766c86ee178e7d8fbec09 100644 (file)
@@ -324,43 +324,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);
 
-        if (parents)
-                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;
+        /* 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 (mode != MODE_INVALID) {
-                r = fchmod(fd, mode);
-                if (r < 0)
+        if (parents)
+                (void) mkdir_parents(path, 0755);
+
+        /* 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 (uid != UID_INVALID || gid != GID_INVALID) {
-                r = fchown(fd, uid, gid);
-                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;
         }
 
+        /* 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) {
index 0edb259f83a599cf3a2509d3e14b7628f93a7c10..f2f571ce2bf7db588f1591f93e161073dbb0fd11 100644 (file)
@@ -392,6 +392,92 @@ static void test_access_fd(void) {
 }
 #endif // 0
 
+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();
@@ -404,6 +490,7 @@ int main(int argc, char *argv[]) {
 #if 0 /// Uses functions that elogind does not need
         test_access_fd();
 #endif // 0
+        test_touch_file();
 
         return 0;
 }