use std::fs::{File, OpenOptions};
use std::ops::Deref;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::io::AsRawFd;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::transport::fusedev::FuseSessionExt;
use mio::{Events, Poll, Token, Waker};
use nix::errno::Errno;
use nix::fcntl::{fcntl, FcntlArg, FdFlag, OFlag};
use nix::mount::{mount, umount2, MntFlags, MsFlags};
use nix::poll::{poll, PollFd, PollFlags};
use nix::sys::epoll::{epoll_ctl, EpollEvent, EpollFlags, EpollOp};
use nix::unistd::{getgid, getuid, read};
use super::{
super::pagesize,
Error::{IoError, SessionFailure},
FuseBuf, FuseDevWriter, Reader, Result, FUSE_HEADER_SIZE, FUSE_KERN_BUF_PAGES,
};
const POLL_EVENTS_CAPACITY: usize = 1024;
const FUSE_DEVICE: &str = "/dev/fuse";
const FUSE_FSTYPE: &str = "fuse";
const FUSERMOUNT_BIN: &str = "fusermount3";
const EXIT_FUSE_EVENT: Token = Token(0);
const FUSE_DEV_EVENT: Token = Token(1);
pub struct FuseSession {
mountpoint: PathBuf,
fsname: String,
subtype: String,
file: Option<File>,
keep_alive: Option<UnixStream>,
bufsize: usize,
readonly: bool,
wakers: Mutex<Vec<Arc<Waker>>>,
auto_unmount: bool,
allow_other: bool,
target_mntns: Option<libc::pid_t>,
fusermount: String,
}
impl FuseSession {
pub fn new(
mountpoint: &Path,
fsname: &str,
subtype: &str,
readonly: bool,
) -> Result<FuseSession> {
FuseSession::new_with_autounmount(mountpoint, fsname, subtype, readonly, false)
}
pub fn new_with_autounmount(
mountpoint: &Path,
fsname: &str,
subtype: &str,
readonly: bool,
auto_unmount: bool,
) -> Result<FuseSession> {
let dest = mountpoint
.canonicalize()
.map_err(|_| SessionFailure(format!("invalid mountpoint {mountpoint:?}")))?;
if !dest.is_dir() {
return Err(SessionFailure(format!("{dest:?} is not a directory")));
}
Ok(FuseSession {
mountpoint: dest,
fsname: fsname.to_owned(),
subtype: subtype.to_owned(),
file: None,
keep_alive: None,
bufsize: FUSE_KERN_BUF_PAGES * pagesize() + FUSE_HEADER_SIZE,
readonly,
wakers: Mutex::new(Vec::new()),
auto_unmount,
target_mntns: None,
fusermount: FUSERMOUNT_BIN.to_string(),
allow_other: true,
})
}
pub fn set_target_mntns(&mut self, pid: Option<libc::pid_t>) {
self.target_mntns = pid;
}
pub fn set_fusermount(&mut self, bin: &str) {
self.fusermount = bin.to_string();
}
pub fn set_allow_other(&mut self, allow_other: bool) {
self.allow_other = allow_other;
}
pub fn get_fusermount(&self) -> &str {
self.fusermount.as_str()
}
pub fn get_fuse_file(&self) -> Option<&File> {
self.file.as_ref()
}
pub fn set_fuse_file(&mut self, file: File) {
self.file = Some(file);
}
pub fn clone_fuse_file(&self) -> Result<File> {
let mut old_fd = self
.file
.as_ref()
.ok_or(SessionFailure(
"fuse session file doesn't exist".to_string(),
))?
.as_raw_fd();
let cloned_file = OpenOptions::new()
.create(false)
.read(true)
.write(true)
.open(FUSE_DEVICE)
.map_err(|e| SessionFailure(format!("open {FUSE_DEVICE}: {e}")))?;
nix::ioctl_read!(clone_fuse_fd, 229, 0, i32);
unsafe { clone_fuse_fd(cloned_file.as_raw_fd(), (&mut old_fd) as *mut i32) }
.map_err(|e| SessionFailure(format!("failed to clone fuse file: {:?}", e)))?;
Ok(cloned_file)
}
pub fn mountpoint(&self) -> &Path {
&self.mountpoint
}
pub fn fsname(&self) -> &str {
&self.fsname
}
pub fn subtype(&self) -> &str {
&self.subtype
}
pub fn bufsize(&self) -> usize {
self.bufsize
}
pub fn mount(&mut self) -> Result<()> {
let mut flags = MsFlags::MS_NOSUID | MsFlags::MS_NODEV | MsFlags::MS_NOATIME;
if self.readonly {
flags |= MsFlags::MS_RDONLY;
}
let (file, socket) = fuse_kern_mount(
&self.mountpoint,
&self.fsname,
&self.subtype,
flags,
self.auto_unmount,
self.allow_other,
self.target_mntns,
&self.fusermount,
)?;
fcntl(file.as_raw_fd(), FcntlArg::F_SETFL(OFlag::O_NONBLOCK))
.map_err(|e| SessionFailure(format!("set fd nonblocking: {e}")))?;
self.file = Some(file);
self.keep_alive = socket;
Ok(())
}
pub fn umount(&mut self) -> Result<()> {
if let (None, Some(file)) = (self.keep_alive.take(), self.file.take()) {
if let Some(mountpoint) = self.mountpoint.to_str() {
fuse_kern_umount(mountpoint, file, self.fusermount.as_str())
} else {
Err(SessionFailure("invalid mountpoint".to_string()))
}
} else {
Ok(())
}
}
pub fn new_channel(&self) -> Result<FuseChannel> {
if let Some(file) = &self.file {
let file = file
.try_clone()
.map_err(|e| SessionFailure(format!("dup fd: {e}")))?;
let channel = FuseChannel::new(file, self.bufsize)?;
let waker = channel.get_waker();
self.add_waker(waker)?;
Ok(channel)
} else {
Err(SessionFailure("invalid fuse session".to_string()))
}
}
pub fn wake(&self) -> Result<()> {
let wakers = self
.wakers
.lock()
.map_err(|e| SessionFailure(format!("lock wakers: {e}")))?;
for waker in wakers.iter() {
waker
.wake()
.map_err(|e| SessionFailure(format!("wake channel: {e}")))?;
}
Ok(())
}
fn add_waker(&self, waker: Arc<Waker>) -> Result<()> {
let mut wakers = self
.wakers
.lock()
.map_err(|e| SessionFailure(format!("lock wakers: {e}")))?;
wakers.push(waker);
Ok(())
}
}
impl Drop for FuseSession {
fn drop(&mut self) {
let _ = self.umount();
}
}
impl FuseSessionExt for FuseSession {
fn file(&self) -> Option<&File> {
self.file.as_ref()
}
fn bufsize(&self) -> usize {
self.bufsize
}
}
pub struct FuseChannel {
file: File,
poll: Poll,
waker: Arc<Waker>,
buf: Vec<u8>,
}
impl FuseChannel {
fn new(file: File, bufsize: usize) -> Result<Self> {
let poll = Poll::new().map_err(|e| SessionFailure(format!("epoll create: {e}")))?;
let waker = Waker::new(poll.registry(), EXIT_FUSE_EVENT)
.map_err(|e| SessionFailure(format!("epoll register session fd: {e}")))?;
let waker = Arc::new(waker);
let epoll = poll.as_raw_fd();
let mut event = EpollEvent::new(EpollFlags::EPOLLIN, usize::from(FUSE_DEV_EVENT) as u64);
epoll_ctl(
epoll,
EpollOp::EpollCtlAdd,
file.as_raw_fd(),
Some(&mut event),
)
.map_err(|e| SessionFailure(format!("epoll register channel fd: {e}")))?;
Ok(FuseChannel {
file,
poll,
waker,
buf: vec![0x0u8; bufsize],
})
}
fn get_waker(&self) -> Arc<Waker> {
self.waker.clone()
}
pub fn get_request(&mut self) -> Result<Option<(Reader<'_>, FuseDevWriter<'_>)>> {
let mut events = Events::with_capacity(POLL_EVENTS_CAPACITY);
let mut need_exit = false;
loop {
let mut fusereq_available = false;
match self.poll.poll(&mut events, None) {
Ok(_) => {}
Err(ref e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
Err(e) => return Err(SessionFailure(format!("epoll wait: {e}"))),
}
for event in events.iter() {
if event.is_readable() {
match event.token() {
EXIT_FUSE_EVENT => need_exit = true,
FUSE_DEV_EVENT => fusereq_available = true,
x => {
error!("unexpected epoll event");
return Err(SessionFailure(format!("unexpected epoll event: {}", x.0)));
}
}
} else if event.is_error() {
info!("FUSE channel already closed!");
return Err(SessionFailure("epoll error".to_string()));
} else {
panic!("unknown epoll result events");
}
}
if need_exit {
info!("Will exit from fuse service");
return Ok(None);
}
if fusereq_available {
let fd = self.file.as_raw_fd();
match read(fd, &mut self.buf) {
Ok(len) => {
let buf = unsafe {
std::slice::from_raw_parts_mut(self.buf.as_mut_ptr(), self.buf.len())
};
let reader =
Reader::from_fuse_buffer(FuseBuf::new(&mut self.buf[..len])).unwrap();
let writer = FuseDevWriter::new(fd, buf).unwrap();
return Ok(Some((reader, writer)));
}
Err(e) => match e {
Errno::ENOENT => {
trace!("restart reading due to ENOENT");
continue;
}
Errno::EAGAIN => {
trace!("restart reading due to EAGAIN");
continue;
}
Errno::EINTR => {
trace!("syscall interrupted");
continue;
}
Errno::ENODEV => {
info!("fuse filesystem umounted");
return Ok(None);
}
e => {
warn! {"read fuse dev failed on fd {}: {}", fd, e};
return Err(SessionFailure(format!("read new request: {e:?}")));
}
},
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn fuse_kern_mount(
mountpoint: &Path,
fsname: &str,
subtype: &str,
flags: MsFlags,
auto_unmount: bool,
allow_other: bool,
target_mntns: Option<libc::pid_t>,
fusermount: &str,
) -> Result<(File, Option<UnixStream>)> {
let file = OpenOptions::new()
.create(false)
.read(true)
.write(true)
.open(FUSE_DEVICE)
.map_err(|e| SessionFailure(format!("open {FUSE_DEVICE}: {e}")))?;
let meta = mountpoint
.metadata()
.map_err(|e| SessionFailure(format!("stat {mountpoint:?}: {e}")))?;
let max_read = FUSE_KERN_BUF_PAGES * pagesize() + FUSE_HEADER_SIZE;
let mut opts = format!(
"default_permissions,fd={},rootmode={:o},user_id={},group_id={},max_read={}",
file.as_raw_fd(),
meta.permissions().mode() & libc::S_IFMT,
getuid(),
getgid(),
max_read
);
if allow_other {
opts.push_str(",allow_other");
}
let mut fstype = String::from(FUSE_FSTYPE);
if !subtype.is_empty() {
fstype.push('.');
fstype.push_str(subtype);
}
if let Some(mountpoint) = mountpoint.to_str() {
info!(
"mount source {} dest {} with fstype {} opts {} fd {}",
fsname,
mountpoint,
fstype,
opts,
file.as_raw_fd(),
);
}
if auto_unmount || target_mntns.is_some() {
fuse_fusermount_mount(
mountpoint,
fsname,
subtype,
opts,
flags,
auto_unmount,
target_mntns,
fusermount,
)
} else {
match mount(
Some(fsname),
mountpoint,
Some(fstype.deref()),
flags,
Some(opts.deref()),
) {
Ok(()) => Ok((file, None)),
Err(Errno::EPERM) => fuse_fusermount_mount(
mountpoint,
fsname,
subtype,
opts,
flags,
auto_unmount,
target_mntns,
fusermount,
),
Err(e) => Err(SessionFailure(format!(
"failed to mount {mountpoint:?}: {e}"
))),
}
}
}
fn msflags_to_string(flags: MsFlags) -> String {
[
(MsFlags::MS_RDONLY, ("rw", "ro")),
(MsFlags::MS_NOSUID, ("suid", "nosuid")),
(MsFlags::MS_NODEV, ("dev", "nodev")),
(MsFlags::MS_NOEXEC, ("exec", "noexec")),
(MsFlags::MS_SYNCHRONOUS, ("async", "sync")),
(MsFlags::MS_NOATIME, ("atime", "noatime")),
]
.map(
|(flag, (neg, pos))| {
if flags.contains(flag) {
pos
} else {
neg
}
},
)
.join(",")
}
#[allow(clippy::too_many_arguments)]
fn fuse_fusermount_mount(
mountpoint: &Path,
fsname: &str,
subtype: &str,
opts: String,
flags: MsFlags,
auto_unmount: bool,
target_mntns: Option<libc::pid_t>,
fusermount: &str,
) -> Result<(File, Option<UnixStream>)> {
let mut opts = vec![format!("fsname={fsname}"), opts, msflags_to_string(flags)];
if !subtype.is_empty() {
opts.push(format!("subtype={subtype}"));
}
if auto_unmount {
opts.push("auto_unmount".to_owned());
}
let opts = opts.join(",");
let (send, recv) = UnixStream::pair().unwrap();
fcntl(send.as_raw_fd(), FcntlArg::F_SETFD(FdFlag::empty()))
.map_err(|e| SessionFailure(format!("Failed to remove close-on-exec flag: {e}")))?;
let mut cmd = match target_mntns {
Some(pid) => {
let mut c = std::process::Command::new("nsenter");
c.arg("-t")
.arg(format!("{}", pid))
.arg("-m")
.arg(fusermount);
c
}
None => std::process::Command::new(fusermount),
};
let mut proc = cmd
.env("_FUSE_COMMFD", format!("{}", send.as_raw_fd()))
.arg("-o")
.arg(opts)
.arg("--")
.arg(mountpoint)
.spawn()
.map_err(IoError)?;
if auto_unmount {
std::thread::spawn(move || {
let _ = proc.wait();
});
} else {
match proc.wait().map_err(IoError)?.code() {
Some(0) => {}
exit_code => {
return Err(SessionFailure(format!(
"Unexpected exit code when running fusermount: {exit_code:?}"
)))
}
}
}
drop(send);
match vmm_sys_util::sock_ctrl_msg::ScmSocket::recv_with_fd(&recv, &mut [0u8; 8]).map_err(
|e| {
SessionFailure(format!(
"Unexpected error when receiving fuse file descriptor from fusermount: {}",
e
))
},
)? {
(_recv_bytes, Some(file)) => Ok((file, if auto_unmount { Some(recv) } else { None })),
(recv_bytes, None) => Err(SessionFailure(format!(
"fusermount did not send a file descriptor. We received {recv_bytes} bytes."
))),
}
}
fn fuse_kern_umount(mountpoint: &str, file: File, fusermount: &str) -> Result<()> {
let mut fds = [PollFd::new(file.as_raw_fd(), PollFlags::empty())];
if poll(&mut fds, 0).is_ok() {
if let Some(event) = fds[0].revents() {
if event == PollFlags::POLLERR {
return Ok(());
}
}
}
drop(file);
match umount2(mountpoint, MntFlags::MNT_DETACH) {
Ok(()) => Ok(()),
Err(Errno::EPERM) => fuse_fusermount_umount(mountpoint, fusermount),
Err(e) => Err(SessionFailure(format!(
"failed to umount {mountpoint}: {e}"
))),
}
}
fn fuse_fusermount_umount(mountpoint: &str, fusermount: &str) -> Result<()> {
match std::process::Command::new(fusermount)
.arg("--unmount")
.arg("--quiet")
.arg("--lazy")
.arg("--")
.arg(mountpoint)
.status()
.map_err(IoError)?
.code()
{
Some(0) => Ok(()),
exit_code => Err(SessionFailure(format!(
"Unexpected exit code when unmounting via running fusermount: {exit_code:?}"
))),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
use std::os::unix::io::FromRawFd;
use std::path::Path;
use vmm_sys_util::tempdir::TempDir;
#[test]
fn test_new_session() {
let se = FuseSession::new(Path::new("haha"), "foo", "bar", true);
assert!(se.is_err());
let dir = TempDir::new().unwrap();
let se = FuseSession::new(dir.as_path(), "foo", "bar", false);
assert!(se.is_ok());
}
#[test]
fn test_new_channel() {
let fd = nix::unistd::dup(std::io::stdout().as_raw_fd()).unwrap();
let file = unsafe { File::from_raw_fd(fd) };
let _ = FuseChannel::new(file, 3).unwrap();
}
#[test]
fn test_fusermount() {
let dir = TempDir::new().unwrap();
let se = FuseSession::new(dir.as_path(), "foo", "bar", true);
assert!(se.is_ok());
let mut se = se.unwrap();
assert_eq!(se.get_fusermount(), FUSERMOUNT_BIN);
se.set_fusermount("fusermount");
assert_eq!(se.get_fusermount(), "fusermount");
}
#[test]
fn test_clone_fuse_file() {
let dir = TempDir::new().unwrap();
let mut se = FuseSession::new(dir.as_path(), "foo", "bar", true).unwrap();
se.mount().unwrap();
let cloned_file = se.clone_fuse_file().unwrap();
assert!(cloned_file.as_raw_fd() > 0);
se.umount().unwrap();
se.set_fuse_file(cloned_file);
se.mount().unwrap();
}
}
#[cfg(feature = "async_io")]
pub use asyncio::FuseDevTask;
#[cfg(feature = "async_io")]
mod asyncio {
use std::os::unix::io::RawFd;
use std::sync::Arc;
use crate::api::filesystem::AsyncFileSystem;
use crate::api::server::Server;
use crate::transport::{FuseBuf, Reader, Writer};
pub struct FuseDevTask<F: AsyncFileSystem + Sync> {
fd: RawFd,
buf: Vec<u8>,
state: AsyncExecutorState,
server: Arc<Server<F>>,
}
impl<F: AsyncFileSystem + Sync> FuseDevTask<F> {
pub fn new(
buf_size: usize,
fd: RawFd,
server: Arc<Server<F>>,
state: AsyncExecutorState,
) -> Self {
FuseDevTask {
fd,
server,
state,
buf: vec![0x0u8; buf_size],
}
}
pub async fn poll_handler(&mut self) {
let drive = AsyncDriver::default();
while !self.state.quiescing() {
let result = AsyncUtil::read(drive.clone(), self.fd, &mut self.buf, 0).await;
match result {
Ok(len) => {
let buf = unsafe {
std::slice::from_raw_parts_mut(self.buf.as_mut_ptr(), self.buf.len())
};
let reader =
Reader::<()>::new(FuseBuf::new(&mut self.buf[0..len])).unwrap();
let writer = Writer::new(self.fd, buf).unwrap();
let result = unsafe {
self.server
.async_handle_message(drive.clone(), reader, writer, None, None)
.await
};
if let Err(e) = result {
error!("failed to handle fuse request, {}", e);
}
}
Err(e) => {
error!("failed to read request from fuse device fd, {}", e);
}
}
}
self.state.report();
}
}
impl<F: AsyncFileSystem + Sync> Clone for FuseDevTask<F> {
fn clone(&self) -> Self {
FuseDevTask {
fd: self.fd,
server: self.server.clone(),
state: self.state.clone(),
buf: vec![0x0u8; self.buf.capacity()],
}
}
}
#[cfg(test)]
mod tests {
use std::os::unix::io::AsRawFd;
use super::*;
use crate::api::{Vfs, VfsOptions};
use crate::async_util::{AsyncDriver, AsyncExecutor};
#[test]
fn test_fuse_task() {
let state = AsyncExecutorState::new();
let fs = Vfs::<AsyncDriver, ()>::new(VfsOptions::default());
let _server = Arc::new(Server::<Vfs<AsyncDriver, ()>, AsyncDriver, ()>::new(fs));
let file = vmm_sys_util::tempfile::TempFile::new().unwrap();
let _fd = file.as_file().as_raw_fd();
let mut executor = AsyncExecutor::new(32);
executor.setup().unwrap();
for _i in 0..10 {
executor.run_once(false).unwrap();
}
state.quiesce();
drop(file);
for _i in 0..10 {
executor.run_once(false).unwrap();
}
}
}
}