[PATCH 6/8] fuse: update file mode when updating acls

Darrick J. Wong
2025-09-16T00:25:38Z

If someone sets ACLs on a file that can be expressed fully as Unix DAC
mode bits, most local filesystems will then update the mode bits and
drop the ACL xattr to reduce inefficiency in the file access paths.
Let's do that too. Note that means that we can setacl and end up with
no ACL xattrs, so we also need to tolerate ENODATA returns from
fuse_removexattr.

Note that here we define a "local" fuse filesystem as one that uses
fuseblk mode; we'll shortly add fuse servers that use iomap for the file
IO path to that list.

Signed-off-by: "Darrick J. Wong" <djwong@kernel.org>
---
fs/fuse/acl.c | 40 +++++++++++++++++++++++++++++++++++++++-
1 file changed, 39 insertions(+), 1 deletion(-)

diff --git a/fs/fuse/acl.c b/fs/fuse/acl.c
index 8f484b105f13ab..4997827ee83c6d 100644
--- a/fs/fuse/acl.c
+++ b/fs/fuse/acl.c
@@ -11,6 +11,16 @@
#include <linux/posix_acl.h>
#include <linux/posix_acl_xattr.h>

+/*
+ * If this fuse server behaves like a local filesystem, we can implement the
+ * kernel's optimizations for ACLs for local filesystems instead of passing
+ * the ACL requests straight through to another server.
+ */
+static inline bool fuse_has_local_acls(const struct fuse_conn *fc)
+{
+ return fc->posix_acl && fc->local_fs;
+}
+
static struct posix_acl *__fuse_get_acl(struct fuse_conn *fc,
struct inode *inode, int type, bool rcu)
{
@@ -98,6 +108,7 @@ int fuse_set_acl(struct mnt_idmap *idmap, struct dentry *dentry,
struct inode *inode = d_inode(dentry);
struct fuse_conn *fc = get_fuse_conn(inode);
const char *name;
+ umode_t mode = inode->i_mode;
int ret;

if (fuse_is_bad(inode))
@@ -113,6 +124,17 @@ int fuse_set_acl(struct mnt_idmap *idmap, struct dentry *dentry,
else
return -EINVAL;

+ /*
+ * If the ACL can be represented entirely with changes to the mode
+ * bits, then most filesystems will update the mode bits and delete
+ * the ACL xattr.
+ */
+ if (acl && type == ACL_TYPE_ACCESS && fuse_has_local_acls(fc)) {
+ ret = posix_acl_update_mode(idmap, inode, &mode, &acl);
+ if (ret)
+ return ret;
+ }
+
if (acl) {
unsigned int extra_flags = 0;
/*
@@ -143,7 +165,7 @@ int fuse_set_acl(struct mnt_idmap *idmap, struct dentry *dentry,
* through POSIX ACLs. Such daemons don't expect setgid bits to
* be stripped.
*/
- if (fc->posix_acl &&
+ if (fc->posix_acl && mode == inode->i_mode &&
!in_group_or_capable(idmap, inode,
i_gid_into_vfsgid(idmap, inode)))
extra_flags |= FUSE_SETXATTR_ACL_KILL_SGID;
@@ -152,6 +174,22 @@ int fuse_set_acl(struct mnt_idmap *idmap, struct dentry *dentry,
kfree(value);
} else {
ret = fuse_removexattr(inode, name);
+ /* If the acl didn't exist to start with that's fine. */
+ if (ret == -ENODATA)
+ ret = 0;
+ }
+
+ /* If we scheduled a mode update above, push that to userspace now. */
+ if (!ret) {
+ struct iattr attr = { };
+
+ if (mode != inode->i_mode) {
+ attr.ia_valid |= ATTR_MODE;
+ attr.ia_mode = mode;
+ }
+
+ if (attr.ia_valid)
+ ret = fuse_do_setattr(idmap, dentry, &attr, NULL);
}

if (fc->posix_acl) {