Skip to content

fuse: expose POSIX ACLs on Linux mounts, fixes #1042#9954

Open
ThomasWaldmann wants to merge 2 commits into
borgbackup:masterfrom
ThomasWaldmann:fuse-mount-acls-1042
Open

fuse: expose POSIX ACLs on Linux mounts, fixes #1042#9954
ThomasWaldmann wants to merge 2 commits into
borgbackup:masterfrom
ThomasWaldmann:fuse-mount-acls-1042

Conversation

@ThomasWaldmann

Copy link
Copy Markdown
Member

Fixes #1042.

The FUSE mount did not expose the archived ACLs at all, so tools reading from a mounted archive (getfacl, rsync -A, cp --preserve=all, ...) did not see them.

What it does

On Linux, POSIX ACLs are exposed by the kernel via the special system.posix_acl_access / system.posix_acl_default xattrs, which use a binary format (see linux/include/uapi/linux/posix_acl_xattr.h): a 4 byte little-endian version header, followed by 8 byte entries of {__le16 e_tag, __le16 e_perm, __le32 e_id}.

  • acl_text_to_xattr() (platform/linux.pyx) converts from our stored ACL text representation (acl_access / acl_default) to that binary format. It reuses the existing acl_use_local_uid_gid / posix_acl_use_stored_uid_gid converters, so --numeric-ids behaves like it does for borg extract.
  • listxattr / getxattr serve those xattrs in both FUSE implementations: fuse.py (llfuse/pyfuse3) and hlfuse.py (mfusepy).
  • Stub in platform/base.py + dispatch in platform/__init__.py, so non-Linux platforms are unaffected.
  • ACL text that can not be converted raises ValueError, which the FUSE layer turns into a warning + EIO instead of killing the mount.

Why the previous attempt did not work

#8843 returned libacl's internal acl_t structure (sized via acl_size()). That is libacl's own representation, not the kernel's xattr wire format, so the kernel could not make sense of it.

To make sure this does not happen again, the conversion is tested against what the kernel itself produces: test_acl_text_to_xattr_matches_kernel sets an ACL on a real file, reads the raw system.posix_acl_access xattr the kernel returns, and requires acl_text_to_xattr() output to be byte identical to it (named and numeric mode).

Limitations

  • Read-only exposure: the mount does not enforce the ACLs for permission checks (we do not negotiate FUSE_POSIX_ACL).
  • Linux only. Other platforms keep the previous behaviour (no ACLs on the mount).
  • Since kernel 6.2 (facd61053cff), the kernel refuses ACL xattr passthrough for FUSE mounts whose superblock lives in a non-initial user namespace (e.g. a mount made inside a rootless container) unless FUSE_POSIX_ACL was negotiated - and none of the Python FUSE bindings expose that init flag. For init-userns mounts (normal hosts, CI) the kernel does the plain passthrough as before, so this works there. The e2e test skips with a clear reason in the userns case.

Tests

  • test_acl_text_to_xattr - binary format / error handling unit test.
  • test_acl_text_to_xattr_matches_kernel - byte-identity against the kernel, see above.
  • test_fuse_acls - e2e: file + dir with access and default ACLs, archived and mounted, compared against the source fs objects both at raw xattr level and via platform.acl_get(), with and without --numeric-ids.

Ran locally on Linux (container) with both pyfuse3 and mfusepy; mypy, ruff and black are happy.

Docs (mount epilog) updated - they claimed ACLs were not supported.

The FUSE mount did not expose the archived ACLs at all, so tools reading
from a mounted archive (getfacl, rsync -A, cp --preserve=all, ...) did not
see them.

On Linux, POSIX ACLs are exposed by the kernel via the special
system.posix_acl_access / system.posix_acl_default xattrs, which use a
binary format (see linux/include/uapi/linux/posix_acl_xattr.h): a 4 byte
little-endian version header, followed by 8 byte entries of
{__le16 e_tag, __le16 e_perm, __le32 e_id}.

Add acl_text_to_xattr() to convert from our stored ACL text representation
(acl_access / acl_default) to that binary format, honouring numeric_ids the
same way borg extract does, and serve these xattrs from listxattr/getxattr
in both FUSE implementations (fuse.py for llfuse/pyfuse3, hlfuse.py for
mfusepy).

Note that a previous attempt at this (borgbackup#8843) returned libacl's internal
acl_t structure (via acl_size), which is not the kernel's xattr wire
format - that is why it did not work. The new conversion is tested against
what the kernel itself produces: acl_text_to_xattr() output must be byte
identical to the system.posix_acl_access xattr the kernel returns for the
same ACL.

The ACLs are exposed read-only and are not enforced for permission checks
(we do not negotiate FUSE_POSIX_ACL). Also note that since kernel 6.2, the
kernel refuses ACL xattr passthrough for FUSE mounts whose superblock lives
in a non-initial user namespace (e.g. rootless containers) unless
FUSE_POSIX_ACL was negotiated, which none of the Python FUSE bindings
support - the test skips in that case.
@codecov

codecov Bot commented Jul 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.51%. Comparing base (e5ed8f6) to head (788f2d8).
⚠️ Report is 7 commits behind head on master.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #9954      +/-   ##
==========================================
+ Coverage   85.49%   85.51%   +0.02%     
==========================================
  Files          93       93              
  Lines       16122    16152      +30     
  Branches     2466     2470       +4     
==========================================
+ Hits        13783    13813      +30     
  Misses       1632     1632              
  Partials      707      707              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

The error paths of the ACL xattr emulation (no such ACL -> ENOATTR,
unconvertible ACL -> EIO) are not reachable via a real mount, so they were
not covered. Add tests that call listxattr/getxattr directly with a fake
item - these also work where the kernel does not offer ACL xattrs on FUSE
mounts (mounts made inside a user namespace).

Also decide once, at import time, whether the ACL xattrs can be emulated:
ACL_XATTRS is empty if not, so the handlers do not need to check is_linux.
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.

FUSE mount and ACLs

1 participant