fuser/mnt/
fuse_pure.rs

1//! Native FFI bindings to libfuse.
2//!
3//! This is a small set of bindings that are required to mount/unmount FUSE filesystems and
4//! open/close a fd to the FUSE kernel driver.
5
6#![warn(missing_debug_implementations)]
7#![allow(missing_docs)]
8
9use super::is_mounted;
10use super::mount_options::{MountOption, option_to_string};
11use libc::c_int;
12use log::{debug, error};
13use std::ffi::{CStr, CString, OsStr};
14use std::fs::{File, OpenOptions};
15use std::io;
16use std::io::{Error, ErrorKind, Read};
17use std::os::unix::ffi::OsStrExt;
18use std::os::unix::fs::PermissionsExt;
19use std::os::unix::io::{AsRawFd, FromRawFd};
20use std::os::unix::net::UnixStream;
21use std::path::Path;
22use std::process::{Command, Stdio};
23use std::sync::Arc;
24use std::{mem, ptr};
25
26const FUSERMOUNT_BIN: &str = "fusermount";
27const FUSERMOUNT3_BIN: &str = "fusermount3";
28const FUSERMOUNT_COMM_ENV: &str = "_FUSE_COMMFD";
29
30#[derive(Debug)]
31pub struct Mount {
32    mountpoint: CString,
33    auto_unmount_socket: Option<UnixStream>,
34    fuse_device: Arc<File>,
35}
36impl Mount {
37    pub fn new(mountpoint: &Path, options: &[MountOption]) -> io::Result<(Arc<File>, Mount)> {
38        let mountpoint = mountpoint.canonicalize()?;
39        let (file, sock) = fuse_mount_pure(mountpoint.as_os_str(), options)?;
40        let file = Arc::new(file);
41        Ok((
42            file.clone(),
43            Mount {
44                mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?,
45                auto_unmount_socket: sock,
46                fuse_device: file,
47            },
48        ))
49    }
50}
51
52impl Drop for Mount {
53    fn drop(&mut self) {
54        use std::io::ErrorKind::PermissionDenied;
55        if !is_mounted(&self.fuse_device) {
56            // If the filesystem has already been unmounted, avoid unmounting it again.
57            // Unmounting it a second time could cause a race with a newly mounted filesystem
58            // living at the same mountpoint
59            return;
60        }
61        if let Some(sock) = mem::take(&mut self.auto_unmount_socket) {
62            drop(sock);
63            // fusermount in auto-unmount mode, no more work to do.
64            return;
65        }
66        if let Err(err) = super::libc_umount(&self.mountpoint) {
67            if err.kind() == PermissionDenied {
68                // Linux always returns EPERM for non-root users.  We have to let the
69                // library go through the setuid-root "fusermount -u" to unmount.
70                fuse_unmount_pure(&self.mountpoint)
71            } else {
72                error!("Unmount failed: {}", err)
73            }
74        }
75    }
76}
77
78fn fuse_mount_pure(
79    mountpoint: &OsStr,
80    options: &[MountOption],
81) -> Result<(File, Option<UnixStream>), io::Error> {
82    if options.contains(&MountOption::AutoUnmount) {
83        // Auto unmount is only supported via fusermount
84        return fuse_mount_fusermount(mountpoint, options);
85    }
86
87    let res = fuse_mount_sys(mountpoint, options)?;
88    match res {
89        Some(file) => Ok((file, None)),
90        _ => {
91            // Retry
92            fuse_mount_fusermount(mountpoint, options)
93        }
94    }
95}
96
97fn fuse_unmount_pure(mountpoint: &CStr) {
98    #[cfg(target_os = "linux")]
99    unsafe {
100        let result = libc::umount2(mountpoint.as_ptr(), libc::MNT_DETACH);
101        if result == 0 {
102            return;
103        }
104    }
105    #[cfg(target_os = "macos")]
106    unsafe {
107        let result = libc::unmount(mountpoint.as_ptr(), libc::MNT_FORCE);
108        if result == 0 {
109            return;
110        }
111    }
112
113    let mut builder = Command::new(detect_fusermount_bin());
114    builder.stdout(Stdio::piped()).stderr(Stdio::piped());
115    builder
116        .arg("-u")
117        .arg("-q")
118        .arg("-z")
119        .arg("--")
120        .arg(OsStr::new(&mountpoint.to_string_lossy().into_owned()));
121
122    if let Ok(output) = builder.output() {
123        debug!("fusermount: {}", String::from_utf8_lossy(&output.stdout));
124        debug!("fusermount: {}", String::from_utf8_lossy(&output.stderr));
125    }
126}
127
128fn detect_fusermount_bin() -> String {
129    for name in [
130        FUSERMOUNT3_BIN.to_string(),
131        FUSERMOUNT_BIN.to_string(),
132        format!("/bin/{FUSERMOUNT3_BIN}"),
133        format!("/bin/{FUSERMOUNT_BIN}"),
134    ]
135    .iter()
136    {
137        if Command::new(name).arg("-h").output().is_ok() {
138            return name.to_string();
139        }
140    }
141    // Default to fusermount3
142    FUSERMOUNT3_BIN.to_string()
143}
144
145fn receive_fusermount_message(socket: &UnixStream) -> Result<File, Error> {
146    let mut io_vec_buf = [0u8];
147    let mut io_vec = libc::iovec {
148        iov_base: io_vec_buf.as_mut_ptr() as *mut libc::c_void,
149        iov_len: io_vec_buf.len(),
150    };
151    let cmsg_buffer_len = unsafe { libc::CMSG_SPACE(mem::size_of::<c_int>() as libc::c_uint) };
152    let mut cmsg_buffer = vec![0u8; cmsg_buffer_len as usize];
153    let mut message: libc::msghdr;
154    #[cfg(all(target_os = "linux", not(target_env = "musl")))]
155    {
156        message = libc::msghdr {
157            msg_name: ptr::null_mut(),
158            msg_namelen: 0,
159            msg_iov: &mut io_vec,
160            msg_iovlen: 1,
161            msg_control: cmsg_buffer.as_mut_ptr() as *mut libc::c_void,
162            msg_controllen: cmsg_buffer.len(),
163            msg_flags: 0,
164        };
165    }
166    #[cfg(all(target_os = "linux", target_env = "musl"))]
167    {
168        message = unsafe { std::mem::MaybeUninit::zeroed().assume_init() };
169        message.msg_name = ptr::null_mut();
170        message.msg_namelen = 0;
171        message.msg_iov = &mut io_vec;
172        message.msg_iovlen = 1;
173        message.msg_control = (&mut cmsg_buffer).as_mut_ptr() as *mut libc::c_void;
174        message.msg_controllen = cmsg_buffer.len() as u32;
175        message.msg_flags = 0;
176    }
177    #[cfg(any(
178        target_os = "macos",
179        target_os = "freebsd",
180        target_os = "dragonfly",
181        target_os = "openbsd",
182        target_os = "netbsd"
183    ))]
184    {
185        message = libc::msghdr {
186            msg_name: ptr::null_mut(),
187            msg_namelen: 0,
188            msg_iov: &mut io_vec,
189            msg_iovlen: 1,
190            msg_control: (&mut cmsg_buffer).as_mut_ptr() as *mut libc::c_void,
191            msg_controllen: cmsg_buffer.len() as u32,
192            msg_flags: 0,
193        };
194    }
195
196    let mut result;
197    loop {
198        unsafe {
199            result = libc::recvmsg(socket.as_raw_fd(), &mut message, 0);
200        }
201        if result != -1 {
202            break;
203        }
204        let err = Error::last_os_error();
205        if err.kind() != ErrorKind::Interrupted {
206            return Err(err);
207        }
208    }
209    if result == 0 {
210        return Err(Error::new(
211            ErrorKind::UnexpectedEof,
212            "Unexpected EOF reading from fusermount",
213        ));
214    }
215
216    unsafe {
217        let control_msg = libc::CMSG_FIRSTHDR(&message);
218        if (*control_msg).cmsg_type != libc::SCM_RIGHTS {
219            return Err(Error::new(
220                ErrorKind::InvalidData,
221                format!(
222                    "Unknown control message from fusermount: {}",
223                    (*control_msg).cmsg_type
224                ),
225            ));
226        }
227        let fd_data = libc::CMSG_DATA(control_msg);
228
229        let fd = *(fd_data as *const c_int);
230        if fd < 0 {
231            Err(ErrorKind::InvalidData.into())
232        } else {
233            Ok(File::from_raw_fd(fd))
234        }
235    }
236}
237
238fn fuse_mount_fusermount(
239    mountpoint: &OsStr,
240    options: &[MountOption],
241) -> Result<(File, Option<UnixStream>), Error> {
242    let (child_socket, receive_socket) = UnixStream::pair()?;
243
244    unsafe {
245        libc::fcntl(child_socket.as_raw_fd(), libc::F_SETFD, 0);
246    }
247
248    let mut builder = Command::new(detect_fusermount_bin());
249    builder.stdout(Stdio::piped()).stderr(Stdio::piped());
250    if !options.is_empty() {
251        builder.arg("-o");
252        let options_strs: Vec<String> = options.iter().map(option_to_string).collect();
253        builder.arg(options_strs.join(","));
254    }
255    builder
256        .arg("--")
257        .arg(mountpoint)
258        .env(FUSERMOUNT_COMM_ENV, child_socket.as_raw_fd().to_string());
259
260    let fusermount_child = builder.spawn()?;
261
262    drop(child_socket); // close socket in parent
263
264    let file = match receive_fusermount_message(&receive_socket) {
265        Ok(f) => f,
266        Err(_) => {
267            // Drop receive socket, since fusermount has exited with an error
268            drop(receive_socket);
269            let output = fusermount_child.wait_with_output().unwrap();
270            let stderr_string = String::from_utf8_lossy(&output.stderr).to_string();
271            return if stderr_string.contains("only allowed if 'user_allow_other' is set") {
272                Err(io::Error::new(ErrorKind::PermissionDenied, stderr_string))
273            } else {
274                Err(io::Error::new(ErrorKind::Other, stderr_string))
275            };
276        }
277    };
278    let mut receive_socket = Some(receive_socket);
279
280    if !options.contains(&MountOption::AutoUnmount) {
281        // Only close the socket, if auto unmount is not set.
282        // fusermount will keep running until the socket is closed, if auto unmount is set
283        drop(mem::take(&mut receive_socket));
284        let output = fusermount_child.wait_with_output()?;
285        debug!("fusermount: {}", String::from_utf8_lossy(&output.stdout));
286        debug!("fusermount: {}", String::from_utf8_lossy(&output.stderr));
287    } else {
288        if let Some(mut stdout) = fusermount_child.stdout {
289            let stdout_fd = stdout.as_raw_fd();
290            unsafe {
291                let mut flags = libc::fcntl(stdout_fd, libc::F_GETFL, 0);
292                flags |= libc::O_NONBLOCK;
293                libc::fcntl(stdout_fd, libc::F_SETFL, flags);
294            }
295            let mut buf = vec![0; 64 * 1024];
296            if let Ok(len) = stdout.read(&mut buf) {
297                debug!("fusermount: {}", String::from_utf8_lossy(&buf[..len]));
298            }
299        }
300        if let Some(mut stderr) = fusermount_child.stderr {
301            let stderr_fd = stderr.as_raw_fd();
302            unsafe {
303                let mut flags = libc::fcntl(stderr_fd, libc::F_GETFL, 0);
304                flags |= libc::O_NONBLOCK;
305                libc::fcntl(stderr_fd, libc::F_SETFL, flags);
306            }
307            let mut buf = vec![0; 64 * 1024];
308            if let Ok(len) = stderr.read(&mut buf) {
309                debug!("fusermount: {}", String::from_utf8_lossy(&buf[..len]));
310            }
311        }
312    }
313
314    unsafe {
315        libc::fcntl(file.as_raw_fd(), libc::F_SETFD, libc::FD_CLOEXEC);
316    }
317
318    Ok((file, receive_socket))
319}
320
321// If returned option is none. Then fusermount binary should be tried
322fn fuse_mount_sys(mountpoint: &OsStr, options: &[MountOption]) -> Result<Option<File>, Error> {
323    let fuse_device_name = "/dev/fuse";
324
325    let mountpoint_mode = File::open(mountpoint)?.metadata()?.permissions().mode();
326
327    // Auto unmount requests must be sent to fusermount binary
328    assert!(!options.contains(&MountOption::AutoUnmount));
329
330    let file = match OpenOptions::new()
331        .read(true)
332        .write(true)
333        .open(fuse_device_name)
334    {
335        Ok(file) => file,
336        Err(error) => {
337            if error.kind() == ErrorKind::NotFound {
338                error!("{} not found. Try 'modprobe fuse'", fuse_device_name);
339            }
340            return Err(error);
341        }
342    };
343    assert!(
344        file.as_raw_fd() > 2,
345        "Conflict with stdin/stdout/stderr. fd={}",
346        file.as_raw_fd()
347    );
348
349    let mut mount_options = format!(
350        "fd={},rootmode={:o},user_id={},group_id={}",
351        file.as_raw_fd(),
352        mountpoint_mode,
353        nix::unistd::getuid(),
354        nix::unistd::getgid()
355    );
356
357    for option in options
358        .iter()
359        .filter(|x| option_group(x) == MountOptionGroup::KernelOption)
360    {
361        mount_options.push(',');
362        mount_options.push_str(&option_to_string(option));
363    }
364
365    let mut flags = 0;
366    if !options.contains(&MountOption::Dev) {
367        // Default to nodev
368        #[cfg(target_os = "linux")]
369        {
370            flags |= libc::MS_NODEV;
371        }
372        #[cfg(target_os = "macos")]
373        {
374            flags |= libc::MNT_NODEV;
375        }
376    }
377    if !options.contains(&MountOption::Suid) {
378        // Default to nosuid
379        #[cfg(target_os = "linux")]
380        {
381            flags |= libc::MS_NOSUID;
382        }
383        #[cfg(target_os = "macos")]
384        {
385            flags |= libc::MNT_NOSUID;
386        }
387    }
388    for flag in options
389        .iter()
390        .filter(|x| option_group(x) == MountOptionGroup::KernelFlag)
391    {
392        flags |= option_to_flag(flag);
393    }
394
395    // Default name is "/dev/fuse", then use the subtype, and lastly prefer the name
396    let mut source = fuse_device_name;
397    if let Some(MountOption::Subtype(subtype)) = options
398        .iter()
399        .find(|x| matches!(**x, MountOption::Subtype(_)))
400    {
401        source = subtype;
402    }
403    if let Some(MountOption::FSName(name)) = options
404        .iter()
405        .find(|x| matches!(**x, MountOption::FSName(_)))
406    {
407        source = name;
408    }
409
410    let c_source = CString::new(source).unwrap();
411    let c_mountpoint = CString::new(mountpoint.as_bytes()).unwrap();
412
413    let result = unsafe {
414        #[cfg(target_os = "linux")]
415        {
416            let c_options = CString::new(mount_options).unwrap();
417            let c_type = CString::new("fuse").unwrap();
418            libc::mount(
419                c_source.as_ptr(),
420                c_mountpoint.as_ptr(),
421                c_type.as_ptr(),
422                flags,
423                c_options.as_ptr() as *const libc::c_void,
424            )
425        }
426        #[cfg(target_os = "macos")]
427        {
428            let mut c_options = CString::new(mount_options).unwrap();
429            libc::mount(
430                c_source.as_ptr(),
431                c_mountpoint.as_ptr(),
432                flags,
433                c_options.as_ptr() as *mut libc::c_void,
434            )
435        }
436    };
437    if result == -1 {
438        let err = Error::last_os_error();
439        if err.kind() == ErrorKind::PermissionDenied {
440            return Ok(None); // Retry with fusermount
441        } else {
442            return Err(Error::new(
443                err.kind(),
444                format!("Error calling mount() at {mountpoint:?}: {err}"),
445            ));
446        }
447    }
448
449    Ok(Some(file))
450}
451
452#[derive(PartialEq)]
453pub enum MountOptionGroup {
454    KernelOption,
455    KernelFlag,
456    Fusermount,
457}
458
459pub fn option_group(option: &MountOption) -> MountOptionGroup {
460    match option {
461        MountOption::FSName(_) => MountOptionGroup::Fusermount,
462        MountOption::Subtype(_) => MountOptionGroup::Fusermount,
463        MountOption::CUSTOM(_) => MountOptionGroup::KernelOption,
464        MountOption::AutoUnmount => MountOptionGroup::Fusermount,
465        MountOption::AllowOther => MountOptionGroup::KernelOption,
466        MountOption::Dev => MountOptionGroup::KernelFlag,
467        MountOption::NoDev => MountOptionGroup::KernelFlag,
468        MountOption::Suid => MountOptionGroup::KernelFlag,
469        MountOption::NoSuid => MountOptionGroup::KernelFlag,
470        MountOption::RO => MountOptionGroup::KernelFlag,
471        MountOption::RW => MountOptionGroup::KernelFlag,
472        MountOption::Exec => MountOptionGroup::KernelFlag,
473        MountOption::NoExec => MountOptionGroup::KernelFlag,
474        MountOption::Atime => MountOptionGroup::KernelFlag,
475        MountOption::NoAtime => MountOptionGroup::KernelFlag,
476        MountOption::DirSync => MountOptionGroup::KernelFlag,
477        MountOption::Sync => MountOptionGroup::KernelFlag,
478        MountOption::Async => MountOptionGroup::KernelFlag,
479        MountOption::AllowRoot => MountOptionGroup::KernelOption,
480        MountOption::DefaultPermissions => MountOptionGroup::KernelOption,
481    }
482}
483
484#[cfg(target_os = "linux")]
485pub fn option_to_flag(option: &MountOption) -> libc::c_ulong {
486    match option {
487        MountOption::Dev => 0, // There is no option for dev. It's the absence of NoDev
488        MountOption::NoDev => libc::MS_NODEV,
489        MountOption::Suid => 0,
490        MountOption::NoSuid => libc::MS_NOSUID,
491        MountOption::RW => 0,
492        MountOption::RO => libc::MS_RDONLY,
493        MountOption::Exec => 0,
494        MountOption::NoExec => libc::MS_NOEXEC,
495        MountOption::Atime => 0,
496        MountOption::NoAtime => libc::MS_NOATIME,
497        MountOption::Async => 0,
498        MountOption::Sync => libc::MS_SYNCHRONOUS,
499        MountOption::DirSync => libc::MS_DIRSYNC,
500        _ => unreachable!(),
501    }
502}
503
504#[cfg(target_os = "macos")]
505pub fn option_to_flag(option: &MountOption) -> libc::c_int {
506    match option {
507        MountOption::Dev => 0, // There is no option for dev. It's the absence of NoDev
508        MountOption::NoDev => libc::MNT_NODEV,
509        MountOption::Suid => 0,
510        MountOption::NoSuid => libc::MNT_NOSUID,
511        MountOption::RW => 0,
512        MountOption::RO => libc::MNT_RDONLY,
513        MountOption::Exec => 0,
514        MountOption::NoExec => libc::MNT_NOEXEC,
515        MountOption::Atime => 0,
516        MountOption::NoAtime => libc::MNT_NOATIME,
517        MountOption::Async => 0,
518        MountOption::Sync => libc::MNT_SYNCHRONOUS,
519        _ => unreachable!(),
520    }
521}