]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
tmpfiles: don't resolve pathnames when traversing recursively through directory trees
authorFranck Bui <fbui@suse.com>
Fri, 2 Mar 2018 16:19:32 +0000 (17:19 +0100)
committerFranck Bui <fbui@suse.com>
Mon, 5 Mar 2018 18:00:11 +0000 (19:00 +0100)
Otherwise we can be fooled if one path component is replaced underneath us.

The patch achieves that by always operating at file descriptor level (by using
*at() helpers) and by making sure we do not any path resolution when traversing
direcotry trees.

However this is not always possible, for instance when listing the content of a
directory or some operations don't provide the *at() helpers or others (such as
fchmodat()) don't have the AT_EMPTY_PATH flag. In such cases we operate on
/proc/self/fd/%i pseudo-symlink instead, which works the same for all kinds of
objects and requires no checking of type beforehand.

Also O_PATH flag is used when opening file objects in order to prevent
undesired behaviors: device nodes from reacting, automounts from
triggering, etc...

Fixes: #7986
Fixes: CVE-2018-6954
src/tmpfiles/tmpfiles.c

index dba31526719bb5e11778b9d1c4afd11a42568130..f1890f326131da4fe3589d927b12f747a4a3c373 100644 (file)
@@ -783,94 +783,105 @@ static bool hardlink_vulnerable(const struct stat *st) {
         return !S_ISDIR(st->st_mode) && st->st_nlink > 1 && dangerous_hardlinks();
 }
 
-static int path_set_perms(Item *i, const char *path) {
-        char fn[STRLEN("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
-        _cleanup_close_ int fd = -1;
-        struct stat st;
+static int fd_set_perms(Item *i, int fd, const struct stat *st) {
+        _cleanup_free_ char *path = NULL;
+        int r;
 
         assert(i);
-        assert(path);
-
-        if (!i->mode_set && !i->uid_set && !i->gid_set)
-                goto shortcut;
-
-        /* We open the file with O_PATH here, to make the operation
-         * somewhat atomic. Also there's unfortunately no fchmodat()
-         * with AT_SYMLINK_NOFOLLOW, hence we emulate it here via
-         * O_PATH. */
-
-        fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH);
-        if (fd < 0) {
-                int level = LOG_ERR, r = -errno;
+        assert(fd);
 
-                /* Option "e" operates only on existing objects. Do not
-                 * print errors about non-existent files or directories */
-                if (i->type == EMPTY_DIRECTORY && errno == ENOENT) {
-                        level = LOG_DEBUG;
-                        r = 0;
-                }
-
-                log_full_errno(level, errno, "Adjusting owner and mode for %s failed: %m", path);
+        r = fd_get_path(fd, &path);
+        if (r < 0)
                 return r;
-        }
 
-        if (fstat(fd, &st) < 0)
-                return log_error_errno(errno, "Failed to fstat() file %s: %m", path);
+        if (!i->mode_set && !i->uid_set && !i->gid_set)
+                goto shortcut;
 
-        if (hardlink_vulnerable(&st)) {
+        if (hardlink_vulnerable(st)) {
                 log_error("Refusing to set permissions on hardlinked file %s while the fs.protected_hardlinks sysctl is turned off.", path);
                 return -EPERM;
         }
 
-        xsprintf(fn, "/proc/self/fd/%i", fd);
-
         if (i->mode_set) {
-                if (S_ISLNK(st.st_mode))
+                if (S_ISLNK(st->st_mode))
                         log_debug("Skipping mode fix for symlink %s.", path);
                 else {
                         mode_t m = i->mode;
 
                         if (i->mask_perms) {
-                                if (!(st.st_mode & 0111))
+                                if (!(st->st_mode & 0111))
                                         m &= ~0111;
-                                if (!(st.st_mode & 0222))
+                                if (!(st->st_mode & 0222))
                                         m &= ~0222;
-                                if (!(st.st_mode & 0444))
+                                if (!(st->st_mode & 0444))
                                         m &= ~0444;
-                                if (!S_ISDIR(st.st_mode))
+                                if (!S_ISDIR(st->st_mode))
                                         m &= ~07000; /* remove sticky/sgid/suid bit, unless directory */
                         }
 
-                        if (m == (st.st_mode & 07777))
-                                log_debug("\"%s\" has correct mode %o already.", path, st.st_mode);
+                        if (m == (st->st_mode & 07777))
+                                log_debug("\"%s\" has correct mode %o already.", path, st->st_mode);
                         else {
+                                char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
+
                                 log_debug("Changing \"%s\" to mode %o.", path, m);
 
-                                if (chmod(fn, m) < 0)
-                                        return log_error_errno(errno, "chmod() of %s via %s failed: %m", path, fn);
+                                /* fchmodat() still doesn't have AT_EMPTY_PATH flag. */
+                                xsprintf(procfs_path, "/proc/self/fd/%i", fd);
+
+                                if (chmod(procfs_path, m) < 0)
+                                        return log_error_errno(errno, "chmod() of %s via %s failed: %m", path, procfs_path);
                         }
                 }
         }
 
-        if ((i->uid_set && i->uid != st.st_uid) ||
-            (i->gid_set && i->gid != st.st_gid)) {
+        if ((i->uid_set && i->uid != st->st_uid) ||
+            (i->gid_set && i->gid != st->st_gid)) {
                 log_debug("Changing \"%s\" to owner "UID_FMT":"GID_FMT,
                           path,
                           i->uid_set ? i->uid : UID_INVALID,
                           i->gid_set ? i->gid : GID_INVALID);
 
-                if (chown(fn,
-                          i->uid_set ? i->uid : UID_INVALID,
-                          i->gid_set ? i->gid : GID_INVALID) < 0)
-                        return log_error_errno(errno, "chown() of %s via %s failed: %m", path, fn);
+                if (fchownat(fd,
+                             "",
+                             i->uid_set ? i->uid : UID_INVALID,
+                             i->gid_set ? i->gid : GID_INVALID,
+                             AT_EMPTY_PATH) < 0)
+                        return log_error_errno(errno, "fchownat() of %s failed: %m", path);
         }
 
-        fd = safe_close(fd);
-
 shortcut:
         return label_fix(path, false, false);
 }
 
+static int path_set_perms(Item *i, const char *path) {
+        _cleanup_close_ int fd = -1;
+        struct stat st;
+
+        assert(i);
+        assert(path);
+
+        fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH);
+        if (fd < 0) {
+                int level = LOG_ERR, r = -errno;
+
+                /* Option "e" operates only on existing objects. Do not
+                 * print errors about non-existent files or directories */
+                if (i->type == EMPTY_DIRECTORY && errno == ENOENT) {
+                        level = LOG_DEBUG;
+                        r = 0;
+                }
+
+                log_full_errno(level, errno, "Adjusting owner and mode for %s failed: %m", path);
+                return r;
+        }
+
+        if (fstat(fd, &st) < 0)
+                return log_error_errno(errno, "Failed to fstat() file %s: %m", path);
+
+        return fd_set_perms(i, fd, &st);
+}
+
 static int parse_xattrs_from_arg(Item *i) {
         const char *p;
         int r;
@@ -909,21 +920,43 @@ static int parse_xattrs_from_arg(Item *i) {
         return 0;
 }
 
-static int path_set_xattrs(Item *i, const char *path) {
+static int fd_set_xattrs(Item *i, int fd, const struct stat *st) {
+        char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
+        _cleanup_free_ char *path = NULL;
         char **name, **value;
+        int r;
 
         assert(i);
-        assert(path);
+        assert(fd);
+
+        r = fd_get_path(fd, &path);
+        if (r < 0)
+                return r;
+
+        xsprintf(procfs_path, "/proc/self/fd/%i", fd);
 
         STRV_FOREACH_PAIR(name, value, i->xattrs) {
                 log_debug("Setting extended attribute '%s=%s' on %s.", *name, *value, path);
-                if (lsetxattr(path, *name, *value, strlen(*value), 0) < 0)
+                if (setxattr(procfs_path, *name, *value, strlen(*value), 0) < 0)
                         return log_error_errno(errno, "Setting extended attribute %s=%s on %s failed: %m",
                                                *name, *value, path);
         }
         return 0;
 }
 
+static int path_set_xattrs(Item *i, const char *path) {
+        _cleanup_close_ int fd = -1;
+
+        assert(i);
+        assert(path);
+
+        fd = open(path, O_CLOEXEC|O_NOFOLLOW|O_PATH);
+        if (fd < 0)
+                return log_error_errno(errno, "Cannot open '%s': %m", path);
+
+        return fd_set_xattrs(i, fd, NULL);
+}
+
 static int parse_acls_from_arg(Item *item) {
 #if HAVE_ACL
         int r;
@@ -989,52 +1022,71 @@ static int path_set_acl(const char *path, const char *pretty, acl_type_t type, a
 }
 #endif
 
-static int path_set_acls(Item *item, const char *path) {
+static int fd_set_acls(Item *item, int fd, const struct stat *st) {
         int r = 0;
 #if HAVE_ACL
-        char fn[STRLEN("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
-        _cleanup_close_ int fd = -1;
-        struct stat st;
+        char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
+        _cleanup_free_ char *path = NULL;
 
         assert(item);
-        assert(path);
-
-        fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH);
-        if (fd < 0)
-                return log_error_errno(errno, "Adjusting ACL of %s failed: %m", path);
+        assert(fd);
+        assert(st);
 
-        if (fstat(fd, &st) < 0)
-                return log_error_errno(errno, "Failed to fstat() file %s: %m", path);
+        r = fd_get_path(fd, &path);
+        if (r < 0)
+                return r;
 
-        if (hardlink_vulnerable(&st)) {
+        if (hardlink_vulnerable(st)) {
                 log_error("Refusing to set ACLs on hardlinked file %s while the fs.protected_hardlinks sysctl is turned off.", path);
                 return -EPERM;
         }
 
-        if (S_ISLNK(st.st_mode)) {
+        if (S_ISLNK(st->st_mode)) {
                 log_debug("Skipping ACL fix for symlink %s.", path);
                 return 0;
         }
 
-        xsprintf(fn, "/proc/self/fd/%i", fd);
+        xsprintf(procfs_path, "/proc/self/fd/%i", fd);
 
         if (item->acl_access)
-                r = path_set_acl(fn, path, ACL_TYPE_ACCESS, item->acl_access, item->force);
+                r = path_set_acl(procfs_path, path, ACL_TYPE_ACCESS, item->acl_access, item->force);
 
         if (r == 0 && item->acl_default)
-                r = path_set_acl(fn, path, ACL_TYPE_DEFAULT, item->acl_default, item->force);
+                r = path_set_acl(procfs_path, path, ACL_TYPE_DEFAULT, item->acl_default, item->force);
 
         if (r > 0)
                 return -r; /* already warned */
-        else if (r == -EOPNOTSUPP) {
+        if (r == -EOPNOTSUPP) {
                 log_debug_errno(r, "ACLs not supported by file system at %s", path);
                 return 0;
-        } else if (r < 0)
-                log_error_errno(r, "ACL operation on \"%s\" failed: %m", path);
+        }
+        if (r < 0)
+                return log_error_errno(r, "ACL operation on \"%s\" failed: %m", path);
 #endif
         return r;
 }
 
+static int path_set_acls(Item *item, const char *path) {
+        int r = 0;
+#ifdef HAVE_ACL
+        _cleanup_close_ int fd = -1;
+        struct stat st;
+
+        assert(item);
+        assert(path);
+
+        fd = open(path, O_NOFOLLOW|O_CLOEXEC|O_PATH);
+        if (fd < 0)
+                return log_error_errno(errno, "Adjusting ACL of %s failed: %m", path);
+
+        if (fstat(fd, &st) < 0)
+                return log_error_errno(errno, "Failed to fstat() file %s: %m", path);
+
+        r = fd_set_acls(item, fd, &st);
+ #endif
+         return r;
+ }
+
 #define ATTRIBUTES_ALL                          \
         (FS_NOATIME_FL      |                   \
          FS_SYNC_FL         |                   \
@@ -1134,30 +1186,24 @@ static int parse_attribute_from_arg(Item *item) {
         return 0;
 }
 
-static int path_set_attribute(Item *item, const char *path) {
-        _cleanup_close_ int fd = -1;
-        struct stat st;
+static int fd_set_attribute(Item *item, int fd, const struct stat *st) {
+        char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
+        _cleanup_close_ int procfs_fd = -1;
+        _cleanup_free_ char *path = NULL;
         unsigned f;
         int r;
 
         if (!item->attribute_set || item->attribute_mask == 0)
                 return 0;
 
-        fd = open(path, O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_NOATIME|O_NOFOLLOW);
-        if (fd < 0) {
-                if (errno == ELOOP)
-                        return log_error_errno(errno, "Skipping file attributes adjustment on symlink %s.", path);
-
-                return log_error_errno(errno, "Cannot open '%s': %m", path);
-        }
-
-        if (fstat(fd, &st) < 0)
-                return log_error_errno(errno, "Cannot stat '%s': %m", path);
+        r = fd_get_path(fd, &path);
+        if (r < 0)
+                return r;
 
         /* Issuing the file attribute ioctls on device nodes is not
          * safe, as that will be delivered to the drivers, not the
          * file system containing the device node. */
-        if (!S_ISREG(st.st_mode) && !S_ISDIR(st.st_mode)) {
+        if (!S_ISREG(st->st_mode) && !S_ISDIR(st->st_mode)) {
                 log_error("Setting file flags is only supported on regular files and directories, cannot set on '%s'.", path);
                 return -EINVAL;
         }
@@ -1165,10 +1211,16 @@ static int path_set_attribute(Item *item, const char *path) {
         f = item->attribute_value & item->attribute_mask;
 
         /* Mask away directory-specific flags */
-        if (!S_ISDIR(st.st_mode))
+        if (!S_ISDIR(st->st_mode))
                 f &= ~FS_DIRSYNC_FL;
 
-        r = chattr_fd(fd, f, item->attribute_mask);
+        xsprintf(procfs_path, "/proc/self/fd/%i", fd);
+
+        procfs_fd = open(procfs_path, O_RDONLY|O_CLOEXEC|O_NOATIME);
+        if (procfs_fd < 0)
+                return -errno;
+
+        r = chattr_fd(procfs_fd, f, item->attribute_mask);
         if (r < 0)
                 log_full_errno(IN_SET(r, -ENOTTY, -EOPNOTSUPP) ? LOG_DEBUG : LOG_WARNING,
                                r,
@@ -1178,6 +1230,23 @@ static int path_set_attribute(Item *item, const char *path) {
         return 0;
 }
 
+static int path_set_attribute(Item *item, const char *path) {
+        _cleanup_close_ int fd = -1;
+        struct stat st;
+
+        if (!item->attribute_set || item->attribute_mask == 0)
+                return 0;
+
+        fd = open(path, O_CLOEXEC|O_NOFOLLOW|O_PATH);
+        if (fd < 0)
+                return log_error_errno(errno, "Cannot open '%s': %m", path);
+
+        if (fstat(fd, &st) < 0)
+                return log_error_errno(errno, "Cannot stat '%s': %m", path);
+
+        return fd_set_attribute(item, fd, &st);
+}
+
 static int write_one_file(Item *i, const char *path) {
         _cleanup_close_ int fd = -1;
         int flags, r = 0;
@@ -1242,48 +1311,58 @@ done:
 }
 
 typedef int (*action_t)(Item *, const char *);
+typedef int (*fdaction_t)(Item *, int fd, const struct stat *st);
 
-static int item_do_children(Item *i, const char *path, action_t action) {
-        _cleanup_closedir_ DIR *d;
-        struct dirent *de;
-        int r = 0;
+static int item_do(Item *i, int fd, const struct stat *st, fdaction_t action) {
+        int r = 0, q;
 
         assert(i);
-        assert(path);
+        assert(fd >= 0);
+        assert(st);
 
         /* This returns the first error we run into, but nevertheless
          * tries to go on */
+        r = action(i, fd, st);
 
-        d = opendir_nomod(path);
-        if (!d)
-                return IN_SET(errno, ENOENT, ENOTDIR, ELOOP) ? 0 : -errno;
+        if (S_ISDIR(st->st_mode)) {
+                char procfs_path[strlen("/proc/self/fd/") + DECIMAL_STR_MAX(int)];
+                _cleanup_closedir_ DIR *d = NULL;
+                struct dirent *de;
 
-        FOREACH_DIRENT_ALL(de, d, r = -errno) {
-                _cleanup_free_ char *p = NULL;
-                int q;
+                /* The passed 'fd' was opened with O_PATH. We need to convert
+                 * it into a 'regular' fd before reading the directory content. */
+                xsprintf(procfs_path, "/proc/self/fd/%i", fd);
 
-                if (dot_or_dot_dot(de->d_name))
-                        continue;
+                d = opendir(procfs_path);
+                if (!d) {
+                        r = r ?: -errno;
+                        goto finish;
+                }
 
-                p = strjoin(path, "/", de->d_name);
-                if (!p)
-                        return -ENOMEM;
+                FOREACH_DIRENT_ALL(de, d, q = -errno; goto finish) {
+                        struct stat de_st;
+                        int de_fd;
+
+                        if (dot_or_dot_dot(de->d_name))
+                                continue;
 
-                q = action(i, p);
-                if (q < 0 && q != -ENOENT && r == 0)
-                        r = q;
+                        de_fd = openat(fd, de->d_name, O_NOFOLLOW|O_CLOEXEC|O_PATH);
+                        if (de_fd >= 0 && fstat(de_fd, &de_st) >= 0)
+                                /* pass ownership of dirent fd over  */
+                                q = item_do(i, de_fd, &de_st, action);
+                        else
+                                q = -errno;
 
-                if (IN_SET(de->d_type, DT_UNKNOWN, DT_DIR)) {
-                        q = item_do_children(i, p, action);
                         if (q < 0 && r == 0)
                                 r = q;
                 }
         }
-
+finish:
+        safe_close(fd);
         return r;
 }
 
-static int glob_item(Item *i, action_t action, bool recursive) {
+static int glob_item(Item *i, action_t action) {
         _cleanup_globfree_ glob_t g = {
                 .gl_opendir = (void *(*)(const char *)) opendir_nomod,
         };
@@ -1298,12 +1377,48 @@ static int glob_item(Item *i, action_t action, bool recursive) {
                 k = action(i, *fn);
                 if (k < 0 && r == 0)
                         r = k;
+        }
 
-                if (recursive) {
-                        k = item_do_children(i, *fn, action);
-                        if (k < 0 && r == 0)
-                                r = k;
+        return r;
+}
+
+static int glob_item_recursively(Item *i, fdaction_t action) {
+        _cleanup_globfree_ glob_t g = {
+                .gl_opendir = (void *(*)(const char *)) opendir_nomod,
+        };
+        int r = 0, k;
+        char **fn;
+
+        k = safe_glob(i->path, GLOB_NOSORT|GLOB_BRACE, &g);
+        if (k < 0 && k != -ENOENT)
+                return log_error_errno(k, "glob(%s) failed: %m", i->path);
+
+        STRV_FOREACH(fn, g.gl_pathv) {
+                _cleanup_close_ int fd = -1;
+                struct stat st;
+
+                /* Make sure we won't trigger/follow file object (such as
+                 * device nodes, automounts, ...) pointed out by 'fn' with
+                 * O_PATH. Note, when O_PATH is used, flags other than
+                 * O_CLOEXEC, O_DIRECTORY, and O_NOFOLLOW are ignored. */
+
+                fd = open(*fn, O_CLOEXEC|O_NOFOLLOW|O_PATH);
+                if (fd < 0) {
+                        r = r ?: -errno;
+                        continue;
+                }
+
+                if (fstat(fd, &st) < 0) {
+                        r = r ?: -errno;
+                        continue;
                 }
+
+                k = item_do(i, fd, &st, action);
+                if (k < 0 && r == 0)
+                        r = k;
+
+                /* we passed fd ownership to the previous call */
+                fd = -1;
         }
 
         return r;
@@ -1392,7 +1507,7 @@ static int create_item(Item *i) {
                 break;
 
         case WRITE_FILE:
-                r = glob_item(i, write_one_file, false);
+                r = glob_item(i, write_one_file);
                 if (r < 0)
                         return r;
 
@@ -1651,49 +1766,49 @@ static int create_item(Item *i) {
 
         case ADJUST_MODE:
         case RELABEL_PATH:
-                r = glob_item(i, path_set_perms, false);
+                r = glob_item(i, path_set_perms);
                 if (r < 0)
                         return r;
                 break;
 
         case RECURSIVE_RELABEL_PATH:
-                r = glob_item(i, path_set_perms, true);
+                r = glob_item_recursively(i, fd_set_perms);
                 if (r < 0)
                         return r;
                 break;
 
         case SET_XATTR:
-                r = glob_item(i, path_set_xattrs, false);
+                r = glob_item(i, path_set_xattrs);
                 if (r < 0)
                         return r;
                 break;
 
         case RECURSIVE_SET_XATTR:
-                r = glob_item(i, path_set_xattrs, true);
+                r = glob_item_recursively(i, fd_set_xattrs);
                 if (r < 0)
                         return r;
                 break;
 
         case SET_ACL:
-                r = glob_item(i, path_set_acls, false);
+                r = glob_item(i, path_set_acls);
                 if (r < 0)
                         return r;
                 break;
 
         case RECURSIVE_SET_ACL:
-                r = glob_item(i, path_set_acls, true);
+                r = glob_item_recursively(i, fd_set_acls);
                 if (r < 0)
                         return r;
                 break;
 
         case SET_ATTRIBUTE:
-                r = glob_item(i, path_set_attribute, false);
+                r = glob_item(i, path_set_attribute);
                 if (r < 0)
                         return r;
                 break;
 
         case RECURSIVE_SET_ATTRIBUTE:
-                r = glob_item(i, path_set_attribute, true);
+                r = glob_item_recursively(i, fd_set_attribute);
                 if (r < 0)
                         return r;
                 break;
@@ -1743,7 +1858,7 @@ static int remove_item(Item *i) {
         case REMOVE_PATH:
         case TRUNCATE_DIRECTORY:
         case RECURSIVE_REMOVE_PATH:
-                return glob_item(i, remove_item_instance, false);
+                return glob_item(i, remove_item_instance);
 
         default:
                 return 0;
@@ -1817,7 +1932,7 @@ static int clean_item(Item *i) {
                 return 0;
         case EMPTY_DIRECTORY:
         case IGNORE_DIRECTORY_PATH:
-                return glob_item(i, clean_item_instance, false);
+                return glob_item(i, clean_item_instance);
         default:
                 return 0;
         }