Skip to content

Commit 12f05bb

Browse files
committed
Make the use of fusermount(1) explicit
There are multiple ways to mount FUSE filesystems. Processes with elevated privileges (CAP_SYS_ADMIN) can use the mount(2) syscall directly or via a suitable helper. libfuse also installs a helper program, fusermount(1), which is setuid root, to perform this mounting operation. Previously, we would attempt the syscall, and then fallback to trying fusermount (if we can find it) automatically. This is a little bit too magical, so it's better to make the option explicit.
1 parent 5bd35f3 commit 12f05bb

File tree

8 files changed

+146
-58
lines changed

8 files changed

+146
-58
lines changed

examples/hello.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -145,5 +145,10 @@ fn main() {
145145
if matches.get_flag("allow-root") {
146146
options.push(MountOption::AllowRoot);
147147
}
148-
fuser::mount2(HelloFS, mountpoint, &options).unwrap();
148+
149+
if options.contains(&MountOption::AutoUnmount) {
150+
fuser::fusermount(HelloFS, mountpoint, &options).unwrap();
151+
} else {
152+
fuser::mount2(HelloFS, mountpoint, &options).unwrap();
153+
}
149154
}

examples/simple.rs

+14-9
Original file line numberDiff line numberDiff line change
@@ -2039,21 +2039,26 @@ fn main() {
20392039
.unwrap()
20402040
.to_string();
20412041

2042-
let result = fuser::mount2(
2043-
SimpleFS::new(
2044-
data_dir,
2045-
matches.get_flag("direct-io"),
2046-
matches.get_flag("suid"),
2047-
),
2048-
mountpoint,
2049-
&options,
2042+
let fs = SimpleFS::new(
2043+
data_dir,
2044+
matches.get_flag("direct-io"),
2045+
matches.get_flag("suid"),
20502046
);
2047+
let result = if options.contains(&MountOption::AutoUnmount) {
2048+
fuser::fusermount(fs, mountpoint, &options)
2049+
} else {
2050+
fuser::mount2(fs, mountpoint, &options)
2051+
};
2052+
20512053
if let Err(e) = result {
2054+
error!("{}", e.to_string());
2055+
20522056
// Return a special error code for permission denied, which usually indicates that
20532057
// "user_allow_other" is missing from /etc/fuse.conf
20542058
if e.kind() == ErrorKind::PermissionDenied {
2055-
error!("{}", e.to_string());
20562059
std::process::exit(2);
2060+
} else {
2061+
std::process::exit(1);
20572062
}
20582063
}
20592064
}

mount_tests.sh

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ function run_allow_root_test {
1818
useradd fusertest1
1919
useradd fusertest2
2020
DIR=$(su fusertest1 -c "mktemp --directory")
21-
cargo build --example hello --features libfuse,abi-7-30 > /dev/null 2>&1
22-
su fusertest1 -c "target/debug/examples/hello $DIR --allow-root" &
21+
cargo build --example hello --features abi-7-30 > /dev/null 2>&1
22+
su fusertest1 -c "target/debug/examples/hello $DIR --allow-root --auto_unmount" &
2323
FUSE_PID=$!
2424
sleep 2
2525

@@ -62,7 +62,7 @@ function test_no_user_allow_other {
6262
DIR=$(su fusertestnoallow -c "mktemp --directory")
6363
DATA_DIR=$(su fusertestnoallow -c "mktemp --directory")
6464
cargo build --example simple $1 > /dev/null 2>&1
65-
su fusertestnoallow -c "target/debug/examples/simple -vvv --data-dir $DATA_DIR --mount-point $DIR"
65+
su fusertestnoallow -c "target/debug/examples/simple -vvv --data-dir $DATA_DIR --mount-point $DIR --auto_unmount"
6666
exitCode=$?
6767
if [[ $exitCode -eq 2 ]]; then
6868
echo -e "$GREEN OK Detected lack of user_allow_other: $2 $NC"

src/lib.rs

+36-4
Original file line numberDiff line numberDiff line change
@@ -1016,15 +1016,28 @@ pub fn mount2<FS: Filesystem, P: AsRef<Path>>(
10161016
mountpoint: P,
10171017
options: &[MountOption],
10181018
) -> io::Result<()> {
1019-
check_option_conflicts(options)?;
1019+
check_option_conflicts(options, false)?;
10201020
Session::new(filesystem, mountpoint.as_ref(), options).and_then(|mut se| se.run())
10211021
}
10221022

1023+
/// Mount the given filesystem using fusermount(1). The binary must exist on
1024+
/// the system and be setuid root.
1025+
pub fn fusermount(
1026+
filesystem: impl Filesystem,
1027+
mountpoint: impl AsRef<Path>,
1028+
options: &[MountOption],
1029+
) -> io::Result<()> {
1030+
check_option_conflicts(options, true)?;
1031+
Session::new_fusermount(filesystem, mountpoint, options).and_then(|mut se| se.run())
1032+
}
1033+
10231034
/// Mount the given filesystem to the given mountpoint. This function spawns
10241035
/// a background thread to handle filesystem operations while being mounted
10251036
/// and therefore returns immediately. The returned handle should be stored
10261037
/// to reference the mounted filesystem. If it's dropped, the filesystem will
10271038
/// be unmounted.
1039+
///
1040+
/// This function requires CAP_SYS_ADMIN to run.
10281041
#[deprecated(note = "use spawn_mount2() instead")]
10291042
pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef<Path>>(
10301043
filesystem: FS,
@@ -1036,7 +1049,7 @@ pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef<Path>>(
10361049
.map(|x| Some(MountOption::from_str(x.to_str()?)))
10371050
.collect();
10381051
let options = options.ok_or(ErrorKind::InvalidData)?;
1039-
Session::new(filesystem, mountpoint.as_ref(), options.as_ref()).and_then(|se| se.spawn())
1052+
spawn_mount2(filesystem, mountpoint, &options)
10401053
}
10411054

10421055
/// Mount the given filesystem to the given mountpoint. This function spawns
@@ -1045,12 +1058,31 @@ pub fn spawn_mount<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef<Path>>(
10451058
/// to reference the mounted filesystem. If it's dropped, the filesystem will
10461059
/// be unmounted.
10471060
///
1048-
/// NOTE: This is the corresponding function to mount2.
1061+
/// NOTE: This is the corresponding function to [mount2], and likewise requires
1062+
/// CAP_SYS_ADMIN.
10491063
pub fn spawn_mount2<'a, FS: Filesystem + Send + 'static + 'a, P: AsRef<Path>>(
10501064
filesystem: FS,
10511065
mountpoint: P,
10521066
options: &[MountOption],
10531067
) -> io::Result<BackgroundSession> {
1054-
check_option_conflicts(options)?;
1068+
check_option_conflicts(options, false)?;
10551069
Session::new(filesystem, mountpoint.as_ref(), options).and_then(|se| se.spawn())
10561070
}
1071+
1072+
/// Mount the given filesystem to the given mountpoint. This function spawns
1073+
/// a background thread to handle filesystem operations while being mounted
1074+
/// and therefore returns immediately. The returned handle should be stored
1075+
/// to reference the mounted filesystem. If it's dropped, the filesystem will
1076+
/// be unmounted.
1077+
///
1078+
/// NOTE: This is the corresponding function to [fusermount]. Unlike
1079+
/// [spawn_mount], this uses fusermount(1), which must be present on the
1080+
/// system and setuid root, to circumvent the need for elevated priveleges.
1081+
pub fn spawn_fusermount<'a, FS: Filesystem + Send + 'static + 'a>(
1082+
filesystem: FS,
1083+
mountpoint: impl AsRef<Path>,
1084+
options: &[MountOption],
1085+
) -> io::Result<BackgroundSession> {
1086+
check_option_conflicts(options, true)?;
1087+
Session::new_fusermount(filesystem, mountpoint, options).and_then(|se| se.spawn())
1088+
}

src/mount_options.rs

+16-3
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,23 @@ impl MountOption {
8686
}
8787
}
8888

89-
pub(crate) fn check_option_conflicts(options: &[MountOption]) -> Result<(), io::Error> {
89+
pub(crate) fn check_option_conflicts(
90+
options: &[MountOption],
91+
allow_auto_unmount: bool,
92+
) -> Result<(), io::Error> {
9093
let mut options_set = HashSet::new();
9194
options_set.extend(options.iter().cloned());
95+
96+
if !allow_auto_unmount && options_set.contains(&MountOption::AutoUnmount) {
97+
return Err(io::Error::new(
98+
ErrorKind::InvalidInput,
99+
"AutoUnmount requires the use of fusermount",
100+
));
101+
}
102+
92103
let conflicting: HashSet<MountOption> = options.iter().flat_map(conflicts_with).collect();
93104
let intersection: Vec<MountOption> = conflicting.intersection(&options_set).cloned().collect();
105+
94106
if !intersection.is_empty() {
95107
Err(io::Error::new(
96108
ErrorKind::InvalidInput,
@@ -188,8 +200,9 @@ mod test {
188200

189201
#[test]
190202
fn option_checking() {
191-
assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoSuid]).is_err());
192-
assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoExec]).is_ok());
203+
assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoSuid], true).is_err());
204+
assert!(check_option_conflicts(&[MountOption::Suid, MountOption::NoExec], false).is_ok());
205+
assert!(check_option_conflicts(&[MountOption::AutoUnmount], true).is_ok());
193206
}
194207
#[test]
195208
fn option_round_trip() {

src/session.rs

+42-17
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,17 @@ use libc::{EAGAIN, EINTR, ENODEV, ENOENT};
99
use log::{info, warn};
1010
use nix::unistd::geteuid;
1111
use std::fmt;
12+
use std::os::fd::OwnedFd;
1213
use std::path::{Path, PathBuf};
1314
use std::sync::{Arc, Mutex};
1415
use std::thread::{self, JoinHandle};
1516
use std::{io, ops::DerefMut};
1617

17-
use crate::channel::Channel;
1818
use crate::ll::fuse_abi as abi;
1919
use crate::request::Request;
20-
use crate::sys;
2120
use crate::Filesystem;
2221
use crate::MountOption;
23-
22+
use crate::{channel::Channel, sys::Mount};
2423
#[cfg(feature = "abi-7-11")]
2524
use crate::{channel::ChannelSender, notify::Notifier};
2625

@@ -48,7 +47,7 @@ pub struct Session<FS: Filesystem> {
4847
/// Communication channel to the kernel driver
4948
ch: Channel,
5049
/// Handle to the mount. Dropping this unmounts.
51-
mount: Arc<Mutex<Option<sys::Mount>>>,
50+
mount: Arc<Mutex<Option<Mount>>>,
5251
/// Mount point
5352
mountpoint: PathBuf,
5453
/// Whether to restrict access to owner, root + owner, or unrestricted
@@ -67,15 +66,28 @@ pub struct Session<FS: Filesystem> {
6766
}
6867

6968
impl<FS: Filesystem> Session<FS> {
70-
/// Create a new session by mounting the given filesystem to the given mountpoint
71-
pub fn new<P: AsRef<Path>>(
69+
/// Create a new session by mounting the given filesystem to the given
70+
/// mountpoint. Uses the mount syscall, which requires CAP_SYS_ADMIN.
71+
pub fn new(
7272
filesystem: FS,
73-
mountpoint: P,
73+
mountpoint: impl AsRef<Path>,
7474
options: &[MountOption],
75-
) -> io::Result<Session<FS>> {
76-
let mountpoint = mountpoint.as_ref();
77-
info!("Mounting {}", mountpoint.display());
75+
) -> io::Result<Self> {
76+
let (device_fd, mount) = Mount::new_sys(mountpoint.as_ref(), options)?;
77+
78+
Ok(Self::new_inner(
79+
filesystem, mountpoint, device_fd, mount, options,
80+
))
81+
}
7882

83+
/// Create a new session by mounting the given filesystem to the given
84+
/// mountpoint. Uses fusermount(1), which must exist on the system and be
85+
/// setuid root, to circumvent the elevated privileges required by [Session::new].
86+
pub fn new_fusermount(
87+
filesystem: FS,
88+
mountpoint: impl AsRef<Path>,
89+
options: &[MountOption],
90+
) -> io::Result<Session<FS>> {
7991
// If AutoUnmount is requested, but not AllowRoot or AllowOther we enforce the ACL
8092
// ourself and implicitly set AllowOther because fusermount needs allow_root or allow_other
8193
// to handle the auto_unmount option
@@ -86,12 +98,25 @@ impl<FS: Filesystem> Session<FS> {
8698
warn!("Given auto_unmount without allow_root or allow_other; adding allow_other, with userspace permission handling");
8799
let mut modified_options = options.to_vec();
88100
modified_options.push(MountOption::AllowOther);
89-
sys::Mount::new(mountpoint, &modified_options)?
101+
Mount::new_fusermount(mountpoint.as_ref(), &modified_options)?
90102
} else {
91-
sys::Mount::new(mountpoint, options)?
103+
Mount::new_fusermount(mountpoint.as_ref(), options)?
92104
};
93105

106+
Ok(Self::new_inner(
107+
filesystem, mountpoint, device_fd, mount, options,
108+
))
109+
}
110+
111+
fn new_inner(
112+
filesystem: FS,
113+
mountpoint: impl AsRef<Path>,
114+
device_fd: OwnedFd,
115+
mount: Mount,
116+
options: &[MountOption],
117+
) -> Self {
94118
let ch = Channel::new(Arc::new(device_fd));
119+
95120
let allowed = if options.contains(&MountOption::AllowRoot) {
96121
SessionACL::RootAndOwner
97122
} else if options.contains(&MountOption::AllowOther) {
@@ -100,18 +125,18 @@ impl<FS: Filesystem> Session<FS> {
100125
SessionACL::Owner
101126
};
102127

103-
Ok(Session {
128+
Session {
104129
filesystem,
105130
ch,
106131
mount: Arc::new(Mutex::new(Some(mount))),
107-
mountpoint: mountpoint.to_owned(),
132+
mountpoint: mountpoint.as_ref().to_owned(),
108133
allowed,
109134
session_owner: geteuid().as_raw(),
110135
proto_major: 0,
111136
proto_minor: 0,
112137
initialized: false,
113138
destroyed: false,
114-
})
139+
}
115140
}
116141

117142
/// Return path of the mounted filesystem
@@ -180,7 +205,7 @@ impl<FS: Filesystem> Session<FS> {
180205
#[derive(Debug)]
181206
/// A thread-safe object that can be used to unmount a Filesystem
182207
pub struct SessionUnmounter {
183-
mount: Arc<Mutex<Option<sys::Mount>>>,
208+
mount: Arc<Mutex<Option<Mount>>>,
184209
}
185210

186211
impl SessionUnmounter {
@@ -227,7 +252,7 @@ pub struct BackgroundSession {
227252
#[cfg(feature = "abi-7-11")]
228253
sender: ChannelSender,
229254
/// Ensures the filesystem is unmounted when the session ends
230-
_mount: sys::Mount,
255+
_mount: Mount,
231256
}
232257

233258
impl BackgroundSession {

src/sys.rs

+28-20
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,33 @@ pub(crate) struct Mount {
6161
}
6262

6363
impl Mount {
64-
pub fn new(mountpoint: &Path, options: &[MountOption]) -> io::Result<(OwnedFd, Self)> {
65-
let mountpoint = mountpoint.canonicalize()?;
66-
let (fd, sock) = mount(mountpoint.as_os_str(), options)?;
64+
pub fn new_sys(
65+
mountpoint: impl AsRef<Path>,
66+
options: &[MountOption],
67+
) -> io::Result<(OwnedFd, Self)> {
68+
let mountpoint = mountpoint.as_ref().canonicalize()?;
69+
let fd = mount_sys(mountpoint.as_os_str(), options)?;
70+
71+
// Make a dup of the fuse device FD, so we can poll if the filesystem
72+
// is still mounted.
73+
let fuse_device = fd.as_fd().try_clone_to_owned()?;
74+
75+
Ok((
76+
fd,
77+
Self {
78+
mountpoint: CString::new(mountpoint.as_os_str().as_bytes())?,
79+
fuse_device,
80+
auto_unmount_socket: None,
81+
},
82+
))
83+
}
84+
85+
pub fn new_fusermount(
86+
mountpoint: impl AsRef<Path>,
87+
options: &[MountOption],
88+
) -> io::Result<(OwnedFd, Self)> {
89+
let mountpoint = mountpoint.as_ref().canonicalize()?;
90+
let (fd, sock) = mount_fusermount(mountpoint.as_os_str(), options)?;
6791

6892
// Make a dup of the fuse device FD, so we can poll if the filesystem
6993
// is still mounted.
@@ -106,22 +130,6 @@ impl Drop for Mount {
106130
}
107131
}
108132

109-
fn mount(mountpoint: &OsStr, options: &[MountOption]) -> io::Result<(OwnedFd, Option<UnixStream>)> {
110-
if options.contains(&MountOption::AutoUnmount) {
111-
// Auto unmount is only supported via fusermount
112-
return mount_fusermount(mountpoint, options);
113-
}
114-
115-
match mount_sys(mountpoint, options) {
116-
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
117-
// Retry
118-
mount_fusermount(mountpoint, options)
119-
}
120-
Err(e) => Err(e),
121-
Ok(fd) => Ok((fd, None)),
122-
}
123-
}
124-
125133
fn fuse_unmount_pure(mountpoint: &CStr) {
126134
#[cfg(target_os = "linux")]
127135
unsafe {
@@ -604,7 +612,7 @@ mod test {
604612
// deadlock.
605613
let tmp = ManuallyDrop::new(tempfile::tempdir().unwrap());
606614
let device_fd = open_device().unwrap();
607-
let mount = Mount::new(tmp.path(), &[]).unwrap();
615+
let mount = Mount::new_fusermount(tmp.path(), &[]).unwrap();
608616

609617
let mnt = cmd_mount();
610618
eprintln!("Our mountpoint: {:?}\nfuse mounts:\n{}", tmp.path(), mnt,);

tests/integration_tests.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fn unmount_no_send() {
1313
impl Filesystem for NoSendFS {}
1414

1515
let tmpdir: TempDir = tempfile::tempdir().unwrap();
16-
let mut session = Session::new(NoSendFS(Rc::new(())), tmpdir.path(), &[]).unwrap();
16+
let mut session = Session::new_fusermount(NoSendFS(Rc::new(())), tmpdir.path(), &[]).unwrap();
1717
let mut unmounter = session.unmount_callable();
1818
thread::spawn(move || {
1919
thread::sleep(Duration::from_secs(1));

0 commit comments

Comments
 (0)