diff --git a/Cargo.toml b/Cargo.toml index 7c32bbbf..85e2bf7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ experimental = ["async-trait", "tokio"] libfuse = [] libfuse2 = ["libfuse"] libfuse3 = ["libfuse"] +direct-mount = ["nix/resource", "nix/signal"] serializable = ["serde"] macfuse-4-compat = [] # Disable mount implementations. Code will compile but won't work. diff --git a/build.rs b/build.rs index d684987d..f1153d41 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,7 @@ fn main() { // Register rustc cfg for switching between mount implementations. println!( - "cargo::rustc-check-cfg=cfg(fuser_mount_impl, values(\"pure-rust\", \"libfuse2\", \"libfuse3\", \"macos-no-mount\"))" + "cargo::rustc-check-cfg=cfg(fuser_mount_impl, values(\"direct-mount\", \"pure-rust\", \"libfuse2\", \"libfuse3\", \"macos-no-mount\"))" ); let target_os = @@ -12,7 +12,11 @@ fn main() { "linux" | "freebsd" | "dragonfly" | "openbsd" | "netbsd" ) && cfg!(not(feature = "libfuse")) { - println!("cargo::rustc-cfg=fuser_mount_impl=\"pure-rust\""); + if cfg!(feature = "direct-mount") { + println!("cargo::rustc-cfg=fuser_mount_impl=\"direct-mount\""); + } else { + println!("cargo::rustc-cfg=fuser_mount_impl=\"pure-rust\""); + } } else if target_os == "macos" { if cfg!(feature = "macos-no-mount") { println!("cargo::rustc-cfg=fuser_mount_impl=\"macos-no-mount\""); diff --git a/fuser-tests/src/commands/bsd_mount.rs b/fuser-tests/src/commands/bsd_mount.rs index 4d91fcef..d7af84cc 100644 --- a/fuser-tests/src/commands/bsd_mount.rs +++ b/fuser-tests/src/commands/bsd_mount.rs @@ -8,13 +8,24 @@ use crate::ansi::green; use crate::canonical_temp_dir::CanonicalTempDir; use crate::cargo::cargo_build_example; use crate::command_utils::command_success; +use crate::features::Feature; use crate::mount_util::wait_for_fuse_mount; pub(crate) async fn run_bsd_mount_tests() -> anyhow::Result<()> { + // Run tests using pure-rust (fusermount) implementation + run_bsd_mount_tests_with_features(&[]).await?; + + // Run tests using direct-mount (direct mount syscall) implementation + run_bsd_mount_tests_with_features(&[Feature::DirectMount]).await?; + + Ok(()) +} + +async fn run_bsd_mount_tests_with_features(features: &[Feature]) -> anyhow::Result<()> { let mount_dir = CanonicalTempDir::new()?; let mount_path = mount_dir.path(); - let hello_exe = cargo_build_example("hello", &[]).await?; + let hello_exe = cargo_build_example("hello", features).await?; eprintln!("Starting hello filesystem..."); let mut fuse_process = Command::new(&hello_exe) diff --git a/fuser-tests/src/commands/mount.rs b/fuser-tests/src/commands/mount.rs index 6bf7f6e8..ba3bbb2a 100644 --- a/fuser-tests/src/commands/mount.rs +++ b/fuser-tests/src/commands/mount.rs @@ -44,6 +44,25 @@ async fn run_mount_tests_inner(libfuse: Libfuse) -> anyhow::Result<()> { run_test(&[], Unmount::Auto, libfuse.fusermount(), 1, false).await?; test_no_user_allow_other(&[], &libfuse).await?; + // Tests without libfuse feature (direct mount syscall implementation) + run_test( + &[Feature::DirectMount], + Unmount::Manual, + Fusermount::False, + 1, + false, + ) + .await?; + run_test( + &[Feature::DirectMount], + Unmount::Auto, + Fusermount::False, + 1, + false, + ) + .await?; + test_no_user_allow_other(&[Feature::DirectMount], &libfuse).await?; + // Tests with libfuse run_test( &[libfuse.feature()], @@ -65,6 +84,22 @@ async fn run_mount_tests_inner(libfuse: Libfuse) -> anyhow::Result<()> { // Multi-threaded tests run_test(&[], Unmount::Auto, libfuse.fusermount(), 2, false).await?; run_test(&[], Unmount::Auto, libfuse.fusermount(), 2, true).await?; + run_test( + &[Feature::DirectMount], + Unmount::Auto, + Fusermount::False, + 2, + false, + ) + .await?; + run_test( + &[Feature::DirectMount], + Unmount::Auto, + Fusermount::False, + 2, + true, + ) + .await?; if let Libfuse::Libfuse3 = libfuse { run_allow_root_test() diff --git a/fuser-tests/src/features.rs b/fuser-tests/src/features.rs index cbdb9671..0188412f 100644 --- a/fuser-tests/src/features.rs +++ b/fuser-tests/src/features.rs @@ -11,6 +11,8 @@ pub(crate) enum Feature { Libfuse2, /// Use libfuse3 for mounting. Libfuse3, + /// Use mount syscall directly for mounting. + DirectMount, } impl Feature { @@ -20,6 +22,7 @@ impl Feature { Feature::Experimental => "experimental", Feature::Libfuse2 => "libfuse2", Feature::Libfuse3 => "libfuse3", + Feature::DirectMount => "direct-mount", } } } diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs new file mode 100644 index 00000000..16eafe8a --- /dev/null +++ b/src/mnt/fuse_direct.rs @@ -0,0 +1,605 @@ +use std::ffi::OsString; +use std::fs::File; +use std::io; +use std::io::Read; +use std::os::fd::AsFd; +use std::os::fd::AsRawFd; +use std::os::fd::RawFd; +use std::os::unix::ffi::OsStringExt; +use std::os::unix::net::UnixStream; +use std::path::Path; +use std::path::PathBuf; +use std::process::exit; +use std::sync::Arc; + +use nix::fcntl::OFlag; +use nix::fcntl::open; +use nix::sys::resource::Resource; +use nix::sys::resource::getrlimit; +use nix::sys::signal::SigSet; +use nix::sys::signal::SigmaskHow; +use nix::sys::signal::sigprocmask; +use nix::sys::stat::Mode; +use nix::sys::wait::WaitPidFlag; +use nix::sys::wait::WaitStatus; +use nix::sys::wait::waitpid; +use nix::unistd::ForkResult; +use nix::unistd::Pid; +use nix::unistd::Uid; +use nix::unistd::close; +use nix::unistd::dup2_stderr; +use nix::unistd::dup2_stdin; +use nix::unistd::dup2_stdout; +use nix::unistd::fork; +use nix::unistd::setsid; + +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 DEV_FUSE: &str = "/dev/fuse"; + +#[derive(Debug)] +pub(crate) struct MountImpl { + mountpoint: PathBuf, + auto_unmount_daemon: Option, + fuse_device: Arc, +} + +#[derive(Debug)] +struct AutoUnmountDaemon { + socket: UnixStream, + pid: Pid, +} + +impl MountImpl { + pub(crate) fn new( + mountpoint: &Path, + options: &[MountOption], + acl: SessionACL, + ) -> io::Result<(Arc, Self)> { + let mountpoint = mountpoint.canonicalize()?; + let dev = Arc::new(DevFuse( + File::options().read(true).write(true).open(DEV_FUSE)?, + )); + let dev_fd = dev.as_raw_fd(); + + let uid = Uid::current(); + + let mut fsname: Option<&str> = None; + let mut subtype: Option<&str> = None; + let mut auto_unmount = false; + + #[cfg(target_os = "linux")] + let mut flags = nix::mount::MsFlags::empty(); + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + ))] + let mut flags = nix::mount::MntFlags::empty(); + + #[cfg(not(target_os = "freebsd"))] + if !uid.is_root() || !options.contains(&MountOption::Dev) { + // Default to nodev + #[cfg(target_os = "linux")] + { + flags |= nix::mount::MsFlags::MS_NODEV; + } + #[cfg(any( + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + ))] + { + flags |= nix::mount::MntFlags::MNT_NODEV; + } + } + + if !uid.is_root() || !options.contains(&MountOption::Suid) { + // default to nosuid + #[cfg(target_os = "linux")] + { + flags |= nix::mount::MsFlags::MS_NOSUID; + } + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + ))] + { + flags |= nix::mount::MntFlags::MNT_NOSUID; + } + } + + for opt in options { + match option_group(opt) { + MountOptionGroup::KernelFlag => flags |= option_to_flag(opt)?, + MountOptionGroup::Fusermount => match opt { + MountOption::FSName(val) => fsname = Some(val), + MountOption::Subtype(val) => subtype = Some(val), + MountOption::AutoUnmount => auto_unmount = true, + _ => {} + }, + _ => {} + } + } + + Self::do_mount(&mountpoint, fsname, subtype, flags, options, acl, dev_fd)?; + + let mut mnt = MountImpl { + mountpoint, + auto_unmount_daemon: None, + fuse_device: dev.clone(), + }; + + if auto_unmount { + mnt.setup_auto_unmount()?; + } + + Ok((dev, mnt)) + } + + #[cfg(target_os = "macos")] + fn do_mount( + _mountpoint: &Path, + _fsname: Option<&str>, + _subtype: Option<&str>, + _flags: nix::mount::MsFlags, + _options: &[MountOption], + _acl: SessionACL, + _dev_fd: RawFd, + ) -> io::Result<()> { + // macos-no-mount - Don't actually mount + Ok(()) + } + + #[cfg(target_os = "linux")] + fn do_mount( + mountpoint: &Path, + fsname: Option<&str>, + subtype: Option<&str>, + flags: nix::mount::MsFlags, + options: &[MountOption], + acl: SessionACL, + dev_fd: RawFd, + ) -> io::Result<()> { + use std::io::Write; + use std::os::unix::fs::MetadataExt; + + let mut opts = Vec::new(); + for opt in options { + if option_group(opt) == MountOptionGroup::KernelOption { + write!(opts, "{},", option_to_string(opt))?; + } + } + + if let Some(opt) = acl.to_mount_option() { + write!(opts, "{opt},")?; + } + + let root_mode = mountpoint + .metadata() + .map(|meta| meta.mode() & nix::sys::stat::SFlag::S_IFMT.bits())?; + + let old_len = opts.len(); + write!( + opts, + "fd={},rootmode={:o},user_id={},group_id={}", + dev_fd, + root_mode, + Uid::current().as_raw(), + nix::unistd::Gid::current().as_raw(), + )?; + + let mut ty = subtype.map_or("fuse".into(), |subtype| format!("fuse.{subtype}")); + + let mut source = if let Some(fsname) = fsname { + fsname + } else if let Some(subtype) = subtype { + subtype + } else { + DEV_FUSE + }; + + let pagesize = nix::unistd::sysconf(nix::unistd::SysconfVar::PAGE_SIZE)? + .map_or(usize::MAX, |ps| ps.try_into().unwrap_or(usize::MAX)) + - 1; + + if opts.len() > pagesize { + log::error!( + "mount options too long: '{}'", + String::from_utf8_lossy(&opts) + ); + return Err(nix::Error::EINVAL.into()); + } + + let mut res = nix::mount::mount( + Some(source), + mountpoint, + Some(ty.as_str()), + flags, + Some(opts.as_slice()), + ); + let source_tmp; + if let Err(nix::Error::ENODEV) = &res { + if let Some(subtype) = subtype { + ty = "fuse".into(); + if let Some(fsname) = fsname { + source_tmp = format!("{subtype}#{fsname}"); + source = source_tmp.as_str(); + } else { + source = ty.as_str(); + } + + res = nix::mount::mount( + Some(source), + mountpoint, + Some(ty.as_str()), + flags, + Some(opts.as_slice()), + ); + } + } + if let Err(nix::Error::EINVAL) = &res { + opts.truncate(old_len); + + write!( + opts, + "fd={},rootmode={:o},user_id={}", + dev_fd, + root_mode, + Uid::current().as_raw(), + )?; + + res = nix::mount::mount( + Some(source), + mountpoint, + Some(ty.as_str()), + flags, + Some(opts.as_slice()), + ); + } + res.inspect_err(|err| log::error!("mount failed: {err}"))?; + + Ok(()) + } + + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + ))] + fn do_mount( + mountpoint: &Path, + fsname: Option<&str>, + subtype: Option<&str>, + flags: nix::mount::MntFlags, + options: &[MountOption], + acl: SessionACL, + dev_fd: RawFd, + ) -> io::Result<()> { + let mut nmount = nix::mount::Nmount::new(); + + if let Some(fsname) = fsname { + nmount.str_opt_owned("fsname=", fsname); + } + + if let Some(subtype) = subtype { + nmount.str_opt_owned("subtype=", subtype); + } + + if !matches!(acl, SessionACL::Owner) { + nmount.str_opt_owned("allow_other", ""); + } + + for opt in options { + if option_group(opt) == MountOptionGroup::KernelOption { + nmount.str_opt_owned(option_to_string(opt).as_str(), ""); + } + } + + nmount + .str_opt(c"fstype", c"fusefs") + .str_opt_owned("fspath", mountpoint) + .str_opt(c"from", c"/dev/fuse") + .str_opt_owned("fd", dev_fd.to_string().as_str()) + .nmount(flags)?; + + Ok(()) + } + + pub(crate) fn umount_impl(&mut self) -> io::Result<()> { + if let Some(AutoUnmountDaemon { socket, pid }) = self.auto_unmount_daemon.take() { + // signal the daemon to perform the unmount + drop(socket); + + // wait for the daemon to exit - on error, just fallback to + // unmounting directly + if let Ok(status) = waitpid(pid, Some(WaitPidFlag::WEXITED)) { + match status { + // exited with return code 0 (success) + WaitStatus::Exited(_, 0) => return Ok(()), + + // On non-zero exit status, the daemon failed to unmount, + // and on signal, the daemon crashed or was otherwise + // killed. In either case, lets try to unmount ourselves + // if we can. + WaitStatus::Exited(_, _) | WaitStatus::Signaled(_, _, _) => {} + + // With `WEXITED`, this branch can't actually happen + _ => return Ok(()), + } + } + } + + 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(()); + } + + self.do_unmount(true) + } + + #[cfg(target_os = "linux")] + fn do_unmount(&mut self, lazy: bool) -> io::Result<()> { + let flags = if lazy { + nix::mount::MntFlags::MNT_DETACH + } else { + nix::mount::MntFlags::empty() + }; + nix::mount::umount2(&self.mountpoint, flags)?; + Ok(()) + } + + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_os = "macos", + ))] + fn do_unmount(&mut self, lazy: bool) -> io::Result<()> { + let flags = if lazy { + nix::mount::MntFlags::empty() + } else { + nix::mount::MntFlags::MNT_FORCE + }; + nix::mount::unmount(&self.mountpoint, flags)?; + Ok(()) + } + + fn setup_auto_unmount(&mut self) -> io::Result<()> { + let (tx, rx) = UnixStream::pair()?; + + let pid = match unsafe { fork() }? { + ForkResult::Child => { + exit(match self.do_auto_unmount(rx) { + Ok(()) => 0, + Err(err) => err.raw_os_error().unwrap_or(1), + }); + } + ForkResult::Parent { child } => child, + }; + + self.auto_unmount_daemon = Some(AutoUnmountDaemon { socket: tx, pid }); + + Ok(()) + } + + fn do_auto_unmount(&mut self, mut pipe: UnixStream) -> io::Result<()> { + close_inherited_fds(pipe.as_raw_fd()); + let _ = setsid(); + let _ = sigprocmask(SigmaskHow::SIG_BLOCK, Some(&SigSet::all()), None); + + let mut buf = [0u8; 16]; + loop { + match pipe.read(&mut buf) { + Ok(0) => break, + Ok(_) => {} + Err(err) if err.kind() == io::ErrorKind::Interrupted => {} + _ => break, + } + } + + if self.should_auto_unmount()? { + self.do_unmount(false)?; + } + + Ok(()) + } + + #[cfg(target_os = "macos")] + fn should_auto_unmount(&self) -> io::Result { + Ok(false) + } + + #[cfg(target_os = "linux")] + fn should_auto_unmount(&self) -> io::Result { + use std::io::BufRead; + + let etc_mtab = Path::new("/etc/mtab"); + let proc_mounts = Path::new("/proc/mounts"); + + let mtab_path = if proc_mounts.try_exists()? { + proc_mounts + } else if etc_mtab.try_exists()? { + etc_mtab + } else { + return Err(io::ErrorKind::NotFound.into()); + }; + + let mut mtab = io::BufReader::new(File::open(mtab_path)?); + let mut line = Vec::new(); + loop { + line.clear(); + if mtab.read_until(b'\n', &mut line)? == 0 { + break; + } + let line = line.as_slice(); + + let Some(fs_name_len) = line.iter().position(u8::is_ascii_whitespace) else { + continue; + }; + let line = &line[fs_name_len..]; + + let Some(path_start) = line.iter().position(|b| !b.is_ascii_whitespace()) else { + continue; + }; + let line = &line[path_start..]; + let Some(path_len) = line.iter().position(u8::is_ascii_whitespace) else { + continue; + }; + let path = &line[..path_len]; + let line = &line[path_len..]; + + let Some(fstype_start) = line.iter().position(|b| !b.is_ascii_whitespace()) else { + continue; + }; + let line = &line[fstype_start..]; + let Some(fstype_len) = line.iter().position(u8::is_ascii_whitespace) else { + continue; + }; + let fstype = &line[..fstype_len]; + + let Some(path) = decode_mtab_str(path) else { + continue; + }; + if path != self.mountpoint.as_os_str() + || !(fstype == b"fuse" + || fstype == b"fuseblk" + || fstype.starts_with(b"fuse.") + || fstype.starts_with(b"fuseblk.")) + { + continue; + } + + return Ok(true); + } + + Ok(false) + } + + #[cfg(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + ))] + fn should_auto_unmount(&self) -> io::Result { + let count = unsafe { nix::libc::getfsstat(std::ptr::null_mut(), 0, nix::libc::MNT_WAIT) }; + if count < 0 { + return Err(io::Error::last_os_error()); + } + + let mut buf = Vec::with_capacity(count as usize); + let bufsize = std::mem::size_of::() * (count as usize); + let count = + unsafe { nix::libc::getfsstat(buf.as_mut_ptr(), bufsize as _, nix::libc::MNT_WAIT) }; + if count < 0 { + return Err(io::Error::last_os_error()); + } + unsafe { + buf.set_len(count as usize); + } + + for mnt in &buf { + if unsafe { c_str_eq(mnt.f_fstypename.as_ptr(), b"fusefs") } + && unsafe { c_str_eq(mnt.f_mntfromname.as_ptr(), DEV_FUSE) } + && unsafe { + c_str_eq( + mnt.f_mntonname.as_ptr(), + self.mountpoint.as_os_str().as_encoded_bytes(), + ) + } + { + return Ok(true); + } + } + + Ok(false) + } +} + +#[cfg_attr( + not(any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + )), + expect(dead_code) +)] +unsafe fn c_str_eq(c_str: *const std::ffi::c_char, s: impl AsRef<[u8]>) -> bool { + unsafe { std::ffi::CStr::from_ptr(c_str).to_bytes() == s.as_ref() } +} + +#[cfg_attr(not(target_os = "linux"), expect(dead_code))] +fn decode_mtab_str(mut s: &[u8]) -> Option { + let mut out = Vec::with_capacity(s.len()); + loop { + let Some(next_escape) = s.iter().position(|b| *b == b'\\') else { + out.extend_from_slice(s); + break; + }; + + out.extend_from_slice(&s[..next_escape]); + s = &s[(next_escape + 1)..]; + + if s.len() < 3 { + return None; + } + + let byte = (oct_digit(s[0])? << 6) | (oct_digit(s[1])? << 3) | oct_digit(s[2])?; + + out.push(byte); + + s = &s[3..]; + } + + Some(OsString::from_vec(out)) +} + +fn oct_digit(digit: u8) -> Option { + match digit { + b'0'..=b'7' => Some(digit - b'0'), + _ => None, + } +} + +fn close_inherited_fds(pipe: RawFd) { + let max_fds = getrlimit(Resource::RLIMIT_NOFILE).map_or(RawFd::MAX, |(soft, hard)| { + Ord::min(soft, hard).try_into().unwrap_or(RawFd::MAX) + }); + + let _ = redirect_stdio(); + + for fd in 3..=max_fds { + if fd != pipe { + let _ = close(fd); + } + } +} + +fn redirect_stdio() -> io::Result<()> { + let nullfd = open("/dev/null", OFlag::O_RDWR, Mode::empty())?; + + let _ = dup2_stdin(nullfd.as_fd()); + let _ = dup2_stdout(nullfd.as_fd()); + let _ = dup2_stderr(nullfd.as_fd()); + + Ok(()) +} diff --git a/src/mnt/mod.rs b/src/mnt/mod.rs index e68ef256..f7534d0e 100644 --- a/src/mnt/mod.rs +++ b/src/mnt/mod.rs @@ -13,6 +13,10 @@ mod fuse3_sys; #[cfg(fuser_mount_impl = "pure-rust")] mod fuse_pure; + +#[cfg(fuser_mount_impl = "direct-mount")] +mod fuse_direct; + pub(crate) mod mount_options; use std::io; @@ -65,6 +69,8 @@ use crate::SessionACL; #[derive(Debug)] enum MountImpl { + #[cfg(fuser_mount_impl = "direct-mount")] + Direct(fuse_direct::MountImpl), #[cfg(fuser_mount_impl = "pure-rust")] Pure(fuse_pure::MountImpl), #[cfg(fuser_mount_impl = "libfuse2")] @@ -76,6 +82,8 @@ enum MountImpl { impl MountImpl { fn umount_impl(&mut self) -> io::Result<()> { match self { + #[cfg(fuser_mount_impl = "direct-mount")] + MountImpl::Direct(mount) => mount.umount_impl(), #[cfg(fuser_mount_impl = "pure-rust")] MountImpl::Pure(mount) => mount.umount_impl(), #[cfg(fuser_mount_impl = "libfuse2")] @@ -101,6 +109,17 @@ impl Mount { options: &[MountOption], acl: SessionACL, ) -> io::Result<(Arc, Mount)> { + #[cfg(fuser_mount_impl = "direct-mount")] + { + let (dev_fuse, mount) = fuse_direct::MountImpl::new(mountpoint, options, acl)?; + Ok(( + dev_fuse, + Mount { + mount_impl: Some(MountImpl::Direct(mount)), + mount_point: mountpoint.to_path_buf(), + }, + )) + } #[cfg(fuser_mount_impl = "pure-rust")] { let (dev_fuse, mount) = fuse_pure::MountImpl::new(mountpoint, options, acl)?; @@ -165,7 +184,10 @@ 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 = "direct-mount"), + expect(dead_code) +)] fn libc_umount(mnt: &CStr) -> nix::Result<()> { #[cfg(any( target_os = "macos", @@ -192,7 +214,10 @@ fn libc_umount(mnt: &CStr) -> nix::Result<()> { /// 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"))] +#[cfg(any( + all(not(target_os = "macos"), test), + any(fuser_mount_impl = "pure-rust", fuser_mount_impl = "direct-mount") +))] fn is_mounted(fuse_device: &DevFuse) -> bool { use std::os::unix::io::AsFd; use std::slice;