use super::{check_liveness, parse_user, read_state, ContainerStatus};
use pelagos::container::{Command, Namespace, Stdio};
use std::os::unix::io::AsRawFd;
use std::path::PathBuf;
use std::sync::{
atomic::{AtomicI32, Ordering},
Arc,
};
#[derive(Debug, clap::Args)]
pub struct ExecArgs {
pub name: String,
#[clap(long, short = 'i')]
pub interactive: bool,
#[clap(long = "env", short = 'e')]
pub env: Vec<String>,
#[clap(long = "workdir", short = 'w')]
pub workdir: Option<String>,
#[clap(long = "user", short = 'u')]
pub user: Option<String>,
#[clap(multiple_values = true, required = true, allow_hyphen_values = true)]
pub args: Vec<String>,
}
pub fn cmd_exec(args: ExecArgs) -> Result<(), Box<dyn std::error::Error>> {
let state = read_state(&args.name)
.map_err(|e| format!("container '{}' not found: {}", args.name, e))?;
if state.status != ContainerStatus::Running || !check_liveness(state.pid) {
return Err(format!("container '{}' is not running", args.name).into());
}
let pid = state.pid;
let ns_entries = discover_namespaces(pid)?;
let container_env = read_proc_environ(pid);
let exe = &args.args[0];
let rest = &args.args[1..];
let mut cmd = Command::new(exe).args(rest);
let mut has_mount_ns = false;
for (path, ns) in &ns_entries {
if *ns == Namespace::MOUNT {
has_mount_ns = true;
} else {
cmd = cmd.with_namespace_join(path, *ns);
}
}
let exec_workdir = args.workdir.clone();
if has_mount_ns {
let mnt_ns_path = format!("/proc/{}/ns/mnt", pid);
let mnt_ns_file = std::fs::File::open(&mnt_ns_path)
.map_err(|e| format!("open {}: {}", mnt_ns_path, e))?;
let mnt_ns_fd = mnt_ns_file.as_raw_fd();
let root_pid = find_root_pid(pid);
let root_path = format!("/proc/{}/root", root_pid);
let root_file =
std::fs::File::open(&root_path).map_err(|e| format!("open {}: {}", root_path, e))?;
let root_fd = root_file.as_raw_fd();
cmd = cmd.with_pre_exec(move || {
let _keep_mnt = &mnt_ns_file;
let _keep_root = &root_file;
unsafe {
if libc::setns(mnt_ns_fd, libc::CLONE_NEWNS) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::fchdir(root_fd) != 0 {
return Err(std::io::Error::last_os_error());
}
let dot = std::ffi::CString::new(".").unwrap();
if libc::chroot(dot.as_ptr()) != 0 {
return Err(std::io::Error::last_os_error());
}
let target = exec_workdir.as_deref().unwrap_or("/");
let target_c = std::ffi::CString::new(target).unwrap();
if libc::chdir(target_c.as_ptr()) != 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
});
} else {
let root_pid = find_root_pid(pid);
cmd = cmd.with_chroot(format!("/proc/{}/root", root_pid));
if let Some(ref w) = exec_workdir {
cmd = cmd.with_cwd(w);
}
}
for (k, v) in &container_env {
cmd = cmd.env(k, v);
}
for e in &args.env {
if let Some((k, v)) = e.split_once('=') {
cmd = cmd.env(k, v);
} else if let Ok(v) = std::env::var(e) {
cmd = cmd.env(e, v);
}
}
if let Some(ref u) = args.user {
let (uid, gid) = parse_user(u)?;
cmd = cmd.with_uid(uid);
if let Some(g) = gid {
cmd = cmd.with_gid(g);
}
}
if args.interactive {
let session = cmd
.spawn_interactive()
.map_err(|e| format!("spawn_interactive failed: {}", e))?;
match session.run() {
Ok(status) => {
let code = status.code().unwrap_or(0);
std::process::exit(code);
}
Err(e) => Err(format!("interactive session failed: {}", e).into()),
}
} else {
cmd = cmd
.stdin(Stdio::Inherit)
.stdout(Stdio::Inherit)
.stderr(Stdio::Inherit);
let mut child = cmd
.spawn()
.map_err(|e| format!("exec spawn failed: {}", e))?;
let exit = child
.wait()
.map_err(|e| format!("exec wait failed: {}", e))?;
let code = exit.code().unwrap_or(1);
std::process::exit(code);
}
}
pub fn exec_in_container(pid: i32, args: &[String]) -> Option<bool> {
if args.is_empty() || pid <= 0 {
return None;
}
let ns_entries = discover_namespaces(pid).ok()?;
let mut cmd = Command::new(&args[0]).args(&args[1..]);
cmd = cmd
.stdin(Stdio::Null)
.stdout(Stdio::Null)
.stderr(Stdio::Null);
let mut has_mount_ns = false;
for (path, ns) in &ns_entries {
if *ns == Namespace::MOUNT {
has_mount_ns = true;
} else {
cmd = cmd.with_namespace_join(path, *ns);
}
}
if has_mount_ns {
let mnt_ns_path = format!("/proc/{}/ns/mnt", pid);
let mnt_ns_file = std::fs::File::open(&mnt_ns_path).ok()?;
let mnt_ns_fd = mnt_ns_file.as_raw_fd();
let root_pid = find_root_pid(pid);
let root_path = format!("/proc/{}/root", root_pid);
let root_file = std::fs::File::open(&root_path).ok()?;
let root_fd = root_file.as_raw_fd();
cmd = cmd.with_pre_exec(move || {
let _keep_mnt = &mnt_ns_file;
let _keep_root = &root_file;
unsafe {
if libc::setns(mnt_ns_fd, libc::CLONE_NEWNS) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::fchdir(root_fd) != 0 {
return Err(std::io::Error::last_os_error());
}
let dot = std::ffi::CString::new(".").unwrap();
if libc::chroot(dot.as_ptr()) != 0 {
return Err(std::io::Error::last_os_error());
}
let root_c = std::ffi::CString::new("/").unwrap();
if libc::chdir(root_c.as_ptr()) != 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
});
} else {
let root_pid = find_root_pid(pid);
cmd = cmd.with_chroot(format!("/proc/{}/root", root_pid));
}
match cmd.spawn() {
Ok(mut child) => child.wait().map(|s| s.success()).ok(),
Err(_) => None,
}
}
pub fn exec_in_container_with_pid_sink(
pid: i32,
args: &[String],
child_pid_sink: Arc<AtomicI32>,
) -> Option<bool> {
if args.is_empty() || pid <= 0 {
return None;
}
let ns_entries = discover_namespaces(pid).ok()?;
let mut cmd = Command::new(&args[0]).args(&args[1..]);
cmd = cmd
.stdin(Stdio::Null)
.stdout(Stdio::Null)
.stderr(Stdio::Null);
let mut has_mount_ns = false;
for (path, ns) in &ns_entries {
if *ns == Namespace::MOUNT {
has_mount_ns = true;
} else {
cmd = cmd.with_namespace_join(path, *ns);
}
}
if has_mount_ns {
let mnt_ns_path = format!("/proc/{}/ns/mnt", pid);
let mnt_ns_file = std::fs::File::open(&mnt_ns_path).ok()?;
let mnt_ns_fd = mnt_ns_file.as_raw_fd();
let root_pid = find_root_pid(pid);
let root_path = format!("/proc/{}/root", root_pid);
let root_file = std::fs::File::open(&root_path).ok()?;
let root_fd = root_file.as_raw_fd();
cmd = cmd.with_pre_exec(move || {
let _keep_mnt = &mnt_ns_file;
let _keep_root = &root_file;
unsafe {
if libc::setns(mnt_ns_fd, libc::CLONE_NEWNS) != 0 {
return Err(std::io::Error::last_os_error());
}
if libc::fchdir(root_fd) != 0 {
return Err(std::io::Error::last_os_error());
}
let dot = std::ffi::CString::new(".").unwrap();
if libc::chroot(dot.as_ptr()) != 0 {
return Err(std::io::Error::last_os_error());
}
let root_c = std::ffi::CString::new("/").unwrap();
if libc::chdir(root_c.as_ptr()) != 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
});
} else {
let root_pid = find_root_pid(pid);
cmd = cmd.with_chroot(format!("/proc/{}/root", root_pid));
}
match cmd.spawn() {
Ok(mut child) => {
child_pid_sink.store(child.pid(), Ordering::Relaxed);
child.wait().map(|s| s.success()).ok()
}
Err(_) => None,
}
}
fn find_root_pid(pid: i32) -> i32 {
let path = format!("/proc/{}/task/{}/children", pid, pid);
if let Ok(content) = std::fs::read_to_string(&path) {
let children: Vec<i32> = content
.split_whitespace()
.filter_map(|s| s.parse().ok())
.collect();
if children.len() == 1 {
return children[0];
}
}
pid
}
pub fn discover_namespaces(
pid: i32,
) -> Result<Vec<(PathBuf, Namespace)>, Box<dyn std::error::Error>> {
let ns_map: &[(&str, Namespace)] = &[
("mnt", Namespace::MOUNT),
("uts", Namespace::UTS),
("ipc", Namespace::IPC),
("net", Namespace::NET),
("pid", Namespace::PID),
("user", Namespace::USER),
("cgroup", Namespace::CGROUP),
];
let mut result = Vec::new();
for &(ns_name, ns_flag) in ns_map {
let container_ns = format!("/proc/{}/ns/{}", pid, ns_name);
let init_ns = format!("/proc/1/ns/{}", ns_name);
let container_ino = match std::fs::metadata(&container_ns) {
Ok(m) => {
use std::os::unix::fs::MetadataExt;
m.ino()
}
Err(_) => continue,
};
let init_ino = match std::fs::metadata(&init_ns) {
Ok(m) => {
use std::os::unix::fs::MetadataExt;
m.ino()
}
Err(_) => continue,
};
if container_ino != init_ino {
result.push((PathBuf::from(container_ns), ns_flag));
}
}
let pid_already_found = result.iter().any(|(_, ns)| *ns == Namespace::PID);
if !pid_already_found {
let pfc_path = format!("/proc/{}/ns/pid_for_children", pid);
let init_pid_path = "/proc/1/ns/pid";
let pfc_ino = std::fs::metadata(&pfc_path).ok().map(|m| {
use std::os::unix::fs::MetadataExt;
m.ino()
});
let init_pid_ino = std::fs::metadata(init_pid_path).ok().map(|m| {
use std::os::unix::fs::MetadataExt;
m.ino()
});
if let (Some(pfc), Some(init)) = (pfc_ino, init_pid_ino) {
if pfc != init {
result.push((PathBuf::from(pfc_path), Namespace::PID));
}
}
}
Ok(result)
}
fn read_proc_environ(pid: i32) -> Vec<(String, String)> {
let path = format!("/proc/{}/environ", pid);
let data = match std::fs::read(&path) {
Ok(d) => d,
Err(_) => return Vec::new(),
};
data.split(|&b| b == 0)
.filter(|s| !s.is_empty())
.filter_map(|entry| {
let s = String::from_utf8_lossy(entry);
let (k, v) = s.split_once('=')?;
Some((k.to_string(), v.to_string()))
})
.collect()
}