Re: [RFC PATCH] fuse: Clear SGID bit when setting mode in setacl

From: Vivek Goyal
Date: Mon Mar 01 2021 - 13:10:19 EST


On Fri, Feb 26, 2021 at 06:33:57PM +0000, Luis Henriques wrote:
> Setting file permissions with POSIX ACLs (setxattr) isn't clearing the
> setgid bit. This seems to be CVE-2016-7097, detected by running fstest
> generic/375 in virtiofs. Unfortunately, when the fix for this CVE landed
> in the kernel with commit 073931017b49 ("posix_acl: Clear SGID bit when
> setting file permissions"), FUSE didn't had ACLs support yet.

Hi Luis,

Interesting. I did not know that "chmod" can lead to clearing of SGID
as well. Recently we implemented FUSE_HANDLE_KILLPRIV_V2 flag which
means that file server is responsible for clearing of SUID/SGID/caps
as per following rules.

- caps are always cleared on chown/write/truncate
- suid is always cleared on chown, while for truncate/write it is cleared
only if caller does not have CAP_FSETID.
- sgid is always cleared on chown, while for truncate/write it is cleared
only if caller does not have CAP_FSETID as well as file has group execute
permission.

And we don't have anything about "chmod" in this list. Well, I will test
this and come back to this little later.

I see following comment in fuse_set_acl().

/*
* Fuse userspace is responsible for updating access
* permissions in the inode, if needed. fuse_setxattr
* invalidates the inode attributes, which will force
* them to be refreshed the next time they are used,
* and it also updates i_ctime.
*/

So looks like that original code has been written with intent that
file server is responsible for updating inode permissions. I am
assuming this will include clearing of S_ISGID if needed.

But question is, does file server has enough information to be able
to handle proper clearing of S_ISGID info. IIUC, file server will need
two pieces of information atleast.

- gid of the caller.
- Whether caller has CAP_FSETID or not.

I think we have first piece of information but not the second one. May
be we need to send this in fuse_setxattr_in->flags. And file server
can drop CAP_FSETID while doing setxattr().

What about "gid" info. We don't change to caller's uid/gid while doing
setxattr(). So host might not clear S_ISGID or clear it when it should
not. I am wondering that can we switch to caller's uid/gid in setxattr(),
atleast while setting acls.

Thanks
Vivek

>
> Signed-off-by: Luis Henriques <lhenriques@xxxxxxx>
> ---
> fs/fuse/acl.c | 29 ++++++++++++++++++++++++++---
> 1 file changed, 26 insertions(+), 3 deletions(-)
>
> diff --git a/fs/fuse/acl.c b/fs/fuse/acl.c
> index f529075a2ce8..1b273277c1c9 100644
> --- a/fs/fuse/acl.c
> +++ b/fs/fuse/acl.c
> @@ -54,7 +54,9 @@ int fuse_set_acl(struct inode *inode, struct posix_acl *acl, int type)
> {
> struct fuse_conn *fc = get_fuse_conn(inode);
> const char *name;
> + umode_t mode = inode->i_mode;
> int ret;
> + bool update_mode = false;
>
> if (fuse_is_bad(inode))
> return -EIO;
> @@ -62,11 +64,18 @@ int fuse_set_acl(struct inode *inode, struct posix_acl *acl, int type)
> if (!fc->posix_acl || fc->no_setxattr)
> return -EOPNOTSUPP;
>
> - if (type == ACL_TYPE_ACCESS)
> + if (type == ACL_TYPE_ACCESS) {
> name = XATTR_NAME_POSIX_ACL_ACCESS;
> - else if (type == ACL_TYPE_DEFAULT)
> + if (acl) {
> + ret = posix_acl_update_mode(inode, &mode, &acl);
> + if (ret)
> + return ret;
> + if (inode->i_mode != mode)
> + update_mode = true;
> + }
> + } else if (type == ACL_TYPE_DEFAULT) {
> name = XATTR_NAME_POSIX_ACL_DEFAULT;
> - else
> + } else
> return -EINVAL;
>
> if (acl) {
> @@ -98,6 +107,20 @@ int fuse_set_acl(struct inode *inode, struct posix_acl *acl, int type)
> } else {
> ret = fuse_removexattr(inode, name);
> }
> + if (!ret && update_mode) {
> + struct dentry *entry;
> + struct iattr attr;
> +
> + entry = d_find_alias(inode);
> + if (entry) {
> + memset(&attr, 0, sizeof(attr));
> + attr.ia_valid = ATTR_MODE | ATTR_CTIME;
> + attr.ia_mode = mode;
> + attr.ia_ctime = current_time(inode);
> + ret = fuse_do_setattr(entry, &attr, NULL);
> + dput(entry);
> + }
> + }
> forget_all_cached_acls(inode);
> fuse_invalidate_attr(inode);
>
>