From 11a0fa3c10998a7f106d0dc5aa1e7203af364f41 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Tue, 3 Feb 2026 23:03:26 -0500 Subject: [PATCH 01/16] implemented direct mounting without libfuse or fusermount --- Cargo.toml | 1 + build.rs | 8 +- src/mnt/fuse_direct.rs | 363 +++++++++++++++++++++++++++++++++++++++ src/mnt/mod.rs | 24 ++- src/mnt/mount_options.rs | 5 +- 5 files changed, 397 insertions(+), 4 deletions(-) create mode 100644 src/mnt/fuse_direct.rs diff --git a/Cargo.toml b/Cargo.toml index 3e448de6..173b4b25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,6 +59,7 @@ libfuse2 = ["libfuse"] libfuse3 = ["libfuse"] serializable = ["serde"] macfuse-4-compat = [] +direct-mount = ["nix/resource", "nix/signal"] # abi-7-xx feature flags are deprecated and don't do anything. abi-7-20 = [] abi-7-21 = [] 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/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs new file mode 100644 index 00000000..36a14933 --- /dev/null +++ b/src/mnt/fuse_direct.rs @@ -0,0 +1,363 @@ +use std::fs::File; +use std::io; +use std::io::Write; +use std::os::fd::AsRawFd; +use std::os::fd::RawFd; +use std::os::fd::AsFd; +use std::os::unix::fs::MetadataExt; +use std::os::unix::net::UnixStream; +use std::path::Path; +use std::path::PathBuf; +use std::sync::Arc; +use std::process::exit; +use std::io::Read; + +use nix::mount::MntFlags; +use nix::mount::MsFlags; +use nix::mount::mount; +use nix::mount::umount2; +use nix::sys::resource::Resource; +use nix::sys::resource::getrlimit; +use nix::sys::stat::SFlag; +use nix::unistd::Gid; +use nix::unistd::SysconfVar; +use nix::unistd::Uid; +use nix::unistd::User; +use nix::unistd::sysconf; +use nix::unistd::close; +use nix::unistd::dup2_stdin; +use nix::unistd::dup2_stdout; +use nix::unistd::dup2_stderr; +use nix::fcntl::open; +use nix::fcntl::OFlag; +use nix::sys::stat::Mode; +use nix::unistd::fork; +use nix::unistd::ForkResult; +use nix::unistd::setsid; +use nix::sys::signal::SigSet; +use nix::sys::signal::SigmaskHow; +use nix::sys::signal::sigprocmask; + +use log::error; +use log::warn; + +use crate::SessionACL; +use crate::dev_fuse::DevFuse; +use crate::mnt::mount_options::MountOption; + +const DEV_FUSE: &'static str = "/dev/fuse"; + +#[derive(Debug)] +pub(crate) struct MountImpl { + mountpoint: PathBuf, + auto_unmount_socket: Option, +} + +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 gid = Gid::current(); + + let mut fsname: Option<&str> = None; + let mut subtype: Option<&str> = None; + let mut blkdev = false; + let mut auto_unmount = false; + let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV; + + let mut opts = Vec::new(); + for opt in options { + match opt { + MountOption::FSName(val) => fsname = Some(val), + MountOption::Subtype(val) => subtype = Some(val), + MountOption::CUSTOM(val) if val == "blkdev" => { + if !uid.is_root() { + return Err(io::ErrorKind::PermissionDenied.into()); + } + blkdev = true; + } + MountOption::AutoUnmount => auto_unmount = true, + MountOption::RW => flags &= !MsFlags::MS_RDONLY, + MountOption::RO => flags |= MsFlags::MS_RDONLY, + MountOption::Suid if uid.is_root() => flags &= !MsFlags::MS_NOSUID, + MountOption::Suid => warn!("unsafe mount option 'suid' ignored"), + MountOption::NoSuid => flags |= MsFlags::MS_NOSUID, + MountOption::Dev if uid.is_root() => flags &= !MsFlags::MS_NODEV, + MountOption::Dev => warn!("unsafe mount option 'nodev' ignored"), + MountOption::NoDev => flags |= MsFlags::MS_NODEV, + MountOption::Exec => flags &= !MsFlags::MS_NOEXEC, + MountOption::NoExec => flags |= MsFlags::MS_NOEXEC, + MountOption::Async => flags &= !MsFlags::MS_SYNCHRONOUS, + MountOption::Sync => flags |= MsFlags::MS_SYNCHRONOUS, + MountOption::Atime => flags &= !MsFlags::MS_NOATIME, + MountOption::NoAtime => flags |= !MsFlags::MS_NOATIME, + MountOption::CUSTOM(val) if val == "diratime" => flags &= !MsFlags::MS_NODIRATIME, + MountOption::CUSTOM(val) if val == "nodiratime" => flags |= MsFlags::MS_NODIRATIME, + MountOption::CUSTOM(val) if val == "lazytime" => flags |= MsFlags::MS_LAZYTIME, + MountOption::CUSTOM(val) if val == "nolazytime" => flags &= !MsFlags::MS_LAZYTIME, + MountOption::CUSTOM(val) if val == "relatime" => flags |= MsFlags::MS_RELATIME, + MountOption::CUSTOM(val) if val == "norelatime" => flags &= !MsFlags::MS_RELATIME, + MountOption::CUSTOM(val) if val == "strictatime" => { + flags |= MsFlags::MS_STRICTATIME + } + MountOption::CUSTOM(val) if val == "nostrictatime" => { + flags &= !MsFlags::MS_STRICTATIME + } + MountOption::DirSync => flags |= MsFlags::MS_DIRSYNC, + MountOption::DefaultPermissions => write!(opts, "default_permissions,")?, + MountOption::CUSTOM(val) + if val == "allow_other" + || val.starts_with("max_read=") + || val.starts_with("blksize=") => + { + write!(opts, "{val}")? + } + MountOption::CUSTOM(val) => { + error!("invalid mount option '{val}'"); + return Err(nix::Error::EINVAL.into()); + } + } + } + + if flags.contains(MsFlags::MS_RDONLY) { + write!(opts, "ro,")?; + } else { + write!(opts, "rw,")?; + } + if flags.contains(MsFlags::MS_NOSUID) { + write!(opts, "nosuid,")?; + } + if flags.contains(MsFlags::MS_NODEV) { + write!(opts, "nodev,")?; + } + if flags.contains(MsFlags::MS_NOEXEC) { + write!(opts, "noexec,")?; + } + if flags.contains(MsFlags::MS_SYNCHRONOUS) { + write!(opts, "sync,")?; + } + if flags.contains(MsFlags::MS_NOATIME) { + write!(opts, "noatime,")?; + } + if flags.contains(MsFlags::MS_NODIRATIME) { + write!(opts, "nodiratime,")?; + } + if flags.contains(MsFlags::MS_LAZYTIME) { + write!(opts, "lazytime,")?; + } + if flags.contains(MsFlags::MS_RELATIME) { + write!(opts, "relatime,")?; + } + if flags.contains(MsFlags::MS_STRICTATIME) { + write!(opts, "strictatime,")?; + } + if flags.contains(MsFlags::MS_DIRSYNC) { + write!(opts, "dirsync,")?; + } + + if !uid.is_root() { + if let Some(user) = User::from_uid(uid)? { + write!(opts, "user={},", user.name)?; + } + } + + if let Some(opt) = acl.to_mount_option() { + write!(opts, "{opt}")?; + } + + let root_mode = mountpoint + .metadata() + .map(|meta| meta.mode() & SFlag::S_IFMT.bits())?; + + let old_len = opts.len(); + write!( + opts, + "fd={},rootmode={},user_id={},group_id={}", + dev_fd, + root_mode, + uid.as_raw(), + gid.as_raw(), + )?; + + let mut ty = match (subtype, blkdev) { + (None, false) => "fuse".into(), + (None, true) => "fuseblk".into(), + (Some(subtype), false) => format!("fuse.{subtype}"), + (Some(subtype), true) => format!("fuseblk.{subtype}"), + }; + + let mut source = if let Some(fsname) = fsname { + fsname + } else if let Some(subtype) = subtype { + subtype + } else { + DEV_FUSE + }; + + let pagesize = sysconf(SysconfVar::PAGE_SIZE)? + .map_or(usize::MAX, |ps| ps.try_into().unwrap_or(usize::MAX)) + - 1; + + if opts.len() > pagesize { + error!( + "mount options too long: '{}'", + String::from_utf8_lossy(&opts) + ); + return Err(nix::Error::EINVAL.into()); + } + + let mut res = 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 = (if blkdev { "fuseblk" } else { "fuse" }).into(); + source_tmp = match (fsname, blkdev) { + (Some(fsname), false) => format!("{subtype}#{fsname}"), + (Some(_), true) => source.into(), + _ => ty.clone(), + }; + source = source_tmp.as_str(); + + res = 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={},user_id={}", + dev_fd, + root_mode, + uid.as_raw(), + )?; + + res = mount( + Some(source), + &mountpoint, + Some(ty.as_str()), + flags, + Some(opts.as_slice()), + ); + } + res.inspect_err(|err| error!("mount failed: {err}"))?; + + let mut mnt = MountImpl { + mountpoint, + auto_unmount_socket: None, + }; + + if auto_unmount { + mnt.setup_auto_unmount()?; + } + + Ok((dev, mnt)) + } + + pub(crate) fn umount_impl(&mut self) -> io::Result<()> { + self.do_unmount(true) + } + + fn do_unmount(&mut self, lazy: bool) -> io::Result<()> { + let flags = if lazy { + MntFlags::MNT_DETACH + } else { + MntFlags::empty() + }; + umount2(&self.mountpoint, flags)?; + Ok(()) + } + + fn setup_auto_unmount(&mut self) -> io::Result<()> { + let (tx, rx) = UnixStream::pair()?; + + if let ForkResult::Child = unsafe { fork() }? { + exit(match self.do_auto_unmount(rx) { + Ok(()) => 0, + Err(err) => err.raw_os_error().unwrap_or(1), + }); + } + + self.auto_unmount_socket = Some(tx); + + 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::empty()), + 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(()) + } + + fn should_auto_unmount(&self) -> io::Result { + todo!() + } +} + +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 2eefcd63..112f96dc 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) -> io::Result<()> { #[cfg(any( target_os = "macos", diff --git a/src/mnt/mount_options.rs b/src/mnt/mount_options.rs index 1e18ee05..d040262b 100644 --- a/src/mnt/mount_options.rs +++ b/src/mnt/mount_options.rs @@ -137,7 +137,10 @@ fn conflicts_with(option: &MountOption) -> Vec { } // Format option to be passed to libfuse or kernel -#[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) +)] pub(crate) fn option_to_string(option: &MountOption) -> String { match option { MountOption::FSName(name) => format!("fsname={name}"), From e56eaac2b35b95f81aec39d59a8eaf952f63fbf8 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Tue, 3 Feb 2026 23:45:21 -0500 Subject: [PATCH 02/16] fixed mount options in fuse_direct.rs --- src/mnt/fuse_direct.rs | 105 ++++++++++------------------------------- 1 file changed, 26 insertions(+), 79 deletions(-) diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs index 36a14933..c631ac49 100644 --- a/src/mnt/fuse_direct.rs +++ b/src/mnt/fuse_direct.rs @@ -1,51 +1,49 @@ use std::fs::File; use std::io; +use std::io::Read; use std::io::Write; +use std::os::fd::AsFd; use std::os::fd::AsRawFd; use std::os::fd::RawFd; -use std::os::fd::AsFd; use std::os::unix::fs::MetadataExt; use std::os::unix::net::UnixStream; use std::path::Path; use std::path::PathBuf; -use std::sync::Arc; use std::process::exit; -use std::io::Read; +use std::sync::Arc; +use log::error; +use log::warn; +use nix::fcntl::OFlag; +use nix::fcntl::open; use nix::mount::MntFlags; use nix::mount::MsFlags; use nix::mount::mount; use nix::mount::umount2; 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::stat::SFlag; +use nix::unistd::ForkResult; use nix::unistd::Gid; use nix::unistd::SysconfVar; use nix::unistd::Uid; -use nix::unistd::User; -use nix::unistd::sysconf; use nix::unistd::close; +use nix::unistd::dup2_stderr; use nix::unistd::dup2_stdin; use nix::unistd::dup2_stdout; -use nix::unistd::dup2_stderr; -use nix::fcntl::open; -use nix::fcntl::OFlag; -use nix::sys::stat::Mode; use nix::unistd::fork; -use nix::unistd::ForkResult; use nix::unistd::setsid; -use nix::sys::signal::SigSet; -use nix::sys::signal::SigmaskHow; -use nix::sys::signal::sigprocmask; - -use log::error; -use log::warn; +use nix::unistd::sysconf; use crate::SessionACL; use crate::dev_fuse::DevFuse; use crate::mnt::mount_options::MountOption; -const DEV_FUSE: &'static str = "/dev/fuse"; +const DEV_FUSE: &str = "/dev/fuse"; #[derive(Debug)] pub(crate) struct MountImpl { @@ -115,11 +113,9 @@ impl MountImpl { MountOption::DirSync => flags |= MsFlags::MS_DIRSYNC, MountOption::DefaultPermissions => write!(opts, "default_permissions,")?, MountOption::CUSTOM(val) - if val == "allow_other" - || val.starts_with("max_read=") - || val.starts_with("blksize=") => + if val.starts_with("max_read=") || val.starts_with("blksize=") => { - write!(opts, "{val}")? + write!(opts, "{val},")? } MountOption::CUSTOM(val) => { error!("invalid mount option '{val}'"); @@ -128,50 +124,8 @@ impl MountImpl { } } - if flags.contains(MsFlags::MS_RDONLY) { - write!(opts, "ro,")?; - } else { - write!(opts, "rw,")?; - } - if flags.contains(MsFlags::MS_NOSUID) { - write!(opts, "nosuid,")?; - } - if flags.contains(MsFlags::MS_NODEV) { - write!(opts, "nodev,")?; - } - if flags.contains(MsFlags::MS_NOEXEC) { - write!(opts, "noexec,")?; - } - if flags.contains(MsFlags::MS_SYNCHRONOUS) { - write!(opts, "sync,")?; - } - if flags.contains(MsFlags::MS_NOATIME) { - write!(opts, "noatime,")?; - } - if flags.contains(MsFlags::MS_NODIRATIME) { - write!(opts, "nodiratime,")?; - } - if flags.contains(MsFlags::MS_LAZYTIME) { - write!(opts, "lazytime,")?; - } - if flags.contains(MsFlags::MS_RELATIME) { - write!(opts, "relatime,")?; - } - if flags.contains(MsFlags::MS_STRICTATIME) { - write!(opts, "strictatime,")?; - } - if flags.contains(MsFlags::MS_DIRSYNC) { - write!(opts, "dirsync,")?; - } - - if !uid.is_root() { - if let Some(user) = User::from_uid(uid)? { - write!(opts, "user={},", user.name)?; - } - } - if let Some(opt) = acl.to_mount_option() { - write!(opts, "{opt}")?; + write!(opts, "{opt},")?; } let root_mode = mountpoint @@ -181,7 +135,7 @@ impl MountImpl { let old_len = opts.len(); write!( opts, - "fd={},rootmode={},user_id={},group_id={}", + "fd={},rootmode={:o},user_id={},group_id={}", dev_fd, root_mode, uid.as_raw(), @@ -247,7 +201,7 @@ impl MountImpl { write!( opts, - "fd={},rootmode={},user_id={}", + "fd={},rootmode={:o},user_id={}", dev_fd, root_mode, uid.as_raw(), @@ -307,18 +261,14 @@ impl MountImpl { 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::empty()), - None, - ); + let _ = sigprocmask(SigmaskHow::SIG_BLOCK, Some(&SigSet::empty()), None); let mut buf = [0u8; 16]; loop { match pipe.read(&mut buf) { Ok(0) => break, - Ok(_) => {}, - Err(err) if err.kind() == io::ErrorKind::Interrupted => {}, + Ok(_) => {} + Err(err) if err.kind() == io::ErrorKind::Interrupted => {} _ => break, } } @@ -336,12 +286,9 @@ impl MountImpl { } 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 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(); From 7c4565453ce3103dc3445d5e3c4ab12c5ac1b75d Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sat, 7 Feb 2026 13:51:26 -0500 Subject: [PATCH 03/16] implemented 'should_auto_unmount' and fixed 'NoAtime' option handling --- src/mnt/fuse_direct.rs | 98 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 96 insertions(+), 2 deletions(-) diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs index c631ac49..9d01af5e 100644 --- a/src/mnt/fuse_direct.rs +++ b/src/mnt/fuse_direct.rs @@ -1,10 +1,13 @@ +use std::ffi::OsString; use std::fs::File; use std::io; +use std::io::BufRead; use std::io::Read; use std::io::Write; 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::fs::MetadataExt; use std::os::unix::net::UnixStream; use std::path::Path; @@ -97,7 +100,7 @@ impl MountImpl { MountOption::Async => flags &= !MsFlags::MS_SYNCHRONOUS, MountOption::Sync => flags |= MsFlags::MS_SYNCHRONOUS, MountOption::Atime => flags &= !MsFlags::MS_NOATIME, - MountOption::NoAtime => flags |= !MsFlags::MS_NOATIME, + MountOption::NoAtime => flags |= MsFlags::MS_NOATIME, MountOption::CUSTOM(val) if val == "diratime" => flags &= !MsFlags::MS_NODIRATIME, MountOption::CUSTOM(val) if val == "nodiratime" => flags |= MsFlags::MS_NODIRATIME, MountOption::CUSTOM(val) if val == "lazytime" => flags |= MsFlags::MS_LAZYTIME, @@ -281,7 +284,98 @@ impl MountImpl { } fn should_auto_unmount(&self) -> io::Result { - todo!() + let etc_mtab = Path::new("/etc/mtab"); + let proc_mounts = Path::new("/proc/mounts"); + + let mtab_path = if etc_mtab.try_exists()? { + etc_mtab + } else if proc_mounts.try_exists()? { + proc_mounts + } 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) + } +} + +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, } } From 12dadcff5c221086cd4bd260641d8b6fd397e4d5 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sat, 7 Feb 2026 21:13:14 -0500 Subject: [PATCH 04/16] refactor direct-mount option handling to reuse functions from pure-rust impl and removed handling of CUSTOM options --- src/mnt/fuse_direct.rs | 94 +++++++++++++------------------------- src/mnt/fuse_pure.rs | 80 ++------------------------------ src/mnt/mount_options.rs | 98 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 128 insertions(+), 144 deletions(-) diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs index 9d01af5e..476d1e12 100644 --- a/src/mnt/fuse_direct.rs +++ b/src/mnt/fuse_direct.rs @@ -16,7 +16,6 @@ use std::process::exit; use std::sync::Arc; use log::error; -use log::warn; use nix::fcntl::OFlag; use nix::fcntl::open; use nix::mount::MntFlags; @@ -45,6 +44,10 @@ use nix::unistd::sysconf; use crate::SessionACL; use crate::dev_fuse::DevFuse; 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"; @@ -71,59 +74,29 @@ impl MountImpl { let mut fsname: Option<&str> = None; let mut subtype: Option<&str> = None; - let mut blkdev = false; let mut auto_unmount = false; - let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV; + let mut flags = MsFlags::empty(); + + if !uid.is_root() || !options.contains(&MountOption::Dev) { + // Default to nodev + flags |= MsFlags::MS_NODEV; + } + if !uid.is_root() || !options.contains(&MountOption::Suid) { + // default to nosuid + flags |= MsFlags::MS_NOSUID; + } let mut opts = Vec::new(); for opt in options { - match opt { - MountOption::FSName(val) => fsname = Some(val), - MountOption::Subtype(val) => subtype = Some(val), - MountOption::CUSTOM(val) if val == "blkdev" => { - if !uid.is_root() { - return Err(io::ErrorKind::PermissionDenied.into()); - } - blkdev = true; - } - MountOption::AutoUnmount => auto_unmount = true, - MountOption::RW => flags &= !MsFlags::MS_RDONLY, - MountOption::RO => flags |= MsFlags::MS_RDONLY, - MountOption::Suid if uid.is_root() => flags &= !MsFlags::MS_NOSUID, - MountOption::Suid => warn!("unsafe mount option 'suid' ignored"), - MountOption::NoSuid => flags |= MsFlags::MS_NOSUID, - MountOption::Dev if uid.is_root() => flags &= !MsFlags::MS_NODEV, - MountOption::Dev => warn!("unsafe mount option 'nodev' ignored"), - MountOption::NoDev => flags |= MsFlags::MS_NODEV, - MountOption::Exec => flags &= !MsFlags::MS_NOEXEC, - MountOption::NoExec => flags |= MsFlags::MS_NOEXEC, - MountOption::Async => flags &= !MsFlags::MS_SYNCHRONOUS, - MountOption::Sync => flags |= MsFlags::MS_SYNCHRONOUS, - MountOption::Atime => flags &= !MsFlags::MS_NOATIME, - MountOption::NoAtime => flags |= MsFlags::MS_NOATIME, - MountOption::CUSTOM(val) if val == "diratime" => flags &= !MsFlags::MS_NODIRATIME, - MountOption::CUSTOM(val) if val == "nodiratime" => flags |= MsFlags::MS_NODIRATIME, - MountOption::CUSTOM(val) if val == "lazytime" => flags |= MsFlags::MS_LAZYTIME, - MountOption::CUSTOM(val) if val == "nolazytime" => flags &= !MsFlags::MS_LAZYTIME, - MountOption::CUSTOM(val) if val == "relatime" => flags |= MsFlags::MS_RELATIME, - MountOption::CUSTOM(val) if val == "norelatime" => flags &= !MsFlags::MS_RELATIME, - MountOption::CUSTOM(val) if val == "strictatime" => { - flags |= MsFlags::MS_STRICTATIME - } - MountOption::CUSTOM(val) if val == "nostrictatime" => { - flags &= !MsFlags::MS_STRICTATIME - } - MountOption::DirSync => flags |= MsFlags::MS_DIRSYNC, - MountOption::DefaultPermissions => write!(opts, "default_permissions,")?, - MountOption::CUSTOM(val) - if val.starts_with("max_read=") || val.starts_with("blksize=") => - { - write!(opts, "{val},")? - } - MountOption::CUSTOM(val) => { - error!("invalid mount option '{val}'"); - return Err(nix::Error::EINVAL.into()); - } + match option_group(opt) { + MountOptionGroup::KernelFlag => flags |= option_to_flag(opt)?, + MountOptionGroup::KernelOption => write!(opts, "{},", option_to_string(opt))?, + MountOptionGroup::Fusermount => match opt { + MountOption::FSName(val) => fsname = Some(val), + MountOption::Subtype(val) => subtype = Some(val), + MountOption::AutoUnmount => auto_unmount = true, + _ => {} + }, } } @@ -145,12 +118,7 @@ impl MountImpl { gid.as_raw(), )?; - let mut ty = match (subtype, blkdev) { - (None, false) => "fuse".into(), - (None, true) => "fuseblk".into(), - (Some(subtype), false) => format!("fuse.{subtype}"), - (Some(subtype), true) => format!("fuseblk.{subtype}"), - }; + let mut ty = subtype.map_or("fuse".into(), |subtype| format!("fuse.{subtype}")); let mut source = if let Some(fsname) = fsname { fsname @@ -182,13 +150,13 @@ impl MountImpl { let source_tmp; if let Err(nix::Error::ENODEV) = &res { if let Some(subtype) = subtype { - ty = (if blkdev { "fuseblk" } else { "fuse" }).into(); - source_tmp = match (fsname, blkdev) { - (Some(fsname), false) => format!("{subtype}#{fsname}"), - (Some(_), true) => source.into(), - _ => ty.clone(), - }; - source = source_tmp.as_str(); + ty = "fuse".into(); + if let Some(fsname) = fsname { + source_tmp = format!("{subtype}#{fsname}"); + source = source_tmp.as_str(); + } else { + source = ty.as_str(); + } res = mount( Some(source), diff --git a/src/mnt/fuse_pure.rs b/src/mnt/fuse_pure.rs index b732ce98..95fc43d6 100644 --- a/src/mnt/fuse_pure.rs +++ b/src/mnt/fuse_pure.rs @@ -42,6 +42,9 @@ 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"; @@ -504,83 +507,6 @@ fn fuse_mount_sys( Ok(None) } -#[cfg(any(target_os = "linux", target_os = "macos"))] -#[derive(PartialEq)] -pub(crate) enum MountOptionGroup { - KernelOption, - KernelFlag, - Fusermount, -} - -#[cfg(any(target_os = "linux", target_os = "macos"))] -pub(crate) fn option_group(option: &MountOption) -> MountOptionGroup { - match option { - MountOption::FSName(_) => MountOptionGroup::Fusermount, - MountOption::Subtype(_) => MountOptionGroup::Fusermount, - MountOption::CUSTOM(_) => MountOptionGroup::KernelOption, - MountOption::AutoUnmount => MountOptionGroup::Fusermount, - MountOption::Dev => MountOptionGroup::KernelFlag, - MountOption::NoDev => MountOptionGroup::KernelFlag, - MountOption::Suid => MountOptionGroup::KernelFlag, - MountOption::NoSuid => MountOptionGroup::KernelFlag, - MountOption::RO => MountOptionGroup::KernelFlag, - MountOption::RW => MountOptionGroup::KernelFlag, - MountOption::Exec => MountOptionGroup::KernelFlag, - MountOption::NoExec => MountOptionGroup::KernelFlag, - MountOption::Atime => MountOptionGroup::KernelFlag, - MountOption::NoAtime => MountOptionGroup::KernelFlag, - MountOption::DirSync => MountOptionGroup::KernelFlag, - MountOption::Sync => MountOptionGroup::KernelFlag, - MountOption::Async => MountOptionGroup::KernelFlag, - MountOption::DefaultPermissions => MountOptionGroup::KernelOption, - } -} - -#[cfg(target_os = "linux")] -pub(crate) fn option_to_flag(option: &MountOption) -> io::Result { - match option { - MountOption::Dev => Ok(nix::mount::MsFlags::empty()), // There is no option for dev. It's the absence of NoDev - MountOption::NoDev => Ok(nix::mount::MsFlags::MS_NODEV), - MountOption::Suid => Ok(nix::mount::MsFlags::empty()), - MountOption::NoSuid => Ok(nix::mount::MsFlags::MS_NOSUID), - MountOption::RW => Ok(nix::mount::MsFlags::empty()), - MountOption::RO => Ok(nix::mount::MsFlags::MS_RDONLY), - MountOption::Exec => Ok(nix::mount::MsFlags::empty()), - MountOption::NoExec => Ok(nix::mount::MsFlags::MS_NOEXEC), - MountOption::Atime => Ok(nix::mount::MsFlags::empty()), - MountOption::NoAtime => Ok(nix::mount::MsFlags::MS_NOATIME), - MountOption::Async => Ok(nix::mount::MsFlags::empty()), - MountOption::Sync => Ok(nix::mount::MsFlags::MS_SYNCHRONOUS), - MountOption::DirSync => Ok(nix::mount::MsFlags::MS_DIRSYNC), - option => Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("Invalid mount option for flag conversion: {option:?}"), - )), - } -} - -#[cfg(target_os = "macos")] -pub(crate) fn option_to_flag(option: &MountOption) -> io::Result { - match option { - MountOption::Dev => Ok(nix::mount::MntFlags::empty()), // There is no option for dev. It's the absence of NoDev - MountOption::NoDev => Ok(nix::mount::MntFlags::MNT_NODEV), - MountOption::Suid => Ok(nix::mount::MntFlags::empty()), - MountOption::NoSuid => Ok(nix::mount::MntFlags::MNT_NOSUID), - MountOption::RW => Ok(nix::mount::MntFlags::empty()), - MountOption::RO => Ok(nix::mount::MntFlags::MNT_RDONLY), - MountOption::Exec => Ok(nix::mount::MntFlags::empty()), - MountOption::NoExec => Ok(nix::mount::MntFlags::MNT_NOEXEC), - MountOption::Atime => Ok(nix::mount::MntFlags::empty()), - MountOption::NoAtime => Ok(nix::mount::MntFlags::MNT_NOATIME), - MountOption::Async => Ok(nix::mount::MntFlags::empty()), - MountOption::Sync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS), - option => Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("Invalid mount option for flag conversion: {option:?}"), - )), - } -} - #[cfg_attr( any( target_os = "freebsd", diff --git a/src/mnt/mount_options.rs b/src/mnt/mount_options.rs index d040262b..37ec9a3a 100644 --- a/src/mnt/mount_options.rs +++ b/src/mnt/mount_options.rs @@ -69,6 +69,14 @@ pub enum MountOption { to libfuse, and not part of the kernel ABI */ } +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[derive(PartialEq)] +pub(crate) enum MountOptionGroup { + KernelOption, + KernelFlag, + Fusermount, +} + impl MountOption { pub(crate) fn from_str(s: &str) -> MountOption { match s { @@ -137,10 +145,7 @@ fn conflicts_with(option: &MountOption) -> Vec { } // Format option to be passed to libfuse or kernel -#[cfg_attr( - any(fuser_mount_impl = "macos-no-mount", fuser_mount_impl = "direct-mount"), - expect(dead_code) -)] +#[cfg_attr(fuser_mount_impl = "macos-no-mount", expect(dead_code))] pub(crate) fn option_to_string(option: &MountOption) -> String { match option { MountOption::FSName(name) => format!("fsname={name}"), @@ -164,6 +169,91 @@ pub(crate) fn option_to_string(option: &MountOption) -> String { } } +#[cfg(any(target_os = "linux", target_os = "macos"))] +#[cfg_attr( + not(any( + fuser_mount_impl = "macos-no-mount", + fuser_mount_impl = "pure-rust", + fuser_mount_impl = "direct-mount", + )), + expect(dead_code) +)] +pub(crate) fn option_group(option: &MountOption) -> MountOptionGroup { + match option { + MountOption::FSName(_) => MountOptionGroup::Fusermount, + MountOption::Subtype(_) => MountOptionGroup::Fusermount, + MountOption::CUSTOM(_) => MountOptionGroup::KernelOption, + MountOption::AutoUnmount => MountOptionGroup::Fusermount, + MountOption::Dev => MountOptionGroup::KernelFlag, + MountOption::NoDev => MountOptionGroup::KernelFlag, + MountOption::Suid => MountOptionGroup::KernelFlag, + MountOption::NoSuid => MountOptionGroup::KernelFlag, + MountOption::RO => MountOptionGroup::KernelFlag, + MountOption::RW => MountOptionGroup::KernelFlag, + MountOption::Exec => MountOptionGroup::KernelFlag, + MountOption::NoExec => MountOptionGroup::KernelFlag, + MountOption::Atime => MountOptionGroup::KernelFlag, + MountOption::NoAtime => MountOptionGroup::KernelFlag, + MountOption::DirSync => MountOptionGroup::KernelFlag, + MountOption::Sync => MountOptionGroup::KernelFlag, + MountOption::Async => MountOptionGroup::KernelFlag, + MountOption::DefaultPermissions => MountOptionGroup::KernelOption, + } +} + +#[cfg(target_os = "linux")] +#[cfg_attr( + not(any( + fuser_mount_impl = "macos-no-mount", + fuser_mount_impl = "pure-rust", + fuser_mount_impl = "direct-mount", + )), + expect(dead_code) +)] +pub(crate) fn option_to_flag(option: &MountOption) -> io::Result { + match option { + MountOption::Dev => Ok(nix::mount::MsFlags::empty()), // There is no option for dev. It's the absence of NoDev + MountOption::NoDev => Ok(nix::mount::MsFlags::MS_NODEV), + MountOption::Suid => Ok(nix::mount::MsFlags::empty()), + MountOption::NoSuid => Ok(nix::mount::MsFlags::MS_NOSUID), + MountOption::RW => Ok(nix::mount::MsFlags::empty()), + MountOption::RO => Ok(nix::mount::MsFlags::MS_RDONLY), + MountOption::Exec => Ok(nix::mount::MsFlags::empty()), + MountOption::NoExec => Ok(nix::mount::MsFlags::MS_NOEXEC), + MountOption::Atime => Ok(nix::mount::MsFlags::empty()), + MountOption::NoAtime => Ok(nix::mount::MsFlags::MS_NOATIME), + MountOption::Async => Ok(nix::mount::MsFlags::empty()), + MountOption::Sync => Ok(nix::mount::MsFlags::MS_SYNCHRONOUS), + MountOption::DirSync => Ok(nix::mount::MsFlags::MS_DIRSYNC), + option => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid mount option for flag conversion: {option:?}"), + )), + } +} + +#[cfg(target_os = "macos")] +pub(crate) fn option_to_flag(option: &MountOption) -> io::Result { + match option { + MountOption::Dev => Ok(nix::mount::MntFlags::empty()), // There is no option for dev. It's the absence of NoDev + MountOption::NoDev => Ok(nix::mount::MntFlags::MNT_NODEV), + MountOption::Suid => Ok(nix::mount::MntFlags::empty()), + MountOption::NoSuid => Ok(nix::mount::MntFlags::MNT_NOSUID), + MountOption::RW => Ok(nix::mount::MntFlags::empty()), + MountOption::RO => Ok(nix::mount::MntFlags::MNT_RDONLY), + MountOption::Exec => Ok(nix::mount::MntFlags::empty()), + MountOption::NoExec => Ok(nix::mount::MntFlags::MNT_NOEXEC), + MountOption::Atime => Ok(nix::mount::MntFlags::empty()), + MountOption::NoAtime => Ok(nix::mount::MntFlags::MNT_NOATIME), + MountOption::Async => Ok(nix::mount::MntFlags::empty()), + MountOption::Sync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS), + option => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid mount option for flag conversion: {option:?}"), + )), + } +} + /// Parses mount command args. /// /// Input: `"-o", "suid", "-o", "ro,nodev,noexec", "-osync"` From 37bf52fe13c80fb153c26e2f49452f64347ed5c3 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sat, 7 Feb 2026 21:51:01 -0500 Subject: [PATCH 05/16] added direct-mount tests to Linux and BSD suites --- fuser-tests/src/commands/bsd_mount.rs | 13 ++++++++++++- fuser-tests/src/commands/mount.rs | 17 +++++++++++++++++ fuser-tests/src/features.rs | 3 +++ 3 files changed, 32 insertions(+), 1 deletion(-) 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 09975f13..85e82909 100644 --- a/fuser-tests/src/commands/mount.rs +++ b/fuser-tests/src/commands/mount.rs @@ -44,6 +44,23 @@ async fn run_mount_tests_inner(libfuse: Libfuse) -> anyhow::Result<()> { run_test(&[], Unmount::Auto, libfuse.fusermount(), 1).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, + ) + .await?; + run_test( + &[Feature::DirectMount], + Unmount::Auto, + libfuse.fusermount(), + 1, + ) + .await?; + test_no_user_allow_other(&[Feature::DirectMount], &libfuse).await?; + // Tests with libfuse run_test(&[libfuse.feature()], Unmount::Manual, Fusermount::False, 1).await?; run_test(&[libfuse.feature()], Unmount::Auto, libfuse.fusermount(), 1).await?; 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", } } } From 8d92cf887bee1130c281b6bd331894aeae7d0ff7 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sat, 7 Feb 2026 23:17:25 -0500 Subject: [PATCH 06/16] fixed typo'd tests --- fuser-tests/src/commands/mount.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/fuser-tests/src/commands/mount.rs b/fuser-tests/src/commands/mount.rs index 0a5a9ee4..b51a1645 100644 --- a/fuser-tests/src/commands/mount.rs +++ b/fuser-tests/src/commands/mount.rs @@ -50,6 +50,7 @@ async fn run_mount_tests_inner(libfuse: Libfuse) -> anyhow::Result<()> { Unmount::Manual, Fusermount::False, 1, + false, ) .await?; run_test( @@ -57,6 +58,7 @@ async fn run_mount_tests_inner(libfuse: Libfuse) -> anyhow::Result<()> { Unmount::Auto, libfuse.fusermount(), 1, + false, ) .await?; test_no_user_allow_other(&[Feature::DirectMount], &libfuse).await?; @@ -82,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, + libfuse.fusermount(), + 2, + false, + ) + .await?; + run_test( + &[Feature::DirectMount], + Unmount::Auto, + libfuse.fusermount(), + 2, + true, + ) + .await?; if let Libfuse::Libfuse3 = libfuse { run_allow_root_test() From 07c9af476c8f61a841a971cc003601fa0c57e3e8 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sat, 7 Feb 2026 23:26:42 -0500 Subject: [PATCH 07/16] removed unneeded #[cfg()] on mount option utilities --- src/mnt/mount_options.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/mnt/mount_options.rs b/src/mnt/mount_options.rs index 92401e97..66027f88 100644 --- a/src/mnt/mount_options.rs +++ b/src/mnt/mount_options.rs @@ -73,7 +73,6 @@ pub enum MountOption { to libfuse, and not part of the kernel ABI */ } -#[cfg(any(target_os = "linux", target_os = "macos"))] #[derive(PartialEq)] pub(crate) enum MountOptionGroup { KernelOption, @@ -173,7 +172,6 @@ pub(crate) fn option_to_string(option: &MountOption) -> String { } } -#[cfg(any(target_os = "linux", target_os = "macos"))] #[cfg_attr( not(any( fuser_mount_impl = "macos-no-mount", @@ -205,7 +203,6 @@ pub(crate) fn option_group(option: &MountOption) -> MountOptionGroup { } } -#[cfg(target_os = "linux")] #[cfg_attr( not(any( fuser_mount_impl = "macos-no-mount", From 2bc26aa6e95a143821cef6dffe1a9dac967387c7 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sun, 8 Feb 2026 10:15:51 -0500 Subject: [PATCH 08/16] direct-mount implementation for FreeBSD --- src/mnt/fuse_direct.rs | 201 ++++++++++++++++++++++++++++++++------- src/mnt/fuse_pure.rs | 43 --------- src/mnt/mount_options.rs | 55 ++++++++++- 3 files changed, 217 insertions(+), 82 deletions(-) diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs index 476d1e12..165835ed 100644 --- a/src/mnt/fuse_direct.rs +++ b/src/mnt/fuse_direct.rs @@ -8,30 +8,21 @@ 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::fs::MetadataExt; use std::os::unix::net::UnixStream; use std::path::Path; use std::path::PathBuf; use std::process::exit; use std::sync::Arc; -use log::error; use nix::fcntl::OFlag; use nix::fcntl::open; -use nix::mount::MntFlags; -use nix::mount::MsFlags; -use nix::mount::mount; -use nix::mount::umount2; 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::stat::SFlag; use nix::unistd::ForkResult; -use nix::unistd::Gid; -use nix::unistd::SysconfVar; use nix::unistd::Uid; use nix::unistd::close; use nix::unistd::dup2_stderr; @@ -39,7 +30,6 @@ use nix::unistd::dup2_stdin; use nix::unistd::dup2_stdout; use nix::unistd::fork; use nix::unistd::setsid; -use nix::unistd::sysconf; use crate::SessionACL; use crate::dev_fuse::DevFuse; @@ -70,20 +60,56 @@ impl MountImpl { let dev_fd = dev.as_raw_fd(); let uid = Uid::current(); - let gid = Gid::current(); let mut fsname: Option<&str> = None; let mut subtype: Option<&str> = None; let mut auto_unmount = false; - let mut flags = MsFlags::empty(); + #[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 - flags |= MsFlags::MS_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 - flags |= MsFlags::MS_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; + } } let mut opts = Vec::new(); @@ -100,13 +126,60 @@ impl MountImpl { } } + Self::do_mount(&mountpoint, fsname, subtype, flags, options, acl, dev_fd)?; + + let mut mnt = MountImpl { + mountpoint, + auto_unmount_socket: None, + }; + + 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::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() & SFlag::S_IFMT.bits())?; + .map(|meta| meta.mode() & nix::sys::stat::SFlag::S_IFMT.bits())?; let old_len = opts.len(); write!( @@ -114,8 +187,8 @@ impl MountImpl { "fd={},rootmode={:o},user_id={},group_id={}", dev_fd, root_mode, - uid.as_raw(), - gid.as_raw(), + Uid::current().as_raw(), + nix::unistd::Gid::current().as_raw(), )?; let mut ty = subtype.map_or("fuse".into(), |subtype| format!("fuse.{subtype}")); @@ -128,21 +201,21 @@ impl MountImpl { DEV_FUSE }; - let pagesize = sysconf(SysconfVar::PAGE_SIZE)? + 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 { - error!( + log::error!( "mount options too long: '{}'", String::from_utf8_lossy(&opts) ); return Err(nix::Error::EINVAL.into()); } - let mut res = mount( + let mut res = nix::mount::mount( Some(source), - &mountpoint, + mountpoint, Some(ty.as_str()), flags, Some(opts.as_slice()), @@ -158,9 +231,9 @@ impl MountImpl { source = ty.as_str(); } - res = mount( + res = nix::mount::mount( Some(source), - &mountpoint, + mountpoint, Some(ty.as_str()), flags, Some(opts.as_slice()), @@ -175,42 +248,96 @@ impl MountImpl { "fd={},rootmode={:o},user_id={}", dev_fd, root_mode, - uid.as_raw(), + Uid::current().as_raw(), )?; - res = mount( + res = nix::mount::mount( Some(source), - &mountpoint, + mountpoint, Some(ty.as_str()), flags, Some(opts.as_slice()), ); } - res.inspect_err(|err| error!("mount failed: {err}"))?; + res.inspect_err(|err| log::error!("mount failed: {err}"))?; - let mut mnt = MountImpl { - mountpoint, - auto_unmount_socket: None, - }; + Ok(()) + } - if auto_unmount { - mnt.setup_auto_unmount()?; + #[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); } - Ok((dev, mnt)) + 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<()> { 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 { - MntFlags::MNT_DETACH + nix::mount::MntFlags::MNT_FORCE } else { - MntFlags::empty() + nix::mount::MntFlags::empty() }; - umount2(&self.mountpoint, flags)?; + nix::mount::unmount(&self.mountpoint, flags)?; Ok(()) } diff --git a/src/mnt/fuse_pure.rs b/src/mnt/fuse_pure.rs index 95fc43d6..e3297603 100644 --- a/src/mnt/fuse_pure.rs +++ b/src/mnt/fuse_pure.rs @@ -506,46 +506,3 @@ fn fuse_mount_sys( ) -> Result, Error> { Ok(None) } - -#[cfg_attr( - any( - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" - ), - allow(dead_code) -)] -#[cfg(any( - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "netbsd" -))] -pub(crate) fn option_to_flag(option: &MountOption) -> io::Result { - match option { - MountOption::Dev => Ok(nix::mount::MntFlags::empty()), - #[cfg(target_os = "freebsd")] - MountOption::NoDev => Err(io::Error::new( - io::ErrorKind::Unsupported, - "NoDev option is not supported on FreeBSD", - )), - #[cfg(not(target_os = "freebsd"))] - MountOption::NoDev => Ok(nix::mount::MntFlags::MNT_NODEV), - MountOption::Suid => Ok(nix::mount::MntFlags::empty()), - MountOption::NoSuid => Ok(nix::mount::MntFlags::MNT_NOSUID), - MountOption::RW => Ok(nix::mount::MntFlags::empty()), - MountOption::RO => Ok(nix::mount::MntFlags::MNT_RDONLY), - MountOption::Exec => Ok(nix::mount::MntFlags::empty()), - MountOption::NoExec => Ok(nix::mount::MntFlags::MNT_NOEXEC), - MountOption::Atime => Ok(nix::mount::MntFlags::empty()), - MountOption::NoAtime => Ok(nix::mount::MntFlags::MNT_NOATIME), - MountOption::Async => Ok(nix::mount::MntFlags::MNT_ASYNC), - MountOption::Sync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS), - MountOption::DirSync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS), - option => Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("Invalid mount option for flag conversion: {option:?}"), - )), - } -} diff --git a/src/mnt/mount_options.rs b/src/mnt/mount_options.rs index 66027f88..608d0452 100644 --- a/src/mnt/mount_options.rs +++ b/src/mnt/mount_options.rs @@ -73,6 +73,10 @@ pub enum MountOption { to libfuse, and not part of the kernel ABI */ } +#[cfg_attr( + all(fuser_mount_impl = "direct-mount", fuser_mount_impl = "macos-no-mount"), + expect(dead_code) +)] #[derive(PartialEq)] pub(crate) enum MountOptionGroup { KernelOption, @@ -174,9 +178,11 @@ pub(crate) fn option_to_string(option: &MountOption) -> String { #[cfg_attr( not(any( - fuser_mount_impl = "macos-no-mount", fuser_mount_impl = "pure-rust", - fuser_mount_impl = "direct-mount", + any( + fuser_mount_impl = "direct-mount", + not(fuser_mount_impl = "macos-no-mount") + ), )), expect(dead_code) )] @@ -203,6 +209,7 @@ pub(crate) fn option_group(option: &MountOption) -> MountOptionGroup { } } +#[cfg(target_os = "linux")] #[cfg_attr( not(any( fuser_mount_impl = "macos-no-mount", @@ -234,6 +241,7 @@ pub(crate) fn option_to_flag(option: &MountOption) -> io::Result io::Result { match option { MountOption::Dev => Ok(nix::mount::MntFlags::empty()), // There is no option for dev. It's the absence of NoDev @@ -255,6 +263,49 @@ pub(crate) fn option_to_flag(option: &MountOption) -> io::Result io::Result { + match option { + MountOption::Dev => Ok(nix::mount::MntFlags::empty()), + #[cfg(target_os = "freebsd")] + MountOption::NoDev => Err(io::Error::new( + io::ErrorKind::Unsupported, + "NoDev option is not supported on FreeBSD", + )), + #[cfg(not(target_os = "freebsd"))] + MountOption::NoDev => Ok(nix::mount::MntFlags::MNT_NODEV), + MountOption::Suid => Ok(nix::mount::MntFlags::empty()), + MountOption::NoSuid => Ok(nix::mount::MntFlags::MNT_NOSUID), + MountOption::RW => Ok(nix::mount::MntFlags::empty()), + MountOption::RO => Ok(nix::mount::MntFlags::MNT_RDONLY), + MountOption::Exec => Ok(nix::mount::MntFlags::empty()), + MountOption::NoExec => Ok(nix::mount::MntFlags::MNT_NOEXEC), + MountOption::Atime => Ok(nix::mount::MntFlags::empty()), + MountOption::NoAtime => Ok(nix::mount::MntFlags::MNT_NOATIME), + MountOption::Async => Ok(nix::mount::MntFlags::MNT_ASYNC), + MountOption::Sync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS), + MountOption::DirSync => Ok(nix::mount::MntFlags::MNT_SYNCHRONOUS), + option => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("Invalid mount option for flag conversion: {option:?}"), + )), + } +} + /// Parses mount command args. /// /// Input: `"-o", "suid", "-o", "ro,nodev,noexec", "-osync"` From 2de658c3881aa9a09012531209ce7136023cd037 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sun, 8 Feb 2026 11:00:44 -0500 Subject: [PATCH 09/16] fixed macos and freebsd lints --- src/mnt/fuse_pure.rs | 6 ++++-- src/mnt/mount_options.rs | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/mnt/fuse_pure.rs b/src/mnt/fuse_pure.rs index e3297603..94993800 100644 --- a/src/mnt/fuse_pure.rs +++ b/src/mnt/fuse_pure.rs @@ -28,7 +28,6 @@ use std::process::Stdio; use std::sync::Arc; use log::debug; -use log::error; use nix::fcntl::FcntlArg; use nix::fcntl::FdFlag; use nix::fcntl::OFlag; @@ -42,8 +41,11 @@ use crate::SessionACL; use crate::dev_fuse::DevFuse; use crate::mnt::is_mounted; use crate::mnt::mount_options::MountOption; +#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::mnt::mount_options::MountOptionGroup; +#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::mnt::mount_options::option_group; +#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::mnt::mount_options::option_to_flag; use crate::mnt::mount_options::option_to_string; @@ -395,7 +397,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/mount_options.rs b/src/mnt/mount_options.rs index 608d0452..94d040ea 100644 --- a/src/mnt/mount_options.rs +++ b/src/mnt/mount_options.rs @@ -73,10 +73,6 @@ pub enum MountOption { to libfuse, and not part of the kernel ABI */ } -#[cfg_attr( - all(fuser_mount_impl = "direct-mount", fuser_mount_impl = "macos-no-mount"), - expect(dead_code) -)] #[derive(PartialEq)] pub(crate) enum MountOptionGroup { KernelOption, @@ -178,11 +174,18 @@ pub(crate) fn option_to_string(option: &MountOption) -> String { #[cfg_attr( not(any( - fuser_mount_impl = "pure-rust", - any( - fuser_mount_impl = "direct-mount", - not(fuser_mount_impl = "macos-no-mount") + all(target_os = "linux", fuser_mount_impl = "pure-rust"), + all(target_os = "linux", fuser_mount_impl = "direct-mount"), + all( + any( + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + ), + fuser_mount_impl = "direct-mount" ), + all(target_os = "macos", fuser_mount_impl = "pure-rust"), )), expect(dead_code) )] From 15909d22a5be0dcb7783d00c1cbb1a27040a0b17 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sun, 8 Feb 2026 11:57:34 -0500 Subject: [PATCH 10/16] cleanup and fixed a few mistakes/oversights --- src/mnt/fuse_direct.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs index 165835ed..cab426e9 100644 --- a/src/mnt/fuse_direct.rs +++ b/src/mnt/fuse_direct.rs @@ -3,7 +3,6 @@ use std::fs::File; use std::io; use std::io::BufRead; use std::io::Read; -use std::io::Write; use std::os::fd::AsFd; use std::os::fd::AsRawFd; use std::os::fd::RawFd; @@ -112,17 +111,16 @@ impl MountImpl { } } - let mut opts = Vec::new(); for opt in options { match option_group(opt) { MountOptionGroup::KernelFlag => flags |= option_to_flag(opt)?, - MountOptionGroup::KernelOption => write!(opts, "{},", option_to_string(opt))?, MountOptionGroup::Fusermount => match opt { MountOption::FSName(val) => fsname = Some(val), MountOption::Subtype(val) => subtype = Some(val), MountOption::AutoUnmount => auto_unmount = true, _ => {} }, + _ => {} } } @@ -164,6 +162,7 @@ impl MountImpl { acl: SessionACL, dev_fd: RawFd, ) -> io::Result<()> { + use std::io::Write; use std::os::unix::fs::MetadataExt; let mut opts = Vec::new(); @@ -333,9 +332,9 @@ impl MountImpl { ))] fn do_unmount(&mut self, lazy: bool) -> io::Result<()> { let flags = if lazy { - nix::mount::MntFlags::MNT_FORCE - } else { nix::mount::MntFlags::empty() + } else { + nix::mount::MntFlags::MNT_FORCE }; nix::mount::unmount(&self.mountpoint, flags)?; Ok(()) @@ -359,7 +358,7 @@ impl MountImpl { 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::empty()), None); + let _ = sigprocmask(SigmaskHow::SIG_BLOCK, Some(&SigSet::all()), None); let mut buf = [0u8; 16]; loop { From c7fbf549ef2992c51b81309eabd4170f6f04e879 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sun, 8 Feb 2026 15:31:04 -0500 Subject: [PATCH 11/16] implemented BSD specific version of MountImpl::should_auto_unmount --- src/mnt/fuse_direct.rs | 63 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs index cab426e9..c48a50c4 100644 --- a/src/mnt/fuse_direct.rs +++ b/src/mnt/fuse_direct.rs @@ -1,7 +1,6 @@ use std::ffi::OsString; use std::fs::File; use std::io; -use std::io::BufRead; use std::io::Read; use std::os::fd::AsFd; use std::os::fd::AsRawFd; @@ -377,7 +376,15 @@ impl MountImpl { 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"); @@ -439,8 +446,62 @@ impl MountImpl { 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 { From fd307d2ebcb50e303ee116d568700fc8243b268f Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Sun, 8 Feb 2026 15:34:59 -0500 Subject: [PATCH 12/16] small correction to direct-mount linux test cases --- fuser-tests/src/commands/mount.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fuser-tests/src/commands/mount.rs b/fuser-tests/src/commands/mount.rs index b51a1645..77602fb7 100644 --- a/fuser-tests/src/commands/mount.rs +++ b/fuser-tests/src/commands/mount.rs @@ -87,7 +87,7 @@ async fn run_mount_tests_inner(libfuse: Libfuse) -> anyhow::Result<()> { run_test( &[Feature::DirectMount], Unmount::Auto, - libfuse.fusermount(), + Fusermount::False, 2, false, ) @@ -95,7 +95,7 @@ async fn run_mount_tests_inner(libfuse: Libfuse) -> anyhow::Result<()> { run_test( &[Feature::DirectMount], Unmount::Auto, - libfuse.fusermount(), + Fusermount::False, 2, true, ) From a17679919287435717544b1ba688f6aea8253092 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Mon, 9 Feb 2026 08:01:57 -0500 Subject: [PATCH 13/16] updated direct-mount MountImpl::unmount_impl to properly check if the the mount is still mounted --- src/mnt/fuse_direct.rs | 14 ++++++++++++++ src/mnt/mod.rs | 5 ++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs index c48a50c4..1c5d3e37 100644 --- a/src/mnt/fuse_direct.rs +++ b/src/mnt/fuse_direct.rs @@ -31,6 +31,7 @@ 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; @@ -43,6 +44,7 @@ const DEV_FUSE: &str = "/dev/fuse"; pub(crate) struct MountImpl { mountpoint: PathBuf, auto_unmount_socket: Option, + fuse_device: Arc, } impl MountImpl { @@ -128,6 +130,7 @@ impl MountImpl { let mut mnt = MountImpl { mountpoint, auto_unmount_socket: None, + fuse_device: dev.clone(), }; if auto_unmount { @@ -308,6 +311,17 @@ impl MountImpl { } 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) = self.auto_unmount_socket.take() { + drop(sock); + // fusermount in auto-unmount mode, no more work to do. + return Ok(()); + } self.do_unmount(true) } diff --git a/src/mnt/mod.rs b/src/mnt/mod.rs index 6abb1b47..f7534d0e 100644 --- a/src/mnt/mod.rs +++ b/src/mnt/mod.rs @@ -214,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; From 0f4f455950529a63056464a8d7127eb6d5be7ecd Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Mon, 16 Feb 2026 10:13:44 -0500 Subject: [PATCH 14/16] direct-mount waits for auto unmount daemon on unmount, and changed to prefer using /proc/mounts over /etc/mtab --- fuser-tests/src/commands/mount.rs | 2 +- src/mnt/fuse_direct.rs | 66 +++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/fuser-tests/src/commands/mount.rs b/fuser-tests/src/commands/mount.rs index 77602fb7..ba3bbb2a 100644 --- a/fuser-tests/src/commands/mount.rs +++ b/fuser-tests/src/commands/mount.rs @@ -56,7 +56,7 @@ async fn run_mount_tests_inner(libfuse: Libfuse) -> anyhow::Result<()> { run_test( &[Feature::DirectMount], Unmount::Auto, - libfuse.fusermount(), + Fusermount::False, 1, false, ) diff --git a/src/mnt/fuse_direct.rs b/src/mnt/fuse_direct.rs index 1c5d3e37..16eafe8a 100644 --- a/src/mnt/fuse_direct.rs +++ b/src/mnt/fuse_direct.rs @@ -20,7 +20,11 @@ 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; @@ -43,10 +47,16 @@ const DEV_FUSE: &str = "/dev/fuse"; #[derive(Debug)] pub(crate) struct MountImpl { mountpoint: PathBuf, - auto_unmount_socket: Option, + auto_unmount_daemon: Option, fuse_device: Arc, } +#[derive(Debug)] +struct AutoUnmountDaemon { + socket: UnixStream, + pid: Pid, +} + impl MountImpl { pub(crate) fn new( mountpoint: &Path, @@ -129,7 +139,7 @@ impl MountImpl { let mut mnt = MountImpl { mountpoint, - auto_unmount_socket: None, + auto_unmount_daemon: None, fuse_device: dev.clone(), }; @@ -311,17 +321,36 @@ impl MountImpl { } 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(()); } - if let Some(sock) = self.auto_unmount_socket.take() { - drop(sock); - // fusermount in auto-unmount mode, no more work to do. - return Ok(()); - } + self.do_unmount(true) } @@ -356,14 +385,17 @@ impl MountImpl { fn setup_auto_unmount(&mut self) -> io::Result<()> { let (tx, rx) = UnixStream::pair()?; - if let ForkResult::Child = unsafe { fork() }? { - exit(match self.do_auto_unmount(rx) { - Ok(()) => 0, - Err(err) => err.raw_os_error().unwrap_or(1), - }); - } + 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_socket = Some(tx); + self.auto_unmount_daemon = Some(AutoUnmountDaemon { socket: tx, pid }); Ok(()) } @@ -402,10 +434,10 @@ impl MountImpl { let etc_mtab = Path::new("/etc/mtab"); let proc_mounts = Path::new("/proc/mounts"); - let mtab_path = if etc_mtab.try_exists()? { - etc_mtab - } else if proc_mounts.try_exists()? { + 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()); }; From 4ce1d6293a2309109a99115f28b8358c6c702435 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Mon, 16 Feb 2026 10:35:32 -0500 Subject: [PATCH 15/16] CI bump From 35ab8218ffff8ede7abd71d6b6571bd63695e533 Mon Sep 17 00:00:00 2001 From: Jack Bernard Date: Wed, 18 Feb 2026 20:21:03 -0500 Subject: [PATCH 16/16] CI bump