From 9724cd38eba10dbebf53b1be48c53b55bc06c291 Mon Sep 17 00:00:00 2001 From: Colin Marc Date: Sun, 6 Oct 2024 19:17:26 +0200 Subject: [PATCH] Refactor Session to allow callers to mount it themselves This is a minor refactor of `Session`, with the aim of allowing callers to create an unmounted session and mount it themselves (using the AsFd implementation to get the FD). One use case for this is when mounting inside containers, when you need to call setns(2) before mounting. Fixes #300 --- examples/notify_inval_entry.rs | 4 +- examples/notify_inval_inode.rs | 4 +- examples/poll.rs | 4 +- src/lib.rs | 21 +++- src/session.rs | 214 +++++++++++++++++++++------------ src/sys.rs | 30 ++--- tests/integration_tests.rs | 7 +- 7 files changed, 179 insertions(+), 105 deletions(-) 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(); }