Merge tag 'fs.setgid.v6.0' of git://git.kernel.org/pub/scm/linux/kernel/git/brauner...
authorLinus Torvalds <torvalds@linux-foundation.org>
Tue, 9 Aug 2022 16:52:28 +0000 (09:52 -0700)
committerLinus Torvalds <torvalds@linux-foundation.org>
Tue, 9 Aug 2022 16:52:28 +0000 (09:52 -0700)
Pull setgid updates from Christian Brauner:
 "This contains the work to move setgid stripping out of individual
  filesystems and into the VFS itself.

  Creating files that have both the S_IXGRP and S_ISGID bit raised in
  directories that themselves have the S_ISGID bit set requires
  additional privileges to avoid security issues.

  When a filesystem creates a new inode it needs to take care that the
  caller is either in the group of the newly created inode or they have
  CAP_FSETID in their current user namespace and are privileged over the
  parent directory of the new inode. If any of these two conditions is
  true then the S_ISGID bit can be raised for an S_IXGRP file and if not
  it needs to be stripped.

  However, there are several key issues with the current implementation:

   - S_ISGID stripping logic is entangled with umask stripping.

     For example, if the umask removes the S_IXGRP bit from the file
     about to be created then the S_ISGID bit will be kept.

     The inode_init_owner() helper is responsible for S_ISGID stripping
     and is called before posix_acl_create(). So we can end up with two
     different orderings:

     1. FS without POSIX ACL support

        First strip umask then strip S_ISGID in inode_init_owner().

        In other words, if a filesystem doesn't support or enable POSIX
        ACLs then umask stripping is done directly in the vfs before
        calling into the filesystem:

     2. FS with POSIX ACL support

        First strip S_ISGID in inode_init_owner() then strip umask in
        posix_acl_create().

        In other words, if the filesystem does support POSIX ACLs then
        unmask stripping may be done in the filesystem itself when
        calling posix_acl_create().

     Note that technically filesystems are free to impose their own
     ordering between posix_acl_create() and inode_init_owner() meaning
     that there's additional ordering issues that influence S_ISGID
     inheritance.

     (Note that the commit message of commit 1639a49ccdce ("fs: move
     S_ISGID stripping into the vfs_*() helpers") gets the ordering
     between inode_init_owner() and posix_acl_create() the wrong way
     around. I realized this too late.)

   - Filesystems that don't rely on inode_init_owner() don't get S_ISGID
     stripping logic.

     While that may be intentional (e.g. network filesystems might just
     defer setgid stripping to a server) it is often just a security
     issue.

     Note that mandating the use of inode_init_owner() was proposed as
     an alternative solution but that wouldn't fix the ordering issues
     and there are examples such as afs where the use of
     inode_init_owner() isn't possible.

     In any case, we should also try the cleaner and generalized
     solution first before resorting to this approach.

   - We still have S_ISGID inheritance bugs years after the initial
     round of S_ISGID inheritance fixes:

       e014f37db1a2 ("xfs: use setattr_copy to set vfs inode attributes")
       01ea173e103e ("xfs: fix up non-directory creation in SGID directories")
       fd84bfdddd16 ("ceph: fix up non-directory creation in SGID directories")

  All of this led us to conclude that the current state is too messy.
  While we won't be able to make it completely clean as
  posix_acl_create() is still a filesystem specific call we can improve
  the S_SIGD stripping situation quite a bit by hoisting it out of
  inode_init_owner() and into the respective vfs creation operations.

  The obvious advantage is that we don't need to rely on individual
  filesystems getting S_ISGID stripping right and instead can
  standardize the ordering between S_ISGID and umask stripping directly
  in the VFS.

  A few short implementation notes:

   - The stripping logic needs to happen in vfs_*() helpers for the sake
     of stacking filesystems such as overlayfs that rely on these
     helpers taking care of S_ISGID stripping.

   - Security hooks have never seen the mode as it is ultimately seen by
     the filesystem because of the ordering issue we mentioned. Nothing
     is changed for them. We simply continue to strip the umask before
     passing the mode down to the security hooks.

   - The following filesystems use inode_init_owner() and thus relied on
     S_ISGID stripping: spufs, 9p, bfs, btrfs, ext2, ext4, f2fs,
     hfsplus, hugetlbfs, jfs, minix, nilfs2, ntfs3, ocfs2, omfs,
     overlayfs, ramfs, reiserfs, sysv, ubifs, udf, ufs, xfs, zonefs,
     bpf, tmpfs.

     We've audited all callchains as best as we could. More details can
     be found in the commit message to 1639a49ccdce ("fs: move S_ISGID
     stripping into the vfs_*() helpers")"

* tag 'fs.setgid.v6.0' of git://git.kernel.org/pub/scm/linux/kernel/git/brauner/linux:
  ceph: rely on vfs for setgid stripping
  fs: move S_ISGID stripping into the vfs_*() helpers
  fs: Add missing umask strip in vfs_tmpfile
  fs: add mode_strip_sgid() helper

fs/ceph/file.c
fs/inode.c
fs/namei.c
fs/ocfs2/namei.c
include/linux/fs.h

index 8fab5db..284d2fd 100644 (file)
@@ -656,10 +656,6 @@ static int ceph_finish_async_create(struct inode *dir, struct dentry *dentry,
                /* Directories always inherit the setgid bit. */
                if (S_ISDIR(mode))
                        mode |= S_ISGID;
-               else if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP) &&
-                        !in_group_p(dir->i_gid) &&
-                        !capable_wrt_inode_uidgid(&init_user_ns, dir, CAP_FSETID))
-                       mode &= ~S_ISGID;
        } else {
                in.gid = cpu_to_le32(from_kgid(&init_user_ns, current_fsgid()));
        }
index 524ee91..9c3cd54 100644 (file)
@@ -2326,10 +2326,6 @@ void inode_init_owner(struct user_namespace *mnt_userns, struct inode *inode,
                /* Directories are special, and always inherit S_ISGID */
                if (S_ISDIR(mode))
                        mode |= S_ISGID;
-               else if ((mode & (S_ISGID | S_IXGRP)) == (S_ISGID | S_IXGRP) &&
-                        !in_group_p(i_gid_into_mnt(mnt_userns, dir)) &&
-                        !capable_wrt_inode_uidgid(mnt_userns, dir, CAP_FSETID))
-                       mode &= ~S_ISGID;
        } else
                inode_fsgid_set(inode, mnt_userns);
        inode->i_mode = mode;
@@ -2485,3 +2481,33 @@ struct timespec64 current_time(struct inode *inode)
        return timestamp_truncate(now, inode);
 }
 EXPORT_SYMBOL(current_time);
+
+/**
+ * mode_strip_sgid - handle the sgid bit for non-directories
+ * @mnt_userns: User namespace of the mount the inode was created from
+ * @dir: parent directory inode
+ * @mode: mode of the file to be created in @dir
+ *
+ * If the @mode of the new file has both the S_ISGID and S_IXGRP bit
+ * raised and @dir has the S_ISGID bit raised ensure that the caller is
+ * either in the group of the parent directory or they have CAP_FSETID
+ * in their user namespace and are privileged over the parent directory.
+ * In all other cases, strip the S_ISGID bit from @mode.
+ *
+ * Return: the new mode to use for the file
+ */
+umode_t mode_strip_sgid(struct user_namespace *mnt_userns,
+                       const struct inode *dir, umode_t mode)
+{
+       if ((mode & (S_ISGID | S_IXGRP)) != (S_ISGID | S_IXGRP))
+               return mode;
+       if (S_ISDIR(mode) || !dir || !(dir->i_mode & S_ISGID))
+               return mode;
+       if (in_group_p(i_gid_into_mnt(mnt_userns, dir)))
+               return mode;
+       if (capable_wrt_inode_uidgid(mnt_userns, dir, CAP_FSETID))
+               return mode;
+
+       return mode & ~S_ISGID;
+}
+EXPORT_SYMBOL(mode_strip_sgid);
index ed3ffd9..53b4bc0 100644 (file)
@@ -3023,6 +3023,65 @@ void unlock_rename(struct dentry *p1, struct dentry *p2)
 }
 EXPORT_SYMBOL(unlock_rename);
 
+/**
+ * mode_strip_umask - handle vfs umask stripping
+ * @dir:       parent directory of the new inode
+ * @mode:      mode of the new inode to be created in @dir
+ *
+ * Umask stripping depends on whether or not the filesystem supports POSIX
+ * ACLs. If the filesystem doesn't support it umask stripping is done directly
+ * in here. If the filesystem does support POSIX ACLs umask stripping is
+ * deferred until the filesystem calls posix_acl_create().
+ *
+ * Returns: mode
+ */
+static inline umode_t mode_strip_umask(const struct inode *dir, umode_t mode)
+{
+       if (!IS_POSIXACL(dir))
+               mode &= ~current_umask();
+       return mode;
+}
+
+/**
+ * vfs_prepare_mode - prepare the mode to be used for a new inode
+ * @mnt_userns:                user namespace of the mount the inode was found from
+ * @dir:       parent directory of the new inode
+ * @mode:      mode of the new inode
+ * @mask_perms:        allowed permission by the vfs
+ * @type:      type of file to be created
+ *
+ * This helper consolidates and enforces vfs restrictions on the @mode of a new
+ * object to be created.
+ *
+ * Umask stripping depends on whether the filesystem supports POSIX ACLs (see
+ * the kernel documentation for mode_strip_umask()). Moving umask stripping
+ * after setgid stripping allows the same ordering for both non-POSIX ACL and
+ * POSIX ACL supporting filesystems.
+ *
+ * Note that it's currently valid for @type to be 0 if a directory is created.
+ * Filesystems raise that flag individually and we need to check whether each
+ * filesystem can deal with receiving S_IFDIR from the vfs before we enforce a
+ * non-zero type.
+ *
+ * Returns: mode to be passed to the filesystem
+ */
+static inline umode_t vfs_prepare_mode(struct user_namespace *mnt_userns,
+                                      const struct inode *dir, umode_t mode,
+                                      umode_t mask_perms, umode_t type)
+{
+       mode = mode_strip_sgid(mnt_userns, dir, mode);
+       mode = mode_strip_umask(dir, mode);
+
+       /*
+        * Apply the vfs mandated allowed permission mask and set the type of
+        * file to be created before we call into the filesystem.
+        */
+       mode &= (mask_perms & ~S_IFMT);
+       mode |= (type & S_IFMT);
+
+       return mode;
+}
+
 /**
  * vfs_create - create new file
  * @mnt_userns:        user namespace of the mount the inode was found from
@@ -3048,8 +3107,8 @@ int vfs_create(struct user_namespace *mnt_userns, struct inode *dir,
 
        if (!dir->i_op->create)
                return -EACCES; /* shouldn't it be ENOSYS? */
-       mode &= S_IALLUGO;
-       mode |= S_IFREG;
+
+       mode = vfs_prepare_mode(mnt_userns, dir, mode, S_IALLUGO, S_IFREG);
        error = security_inode_create(dir, dentry, mode);
        if (error)
                return error;
@@ -3312,8 +3371,7 @@ static struct dentry *lookup_open(struct nameidata *nd, struct file *file,
        if (open_flag & O_CREAT) {
                if (open_flag & O_EXCL)
                        open_flag &= ~O_TRUNC;
-               if (!IS_POSIXACL(dir->d_inode))
-                       mode &= ~current_umask();
+               mode = vfs_prepare_mode(mnt_userns, dir->d_inode, mode, mode, mode);
                if (likely(got_write))
                        create_error = may_o_create(mnt_userns, &nd->path,
                                                    dentry, mode);
@@ -3544,6 +3602,7 @@ struct dentry *vfs_tmpfile(struct user_namespace *mnt_userns,
        child = d_alloc(dentry, &slash_name);
        if (unlikely(!child))
                goto out_err;
+       mode = vfs_prepare_mode(mnt_userns, dir, mode, mode, mode);
        error = dir->i_op->tmpfile(mnt_userns, dir, child, mode);
        if (error)
                goto out_err;
@@ -3821,6 +3880,7 @@ int vfs_mknod(struct user_namespace *mnt_userns, struct inode *dir,
        if (!dir->i_op->mknod)
                return -EPERM;
 
+       mode = vfs_prepare_mode(mnt_userns, dir, mode, mode, mode);
        error = devcgroup_inode_mknod(mode, dev);
        if (error)
                return error;
@@ -3871,9 +3931,8 @@ retry:
        if (IS_ERR(dentry))
                goto out1;
 
-       if (!IS_POSIXACL(path.dentry->d_inode))
-               mode &= ~current_umask();
-       error = security_path_mknod(&path, dentry, mode, dev);
+       error = security_path_mknod(&path, dentry,
+                       mode_strip_umask(path.dentry->d_inode, mode), dev);
        if (error)
                goto out2;
 
@@ -3943,7 +4002,7 @@ int vfs_mkdir(struct user_namespace *mnt_userns, struct inode *dir,
        if (!dir->i_op->mkdir)
                return -EPERM;
 
-       mode &= (S_IRWXUGO|S_ISVTX);
+       mode = vfs_prepare_mode(mnt_userns, dir, mode, S_IRWXUGO | S_ISVTX, 0);
        error = security_inode_mkdir(dir, dentry, mode);
        if (error)
                return error;
@@ -3971,9 +4030,8 @@ retry:
        if (IS_ERR(dentry))
                goto out_putname;
 
-       if (!IS_POSIXACL(path.dentry->d_inode))
-               mode &= ~current_umask();
-       error = security_path_mkdir(&path, dentry, mode);
+       error = security_path_mkdir(&path, dentry,
+                       mode_strip_umask(path.dentry->d_inode, mode));
        if (!error) {
                struct user_namespace *mnt_userns;
                mnt_userns = mnt_user_ns(path.mnt);
index c75fd54..961d1cf 100644 (file)
@@ -197,6 +197,7 @@ static struct inode *ocfs2_get_init_inode(struct inode *dir, umode_t mode)
         * callers. */
        if (S_ISDIR(mode))
                set_nlink(inode, 2);
+       mode = mode_strip_sgid(&init_user_ns, dir, mode);
        inode_init_owner(&init_user_ns, inode, dir, mode);
        status = dquot_initialize(inode);
        if (status)
index 8c127ff..5113f65 100644 (file)
@@ -2035,6 +2035,8 @@ extern long compat_ptr_ioctl(struct file *file, unsigned int cmd,
 void inode_init_owner(struct user_namespace *mnt_userns, struct inode *inode,
                      const struct inode *dir, umode_t mode);
 extern bool may_open_dev(const struct path *path);
+umode_t mode_strip_sgid(struct user_namespace *mnt_userns,
+                       const struct inode *dir, umode_t mode);
 
 /*
  * This is the "filldir" function type, used by readdir() to let