use std::io;
use std::os::fd::{AsRawFd, OwnedFd};
use std::path::Path;
use anyhow::Context as _;
use crate::rpc::ErrorResponse;
const TOOLS_DIR: &str = "/run/arcbox/debug-tools";
const BUSYBOX: &str = "/bin/busybox";
pub(super) struct NsEnter {
net: OwnedFd,
ipc: OwnedFd,
uts: OwnedFd,
pid: OwnedFd,
root: OwnedFd,
}
impl NsEnter {
pub(super) async fn resolve(container: &str) -> Result<Self, ErrorResponse> {
let pid = container_init_pid(container).await?;
let open = |path: String| -> Result<OwnedFd, ErrorResponse> {
std::fs::File::open(&path)
.map(OwnedFd::from)
.map_err(|e| ErrorResponse::new(500, format!("open {path}: {e}")))
};
Ok(Self {
net: open(format!("/proc/{pid}/ns/net"))?,
ipc: open(format!("/proc/{pid}/ns/ipc"))?,
uts: open(format!("/proc/{pid}/ns/uts"))?,
pid: open(format!("/proc/{pid}/ns/pid"))?,
root: open(format!("/proc/{pid}/root"))?,
})
}
pub(super) fn apply(&self) -> io::Result<()> {
setns(self.net.as_raw_fd(), libc::CLONE_NEWNET)?;
setns(self.ipc.as_raw_fd(), libc::CLONE_NEWIPC)?;
setns(self.uts.as_raw_fd(), libc::CLONE_NEWUTS)?;
setns(self.pid.as_raw_fd(), libc::CLONE_NEWPID)?;
match unsafe { libc::fork() } {
-1 => Err(io::Error::last_os_error()),
0 => self.setup_child(),
grandchild => reap_and_exit(grandchild),
}
}
fn setup_child(&self) -> io::Result<()> {
cvt(unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) })?;
cvt(unsafe { libc::unshare(libc::CLONE_NEWNS) })?;
cvt(unsafe {
libc::mount(
c"none".as_ptr(),
c"/".as_ptr(),
std::ptr::null(),
libc::MS_REC | libc::MS_PRIVATE,
std::ptr::null(),
)
})?;
cvt(unsafe {
libc::mount(
c"proc".as_ptr(),
c"/proc".as_ptr(),
c"proc".as_ptr(),
0,
std::ptr::null(),
)
})?;
cvt(unsafe {
libc::mount(
c"sysfs".as_ptr(),
c"/sys".as_ptr(),
c"sysfs".as_ptr(),
0,
std::ptr::null(),
)
})?;
cvt(unsafe { libc::fchdir(self.root.as_raw_fd()) })?;
Ok(())
}
}
fn reap_and_exit(grandchild: libc::pid_t) -> ! {
let (first, last, flags) = (
3 as libc::c_long,
libc::c_uint::MAX as libc::c_long,
0 as libc::c_long,
);
if unsafe { libc::syscall(libc::SYS_close_range, first, last, flags) } == -1 {
for fd in 3..1024 {
unsafe { libc::close(fd) };
}
}
unsafe { libc::setsid() };
let mut status: libc::c_int = 0;
while unsafe { libc::waitpid(grandchild, std::ptr::addr_of_mut!(status), 0) } == -1 {
if io::Error::last_os_error().raw_os_error() != Some(libc::EINTR) {
break;
}
}
if libc::WIFSIGNALED(status) {
let signal = libc::WTERMSIG(status);
unsafe { libc::signal(signal, libc::SIG_DFL) };
unsafe { libc::raise(signal) };
unsafe { libc::_exit(128 + signal) };
}
unsafe { libc::_exit(libc::WEXITSTATUS(status)) }
}
pub(super) async fn tools_dir() -> Result<&'static str, ErrorResponse> {
static INSTALLED: tokio::sync::OnceCell<()> = tokio::sync::OnceCell::const_new();
INSTALLED
.get_or_try_init(|| install_tools(Path::new(BUSYBOX), Path::new(TOOLS_DIR)))
.await
.map_err(|e| ErrorResponse::new(500, format!("install debug tools: {e:#}")))?;
Ok(TOOLS_DIR)
}
async fn install_tools(busybox: &Path, dir: &Path) -> anyhow::Result<()> {
let list = tokio::process::Command::new(busybox)
.arg("--list")
.output()
.await
.with_context(|| format!("run {} --list", busybox.display()))?;
anyhow::ensure!(
list.status.success(),
"{} --list: {}",
busybox.display(),
list.status
);
std::fs::create_dir_all(dir).with_context(|| format!("create {}", dir.display()))?;
for applet in String::from_utf8_lossy(&list.stdout).lines() {
match std::os::unix::fs::symlink(busybox, dir.join(applet)) {
Err(e) if e.kind() != io::ErrorKind::AlreadyExists => {
return Err(e).with_context(|| format!("link {applet}"));
}
_ => {}
}
}
Ok(())
}
async fn container_init_pid(container: &str) -> Result<u32, ErrorResponse> {
let info = crate::docker_events::docker_get(&format!("/containers/{container}/json"))
.await
.map_err(|e| ErrorResponse::new(502, format!("docker inspect {container}: {e}")))?;
let running = info
.pointer("/State/Running")
.and_then(serde_json::Value::as_bool)
.unwrap_or(false);
let pid = info
.pointer("/State/Pid")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
if running && pid != 0 {
return u32::try_from(pid)
.map_err(|_| ErrorResponse::new(500, "container init PID out of range"));
}
if let Some(message) = info.pointer("/message").and_then(serde_json::Value::as_str) {
return Err(ErrorResponse::new(404, message.to_owned()));
}
Err(ErrorResponse::new(
412,
format!("container '{container}' is not running"),
))
}
fn setns(fd: libc::c_int, nstype: libc::c_int) -> io::Result<()> {
cvt(unsafe { libc::setns(fd, nstype) })
}
fn cvt(ret: libc::c_int) -> io::Result<()> {
if ret == -1 {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use std::os::unix::fs::PermissionsExt as _;
use super::*;
#[tokio::test]
async fn every_listed_applet_is_linked_again_after_an_agent_restart() {
let root = tempfile::tempdir().unwrap();
let busybox = root.path().join("busybox");
std::fs::write(&busybox, "#!/bin/sh\nprintf 'ps\\ngrep\\n'\n").unwrap();
std::fs::set_permissions(&busybox, std::fs::Permissions::from_mode(0o755)).unwrap();
let dir = root.path().join("tools");
install_tools(&busybox, &dir).await.unwrap();
install_tools(&busybox, &dir).await.unwrap();
for applet in ["ps", "grep"] {
assert_eq!(std::fs::read_link(dir.join(applet)).unwrap(), busybox);
}
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 2);
}
}