From 5bd35f3c5534f898dba01bd310741ad3c7388e44 Mon Sep 17 00:00:00 2001 From: Colin Marc Date: Sun, 6 Oct 2024 18:09:54 +0200 Subject: [PATCH 1/3] Drop support for libfuse, in favor of a pure-rust solution --- Cargo.toml | 6 - build.rs | 46 ------ mount_tests.sh | 16 --- src/channel.rs | 21 ++- src/lib.rs | 8 +- src/mnt/fuse2.rs | 69 --------- src/mnt/fuse2_sys.rs | 34 ----- src/mnt/fuse3.rs | 62 -------- src/mnt/fuse3_sys.rs | 31 ---- src/mnt/mod.rs | 189 ------------------------ src/{mnt => }/mount_options.rs | 4 +- src/session.rs | 19 +-- src/{mnt/fuse_pure.rs => sys.rs} | 239 +++++++++++++++++++++++-------- 13 files changed, 213 insertions(+), 531 deletions(-) delete mode 100644 build.rs delete mode 100644 src/mnt/fuse2.rs delete mode 100644 src/mnt/fuse2_sys.rs delete mode 100644 src/mnt/fuse3.rs delete mode 100644 src/mnt/fuse3_sys.rs delete mode 100644 src/mnt/mod.rs rename src/{mnt => }/mount_options.rs (98%) rename src/{mnt/fuse_pure.rs => sys.rs} (74%) diff --git a/Cargo.toml b/Cargo.toml index 1f0fae48..c476b154 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,6 @@ readme = "README.md" authors = ["Christopher Berner "] keywords = ["fuse", "filesystem", "system", "bindings"] categories = ["external-ffi-bindings", "api-bindings", "filesystem", "os::unix-apis"] -build = "build.rs" [dependencies] libc = "0.2.51" @@ -31,12 +30,7 @@ serde = { version = "1.0.102", features = ["std", "derive"] } tempfile = "3.10.1" nix = { version = "0.28.0", features = ["poll", "fs", "ioctl"] } -[build-dependencies] -pkg-config = { version = "0.3.14", optional = true } - [features] -default = ["libfuse"] -libfuse = ["pkg-config"] serializable = ["serde"] abi-7-9 = [] abi-7-10 = ["abi-7-9"] diff --git a/build.rs b/build.rs deleted file mode 100644 index b5c14a2c..00000000 --- a/build.rs +++ /dev/null @@ -1,46 +0,0 @@ -fn main() { - #[cfg(all(not(feature = "libfuse"), not(target_os = "linux")))] - unimplemented!("Building without libfuse is only supported on Linux"); - - #[cfg(feature = "libfuse")] - { - #[cfg(target_os = "macos")] - { - if pkg_config::Config::new() - .atleast_version("2.6.0") - .probe("fuse") // for macFUSE 4.x - .map_err(|e| eprintln!("{}", e)) - .is_ok() - { - println!("cargo:rustc-cfg=feature=\"libfuse2\""); - } else { - pkg_config::Config::new() - .atleast_version("2.6.0") - .probe("osxfuse") // for osxfuse 3.x - .map_err(|e| eprintln!("{}", e)) - .unwrap(); - println!("cargo:rustc-cfg=feature=\"libfuse2\""); - } - } - #[cfg(not(target_os = "macos"))] - { - // First try to link with libfuse3 - if pkg_config::Config::new() - .atleast_version("3.0.0") - .probe("fuse3") - .map_err(|e| eprintln!("{e}")) - .is_ok() - { - println!("cargo:rustc-cfg=feature=\"libfuse3\""); - } else { - // Fallback to libfuse - pkg_config::Config::new() - .atleast_version("2.6.0") - .probe("fuse") - .map_err(|e| eprintln!("{e}")) - .unwrap(); - println!("cargo:rustc-cfg=feature=\"libfuse2\""); - } - } - } -} diff --git a/mount_tests.sh b/mount_tests.sh index ef999459..b9a3c25e 100755 --- a/mount_tests.sh +++ b/mount_tests.sh @@ -139,22 +139,6 @@ run_test --no-default-features 'without libfuse, with fusermount3' run_test --no-default-features 'without libfuse, with fusermount3' --auto_unmount test_no_user_allow_other --no-default-features 'without libfuse, with fusermount3' -apt remove --purge -y fuse3 -apt autoremove -y -apt install -y libfuse-dev pkg-config fuse -echo 'user_allow_other' >> /etc/fuse.conf - -run_test --features=libfuse 'with libfuse' -run_test --features=libfuse 'with libfuse' --auto_unmount - -apt remove --purge -y libfuse-dev fuse -apt autoremove -y -apt install -y libfuse3-dev fuse3 -echo 'user_allow_other' >> /etc/fuse.conf - -run_test --features=libfuse,abi-7-30 'with libfuse3' -run_test --features=libfuse,abi-7-30 'with libfuse3' --auto_unmount - run_allow_root_test export TEST_EXIT_STATUS=0 diff --git a/src/channel.rs b/src/channel.rs index add5c831..7eb467fb 100644 --- a/src/channel.rs +++ b/src/channel.rs @@ -1,4 +1,11 @@ -use std::{fs::File, io, os::unix::prelude::AsRawFd, sync::Arc}; +use std::{ + io, + os::{ + fd::{AsFd, BorrowedFd, OwnedFd}, + unix::prelude::AsRawFd, + }, + sync::Arc, +}; use libc::{c_int, c_void, size_t}; @@ -6,13 +13,13 @@ use crate::reply::ReplySender; /// A raw communication channel to the FUSE kernel driver #[derive(Debug)] -pub struct Channel(Arc); +pub struct Channel(Arc); impl Channel { /// Create a new communication channel to the kernel driver by mounting the /// given path. The kernel driver will delegate filesystem operations of /// the given path to the channel. - pub(crate) fn new(device: Arc) -> Self { + pub(crate) fn new(device: Arc) -> Self { Self(device) } @@ -42,8 +49,14 @@ impl Channel { } } +impl AsFd for Channel { + fn as_fd(&self) -> BorrowedFd<'_> { + self.0.as_fd() + } +} + #[derive(Clone, Debug)] -pub struct ChannelSender(Arc); +pub struct ChannelSender(Arc); impl ReplySender for ChannelSender { fn send(&self, bufs: &[io::IoSlice<'_>]) -> io::Result<()> { diff --git a/src/lib.rs b/src/lib.rs index d1f126bd..77a1b9e4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ use libc::{c_int, ENOSYS, EPERM}; use log::{debug, warn}; -use mnt::mount_options::parse_options_from_args; +use mount_options::{check_option_conflicts, parse_options_from_args}; #[cfg(feature = "serializable")] use serde::{Deserialize, Serialize}; use std::ffi::OsStr; @@ -22,11 +22,10 @@ use std::{convert::AsRef, io::ErrorKind}; use crate::ll::fuse_abi::consts::*; pub use crate::ll::fuse_abi::FUSE_ROOT_ID; pub use crate::ll::{fuse_abi::consts, TimeOrNow}; -use crate::mnt::mount_options::check_option_conflicts; use crate::session::MAX_WRITE_SIZE; #[cfg(feature = "abi-7-16")] pub use ll::fuse_abi::fuse_forget_one; -pub use mnt::mount_options::MountOption; +pub use mount_options::MountOption; #[cfg(feature = "abi-7-11")] pub use notify::Notifier; #[cfg(feature = "abi-7-11")] @@ -48,12 +47,13 @@ use std::cmp::min; mod channel; mod ll; -mod mnt; +mod mount_options; #[cfg(feature = "abi-7-11")] mod notify; mod reply; mod request; mod session; +mod sys; /// We generally support async reads #[cfg(all(not(target_os = "macos"), not(feature = "abi-7-10")))] diff --git a/src/mnt/fuse2.rs b/src/mnt/fuse2.rs deleted file mode 100644 index 6f3e391e..00000000 --- a/src/mnt/fuse2.rs +++ /dev/null @@ -1,69 +0,0 @@ -use super::{fuse2_sys::*, with_fuse_args, MountOption}; -use log::warn; -use std::{ - ffi::CString, - fs::File, - io, - os::unix::prelude::{FromRawFd, OsStrExt}, - path::Path, - sync::Arc, -}; - -/// Ensures that an os error is never 0/Success -fn ensure_last_os_error() -> io::Error { - let err = io::Error::last_os_error(); - match err.raw_os_error() { - Some(0) => io::Error::new(io::ErrorKind::Other, "Unspecified Error"), - _ => err, - } -} - -#[derive(Debug)] -pub struct Mount { - mountpoint: CString, -} -impl Mount { - pub fn new(mountpoint: &Path, options: &[MountOption]) -> io::Result<(Arc, Mount)> { - let mountpoint = CString::new(mountpoint.as_os_str().as_bytes()).unwrap(); - with_fuse_args(options, |args| { - let fd = unsafe { fuse_mount_compat25(mountpoint.as_ptr(), args) }; - if fd < 0 { - Err(ensure_last_os_error()) - } else { - let file = unsafe { File::from_raw_fd(fd) }; - Ok((Arc::new(file), Mount { mountpoint })) - } - }) - } -} -impl Drop for Mount { - fn drop(&mut self) { - use std::io::ErrorKind::PermissionDenied; - - // fuse_unmount_compat22 unfortunately doesn't return a status. Additionally, - // it attempts to call realpath, which in turn calls into the filesystem. So - // if the filesystem returns an error, the unmount does not take place, with - // 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) = super::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.kind() == PermissionDenied { - #[cfg(not(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "bitrig", - target_os = "netbsd" - )))] - unsafe { - fuse_unmount_compat22(self.mountpoint.as_ptr()); - return; - } - } - warn!("umount failed with {:?}", err); - } - } -} diff --git a/src/mnt/fuse2_sys.rs b/src/mnt/fuse2_sys.rs deleted file mode 100644 index d99597c9..00000000 --- a/src/mnt/fuse2_sys.rs +++ /dev/null @@ -1,34 +0,0 @@ -//! Native FFI bindings to libfuse2. -//! -//! This is a small set of bindings that are required to mount/unmount FUSE filesystems and -//! open/close a fd to the FUSE kernel driver. - -#![warn(missing_debug_implementations)] -#![allow(missing_docs)] - -use libc::{c_char, c_int}; - -#[repr(C)] -#[derive(Debug)] -pub struct fuse_args { - pub argc: c_int, - pub argv: *const *const c_char, - pub allocated: c_int, -} - -#[cfg(feature = "libfuse2")] -extern "C" { - // *_compat25 functions were introduced in FUSE 2.6 when function signatures changed. - // Therefore, the minimum version requirement for *_compat25 functions is libfuse-2.6.0. - - pub 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 = "bitrig", - target_os = "netbsd" - )))] - pub fn fuse_unmount_compat22(mountpoint: *const c_char); -} diff --git a/src/mnt/fuse3.rs b/src/mnt/fuse3.rs deleted file mode 100644 index b7942b54..00000000 --- a/src/mnt/fuse3.rs +++ /dev/null @@ -1,62 +0,0 @@ -use super::fuse3_sys::{ - fuse_session_destroy, fuse_session_fd, fuse_session_mount, fuse_session_new, - fuse_session_unmount, -}; -use super::{with_fuse_args, MountOption}; -use std::{ - ffi::{c_void, CString}, - fs::File, - io, - os::unix::{ffi::OsStrExt, io::FromRawFd}, - path::Path, - ptr, - sync::Arc, -}; - -/// Ensures that an os error is never 0/Success -fn ensure_last_os_error() -> io::Error { - let err = io::Error::last_os_error(); - match err.raw_os_error() { - Some(0) => io::Error::new(io::ErrorKind::Other, "Unspecified Error"), - _ => err, - } -} - -#[derive(Debug)] -pub struct Mount { - fuse_session: *mut c_void, -} -impl Mount { - pub fn new(mnt: &Path, options: &[MountOption]) -> io::Result<(Arc, Mount)> { - let mnt = CString::new(mnt.as_os_str().as_bytes()).unwrap(); - with_fuse_args(options, |args| { - let fuse_session = unsafe { fuse_session_new(args, ptr::null(), 0, ptr::null_mut()) }; - if fuse_session.is_null() { - return Err(io::Error::last_os_error()); - } - let mount = Mount { fuse_session }; - let result = unsafe { fuse_session_mount(mount.fuse_session, mnt.as_ptr()) }; - if result != 0 { - return Err(ensure_last_os_error()); - } - let fd = unsafe { fuse_session_fd(mount.fuse_session) }; - if fd < 0 { - return Err(io::Error::last_os_error()); - } - // 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 = nix::fcntl::fcntl(fd, nix::fcntl::FcntlArg::F_DUPFD_CLOEXEC(0))?; - let file = unsafe { File::from_raw_fd(fd) }; - Ok((Arc::new(file), mount)) - }) - } -} -impl Drop for Mount { - fn drop(&mut self) { - unsafe { - fuse_session_unmount(self.fuse_session); - fuse_session_destroy(self.fuse_session); - } - } -} -unsafe impl Send for Mount {} diff --git a/src/mnt/fuse3_sys.rs b/src/mnt/fuse3_sys.rs deleted file mode 100644 index c1af6131..00000000 --- a/src/mnt/fuse3_sys.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Native FFI bindings to libfuse3. -//! -//! This is a small set of bindings that are required to mount/unmount FUSE filesystems and -//! open/close a fd to the FUSE kernel driver. - -#![warn(missing_debug_implementations)] -#![allow(missing_docs)] - -use super::fuse2_sys::fuse_args; -use libc::c_void; -use libc::{c_char, c_int}; - -extern "C" { - // Really this returns *fuse_session, but we don't need to access its fields - pub fn fuse_session_new( - args: *const fuse_args, - op: *const c_void, // This argument is really a *const fuse_lowlevel_ops, but we don't use them - op_size: libc::size_t, - userdata: *mut c_void, - ) -> *mut c_void; - pub fn fuse_session_mount( - se: *mut c_void, // This argument is really a *fuse_session - mountpoint: *const c_char, - ) -> c_int; - // This function's argument is really a *fuse_session - pub fn fuse_session_fd(se: *mut c_void) -> c_int; - // This function's argument is really a *fuse_session - pub fn fuse_session_unmount(se: *mut c_void); - // This function's argument is really a *fuse_session - pub fn fuse_session_destroy(se: *mut c_void); -} diff --git a/src/mnt/mod.rs b/src/mnt/mod.rs deleted file mode 100644 index 79d283cc..00000000 --- a/src/mnt/mod.rs +++ /dev/null @@ -1,189 +0,0 @@ -//! FUSE kernel driver communication -//! -//! Raw communication channel to the FUSE kernel driver. - -#[cfg(feature = "libfuse2")] -mod fuse2; -#[cfg(any(feature = "libfuse", test))] -mod fuse2_sys; -#[cfg(feature = "libfuse3")] -mod fuse3; -#[cfg(feature = "libfuse3")] -mod fuse3_sys; - -#[cfg(not(feature = "libfuse"))] -mod fuse_pure; -pub mod mount_options; - -#[cfg(any(feature = "libfuse", test))] -use fuse2_sys::fuse_args; -#[cfg(any(test, not(feature = "libfuse")))] -use std::fs::File; -#[cfg(any(test, not(feature = "libfuse"), not(feature = "libfuse3")))] -use std::io; - -#[cfg(any(feature = "libfuse", test))] -use mount_options::MountOption; - -/// Helper function to provide options as a fuse_args struct -/// (which contains an argc count and an argv pointer) -#[cfg(any(feature = "libfuse", test))] -fn with_fuse_args T>(options: &[MountOption], f: F) -> T { - use mount_options::option_to_string; - use std::ffi::CString; - - let mut args = vec![CString::new("rust-fuse").unwrap()]; - for x in options { - args.extend_from_slice(&[ - CString::new("-o").unwrap(), - CString::new(option_to_string(x)).unwrap(), - ]); - } - let argptrs: Vec<_> = args.iter().map(|s| s.as_ptr()).collect(); - f(&fuse_args { - argc: argptrs.len() as i32, - argv: argptrs.as_ptr(), - allocated: 0, - }) -} - -#[cfg(feature = "libfuse2")] -pub use fuse2::Mount; -#[cfg(feature = "libfuse3")] -pub use fuse3::Mount; -#[cfg(not(feature = "libfuse"))] -pub use fuse_pure::Mount; -#[cfg(not(feature = "libfuse3"))] -use std::ffi::CStr; - -#[cfg(not(feature = "libfuse3"))] -#[inline] -fn libc_umount(mnt: &CStr) -> io::Result<()> { - #[cfg(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "bitrig", - target_os = "netbsd" - ))] - let r = unsafe { libc::unmount(mnt.as_ptr(), 0) }; - - #[cfg(not(any( - target_os = "macos", - target_os = "freebsd", - target_os = "dragonfly", - target_os = "openbsd", - target_os = "bitrig", - target_os = "netbsd" - )))] - let r = unsafe { libc::umount(mnt.as_ptr()) }; - if r < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } -} - -/// Warning: This will return true if the filesystem has been detached (lazy unmounted), but not -/// yet destroyed by the kernel. -#[cfg(any(test, not(feature = "libfuse")))] -fn is_mounted(fuse_device: &File) -> bool { - use libc::{poll, pollfd}; - use std::os::unix::prelude::AsRawFd; - - let mut poll_result = pollfd { - fd: fuse_device.as_raw_fd(), - events: 0, - revents: 0, - }; - loop { - let res = unsafe { poll(&mut poll_result, 1, 0) }; - break match res { - 0 => true, - 1 => (poll_result.revents & libc::POLLERR) != 0, - -1 => { - let err = io::Error::last_os_error(); - if err.kind() == io::ErrorKind::Interrupted { - continue; - } else { - // This should never happen. The fd is guaranteed good as `File` owns it. - // According to man poll ENOMEM is the only error code unhandled, so we panic - // consistent with rust's usual ENOMEM behaviour. - panic!("Poll failed with error {}", err) - } - } - _ => unreachable!(), - }; - } -} - -#[cfg(test)] -mod test { - use super::*; - use std::{ffi::CStr, mem::ManuallyDrop}; - - #[test] - fn fuse_args() { - with_fuse_args( - &[ - MountOption::CUSTOM("foo".into()), - MountOption::CUSTOM("bar".into()), - ], - |args| { - let v: Vec<_> = (0..args.argc) - .map(|n| unsafe { - CStr::from_ptr(*args.argv.offset(n as isize)) - .to_str() - .unwrap() - }) - .collect(); - assert_eq!(*v, ["rust-fuse", "-o", "foo", "-o", "bar"]); - }, - ); - } - fn cmd_mount() -> String { - std::str::from_utf8( - std::process::Command::new("sh") - .arg("-c") - .arg("mount | grep fuse") - .output() - .unwrap() - .stdout - .as_ref(), - ) - .unwrap() - .to_owned() - } - - #[test] - fn mount_unmount() { - // We use ManuallyDrop here to leak the directory on test failure. We don't - // want to try and clean up the directory if it's a mountpoint otherwise we'll - // deadlock. - let tmp = ManuallyDrop::new(tempfile::tempdir().unwrap()); - let (file, mount) = Mount::new(tmp.path(), &[]).unwrap(); - let mnt = cmd_mount(); - eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,); - assert!(mnt.contains(&*tmp.path().to_string_lossy())); - assert!(is_mounted(&file)); - drop(mount); - let mnt = cmd_mount(); - eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,); - - let detached = !mnt.contains(&*tmp.path().to_string_lossy()); - // Linux supports MNT_DETACH, so we expect unmount to succeed even if the FS - // is busy. Other systems don't so the unmount may fail and we will still - // have the mount listed. The mount will get cleaned up later. - #[cfg(target_os = "linux")] - assert!(detached); - - if detached { - // We've detached successfully, it's safe to clean up: - std::mem::ManuallyDrop::<_>::into_inner(tmp); - } - - // Filesystem may have been lazy unmounted, so we can't assert this: - // assert!(!is_mounted(&file)); - } -} diff --git a/src/mnt/mount_options.rs b/src/mount_options.rs similarity index 98% rename from src/mnt/mount_options.rs rename to src/mount_options.rs index 1778d28e..1ac290b5 100644 --- a/src/mnt/mount_options.rs +++ b/src/mount_options.rs @@ -86,7 +86,7 @@ impl MountOption { } } -pub fn check_option_conflicts(options: &[MountOption]) -> Result<(), io::Error> { +pub(crate) fn check_option_conflicts(options: &[MountOption]) -> Result<(), io::Error> { let mut options_set = HashSet::new(); options_set.extend(options.iter().cloned()); let conflicting: HashSet = options.iter().flat_map(conflicts_with).collect(); @@ -127,7 +127,7 @@ fn conflicts_with(option: &MountOption) -> Vec { } // Format option to be passed to libfuse or kernel -pub fn option_to_string(option: &MountOption) -> String { +pub(crate) fn option_to_string(option: &MountOption) -> String { match option { MountOption::FSName(name) => format!("fsname={name}"), MountOption::Subtype(subtype) => format!("subtype={subtype}"), diff --git a/src/session.rs b/src/session.rs index 44919264..babc03b3 100644 --- a/src/session.rs +++ b/src/session.rs @@ -14,11 +14,13 @@ use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use std::{io, ops::DerefMut}; +use crate::channel::Channel; use crate::ll::fuse_abi as abi; use crate::request::Request; +use crate::sys; use crate::Filesystem; use crate::MountOption; -use crate::{channel::Channel, mnt::Mount}; + #[cfg(feature = "abi-7-11")] use crate::{channel::ChannelSender, notify::Notifier}; @@ -46,7 +48,7 @@ pub struct Session { /// Communication channel to the kernel driver ch: Channel, /// Handle to the mount. Dropping this unmounts. - mount: Arc>>, + mount: Arc>>, /// Mount point mountpoint: PathBuf, /// Whether to restrict access to owner, root + owner, or unrestricted @@ -73,22 +75,23 @@ impl Session { ) -> io::Result> { let mountpoint = mountpoint.as_ref(); info!("Mounting {}", mountpoint.display()); + // If AutoUnmount is requested, but not AllowRoot or AllowOther we enforce the ACL // ourself and implicitly set AllowOther because fusermount needs allow_root or allow_other // to handle the auto_unmount option - let (file, mount) = if options.contains(&MountOption::AutoUnmount) + let (device_fd, mount) = if options.contains(&MountOption::AutoUnmount) && !(options.contains(&MountOption::AllowRoot) || options.contains(&MountOption::AllowOther)) { warn!("Given auto_unmount without allow_root or allow_other; adding allow_other, with userspace permission handling"); let mut modified_options = options.to_vec(); modified_options.push(MountOption::AllowOther); - Mount::new(mountpoint, &modified_options)? + sys::Mount::new(mountpoint, &modified_options)? } else { - Mount::new(mountpoint, options)? + sys::Mount::new(mountpoint, options)? }; - let ch = Channel::new(file); + let ch = Channel::new(Arc::new(device_fd)); let allowed = if options.contains(&MountOption::AllowRoot) { SessionACL::RootAndOwner } else if options.contains(&MountOption::AllowOther) { @@ -177,7 +180,7 @@ impl Session { #[derive(Debug)] /// A thread-safe object that can be used to unmount a Filesystem pub struct SessionUnmounter { - mount: Arc>>, + mount: Arc>>, } impl SessionUnmounter { @@ -224,7 +227,7 @@ pub struct BackgroundSession { #[cfg(feature = "abi-7-11")] sender: ChannelSender, /// Ensures the filesystem is unmounted when the session ends - _mount: Mount, + _mount: sys::Mount, } impl BackgroundSession { diff --git a/src/mnt/fuse_pure.rs b/src/sys.rs similarity index 74% rename from src/mnt/fuse_pure.rs rename to src/sys.rs index d0412133..e744dc96 100644 --- a/src/mnt/fuse_pure.rs +++ b/src/sys.rs @@ -6,44 +6,75 @@ #![warn(missing_debug_implementations)] #![allow(missing_docs)] -use super::is_mounted; -use super::mount_options::{option_to_string, MountOption}; +use crate::mount_options::{option_to_string, MountOption}; use libc::c_int; use log::{debug, error}; use std::ffi::{CStr, CString, OsStr}; use std::fs::{File, OpenOptions}; use std::io; use std::io::{Error, ErrorKind, Read}; +use std::os::fd::{AsFd, OwnedFd}; use std::os::unix::ffi::OsStrExt; use std::os::unix::fs::PermissionsExt; use std::os::unix::io::{AsRawFd, FromRawFd}; use std::os::unix::net::UnixStream; use std::path::Path; use std::process::{Command, Stdio}; -use std::sync::Arc; use std::{mem, ptr}; const FUSERMOUNT_BIN: &str = "fusermount"; const FUSERMOUNT3_BIN: &str = "fusermount3"; const FUSERMOUNT_COMM_ENV: &str = "_FUSE_COMMFD"; +const FUSE_DEV_NAME: &str = "/dev/fuse"; + +/// Opens /dev/fuse. +fn open_device() -> io::Result { + let file = match OpenOptions::new() + .read(true) + .write(true) + .open(FUSE_DEV_NAME) + { + Ok(file) => file, + Err(error) => { + if error.kind() == ErrorKind::NotFound { + error!("{} not found. Try 'modprobe fuse'", FUSE_DEV_NAME); + } + return Err(error); + } + }; + + assert!( + file.as_raw_fd() > 2, + "Conflict with stdin/stdout/stderr. fd={}", + file.as_raw_fd() + ); + + Ok(file.into()) +} #[derive(Debug)] -pub struct Mount { +/// A helper to unmount a filesystem on Drop. +pub(crate) struct Mount { mountpoint: CString, + fuse_device: OwnedFd, auto_unmount_socket: Option, - fuse_device: Arc, } + impl Mount { - pub fn new(mountpoint: &Path, options: &[MountOption]) -> io::Result<(Arc, Mount)> { + pub fn new(mountpoint: &Path, options: &[MountOption]) -> io::Result<(OwnedFd, Self)> { let mountpoint = mountpoint.canonicalize()?; - let (file, sock) = fuse_mount_pure(mountpoint.as_os_str(), options)?; - let file = Arc::new(file); + let (fd, sock) = mount(mountpoint.as_os_str(), options)?; + + // Make a dup of the fuse device FD, so we can poll if the filesystem + // is still mounted. + let fuse_device = fd.as_fd().try_clone_to_owned()?; + Ok(( - file.clone(), - Mount { + fd, + Self { mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?, + fuse_device, auto_unmount_socket: sock, - fuse_device: file, }, )) } @@ -63,7 +94,7 @@ impl Drop for Mount { // fusermount in auto-unmount mode, no more work to do. return; } - if let Err(err) = super::libc_umount(&self.mountpoint) { + if let Err(err) = umount(&self.mountpoint) { if err.kind() == PermissionDenied { // Linux always returns EPERM for non-root users. We have to let the // library go through the setuid-root "fusermount -u" to unmount. @@ -75,21 +106,19 @@ impl Drop for Mount { } } -fn fuse_mount_pure( - mountpoint: &OsStr, - options: &[MountOption], -) -> Result<(File, Option), io::Error> { +fn mount(mountpoint: &OsStr, options: &[MountOption]) -> io::Result<(OwnedFd, Option)> { if options.contains(&MountOption::AutoUnmount) { // Auto unmount is only supported via fusermount - return fuse_mount_fusermount(mountpoint, options); + return mount_fusermount(mountpoint, options); } - let res = fuse_mount_sys(mountpoint, options)?; - if let Some(file) = res { - Ok((file, None)) - } else { - // Retry - fuse_mount_fusermount(mountpoint, options) + match mount_sys(mountpoint, options) { + Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { + // Retry + mount_fusermount(mountpoint, options) + } + Err(e) => Err(e), + Ok(fd) => Ok((fd, None)), } } @@ -228,10 +257,10 @@ fn receive_fusermount_message(socket: &UnixStream) -> Result { } } -fn fuse_mount_fusermount( +pub(crate) fn mount_fusermount( mountpoint: &OsStr, options: &[MountOption], -) -> Result<(File, Option), Error> { +) -> io::Result<(OwnedFd, Option)> { let (child_socket, receive_socket) = UnixStream::pair()?; unsafe { @@ -308,40 +337,20 @@ fn fuse_mount_fusermount( libc::fcntl(file.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC); } - Ok((file, receive_socket)) + Ok((file.into(), receive_socket)) } -// If returned option is none. Then fusermount binary should be tried -fn fuse_mount_sys(mountpoint: &OsStr, options: &[MountOption]) -> Result, Error> { - let fuse_device_name = "/dev/fuse"; - +// Performs the mount(2) syscall for the session. +fn mount_sys(mountpoint: &OsStr, options: &[MountOption]) -> io::Result { + let fd = open_device()?; let mountpoint_mode = File::open(mountpoint)?.metadata()?.permissions().mode(); // Auto unmount requests must be sent to fusermount binary assert!(!options.contains(&MountOption::AutoUnmount)); - let file = match OpenOptions::new() - .read(true) - .write(true) - .open(fuse_device_name) - { - Ok(file) => file, - Err(error) => { - if error.kind() == ErrorKind::NotFound { - error!("{} not found. Try 'modprobe fuse'", fuse_device_name); - } - return Err(error); - } - }; - assert!( - file.as_raw_fd() > 2, - "Conflict with stdin/stdout/stderr. fd={}", - file.as_raw_fd() - ); - let mut mount_options = format!( "fd={},rootmode={:o},user_id={},group_id={}", - file.as_raw_fd(), + fd.as_fd().as_raw_fd(), mountpoint_mode, nix::unistd::getuid(), nix::unistd::getgid() @@ -386,7 +395,7 @@ fn fuse_mount_sys(mountpoint: &OsStr, options: &[MountOption]) -> Result Result libc::c_int { _ => unreachable!(), } } + +/// Warning: This will return true if the filesystem has been detached (lazy unmounted), but not +/// yet destroyed by the kernel. +fn is_mounted(fuse_device: impl AsFd) -> bool { + use libc::{poll, pollfd}; + + let mut poll_result = pollfd { + fd: fuse_device.as_fd().as_raw_fd(), + events: 0, + revents: 0, + }; + loop { + let res = unsafe { poll(&mut poll_result, 1, 0) }; + break match res { + 0 => true, + 1 => (poll_result.revents & libc::POLLERR) != 0, + -1 => { + let err = io::Error::last_os_error(); + if err.kind() == io::ErrorKind::Interrupted { + continue; + } else { + // This should never happen. The fd is guaranteed good as `File` owns it. + // According to man poll ENOMEM is the only error code unhandled, so we panic + // consistent with rust's usual ENOMEM behaviour. + panic!("Poll failed with error {}", err) + } + } + _ => unreachable!(), + }; + } +} + +#[inline] +fn umount(mnt: &CStr) -> io::Result<()> { + #[cfg(any( + target_os = "macos", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "bitrig", + target_os = "netbsd" + ))] + let r = unsafe { libc::unmount(mnt.as_ptr(), 0) }; + + #[cfg(not(any( + target_os = "macos", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "bitrig", + target_os = "netbsd" + )))] + let r = unsafe { libc::umount(mnt.as_ptr()) }; + if r < 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::mem::ManuallyDrop; + + fn cmd_mount() -> String { + std::str::from_utf8( + std::process::Command::new("sh") + .arg("-c") + .arg("mount | grep fuse") + .output() + .unwrap() + .stdout + .as_ref(), + ) + .unwrap() + .to_owned() + } + + #[test] + fn mount_unmount() { + // We use ManuallyDrop here to leak the directory on test failure. We don't + // want to try and clean up the directory if it's a mountpoint otherwise we'll + // deadlock. + let tmp = ManuallyDrop::new(tempfile::tempdir().unwrap()); + let device_fd = open_device().unwrap(); + let mount = Mount::new(tmp.path(), &[]).unwrap(); + + let mnt = cmd_mount(); + eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,); + assert!(mnt.contains(&*tmp.path().to_string_lossy())); + assert!(is_mounted(&device_fd)); + + drop(mount); + let mnt = cmd_mount(); + eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,); + + let detached = !mnt.contains(&*tmp.path().to_string_lossy()); + // Linux supports MNT_DETACH, so we expect unmount to succeed even if the FS + // is busy. Other systems don't so the unmount may fail and we will still + // have the mount listed. The mount will get cleaned up later. + #[cfg(target_os = "linux")] + assert!(detached); + + if detached { + // We've detached successfully, it's safe to clean up: + std::mem::ManuallyDrop::<_>::into_inner(tmp); + } + + // Filesystem may have been lazy unmounted, so we can't assert this: + // assert!(!is_mounted(&file)); + } +} From 12f05bbabce87bad60dad6966cd9c1191bb2a4a7 Mon Sep 17 00:00:00 2001 From: Colin Marc Date: Sun, 6 Oct 2024 18:36:35 +0200 Subject: [PATCH 2/3] Make the use of fusermount(1) explicit There are multiple ways to mount FUSE filesystems. Processes with elevated privileges (CAP_SYS_ADMIN) can use the mount(2) syscall directly or via a suitable helper. libfuse also installs a helper program, fusermount(1), which is setuid root, to perform this mounting operation. Previously, we would attempt the syscall, and then fallback to trying fusermount (if we can find it) automatically. This is a little bit too magical, so it's better to make the option explicit. --- examples/hello.rs | 7 ++++- examples/simple.rs | 23 +++++++++------ mount_tests.sh | 6 ++-- src/lib.rs | 40 +++++++++++++++++++++++--- src/mount_options.rs | 19 ++++++++++-- src/session.rs | 59 +++++++++++++++++++++++++++----------- src/sys.rs | 48 ++++++++++++++++++------------- tests/integration_tests.rs | 2 +- 8 files changed, 146 insertions(+), 58 deletions(-) diff --git a/examples/hello.rs b/examples/hello.rs index c4150495..b845ab47 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -145,5 +145,10 @@ fn main() { if matches.get_flag("allow-root") { options.push(MountOption::AllowRoot); } - fuser::mount2(HelloFS, mountpoint, &options).unwrap(); + + if options.contains(&MountOption::AutoUnmount) { + fuser::fusermount(HelloFS, mountpoint, &options).unwrap(); + } else { + fuser::mount2(HelloFS, mountpoint, &options).unwrap(); + } } diff --git a/examples/simple.rs b/examples/simple.rs index 12469f38..8263ee8b 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -2039,21 +2039,26 @@ fn main() { .unwrap() .to_string(); - let result = fuser::mount2( - SimpleFS::new( - data_dir, - matches.get_flag("direct-io"), - matches.get_flag("suid"), - ), - mountpoint, - &options, + let fs = SimpleFS::new( + data_dir, + matches.get_flag("direct-io"), + matches.get_flag("suid"), ); + let result = if options.contains(&MountOption::AutoUnmount) { + fuser::fusermount(fs, mountpoint, &options) + } else { + fuser::mount2(fs, mountpoint, &options) + }; + if let Err(e) = result { + error!("{}", e.to_string()); + // Return a special error code for permission denied, which usually indicates that // "user_allow_other" is missing from /etc/fuse.conf if e.kind() == ErrorKind::PermissionDenied { - error!("{}", e.to_string()); std::process::exit(2); + } else { + std::process::exit(1); } } } diff --git a/mount_tests.sh b/mount_tests.sh index b9a3c25e..4fe570a5 100755 --- a/mount_tests.sh +++ b/mount_tests.sh @@ -18,8 +18,8 @@ function run_allow_root_test { useradd fusertest1 useradd fusertest2 DIR=$(su fusertest1 -c "mktemp --directory") - cargo build --example hello --features libfuse,abi-7-30 > /dev/null 2>&1 - su fusertest1 -c "target/debug/examples/hello $DIR --allow-root" & + cargo build --example hello --features abi-7-30 > /dev/null 2>&1 + su fusertest1 -c "target/debug/examples/hello $DIR --allow-root --auto_unmount" & FUSE_PID=$! sleep 2 @@ -62,7 +62,7 @@ function test_no_user_allow_other { DIR=$(su fusertestnoallow -c "mktemp --directory") DATA_DIR=$(su fusertestnoallow -c "mktemp --directory") cargo build --example simple $1 > /dev/null 2>&1 - su fusertestnoallow -c "target/debug/examples/simple -vvv --data-dir $DATA_DIR --mount-point $DIR" + su fusertestnoallow -c "target/debug/examples/simple -vvv --data-dir $DATA_DIR --mount-point $DIR --auto_unmount" exitCode=$? if [[ $exitCode -eq 2 ]]; then echo -e "$GREEN OK Detected lack of user_allow_other: $2 $NC" diff --git a/src/lib.rs b/src/lib.rs index 77a1b9e4..a4364d87 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1016,15 +1016,28 @@ pub fn mount2>( mountpoint: P, options: &[MountOption], ) -> io::Result<()> { - check_option_conflicts(options)?; + check_option_conflicts(options, false)?; Session::new(filesystem, mountpoint.as_ref(), options).and_then(|mut se| se.run()) } +/// Mount the given filesystem using fusermount(1). The binary must exist on +/// the system and be setuid root. +pub fn fusermount( + filesystem: impl Filesystem, + mountpoint: impl AsRef, + options: &[MountOption], +) -> io::Result<()> { + check_option_conflicts(options, true)?; + Session::new_fusermount(filesystem, mountpoint, options).and_then(|mut se| se.run()) +} + /// Mount the given filesystem to the given mountpoint. This function spawns /// a background thread to handle filesystem operations while being mounted /// and therefore returns immediately. The returned handle should be stored /// to reference the mounted filesystem. If it's dropped, the filesystem will /// be unmounted. +/// +/// This function requires CAP_SYS_ADMIN to run. #[deprecated(note = "use spawn_mount2() instead")] pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( filesystem: FS, @@ -1036,7 +1049,7 @@ pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( .map(|x| Some(MountOption::from_str(x.to_str()?))) .collect(); let options = options.ok_or(ErrorKind::InvalidData)?; - Session::new(filesystem, mountpoint.as_ref(), options.as_ref()).and_then(|se| se.spawn()) + spawn_mount2(filesystem, mountpoint, &options) } /// Mount the given filesystem to the given mountpoint. This function spawns @@ -1045,12 +1058,31 @@ pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( /// to reference the mounted filesystem. If it's dropped, the filesystem will /// be unmounted. /// -/// NOTE: This is the corresponding function to mount2. +/// NOTE: This is the corresponding function to [mount2], and likewise requires +/// CAP_SYS_ADMIN. pub fn spawn_mount2<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( filesystem: FS, mountpoint: P, options: &[MountOption], ) -> io::Result { - check_option_conflicts(options)?; + check_option_conflicts(options, false)?; Session::new(filesystem, mountpoint.as_ref(), options).and_then(|se| se.spawn()) } + +/// Mount the given filesystem to the given mountpoint. This function spawns +/// a background thread to handle filesystem operations while being mounted +/// and therefore returns immediately. The returned handle should be stored +/// to reference the mounted filesystem. If it's dropped, the filesystem will +/// be unmounted. +/// +/// NOTE: This is the corresponding function to [fusermount]. Unlike +/// [spawn_mount], this uses fusermount(1), which must be present on the +/// system and setuid root, to circumvent the need for elevated priveleges. +pub fn spawn_fusermount<'a, FS: Filesystem + Send + 'static + 'a>( + filesystem: FS, + mountpoint: impl AsRef, + options: &[MountOption], +) -> io::Result { + check_option_conflicts(options, true)?; + Session::new_fusermount(filesystem, mountpoint, options).and_then(|se| se.spawn()) +} diff --git a/src/mount_options.rs b/src/mount_options.rs index 1ac290b5..7724fbd0 100644 --- a/src/mount_options.rs +++ b/src/mount_options.rs @@ -86,11 +86,23 @@ impl MountOption { } } -pub(crate) fn check_option_conflicts(options: &[MountOption]) -> Result<(), io::Error> { +pub(crate) fn check_option_conflicts( + options: &[MountOption], + allow_auto_unmount: bool, +) -> Result<(), io::Error> { let mut options_set = HashSet::new(); options_set.extend(options.iter().cloned()); + + if !allow_auto_unmount && options_set.contains(&MountOption::AutoUnmount) { + return Err(io::Error::new( + ErrorKind::InvalidInput, + "AutoUnmount requires the use of fusermount", + )); + } + let conflicting: HashSet = options.iter().flat_map(conflicts_with).collect(); let intersection: Vec = conflicting.intersection(&options_set).cloned().collect(); + if !intersection.is_empty() { Err(io::Error::new( ErrorKind::InvalidInput, @@ -188,8 +200,9 @@ mod test { #[test] fn option_checking() { - assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoSuid]).is_err()); - assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoExec]).is_ok()); + assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoSuid], true).is_err()); + assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoExec], false).is_ok()); + assert!(check_option_conflicts(&[MountOption::AutoUnmount], true).is_ok()); } #[test] fn option_round_trip() { diff --git a/src/session.rs b/src/session.rs index babc03b3..6d38616a 100644 --- a/src/session.rs +++ b/src/session.rs @@ -9,18 +9,17 @@ use libc::{EAGAIN, EINTR, ENODEV, ENOENT}; use log::{info, warn}; use nix::unistd::geteuid; use std::fmt; +use std::os::fd::OwnedFd; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; use std::{io, ops::DerefMut}; -use crate::channel::Channel; use crate::ll::fuse_abi as abi; use crate::request::Request; -use crate::sys; use crate::Filesystem; use crate::MountOption; - +use crate::{channel::Channel, sys::Mount}; #[cfg(feature = "abi-7-11")] use crate::{channel::ChannelSender, notify::Notifier}; @@ -48,7 +47,7 @@ pub struct Session { /// Communication channel to the kernel driver ch: Channel, /// Handle to the mount. Dropping this unmounts. - mount: Arc>>, + mount: Arc>>, /// Mount point mountpoint: PathBuf, /// Whether to restrict access to owner, root + owner, or unrestricted @@ -67,15 +66,28 @@ pub struct Session { } impl Session { - /// Create a new session by mounting the given filesystem to the given mountpoint - pub fn new>( + /// Create a new session by mounting the given filesystem to the given + /// mountpoint. Uses the mount syscall, which requires CAP_SYS_ADMIN. + pub fn new( filesystem: FS, - mountpoint: P, + mountpoint: impl AsRef, options: &[MountOption], - ) -> io::Result> { - let mountpoint = mountpoint.as_ref(); - info!("Mounting {}", mountpoint.display()); + ) -> io::Result { + let (device_fd, mount) = Mount::new_sys(mountpoint.as_ref(), options)?; + + Ok(Self::new_inner( + filesystem, mountpoint, device_fd, mount, options, + )) + } + /// Create a new session by mounting the given filesystem to the given + /// mountpoint. Uses fusermount(1), which must exist on the system and be + /// setuid root, to circumvent the elevated privileges required by [Session::new]. + pub fn new_fusermount( + filesystem: FS, + mountpoint: impl AsRef, + options: &[MountOption], + ) -> io::Result> { // If AutoUnmount is requested, but not AllowRoot or AllowOther we enforce the ACL // ourself and implicitly set AllowOther because fusermount needs allow_root or allow_other // to handle the auto_unmount option @@ -86,12 +98,25 @@ impl Session { warn!("Given auto_unmount without allow_root or allow_other; adding allow_other, with userspace permission handling"); let mut modified_options = options.to_vec(); modified_options.push(MountOption::AllowOther); - sys::Mount::new(mountpoint, &modified_options)? + Mount::new_fusermount(mountpoint.as_ref(), &modified_options)? } else { - sys::Mount::new(mountpoint, options)? + Mount::new_fusermount(mountpoint.as_ref(), options)? }; + Ok(Self::new_inner( + filesystem, mountpoint, device_fd, mount, options, + )) + } + + fn new_inner( + filesystem: FS, + mountpoint: impl AsRef, + device_fd: OwnedFd, + mount: Mount, + options: &[MountOption], + ) -> Self { let ch = Channel::new(Arc::new(device_fd)); + let allowed = if options.contains(&MountOption::AllowRoot) { SessionACL::RootAndOwner } else if options.contains(&MountOption::AllowOther) { @@ -100,18 +125,18 @@ impl Session { SessionACL::Owner }; - Ok(Session { + Session { filesystem, ch, mount: Arc::new(Mutex::new(Some(mount))), - mountpoint: mountpoint.to_owned(), + mountpoint: mountpoint.as_ref().to_owned(), allowed, session_owner: geteuid().as_raw(), proto_major: 0, proto_minor: 0, initialized: false, destroyed: false, - }) + } } /// Return path of the mounted filesystem @@ -180,7 +205,7 @@ impl Session { #[derive(Debug)] /// A thread-safe object that can be used to unmount a Filesystem pub struct SessionUnmounter { - mount: Arc>>, + mount: Arc>>, } impl SessionUnmounter { @@ -227,7 +252,7 @@ pub struct BackgroundSession { #[cfg(feature = "abi-7-11")] sender: ChannelSender, /// Ensures the filesystem is unmounted when the session ends - _mount: sys::Mount, + _mount: Mount, } impl BackgroundSession { diff --git a/src/sys.rs b/src/sys.rs index e744dc96..cf2cec11 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -61,9 +61,33 @@ pub(crate) struct Mount { } impl Mount { - pub fn new(mountpoint: &Path, options: &[MountOption]) -> io::Result<(OwnedFd, Self)> { - let mountpoint = mountpoint.canonicalize()?; - let (fd, sock) = mount(mountpoint.as_os_str(), options)?; + pub fn new_sys( + mountpoint: impl AsRef, + options: &[MountOption], + ) -> io::Result<(OwnedFd, Self)> { + let mountpoint = mountpoint.as_ref().canonicalize()?; + let fd = mount_sys(mountpoint.as_os_str(), options)?; + + // Make a dup of the fuse device FD, so we can poll if the filesystem + // is still mounted. + let fuse_device = fd.as_fd().try_clone_to_owned()?; + + Ok(( + fd, + Self { + mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?, + fuse_device, + auto_unmount_socket: None, + }, + )) + } + + pub fn new_fusermount( + mountpoint: impl AsRef, + options: &[MountOption], + ) -> io::Result<(OwnedFd, Self)> { + let mountpoint = mountpoint.as_ref().canonicalize()?; + let (fd, sock) = mount_fusermount(mountpoint.as_os_str(), options)?; // Make a dup of the fuse device FD, so we can poll if the filesystem // is still mounted. @@ -106,22 +130,6 @@ impl Drop for Mount { } } -fn mount(mountpoint: &OsStr, options: &[MountOption]) -> io::Result<(OwnedFd, Option)> { - if options.contains(&MountOption::AutoUnmount) { - // Auto unmount is only supported via fusermount - return mount_fusermount(mountpoint, options); - } - - match mount_sys(mountpoint, options) { - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { - // Retry - mount_fusermount(mountpoint, options) - } - Err(e) => Err(e), - Ok(fd) => Ok((fd, None)), - } -} - fn fuse_unmount_pure(mountpoint: &CStr) { #[cfg(target_os = "linux")] unsafe { @@ -604,7 +612,7 @@ mod test { // deadlock. let tmp = ManuallyDrop::new(tempfile::tempdir().unwrap()); let device_fd = open_device().unwrap(); - let mount = Mount::new(tmp.path(), &[]).unwrap(); + let mount = Mount::new_fusermount(tmp.path(), &[]).unwrap(); let mnt = cmd_mount(); eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 0ed8c804..17338a28 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -13,7 +13,7 @@ fn unmount_no_send() { impl Filesystem for NoSendFS {} let tmpdir: TempDir = tempfile::tempdir().unwrap(); - let mut session = Session::new(NoSendFS(Rc::new(())), tmpdir.path(), &[]).unwrap(); + let mut session = Session::new_fusermount(NoSendFS(Rc::new(())), tmpdir.path(), &[]).unwrap(); let mut unmounter = session.unmount_callable(); thread::spawn(move || { thread::sleep(Duration::from_secs(1)); From 55a449d197d368ef9c55bd1b0cfb5d35f6779f4a Mon Sep 17 00:00:00 2001 From: Colin Marc Date: Sun, 6 Oct 2024 19:17:26 +0200 Subject: [PATCH 3/3] Refactor Session to allow callers to mount it themselves This is a minor refactor of `Session`, to make the API slightly more flexible. Splitting `Mount` into a separate object allows callers to - Create a mount without a Session object - Create a session given an existing /dev/fuse FD One use case for this is when mounting inside containers, when you need to handle the session and mounting in separate processes. Fixes #300 --- examples/notify_inval_entry.rs | 3 +- examples/notify_inval_inode.rs | 3 +- examples/poll.rs | 6 +- src/lib.rs | 67 +++++++- src/session.rs | 291 +++++++++++++++++++-------------- src/sys.rs | 119 ++------------ tests/integration_tests.rs | 11 +- 7 files changed, 256 insertions(+), 244 deletions(-) diff --git a/examples/notify_inval_entry.rs b/examples/notify_inval_entry.rs index e62ea58b..21f3c6d9 100644 --- a/examples/notify_inval_entry.rs +++ b/examples/notify_inval_entry.rs @@ -162,9 +162,8 @@ fn main() { timeout: Duration::from_secs_f32(opts.timeout), }; - let session = fuser::Session::new(fs, opts.mount_point, &options).unwrap(); + let session = fuser::spawn_mount2(fs, opts.mount_point, &options).expect("failed to mount"); let notifier = session.notifier(); - let _bg = session.spawn().unwrap(); loop { let mut fname = fname.lock().unwrap(); diff --git a/examples/notify_inval_inode.rs b/examples/notify_inval_inode.rs index 84f1418f..80f86ef8 100644 --- a/examples/notify_inval_inode.rs +++ b/examples/notify_inval_inode.rs @@ -200,9 +200,8 @@ fn main() { lookup_cnt, }; - let session = fuser::Session::new(fs, opts.mount_point, &options).unwrap(); + let session = fuser::spawn_mount2(fs, opts.mount_point, &options).expect("failed to mount"); let notifier = session.notifier(); - let _bg = session.spawn().unwrap(); loop { let mut s = fdata.lock().unwrap(); diff --git a/examples/poll.rs b/examples/poll.rs index 6ae3d1c2..3a5a0dc1 100644 --- a/examples/poll.rs +++ b/examples/poll.rs @@ -337,8 +337,6 @@ fn main() { let fs = FSelFS { data: data.clone() }; let mntpt = std::env::args().nth(1).unwrap(); - let session = fuser::Session::new(fs, mntpt, &options).unwrap(); - let bg = session.spawn().unwrap(); - - producer(&data, &bg.notifier()); + let session = fuser::spawn_mount2(fs, mntpt, &options).expect("failed to mount"); + producer(&data, &session.notifier()); } diff --git a/src/lib.rs b/src/lib.rs index a4364d87..bd37a4b5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,6 +13,7 @@ use mount_options::{check_option_conflicts, parse_options_from_args}; use serde::{Deserialize, Serialize}; use std::ffi::OsStr; use std::io; +use std::os::fd::AsFd; use std::path::Path; #[cfg(feature = "abi-7-23")] use std::time::Duration; @@ -39,7 +40,7 @@ pub use reply::{ ReplyStatfs, ReplyWrite, }; pub use request::Request; -pub use session::{BackgroundSession, Session, SessionUnmounter}; +pub use session::{BackgroundSession, Mount, Session, SessionACL, Unmounter}; #[cfg(feature = "abi-7-28")] use std::cmp::max; #[cfg(feature = "abi-7-13")] @@ -1008,7 +1009,8 @@ pub fn mount>( } /// Mount the given filesystem to the given mountpoint. This function will -/// not return until the filesystem is unmounted. +/// not return until the filesystem is unmounted. This function requires +/// CAP_SYS_ADMIN to run. /// /// NOTE: This will eventually replace mount(), once the API is stable pub fn mount2>( @@ -1017,7 +1019,10 @@ pub fn mount2>( options: &[MountOption], ) -> io::Result<()> { check_option_conflicts(options, false)?; - Session::new(filesystem, mountpoint.as_ref(), options).and_then(|mut se| se.run()) + let mut session = Session::new(filesystem, SessionACL::from_mount_options(options))?; + let _mount = Mount::new(session.as_fd(), mountpoint, options)?; + + session.run() } /// Mount the given filesystem using fusermount(1). The binary must exist on @@ -1028,7 +1033,30 @@ pub fn fusermount( options: &[MountOption], ) -> io::Result<()> { check_option_conflicts(options, true)?; - Session::new_fusermount(filesystem, mountpoint, options).and_then(|mut se| se.run()) + + // AutoUnmount requires either AllowRoot or AllowOther; we can inject + // that option but still block requests at the library level. + let (device_fd, _mount) = if options.contains(&MountOption::AutoUnmount) + && !(options.contains(&MountOption::AllowRoot) + || options.contains(&MountOption::AllowOther)) + { + warn!("Given auto_unmount without allow_root or allow_other; adding allow_other, with userspace permission handling"); + + let mut modified_options = options.to_vec(); + modified_options.push(MountOption::AllowOther); + + Mount::new_fusermount(mountpoint.as_ref(), &modified_options)? + } else { + Mount::new_fusermount(mountpoint.as_ref(), options)? + }; + + let mut session = Session::from_fd( + device_fd, + filesystem, + SessionACL::from_mount_options(options), + ); + + session.run() } /// Mount the given filesystem to the given mountpoint. This function spawns @@ -1066,7 +1094,11 @@ pub fn spawn_mount2<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef>( options: &[MountOption], ) -> io::Result { check_option_conflicts(options, false)?; - Session::new(filesystem, mountpoint.as_ref(), options).and_then(|se| se.spawn()) + + let session = Session::new(filesystem, SessionACL::from_mount_options(options))?; + let mount = Mount::new(session.as_fd(), &mountpoint, options)?; + + BackgroundSession::new(session, mount, mountpoint) } /// Mount the given filesystem to the given mountpoint. This function spawns @@ -1084,5 +1116,28 @@ pub fn spawn_fusermount<'a, FS: Filesystem + Send + 'static + 'a>( options: &[MountOption], ) -> io::Result { check_option_conflicts(options, true)?; - Session::new_fusermount(filesystem, mountpoint, options).and_then(|se| se.spawn()) + + // AutoUnmount requires either AllowRoot or AllowOther; we can inject + // that option but still block requests at the library level. + let (device_fd, mount) = if options.contains(&MountOption::AutoUnmount) + && !(options.contains(&MountOption::AllowRoot) + || options.contains(&MountOption::AllowOther)) + { + warn!("Given auto_unmount without allow_root or allow_other; adding allow_other, with userspace permission handling"); + + let mut modified_options = options.to_vec(); + modified_options.push(MountOption::AllowOther); + + Mount::new_fusermount(mountpoint.as_ref(), &modified_options)? + } else { + Mount::new_fusermount(mountpoint.as_ref(), options)? + }; + + let session = Session::from_fd( + device_fd, + filesystem, + SessionACL::from_mount_options(options), + ); + + BackgroundSession::new(session, mount, mountpoint) } diff --git a/src/session.rs b/src/session.rs index 6d38616a..ceaf01f8 100644 --- a/src/session.rs +++ b/src/session.rs @@ -6,20 +6,23 @@ //! for filesystem operations under its mount point. use libc::{EAGAIN, EINTR, ENODEV, ENOENT}; -use log::{info, warn}; +use log::error; use nix::unistd::geteuid; use std::fmt; -use std::os::fd::OwnedFd; +use std::os::fd::{AsFd, OwnedFd}; +use std::os::unix::net::UnixStream; use std::path::{Path, PathBuf}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, Mutex, Weak}; use std::thread::{self, JoinHandle}; use std::{io, ops::DerefMut}; +use crate::channel::Channel; use crate::ll::fuse_abi as abi; use crate::request::Request; +use crate::sys; use crate::Filesystem; use crate::MountOption; -use crate::{channel::Channel, sys::Mount}; + #[cfg(feature = "abi-7-11")] use crate::{channel::ChannelSender, notify::Notifier}; @@ -32,13 +35,121 @@ pub const MAX_WRITE_SIZE: usize = 16 * 1024 * 1024; /// up to MAX_WRITE_SIZE bytes in a write request, we use that value plus some extra space. const BUFFER_SIZE: usize = MAX_WRITE_SIZE + 4096; +/// A mountpoint, bound to a /dev/fuse file descriptor. Unmounts the filesystem +/// on drop. +pub struct Mount { + mountpoint: PathBuf, + fuse_device: OwnedFd, + auto_unmount_socket: Option, +} + +impl std::fmt::Debug for Mount { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Mount").field(&self.mountpoint).finish() + } +} + +impl Mount { + /// Creates a new mount for the given device FD (which can be wrapped in a + /// [Session]). + /// + /// Mounting requires CAP_SYS_ADMIN. + pub fn new( + device_fd: impl AsFd, + mountpoint: impl AsRef, + options: &[MountOption], + ) -> io::Result { + let mountpoint = mountpoint.as_ref().canonicalize()?; + sys::mount(mountpoint.as_os_str(), device_fd.as_fd(), options)?; + + // Make a dup of the fuse device FD, so we can poll if the filesystem + // is still mounted. + let fuse_device = device_fd.as_fd().try_clone_to_owned()?; + + Ok(Self { + mountpoint, + fuse_device, + auto_unmount_socket: None, + }) + } + + /// Uses fusermount(1) to mount the filesystem. Unlike [Mount::new], + /// fusermount opens the /dev/fuse FD for you, and it is returend as the + /// first element of the tuple. This file descriptor can then be wrapped + /// using [crate::Session::from_fd]. + pub fn new_fusermount( + mountpoint: impl AsRef, + options: &[MountOption], + ) -> io::Result<(OwnedFd, Self)> { + let mountpoint = mountpoint.as_ref().canonicalize()?; + let (fd, sock) = sys::fusermount(mountpoint.as_os_str(), options)?; + + // Make a dup of the fuse device FD, so we can poll if the filesystem + // is still mounted. + let fuse_device = fd.as_fd().try_clone_to_owned()?; + + Ok(( + fd, + Self { + mountpoint, + fuse_device, + auto_unmount_socket: sock, + }, + )) + } +} + +impl Drop for Mount { + fn drop(&mut self) { + use std::io::ErrorKind::PermissionDenied; + if !sys::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; + } + + if let Some(sock) = std::mem::take(&mut self.auto_unmount_socket) { + drop(sock); + // fusermount in auto-unmount mode, no more work to do. + return; + } + + if let Err(err) = sys::umount(self.mountpoint.as_os_str()) { + if err.kind() == PermissionDenied { + // Linux always returns EPERM for non-root users. We have to let the + // library go through the setuid-root "fusermount -u" to unmount. + sys::fusermount_umount(&self.mountpoint) + } else { + error!("Unmount failed: {}", err) + } + } + } +} + #[derive(Debug, Eq, PartialEq)] -pub(crate) enum SessionACL { +/// Defines which processes should be allowed to interact with a filesystem. +pub enum SessionACL { + /// Allow requests from all uids. Equivalent to allow_other. All, + /// Allow requests from root (uid 0) and the session owner. Equivalent to allow_root. RootAndOwner, + /// Allow only requests from the session owner. FUSE's default mode of operation. Owner, } +impl SessionACL { + pub(crate) fn from_mount_options(options: &[MountOption]) -> Self { + if options.contains(&MountOption::AllowRoot) { + SessionACL::RootAndOwner + } else if options.contains(&MountOption::AllowOther) { + SessionACL::All + } else { + SessionACL::Owner + } + } +} + /// The session data structure #[derive(Debug)] pub struct Session { @@ -46,10 +157,6 @@ pub struct Session { pub(crate) filesystem: FS, /// Communication channel to the kernel driver ch: Channel, - /// Handle to the mount. Dropping this unmounts. - mount: Arc>>, - /// Mount point - mountpoint: PathBuf, /// Whether to restrict access to owner, root + owner, or unrestricted /// Used to implement allow_root and auto_unmount pub(crate) allowed: SessionACL, @@ -65,72 +172,29 @@ pub struct Session { pub(crate) destroyed: bool, } -impl Session { - /// Create a new session by mounting the given filesystem to the given - /// mountpoint. Uses the mount syscall, which requires CAP_SYS_ADMIN. - pub fn new( - filesystem: FS, - mountpoint: impl AsRef, - options: &[MountOption], - ) -> io::Result { - let (device_fd, mount) = Mount::new_sys(mountpoint.as_ref(), options)?; - - Ok(Self::new_inner( - filesystem, mountpoint, device_fd, mount, options, - )) +impl AsFd for Session { + fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { + self.ch.as_fd() } +} - /// Create a new session by mounting the given filesystem to the given - /// mountpoint. Uses fusermount(1), which must exist on the system and be - /// setuid root, to circumvent the elevated privileges required by [Session::new]. - pub fn new_fusermount( - filesystem: FS, - mountpoint: impl AsRef, - options: &[MountOption], - ) -> io::Result> { - // If AutoUnmount is requested, but not AllowRoot or AllowOther we enforce the ACL - // ourself and implicitly set AllowOther because fusermount needs allow_root or allow_other - // to handle the auto_unmount option - let (device_fd, mount) = if options.contains(&MountOption::AutoUnmount) - && !(options.contains(&MountOption::AllowRoot) - || options.contains(&MountOption::AllowOther)) - { - warn!("Given auto_unmount without allow_root or allow_other; adding allow_other, with userspace permission handling"); - let mut modified_options = options.to_vec(); - modified_options.push(MountOption::AllowOther); - Mount::new_fusermount(mountpoint.as_ref(), &modified_options)? - } else { - Mount::new_fusermount(mountpoint.as_ref(), options)? - }; - - Ok(Self::new_inner( - filesystem, mountpoint, device_fd, mount, options, - )) +impl Session { + /// Creates a new session. This function does not mount the session; use + /// [crate::mount2] or similar or use [Session::as_fd] to extract the + /// /dev/fuse file descriptor and mount it separately. + pub fn new(filesystem: FS, acl: SessionACL) -> io::Result { + let device_fd = sys::open_device()?; + Ok(Self::from_fd(device_fd, filesystem, acl)) } - fn new_inner( - filesystem: FS, - mountpoint: impl AsRef, - device_fd: OwnedFd, - mount: Mount, - options: &[MountOption], - ) -> Self { + /// Creates a new session, using an existing /dev/fuse file descriptor. + pub fn from_fd(device_fd: OwnedFd, filesystem: FS, acl: SessionACL) -> Self { let ch = Channel::new(Arc::new(device_fd)); - let allowed = if options.contains(&MountOption::AllowRoot) { - SessionACL::RootAndOwner - } else if options.contains(&MountOption::AllowOther) { - SessionACL::All - } else { - SessionACL::Owner - }; - Session { filesystem, ch, - mount: Arc::new(Mutex::new(Some(mount))), - mountpoint: mountpoint.as_ref().to_owned(), - allowed, + allowed: acl, session_owner: geteuid().as_raw(), proto_major: 0, proto_minor: 0, @@ -139,11 +203,6 @@ impl Session { } } - /// Return path of the mounted filesystem - pub fn mountpoint(&self) -> &Path { - &self.mountpoint - } - /// Run the session loop that receives kernel requests and dispatches them to method /// calls into the filesystem. This read-dispatch-loop is non-concurrent to prevent /// having multiple buffers (which take up much memory), but the filesystem methods @@ -183,18 +242,6 @@ impl Session { Ok(()) } - /// Unmount the filesystem - pub fn unmount(&mut self) { - drop(std::mem::take(&mut *self.mount.lock().unwrap())); - } - - /// Returns a thread-safe object that can be used to unmount the Filesystem - pub fn unmount_callable(&mut self) -> SessionUnmounter { - SessionUnmounter { - mount: self.mount.clone(), - } - } - /// Returns an object that can be used to send notifications to the kernel #[cfg(feature = "abi-7-11")] pub fn notifier(&self) -> Notifier { @@ -202,20 +249,6 @@ impl Session { } } -#[derive(Debug)] -/// A thread-safe object that can be used to unmount a Filesystem -pub struct SessionUnmounter { - mount: Arc>>, -} - -impl SessionUnmounter { - /// Unmount the filesystem - pub fn unmount(&mut self) -> io::Result<()> { - drop(std::mem::take(&mut *self.mount.lock().unwrap())); - Ok(()) - } -} - fn aligned_sub_buf(buf: &mut [u8], alignment: usize) -> &mut [u8] { let off = alignment - (buf.as_ptr() as usize) % alignment; if off == alignment { @@ -225,20 +258,12 @@ fn aligned_sub_buf(buf: &mut [u8], alignment: usize) -> &mut [u8] { } } -impl Session { - /// Run the session loop in a background thread - pub fn spawn(self) -> io::Result { - BackgroundSession::new(self) - } -} - impl Drop for Session { fn drop(&mut self) { if !self.destroyed { self.filesystem.destroy(); self.destroyed = true; } - info!("Unmounted {}", self.mountpoint().display()); } } @@ -246,51 +271,58 @@ impl Drop for Session { pub struct BackgroundSession { /// Path of the mounted filesystem pub mountpoint: PathBuf, + /// Unmounts the filesystem on drop + mount: Arc>>, /// Thread guard of the background session pub guard: JoinHandle>, /// Object for creating Notifiers for client use #[cfg(feature = "abi-7-11")] sender: ChannelSender, - /// Ensures the filesystem is unmounted when the session ends - _mount: Mount, } impl BackgroundSession { /// Create a new background session for the given session by running its /// session loop in a background thread. If the returned handle is dropped, /// the filesystem is unmounted and the given session ends. - pub fn new(se: Session) -> io::Result { - let mountpoint = se.mountpoint().to_path_buf(); + pub(crate) fn new( + se: Session, + mount: Mount, + mountpoint: impl AsRef, + ) -> io::Result { + let mountpoint = mountpoint.as_ref().to_owned(); + #[cfg(feature = "abi-7-11")] let sender = se.ch.sender(); - // Take the fuse_session, so that we can unmount it - let mount = std::mem::take(&mut *se.mount.lock().unwrap()); - let mount = mount.ok_or_else(|| io::Error::from_raw_os_error(libc::ENODEV))?; + let guard = thread::spawn(move || { let mut se = se; se.run() }); + Ok(BackgroundSession { mountpoint, + mount: Arc::new(Mutex::new(Some(mount))), guard, #[cfg(feature = "abi-7-11")] sender, - _mount: mount, }) } /// Unmount the filesystem and join the background thread. pub fn join(self) { - let Self { - mountpoint: _, - guard, - #[cfg(feature = "abi-7-11")] - sender: _, - _mount, - } = self; - drop(_mount); + let Self { guard, mount, .. } = self; + + drop(mount); // Unmounts the filesystem. guard.join().unwrap().unwrap(); } + /// Returns a thread-safe handle that can be used to unmount the + /// filesystem. + pub fn unmounter(&self) -> Unmounter { + Unmounter { + mount: Arc::downgrade(&self.mount), + } + } + /// Returns an object that can be used to send notifications to the kernel #[cfg(feature = "abi-7-11")] pub fn notifier(&self) -> Notifier { @@ -309,3 +341,20 @@ impl fmt::Debug for BackgroundSession { ) } } + +#[derive(Debug, Clone)] +/// A thread-safe object that can be used to unmount a Filesystem +pub struct Unmounter { + mount: Weak>>, +} + +impl Unmounter { + /// Unmount the filesystem + pub fn unmount(&mut self) -> io::Result<()> { + if let Some(mount) = self.mount.upgrade() { + mount.lock().unwrap().take(); + } + + Ok(()) + } +} diff --git a/src/sys.rs b/src/sys.rs index cf2cec11..b4bfe539 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -9,7 +9,7 @@ use crate::mount_options::{option_to_string, MountOption}; use libc::c_int; use log::{debug, error}; -use std::ffi::{CStr, CString, OsStr}; +use std::ffi::{CString, OsStr}; use std::fs::{File, OpenOptions}; use std::io; use std::io::{Error, ErrorKind, Read}; @@ -28,7 +28,7 @@ const FUSERMOUNT_COMM_ENV: &str = "_FUSE_COMMFD"; const FUSE_DEV_NAME: &str = "/dev/fuse"; /// Opens /dev/fuse. -fn open_device() -> io::Result { +pub(crate) fn open_device() -> io::Result { let file = match OpenOptions::new() .read(true) .write(true) @@ -52,100 +52,7 @@ fn open_device() -> io::Result { Ok(file.into()) } -#[derive(Debug)] -/// A helper to unmount a filesystem on Drop. -pub(crate) struct Mount { - mountpoint: CString, - fuse_device: OwnedFd, - auto_unmount_socket: Option, -} - -impl Mount { - pub fn new_sys( - mountpoint: impl AsRef, - options: &[MountOption], - ) -> io::Result<(OwnedFd, Self)> { - let mountpoint = mountpoint.as_ref().canonicalize()?; - let fd = mount_sys(mountpoint.as_os_str(), options)?; - - // Make a dup of the fuse device FD, so we can poll if the filesystem - // is still mounted. - let fuse_device = fd.as_fd().try_clone_to_owned()?; - - Ok(( - fd, - Self { - mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?, - fuse_device, - auto_unmount_socket: None, - }, - )) - } - - pub fn new_fusermount( - mountpoint: impl AsRef, - options: &[MountOption], - ) -> io::Result<(OwnedFd, Self)> { - let mountpoint = mountpoint.as_ref().canonicalize()?; - let (fd, sock) = mount_fusermount(mountpoint.as_os_str(), options)?; - - // Make a dup of the fuse device FD, so we can poll if the filesystem - // is still mounted. - let fuse_device = fd.as_fd().try_clone_to_owned()?; - - Ok(( - fd, - Self { - mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?, - fuse_device, - auto_unmount_socket: sock, - }, - )) - } -} - -impl Drop for Mount { - fn drop(&mut self) { - use std::io::ErrorKind::PermissionDenied; - 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; - } - if let Some(sock) = mem::take(&mut self.auto_unmount_socket) { - drop(sock); - // fusermount in auto-unmount mode, no more work to do. - return; - } - if let Err(err) = umount(&self.mountpoint) { - if err.kind() == PermissionDenied { - // 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) - } else { - error!("Unmount failed: {}", err) - } - } - } -} - -fn fuse_unmount_pure(mountpoint: &CStr) { - #[cfg(target_os = "linux")] - unsafe { - let result = libc::umount2(mountpoint.as_ptr(), libc::MNT_DETACH); - if result == 0 { - return; - } - } - #[cfg(target_os = "macos")] - unsafe { - let result = libc::unmount(mountpoint.as_ptr(), libc::MNT_FORCE); - if result == 0 { - return; - } - } - +pub(crate) fn fusermount_umount(mountpoint: impl AsRef) { let mut builder = Command::new(detect_fusermount_bin()); builder.stdout(Stdio::piped()).stderr(Stdio::piped()); builder @@ -153,7 +60,7 @@ fn fuse_unmount_pure(mountpoint: &CStr) { .arg("-q") .arg("-z") .arg("--") - .arg(OsStr::new(&mountpoint.to_string_lossy().into_owned())); + .arg(mountpoint.as_ref()); if let Ok(output) = builder.output() { debug!("fusermount: {}", String::from_utf8_lossy(&output.stdout)); @@ -265,7 +172,7 @@ fn receive_fusermount_message(socket: &UnixStream) -> Result { } } -pub(crate) fn mount_fusermount( +pub(crate) fn fusermount( mountpoint: &OsStr, options: &[MountOption], ) -> io::Result<(OwnedFd, Option)> { @@ -349,8 +256,7 @@ pub(crate) fn mount_fusermount( } // Performs the mount(2) syscall for the session. -fn mount_sys(mountpoint: &OsStr, options: &[MountOption]) -> io::Result { - let fd = open_device()?; +pub(crate) fn mount(mountpoint: &OsStr, fd: impl AsFd, options: &[MountOption]) -> io::Result<()> { let mountpoint_mode = File::open(mountpoint)?.metadata()?.permissions().mode(); // Auto unmount requests must be sent to fusermount binary @@ -453,7 +359,7 @@ fn mount_sys(mountpoint: &OsStr, options: &[MountOption]) -> io::Result )); } - Ok(fd) + Ok(()) } #[derive(PartialEq)] @@ -529,7 +435,7 @@ pub fn option_to_flag(option: &MountOption) -> libc::c_int { /// Warning: This will return true if the filesystem has been detached (lazy unmounted), but not /// yet destroyed by the kernel. -fn is_mounted(fuse_device: impl AsFd) -> bool { +pub(crate) fn is_mounted(fuse_device: impl AsFd) -> bool { use libc::{poll, pollfd}; let mut poll_result = pollfd { @@ -559,7 +465,9 @@ fn is_mounted(fuse_device: impl AsFd) -> bool { } #[inline] -fn umount(mnt: &CStr) -> io::Result<()> { +pub(crate) fn umount(mnt: impl AsRef) -> io::Result<()> { + let mnt = CString::new(mnt.as_ref().as_os_str().as_bytes()).unwrap(); + #[cfg(any( target_os = "macos", target_os = "freebsd", @@ -588,6 +496,8 @@ fn umount(mnt: &CStr) -> io::Result<()> { #[cfg(test)] mod test { + use crate::Mount; + use super::*; use std::mem::ManuallyDrop; @@ -611,8 +521,7 @@ mod test { // want to try and clean up the directory if it's a mountpoint otherwise we'll // deadlock. let tmp = ManuallyDrop::new(tempfile::tempdir().unwrap()); - let device_fd = open_device().unwrap(); - let mount = Mount::new_fusermount(tmp.path(), &[]).unwrap(); + let (device_fd, mount) = Mount::new_fusermount(tmp.path(), &[]).unwrap(); let mnt = cmd_mount(); eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 17338a28..5f275be5 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,4 +1,4 @@ -use fuser::{Filesystem, Session}; +use fuser::{Filesystem, Mount, Session}; use std::rc::Rc; use std::thread; use std::time::Duration; @@ -13,11 +13,14 @@ fn unmount_no_send() { impl Filesystem for NoSendFS {} let tmpdir: TempDir = tempfile::tempdir().unwrap(); - let mut session = Session::new_fusermount(NoSendFS(Rc::new(())), tmpdir.path(), &[]).unwrap(); - let mut unmounter = session.unmount_callable(); + + let (device_fd, mount) = Mount::new_fusermount(tmpdir.path(), &[]).expect("failed to mount"); + let mut session = Session::from_fd(device_fd, NoSendFS(Rc::new(())), fuser::SessionACL::Owner); + thread::spawn(move || { thread::sleep(Duration::from_secs(1)); - unmounter.unmount().unwrap(); + drop(mount); }); + session.run().unwrap(); }