diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f8522c82..575b9285 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,6 +75,10 @@ jobs: run: | rustup target add x86_64-unknown-linux-musl + - name: Enable FUSE allow_other + run: | + echo 'user_allow_other' | sudo tee /etc/fuse.conf + - name: Run tests run: | cargo build --all --all-targets --features=libfuse,${{ matrix.features }} diff --git a/Cargo.toml b/Cargo.toml index 59c391fb..189585b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,10 @@ async-trait = { version = "0.1", optional = true } tokio = { version = "1.48.0", features = ["rt", "rt-multi-thread"], optional = true } zerocopy = { version = "0.8", features = ["derive"] } nix = { version = "0.30.0", features = ["fs", "user", "poll", "socket", "uio", "mount", "process", "ioctl"] } +bimap = "0.6.3" +dashmap = "6.2.1" +lazy_static = "1.5.0" +regex = "1.12.4" [dev-dependencies] env_logger = "0.11.7" @@ -47,6 +51,7 @@ bincode = "1.3.1" serde = { version = "1.0.102", features = ["std", "derive"] } tempfile = "3.10.1" nix = { version = "0.30.0", features = ["poll", "fs", "ioctl"] } +test-log = "0.2.21" [build-dependencies] pkg-config = "0.3.14" diff --git a/build.rs b/build.rs index d684987d..25a63f63 100644 --- a/build.rs +++ b/build.rs @@ -47,6 +47,9 @@ fn main() { } fn configure_libfuse3() -> Result<(), pkg_config::Error> { + eprintln!( + "Using libfuse3 as the mount backend may cause memory leaks in some scenarios, for example, if AutoUnmount is set." + ); pkg_config::Config::new() .atleast_version("3.0.0") .probe("fuse3")?; diff --git a/src/ll/errno_mapping.rs b/src/ll/errno_mapping.rs new file mode 100644 index 00000000..de4710be --- /dev/null +++ b/src/ll/errno_mapping.rs @@ -0,0 +1,447 @@ +//! Module responsible for parsing error strings returned by standard output of utility binaries. +//! +use bimap::BiHashMap; +use dashmap::DashMap; +use dashmap::mapref::one::Ref; +use lazy_static::lazy_static; +use libc::strerror_r; +use std::ffi::CStr; +use std::ffi::CString; +use std::ffi::NulError; +use std::ffi::OsStr; +use std::ffi::OsString; +use std::io; +use std::os::unix::ffi::OsStrExt; + +use crate::Errno; + +unsafe extern "C" { + #[cfg(not(any(target_os = "dragonfly", target_os = "vxworks", target_os = "rtems")))] + #[cfg_attr( + any( + target_os = "linux", + target_os = "emscripten", + target_os = "fuchsia", + target_os = "l4re", + target_os = "hurd", + ), + link_name = "__errno_location" + )] + #[cfg_attr( + any( + target_os = "netbsd", + target_os = "openbsd", + target_os = "android", + target_os = "redox", + target_os = "nuttx", + target_env = "newlib" + ), + link_name = "__errno" + )] + #[cfg_attr( + any(target_os = "solaris", target_os = "illumos"), + link_name = "___errno" + )] + #[cfg_attr(target_os = "nto", link_name = "__get_errno_ptr")] + #[cfg_attr( + any(target_os = "freebsd", target_vendor = "apple"), + link_name = "__error" + )] + #[cfg_attr(target_os = "haiku", link_name = "_errnop")] + #[cfg_attr(target_os = "aix", link_name = "_Errno")] + fn errno_location() -> *mut libc::c_int; +} + +pub(crate) fn set_errno(value: i32) { + unsafe { *errno_location() = value }; +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) struct LocaleId(CString); + +impl TryFrom<&str> for LocaleId { + type Error = NulError; + fn try_from(locale: &str) -> Result { + let cstr = CString::new(locale)?; + Ok(Self(cstr)) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct Locale(libc::locale_t); + +impl Locale { + fn new(category: libc::c_int, locale: LocaleId) -> Result { + let inner = unsafe { libc::newlocale(category, locale.0.as_ptr(), std::ptr::null_mut()) }; + if inner.is_null() { + return Err(std::io::Error::last_os_error()); + } + Ok(Self(inner)) + } + + /// Sets the locale for the current thread. + /// SAFETY: this function stores a raw pointer to the previous locale, which may be + /// invalid when another Locale object is dropped. The locale guards must be dropped + /// in the correct initialization order. + unsafe fn activate(&self) -> LocaleActivationGuard<'_> { + let old_locale = unsafe { libc::uselocale(self.0) }; + LocaleActivationGuard::new(self, old_locale) + } + + fn with_activate(&self, f: impl FnOnce() -> R) -> R { + let _guard = unsafe { self.activate() }; + f() + } +} + +impl Drop for Locale { + fn drop(&mut self) { + unsafe { libc::freelocale(self.0) }; + } +} + +struct LocaleActivationGuard<'a> { + _locale: &'a Locale, + old_locale: libc::locale_t, +} + +impl<'a> LocaleActivationGuard<'a> { + fn new(locale: &'a Locale, old_locale: libc::locale_t) -> Self { + Self { + _locale: locale, + old_locale, + } + } +} + +impl Drop for LocaleActivationGuard<'_> { + fn drop(&mut self) { + unsafe { libc::uselocale(self.old_locale) }; + } +} + +// Sourced from https://github.com/pgdr/moreutils/blob/master/Makefile +const ALL_RAW_ERRNOS: &[libc::c_int] = &[ + libc::EPERM, + libc::ENOENT, + libc::ESRCH, + libc::EINTR, + libc::EIO, + libc::ENXIO, + libc::E2BIG, + libc::ENOEXEC, + libc::EBADF, + libc::ECHILD, + libc::EAGAIN, + libc::ENOMEM, + libc::EACCES, + libc::EFAULT, + libc::ENOTBLK, + libc::EBUSY, + libc::EEXIST, + libc::EXDEV, + libc::ENODEV, + libc::ENOTDIR, + libc::EISDIR, + libc::EINVAL, + libc::ENFILE, + libc::EMFILE, + libc::ENOTTY, + libc::ETXTBSY, + libc::EFBIG, + libc::ENOSPC, + libc::ESPIPE, + libc::EROFS, + libc::EMLINK, + libc::EPIPE, + libc::EDOM, + libc::ERANGE, + libc::EDEADLK, + libc::ENAMETOOLONG, + libc::ENOLCK, + libc::ENOSYS, + libc::ENOTEMPTY, + libc::ELOOP, + libc::EWOULDBLOCK, + libc::ENOMSG, + libc::EIDRM, + #[cfg(target_os = "linux")] + libc::ECHRNG, + #[cfg(target_os = "linux")] + libc::EL2NSYNC, + #[cfg(target_os = "linux")] + libc::EL3HLT, + #[cfg(target_os = "linux")] + libc::EL3RST, + #[cfg(target_os = "linux")] + libc::ELNRNG, + #[cfg(target_os = "linux")] + libc::EUNATCH, + #[cfg(target_os = "linux")] + libc::ENOCSI, + #[cfg(target_os = "linux")] + libc::EL2HLT, + #[cfg(target_os = "linux")] + libc::EBADE, + #[cfg(target_os = "linux")] + libc::EBADR, + #[cfg(target_os = "linux")] + libc::EXFULL, + #[cfg(target_os = "linux")] + libc::ENOANO, + #[cfg(target_os = "linux")] + libc::EBADRQC, + #[cfg(target_os = "linux")] + libc::EBADSLT, + #[cfg(target_os = "linux")] + libc::EDEADLOCK, + #[cfg(target_os = "linux")] + libc::EBFONT, + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd" + )))] + libc::ENOSTR, + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd" + )))] + libc::ENODATA, + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd" + )))] + libc::ETIME, + #[cfg(not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd" + )))] + libc::ENOSR, + #[cfg(target_os = "linux")] + libc::ENONET, + #[cfg(target_os = "linux")] + libc::ENOPKG, + libc::EREMOTE, + libc::ENOLINK, + #[cfg(target_os = "linux")] + libc::EADV, + #[cfg(target_os = "linux")] + libc::ESRMNT, + #[cfg(target_os = "linux")] + libc::ECOMM, + libc::EPROTO, + libc::EMULTIHOP, + #[cfg(target_os = "linux")] + libc::EDOTDOT, + libc::EBADMSG, + libc::EOVERFLOW, + #[cfg(target_os = "linux")] + libc::ENOTUNIQ, + #[cfg(target_os = "linux")] + libc::EBADFD, + #[cfg(target_os = "linux")] + libc::EREMCHG, + #[cfg(target_os = "linux")] + libc::ELIBACC, + #[cfg(target_os = "linux")] + libc::ELIBBAD, + #[cfg(target_os = "linux")] + libc::ELIBSCN, + #[cfg(target_os = "linux")] + libc::ELIBMAX, + #[cfg(target_os = "linux")] + libc::ELIBEXEC, + libc::EILSEQ, + #[cfg(target_os = "linux")] + libc::ERESTART, + #[cfg(target_os = "linux")] + libc::ESTRPIPE, + libc::EUSERS, + libc::ENOTSOCK, + libc::EDESTADDRREQ, + libc::EMSGSIZE, + libc::EPROTOTYPE, + libc::ENOPROTOOPT, + libc::EPROTONOSUPPORT, + libc::ESOCKTNOSUPPORT, + libc::EOPNOTSUPP, + libc::EPFNOSUPPORT, + libc::EAFNOSUPPORT, + libc::EADDRINUSE, + libc::EADDRNOTAVAIL, + libc::ENETDOWN, + libc::ENETUNREACH, + libc::ENETRESET, + libc::ECONNABORTED, + libc::ECONNRESET, + libc::ENOBUFS, + libc::EISCONN, + libc::ENOTCONN, + libc::ESHUTDOWN, + libc::ETOOMANYREFS, + libc::ETIMEDOUT, + libc::ECONNREFUSED, + libc::EHOSTDOWN, + libc::EHOSTUNREACH, + libc::EALREADY, + libc::EINPROGRESS, + libc::ESTALE, + #[cfg(target_os = "linux")] + libc::EUCLEAN, + #[cfg(target_os = "linux")] + libc::ENOTNAM, + #[cfg(target_os = "linux")] + libc::ENAVAIL, + #[cfg(target_os = "linux")] + libc::EISNAM, + #[cfg(target_os = "linux")] + libc::EREMOTEIO, + libc::EDQUOT, + #[cfg(target_os = "linux")] + libc::ENOMEDIUM, + #[cfg(target_os = "linux")] + libc::EMEDIUMTYPE, + libc::ECANCELED, + #[cfg(target_os = "linux")] + libc::ENOKEY, + #[cfg(target_os = "linux")] + libc::EKEYEXPIRED, + #[cfg(target_os = "linux")] + libc::EKEYREVOKED, + #[cfg(target_os = "linux")] + libc::EKEYREJECTED, + libc::EOWNERDEAD, + libc::ENOTRECOVERABLE, + #[cfg(target_os = "linux")] + libc::ERFKILL, + #[cfg(target_os = "linux")] + libc::EHWPOISON, + libc::ENOTSUP, +]; + +lazy_static! { + static ref ERRNO_MAPPING: ErrnoMapping = ErrnoMapping::new(); +} + +type ErrnoLocaleMapping = BiHashMap; +type ErrnoMapping = DashMap; + +fn strerror_r_dynamic(errnum: i32) -> Result { + let mut buffer = vec![0i8; 1024]; + set_errno(0); + while unsafe { strerror_r(errnum, buffer.as_mut_ptr(), buffer.len()) } != 0 { + let error = io::Error::last_os_error(); + if error.raw_os_error().unwrap_or(0) == libc::ERANGE { + buffer.resize(buffer.len() + 1024, 0); + continue; + } else { + return Err(error); + } + } + let cstr = unsafe { CStr::from_ptr(buffer.as_ptr()) }; + Ok(OsStr::from_bytes(cstr.to_bytes()).to_os_string()) +} + +fn populate_errno_mapping( + mapping: &mut ErrnoLocaleMapping, + locale_id: &LocaleId, +) -> Result<(), std::io::Error> { + let locale = Locale::new(libc::LC_MESSAGES_MASK, locale_id.clone())?; + locale.with_activate(|| { + for errno in ALL_RAW_ERRNOS.iter() { + let errno = Errno::from_i32(*errno); + if mapping.contains_left(&errno) { + continue; + } + let error_str = match strerror_r_dynamic(errno.code()) { + Ok(os_str) => os_str, + Err(_) => continue, + }; + mapping.insert(errno, error_str); + } + }); + Ok(()) +} + +fn get_errno_mapping<'a>( + mapping: &'a ErrnoMapping, + locale_id: &LocaleId, +) -> Result, std::io::Error> { + if let Some(locale_mapping) = mapping.get(locale_id) { + return Ok(locale_mapping); + } + let ref_mut = + mapping + .entry(locale_id.clone()) + .or_try_insert_with(|| -> Result<_, std::io::Error> { + let mut mapping = ErrnoLocaleMapping::new(); + populate_errno_mapping(&mut mapping, locale_id)?; + Ok(mapping) + })?; + Ok(ref_mut.downgrade()) +} + +#[allow(unused)] +pub(crate) fn get_errno_message( + errno: impl Into, + locale_id: &LocaleId, +) -> Result, std::io::Error> { + let mapping = get_errno_mapping(&ERRNO_MAPPING, locale_id)?; + Ok(mapping + .get_by_left(&errno.into()) + .map(|os_str| os_str.to_owned())) +} + +/// Attempts to convert a message to an errno object. +#[allow(unused)] +pub(crate) fn get_errno_by_message( + message: impl Into, + locale_id: &LocaleId, +) -> Result, std::io::Error> { + let mapping = get_errno_mapping(&ERRNO_MAPPING, locale_id)?; + Ok(mapping.get_by_right(&message.into()).copied()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_errno_message() { + let errno = Errno::EPERM; + let message = get_errno_message(errno, &"C".try_into().expect("locale should be valid")) + .expect("locale should be valid") + .expect("message should be present"); + assert_eq!(message, "Operation not permitted"); + } + + #[test] + fn test_get_errno_by_message() { + let message = OsString::from("Operation not permitted"); + let errno = get_errno_by_message(message, &"C".try_into().expect("locale should be valid")) + .expect("locale should be valid") + .expect("errno should be present"); + assert_eq!(errno, Errno::EPERM); + } + + #[ignore = "German locale needs to be installed"] + #[test] + fn test_get_errno_message_in_german() { + let errno = Errno::ENOENT; + let message = get_errno_message( + errno, + &"de_DE.utf8".try_into().expect("locale should be valid"), + ) + .expect("locale should be valid") + .expect("message should be present"); + assert_eq!(message, "Datei oder Verzeichnis nicht gefunden"); + } +} diff --git a/src/ll/mod.rs b/src/ll/mod.rs index 1b139cfe..169ec4f0 100644 --- a/src/ll/mod.rs +++ b/src/ll/mod.rs @@ -1,6 +1,7 @@ //! Low-level kernel communication. mod argument; +pub(crate) mod errno_mapping; pub(crate) mod flags; pub(crate) mod fuse_abi; pub(crate) mod ioctl; @@ -50,7 +51,7 @@ macro_rules! no_xattr_doc { } /// Represents an error code to be returned to the caller -#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub struct Errno( /// Positive value. NonZeroI32, diff --git a/src/mnt/fuse2.rs b/src/mnt/fuse2.rs index b978f363..8c9593ff 100644 --- a/src/mnt/fuse2.rs +++ b/src/mnt/fuse2.rs @@ -5,6 +5,7 @@ use std::os::unix::prelude::FromRawFd; use std::os::unix::prelude::OsStrExt; use std::path::Path; use std::sync::Arc; +use std::time::Duration; use crate::SessionACL; use crate::dev_fuse::DevFuse; @@ -25,12 +26,16 @@ fn ensure_last_os_error() -> io::Error { pub(crate) struct MountImpl { mountpoint: CString, } + impl MountImpl { pub(crate) fn new( mountpoint: &Path, options: &[MountOption], acl: SessionACL, ) -> io::Result<(Arc, MountImpl)> { + log::warn!( + "Using libfuse2 as the mount backend may cause memory leaks in some scenarios, for example, if AutoUnmount is set." + ); let mountpoint = CString::new(mountpoint.as_os_str().as_bytes()).unwrap(); with_fuse_args(options, acl, |args| { let fd = unsafe { fuse_mount_compat25(mountpoint.as_ptr(), args) }; @@ -50,24 +55,22 @@ impl MountImpl { // no indication of the error available to the caller. So we call unmount // directly, which is what osxfuse does anyway, since we already converted // to the real path when we first mounted. - if let Err(err) = crate::mnt::libc_umount(&self.mountpoint) { - // Linux always returns EPERM for non-root users. We have to let the - // library go through the setuid-root "fusermount -u" to unmount. - if err == nix::errno::Errno::EPERM { - #[cfg(not(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" - )))] - unsafe { - fuse_unmount_compat22(self.mountpoint.as_ptr()); - return Ok(()); + super::retry_on_unmount_errors( + || { + if let Err(err) = crate::mnt::libc_umount(&self.mountpoint) { + // Linux always returns EPERM for non-root users. We have to let the + // library go through the setuid-root "fusermount -u" to unmount. + if err == nix::errno::Errno::EPERM { + unsafe { + fuse_unmount_compat22(self.mountpoint.as_ptr()); + return Ok(()); + } + } + return Err(err.into()); } - } - return Err(err.into()); - } - Ok(()) + Ok(()) + }, + Duration::from_secs(1), + ) } } diff --git a/src/mnt/fuse2_sys.rs b/src/mnt/fuse2_sys.rs index 2cc69dcc..28a15dd3 100644 --- a/src/mnt/fuse2_sys.rs +++ b/src/mnt/fuse2_sys.rs @@ -23,12 +23,6 @@ unsafe extern "C" { // Therefore, the minimum version requirement for *_compat25 functions is libfuse-2.6.0. pub(crate) fn fuse_mount_compat25(mountpoint: *const c_char, args: *const fuse_args) -> c_int; - #[cfg(not(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" - )))] + pub(crate) fn fuse_unmount_compat22(mountpoint: *const c_char); } diff --git a/src/mnt/fuse3.rs b/src/mnt/fuse3.rs index 1cafb199..5dbe4926 100644 --- a/src/mnt/fuse3.rs +++ b/src/mnt/fuse3.rs @@ -31,18 +31,21 @@ fn ensure_last_os_error() -> io::Error { #[derive(Debug)] pub(crate) struct MountImpl { fuse_session: *mut c_void, - mountpoint: CString, + _mountpoint: CString, } + impl MountImpl { pub(crate) fn new( mnt: &Path, options: &[MountOption], acl: SessionACL, ) -> io::Result<(Arc, MountImpl)> { + log::warn!( + "Using libfuse3 as the mount backend may cause memory leaks in some scenarios, for example, if AutoUnmount is set." + ); let mnt = CString::new(mnt.as_os_str().as_bytes()).unwrap(); with_fuse_args(options, acl, |args| { let ops = fuse_lowlevel_ops::default(); - let fuse_session = unsafe { fuse_session_new( args, @@ -56,39 +59,41 @@ impl MountImpl { } let mount = MountImpl { fuse_session, - mountpoint: mnt.clone(), + _mountpoint: mnt.clone(), }; let result = unsafe { fuse_session_mount(mount.fuse_session, mnt.as_ptr()) }; if result != 0 { + unsafe { + fuse_session_destroy(fuse_session); + } return Err(ensure_last_os_error()); } let fd = unsafe { fuse_session_fd(mount.fuse_session) }; if fd < 0 { + unsafe { + fuse_session_unmount(fuse_session); + fuse_session_destroy(fuse_session); + } return Err(io::Error::last_os_error()); } let fd = unsafe { BorrowedFd::borrow_raw(fd) }; // We dup the fd here as the existing fd is owned by the fuse_session, and we // don't want it being closed out from under us: - let fd = fd.try_clone_to_owned()?; + let fd = fd.try_clone_to_owned().inspect_err(|_| unsafe { + fuse_session_unmount(fuse_session); + fuse_session_destroy(fuse_session); + })?; let file = File::from(fd); Ok((Arc::new(DevFuse(file)), mount)) }) } pub(crate) fn umount_impl(&mut self) -> io::Result<()> { - if let Err(err) = crate::mnt::libc_umount(&self.mountpoint) { - // Linux always returns EPERM for non-root users. We have to let the - // library go through the setuid-root "fusermount -u" to unmount. - if err == nix::errno::Errno::EPERM { - #[cfg(target_os = "linux")] - unsafe { - fuse_session_unmount(self.fuse_session); - fuse_session_destroy(self.fuse_session); - return Ok(()); - } - } - return Err(err.into()); - } + // Because fuse_session_new and fuse_session_mount were called, which initializes FFI structures, libfuse expects these 2 functions to be called unconditionally to clean up, or a structure leak will occur (alongside the auto unmount fd leak which is unfixable) + unsafe { + fuse_session_unmount(self.fuse_session); + fuse_session_destroy(self.fuse_session); + }; Ok(()) } } diff --git a/src/mnt/fuse_pure.rs b/src/mnt/fuse_pure.rs index b2a560df..adc4a6fe 100644 --- a/src/mnt/fuse_pure.rs +++ b/src/mnt/fuse_pure.rs @@ -7,6 +7,7 @@ use std::env; use std::ffi::CStr; use std::ffi::CString; use std::ffi::OsStr; +use std::ffi::OsString; use std::fs::File; use std::io; use std::io::Error; @@ -23,12 +24,13 @@ use std::os::unix::io::RawFd; use std::os::unix::net::UnixStream; use std::os::unix::process::CommandExt; use std::path::Path; +use std::process::Child; use std::process::Command; use std::process::Stdio; use std::sync::Arc; +use std::time::Duration; use log::debug; -use log::error; use nix::fcntl::FcntlArg; use nix::fcntl::FdFlag; use nix::fcntl::OFlag; @@ -37,14 +39,12 @@ use nix::sys::socket::ControlMessageOwned; use nix::sys::socket::MsgFlags; use nix::sys::socket::SockaddrStorage; use nix::sys::socket::recvmsg; +use regex::bytes::Regex; use crate::SessionACL; use crate::dev_fuse::DevFuse; use crate::mnt::is_mounted; use crate::mnt::mount_options::MountOption; -use crate::mnt::mount_options::MountOptionGroup; -use crate::mnt::mount_options::option_group; -use crate::mnt::mount_options::option_to_flag; use crate::mnt::mount_options::option_to_string; const FUSERMOUNT_BIN: &str = "fusermount"; @@ -55,9 +55,35 @@ const MOUNT_FUSEFS_BIN: &str = "mount_fusefs"; #[derive(Debug)] pub(crate) struct MountImpl { mountpoint: CString, - auto_unmount_socket: Option, + _auto_unmount: Option, fuse_device: Arc, } + +#[derive(Debug)] +struct AutoUnmount { + process: Child, + _socket: Option, +} + +impl AutoUnmount { + fn new(process: Child, socket: UnixStream) -> Self { + Self { + process, + _socket: Some(socket), + } + } +} + +impl Drop for AutoUnmount { + fn drop(&mut self) { + if let Some(socket) = mem::take(&mut self._socket) { + drop(socket); + } + // Do not allow the process to persist as a zombie + let _ = self.process.wait(); + } +} + impl MountImpl { pub(crate) fn new( mountpoint: &Path, @@ -65,41 +91,41 @@ impl MountImpl { acl: SessionACL, ) -> io::Result<(Arc, MountImpl)> { let mountpoint = mountpoint.canonicalize()?; - let (file, sock) = fuse_mount_pure(mountpoint.as_os_str(), options, acl)?; + let (file, auto_unmount) = fuse_mount_pure(mountpoint.as_os_str(), options, acl)?; let file = Arc::new(file); Ok(( file.clone(), MountImpl { mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?, - auto_unmount_socket: sock, + _auto_unmount: auto_unmount, fuse_device: file, }, )) } pub(crate) fn umount_impl(&mut self) -> io::Result<()> { - if !is_mounted(&self.fuse_device) { - // If the filesystem has already been unmounted, avoid unmounting it again. - // Unmounting it a second time could cause a race with a newly mounted filesystem - // living at the same mountpoint - return Ok(()); - } - if let Some(sock) = mem::take(&mut self.auto_unmount_socket) { - drop(sock); - // fusermount in auto-unmount mode, no more work to do. - return Ok(()); - } - if let Err(err) = crate::mnt::libc_umount(&self.mountpoint) { - if err == nix::errno::Errno::EPERM { - // Linux always returns EPERM for non-root users. We have to let the - // library go through the setuid-root "fusermount -u" to unmount. - fuse_unmount_pure(&self.mountpoint); - return Ok(()); - } else { - return Err(err.into()); - } - } - Ok(()) + super::retry_on_unmount_errors( + || { + if !is_mounted(&self.fuse_device) { + // If the filesystem has already been unmounted, avoid unmounting it again. + // Unmounting it a second time could cause a race with a newly mounted filesystem + // living at the same mountpoint + return Ok(()); + } + if let Err(err) = crate::mnt::libc_umount(&self.mountpoint) { + if err == nix::errno::Errno::EPERM { + // Linux always returns EPERM for non-root users. We have to let the + // library go through the setuid-root "fusermount -u" to unmount. + fuse_unmount_pure(&self.mountpoint)?; + return Ok(()); + } else { + return Err(err.into()); + } + } + Ok(()) + }, + Duration::from_secs(1), + ) } } @@ -107,7 +133,7 @@ fn fuse_mount_pure( mountpoint: &OsStr, options: &[MountOption], acl: SessionACL, -) -> Result<(DevFuse, Option), io::Error> { +) -> Result<(DevFuse, Option), io::Error> { if options.contains(&MountOption::AutoUnmount) { // Auto unmount is only supported via fusermount return fuse_mount_fusermount(mountpoint, options, acl); @@ -129,33 +155,56 @@ fn fuse_mount_pure( fuse_mount_fusermount(mountpoint, options, acl) } -fn fuse_unmount_pure(mountpoint: &CStr) { - #[cfg(target_os = "linux")] - { - if nix::mount::umount2(mountpoint, nix::mount::MntFlags::MNT_DETACH).is_ok() { - return; - } - } - #[cfg(target_os = "macos")] - { - if nix::mount::unmount(mountpoint, nix::mount::MntFlags::MNT_FORCE).is_ok() { - return; - } - } - +/// Lazily unmount via the fusermount binary, which has the setuid bit set to allow running as root. +fn fuse_unmount_pure(mountpoint: &CStr) -> io::Result<()> { let mut builder = Command::new(detect_fusermount_bin()); builder.stdout(Stdio::piped()).stderr(Stdio::piped()); builder .arg("-u") - .arg("-q") + // TODO: Some systems do not support lazy unmounting and may return errors .arg("-z") .arg("--") + .env("LC_MESSAGES", "C") .arg(OsStr::new(&mountpoint.to_string_lossy().into_owned())); - - if let Ok(output) = builder.output() { - debug!("fusermount: {}", String::from_utf8_lossy(&output.stdout)); - debug!("fusermount: {}", String::from_utf8_lossy(&output.stderr)); + let output = builder.output()?; + debug!( + "fusermount stdout on {}: {}", + mountpoint.to_string_lossy(), + String::from_utf8_lossy(&output.stdout) + ); + debug!( + "fusermount stderr on {}: {}", + mountpoint.to_string_lossy(), + String::from_utf8_lossy(&output.stderr) + ); + if output.status.success() { + return Ok(()); } + let fusermount_error = parse_fusermount_unmount_stderr(OsStr::from_bytes(&output.stderr)) + .ok_or_else(|| io::Error::other("failed to parse fusermount umount error message"))?; + // Since `fusermount` does not invoke any locale functions, + // the locale used for `strerror` in the program is guaranteed to be `C`. + let errno = crate::ll::errno_mapping::get_errno_by_message( + &fusermount_error, + &"C".try_into().expect("locale should be valid"), + ) + .map_err(io::Error::other)? + .ok_or_else(|| { + io::Error::new( + ErrorKind::Other, + "errno not found for fusermount umount message", + ) + })?; + Err(io::Error::from_raw_os_error(errno.code())) +} + +fn parse_fusermount_unmount_stderr(output: &OsStr) -> Option { + let parse_regex = Regex::new(r"([^:]+): failed to unmount ([^:]+): (.+)") + .expect("built-in regex should be valid"); + parse_regex.captures(output.as_bytes()).map(|captures| { + let error = captures.get(3).map(|m| m.as_bytes()).unwrap_or_default(); + OsStr::from_bytes(error).to_os_string() + }) } fn detect_fusermount_bin() -> String { @@ -196,7 +245,7 @@ fn receive_fusermount_message(socket: &UnixStream) -> Result { socket.as_raw_fd(), &mut iov, Some(&mut cmsg_buffer), - MsgFlags::empty(), + MsgFlags::MSG_CMSG_CLOEXEC, ) { Ok(msg) => break msg, Err(nix::errno::Errno::EINTR) => continue, @@ -211,18 +260,24 @@ fn receive_fusermount_message(socket: &UnixStream) -> Result { )); } - for cmsg in msg + if let Some(cmsg) = msg .cmsgs() .map_err(|e| Error::new(ErrorKind::InvalidData, e.to_string()))? + .next() { + let mut owned_fds = Vec::new(); match cmsg { ControlMessageOwned::ScmRights(fds) => { - if let Some(&fd) = fds.first() { + for fd in fds { if fd < 0 { return Err(ErrorKind::InvalidData.into()); } - return Ok(DevFuse(unsafe { File::from_raw_fd(fd) })); + owned_fds.push(unsafe { File::from_raw_fd(fd) }); + } + if owned_fds.is_empty() { + return Err(ErrorKind::InvalidData.into()); } + return Ok(DevFuse(owned_fds.remove(0))); } other => { return Err(Error::new( @@ -261,7 +316,7 @@ fn fuse_mount_fusermount( mountpoint: &OsStr, options: &[MountOption], acl: SessionACL, -) -> Result<(DevFuse, Option), Error> { +) -> Result<(DevFuse, Option), Error> { let fusermount_bin = detect_fusermount_bin(); if fusermount_bin.ends_with(MOUNT_FUSEFS_BIN) { @@ -287,7 +342,7 @@ fn fuse_mount_fusermount( clear_cloexec_in_pre_exec(&mut builder, child_socket.as_fd()); } - let fusermount_child = builder.spawn()?; + let mut fusermount_child = builder.spawn()?; drop(child_socket); // close socket in parent @@ -305,17 +360,18 @@ fn fuse_mount_fusermount( }; } }; - let mut receive_socket = Some(receive_socket); + + let mut auto_unmount = None; if !options.contains(&MountOption::AutoUnmount) { // Only close the socket, if auto unmount is not set. // fusermount will keep running until the socket is closed, if auto unmount is set - drop(mem::take(&mut receive_socket)); + drop(receive_socket); let output = fusermount_child.wait_with_output()?; debug!("fusermount: {}", String::from_utf8_lossy(&output.stdout)); debug!("fusermount: {}", String::from_utf8_lossy(&output.stderr)); } else { - if let Some(mut stdout) = fusermount_child.stdout { + if let Some(stdout) = &mut fusermount_child.stdout { // TODO: do not ignore error. if let Ok(flags) = fcntl(&stdout, FcntlArg::F_GETFL) { let new_flags = OFlag::from_bits_retain(flags) | OFlag::O_NONBLOCK; @@ -326,7 +382,7 @@ fn fuse_mount_fusermount( debug!("fusermount: {}", String::from_utf8_lossy(&buf[..len])); } } - if let Some(mut stderr) = fusermount_child.stderr { + if let Some(stderr) = &mut fusermount_child.stderr { // TODO: do not ignore error. if let Ok(flags) = fcntl(&stderr, FcntlArg::F_GETFL) { let new_flags = OFlag::from_bits_retain(flags) | OFlag::O_NONBLOCK; @@ -337,12 +393,12 @@ fn fuse_mount_fusermount( debug!("fusermount: {}", String::from_utf8_lossy(&buf[..len])); } } + auto_unmount = Some(AutoUnmount::new(fusermount_child, receive_socket)); } // TODO: do not ignore error. let _ = fcntl(&file, FcntlArg::F_SETFD(FdFlag::FD_CLOEXEC)); - - Ok((file, receive_socket)) + Ok((file, auto_unmount)) } fn fuse_mount_mount_fusefs( @@ -350,7 +406,7 @@ fn fuse_mount_mount_fusefs( mountpoint: &OsStr, options: &[MountOption], acl: SessionACL, -) -> Result<(DevFuse, Option), Error> { +) -> Result<(DevFuse, Option), Error> { let fuse_device = DevFuse::open()?; let fuse_fd = fuse_device.as_raw_fd(); @@ -386,6 +442,9 @@ fn fuse_mount_sys( options: &[MountOption], acl: SessionACL, ) -> Result, Error> { + use crate::mnt::mount_options::MountOptionGroup; + use crate::mnt::mount_options::option_group; + use crate::mnt::mount_options::option_to_flag; use std::os::unix::fs::PermissionsExt; let mountpoint_mode = File::open(mountpoint)?.metadata()?.permissions().mode(); @@ -397,7 +456,7 @@ fn fuse_mount_sys( Ok(dev_fuse) => dev_fuse, Err(error) => { if error.kind() == ErrorKind::NotFound { - error!("{} not found. Try 'modprobe fuse'", DevFuse::PATH); + log::error!("{} not found. Try 'modprobe fuse'", DevFuse::PATH); } return Err(error); } diff --git a/src/mnt/mod.rs b/src/mnt/mod.rs index e68ef256..8f63ada1 100644 --- a/src/mnt/mod.rs +++ b/src/mnt/mod.rs @@ -16,6 +16,7 @@ mod fuse_pure; pub(crate) mod mount_options; use std::io; +use std::time::Duration; #[cfg(any(test, fuser_mount_impl = "libfuse2", fuser_mount_impl = "libfuse3"))] use fuse2_sys::fuse_args; @@ -165,8 +166,16 @@ impl Drop for Mount { } } -#[cfg_attr(fuser_mount_impl = "macos-no-mount", expect(dead_code))] +#[cfg_attr( + any(fuser_mount_impl = "macos-no-mount", fuser_mount_impl = "libfuse3"), + expect(dead_code) +)] fn libc_umount(mnt: &CStr) -> nix::Result<()> { + let flags = unmount_flags(); + #[cfg(target_os = "linux")] + { + nix::mount::umount2(mnt, flags) + } #[cfg(any( target_os = "macos", target_os = "freebsd", @@ -175,10 +184,10 @@ fn libc_umount(mnt: &CStr) -> nix::Result<()> { target_os = "netbsd" ))] { - nix::mount::unmount(mnt, nix::mount::MntFlags::empty()) + nix::mount::unmount(mnt, flags) } - #[cfg(not(any( + target_os = "linux", target_os = "macos", target_os = "freebsd", target_os = "dragonfly", @@ -190,6 +199,37 @@ fn libc_umount(mnt: &CStr) -> nix::Result<()> { } } +fn unmount_flags() -> nix::mount::MntFlags { + #[cfg(target_os = "linux")] + { + // FIXME: Only supported on Linux 2.4.11+, procfs may be needed to determine version + nix::mount::MntFlags::MNT_DETACH + } + #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd" + ))] + { + // fuse_unmount_pure uses MNT_FORCE to terminate requests and unmount the filesystem on MacOS, the unmount API of BSD systems resemble MacOS as well. + // This does not completely prevent failing with EBUSY though. + nix::mount::MntFlags::MNT_FORCE + } + #[cfg(not(any( + target_os = "linux", + target_os = "macos", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd" + )))] + { + nix::mount::MntFlags::empty() + } +} + /// Warning: This will return true if the filesystem has been detached (lazy unmounted), but not /// yet destroyed by the kernel. #[cfg(any(all(not(target_os = "macos"), test), fuser_mount_impl = "pure-rust"))] @@ -222,6 +262,36 @@ fn is_mounted(fuse_device: &DevFuse) -> bool { } } +/// Retry a callback on unmount errors. +/// +/// This function will retry the callback on the following errors: +/// - EINTR: Interrupted system call, retry immediately. +/// - EBUSY: Resource busy due to other processes using the filesystem, wait for the interval and retry. +/// - Other errors, return immediately. +#[cfg_attr( + any(fuser_mount_impl = "libfuse3", fuser_mount_impl = "macos-no-mount"), + expect(dead_code) +)] +fn retry_on_unmount_errors(mut callback: F, interval: Duration) -> io::Result +where + F: FnMut() -> io::Result, +{ + loop { + match callback() { + Ok(result) => return Ok(result), + // Interrupted system call, retry immediately. + Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, + // On EBUSY, wait for the interval and retry. + Err(err) if err.kind() == io::ErrorKind::ResourceBusy => { + std::thread::sleep(interval); + continue; + } + // Other errors, return immediately. + Err(err) => return Err(err), + } + } +} + #[cfg(test)] mod test { use std::ffi::CStr; diff --git a/tests/fixtures/mod.rs b/tests/fixtures/mod.rs new file mode 100644 index 00000000..96223d54 --- /dev/null +++ b/tests/fixtures/mod.rs @@ -0,0 +1,2 @@ +#![allow(unused)] +pub mod simple; diff --git a/tests/fixtures/simple.rs b/tests/fixtures/simple.rs new file mode 100644 index 00000000..778962ae --- /dev/null +++ b/tests/fixtures/simple.rs @@ -0,0 +1,2112 @@ +//! A full-fledged fixture filesystem for testing niche interactions (e.g. auto_unmount) +#![allow(clippy::needless_return)] +#![allow(clippy::unnecessary_cast)] // libc::S_* are u16 or u32 depending on the platform +#![allow(unused)] + +use std::cmp::min; +use std::collections::BTreeMap; +use std::ffi::OsStr; +use std::fs; +use std::fs::File; +use std::fs::OpenOptions; +use std::io; +use std::io::BufRead; +use std::io::BufReader; +use std::io::Read; +use std::io::Seek; +use std::io::SeekFrom; +use std::io::Write; +use std::os::unix::ffi::OsStrExt; +use std::os::unix::fs::FileExt; +#[cfg(target_os = "linux")] +use std::os::unix::io::IntoRawFd; +use std::path::Path; +use std::path::PathBuf; +use std::sync::atomic::AtomicU64; +use std::sync::atomic::Ordering; +use std::time::Duration; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; + +use fuser::AccessFlags; +use fuser::BsdFileFlags; +use fuser::Errno; +use fuser::FileHandle; +use fuser::Filesystem; +use fuser::FopenFlags; +use fuser::INodeNo; +use fuser::InitFlags; +use fuser::KernelConfig; +use fuser::LockOwner; +use fuser::OpenAccMode; +use fuser::OpenFlags; +use fuser::RenameFlags; +use fuser::ReplyAttr; +use fuser::ReplyCreate; +use fuser::ReplyData; +use fuser::ReplyDirectory; +use fuser::ReplyEmpty; +use fuser::ReplyEntry; +use fuser::ReplyOpen; +use fuser::ReplyStatfs; +use fuser::ReplyWrite; +use fuser::ReplyXattr; +use fuser::Request; +use fuser::TimeOrNow; +use fuser::TimeOrNow::Now; +use fuser::WriteFlags; +use log::debug; +use log::info; +use log::warn; +use serde::Deserialize; +use serde::Serialize; + +const BLOCK_SIZE: u32 = 512; +const MAX_NAME_LENGTH: u32 = 255; +const MAX_FILE_SIZE: u64 = 1024 * 1024 * 1024 * 1024; + +// Top two file handle bits are used to store permissions +// Note: This isn't safe, since the client can modify those bits. However, this implementation +// is just a toy +const FILE_HANDLE_READ_BIT: u64 = 1 << 63; +const FILE_HANDLE_WRITE_BIT: u64 = 1 << 62; + +const FMODE_EXEC: i32 = 0x20; + +type DirectoryDescriptor = BTreeMap, (u64, FileKind)>; + +#[derive(Serialize, Deserialize, Copy, Clone, PartialEq)] +enum FileKind { + File, + Directory, + Symlink, +} + +impl From for fuser::FileType { + fn from(kind: FileKind) -> Self { + match kind { + FileKind::File => fuser::FileType::RegularFile, + FileKind::Directory => fuser::FileType::Directory, + FileKind::Symlink => fuser::FileType::Symlink, + } + } +} + +#[derive(Debug)] +enum XattrNamespace { + Security, + System, + Trusted, + User, +} + +fn parse_xattr_namespace(key: &[u8]) -> Result { + let user = b"user."; + if key.len() < user.len() { + return Err(Errno::ENOTSUP); + } + if key[..user.len()].eq(user) { + return Ok(XattrNamespace::User); + } + + let system = b"system."; + if key.len() < system.len() { + return Err(Errno::ENOTSUP); + } + if key[..system.len()].eq(system) { + return Ok(XattrNamespace::System); + } + + let trusted = b"trusted."; + if key.len() < trusted.len() { + return Err(Errno::ENOTSUP); + } + if key[..trusted.len()].eq(trusted) { + return Ok(XattrNamespace::Trusted); + } + + let security = b"security"; + if key.len() < security.len() { + return Err(Errno::ENOTSUP); + } + if key[..security.len()].eq(security) { + return Ok(XattrNamespace::Security); + } + + return Err(Errno::ENOTSUP); +} + +fn clear_suid_sgid(attr: &mut InodeAttributes) { + attr.mode &= !libc::S_ISUID as u16; + // SGID is only suppose to be cleared if XGRP is set + if attr.mode & libc::S_IXGRP as u16 != 0 { + attr.mode &= !libc::S_ISGID as u16; + } +} + +fn creation_gid(parent: &InodeAttributes, gid: u32) -> u32 { + if parent.mode & libc::S_ISGID as u16 != 0 { + return parent.gid; + } + + gid +} + +fn xattr_access_check( + key: &[u8], + access_mask: i32, + inode_attrs: &InodeAttributes, + request: &Request, +) -> Result<(), Errno> { + match parse_xattr_namespace(key)? { + XattrNamespace::Security => { + if access_mask != libc::R_OK && request.uid() != 0 { + return Err(Errno::EPERM); + } + } + XattrNamespace::Trusted => { + if request.uid() != 0 { + return Err(Errno::EPERM); + } + } + XattrNamespace::System => { + if key.eq(b"system.posix_acl_access") { + if !check_access( + inode_attrs.uid, + inode_attrs.gid, + inode_attrs.mode, + request.uid(), + request.gid(), + AccessFlags::from_bits_retain(access_mask), + ) { + return Err(Errno::EPERM); + } + } else if request.uid() != 0 { + return Err(Errno::EPERM); + } + } + XattrNamespace::User => { + if !check_access( + inode_attrs.uid, + inode_attrs.gid, + inode_attrs.mode, + request.uid(), + request.gid(), + AccessFlags::from_bits_retain(access_mask), + ) { + return Err(Errno::EPERM); + } + } + } + + Ok(()) +} + +fn time_now() -> (i64, u32) { + time_from_system_time(&SystemTime::now()) +} + +fn system_time_from_time(secs: i64, nsecs: u32) -> SystemTime { + if secs >= 0 { + UNIX_EPOCH + Duration::new(secs as u64, nsecs) + } else { + UNIX_EPOCH - Duration::new((-secs) as u64, nsecs) + } +} + +fn time_from_system_time(system_time: &SystemTime) -> (i64, u32) { + // Convert to signed 64-bit time with epoch at 0 + match system_time.duration_since(UNIX_EPOCH) { + Ok(duration) => (duration.as_secs() as i64, duration.subsec_nanos()), + Err(before_epoch_error) => ( + -(before_epoch_error.duration().as_secs() as i64), + before_epoch_error.duration().subsec_nanos(), + ), + } +} + +#[derive(Serialize, Deserialize)] +struct InodeAttributes { + pub inode: u64, + pub open_file_handles: u64, // Ref count of open file handles to this inode + pub size: u64, + pub last_accessed: (i64, u32), + pub last_modified: (i64, u32), + pub last_metadata_changed: (i64, u32), + pub kind: FileKind, + // Permissions and special mode bits + pub mode: u16, + pub hardlinks: u32, + pub uid: u32, + pub gid: u32, + pub xattrs: BTreeMap, Vec>, +} + +impl From for fuser::FileAttr { + fn from(attrs: InodeAttributes) -> Self { + fuser::FileAttr { + ino: INodeNo(attrs.inode), + size: attrs.size, + blocks: attrs.size.div_ceil(u64::from(BLOCK_SIZE)), + atime: system_time_from_time(attrs.last_accessed.0, attrs.last_accessed.1), + mtime: system_time_from_time(attrs.last_modified.0, attrs.last_modified.1), + ctime: system_time_from_time( + attrs.last_metadata_changed.0, + attrs.last_metadata_changed.1, + ), + crtime: SystemTime::UNIX_EPOCH, + kind: attrs.kind.into(), + perm: attrs.mode, + nlink: attrs.hardlinks, + uid: attrs.uid, + gid: attrs.gid, + rdev: 0, + blksize: BLOCK_SIZE, + flags: 0, + } + } +} + +// Stores inode metadata data in "$data_dir/inodes" and file contents in "$data_dir/contents" +// Directory data is stored in the file's contents, as a serialized DirectoryDescriptor +pub struct SimpleFS { + data_dir: String, + next_file_handle: AtomicU64, + direct_io: bool, + suid_support: bool, +} + +impl SimpleFS { + pub fn new(data_dir: String, direct_io: bool, suid_support: bool) -> SimpleFS { + SimpleFS { + data_dir, + next_file_handle: AtomicU64::new(1), + direct_io, + suid_support, + } + } + + fn creation_mode(&self, mode: u32) -> u16 { + if self.suid_support { + mode as u16 + } else { + (mode & !(libc::S_ISUID | libc::S_ISGID) as u32) as u16 + } + } + + fn allocate_next_inode(&self) -> INodeNo { + let path = Path::new(&self.data_dir).join("superblock"); + let current_inode = match File::open(&path) { + Ok(file) => INodeNo(bincode::deserialize_from(file).unwrap()), + _ => INodeNo::ROOT, + }; + + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&path) + .unwrap(); + bincode::serialize_into(file, &(current_inode.0 + 1)).unwrap(); + + INodeNo(current_inode.0 + 1) + } + + fn allocate_next_file_handle(&self, read: bool, write: bool) -> u64 { + let mut fh = self.next_file_handle.fetch_add(1, Ordering::SeqCst); + // Assert that we haven't run out of file handles + assert!(fh < FILE_HANDLE_READ_BIT.min(FILE_HANDLE_WRITE_BIT)); + if read { + fh |= FILE_HANDLE_READ_BIT; + } + if write { + fh |= FILE_HANDLE_WRITE_BIT; + } + + fh + } + + fn check_file_handle_read(file_handle: u64) -> bool { + (file_handle & FILE_HANDLE_READ_BIT) != 0 + } + + fn check_file_handle_write(file_handle: u64) -> bool { + (file_handle & FILE_HANDLE_WRITE_BIT) != 0 + } + + fn content_path(&self, inode: INodeNo) -> PathBuf { + Path::new(&self.data_dir) + .join("contents") + .join(inode.to_string()) + } + + fn get_directory_content(&self, inode: INodeNo) -> Result { + let path = Path::new(&self.data_dir) + .join("contents") + .join(inode.to_string()); + match File::open(path) { + Ok(file) => Ok(bincode::deserialize_from(file).unwrap()), + _ => Err(Errno::ENOENT), + } + } + + fn write_directory_content(&self, inode: INodeNo, entries: &DirectoryDescriptor) { + let path = Path::new(&self.data_dir) + .join("contents") + .join(inode.to_string()); + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .unwrap(); + bincode::serialize_into(file, &entries).unwrap(); + } + + fn get_inode(&self, inode: INodeNo) -> Result { + let path = Path::new(&self.data_dir) + .join("inodes") + .join(inode.to_string()); + match File::open(path) { + Ok(file) => Ok(bincode::deserialize_from(file).unwrap()), + _ => Err(Errno::ENOENT), + } + } + + fn write_inode(&self, inode: &InodeAttributes) { + let path = Path::new(&self.data_dir) + .join("inodes") + .join(inode.inode.to_string()); + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .unwrap(); + bincode::serialize_into(file, inode).unwrap(); + } + + // Check whether a file should be removed from storage. Should be called after decrementing + // the link count, or closing a file handle + fn gc_inode(&self, inode: &InodeAttributes) -> bool { + if inode.hardlinks == 0 && inode.open_file_handles == 0 { + let inode_path = Path::new(&self.data_dir) + .join("inodes") + .join(inode.inode.to_string()); + fs::remove_file(inode_path).unwrap(); + let content_path = Path::new(&self.data_dir) + .join("contents") + .join(inode.inode.to_string()); + fs::remove_file(content_path).unwrap(); + + return true; + } + + return false; + } + + fn truncate( + &self, + inode: INodeNo, + new_length: u64, + uid: u32, + gid: u32, + ) -> Result { + if new_length > MAX_FILE_SIZE { + return Err(Errno::EFBIG); + } + + let mut attrs = self.get_inode(inode)?; + + if !check_access( + attrs.uid, + attrs.gid, + attrs.mode, + uid, + gid, + AccessFlags::W_OK, + ) { + return Err(Errno::EACCES); + } + + let path = self.content_path(inode); + let file = OpenOptions::new().write(true).open(path).unwrap(); + file.set_len(new_length).unwrap(); + + attrs.size = new_length; + attrs.last_metadata_changed = time_now(); + attrs.last_modified = time_now(); + + // Clear SETUID & SETGID on truncate + clear_suid_sgid(&mut attrs); + + self.write_inode(&attrs); + + Ok(attrs) + } + + fn lookup_name(&self, parent: INodeNo, name: &OsStr) -> Result { + let entries = self.get_directory_content(parent)?; + if let Some((inode, _)) = entries.get(name.as_bytes()) { + return self.get_inode(INodeNo(*inode)); + } + return Err(Errno::ENOENT); + } + + fn insert_link( + &self, + req: &Request, + parent: INodeNo, + name: &OsStr, + inode: INodeNo, + kind: FileKind, + ) -> Result<(), Errno> { + if self.lookup_name(parent, name).is_ok() { + return Err(Errno::EEXIST); + } + + let mut parent_attrs = self.get_inode(parent)?; + + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + req.uid(), + req.gid(), + AccessFlags::W_OK, + ) { + return Err(Errno::EACCES); + } + parent_attrs.last_modified = time_now(); + parent_attrs.last_metadata_changed = time_now(); + self.write_inode(&parent_attrs); + + let mut entries = self.get_directory_content(parent).unwrap(); + entries.insert(name.as_bytes().to_vec(), (inode.0, kind)); + self.write_directory_content(parent, &entries); + + Ok(()) + } +} + +impl Filesystem for SimpleFS { + fn init( + &mut self, + _req: &Request, + #[allow(unused_variables)] config: &mut KernelConfig, + ) -> io::Result<()> { + if config + .add_capabilities(InitFlags::FUSE_HANDLE_KILLPRIV) + .is_err() + { + info!("FUSE_HANDLE_KILLPRIV not supported"); + self.suid_support = false; + } + + fs::create_dir_all(Path::new(&self.data_dir).join("inodes")).unwrap(); + fs::create_dir_all(Path::new(&self.data_dir).join("contents")).unwrap(); + if self.get_inode(INodeNo::ROOT).is_err() { + // Initialize with empty filesystem + let root = InodeAttributes { + inode: INodeNo::ROOT.0, + open_file_handles: 0, + size: 0, + last_accessed: time_now(), + last_modified: time_now(), + last_metadata_changed: time_now(), + kind: FileKind::Directory, + mode: 0o777, + hardlinks: 2, + uid: 0, + gid: 0, + xattrs: BTreeMap::default(), + }; + self.write_inode(&root); + let mut entries = BTreeMap::new(); + entries.insert(b".".to_vec(), (INodeNo::ROOT.0, FileKind::Directory)); + self.write_directory_content(INodeNo::ROOT, &entries); + } + Ok(()) + } + + fn lookup(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEntry) { + if name.len() > MAX_NAME_LENGTH as usize { + reply.error(Errno::ENAMETOOLONG); + return; + } + let parent_attrs = self.get_inode(parent).unwrap(); + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::X_OK, + ) { + reply.error(Errno::EACCES); + return; + } + + match self.lookup_name(parent, name) { + Ok(attrs) => reply.entry(&Duration::new(0, 0), &attrs.into(), fuser::Generation(0)), + Err(error_code) => reply.error(error_code), + } + } + + fn forget(&self, _req: &Request, _ino: INodeNo, _nlookup: u64) {} + + fn getattr(&self, _req: &Request, ino: INodeNo, _fh: Option, reply: ReplyAttr) { + match self.get_inode(ino) { + Ok(attrs) => reply.attr(&Duration::new(0, 0), &attrs.into()), + Err(error_code) => reply.error(error_code), + } + } + + fn setattr( + &self, + _req: &Request, + ino: INodeNo, + mode: Option, + uid: Option, + gid: Option, + size: Option, + _atime: Option, + _mtime: Option, + _ctime: Option, + fh: Option, + _crtime: Option, + _chgtime: Option, + _bkuptime: Option, + _flags: Option, + reply: ReplyAttr, + ) { + let mut attrs = match self.get_inode(ino) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + if let Some(mode) = mode { + debug!("chmod() called with {ino:?}, {mode:o}"); + #[cfg(target_os = "freebsd")] + { + // FreeBSD: sticky bit only valid on directories; otherwise EFTYPE + if _req.uid() != 0 + && (mode as u16 & libc::S_ISVTX as u16) != 0 + && attrs.kind != FileKind::Directory + { + reply.error(Errno::EFTYPE); + return; + } + } + if _req.uid() != 0 && _req.uid() != attrs.uid { + reply.error(Errno::EPERM); + return; + } + if _req.uid() != 0 + && _req.gid() != attrs.gid + && !get_groups(_req.pid()).contains(&attrs.gid) + { + // If SGID is set and the file belongs to a group that the caller is not part of + // then the SGID bit is suppose to be cleared during chmod + attrs.mode = (mode & !libc::S_ISGID as u32) as u16; + } else { + attrs.mode = mode as u16; + } + attrs.last_metadata_changed = time_now(); + self.write_inode(&attrs); + reply.attr(&Duration::new(0, 0), &attrs.into()); + return; + } + + if uid.is_some() || gid.is_some() { + debug!("chown() called with {ino:?} {uid:?} {gid:?}"); + if let Some(gid) = gid { + // Non-root users can only change gid to a group they're in + if _req.uid() != 0 && !get_groups(_req.pid()).contains(&gid) { + reply.error(Errno::EPERM); + return; + } + } + if let Some(uid) = uid { + if _req.uid() != 0 + // but no-op changes by the owner are not an error + && !(uid == attrs.uid && _req.uid() == attrs.uid) + { + reply.error(Errno::EPERM); + return; + } + } + // Only owner may change the group + if gid.is_some() && _req.uid() != 0 && _req.uid() != attrs.uid { + reply.error(Errno::EPERM); + return; + } + + if attrs.mode & (libc::S_IXUSR | libc::S_IXGRP | libc::S_IXOTH) as u16 != 0 { + // SUID & SGID are suppose to be cleared when chown'ing an executable file + clear_suid_sgid(&mut attrs); + } + + if let Some(uid) = uid { + attrs.uid = uid; + // Clear SETUID on owner change + attrs.mode &= !libc::S_ISUID as u16; + } + if let Some(gid) = gid { + attrs.gid = gid; + // Clear SETGID unless user is root + if _req.uid() != 0 { + attrs.mode &= !libc::S_ISGID as u16; + } + } + attrs.last_metadata_changed = time_now(); + self.write_inode(&attrs); + reply.attr(&Duration::new(0, 0), &attrs.into()); + return; + } + + if let Some(size) = size { + debug!("truncate() called with {ino:?} {size:?}"); + if let Some(handle) = fh { + // If the file handle is available, check access locally. + // This is important as it preserves the semantic that a file handle opened + // with W_OK will never fail to truncate, even if the file has been subsequently + // chmod'ed + if Self::check_file_handle_write(handle.into()) { + if let Err(error_code) = self.truncate(ino, size, 0, 0) { + reply.error(error_code); + return; + } + } else { + reply.error(Errno::EACCES); + return; + } + } else if let Err(error_code) = self.truncate(ino, size, _req.uid(), _req.gid()) { + reply.error(error_code); + return; + } + } + + let now = time_now(); + if let Some(atime) = _atime { + debug!("utimens() called with {ino:?}, atime={atime:?}"); + + if attrs.uid != _req.uid() && _req.uid() != 0 && atime != Now { + reply.error(Errno::EPERM); + return; + } + + if attrs.uid != _req.uid() + && !check_access( + attrs.uid, + attrs.gid, + attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) + { + reply.error(Errno::EACCES); + return; + } + + attrs.last_accessed = match atime { + TimeOrNow::SpecificTime(time) => time_from_system_time(&time), + Now => now, + }; + attrs.last_metadata_changed = now; + self.write_inode(&attrs); + } + if let Some(mtime) = _mtime { + debug!("utimens() called with {ino:?}, mtime={mtime:?}"); + + if attrs.uid != _req.uid() && _req.uid() != 0 && mtime != Now { + reply.error(Errno::EPERM); + return; + } + + if attrs.uid != _req.uid() + && !check_access( + attrs.uid, + attrs.gid, + attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) + { + reply.error(Errno::EACCES); + return; + } + + attrs.last_modified = match mtime { + TimeOrNow::SpecificTime(time) => time_from_system_time(&time), + Now => now, + }; + attrs.last_metadata_changed = now; + self.write_inode(&attrs); + } + + let attrs = self.get_inode(ino).unwrap(); + reply.attr(&Duration::new(0, 0), &attrs.into()); + return; + } + + fn readlink(&self, _req: &Request, ino: INodeNo, reply: ReplyData) { + debug!("readlink() called on {ino:?}"); + let path = self.content_path(ino); + match File::open(path) { + Ok(mut file) => { + let file_size = file.metadata().unwrap().len(); + let mut buffer = vec![0; file_size as usize]; + file.read_exact(&mut buffer).unwrap(); + reply.data(&buffer); + } + _ => { + reply.error(Errno::ENOENT); + } + } + } + + fn mknod( + &self, + _req: &Request, + parent: INodeNo, + name: &OsStr, + mut mode: u32, + _umask: u32, + _rdev: u32, + reply: ReplyEntry, + ) { + let file_type = mode & libc::S_IFMT as u32; + + if file_type != libc::S_IFREG as u32 + && file_type != libc::S_IFLNK as u32 + && file_type != libc::S_IFDIR as u32 + { + // TODO + warn!( + "mknod() implementation is incomplete. Only supports regular files, symlinks, and directories. Got {mode:o}" + ); + reply.error(Errno::EPERM); + return; + } + + if self.lookup_name(parent, name).is_ok() { + reply.error(Errno::EEXIST); + return; + } + + let mut parent_attrs = match self.get_inode(parent) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) { + reply.error(Errno::EACCES); + return; + } + parent_attrs.last_modified = time_now(); + parent_attrs.last_metadata_changed = time_now(); + self.write_inode(&parent_attrs); + + if _req.uid() != 0 { + mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; + } + + #[cfg(target_os = "freebsd")] + { + let kind = as_file_kind(mode); + // FreeBSD: sticky bit only valid on directories; otherwise EFTYPE + if _req.uid() != 0 + && (mode as u16 & libc::S_ISVTX as u16) != 0 + && kind != FileKind::Directory + { + reply.error(Errno::EFTYPE); + return; + } + } + + let inode = self.allocate_next_inode(); + let attrs = InodeAttributes { + inode: inode.0, + open_file_handles: 0, + size: 0, + last_accessed: time_now(), + last_modified: time_now(), + last_metadata_changed: time_now(), + kind: as_file_kind(mode), + mode: self.creation_mode(mode), + hardlinks: 1, + uid: _req.uid(), + gid: creation_gid(&parent_attrs, _req.gid()), + xattrs: BTreeMap::default(), + }; + self.write_inode(&attrs); + File::create(self.content_path(inode)).unwrap(); + + if as_file_kind(mode) == FileKind::Directory { + let mut entries = BTreeMap::new(); + entries.insert(b".".to_vec(), (inode.0, FileKind::Directory)); + entries.insert(b"..".to_vec(), (parent.0, FileKind::Directory)); + self.write_directory_content(inode, &entries); + } + + let mut entries = self.get_directory_content(parent).unwrap(); + entries.insert(name.as_bytes().to_vec(), (inode.0, attrs.kind)); + self.write_directory_content(parent, &entries); + + // TODO: implement flags + reply.entry(&Duration::new(0, 0), &attrs.into(), fuser::Generation(0)); + } + + fn mkdir( + &self, + _req: &Request, + parent: INodeNo, + name: &OsStr, + mut mode: u32, + _umask: u32, + reply: ReplyEntry, + ) { + debug!("mkdir() called with {parent:?} {name:?} {mode:o}"); + if self.lookup_name(parent, name).is_ok() { + reply.error(Errno::EEXIST); + return; + } + + let mut parent_attrs = match self.get_inode(parent) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) { + reply.error(Errno::EACCES); + return; + } + parent_attrs.last_modified = time_now(); + parent_attrs.last_metadata_changed = time_now(); + self.write_inode(&parent_attrs); + + if _req.uid() != 0 { + mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; + } + if parent_attrs.mode & libc::S_ISGID as u16 != 0 { + mode |= libc::S_ISGID as u32; + } + + let inode = self.allocate_next_inode(); + let attrs = InodeAttributes { + inode: inode.0, + open_file_handles: 0, + size: u64::from(BLOCK_SIZE), + last_accessed: time_now(), + last_modified: time_now(), + last_metadata_changed: time_now(), + kind: FileKind::Directory, + mode: self.creation_mode(mode), + hardlinks: 2, // Directories start with link count of 2, since they have a self link + uid: _req.uid(), + gid: creation_gid(&parent_attrs, _req.gid()), + xattrs: BTreeMap::default(), + }; + self.write_inode(&attrs); + + let mut entries = BTreeMap::new(); + entries.insert(b".".to_vec(), (inode.0, FileKind::Directory)); + entries.insert(b"..".to_vec(), (parent.0, FileKind::Directory)); + self.write_directory_content(inode, &entries); + + let mut entries = self.get_directory_content(parent).unwrap(); + entries.insert(name.as_bytes().to_vec(), (inode.0, FileKind::Directory)); + self.write_directory_content(parent, &entries); + + reply.entry(&Duration::new(0, 0), &attrs.into(), fuser::Generation(0)); + } + + fn unlink(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) { + debug!("unlink() called with {parent:?} {name:?}"); + let mut attrs = match self.lookup_name(parent, name) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + let mut parent_attrs = match self.get_inode(parent) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) { + reply.error(Errno::EACCES); + return; + } + + let uid = _req.uid(); + // "Sticky bit" handling + if parent_attrs.mode & libc::S_ISVTX as u16 != 0 + && uid != 0 + && uid != parent_attrs.uid + && uid != attrs.uid + { + reply.error(Errno::EACCES); + return; + } + + parent_attrs.last_metadata_changed = time_now(); + parent_attrs.last_modified = time_now(); + self.write_inode(&parent_attrs); + + attrs.hardlinks -= 1; + attrs.last_metadata_changed = time_now(); + self.write_inode(&attrs); + self.gc_inode(&attrs); + + let mut entries = self.get_directory_content(parent).unwrap(); + entries.remove(name.as_bytes()); + self.write_directory_content(parent, &entries); + + reply.ok(); + } + + fn rmdir(&self, _req: &Request, parent: INodeNo, name: &OsStr, reply: ReplyEmpty) { + debug!("rmdir() called with {parent:?} {name:?}"); + let mut attrs = match self.lookup_name(parent, name) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + let mut parent_attrs = match self.get_inode(parent) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + // Directories always have a self and parent link + if self + .get_directory_content(INodeNo(attrs.inode)) + .unwrap() + .len() + > 2 + { + reply.error(Errno::ENOTEMPTY); + return; + } + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) { + reply.error(Errno::EACCES); + return; + } + + // "Sticky bit" handling + if parent_attrs.mode & libc::S_ISVTX as u16 != 0 + && _req.uid() != 0 + && _req.uid() != parent_attrs.uid + && _req.uid() != attrs.uid + { + reply.error(Errno::EACCES); + return; + } + + parent_attrs.last_metadata_changed = time_now(); + parent_attrs.last_modified = time_now(); + self.write_inode(&parent_attrs); + + attrs.hardlinks = 0; + attrs.last_metadata_changed = time_now(); + self.write_inode(&attrs); + self.gc_inode(&attrs); + + let mut entries = self.get_directory_content(parent).unwrap(); + entries.remove(name.as_bytes()); + self.write_directory_content(parent, &entries); + + reply.ok(); + } + + fn symlink( + &self, + _req: &Request, + parent: INodeNo, + link_name: &OsStr, + target: &Path, + reply: ReplyEntry, + ) { + debug!("symlink() called with {parent:?} {link_name:?} {target:?}"); + let mut parent_attrs = match self.get_inode(parent) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) { + reply.error(Errno::EACCES); + return; + } + parent_attrs.last_modified = time_now(); + parent_attrs.last_metadata_changed = time_now(); + self.write_inode(&parent_attrs); + + let inode = self.allocate_next_inode(); + let attrs = InodeAttributes { + inode: inode.0, + open_file_handles: 0, + size: target.as_os_str().as_bytes().len() as u64, + last_accessed: time_now(), + last_modified: time_now(), + last_metadata_changed: time_now(), + kind: FileKind::Symlink, + mode: 0o777, + hardlinks: 1, + uid: _req.uid(), + gid: creation_gid(&parent_attrs, _req.gid()), + xattrs: BTreeMap::default(), + }; + + if let Err(error_code) = self.insert_link(_req, parent, link_name, inode, FileKind::Symlink) + { + reply.error(error_code); + return; + } + self.write_inode(&attrs); + + let path = self.content_path(inode); + let mut file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .unwrap(); + file.write_all(target.as_os_str().as_bytes()).unwrap(); + + reply.entry(&Duration::new(0, 0), &attrs.into(), fuser::Generation(0)); + } + + fn rename( + &self, + _req: &Request, + parent: INodeNo, + name: &OsStr, + newparent: INodeNo, + newname: &OsStr, + flags: RenameFlags, + reply: ReplyEmpty, + ) { + debug!( + "rename() called with: source {parent:?} {name:?}, \ + destination {newparent:?} {newname:?}, flags {flags:#b}", + ); + let mut inode_attrs = match self.lookup_name(parent, name) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + let mut parent_attrs = match self.get_inode(parent) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) { + reply.error(Errno::EACCES); + return; + } + + // "Sticky bit" handling + if parent_attrs.mode & libc::S_ISVTX as u16 != 0 + && _req.uid() != 0 + && _req.uid() != parent_attrs.uid + && _req.uid() != inode_attrs.uid + { + reply.error(Errno::EACCES); + return; + } + + let mut new_parent_attrs = match self.get_inode(newparent) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + if !check_access( + new_parent_attrs.uid, + new_parent_attrs.gid, + new_parent_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) { + reply.error(Errno::EACCES); + return; + } + + // "Sticky bit" handling in new_parent + if new_parent_attrs.mode & libc::S_ISVTX as u16 != 0 { + if let Ok(existing_attrs) = self.lookup_name(newparent, newname) { + if _req.uid() != 0 + && _req.uid() != new_parent_attrs.uid + && _req.uid() != existing_attrs.uid + { + reply.error(Errno::EACCES); + return; + } + } + } + + #[cfg(target_os = "linux")] + if flags.contains(RenameFlags::RENAME_EXCHANGE) { + let mut new_inode_attrs = match self.lookup_name(newparent, newname) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + let mut entries = self.get_directory_content(newparent).unwrap(); + entries.insert( + newname.as_bytes().to_vec(), + (inode_attrs.inode, inode_attrs.kind), + ); + self.write_directory_content(newparent, &entries); + + let mut entries = self.get_directory_content(parent).unwrap(); + entries.insert( + name.as_bytes().to_vec(), + (new_inode_attrs.inode, new_inode_attrs.kind), + ); + self.write_directory_content(parent, &entries); + + parent_attrs.last_metadata_changed = time_now(); + parent_attrs.last_modified = time_now(); + self.write_inode(&parent_attrs); + new_parent_attrs.last_metadata_changed = time_now(); + new_parent_attrs.last_modified = time_now(); + self.write_inode(&new_parent_attrs); + inode_attrs.last_metadata_changed = time_now(); + self.write_inode(&inode_attrs); + new_inode_attrs.last_metadata_changed = time_now(); + self.write_inode(&new_inode_attrs); + + if inode_attrs.kind == FileKind::Directory { + let mut entries = self + .get_directory_content(INodeNo(inode_attrs.inode)) + .unwrap(); + entries.insert(b"..".to_vec(), (newparent.0, FileKind::Directory)); + self.write_directory_content(INodeNo(inode_attrs.inode), &entries); + } + if new_inode_attrs.kind == FileKind::Directory { + let mut entries = self + .get_directory_content(INodeNo(new_inode_attrs.inode)) + .unwrap(); + entries.insert(b"..".to_vec(), (parent.0, FileKind::Directory)); + self.write_directory_content(INodeNo(new_inode_attrs.inode), &entries); + } + + reply.ok(); + return; + } + + // Only overwrite an existing directory if it's empty + if let Ok(new_name_attrs) = self.lookup_name(newparent, newname) { + if new_name_attrs.kind == FileKind::Directory + && self + .get_directory_content(INodeNo(new_name_attrs.inode)) + .unwrap() + .len() + > 2 + { + reply.error(Errno::ENOTEMPTY); + return; + } + } + + // Only move an existing directory to a new parent, if we have write access to it, + // because that will change the ".." link in it + if inode_attrs.kind == FileKind::Directory + && parent != newparent + && !check_access( + inode_attrs.uid, + inode_attrs.gid, + inode_attrs.mode, + _req.uid(), + _req.gid(), + AccessFlags::W_OK, + ) + { + reply.error(Errno::EACCES); + return; + } + + // If target already exists decrement its hardlink count + if let Ok(mut existing_inode_attrs) = self.lookup_name(newparent, newname) { + let mut entries = self.get_directory_content(newparent).unwrap(); + entries.remove(newname.as_bytes()); + self.write_directory_content(newparent, &entries); + + if existing_inode_attrs.kind == FileKind::Directory { + existing_inode_attrs.hardlinks = 0; + } else { + existing_inode_attrs.hardlinks -= 1; + } + existing_inode_attrs.last_metadata_changed = time_now(); + self.write_inode(&existing_inode_attrs); + self.gc_inode(&existing_inode_attrs); + } + + let mut entries = self.get_directory_content(parent).unwrap(); + entries.remove(name.as_bytes()); + self.write_directory_content(parent, &entries); + + let mut entries = self.get_directory_content(newparent).unwrap(); + entries.insert( + newname.as_bytes().to_vec(), + (inode_attrs.inode, inode_attrs.kind), + ); + self.write_directory_content(newparent, &entries); + + parent_attrs.last_metadata_changed = time_now(); + parent_attrs.last_modified = time_now(); + self.write_inode(&parent_attrs); + new_parent_attrs.last_metadata_changed = time_now(); + new_parent_attrs.last_modified = time_now(); + self.write_inode(&new_parent_attrs); + inode_attrs.last_metadata_changed = time_now(); + self.write_inode(&inode_attrs); + + if inode_attrs.kind == FileKind::Directory { + let mut entries = self + .get_directory_content(INodeNo(inode_attrs.inode)) + .unwrap(); + entries.insert(b"..".to_vec(), (newparent.0, FileKind::Directory)); + self.write_directory_content(INodeNo(inode_attrs.inode), &entries); + } + + reply.ok(); + } + + fn link( + &self, + _req: &Request, + ino: INodeNo, + newparent: INodeNo, + newname: &OsStr, + reply: ReplyEntry, + ) { + debug!("link() called for {ino}, {newparent}, {newname:?}"); + let mut attrs = match self.get_inode(ino) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + if let Err(error_code) = self.insert_link(_req, newparent, newname, ino, attrs.kind) { + reply.error(error_code); + } else { + attrs.hardlinks += 1; + attrs.last_metadata_changed = time_now(); + self.write_inode(&attrs); + reply.entry(&Duration::new(0, 0), &attrs.into(), fuser::Generation(0)); + } + } + + fn open(&self, _req: &Request, _ino: INodeNo, flags: OpenFlags, reply: ReplyOpen) { + debug!("open() called for {_ino:?}"); + let (access_mask, read, write) = match flags.acc_mode() { + OpenAccMode::O_RDONLY => { + // Behavior is undefined, but most filesystems return EACCES + if flags.0 & libc::O_TRUNC != 0 { + reply.error(Errno::EACCES); + return; + } + if flags.0 & FMODE_EXEC != 0 { + // Open is from internal exec syscall + (libc::X_OK, true, false) + } else { + (libc::R_OK, true, false) + } + } + OpenAccMode::O_WRONLY => (libc::W_OK, false, true), + OpenAccMode::O_RDWR => (libc::R_OK | libc::W_OK, true, true), + }; + + match self.get_inode(_ino) { + Ok(mut attr) => { + if check_access( + attr.uid, + attr.gid, + attr.mode, + _req.uid(), + _req.gid(), + AccessFlags::from_bits_retain(access_mask), + ) { + attr.open_file_handles += 1; + self.write_inode(&attr); + let open_flags = if self.direct_io { + FopenFlags::FOPEN_DIRECT_IO + } else { + FopenFlags::empty() + }; + reply.opened( + FileHandle(self.allocate_next_file_handle(read, write)), + open_flags, + ); + } else { + reply.error(Errno::EACCES); + } + return; + } + Err(error_code) => reply.error(error_code), + } + } + + fn read( + &self, + _req: &Request, + ino: INodeNo, + fh: FileHandle, + offset: u64, + size: u32, + _flags: OpenFlags, + _lock_owner: Option, + reply: ReplyData, + ) { + debug!("read() called on {ino:?} offset={offset:?} size={size:?}"); + if !Self::check_file_handle_read(fh.into()) { + reply.error(Errno::EACCES); + return; + } + + let path = self.content_path(ino); + match File::open(path) { + Ok(file) => { + let file_size = file.metadata().unwrap().len(); + // Could underflow if file length is less than local_start + let read_size = min(size, file_size.saturating_sub(offset as u64) as u32); + + let mut buffer = vec![0; read_size as usize]; + file.read_exact_at(&mut buffer, offset as u64).unwrap(); + reply.data(&buffer); + } + _ => { + reply.error(Errno::ENOENT); + } + } + } + + fn write( + &self, + _req: &Request, + ino: INodeNo, + fh: FileHandle, + offset: u64, + data: &[u8], + _write_flags: WriteFlags, + _flags: OpenFlags, + _lock_owner: Option, + reply: ReplyWrite, + ) { + debug!("write() called with {:?} size={:?}", ino, data.len()); + if !Self::check_file_handle_write(fh.into()) { + reply.error(Errno::EACCES); + return; + } + + let path = self.content_path(ino); + match OpenOptions::new().write(true).open(path) { + Ok(mut file) => { + file.seek(SeekFrom::Start(offset)).unwrap(); + file.write_all(data).unwrap(); + + let mut attrs = self.get_inode(ino).unwrap(); + attrs.last_metadata_changed = time_now(); + attrs.last_modified = time_now(); + let Ok(offset_usize): Result = offset.try_into() else { + reply.error(Errno::EFBIG); + return; + }; + let Some(end_offset) = data.len().checked_add(offset_usize) else { + reply.error(Errno::EFBIG); + return; + }; + if end_offset > attrs.size as usize { + attrs.size = end_offset as u64; + } + // if flags & FUSE_WRITE_KILL_PRIV as i32 != 0 { + // clear_suid_sgid(&mut attrs); + // } + // XXX: In theory we should only need to do this when WRITE_KILL_PRIV is set for 7.31+ + // However, xfstests fail in that case + clear_suid_sgid(&mut attrs); + self.write_inode(&attrs); + + reply.written(data.len() as u32); + } + _ => { + reply.error(Errno::EBADF); + } + } + } + + fn release( + &self, + _req: &Request, + _ino: INodeNo, + _fh: FileHandle, + _flags: OpenFlags, + _lock_owner: Option, + _flush: bool, + reply: ReplyEmpty, + ) { + if let Ok(mut attrs) = self.get_inode(_ino) { + attrs.open_file_handles -= 1; + } + reply.ok(); + } + + fn opendir(&self, _req: &Request, _ino: INodeNo, _flags: OpenFlags, reply: ReplyOpen) { + debug!("opendir() called on {_ino:?}"); + let (access_mask, read, write) = match _flags.acc_mode() { + OpenAccMode::O_RDONLY => { + // Behavior is undefined, but most filesystems return EACCES + if _flags.0 & libc::O_TRUNC != 0 { + reply.error(Errno::EACCES); + return; + } + (libc::R_OK, true, false) + } + OpenAccMode::O_WRONLY => (libc::W_OK, false, true), + OpenAccMode::O_RDWR => (libc::R_OK | libc::W_OK, true, true), + }; + + match self.get_inode(_ino) { + Ok(mut attr) => { + if check_access( + attr.uid, + attr.gid, + attr.mode, + _req.uid(), + _req.gid(), + AccessFlags::from_bits_retain(access_mask), + ) { + attr.open_file_handles += 1; + self.write_inode(&attr); + let open_flags = if self.direct_io { + FopenFlags::FOPEN_DIRECT_IO + } else { + FopenFlags::empty() + }; + reply.opened( + FileHandle(self.allocate_next_file_handle(read, write)), + open_flags, + ); + } else { + reply.error(Errno::EACCES); + } + return; + } + Err(error_code) => reply.error(error_code), + } + } + + fn readdir( + &self, + _req: &Request, + ino: INodeNo, + _fh: FileHandle, + offset: u64, + mut reply: ReplyDirectory, + ) { + debug!("readdir() called with {ino:?}"); + let entries = match self.get_directory_content(ino) { + Ok(entries) => entries, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + for (index, entry) in entries.iter().skip(offset as usize).enumerate() { + let (name, (inode, file_type)) = entry; + + let buffer_full: bool = reply.add( + INodeNo(*inode), + offset + index as u64 + 1, + (*file_type).into(), + OsStr::from_bytes(name), + ); + + if buffer_full { + break; + } + } + + reply.ok(); + } + + fn releasedir( + &self, + _req: &Request, + _ino: INodeNo, + _fh: FileHandle, + _flags: OpenFlags, + reply: ReplyEmpty, + ) { + if let Ok(mut attrs) = self.get_inode(_ino) { + attrs.open_file_handles -= 1; + } + reply.ok(); + } + + fn statfs(&self, _req: &Request, _ino: INodeNo, reply: ReplyStatfs) { + warn!("statfs() implementation is a stub"); + // TODO: real implementation of this + reply.statfs( + 10_000, + 10_000, + 10_000, + 1, + 10_000, + BLOCK_SIZE, + MAX_NAME_LENGTH, + BLOCK_SIZE, + ); + } + + fn setxattr( + &self, + _req: &Request, + ino: INodeNo, + name: &OsStr, + _value: &[u8], + _flags: i32, + _position: u32, + reply: ReplyEmpty, + ) { + if let Ok(mut attrs) = self.get_inode(ino) { + if let Err(error) = xattr_access_check(name.as_bytes(), libc::W_OK, &attrs, _req) { + reply.error(error); + return; + } + + attrs + .xattrs + .insert(name.as_bytes().to_vec(), _value.to_vec()); + attrs.last_metadata_changed = time_now(); + self.write_inode(&attrs); + reply.ok(); + } else { + reply.error(Errno::EBADF); + } + } + + fn getxattr( + &self, + request: &Request, + inode: INodeNo, + key: &OsStr, + size: u32, + reply: ReplyXattr, + ) { + if let Ok(attrs) = self.get_inode(inode) { + if let Err(error) = xattr_access_check(key.as_bytes(), libc::R_OK, &attrs, request) { + reply.error(error); + return; + } + + if let Some(data) = attrs.xattrs.get(key.as_bytes()) { + if size == 0 { + reply.size(data.len() as u32); + } else if data.len() <= size as usize { + reply.data(data); + } else { + reply.error(Errno::ERANGE); + } + } else { + #[cfg(target_os = "linux")] + reply.error(Errno::ENODATA); + #[cfg(not(target_os = "linux"))] + reply.error(Errno::ENOATTR); + } + } else { + reply.error(Errno::EBADF); + } + } + + fn listxattr(&self, _req: &Request, ino: INodeNo, size: u32, reply: ReplyXattr) { + if let Ok(attrs) = self.get_inode(ino) { + let mut bytes = vec![]; + // Convert to concatenated null-terminated strings + for key in attrs.xattrs.keys() { + bytes.extend(key); + bytes.push(0); + } + if size == 0 { + reply.size(bytes.len() as u32); + } else if bytes.len() <= size as usize { + reply.data(&bytes); + } else { + reply.error(Errno::ERANGE); + } + } else { + reply.error(Errno::EBADF); + } + } + + fn removexattr(&self, request: &Request, inode: INodeNo, key: &OsStr, reply: ReplyEmpty) { + if let Ok(mut attrs) = self.get_inode(inode) { + if let Err(error) = xattr_access_check(key.as_bytes(), libc::W_OK, &attrs, request) { + reply.error(error); + return; + } + + if attrs.xattrs.remove(key.as_bytes()).is_none() { + #[cfg(target_os = "linux")] + reply.error(Errno::ENODATA); + #[cfg(not(target_os = "linux"))] + reply.error(Errno::ENOATTR); + return; + } + attrs.last_metadata_changed = time_now(); + self.write_inode(&attrs); + reply.ok(); + } else { + reply.error(Errno::EBADF); + } + } + + fn access(&self, _req: &Request, ino: INodeNo, mask: AccessFlags, reply: ReplyEmpty) { + debug!("access() called with {ino:?} {mask:?}"); + match self.get_inode(ino) { + Ok(attr) => { + if check_access(attr.uid, attr.gid, attr.mode, _req.uid(), _req.gid(), mask) { + reply.ok(); + } else { + reply.error(Errno::EACCES); + } + } + Err(error_code) => reply.error(error_code), + } + } + + fn create( + &self, + req: &Request, + parent: INodeNo, + name: &OsStr, + mut mode: u32, + _umask: u32, + flags: i32, + reply: ReplyCreate, + ) { + debug!("create() called with {parent:?} {name:?}"); + if self.lookup_name(parent, name).is_ok() { + reply.error(Errno::EEXIST); + return; + } + + let (read, write) = match flags & libc::O_ACCMODE { + libc::O_RDONLY => (true, false), + libc::O_WRONLY => (false, true), + libc::O_RDWR => (true, true), + // Exactly one access mode flag must be specified + _ => { + reply.error(Errno::EINVAL); + return; + } + }; + + let mut parent_attrs = match self.get_inode(parent) { + Ok(attrs) => attrs, + Err(error_code) => { + reply.error(error_code); + return; + } + }; + + if !check_access( + parent_attrs.uid, + parent_attrs.gid, + parent_attrs.mode, + req.uid(), + req.gid(), + AccessFlags::W_OK, + ) { + reply.error(Errno::EACCES); + return; + } + parent_attrs.last_modified = time_now(); + parent_attrs.last_metadata_changed = time_now(); + self.write_inode(&parent_attrs); + + if req.uid() != 0 { + mode &= !(libc::S_ISUID | libc::S_ISGID) as u32; + } + + #[cfg(target_os = "freebsd")] + { + let kind = as_file_kind(mode); + // FreeBSD: sticky bit only valid on directories; otherwise EFTYPE + if req.uid() != 0 + && (mode as u16 & libc::S_ISVTX as u16) != 0 + && kind != FileKind::Directory + { + reply.error(Errno::EFTYPE); + return; + } + } + + let inode = self.allocate_next_inode(); + let attrs = InodeAttributes { + inode: inode.0, + open_file_handles: 1, + size: 0, + last_accessed: time_now(), + last_modified: time_now(), + last_metadata_changed: time_now(), + kind: as_file_kind(mode), + mode: self.creation_mode(mode), + hardlinks: 1, + uid: req.uid(), + gid: creation_gid(&parent_attrs, req.gid()), + xattrs: BTreeMap::default(), + }; + self.write_inode(&attrs); + File::create(self.content_path(inode)).unwrap(); + + if as_file_kind(mode) == FileKind::Directory { + let mut entries = BTreeMap::new(); + entries.insert(b".".to_vec(), (inode.0, FileKind::Directory)); + entries.insert(b"..".to_vec(), (parent.0, FileKind::Directory)); + self.write_directory_content(inode, &entries); + } + + let mut entries = self.get_directory_content(parent).unwrap(); + entries.insert(name.as_bytes().to_vec(), (inode.0, attrs.kind)); + self.write_directory_content(parent, &entries); + + // TODO: implement flags + reply.created( + &Duration::new(0, 0), + &attrs.into(), + fuser::Generation(0), + FileHandle(self.allocate_next_file_handle(read, write)), + FopenFlags::empty(), + ); + } + + #[cfg(target_os = "linux")] + fn fallocate( + &self, + _req: &Request, + ino: INodeNo, + _fh: FileHandle, + offset: u64, + length: u64, + mode: i32, + reply: ReplyEmpty, + ) { + let path = self.content_path(ino); + match OpenOptions::new().write(true).open(path) { + Ok(file) => { + unsafe { + libc::fallocate64(file.into_raw_fd(), mode, offset as i64, length as i64); + } + if mode & libc::FALLOC_FL_KEEP_SIZE == 0 { + let mut attrs = self.get_inode(ino).unwrap(); + attrs.last_metadata_changed = time_now(); + attrs.last_modified = time_now(); + if offset + length > attrs.size { + attrs.size = (offset + length) as u64; + } + self.write_inode(&attrs); + } + reply.ok(); + } + _ => { + reply.error(Errno::ENOENT); + } + } + } + + fn copy_file_range( + &self, + _req: &Request, + src_inode: INodeNo, + src_fh: FileHandle, + src_offset: u64, + dest_inode: INodeNo, + dest_fh: FileHandle, + dest_offset: u64, + size: u64, + _flags: fuser::CopyFileRangeFlags, + reply: ReplyWrite, + ) { + debug!( + "copy_file_range() called with src=({src_fh}, {src_inode}, {src_offset}) dest=({dest_fh}, {dest_inode}, {dest_offset}) size={size}" + ); + if !Self::check_file_handle_read(src_fh.into()) { + reply.error(Errno::EACCES); + return; + } + if !Self::check_file_handle_write(dest_fh.into()) { + reply.error(Errno::EACCES); + return; + } + + let src_path = self.content_path(src_inode); + match File::open(src_path) { + Ok(file) => { + let file_size = file.metadata().unwrap().len(); + // Could underflow if file length is less than local_start + let read_size = min(size, file_size.saturating_sub(src_offset)); + + let mut data = vec![0; read_size as usize]; + file.read_exact_at(&mut data, src_offset).unwrap(); + + let dest_path = self.content_path(dest_inode); + match OpenOptions::new().write(true).open(dest_path) { + Ok(mut file) => { + file.seek(SeekFrom::Start(dest_offset)).unwrap(); + file.write_all(&data).unwrap(); + + let mut attrs = self.get_inode(dest_inode).unwrap(); + attrs.last_metadata_changed = time_now(); + attrs.last_modified = time_now(); + let Ok(dest_offset_usize): Result = dest_offset.try_into() else { + reply.error(Errno::EFBIG); + return; + }; + let Some(end_offset) = data.len().checked_add(dest_offset_usize) else { + reply.error(Errno::EFBIG); + return; + }; + if end_offset > attrs.size as usize { + attrs.size = end_offset as u64; + } + self.write_inode(&attrs); + + reply.written(data.len() as u32); + } + _ => { + reply.error(Errno::EBADF); + } + } + } + _ => { + reply.error(Errno::ENOENT); + } + } + } +} + +pub fn check_access( + file_uid: u32, + file_gid: u32, + file_mode: u16, + uid: u32, + gid: u32, + mut access_mask: AccessFlags, +) -> bool { + // F_OK tests for existence of file + if access_mask == AccessFlags::F_OK { + return true; + } + let file_mode = i32::from(file_mode); + + // root is allowed to read & write anything + if uid == 0 { + // root only allowed to exec if one of the X bits is set + // TODO: this code is no-op: `X_OK` is zero. + access_mask &= AccessFlags::X_OK; + access_mask &= !AccessFlags::from_bits_retain(access_mask.bits() & (file_mode >> 6)); + access_mask &= !AccessFlags::from_bits_retain(access_mask.bits() & (file_mode >> 3)); + access_mask &= !AccessFlags::from_bits_retain(access_mask.bits() & file_mode); + return access_mask.is_empty(); + } + + if uid == file_uid { + access_mask &= !AccessFlags::from_bits_retain(access_mask.bits() & (file_mode >> 6)); + } else if gid == file_gid { + access_mask &= !AccessFlags::from_bits_retain(access_mask.bits() & (file_mode >> 3)); + } else { + access_mask &= !AccessFlags::from_bits_retain(access_mask.bits() & file_mode); + } + + return access_mask.is_empty(); +} + +fn as_file_kind(mut mode: u32) -> FileKind { + mode &= libc::S_IFMT as u32; + + if mode == libc::S_IFREG as u32 { + return FileKind::File; + } else if mode == libc::S_IFLNK as u32 { + return FileKind::Symlink; + } else if mode == libc::S_IFDIR as u32 { + return FileKind::Directory; + } + unimplemented!("{mode}"); +} + +fn get_groups(pid: u32) -> Vec { + if cfg!(target_os = "linux") { + let path = format!("/proc/{pid}/task/{pid}/status"); + let file = File::open(path).unwrap(); + for line in BufReader::new(file).lines() { + let line = line.unwrap(); + if line.starts_with("Groups:") { + return line["Groups: ".len()..] + .split(' ') + .filter(|x| !x.trim().is_empty()) + .map(|x| x.parse::().unwrap()) + .collect(); + } + } + } + + #[cfg(target_os = "freebsd")] + { + // Use libprocstat to query the kernel for the process's groups. + // Link with: #[link(name = "procstat")] + use libc::c_int; + use libc::c_uint; + use libc::gid_t; + + #[repr(C)] + struct procstat { + _priv: [u8; 0], + } + #[repr(C)] + struct kinfo_proc { + _priv: [u8; 0], + } + + #[link(name = "procstat")] + unsafe extern "C" { + fn procstat_open_sysctl() -> *mut procstat; + fn procstat_close(ps: *mut procstat); + + fn procstat_getprocs( + ps: *mut procstat, + what: c_int, + arg: c_int, + count: *mut c_uint, + ) -> *mut kinfo_proc; + fn procstat_freeprocs(ps: *mut procstat, kp: *mut kinfo_proc); + + fn procstat_getgroups( + ps: *mut procstat, + kp: *mut kinfo_proc, + count: *mut c_uint, + ) -> *mut gid_t; + fn procstat_freegroups(ps: *mut procstat, groups: *mut gid_t); + } + + // From sys/sysctl.h (KERN_PROC_PID == 1) + // https://fxr-style headers and manpages document this constant. + const KERN_PROC_PID: c_int = 1; + + unsafe { + let ps = procstat_open_sysctl(); + if ps.is_null() { + return vec![]; + } + + let mut nprocs: c_uint = 0; + let kps = procstat_getprocs(ps, KERN_PROC_PID, pid as c_int, &mut nprocs); + if kps.is_null() || nprocs == 0 { + procstat_close(ps); + return vec![]; + } + + let mut ngroups: c_uint = 0; + let groups_ptr = procstat_getgroups(ps, kps, &mut ngroups); + + let mut out = Vec::new(); + if !groups_ptr.is_null() && ngroups > 0 { + let slice = std::slice::from_raw_parts(groups_ptr, ngroups as usize); + out.extend(slice.iter().map(|&g| g as u32)); + procstat_freegroups(ps, groups_ptr); + } + + procstat_freeprocs(ps, kps); + procstat_close(ps); + + return out; + } + } + + #[cfg(not(target_os = "freebsd"))] + vec![] +} diff --git a/tests/session.rs b/tests/session.rs new file mode 100644 index 00000000..441d792c --- /dev/null +++ b/tests/session.rs @@ -0,0 +1,29 @@ +#![allow(unused_imports)] +use fuser::{Config, MountOption, SessionACL}; + +mod fixtures; + +/// # Scenario +/// In unpatched versions of `fuser`, if auto_unmount is on, the library relies exclusively on dropping the socket to unmount. The `fusermount` daemon will then request a [directory open](https://github.com/libfuse/libfuse/blob/07a1b913b180627db5e9659e31a86c68637a9308/util/fusermount.c#L1544-L1560) in `should_auto_unmount`. In short, auto_unmount only triggers an unmount if the FUSE device is closed. +/// - If the FUSE handler does not respond, the unmounting process will hang. +/// - In the more likely case where the FUSE handler does respond and return a directory handle or anything other than [`fuser::Errno::ENOTCONN`], `should_auto_unmount` will return `false(0)` and the `fusermount` process exits without doing any unmounting. +/// +/// # Notes +/// SimpleFS fixture is used to emulate FUSE requests. +#[cfg(target_os = "linux")] +#[test_log::test] +fn test_session_auto_unmount() { + let data_dir = tempfile::TempDir::new().unwrap(); + let mountpoint = tempfile::TempDir::new().unwrap(); + let filesystem = + fixtures::simple::SimpleFS::new(data_dir.path().to_str().unwrap().to_string(), false, true); + let mut config = Config::default(); + config.mount_options.extend(vec![ + MountOption::AutoUnmount, + MountOption::FSName("fuser".to_string()), + ]); + config.n_threads = Some(2); + config.acl = SessionACL::All; + let session = fuser::spawn_mount(filesystem, mountpoint, &config).unwrap(); + session.umount_and_join().expect("Failed to unmount"); +}