use crate::sandbox::Sandbox;
use anyhow::Context;
use std::fs;
use std::os::unix::process::CommandExt;
use std::path::Path;
use std::process::{Command, ExitStatus, Stdio};
use tracing::{debug, info, warn};
impl Sandbox {
pub fn mount_bindfs(
&self,
src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/bin/mount";
let mut mount_opts = vec!["bind"];
mount_opts.extend(opts.iter().copied());
let opts_str = mount_opts.join(",");
Ok(Some(
Command::new(cmd)
.arg("-o")
.arg(&opts_str)
.arg(src)
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn mount_devfs(
&self,
_src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/bin/mount";
Ok(Some(
Command::new(cmd)
.arg("-t")
.arg("devtmpfs")
.args(opts)
.arg("devtmpfs")
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn mount_fdfs(
&self,
_src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/bin/mount";
let mut mount_opts = vec!["bind"];
mount_opts.extend(opts.iter().copied());
let opts_str = mount_opts.join(",");
Ok(Some(
Command::new(cmd)
.arg("-o")
.arg(&opts_str)
.arg("/dev/fd")
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn mount_nfs(
&self,
src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/bin/mount";
Ok(Some(
Command::new(cmd)
.arg("-t")
.arg("nfs")
.args(opts)
.arg(src)
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn mount_procfs(
&self,
_src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/bin/mount";
Ok(Some(
Command::new(cmd)
.arg("-t")
.arg("proc")
.args(opts)
.arg("proc")
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn mount_tmpfs(
&self,
_src: &Path,
dest: &Path,
opts: &[&str],
) -> anyhow::Result<Option<ExitStatus>> {
fs::create_dir_all(dest).with_context(|| format!("Failed to create {}", dest.display()))?;
let cmd = "/bin/mount";
let mut args = vec!["-t", "tmpfs"];
let mut mount_opts: Vec<String> = vec![];
for opt in opts {
if opt.starts_with("size=") || opt.starts_with("mode=") {
mount_opts.push(opt.to_string());
}
}
if !mount_opts.is_empty() {
args.push("-o");
}
let opts_str = mount_opts.join(",");
Ok(Some(
Command::new(cmd)
.args(&args)
.arg(if !mount_opts.is_empty() {
&opts_str
} else {
""
})
.arg("tmpfs")
.arg(dest)
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
fn unmount_common(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
let cmd = "/bin/umount";
Ok(Some(
Command::new(cmd)
.arg(dest)
.stdout(Stdio::null())
.stderr(Stdio::null())
.process_group(0)
.status()
.context(format!("Unable to execute {}", cmd))?,
))
}
pub fn unmount_bindfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_devfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_fdfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_nfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_procfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn unmount_tmpfs(&self, dest: &Path) -> anyhow::Result<Option<ExitStatus>> {
self.unmount_common(dest)
}
pub fn kill_processes_for_path(&self, path: &Path) {
for iteration in 0..super::KILL_PROCESSES_MAX_RETRIES {
let output = Command::new("fuser")
.arg("-m")
.arg(path)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.process_group(0)
.output();
let Ok(out) = output else { return };
let stdout = String::from_utf8_lossy(&out.stdout);
if stdout.split_whitespace().next().is_none() {
return;
}
debug!(path = %path.display(), "Killing processes for mount");
let _ = Command::new("fuser")
.arg("-km")
.arg(path)
.stdout(Stdio::null())
.stderr(Stdio::null())
.process_group(0)
.status();
let delay_ms = super::KILL_PROCESSES_INITIAL_DELAY_MS << iteration;
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
}
pub fn kill_processes(&self, sandbox: &Path) {
for iteration in 0..super::KILL_PROCESSES_MAX_RETRIES {
let mut killed: Vec<i32> = Vec::new();
if let Ok(procs) = procfs::process::all_processes() {
for proc in procs.flatten() {
if Self::process_uses_path(&proc, sandbox).is_some() {
killed.push(proc.pid);
unsafe {
libc::kill(proc.pid, libc::SIGKILL);
}
}
}
}
if killed.is_empty() {
debug!(retries = iteration, "No processes found in sandbox");
return;
}
let pids: Vec<String> = killed.iter().map(|p| p.to_string()).collect();
info!(pids = %pids.join(" "), "Killed processes using sandbox");
let delay_ms = super::KILL_PROCESSES_INITIAL_DELAY_MS << iteration;
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
let proc_info = Self::get_process_info(sandbox);
warn!(
max_retries = super::KILL_PROCESSES_MAX_RETRIES,
remaining = %proc_info,
"Gave up killing processes after max retries"
);
}
fn get_process_info(sandbox: &Path) -> String {
let mut info = Vec::new();
if let Ok(procs) = procfs::process::all_processes() {
for proc in procs.flatten() {
if Self::process_uses_path(&proc, sandbox).is_some() {
let cmdline = proc
.cmdline()
.map(|c| c.join(" "))
.unwrap_or_else(|_| String::from("?"));
info.push(format!("pid={} cmd='{}'", proc.pid, cmdline));
}
}
}
if info.is_empty() {
String::from("(none)")
} else {
info.join(", ")
}
}
fn process_uses_path(proc: &procfs::process::Process, dir: &Path) -> Option<String> {
if let Ok(cwd) = proc.cwd() {
if cwd.starts_with(dir) {
return Some(format!("cwd={}", cwd.display()));
}
}
if let Ok(root) = proc.root() {
if root.starts_with(dir) {
return Some(format!("root={}", root.display()));
}
}
if let Ok(fds) = proc.fd() {
for fd in fds.flatten() {
if let procfs::process::FDTarget::Path(path) = fd.target {
if path.starts_with(dir) {
return Some(format!("fd={}", path.display()));
}
}
}
}
None
}
}