diff --git a/examples/notify_inval_entry.rs b/examples/notify_inval_entry.rs index e62ea58b..3f29001b 100644 --- a/examples/notify_inval_entry.rs +++ b/examples/notify_inval_entry.rs @@ -162,9 +162,9 @@ fn main() { timeout: Duration::from_secs_f32(opts.timeout), }; - let session = fuser::Session::new(fs, opts.mount_point, &options).unwrap(); + let (session, mount) = fuser::Session::new(fs, opts.mount_point, &options).unwrap(); let notifier = session.notifier(); - let _bg = session.spawn().unwrap(); + let _bg = fuser::BackgroundSession::new(session, mount); loop { let mut fname = fname.lock().unwrap(); diff --git a/examples/notify_inval_inode.rs b/examples/notify_inval_inode.rs index 84f1418f..bcb9c0df 100644 --- a/examples/notify_inval_inode.rs +++ b/examples/notify_inval_inode.rs @@ -200,9 +200,9 @@ fn main() { lookup_cnt, }; - let session = fuser::Session::new(fs, opts.mount_point, &options).unwrap(); + let (session, mount) = fuser::Session::new(fs, opts.mount_point, &options).unwrap(); let notifier = session.notifier(); - let _bg = session.spawn().unwrap(); + let _bg = fuser::BackgroundSession::new(session, mount); loop { let mut s = fdata.lock().unwrap(); diff --git a/examples/poll.rs b/examples/poll.rs index 6ae3d1c2..03deef5b 100644 --- a/examples/poll.rs +++ b/examples/poll.rs @@ -337,8 +337,8 @@ 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(); + let (session, mount) = fuser::Session::new(fs, mntpt, &options).unwrap(); + let bg = fuser::BackgroundSession::new(session, mount).unwrap(); producer(&data, &bg.notifier()); } diff --git a/src/lib.rs b/src/lib.rs index a4364d87..b81b534e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -39,7 +39,7 @@ pub use reply::{ ReplyStatfs, ReplyWrite, }; pub use request::Request; -pub use session::{BackgroundSession, Session, SessionUnmounter}; +pub use session::{BackgroundSession, Session, Unmounter}; #[cfg(feature = "abi-7-28")] use std::cmp::max; #[cfg(feature = "abi-7-13")] @@ -1008,7 +1008,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 +1018,9 @@ 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, _mount) = Session::new(filesystem, mountpoint, options)?; + + session.run() } /// Mount the given filesystem using fusermount(1). The binary must exist on @@ -1028,7 +1031,9 @@ pub fn fusermount( options: &[MountOption], ) -> io::Result<()> { check_option_conflicts(options, true)?; - Session::new_fusermount(filesystem, mountpoint, options).and_then(|mut se| se.run()) + let (mut session, _mount) = Session::new_fusermount(filesystem, mountpoint, options)?; + + session.run() } /// Mount the given filesystem to the given mountpoint. This function spawns @@ -1066,7 +1071,9 @@ 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, mount) = Session::new(filesystem, mountpoint, options)?; + BackgroundSession::new(session, mount) } /// Mount the given filesystem to the given mountpoint. This function spawns @@ -1084,5 +1091,7 @@ 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()) + + let (session, mount) = Session::new(filesystem, mountpoint, options)?; + BackgroundSession::new(session, mount) } diff --git a/src/session.rs b/src/session.rs index 6d38616a..b3436b75 100644 --- a/src/session.rs +++ b/src/session.rs @@ -9,17 +9,19 @@ use libc::{EAGAIN, EINTR, ENODEV, ENOENT}; use log::{info, warn}; use nix::unistd::geteuid; use std::fmt; -use std::os::fd::OwnedFd; +use std::os::fd::{AsFd, 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}; @@ -33,12 +35,68 @@ pub const MAX_WRITE_SIZE: usize = 16 * 1024 * 1024; const BUFFER_SIZE: usize = MAX_WRITE_SIZE + 4096; #[derive(Debug, Eq, PartialEq)] -pub(crate) enum SessionACL { +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 + } + } +} + +/// A mounted session. +#[derive(Debug)] +pub struct Mount { + /// Handle to the mount. Dropping this unmounts. + mount: Arc>>, + /// Mount point + mountpoint: PathBuf, +} + +impl Mount { + fn new(mount: sys::Mount, mountpoint: impl AsRef) -> Self { + Self { + mount: Arc::new(Mutex::new(Some(mount))), + mountpoint: mountpoint.as_ref().to_owned(), + } + } + + /// Return path of the mounted filesystem + pub fn mountpoint(&self) -> &Path { + &self.mountpoint + } + + /// 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) -> Unmounter { + Unmounter { + mount: self.mount.clone(), + } + } +} + +impl Drop for Mount { + fn drop(&mut self) { + info!("Unmounted {}", self.mountpoint().display()); + } +} + /// The session data structure #[derive(Debug)] pub struct Session { @@ -46,10 +104,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, @@ -59,38 +113,49 @@ pub struct Session { pub(crate) proto_major: u32, /// FUSE protocol minor version pub(crate) proto_minor: u32, + /// True if the filesystem has been mounted. + pub(crate) mounted: bool, /// True if the filesystem is initialized (init operation done) pub(crate) initialized: bool, /// True if the filesystem was destroyed (destroy operation done) pub(crate) destroyed: bool, } +impl AsFd for Session { + fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { + self.ch.as_fd() + } +} + impl Session { - /// Create a new session by mounting the given filesystem to the given - /// mountpoint. Uses the mount syscall, which requires CAP_SYS_ADMIN. + /// Create a new session. The session isn't mounted anywhere until you + /// mount it with [Session::mount]. + pub fn new_unmounted(filesystem: FS, acl: SessionACL) -> io::Result> { + let device_fd = sys::open_device()?; + Ok(Self::new_inner(filesystem, device_fd, acl)) + } + + /// Creates a new session, mounted at 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)?; + ) -> io::Result<(Self, Mount)> { + let mut session = Self::new_unmounted(filesystem, SessionACL::from_mount_options(options))?; + let mount = session.mount(mountpoint, options)?; - Ok(Self::new_inner( - filesystem, mountpoint, device_fd, mount, options, - )) + Ok((session, mount)) } - /// 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]. + /// Creates a new session, mounted at 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 + ) -> io::Result<(Self, Mount)> { let (device_fd, mount) = if options.contains(&MountOption::AutoUnmount) && !(options.contains(&MountOption::AllowRoot) || options.contains(&MountOption::AllowOther)) @@ -98,50 +163,58 @@ 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); - Mount::new_fusermount(mountpoint.as_ref(), &modified_options)? + sys::Mount::new_fusermount(mountpoint.as_ref(), &modified_options)? } else { - Mount::new_fusermount(mountpoint.as_ref(), options)? + sys::Mount::new_fusermount(mountpoint.as_ref(), options)? }; - Ok(Self::new_inner( - filesystem, mountpoint, device_fd, mount, options, - )) + let acl = SessionACL::from_mount_options(options); + let mut session = Self::new_inner(filesystem, device_fd, acl); + session.mounted = true; + + Ok((session, Mount::new(mount, mountpoint))) } - fn new_inner( - filesystem: FS, - mountpoint: impl AsRef, - device_fd: OwnedFd, - mount: Mount, - options: &[MountOption], - ) -> Self { + fn new_inner(filesystem: FS, device_fd: OwnedFd, 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, + mounted: false, initialized: false, destroyed: false, } } - /// Return path of the mounted filesystem - pub fn mountpoint(&self) -> &Path { - &self.mountpoint + /// Mounts the session at the given mount point. Uses the mount syscall, which + /// requires CAP_SYS_ADMIN. + /// + /// If the session was created with [Session::new_fusermount], it is already + /// mounted and this function will return an error. + pub fn mount( + &mut self, + mountpoint: impl AsRef, + options: &[MountOption], + ) -> io::Result { + if self.mounted { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "The session has already been mounted", + )); + } + + let mount = sys::Mount::new_sys(mountpoint.as_ref(), self.as_fd(), options)?; + self.mounted = true; + + Ok(Mount { + mount: Arc::new(Mutex::new(Some(mount))), + mountpoint: mountpoint.as_ref().to_owned(), + }) } /// Run the session loop that receives kernel requests and dispatches them to method @@ -149,6 +222,13 @@ impl Session { /// having multiple buffers (which take up much memory), but the filesystem methods /// may run concurrent by spawning threads. pub fn run(&mut self) -> io::Result<()> { + if !self.mounted { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "The session has not been mounted", + )); + } + // Buffer for receiving requests from the kernel. Only one is allocated and // it is reused immediately after dispatching to conserve memory and allocations. let mut buffer = vec![0; BUFFER_SIZE]; @@ -183,18 +263,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 { @@ -204,11 +272,11 @@ impl Session { #[derive(Debug)] /// A thread-safe object that can be used to unmount a Filesystem -pub struct SessionUnmounter { - mount: Arc>>, +pub struct Unmounter { + mount: Arc>>, } -impl SessionUnmounter { +impl Unmounter { /// Unmount the filesystem pub fn unmount(&mut self) -> io::Result<()> { drop(std::mem::take(&mut *self.mount.lock().unwrap())); @@ -225,20 +293,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()); } } @@ -252,24 +312,30 @@ 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 { /// 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 fn new( + se: Session, + mount: Mount, + ) -> io::Result { + let mountpoint = mount.mountpoint().to_path_buf(); + #[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 = std::mem::take(&mut *mount.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, guard, diff --git a/src/sys.rs b/src/sys.rs index cf2cec11..f2c62682 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -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) @@ -63,23 +63,21 @@ pub(crate) struct Mount { impl Mount { pub fn new_sys( mountpoint: impl AsRef, + device_fd: impl AsFd, options: &[MountOption], - ) -> io::Result<(OwnedFd, Self)> { + ) -> io::Result { let mountpoint = mountpoint.as_ref().canonicalize()?; - let fd = mount_sys(mountpoint.as_os_str(), options)?; + mount_sys(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 = fd.as_fd().try_clone_to_owned()?; + let fuse_device = 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, - }, - )) + Ok(Self { + mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?, + fuse_device, + auto_unmount_socket: None, + }) } pub fn new_fusermount( @@ -349,8 +347,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()?; +fn mount_sys(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 +450,7 @@ fn mount_sys(mountpoint: &OsStr, options: &[MountOption]) -> io::Result )); } - Ok(fd) + Ok(()) } #[derive(PartialEq)] @@ -611,8 +608,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..a7f23bec 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -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 (mut session, mut mount) = + Session::new_fusermount(NoSendFS(Rc::new(())), tmpdir.path(), &[]).unwrap(); + let mut unmounter = mount.unmount_callable(); + thread::spawn(move || { thread::sleep(Duration::from_secs(1)); unmounter.unmount().unwrap(); }); + session.run().unwrap(); }