Skip to content

Fix InitFlags defining Linux-only flags on macOS #751

Description

@cacay

macFUSE only defines a subset of the flags in InitFlags, yet fuser defines all flags on all platforms unconditionally. The worst offenders are bits 23-31, which have different meanings on Linux and macFUSE. For example, I have access to the FUSE_NO_OPENDIR_SUPPORT constant on macOS, yet setting it doesn't change opendir behavior. Instead, it enables FUSE_NODE_RWLOCK, which allows concurrent operations to the same inode. If my file system is not ready for that, I get a nasty concurrency bug.

The fix is to gate non-macFUSE flags using [cfg(not(target_os = "macos"))]; and define the two missing macFUSE flags while we are at it.

Summary

Here's a summary of which platforms define which flags:

Bits Situation
0-6, 10 Exist on both platforms and have the same meaning
7-9, 11-22 Linux only; no effect on macOS (macFUSE assigns these bits no meaning)
23-31 Different meanings on the two platforms (these will lead to nasty bugs)
32+ Don't exist on macFUSE at all; there is no flags2 in its INIT structs

Overlapping flags and what bugs they cause:

You request (Linux name) Bit macFUSE negotiates Effect on macOS
FUSE_CACHE_SYMLINKS 23 FUSE_ACCESS_EXTENDED access() upcalls switch to extended permission masks
FUSE_NO_OPENDIR_SUPPORT 24 FUSE_NODE_RWLOCK kext stops serializing operations per node
FUSE_EXPLICIT_INVAL_DATA 25 FUSE_RENAME_SWAP volume advertises renamex_np(2) swap support
FUSE_MAP_ALIGNMENT 26 FUSE_RENAME_EXCL volume advertises renamex_np(2) exclusive rename
FUSE_SUBMOUNTS 27 FUSE_ALLOCATE kext starts dispatching preallocation
FUSE_HANDLE_KILLPRIV_V2 28 FUSE_EXCHANGE_DATA refused, but only because the kext stopped offering bit 28
FUSE_SETXATTR_EXT 29 FUSE_CASE_INSENSITIVE volume becomes case-insensitive
FUSE_INIT_EXT 30 FUSE_VOL_RENAME accepted if requested, but moot: fuser already ORs this bit into every macOS INIT reply, force-negotiating VOL_RENAME
FUSE_INIT_RESERVED 31 FUSE_XTIMES kext starts sending GETXTIMES

Bit 30 (FUSE_VOL_RENAME/FUSE_INIT_EXT) is especially weird. fuser force enables FUSE_INIT_EXT (Linux) because it doesn't want to track whether the kernel supports flags2, but this ends up enabling FUSE_VOL_RENAME on macOS. And there is a lot of special handling around auto-requesting this capability but hiding it from the user etc., that can all go away.

Bugs in fuser

Hiding some of these flags on macOS will make certain parts of fuser fail to compile. All of these are latent bugs in fuser, though they should be simple to fix. In exchange, parts of the code actually get simpler:

  • ALIASED_UNSUPPORTED_CAPABILITIES is deleted along with its alias-explaining docs; UNSUPPORTED_CAPABILITIES becomes one plain const per platform.
  • The iter_names().last() logging workaround and its "clash" comment are deleted, and capability logs and Debug output name every bit correctly on macOS.
  • macOS INIT negotiation loses its FUSE_INIT_EXT special cases, making FUSE_VOL_RENAME an ordinary capability that can be requested, dropped, and reported.

References

Proposal

Here's a minimal diff to init_flags.rs, sticking with the current organization of Linux+shared flags followed by macOS-only flags. It might be cleaner to interleave the flags so they are ordered by flag index. Or we can group flags as shared, Linux-only, macOS-only, though that'll fail if, say, FreeBSD supports different flags.

bitflags! {
    pub struct InitFlags: u64 {
        /// asynchronous read requests
        const FUSE_ASYNC_READ = 1 << 0;
        /// remote locking for POSIX file locks
        const FUSE_POSIX_LOCKS = 1 << 1;
        /// kernel sends file handle for fstat, etc...
        const FUSE_FILE_OPS = 1 << 2;
        /// handles the O_TRUNC open flag in the filesystem
        const FUSE_ATOMIC_O_TRUNC = 1 << 3;
        /// filesystem handles lookups of "." and ".."
        const FUSE_EXPORT_SUPPORT = 1 << 4;
        /// filesystem can handle write size larger than 4kB
        const FUSE_BIG_WRITES = 1 << 5;
        /// don't apply umask to file mode on create operations
        const FUSE_DONT_MASK = 1 << 6;
        /// kernel supports splice write on the device
        #[cfg(not(target_os = "macos"))]
        const FUSE_SPLICE_WRITE = 1 << 7;
        /// kernel supports splice move on the device
        #[cfg(not(target_os = "macos"))]
        const FUSE_SPLICE_MOVE = 1 << 8;
        /// kernel supports splice read on the device
        #[cfg(not(target_os = "macos"))]
        const FUSE_SPLICE_READ = 1 << 9;
        /// remote locking for BSD style file locks
        const FUSE_FLOCK_LOCKS = 1 << 10;
        /// kernel supports ioctl on directories
        #[cfg(not(target_os = "macos"))]
        const FUSE_HAS_IOCTL_DIR = 1 << 11;
        /// automatically invalidate cached pages
        #[cfg(not(target_os = "macos"))]
        const FUSE_AUTO_INVAL_DATA = 1 << 12;
        /// do READDIRPLUS (READDIR+LOOKUP in one)
        #[cfg(not(target_os = "macos"))]
        const FUSE_DO_READDIRPLUS = 1 << 13;
        /// adaptive readdirplus
        #[cfg(not(target_os = "macos"))]
        const FUSE_READDIRPLUS_AUTO = 1 << 14;
        /// asynchronous direct I/O submission
        #[cfg(not(target_os = "macos"))]
        const FUSE_ASYNC_DIO = 1 << 15;
        /// use writeback cache for buffered writes
        #[cfg(not(target_os = "macos"))]
        const FUSE_WRITEBACK_CACHE = 1 << 16;
        /// kernel supports zero-message opens
        #[cfg(not(target_os = "macos"))]
        const FUSE_NO_OPEN_SUPPORT = 1 << 17;
        /// allow parallel lookups and readdir
        #[cfg(not(target_os = "macos"))]
        const FUSE_PARALLEL_DIROPS = 1 << 18;
        /// fs handles killing suid/sgid/cap on write/chown/trunc
        ///
        /// The filesystem takes responsibility for clearing suid, sgid and the
        /// `security.capability` xattr on every write, chown and truncate. No per-request
        /// signal accompanies this: the operation itself is the signal.
        ///
        /// Unlike [`Self::FUSE_HANDLE_KILLPRIV_V2`] this does not set `SB_NOSEC`, so the kernel
        /// still computes the removal and applies it through a `setattr`. What it buys is that
        /// the kernel skips refreshing attributes first, trusting the filesystem to keep them
        /// right.
        ///
        /// Note that this reaches further than the write, chown and truncate the contract names.
        /// Negotiating either killpriv capability also stops the kernel removing privileges on
        /// `fallocate` and `copy_file_range`, and neither carries a signal saying so, so a
        /// filesystem that wants those to behave like a local one has to clear the bits there
        /// itself
        #[cfg(not(target_os = "macos"))]
        const FUSE_HANDLE_KILLPRIV = 1 << 19;
        /// filesystem supports posix acls
        #[cfg(not(target_os = "macos"))]
        const FUSE_POSIX_ACL = 1 << 20;
        /// reading the device after abort returns ECONNABORTED
        #[cfg(not(target_os = "macos"))]
        const FUSE_ABORT_ERROR = 1 << 21;
        /// init_out.max_pages contains the max number of req pages
        #[cfg(not(target_os = "macos"))]
        const FUSE_MAX_PAGES = 1 << 22;
        /// cache READLINK responses
        #[cfg(not(target_os = "macos"))]
        const FUSE_CACHE_SYMLINKS = 1 << 23;
        /// kernel supports zero-message opendir
        #[cfg(not(target_os = "macos"))]
        const FUSE_NO_OPENDIR_SUPPORT = 1 << 24;
        /// only invalidate cached pages on explicit request
        #[cfg(not(target_os = "macos"))]
        const FUSE_EXPLICIT_INVAL_DATA = 1 << 25;
        /// map_alignment field is valid
        #[cfg(not(target_os = "macos"))]
        const FUSE_MAP_ALIGNMENT = 1 << 26;
        /// filesystem supports submounts
        #[cfg(not(target_os = "macos"))]
        const FUSE_SUBMOUNTS = 1 << 27;
        /// fs handles killing suid/sgid/cap on write/chown/trunc (v2)
        ///
        /// As with [`Self::FUSE_HANDLE_KILLPRIV`], clearing the `security.capability` xattr on
        /// every write, chown and truncate is the filesystem's job and carries no per-request
        /// signal. This capability additionally sets `SB_NOSEC`, which stops the kernel doing
        /// it, though only for an inode whose attributes are cached: fuse clears `S_NOSEC` on
        /// every attribute refresh, since another client may have set the xattr meanwhile. A
        /// filesystem therefore cannot rely on the kernel having done it.
        ///
        /// What v2 adds is finer control over suid and sgid alone, matching what the VFS would
        /// have done: they are cleared on a write or truncate only when the caller lacks
        /// `CAP_FSETID`, and sgid only when the file is group-executable. The kernel reports
        /// the cases it wants cleared through the `kill_suid_gid` argument of
        /// [`Filesystem::setattr`](crate::Filesystem::setattr),
        /// [`open`](crate::Filesystem::open) and [`create`](crate::Filesystem::create), and
        /// through [`WriteFlags::FUSE_WRITE_KILL_SUIDGID`](crate::WriteFlags) on a write.
        /// Honoring those covers suid and sgid only, not file capabilities, and only the
        /// operations that carry a signal: see [`Self::FUSE_HANDLE_KILLPRIV`] for the ones that
        /// do not
        #[cfg(not(target_os = "macos"))]
        const FUSE_HANDLE_KILLPRIV_V2 = 1 << 28;
        /// extended setxattr support
        #[cfg(not(target_os = "macos"))]
        const FUSE_SETXATTR_EXT = 1 << 29;
        /// extended fuse_init_in request
        #[cfg(not(target_os = "macos"))]
        const FUSE_INIT_EXT = 1 << 30;
        /// reserved, do not use
        #[cfg(not(target_os = "macos"))]
        const FUSE_INIT_RESERVED = 1 << 31;
        /// add security context to create/mkdir/symlink/mknod
        #[cfg(not(target_os = "macos"))]
        const FUSE_SECURITY_CTX = 1 << 32;
        /// filesystem supports per-inode DAX
        #[cfg(not(target_os = "macos"))]
        const FUSE_HAS_INODE_DAX = 1 << 33;
        /// create with supplementary group
        #[cfg(not(target_os = "macos"))]
        const FUSE_CREATE_SUPP_GROUP = 1 << 34;
        /// kernel supports expire-only invalidation
        #[cfg(not(target_os = "macos"))]
        const FUSE_HAS_EXPIRE_ONLY = 1 << 35;
        /// allow mmap for direct I/O files
        #[cfg(not(target_os = "macos"))]
        const FUSE_DIRECT_IO_ALLOW_MMAP = 1 << 36;
        /// filesystem wants to use passthrough files
        #[cfg(not(target_os = "macos"))]
        const FUSE_PASSTHROUGH = 1 << 37;
        /// filesystem does not support export
        #[cfg(not(target_os = "macos"))]
        const FUSE_NO_EXPORT_SUPPORT = 1 << 38;
        /// kernel supports resend requests
        #[cfg(not(target_os = "macos"))]
        const FUSE_HAS_RESEND = 1 << 39;
        /// allow idmapped mounts
        #[cfg(not(target_os = "macos"))]
        const FUSE_ALLOW_IDMAP = 1 << 40;
        /// kernel supports io_uring for communication
        #[cfg(not(target_os = "macos"))]
        const FUSE_OVER_IO_URING = 1 << 41;
        /// kernel supports request timeout
        #[cfg(not(target_os = "macos"))]
        const FUSE_REQUEST_TIMEOUT = 1 << 42;

        /// `access()` requests carry macFUSE's extended permission mask (delete, append,
        /// attribute, extended-attribute and ownership rights) rather than plain
        /// read/write/execute bits
        #[cfg(target_os = "macos")]
        const FUSE_ACCESS_EXTENDED = 1 << 23;
        /// the filesystem is safe against concurrent operations on the same node, so the
        /// kernel stops serializing them per node
        #[cfg(target_os = "macos")]
        const FUSE_NODE_RWLOCK = 1 << 24;
        /// filesystem supports the `RENAME_SWAP` flag of `renamex_np(2)`.
        /// Requesting this also selects macFUSE's extended `fuse_rename_in` layout
        #[cfg(target_os = "macos")]
        const FUSE_RENAME_SWAP = 1 << 25;
        /// filesystem supports the `RENAME_EXCL` flag of `renamex_np(2)`.
        /// Requesting this also selects macFUSE's extended `fuse_rename_in` layout
        #[cfg(target_os = "macos")]
        const FUSE_RENAME_EXCL = 1 << 26;
        /// pre-allocate space for a file
        #[cfg(target_os = "macos")]
        const FUSE_ALLOCATE = 1 << 27;
        /// atomically exchange data between files
        #[cfg(target_os = "macos")]
        const FUSE_EXCHANGE_DATA = 1 << 28;
        /// filesystem is case-insensitive
        #[cfg(target_os = "macos")]
        const FUSE_CASE_INSENSITIVE = 1 << 29;
        /// filesystem supports volume renaming
        #[cfg(target_os = "macos")]
        const FUSE_VOL_RENAME = 1 << 30;
        /// filesystem supports extended times (backup and creation times)
        #[cfg(target_os = "macos")]
        const FUSE_XTIMES = 1 << 31;
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions