Skip to content

Support enforcement of POSIX ACLs - #50

Open
ThomasWaldmann wants to merge 1 commit into
mxmlnkn:masterfrom
ThomasWaldmann:posix-acl-enforcement
Open

Support enforcement of POSIX ACLs#50
ThomasWaldmann wants to merge 1 commit into
mxmlnkn:masterfrom
ThomasWaldmann:posix-acl-enforcement

Conversation

@ThomasWaldmann

Copy link
Copy Markdown

Fixes #49.

The kernel only enforces POSIX ACLs when the file system requests FUSE_CAP_POSIX_ACL during the init handshake. Without it, the ACLs a file system returns via getxattr for system.posix_acl_access / system.posix_acl_default are merely visible, e.g. to getfacl, but access is never checked against them. Inside a user namespace, e.g. a rootless container, the kernel even refuses getxattr for these attributes with EOPNOTSUPP unless the feature was negotiated.

Since mfusepy had no way at all to request optional kernel features, this adds the general mechanism and uses POSIX ACLs as its first application:

  • All FUSE_CAP_* constants from fuse_common.h plus feature_flag_names() for log messages.

  • fuse_conn_info.is_capable, is_wanted, set_feature_flag, and unset_feature_flag, mirroring the libfuse helper functions. They set want and want_ext because libfuse >= 3.17 only converts want into want_ext when exactly one of both was changed (fuse_convert_to_conn_want_ext), and they refuse flags missing from capable / capable_ext because libfuse aborts the mount with EPROTO for unknown want flags.

  • Operations.wanted_features as a declarative alternative that does not require overriding init_with_config:

    class MyFileSystem(mfusepy.Operations):
        wanted_features = mfusepy.FUSE_CAP_POSIX_ACL

    It is applied before the file system's init_with_config is called, so it can still check the outcome with conn_info.is_wanted or adjust it. Features that the kernel or the loaded libfuse version is not capable of, e.g. everything beyond FUSE_CAP_IOCTL_DIR with libfuse 2, are skipped with a warning instead of breaking the mount.

Testing

tests/test_posix_acl.py unit-tests the flag helpers everywhere and mounts a small file system for an end-to-end check. Its two root-owned files can only be read (or not read) as expected if the kernel does enforce the returned ACLs:

file mode ACL entry for the calling user expected
allowed 0640 user:<uid>:r-- readable, although the mode bits deny it
denied 0644 user:<uid>:--- EACCES, although the mode bits allow it

The end-to-end test skips on non-Linux, when running as root (which is not subject to ACL checks), and when the kernel or libfuse is not capable of FUSE_CAP_POSIX_ACL, e.g. for libfuse 2.

Verified on Linux 6.18 with libfuse 3.14 in a rootless container: before the change, both files were readable and getxattr for system.posix_acl_access failed with EOPNOTSUPP; with the change, getfacl shows the ACLs through the mount and access is enforced in both directions. black, ruff, flake8, pylint, codespell, mypy, and pytype are clean and the existing test suite still passes.

I did not touch CHANGELOG.md since its entries seem to be written at version-bump time.

🤖 Generated with Claude Code

Add the FUSE_CAP_* feature flags and a way to request them, so that a
file system can enable optional kernel features during the init
handshake:

 - fuse_conn_info.is_capable / is_wanted / set_feature_flag /
   unset_feature_flag, mirroring the libfuse helpers, including the
   'want' vs. 'want_ext' handling for libfuse >= 3.17.
 - Operations.wanted_features as declarative alternative that does not
   require overriding 'init_with_config'. Unsupported features are
   skipped with a warning because requesting a feature that the kernel
   is not capable of makes libfuse abort the mount with EPROTO.

The motivation is FUSE_CAP_POSIX_ACL (Linux): without it, the ACLs
returned by getxattr for system.posix_acl_access / _default are only
visible, e.g. to getfacl, but the kernel does not check access against
them. Inside a user namespace, e.g. a rootless container, getxattr for
these attributes is even refused with EOPNOTSUPP.

The new test mounts a file system whose files can only be read (or not
read) as expected if the kernel does enforce the returned ACLs.

Fixes mxmlnkn#49

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread tests/test_posix_acl.py
Comment on lines +20 to +41
# Values for the binary ACL format documented in acl(5) and defined in <linux/posix_acl_xattr.h>.
POSIX_ACL_XATTR_VERSION = 2
ACL_UNDEFINED_ID = 0xFFFF_FFFF
ACL_USER_OBJ = 0x01
ACL_USER = 0x02
ACL_GROUP_OBJ = 0x04
ACL_GROUP = 0x08
ACL_MASK = 0x10
ACL_OTHER = 0x20
ACL_READ = 4
ACL_WRITE = 2
ACL_EXECUTE = 1

ACL_ACCESS_XATTR = 'system.posix_acl_access'
FILE_CONTENTS = b'Hello ACL!\n'


def pack_acl(entries) -> bytes:
'Pack (tag, permissions, id) triples into the binary format expected by the kernel.'
return struct.pack('<I', POSIX_ACL_XATTR_VERSION) + b''.join(
struct.pack('<HHI', tag, permissions, entry_id) for tag, permissions, entry_id in entries
)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could be moved to a global place in mfusepy, so that users can just import it from there.

Additionally to pack_acl, there could be also some text_to_acl function.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pack_acl, and also the inverse unpack_acl, seems helpful, yes. How standardized/cross-platform is this struct format? Then again, even if it were not, it would be nice for mfusepy to handle the cross-platform compatibility.

Comment thread tests/test_posix_acl.py
Comment on lines +20 to +41
# Values for the binary ACL format documented in acl(5) and defined in <linux/posix_acl_xattr.h>.
POSIX_ACL_XATTR_VERSION = 2
ACL_UNDEFINED_ID = 0xFFFF_FFFF
ACL_USER_OBJ = 0x01
ACL_USER = 0x02
ACL_GROUP_OBJ = 0x04
ACL_GROUP = 0x08
ACL_MASK = 0x10
ACL_OTHER = 0x20
ACL_READ = 4
ACL_WRITE = 2
ACL_EXECUTE = 1

ACL_ACCESS_XATTR = 'system.posix_acl_access'
FILE_CONTENTS = b'Hello ACL!\n'


def pack_acl(entries) -> bytes:
'Pack (tag, permissions, id) triples into the binary format expected by the kernel.'
return struct.pack('<I', POSIX_ACL_XATTR_VERSION) + b''.join(
struct.pack('<HHI', tag, permissions, entry_id) for tag, permissions, entry_id in entries
)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pack_acl, and also the inverse unpack_acl, seems helpful, yes. How standardized/cross-platform is this struct format? Then again, even if it were not, it would be nice for mfusepy to handle the cross-platform compatibility.

Comment thread mfusepy.py
def is_capable(self, flags: int) -> bool:
'Return whether the kernel supports all of the given FUSE_CAP_* feature flags.'
capable = self.capable_ext if self._has_extended_features else self.capable
return bool(flags) and capable & flags == flags

@mxmlnkn mxmlnkn Aug 10, 2026

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return bool(flags) and capable & flags == flags
return (capable & flags) == flags

Maybe this is better? I don't understand why no flags given should be specially handled and set to False, even though no capability flags were required. Same for is_wanted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

support enforcement of ACLs

2 participants