use std::ffi::{CString, OsString};
use std::fs::OpenOptions;
use std::io::Write;
use std::os::fd::AsRawFd;
use std::os::unix::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
use std::process;
use nix::sys::signal::{SigSet, SigmaskHow, Signal, sigprocmask};
use nix::unistd::{ForkResult, fork};
use microsandbox_protocol::{HANDOFF_INIT_AUTO, HANDOFF_INIT_AUTO_CANDIDATES};
use crate::config::HandoffInit;
use crate::error::{AgentdError, AgentdResult};
const POST_HANDOFF_STDERR: &str = "/run/microsandbox/agentd.log";
pub fn do_handoff(spec: HandoffInit) -> AgentdResult<()> {
let cmd = resolve_cmd(&spec.cmd)?;
preflight(&cmd)?;
let argv = build_argv(&cmd, &spec.argv);
let envp = build_envp(&spec.env);
let cmd_c = path_to_cstring(&cmd)?;
match unsafe { fork() }? {
ForkResult::Parent { .. } => {
reset_signals();
let err = nix::unistd::execve(&cmd_c, &argv, &envp).unwrap_err();
let _ = writeln!(
std::io::stderr(),
"agentd: execve({}) failed: {err}",
cmd.display()
);
process::exit(127);
}
ForkResult::Child => {
redirect_child_stderr();
Ok(())
}
}
}
fn resolve_cmd(cmd: &Path) -> AgentdResult<PathBuf> {
if cmd != Path::new(HANDOFF_INIT_AUTO) {
return Ok(cmd.to_path_buf());
}
for candidate in HANDOFF_INIT_AUTO_CANDIDATES {
let p = Path::new(candidate);
if p.exists() {
return Ok(p.to_path_buf());
}
}
Err(AgentdError::Init(format!(
"{HANDOFF_INIT_AUTO}: no init binary found, checked: {}",
HANDOFF_INIT_AUTO_CANDIDATES.join(", ")
)))
}
fn preflight(cmd: &Path) -> AgentdResult<()> {
let metadata = std::fs::metadata(cmd).map_err(|e| {
AgentdError::Init(format!(
"handoff init binary not found at {}: {e}",
cmd.display()
))
})?;
if !metadata.is_file() {
return Err(AgentdError::Init(format!(
"handoff init path is not a regular file: {}",
cmd.display()
)));
}
use std::os::unix::fs::PermissionsExt;
if metadata.permissions().mode() & 0o111 == 0 {
return Err(AgentdError::Init(format!(
"handoff init binary is not executable: {}",
cmd.display()
)));
}
Ok(())
}
fn build_argv(cmd: &Path, supplemental: &[OsString]) -> Vec<CString> {
let mut out = Vec::with_capacity(1 + supplemental.len());
if let Ok(c) = CString::new(cmd.as_os_str().as_encoded_bytes()) {
out.push(c);
}
for arg in supplemental {
if let Ok(c) = CString::new(arg.as_bytes()) {
out.push(c);
}
}
out
}
fn build_envp(extras: &[(OsString, OsString)]) -> Vec<CString> {
use std::collections::HashMap;
let mut env: HashMap<OsString, OsString> = std::env::vars_os().collect();
for var in [
microsandbox_protocol::ENV_HANDOFF_INIT,
microsandbox_protocol::ENV_HANDOFF_INIT_ARGS,
microsandbox_protocol::ENV_HANDOFF_INIT_ENV,
] {
env.remove(&OsString::from(var));
}
for (k, v) in extras {
env.insert(k.clone(), v.clone());
}
env.into_iter()
.filter_map(|(k, v)| {
let mut bytes = k.into_vec();
bytes.push(b'=');
bytes.extend(v.into_vec());
CString::new(bytes).ok()
})
.collect()
}
fn path_to_cstring(path: &Path) -> AgentdResult<CString> {
CString::new(path.as_os_str().as_encoded_bytes()).map_err(|_| {
AgentdError::Config(format!("init path contains NUL byte: {}", path.display()))
})
}
fn reset_signals() {
use nix::sys::signal::{SigHandler, sigaction};
let dfl = nix::sys::signal::SigAction::new(
SigHandler::SigDfl,
nix::sys::signal::SaFlags::empty(),
SigSet::empty(),
);
for signum in 1..=31 {
let Ok(sig) = Signal::try_from(signum) else {
continue;
};
let _ = unsafe { sigaction(sig, &dfl) };
}
let empty = SigSet::empty();
let _ = sigprocmask(SigmaskHow::SIG_SETMASK, Some(&empty), None);
}
fn redirect_child_stderr() {
let Ok(file) = OpenOptions::new()
.create(true)
.append(true)
.open(POST_HANDOFF_STDERR)
else {
return;
};
unsafe {
libc::dup2(file.as_raw_fd(), libc::STDERR_FILENO);
}
}
pub fn is_pid_1() -> bool {
nix::unistd::getpid().as_raw() == 1
}
pub fn signal_init_shutdown() -> AgentdResult<()> {
let sig = libc::SIGRTMIN() + 4;
let ret = unsafe { libc::kill(1, sig) };
if ret != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
pub fn signal_init_term() -> AgentdResult<()> {
let ret = unsafe { libc::kill(1, libc::SIGTERM) };
if ret != 0 {
return Err(std::io::Error::last_os_error().into());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolve_cmd_passes_explicit_path_through() {
let p = Path::new("/lib/systemd/systemd");
let resolved = resolve_cmd(p).unwrap();
assert_eq!(resolved, PathBuf::from("/lib/systemd/systemd"));
}
#[test]
fn resolve_cmd_passes_through_non_existent_explicit_paths() {
let p = Path::new("/no/such/init");
let resolved = resolve_cmd(p).unwrap();
assert_eq!(resolved, PathBuf::from("/no/such/init"));
}
#[test]
fn resolve_cmd_auto_returns_first_existing_candidate_or_errors() {
match resolve_cmd(Path::new(HANDOFF_INIT_AUTO)) {
Ok(p) => {
assert!(
HANDOFF_INIT_AUTO_CANDIDATES
.iter()
.any(|c| Path::new(c) == p),
"resolved path {p:?} not in candidate list"
);
assert!(p.exists(), "resolved path must exist");
}
Err(AgentdError::Init(msg)) => {
assert!(msg.contains("no init binary found"));
for c in HANDOFF_INIT_AUTO_CANDIDATES {
assert!(msg.contains(c), "error should list {c}");
}
}
Err(e) => panic!("unexpected error variant: {e}"),
}
}
}