use std::os::unix::process::{CommandExt, ExitStatusExt}; use std::path::Path; use std::process::{Child, Stdio}; use tempfile::TempDir; use crate::common::*; #[test] fn sigterm_lets_entrypoint_clean_up_in_blacklist_mode() { assert_sigterm_lets_entrypoint_clean_up("--blacklist"); } #[test] fn sigterm_lets_entrypoint_clean_up_in_whitelist_mode() { assert_sigterm_lets_entrypoint_clean_up("--whitelist"); } fn assert_sigterm_lets_entrypoint_clean_up(mode: &str) { let dir = TempDir::new().expect("failed to create temp dir"); let script = "trap 'sleep 0.5; touch cleaned; exit 0' TERM touch ready sleep 60 & wait"; let (_sandbox, mut child) = spawn_sandbox(&[mode], dir.path(), script); wait_for_file(&dir.path().join("ready")); send_signal(&child, libc::SIGTERM); let status = child.wait().unwrap(); assert!( dir.path().join("cleaned").exists(), "cleanup did not finish" ); assert_eq!(status.code(), Some(0)); } #[test] fn forwarded_signal_death_of_entrypoint_is_mirrored() { let dir = TempDir::new().expect("failed to create temp dir"); let script = "touch ready sleep 60 & wait"; let (_sandbox, mut child) = spawn_sandbox(&["--blacklist"], dir.path(), script); wait_for_file(&dir.path().join("ready")); send_signal(&child, libc::SIGTERM); let status = child.wait().unwrap(); assert_eq!(status.signal(), Some(libc::SIGTERM)); } #[test] fn user_and_realtime_signals_reach_entrypoint() { let dir = TempDir::new().expect("failed to create temp dir"); let realtime = libc::SIGRTMIN() + 1; let script = format!( "trap 'touch usr1' USR1 trap 'touch realtime' {realtime} trap 'exit 0' TERM touch ready while :; do sleep 0.05; done" ); let (_sandbox, mut child) = spawn_sandbox(&["--blacklist"], dir.path(), &script); wait_for_file(&dir.path().join("ready")); send_signal(&child, libc::SIGUSR1); wait_for_file(&dir.path().join("usr1")); send_signal(&child, realtime); wait_for_file(&dir.path().join("realtime")); send_signal(&child, libc::SIGTERM); assert_eq!(child.wait().unwrap().code(), Some(0)); } #[test] fn entrypoint_starts_with_default_dispositions_except_inherited_ignores() { let mut sandbox = Sandbox::new(&["--blacklist"]); sandbox.args(["--", "cat", "/proc/self/status"]); unsafe { sandbox.pre_exec(|| { libc::signal(libc::SIGHUP, libc::SIG_IGN); Ok(()) }); } let output = sandbox .output() .expect("agent-sandbox binary failed to execute"); let status = String::from_utf8_lossy(&output.stdout); let ignored = read_signal_mask(&status, "SigIgn:"); let blocked = read_signal_mask(&status, "SigBlk:"); assert!( has_signal(ignored, libc::SIGHUP), "inherited SIGHUP ignore was lost" ); for signal in [libc::SIGINT, libc::SIGTERM, libc::SIGUSR1, libc::SIGPIPE] { assert!(!has_signal(ignored, signal), "signal {signal} is ignored"); } assert_eq!(blocked, 0, "entrypoint starts with blocked signals"); } #[test] fn entrypoint_inherits_no_signalfd() { let output = Sandbox::new(&["--blacklist"]) .args(["--", "ls", "-l", "/proc/self/fd"]) .output() .expect("agent-sandbox binary failed to execute"); let fds = String::from_utf8_lossy(&output.stdout); assert!( !fds.contains("signalfd"), "entrypoint inherits a signalfd: {fds}" ); } fn spawn_sandbox(args: &[&str], dir: &Path, script: &str) -> (Sandbox, Child) { let mut sandbox = Sandbox::new(args); let child = sandbox .args(["--chdir", dir.to_str().unwrap(), "--", "sh", "-c", script]) .stdout(Stdio::null()) .spawn() .expect("agent-sandbox binary failed to execute"); (sandbox, child) } fn send_signal(child: &Child, signal: i32) { unsafe { libc::kill(child.id() as i32, signal) }; } fn read_signal_mask(proc_status: &str, field: &str) -> u64 { let line = proc_status .lines() .find_map(|line| line.strip_prefix(field)) .unwrap_or_else(|| panic!("{field} missing in {proc_status}")); u64::from_str_radix(line.trim(), 16).unwrap() } fn has_signal(mask: u64, signal: i32) -> bool { mask & (1 << (signal - 1)) != 0 }