tmpfiles: introduce truncate_file() which deals with 'F' exclusively

TRUNCATE_FILE is now handled by a new dedicated function
truncate_file(). Indeed we have to take special care when truncating existing
file since the behavior is only specified for regular files.

Well that's not entirely true for fifo and terminal devices since O_TRUNC is
ignored in this case but even in for these types of file, truncating is
probably not the right thing to do.

It is worth noting that both truncate_file() and create_file() have been
modified so they use fstat(2) instead of stat(2) since both functions are not
supposed to follow symlinks.
diff --git a/src/tmpfiles/tmpfiles.c b/src/tmpfiles/tmpfiles.c
index 62a3683..dd19cde 100644
--- a/src/tmpfiles/tmpfiles.c
+++ b/src/tmpfiles/tmpfiles.c
@@ -842,6 +842,28 @@
         return label_fix(path, 0);
 }
 
+static int path_open_parent_safe(const char *path) {
+        _cleanup_free_ char *dn = NULL;
+        int fd;
+
+        if (path_equal(path, "/") || !path_is_normalized(path)) {
+                log_error("Failed to open parent of '%s': invalid path.", path);
+                return -EINVAL;
+        }
+
+        dn = dirname_malloc(path);
+        if (!dn)
+                return log_oom();
+
+        fd = chase_symlinks(dn, NULL, CHASE_OPEN|CHASE_SAFE, NULL);
+        if (fd == -EPERM)
+                return log_error_errno(fd, "Unsafe symlinks encountered in %s, refusing.", path);
+        if (fd < 0)
+                return log_error_errno(fd, "Failed to validate path %s: %m", path);
+
+        return fd;
+}
+
 static int path_set_perms(Item *i, const char *path) {
         _cleanup_close_ int fd = -1;
         struct stat stbuf, *st = NULL;
@@ -1278,65 +1300,145 @@
 }
 
 static int create_file(Item *i, const char *path) {
-        _cleanup_close_ int fd = -1;
-        int flags, r = 0;
-        struct stat st;
+        _cleanup_close_ int fd = -1, dir_fd = -1;
+        struct stat stbuf, *st = NULL;
+        int r = 0;
+        char *bn;
 
         assert(i);
         assert(path);
-        assert(IN_SET(i->type, CREATE_FILE, TRUNCATE_FILE));
+        assert(i->type == CREATE_FILE);
 
-        /* FIXME: O_TRUNC is unspecified if file is neither a regular file nor a
-         * fifo nor a terminal device. Therefore we should fail if file is
-         * anything but a regular file with 'F'. */
-        flags = O_CREAT|O_NOFOLLOW|(i->type == CREATE_FILE ? O_EXCL : O_TRUNC);
+        /* 'f' operates on regular files exclusively. */
+
+        /* Validate the path and keep the fd on the directory for opening the
+         * file so we're sure that it can't be changed behind our back. */
+        dir_fd = path_open_parent_safe(path);
+        if (dir_fd < 0)
+                return dir_fd;
+
+        bn = basename(path);
 
         RUN_WITH_UMASK(0000) {
                 mac_selinux_create_file_prepare(path, S_IFREG);
-                fd = open(path, flags|O_NONBLOCK|O_CLOEXEC|O_WRONLY|O_NOCTTY, i->mode);
+                fd = openat(dir_fd, bn, O_CREAT|O_EXCL|O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC|O_WRONLY|O_NOCTTY, i->mode);
                 mac_selinux_create_file_clear();
         }
 
         if (fd < 0) {
-                if (i->type == CREATE_FILE && errno == EEXIST) {
-                        log_debug_errno(errno, "Not writing to pre-existing file \"%s\": %m", path);
-                        goto done;
+                /* Even on a read-only filesystem, open(2) returns EEXIST if the
+                 * file already exists. It returns EROFS only if it needs to
+                 * create the file. */
+                if (errno != EEXIST)
+                        return log_error_errno(errno, "Failed to create file %s: %m", path);
+
+                /* Re-open the file. At that point it must exist since open(2)
+                 * failed with EEXIST. We still need to check if the perms/mode
+                 * need to be changed. For read-only filesystems, we let
+                 * fd_set_perms() report the error if the perms need to be
+                 * modified. */
+                fd = openat(dir_fd, bn, O_NOFOLLOW|O_CLOEXEC|O_PATH, i->mode);
+                if (fd < 0)
+                        return log_error_errno(errno, "Failed to re-open file %s: %m", path);
+
+                if (fstat(fd, &stbuf) < 0)
+                        return log_error_errno(errno, "stat(%s) failed: %m", path);
+
+                if (!S_ISREG(stbuf.st_mode)) {
+                        log_error("%s exists and is not a regular file.", path);
+                        return -EEXIST;
                 }
 
-                r = -errno;
-                if (!i->argument && errno == EROFS && stat(path, &st) == 0 &&
-                    (i->type == CREATE_FILE || st.st_size == 0))
-                        goto check_mode;
+                st = &stbuf;
+        } else {
 
-                return log_error_errno(r, "Failed to create file %s: %m", path);
-        }
-
-        if (i->argument) {
-                log_debug("%s to \"%s\".", i->type == CREATE_FILE ? "Appending" : "Writing", path);
-
-                r = loop_write(fd, i->argument, strlen(i->argument), false);
-                if (r < 0)
-                        return log_error_errno(r, "Failed to write file \"%s\": %m", path);
-        } else
                 log_debug("\"%s\" has been created.", path);
 
-        fd = safe_close(fd);
+                if (i->argument) {
+                        log_debug("Writing to \"%s\".", path);
 
- done:
-        if (stat(path, &st) < 0)
+                        r = loop_write(fd, i->argument, strlen(i->argument), false);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to write file \"%s\": %m", path);
+                }
+        }
+
+        return fd_set_perms(i, fd, st);
+}
+
+static int truncate_file(Item *i, const char *path) {
+        _cleanup_close_ int fd = -1;
+        struct stat stbuf, *st = NULL;
+        bool erofs = false;
+        int r = 0;
+
+        assert(i);
+        assert(path);
+        assert(i->type == TRUNCATE_FILE);
+
+        /* We want to operate on regular file exclusively especially since
+         * O_TRUNC is unspecified if the file is neither a regular file nor a
+         * fifo nor a terminal device. Therefore we first open the file and make
+         * sure it's a regular one before truncating it. */
+
+        RUN_WITH_UMASK(0000) {
+                mac_selinux_create_file_prepare(path, S_IFREG);
+                fd = open(path, O_CREAT|O_NOFOLLOW|O_NONBLOCK|O_CLOEXEC|O_WRONLY|O_NOCTTY, i->mode);
+                mac_selinux_create_file_clear();
+        }
+
+        if (fd < 0) {
+                if (errno != EROFS)
+                        return log_error_errno(errno, "Failed to open/create file %s: %m", path);
+
+                /* On a read-only filesystem, we don't want to fail if the
+                 * target is already empty and the perms are set. So we still
+                 * proceed with the sanity checks and let the remaining
+                 * operations fail with EROFS if they try to modify the target
+                 * file. */
+
+                fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH, i->mode);
+                if (fd < 0) {
+                        if (errno == ENOENT) {
+                                log_error("Cannot create file %s on a read-only file system.", path);
+                                return -EROFS;
+                        }
+
+                        return log_error_errno(errno, "Failed to re-open file %s: %m", path);
+                }
+
+                erofs = true;
+        }
+
+        if (fstat(fd, &stbuf) < 0)
                 return log_error_errno(errno, "stat(%s) failed: %m", path);
 
- check_mode:
-        if (!S_ISREG(st.st_mode)) {
-                log_error("%s is not a file.", path);
+        if (!S_ISREG(stbuf.st_mode)) {
+                log_error("%s exists and is not a regular file.", path);
                 return -EEXIST;
         }
 
-        r = path_set_perms(i, path);
-        if (r < 0)
-                return r;
+        if (stbuf.st_size > 0) {
+                if (ftruncate(fd, 0) < 0) {
+                        r = erofs ? -EROFS : -errno;
+                        return log_error_errno(r, "Failed to truncate file %s: %m", path);
+                }
+        } else
+                st = &stbuf;
 
-        return 0;
+        log_debug("\"%s\" has been created.", path);
+
+        if (i->argument) {
+                log_debug("Writing to \"%s\".", path);
+
+                r = loop_write(fd, i->argument, strlen(i->argument), false);
+                if (r < 0) {
+                        r = erofs ? -EROFS : r;
+                        return log_error_errno(r, "Failed to write file %s: %m", path);
+                }
+        }
+
+        return fd_set_perms(i, fd, st);
 }
 
 typedef int (*action_t)(Item *, const char *);
@@ -1486,7 +1588,6 @@
                 return 0;
 
         case CREATE_FILE:
-        case TRUNCATE_FILE:
                 RUN_WITH_UMASK(0000)
                         (void) mkdir_parents_label(i->path, 0755);
 
@@ -1495,6 +1596,15 @@
                         return r;
                 break;
 
+        case TRUNCATE_FILE:
+                RUN_WITH_UMASK(0000)
+                        (void) mkdir_parents_label(i->path, 0755);
+
+                r = truncate_file(i, i->path);
+                if (r < 0)
+                        return r;
+                break;
+
         case COPY_FILES:
                 RUN_WITH_UMASK(0000)
                         (void) mkdir_parents_label(i->path, 0755);