fuser/
session.rs

1//! Filesystem session
2//!
3//! A session runs a filesystem implementation while it is being mounted to a specific mount
4//! point. A session begins by mounting the filesystem and ends by unmounting it. While the
5//! filesystem is mounted, the session loop receives, dispatches and replies to kernel requests
6//! for filesystem operations under its mount point.
7
8use libc::{EAGAIN, EINTR, ENODEV, ENOENT};
9use log::{info, warn};
10use nix::unistd::geteuid;
11use std::fmt;
12use std::os::fd::{AsFd, BorrowedFd, OwnedFd};
13use std::path::{Path, PathBuf};
14use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
15use std::sync::{Arc, Mutex};
16use std::thread::{self, JoinHandle};
17use std::{io, ops::DerefMut};
18
19use crate::Filesystem;
20use crate::MountOption;
21use crate::ll::fuse_abi as abi;
22use crate::request::Request;
23use crate::{channel::Channel, mnt::Mount};
24use crate::{channel::ChannelSender, notify::Notifier};
25
26/// The max size of write requests from the kernel. The absolute minimum is 4k,
27/// FUSE recommends at least 128k, max 16M. The FUSE default is 16M on macOS
28/// and 128k on other systems.
29pub const MAX_WRITE_SIZE: usize = 16 * 1024 * 1024;
30
31/// Size of the buffer for reading a request from the kernel. Since the kernel may send
32/// up to MAX_WRITE_SIZE bytes in a write request, we use that value plus some extra space.
33const BUFFER_SIZE: usize = MAX_WRITE_SIZE + 4096;
34
35#[derive(Default, Debug, Eq, PartialEq)]
36/// How requests should be filtered based on the calling UID.
37pub enum SessionACL {
38    /// Allow requests from any user. Corresponds to the `allow_other` mount option.
39    All,
40    /// Allow requests from root. Corresponds to the `allow_root` mount option.
41    RootAndOwner,
42    /// Allow requests from the owning UID. This is FUSE's default mode of operation.
43    #[default]
44    Owner,
45}
46
47/// The session data structure
48#[derive(Debug)]
49pub struct Session<FS: Filesystem> {
50    /// Filesystem operation implementations
51    pub(crate) filesystem: FS,
52    /// Communication channel to the kernel driver
53    pub(crate) ch: Channel,
54    /// Handle to the mount.  Dropping this unmounts.
55    mount: Arc<Mutex<Option<(PathBuf, Mount)>>>,
56    /// Whether to restrict access to owner, root + owner, or unrestricted
57    /// Used to implement allow_root and auto_unmount
58    pub(crate) allowed: SessionACL,
59    /// User that launched the fuser process
60    pub(crate) session_owner: u32,
61    /// FUSE protocol major version
62    pub(crate) proto_major: AtomicU32,
63    /// FUSE protocol minor version
64    pub(crate) proto_minor: AtomicU32,
65    /// True if the filesystem is initialized (init operation done)
66    pub(crate) initialized: AtomicBool,
67    /// True if the filesystem was destroyed (destroy operation done)
68    pub(crate) destroyed: AtomicBool,
69}
70
71impl<FS: Filesystem> AsFd for Session<FS> {
72    fn as_fd(&self) -> BorrowedFd<'_> {
73        self.ch.as_fd()
74    }
75}
76
77impl<FS: Filesystem> Session<FS> {
78    /// Create a new session by mounting the given filesystem to the given mountpoint
79    pub fn new<P: AsRef<Path>>(
80        filesystem: FS,
81        mountpoint: P,
82        options: &[MountOption],
83    ) -> io::Result<Session<FS>> {
84        let mountpoint = mountpoint.as_ref();
85        info!("Mounting {}", mountpoint.display());
86        // If AutoUnmount is requested, but not AllowRoot or AllowOther we enforce the ACL
87        // ourself and implicitly set AllowOther because fusermount needs allow_root or allow_other
88        // to handle the auto_unmount option
89        let (file, mount) = if options.contains(&MountOption::AutoUnmount)
90            && !(options.contains(&MountOption::AllowRoot)
91                || options.contains(&MountOption::AllowOther))
92        {
93            warn!(
94                "Given auto_unmount without allow_root or allow_other; adding allow_other, with userspace permission handling"
95            );
96            let mut modified_options = options.to_vec();
97            modified_options.push(MountOption::AllowOther);
98            Mount::new(mountpoint, &modified_options)?
99        } else {
100            Mount::new(mountpoint, options)?
101        };
102
103        let ch = Channel::new(file);
104        let allowed = if options.contains(&MountOption::AllowRoot) {
105            SessionACL::RootAndOwner
106        } else if options.contains(&MountOption::AllowOther) {
107            SessionACL::All
108        } else {
109            SessionACL::Owner
110        };
111
112        Ok(Session {
113            filesystem,
114            ch,
115            mount: Arc::new(Mutex::new(Some((mountpoint.to_owned(), mount)))),
116            allowed,
117            session_owner: geteuid().as_raw(),
118            proto_major: AtomicU32::new(0),
119            proto_minor: AtomicU32::new(0),
120            initialized: AtomicBool::new(false),
121            destroyed: AtomicBool::new(false),
122        })
123    }
124
125    /// Wrap an existing /dev/fuse file descriptor. This doesn't mount the
126    /// filesystem anywhere; that must be done separately.
127    pub fn from_fd(filesystem: FS, fd: OwnedFd, acl: SessionACL) -> Self {
128        let ch = Channel::new(Arc::new(fd.into()));
129        Session {
130            filesystem,
131            ch,
132            mount: Arc::new(Mutex::new(None)),
133            allowed: acl,
134            session_owner: geteuid().as_raw(),
135            proto_major: AtomicU32::new(0),
136            proto_minor: AtomicU32::new(0),
137            initialized: AtomicBool::new(false),
138            destroyed: AtomicBool::new(false),
139        }
140    }
141
142    /// Run the session loop that receives kernel requests and dispatches them to method
143    /// calls into the filesystem.
144    pub fn run(&self) -> io::Result<()> {
145        self.run_with_callbacks(|_| {}, |_| {}, false)
146    }
147
148    /// Run the session loop that receives kernel requests and dispatches them to method
149    /// calls into the filesystem.
150    /// This version also notifies callers of kernel requests before and after they
151    /// are dispatched to the filesystem.
152    pub fn run_with_callbacks<FA, FB>(
153        &self,
154        before_dispatch: FB,
155        after_dispatch: FA,
156        clone_fuse_fd: bool,
157    ) -> io::Result<()>
158    where
159        FB: FnMut(&Request<'_>),
160        FA: FnMut(&Request<'_>),
161    {
162        info!(
163            "FUSE channel cloning: {}",
164            if clone_fuse_fd { "enabled" } else { "disabled" }
165        );
166
167        if clone_fuse_fd {
168            // Create a worker channel for this thread using FUSE_DEV_IOC_CLONE
169            // This allows multiple threads to read from the FUSE device without contention
170            let worker_channel = self.ch.clone_channel()?;
171            self.process_requests(&worker_channel, before_dispatch, after_dispatch)
172        } else {
173            // Use the original channel without cloning
174            self.process_requests(&self.ch, before_dispatch, after_dispatch)
175        }
176    }
177
178    /// Process requests using the given channel
179    fn process_requests<FA, FB>(
180        &self,
181        channel: &Channel,
182        mut before_dispatch: FB,
183        mut after_dispatch: FA,
184    ) -> io::Result<()>
185    where
186        FB: FnMut(&Request<'_>),
187        FA: FnMut(&Request<'_>),
188    {
189        // Buffer for receiving requests from the kernel. Only one is allocated and
190        // it is reused immediately after dispatching to conserve memory and allocations.
191        let mut buffer = vec![0; BUFFER_SIZE];
192        let buf = aligned_sub_buf(
193            buffer.deref_mut(),
194            std::mem::align_of::<abi::fuse_in_header>(),
195        );
196        loop {
197            // Read the next request from the given channel to kernel driver
198            // The kernel driver makes sure that we get exactly one request per read
199            match channel.receive(buf) {
200                Ok(size) => match Request::new(channel.sender(), &buf[..size]) {
201                    // Dispatch request
202                    Some(req) => {
203                        before_dispatch(&req);
204                        req.dispatch(self);
205                        after_dispatch(&req);
206                    }
207                    // Quit loop on illegal request
208                    None => break,
209                },
210                Err(err) => match err.raw_os_error() {
211                    // Operation interrupted. Accordingly to FUSE, this is safe to retry
212                    Some(ENOENT) => continue,
213                    // Interrupted system call, retry
214                    Some(EINTR) => continue,
215                    // Explicitly try again
216                    Some(EAGAIN) => continue,
217                    // Filesystem was unmounted, quit the loop
218                    Some(ENODEV) => break,
219                    // Unhandled error
220                    _ => return Err(err),
221                },
222            }
223        }
224        Ok(())
225    }
226
227    /// Unmount the filesystem
228    pub fn unmount(&mut self) {
229        drop(std::mem::take(&mut *self.mount.lock().unwrap()));
230    }
231
232    /// Returns a thread-safe object that can be used to unmount the Filesystem
233    pub fn unmount_callable(&mut self) -> SessionUnmounter {
234        SessionUnmounter {
235            mount: self.mount.clone(),
236        }
237    }
238
239    /// Returns an object that can be used to send notifications to the kernel
240    pub fn notifier(&self) -> Notifier {
241        Notifier::new(self.ch.sender())
242    }
243}
244
245#[derive(Debug)]
246/// A thread-safe object that can be used to unmount a Filesystem
247pub struct SessionUnmounter {
248    mount: Arc<Mutex<Option<(PathBuf, Mount)>>>,
249}
250
251impl SessionUnmounter {
252    /// Unmount the filesystem
253    pub fn unmount(&mut self) -> io::Result<()> {
254        drop(std::mem::take(&mut *self.mount.lock().unwrap()));
255        Ok(())
256    }
257}
258
259fn aligned_sub_buf(buf: &mut [u8], alignment: usize) -> &mut [u8] {
260    let off = alignment - (buf.as_ptr() as usize) % alignment;
261    if off == alignment {
262        buf
263    } else {
264        &mut buf[off..]
265    }
266}
267
268impl<FS: 'static + Filesystem + Send> Session<FS> {
269    /// Run the session loop in a background thread
270    pub fn spawn(self) -> io::Result<BackgroundSession> {
271        BackgroundSession::new(self)
272    }
273}
274
275impl<FS: Filesystem> Drop for Session<FS> {
276    fn drop(&mut self) {
277        if !self.destroyed.swap(true, Ordering::SeqCst) {
278            self.filesystem.destroy();
279        }
280
281        if let Some((mountpoint, _mount)) = std::mem::take(&mut *self.mount.lock().unwrap()) {
282            info!("unmounting session at {}", mountpoint.display());
283        }
284    }
285}
286
287/// The background session data structure
288pub struct BackgroundSession {
289    /// Thread guard of the background session
290    pub guard: JoinHandle<io::Result<()>>,
291    /// Object for creating Notifiers for client use
292    sender: ChannelSender,
293    /// Ensures the filesystem is unmounted when the session ends
294    _mount: Option<Mount>,
295}
296
297impl BackgroundSession {
298    /// Create a new background session for the given session by running its
299    /// session loop in a background thread. If the returned handle is dropped,
300    /// the filesystem is unmounted and the given session ends.
301    pub fn new<FS: Filesystem + Send + 'static>(se: Session<FS>) -> io::Result<BackgroundSession> {
302        let sender = se.ch.sender();
303        // Take the fuse_session, so that we can unmount it
304        let mount = std::mem::take(&mut *se.mount.lock().unwrap()).map(|(_, mount)| mount);
305        let guard = thread::spawn(move || se.run());
306        Ok(BackgroundSession {
307            guard,
308            sender,
309            _mount: mount,
310        })
311    }
312    /// Unmount the filesystem and join the background thread.
313    pub fn join(self) {
314        let Self {
315            guard,
316            sender: _,
317            _mount,
318        } = self;
319        drop(_mount);
320        guard.join().unwrap().unwrap();
321    }
322
323    /// Returns an object that can be used to send notifications to the kernel
324    pub fn notifier(&self) -> Notifier {
325        Notifier::new(self.sender.clone())
326    }
327}
328
329// replace with #[derive(Debug)] if Debug ever gets implemented for
330// thread_scoped::JoinGuard
331impl fmt::Debug for BackgroundSession {
332    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
333        write!(f, "BackgroundSession {{ guard: JoinGuard<()> }}",)
334    }
335}