use std::path::Path;
use zygo_core::supervisor::client::Client;
use zygo_core::supervisor::{Request, Response};
use crate::cli::Cli;
use crate::output::Style;
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const SHELLS: &[&str] = &["/bin/bash", "/bin/sh", "/busybox/sh"];
pub fn run(cli: &Cli, name: &str, command: &[String]) -> anyhow::Result<u8> {
let paths = super::paths(cli);
let mut client = Client::connect(&paths)?;
let (pid, workdir) = match client.send(&Request::Shell {
name: name.to_string(),
})? {
Response::Sandbox { pid, workdir, .. } => (pid, workdir),
other => return super::supervisor::report_failure(cli, &other),
};
let style = Style::stdout();
if command.is_empty() {
eprintln!(
"{} {}",
style.dim(&format!("entering {name} (pid {pid});")),
style.dim("no seccomp, no Landlock, not in the tenant's cgroup")
);
}
enter(pid, &workdir, command)
}
#[cfg(target_os = "linux")]
fn enter(pid: u32, workdir: &Path, command: &[String]) -> anyhow::Result<u8> {
use std::os::fd::{AsRawFd, OwnedFd};
use std::os::unix::process::CommandExt;
use anyhow::Context;
let order = ["user", "pid", "net", "ipc", "uts", "cgroup", "mnt"];
let mut namespaces: Vec<OwnedFd> = Vec::with_capacity(order.len());
for kind in order {
let path = format!("/proc/{pid}/ns/{kind}");
let file = std::fs::File::open(&path).with_context(|| {
format!("cannot open {path}; the sandbox may have gone since the supervisor answered")
})?;
namespaces.push(OwnedFd::from(file));
}
let raw: Vec<i32> = namespaces.iter().map(|fd| fd.as_raw_fd()).collect();
let argv = if command.is_empty() {
None
} else {
Some(command.to_vec())
};
let workdir = workdir.to_path_buf();
let mut attempts = Vec::new();
for shell in SHELLS {
let (program, args): (String, Vec<String>) = match &argv {
Some(cmd) => (cmd[0].clone(), cmd[1..].to_vec()),
None => ((*shell).to_string(), vec!["-i".to_string()]),
};
let raw = raw.clone();
let workdir = workdir.clone();
let mut process = std::process::Command::new(&program);
process.args(&args);
process.env(
"TERM",
std::env::var("TERM").unwrap_or_else(|_| "xterm".into()),
);
process.env("PS1", "zygo:\\w$ ");
unsafe {
process.pre_exec(move || {
for fd in &raw {
if libc::setns(*fd, 0) != 0 {
return Err(std::io::Error::last_os_error());
}
}
let dir = std::ffi::CString::new(workdir.as_os_str().as_encoded_bytes())
.unwrap_or_else(|_| c"/".to_owned());
if libc::chdir(dir.as_ptr()) != 0 {
libc::chdir(c"/".as_ptr());
}
libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
drop_capabilities();
Ok(())
});
}
match process.status() {
Ok(status) => {
return Ok(status.code().unwrap_or(130).clamp(0, 255) as u8);
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound && argv.is_none() => {
attempts.push(*shell);
continue;
}
Err(e) => {
return Err(e).with_context(|| format!("could not run `{program}` in the sandbox"));
}
}
}
anyhow::bail!(
"this image has none of {}\n → give a command: zygo shell <name> -- <program>",
attempts.join(", ")
)
}
#[cfg(target_os = "linux")]
unsafe fn drop_capabilities() {
const LINUX_CAPABILITY_VERSION_3: u32 = 0x2008_0522;
const CAP_LAST_CAP_GUESS: libc::c_int = 63;
#[repr(C)]
struct CapHeader {
version: u32,
pid: libc::c_int,
}
#[repr(C)]
#[derive(Default, Clone, Copy)]
struct CapData {
effective: u32,
permitted: u32,
inheritable: u32,
}
unsafe {
libc::prctl(
libc::PR_CAP_AMBIENT,
libc::PR_CAP_AMBIENT_CLEAR_ALL,
0,
0,
0,
);
for cap in 0..=CAP_LAST_CAP_GUESS {
libc::prctl(libc::PR_CAPBSET_DROP, cap, 0, 0, 0);
}
let header = CapHeader {
version: LINUX_CAPABILITY_VERSION_3,
pid: 0,
};
let data = [CapData::default(); 2];
libc::syscall(libc::SYS_capset, &header, data.as_ptr());
}
}
#[cfg(not(target_os = "linux"))]
fn enter(_pid: u32, _workdir: &Path, _command: &[String]) -> anyhow::Result<u8> {
anyhow::bail!(
"entering a sandbox needs Linux namespaces; this host runs {}",
std::env::consts::OS
)
}