server/sandbox/tests/live_podman.rs
//! **The scan sandbox, live** (lane SANDBOX-SCAN): the worker image in a real
//! rootless podman, looked at from inside with the worker's `probe` mode.
//!
//! Ignored by default: they need podman and the minted image
//! (`cargo xtask scan-worker tests` runs them, with `HOLGER_SANDBOX_LIVE=1`).
//! With `HOLGER_TEST_SANDBOX_URL` they drive podman's API socket; without, the
//! podman program.
use holger_sandbox::{Limits, Refusal, Request, Sandbox, SandboxConfig};
fn cfg(limits: Limits) -> SandboxConfig {
SandboxConfig {
url: std::env::var("HOLGER_TEST_SANDBOX_URL").ok(),
work_dir: std::env::var("HOLGER_TEST_SANDBOX_WORKDIR").ok(),
image: std::env::var("HOLGER_SCAN_WORKER_IMAGE")
.unwrap_or_else(|_| holger_sandbox::DEFAULT_IMAGE.into()),
limits,
..Default::default()
}
}
fn live() -> bool {
std::env::var("HOLGER_SANDBOX_LIVE").is_ok()
}
fn probe(limits: Limits, args: &[&str]) -> Result<serde_json::Value, Refusal> {
let sb = Sandbox::podman(cfg(limits))?;
let mut command = vec!["probe".to_string()];
command.extend(args.iter().map(|a| a.to_string()));
let out = sb.run(&Request {
artifact: b"x",
job: b"{}",
mounts: vec![],
sha256: "probe",
command,
})?;
Ok(serde_json::from_slice(&out.report).expect("the probe answers JSON"))
}
#[test]
#[ignore = "needs podman and the minted scan worker image (cargo xtask scan-worker tests)"]
fn the_worker_has_no_network_no_capabilities_and_a_read_only_root() {
if !live() {
return;
}
let v = probe(Limits::default(), &[]).unwrap();
eprintln!("{v:#}");
for k in ["connect_public", "connect_gateway"] {
assert!(v[k].as_str().unwrap().starts_with("refused"), "{k}: {v}");
}
assert!(
!v["resolve"].as_str().unwrap().starts_with("Ok("),
"no name resolves: {v}"
);
for k in ["write_root", "write_etc", "write_in", "write_traversal"] {
assert!(
v[k].as_str().unwrap().starts_with("refused"),
"{k} must not write outside the scratch space: {v}"
);
}
assert_eq!(v["write_scratch"], "written", "{v}");
assert!(
v["write_past_scratch_cap"]
.as_str()
.unwrap()
.starts_with("refused"),
"the scratch cap holds: {v}"
);
assert!(v["uid"].as_str().unwrap().starts_with("65532"), "{v}");
assert_eq!(v["cap_eff"], "0000000000000000", "{v}");
assert_eq!(v["no_new_privs"], "1", "{v}");
assert_eq!(v["seccomp"], "2", "the default seccomp filter is on: {v}");
}
#[test]
#[ignore = "needs podman and the minted scan worker image (cargo xtask scan-worker tests)"]
fn a_worker_past_its_wall_clock_is_killed_and_refused_by_name() {
if !live() {
return;
}
let t = std::time::Instant::now();
let e = probe(
Limits {
timeout_secs: 3,
..Default::default()
},
&["--sleep", "60"],
)
.unwrap_err();
assert_eq!(e.code().code, "holger-sandbox-2", "{e}");
assert!(
t.elapsed().as_secs() < 30,
"killed at the deadline, not at the end: {:?}",
t.elapsed()
);
}
#[test]
#[ignore = "needs podman and the minted scan worker image (cargo xtask scan-worker tests)"]
fn a_worker_past_its_memory_is_killed_and_refused_by_name() {
if !live() {
return;
}
let e = probe(
Limits {
memory_mib: 128,
scratch_mib: 16,
..Default::default()
},
&["--alloc", "1024"],
)
.unwrap_err();
assert_eq!(e.code().code, "holger-sandbox-3", "{e}");
}
#[test]
#[ignore = "needs podman and the minted scan worker image (cargo xtask scan-worker tests)"]
fn a_report_past_its_cap_is_refused_by_name() {
if !live() {
return;
}
let e = probe(
Limits {
report_max_bytes: 1 << 20,
..Default::default()
},
&["--spam", "64"],
)
.unwrap_err();
assert_eq!(e.code().code, "holger-sandbox-4", "{e}");
}
#[test]
#[ignore = "needs podman and the minted scan worker image (cargo xtask scan-worker tests)"]
fn no_scan_container_is_left_behind() {
if !live() {
return;
}
probe(Limits::default(), &[]).unwrap();
let _ = probe(
Limits {
timeout_secs: 2,
..Default::default()
},
&["--sleep", "30"],
);
// Through the program when the test host has one (inside the toolchain
// container only the socket is there, and the engine already removes by name).
let Ok(ps) = std::process::Command::new("podman")
.args(["ps", "-a", "--format", "{{.Names}}"])
.output()
else {
return;
};
let mine = format!("-{}-", std::process::id());
let left: Vec<String> = String::from_utf8_lossy(&ps.stdout)
.lines()
.filter(|n| n.starts_with("holger-scan-") && n.contains(&mine))
.map(str::to_string)
.collect();
assert!(left.is_empty(), "left behind: {left:?}");
}