Support enforcement of POSIX ACLs - #50
Conversation
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>
| # 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 | ||
| ) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| # 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 | ||
| ) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
| 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.
Fixes #49.
The kernel only enforces POSIX ACLs when the file system requests
FUSE_CAP_POSIX_ACLduring the init handshake. Without it, the ACLs a file system returns viagetxattrforsystem.posix_acl_access/system.posix_acl_defaultare merely visible, e.g. togetfacl, but access is never checked against them. Inside a user namespace, e.g. a rootless container, the kernel even refusesgetxattrfor these attributes withEOPNOTSUPPunless 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 fromfuse_common.hplusfeature_flag_names()for log messages.fuse_conn_info.is_capable,is_wanted,set_feature_flag, andunset_feature_flag, mirroring the libfuse helper functions. They setwantandwant_extbecause libfuse >= 3.17 only convertswantintowant_extwhen exactly one of both was changed (fuse_convert_to_conn_want_ext), and they refuse flags missing fromcapable/capable_extbecause libfuse aborts the mount withEPROTOfor unknownwantflags.Operations.wanted_featuresas a declarative alternative that does not require overridinginit_with_config:It is applied before the file system's
init_with_configis called, so it can still check the outcome withconn_info.is_wantedor adjust it. Features that the kernel or the loaded libfuse version is not capable of, e.g. everything beyondFUSE_CAP_IOCTL_DIRwith libfuse 2, are skipped with a warning instead of breaking the mount.Testing
tests/test_posix_acl.pyunit-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:alloweduser:<uid>:r--denieduser:<uid>:---EACCES, although the mode bits allow itThe 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
getxattrforsystem.posix_acl_accessfailed withEOPNOTSUPP; with the change,getfaclshows the ACLs through the mount and access is enforced in both directions.black,ruff,flake8,pylint,codespell,mypy, andpytypeare clean and the existing test suite still passes.I did not touch
CHANGELOG.mdsince its entries seem to be written at version-bump time.🤖 Generated with Claude Code