Skip to content

Commit 1b528af

Browse files
matthiasgoergensbergwolf
authored andcommitted
Support non-privileged users
Use libfuses's fusermount3 to mount as regular user without requiring root. With many thanks to @fzgregor Franz Gregor for the initial prototype. Fixes #76
1 parent b47a7ee commit 1b528af

File tree

1 file changed

+176
-15
lines changed

1 file changed

+176
-15
lines changed

src/transport/fusedev/linux_session.rs

Lines changed: 176 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,22 @@ use std::fs::{File, OpenOptions};
1313
use std::ops::Deref;
1414
use std::os::unix::fs::PermissionsExt;
1515
use std::os::unix::io::AsRawFd;
16+
use std::os::unix::net::UnixStream;
1617
use std::path::{Path, PathBuf};
1718
use std::sync::{Arc, Mutex};
1819

1920
use nix::errno::Errno;
20-
use nix::fcntl::{fcntl, FcntlArg, OFlag};
21+
use nix::fcntl::{fcntl, FcntlArg, FdFlag, OFlag};
2122
use nix::mount::{mount, umount2, MntFlags, MsFlags};
2223
use nix::poll::{poll, PollFd, PollFlags};
2324
use nix::sys::epoll::{epoll_ctl, EpollEvent, EpollFlags, EpollOp};
2425
use nix::unistd::{getgid, getuid, read};
2526

26-
use super::{super::pagesize, Error::SessionFailure, FuseBuf, FuseDevWriter, Reader, Result};
27+
use super::{
28+
super::pagesize,
29+
Error::{IoError, SessionFailure},
30+
FuseBuf, FuseDevWriter, Reader, Result,
31+
};
2732

2833
// These follows definition from libfuse.
2934
const FUSE_KERN_BUF_SIZE: usize = 256;
@@ -42,9 +47,12 @@ pub struct FuseSession {
4247
fsname: String,
4348
subtype: String,
4449
file: Option<File>,
50+
// Socket to keep alive / drop for fusermount's auto_unmount.
51+
keep_alive: Option<UnixStream>,
4552
bufsize: usize,
4653
readonly: bool,
4754
wakers: Mutex<Vec<Arc<Waker>>>,
55+
auto_unmount: bool,
4856
}
4957

5058
impl FuseSession {
@@ -54,6 +62,17 @@ impl FuseSession {
5462
fsname: &str,
5563
subtype: &str,
5664
readonly: bool,
65+
) -> Result<FuseSession> {
66+
FuseSession::new_with_autounmount(mountpoint, fsname, subtype, readonly, false)
67+
}
68+
69+
/// Create a new fuse session, without mounting/connecting to the in kernel fuse driver.
70+
pub fn new_with_autounmount(
71+
mountpoint: &Path,
72+
fsname: &str,
73+
subtype: &str,
74+
readonly: bool,
75+
auto_unmount: bool,
5776
) -> Result<FuseSession> {
5877
let dest = mountpoint
5978
.canonicalize()
@@ -67,9 +86,11 @@ impl FuseSession {
6786
fsname: fsname.to_owned(),
6887
subtype: subtype.to_owned(),
6988
file: None,
89+
keep_alive: None,
7090
bufsize: FUSE_KERN_BUF_SIZE * pagesize() + FUSE_HEADER_SIZE,
7191
readonly,
7292
wakers: Mutex::new(Vec::new()),
93+
auto_unmount,
7394
})
7495
}
7596

@@ -79,11 +100,18 @@ impl FuseSession {
79100
if self.readonly {
80101
flags |= MsFlags::MS_RDONLY;
81102
}
82-
let file = fuse_kern_mount(&self.mountpoint, &self.fsname, &self.subtype, flags)?;
103+
let (file, socket) = fuse_kern_mount(
104+
&self.mountpoint,
105+
&self.fsname,
106+
&self.subtype,
107+
flags,
108+
self.auto_unmount,
109+
)?;
83110

84111
fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(OFlag::O_NONBLOCK))
85112
.map_err(|e| SessionFailure(format!("set fd nonblocking: {e}")))?;
86113
self.file = Some(file);
114+
self.keep_alive = socket;
87115

88116
Ok(())
89117
}
@@ -100,7 +128,9 @@ impl FuseSession {
100128

101129
/// Destroy a fuse session.
102130
pub fn umount(&mut self) -> Result<()> {
103-
if let Some(file) = self.file.take() {
131+
// If we have a keep_alive socket, just drop it,
132+
// and let fusermount3 do the unmount.
133+
if let (None, Some(file)) = (self.keep_alive.take(), self.file.take()) {
104134
if let Some(mountpoint) = self.mountpoint.to_str() {
105135
fuse_kern_umount(mountpoint, file)
106136
} else {
@@ -310,7 +340,13 @@ impl FuseChannel {
310340
}
311341

312342
/// Mount a fuse file system
313-
fn fuse_kern_mount(mountpoint: &Path, fsname: &str, subtype: &str, flags: MsFlags) -> Result<File> {
343+
fn fuse_kern_mount(
344+
mountpoint: &Path,
345+
fsname: &str,
346+
subtype: &str,
347+
flags: MsFlags,
348+
auto_unmount: bool,
349+
) -> Result<(File, Option<UnixStream>)> {
314350
let file = OpenOptions::new()
315351
.create(false)
316352
.read(true)
@@ -343,16 +379,117 @@ fn fuse_kern_mount(mountpoint: &Path, fsname: &str, subtype: &str, flags: MsFlag
343379
file.as_raw_fd(),
344380
);
345381
}
346-
mount(
347-
Some(fsname),
348-
mountpoint,
349-
Some(fstype.deref()),
350-
flags,
351-
Some(opts.deref()),
382+
if auto_unmount {
383+
fuse_fusermount_mount(mountpoint, fsname, subtype, opts, flags, auto_unmount)
384+
} else {
385+
match mount(
386+
Some(fsname),
387+
mountpoint,
388+
Some(fstype.deref()),
389+
flags,
390+
Some(opts.deref()),
391+
) {
392+
Ok(()) => Ok((file, None)),
393+
Err(nix::errno::Errno::EPERM) => {
394+
fuse_fusermount_mount(mountpoint, fsname, subtype, opts, flags, auto_unmount)
395+
}
396+
Err(e) => Err(SessionFailure(format!(
397+
"failed to mount {mountpoint:?}: {e}"
398+
))),
399+
}
400+
}
401+
}
402+
403+
fn msflags_to_string(flags: MsFlags) -> String {
404+
[
405+
(MsFlags::MS_RDONLY, ("rw", "ro")),
406+
(MsFlags::MS_NOSUID, ("suid", "nosuid")),
407+
(MsFlags::MS_NODEV, ("dev", "nodev")),
408+
(MsFlags::MS_NOEXEC, ("exec", "noexec")),
409+
(MsFlags::MS_SYNCHRONOUS, ("async", "sync")),
410+
(MsFlags::MS_NOATIME, ("atime", "noatime")),
411+
(MsFlags::MS_NODIRATIME, ("diratime", "nodiratime")),
412+
(MsFlags::MS_LAZYTIME, ("nolazytime", "lazytime")),
413+
(MsFlags::MS_RELATIME, ("norelatime", "relatime")),
414+
(MsFlags::MS_STRICTATIME, ("nostrictatime", "strictatime")),
415+
]
416+
.map(
417+
|(flag, (neg, pos))| {
418+
if flags.contains(flag) {
419+
pos
420+
} else {
421+
neg
422+
}
423+
},
352424
)
353-
.map_err(|e| SessionFailure(format!("failed to mount {mountpoint:?}: {e}")))?;
425+
.join(",")
426+
}
354427

355-
Ok(file)
428+
/// Mount a fuse file system with fusermount
429+
fn fuse_fusermount_mount(
430+
mountpoint: &Path,
431+
fsname: &str,
432+
subtype: &str,
433+
opts: String,
434+
flags: MsFlags,
435+
auto_unmount: bool,
436+
) -> Result<(File, Option<UnixStream>)> {
437+
let mut opts = vec![format!("fsname={fsname}"), opts, msflags_to_string(flags)];
438+
if !subtype.is_empty() {
439+
opts.push(format!("subtype={subtype}"));
440+
}
441+
if auto_unmount {
442+
opts.push("auto_unmount".to_owned());
443+
}
444+
let opts = opts.join(",");
445+
446+
let (send, recv) = UnixStream::pair().unwrap();
447+
448+
// Keep the sending socket around after exec to pass to fusermount3.
449+
// When its partner recv closes, fusermount3 will unmount.
450+
// Remove the close-on-exec flag from the socket, so we can pass it to
451+
// fusermount3.
452+
nix::fcntl::fcntl(send.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::empty()))
453+
.map_err(|e| SessionFailure(format!("Failed to remove close-on-exec flag: {e}")))?;
454+
455+
let mut proc = std::process::Command::new("fusermount3")
456+
.env("_FUSE_COMMFD", format!("{}", send.as_raw_fd()))
457+
// Old version of fusermount doesn't support long --options, yet.
458+
.arg("-o")
459+
.arg(opts)
460+
.arg("--")
461+
.arg(mountpoint)
462+
.spawn()
463+
.map_err(IoError)?;
464+
if auto_unmount {
465+
std::thread::spawn(move || {
466+
let _ = proc.wait();
467+
});
468+
} else {
469+
match proc.wait().map_err(IoError)?.code() {
470+
Some(0) => {}
471+
exit_code => {
472+
return Err(SessionFailure(format!(
473+
"Unexpected exit code when running fusermount3: {exit_code:?}"
474+
)))
475+
}
476+
}
477+
}
478+
drop(send);
479+
480+
match vmm_sys_util::sock_ctrl_msg::ScmSocket::recv_with_fd(&recv, &mut [0u8; 8]).map_err(
481+
|e| {
482+
SessionFailure(format!(
483+
"Unexpected error when receiving fuse file descriptor from fusermount3: {}",
484+
e
485+
))
486+
},
487+
)? {
488+
(_recv_bytes, Some(file)) => Ok((file, if auto_unmount { Some(recv) } else { None })),
489+
(recv_bytes, None) => Err(SessionFailure(format!(
490+
"fusermount3 did not send a file descriptor. We received {recv_bytes} bytes."
491+
))),
492+
}
356493
}
357494

358495
/// Umount a fuse file system
@@ -372,8 +509,32 @@ fn fuse_kern_umount(mountpoint: &str, file: File) -> Result<()> {
372509
// Drop to close fuse session fd, otherwise synchronous umount can recurse into filesystem and
373510
// cause deadlock.
374511
drop(file);
375-
umount2(mountpoint, MntFlags::MNT_DETACH)
376-
.map_err(|e| SessionFailure(format!("failed to umount {mountpoint}: {e}")))
512+
match umount2(mountpoint, MntFlags::MNT_DETACH) {
513+
Ok(()) => Ok(()),
514+
Err(nix::errno::Errno::EPERM) => fuse_fusermount3_umount(mountpoint),
515+
Err(e) => Err(SessionFailure(format!(
516+
"failed to umount {mountpoint}: {e}"
517+
))),
518+
}
519+
}
520+
521+
/// Umount a fuse file system
522+
fn fuse_fusermount3_umount(mountpoint: &str) -> Result<()> {
523+
match std::process::Command::new("fusermount3")
524+
.arg("--unmount")
525+
.arg("--quiet")
526+
.arg("--lazy")
527+
.arg("--")
528+
.arg(mountpoint)
529+
.status()
530+
.map_err(IoError)?
531+
.code()
532+
{
533+
Some(0) => Ok(()),
534+
exit_code => Err(SessionFailure(format!(
535+
"Unexpected exit code when unmounting via running fusermount3: {exit_code:?}"
536+
))),
537+
}
377538
}
378539

379540
#[cfg(test)]

0 commit comments

Comments
 (0)