use crate::error::{Error, Result};
use std::collections::HashMap;
use std::ffi::CString;
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write};
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::FromRawFd;
use std::path::{Path, PathBuf};
use std::process::Command;
const MNT_DETACH: libc::c_int = 2;
const CLONE_NEWUSER: libc::c_int = 0x10000000;
const CLONE_NEWNS: libc::c_int = 0x00020000;
#[derive(Debug, Clone)]
pub struct MountPoint {
pub id: u32,
pub parent_id: u32,
pub path: PathBuf,
}
pub fn mounts_under(root: &Path) -> Result<Vec<MountPoint>> {
let root_canon = match root.canonicalize() {
Ok(p) => p,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::Io(e)),
};
let file = match File::open("/proc/self/mountinfo") {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(Vec::new()),
Err(e) => return Err(Error::Io(e)),
};
let reader = BufReader::new(file);
let mut mounts = Vec::new();
for line_result in reader.lines() {
let line = line_result.map_err(Error::Io)?;
if let Some(mp) = parse_mountinfo_line(&line) {
if mp.path == root_canon || mp.path.starts_with(&root_canon) {
mounts.push(mp);
}
}
}
let depths = compute_depths(&mounts);
mounts.sort_by_key(|mp| depths.get(&mp.id).copied().unwrap_or(0));
mounts.reverse();
Ok(mounts)
}
pub fn unmount_lazy(mp: &MountPoint) -> Result<()> {
match try_umount2_detach(&mp.path) {
Ok(()) => return Ok(()),
Err(e) if matches!(e.raw_os_error(), Some(libc::EPERM) | Some(libc::EACCES)) => {}
Err(e) => return Err(Error::Io(e)),
}
match try_umount2_in_user_ns(&mp.path) {
Ok(()) => return Ok(()),
Err(e) if e.raw_os_error() == Some(libc::EPERM) => {}
Err(e) if e.raw_os_error() == Some(libc::EACCES) => {
return Err(Error::UnmountFailed {
path: mp.path.clone(),
reason: format!(
"mount at '{}' does not belong to the current user; \
unmount it manually before retrying",
mp.path.display()
),
});
}
Err(e) => return Err(Error::Io(e)),
}
match try_fusermount(&mp.path) {
Ok(()) => return Ok(()),
Err(_) => {}
}
Err(Error::UnmountFailed {
path: mp.path.clone(),
reason: "failed to unmount; may require CAP_SYS_ADMIN or manual intervention".into(),
})
}
fn try_umount2_detach(path: &Path) -> io::Result<()> {
let cpath = CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
let ret = unsafe { libc::umount2(cpath.as_ptr(), MNT_DETACH) };
if ret == 0 {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
fn try_umount2_in_user_ns(path: &Path) -> io::Result<()> {
let cpath = CString::new(path.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "path contains null byte"))?;
let uid = unsafe { libc::getuid() };
let gid = unsafe { libc::getgid() };
let pid = unsafe { libc::fork() };
match pid {
-1 => Err(io::Error::last_os_error()),
0 => {
if unsafe { libc::unshare(CLONE_NEWUSER | CLONE_NEWNS) } != 0 {
unsafe { libc::_exit(1) };
}
if write_proc_file("/proc/self/setgroups", "deny").is_err() {
unsafe { libc::_exit(2) };
}
let uid_map = format!("0 {uid} 1");
if write_proc_file("/proc/self/uid_map", &uid_map).is_err() {
unsafe { libc::_exit(2) };
}
let gid_map = format!("0 {gid} 1");
if write_proc_file("/proc/self/gid_map", &gid_map).is_err() {
unsafe { libc::_exit(2) };
}
let ret = unsafe { libc::umount2(cpath.as_ptr(), MNT_DETACH) };
unsafe { libc::_exit(if ret == 0 { 0 } else { 2 }) };
}
child_pid => {
let mut status: libc::c_int = 0;
loop {
let ret = unsafe { libc::waitpid(child_pid, &mut status, 0) };
if ret == -1 {
let e = io::Error::last_os_error();
if e.raw_os_error() == Some(libc::EINTR) {
continue;
}
return Err(e);
}
break;
}
match (libc::WIFEXITED(status), libc::WEXITSTATUS(status)) {
(true, 0) => Ok(()),
(true, 1) => Err(io::Error::from_raw_os_error(libc::EPERM)),
(true, _) => Err(io::Error::from_raw_os_error(libc::EACCES)),
_ => Err(io::Error::from_raw_os_error(libc::EINTR)),
}
}
}
}
fn try_fusermount(path: &Path) -> std::result::Result<(), String> {
for bin in &["fusermount3", "fusermount"] {
match run_fusermount(bin, path) {
Ok(()) => return Ok(()),
Err(ref msg) if msg.contains("not found") || msg.contains("ENOENT") => continue,
Err(msg) => return Err(msg),
}
}
Err("neither fusermount3 nor fusermount is installed; \
install fuse3 (or fuse2) or run as root"
.into())
}
fn run_fusermount(bin: &str, path: &Path) -> std::result::Result<(), String> {
let output = Command::new(bin)
.args(["-u", "-z", "--"])
.arg(path)
.output();
match output {
Ok(out) if out.status.success() => Ok(()),
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
let msg = if !stderr.trim().is_empty() {
stderr.trim().to_owned()
} else {
stdout.trim().to_owned()
};
let exit = out
.status
.code()
.map_or_else(|| "signal".into(), |c| c.to_string());
Err(format!("{bin}: {msg} (exit {exit})"))
}
Err(e) if e.kind() == io::ErrorKind::NotFound => Err(format!("{bin}: not found")),
Err(e) => Err(format!("{bin}: {e}")),
}
}
fn write_proc_file(path: &str, content: &str) -> io::Result<()> {
let cpath = CString::new(path).expect("proc path has no null byte");
let fd = unsafe { libc::open(cpath.as_ptr(), libc::O_WRONLY | libc::O_CLOEXEC) };
if fd < 0 {
return Err(io::Error::last_os_error());
}
let mut file = unsafe { File::from_raw_fd(fd) };
file.write_all(content.as_bytes())
}
fn parse_mountinfo_line(line: &str) -> Option<MountPoint> {
let tokens: Vec<&str> = line.split(' ').collect();
let mount_id = tokens.get(0)?.parse::<u32>().ok()?;
let parent_id = tokens.get(1)?.parse::<u32>().ok()?;
let mountpoint_raw = tokens.get(4)?;
let mountpoint = unescape_mountinfo(mountpoint_raw);
Some(MountPoint {
id: mount_id,
parent_id,
path: PathBuf::from(mountpoint),
})
}
fn unescape_mountinfo(s: &str) -> String {
let mut out: Vec<u8> = Vec::with_capacity(s.len());
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\\' && i + 3 < bytes.len() {
let decode = (|| {
let a = (bytes[i + 1] as char).to_digit(8)? as u8;
let b = (bytes[i + 2] as char).to_digit(8)? as u8;
let c = (bytes[i + 3] as char).to_digit(8)? as u8;
Some((a << 6) | (b << 3) | c)
})();
if let Some(byte) = decode {
out.push(byte);
i += 4;
continue;
}
}
out.push(bytes[i]);
i += 1;
}
String::from_utf8(out).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
}
fn compute_depths(mounts: &[MountPoint]) -> HashMap<u32, usize> {
let map: HashMap<u32, &MountPoint> = mounts.iter().map(|m| (m.id, m)).collect();
let mut depths = HashMap::new();
for mp in mounts {
let mut depth = 0;
let mut current = mp;
while let Some(parent) = map.get(¤t.parent_id) {
depth += 1;
current = parent;
}
depths.insert(mp.id, depth);
}
depths
}
#[cfg(test)]
#[path = "unmount_tests.rs"]
mod tests;