server/scan-worker/tests/worker_seccomp_profile.rs
//! **The scan worker under its GENERATED seccomp profile, on a real kernel** (lane
//! HEIMDALL-ALLIN prof): `engine: heimdall` with the real `heimdall-container-shim` and the
//! real, static `holger-scan-worker`, exactly as `jobs_under_heimdall.rs` lays them, with
//! the worker's generated profile (`jobs/runner/seccomp/holger-scan-worker/`, measured with
//! heimdall-probe over these very jobs) installed by Heimdall on top of its fixed one:
//!
//! * every verify-bundle job (native and the split wasm guest, good and changed bundles)
//! runs green under it, and its signed result says `Generated` with the pinned digest;
//! * a scan of every real package of the corpus (`HOLGER_PROFILE_PACKAGES`: pip, maven,
//! rust, npm, helm, debian, rpm, oci, raw; listed with their sha256 in
//! `jobs/runner/seccomp/holger-scan-worker/README.md`) and of the made-up subjects below
//! (a gzip bomb, YARA and secret hits, model files, an empty file) runs green, all kinds;
//! * a job past its wall clock is still Holger's timeout, not a seccomp kill;
//! * a call outside the profile ends the job: the probe (which tries a TCP connect on
//! purpose) under the profile is Holger's killed worker with `heimdall-seccomp-1` first;
//! * the probe without it keeps Heimdall's fixed profile, as the wiring promises.
//!
//! `HOLGER_PROFILE_CAPTURE=1` runs the same jobs under the fixed profile only (the
//! measurement run: scripts/scan-worker-profile.sh probes the jobs it stages). `#[ignore]`d:
//! scripts/heimdall-kernel-tests.sh runs them in privileged rootless podman.
mod common;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use server_lib::job_runner::heimdall_core::{
Job, JobId, JobResult, Nonce, Policy, Protection, Verdict,
};
use server_lib::job_runner::holger_job_runner::{seccomp as profile, worker};
use server_lib::job_runner::holger_sandbox::{EngineKind, HeimdallSettings, SandboxConfig};
use server_lib::job_runner::verify_bundle as vb;
use server_lib::job_runner::{JobDirs, Jobs, SandboxRule};
fn need(var: &str) -> PathBuf {
PathBuf::from(
std::env::var(var)
.unwrap_or_else(|_| panic!("{var} is not set: run scripts/heimdall-kernel-tests.sh")),
)
}
/// The measurement run: the same jobs under Heimdall's fixed profile only.
fn capture() -> bool {
std::env::var_os("HOLGER_PROFILE_CAPTURE").is_some()
}
fn cfg(dir: &Path) -> SandboxConfig {
let worker = std::fs::read(need("HOLGER_SCAN_WORKER_STATIC")).unwrap();
let tar = dir.join("holger-scan-worker.rootfs.tar");
std::fs::write(
&tar,
server_lib::job_runner::holger_sandbox::media::worker_rootfs_tar(&worker),
)
.unwrap();
let mut c = SandboxConfig {
engine: EngineKind::Heimdall,
userns_base: 1,
work_dir: Some(dir.join("work").display().to_string()),
heimdall: Some(HeimdallSettings {
image_layout: dir.join("oci").display().to_string(),
rootfs_tar: Some(tar.display().to_string()),
shim: Some(need("HEIMDALL_CONTAINER_SHIM").display().to_string()),
cgroup_parent: std::env::var("HOLGER_HEIMDALL_CGROUP_PARENT")
.unwrap_or_else(|_| "heimdall".into()),
work_dir: Some(dir.join("heimdall").display().to_string()),
fallback_plain: false,
host_id_count: 60000,
generated_seccomp: !capture(),
..Default::default()
}),
..Default::default()
};
c.limits.timeout_secs = 120;
c
}
fn jobs(dir: &Path) -> Jobs {
let j = Jobs::from_config(SandboxRule::Required, Some(cfg(dir))).expect("Heimdall starts");
assert_eq!(j.fallback(), None, "Heimdall itself, no fallback");
assert_eq!(j.features().runner, "heimdall");
j
}
/// The pinned profile, as the server names it.
fn the_profile(dir: &Path) -> server_lib::job_runner::heimdall_core::SeccompProfileRef {
profile::scan_worker_profile(&dir.join("profile"))
.unwrap()
.expect("x86_64 has a measured profile")
}
/// Under the generated profile (not in the measurement run) the signed result says so.
fn assert_generated(r: &JobResult) {
if capture() {
return;
}
assert_eq!(
r.runner.seccomp,
Protection::Generated {
profile_digest: profile::SCAN_WORKER_SHA256.into()
},
"{r:?}"
);
}
fn verify_job(
base: &Path,
bundle: &[u8],
policy: &update_bundle::TrustPolicy,
guest: bool,
) -> (Job, JobDirs) {
let id = JobId::random();
let dirs = JobDirs::new(base, &id).unwrap();
std::fs::write(dirs.input.join(vb::BUNDLE_FILE), bundle).unwrap();
std::fs::write(
dirs.input.join(vb::POLICY_FILE),
serde_json::to_vec(policy).unwrap(),
)
.unwrap();
let repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let mut read = Vec::new();
if guest {
let pem = std::fs::read(repo.join("holger-recipe/trust/korp-root-ca.pem")).unwrap();
std::fs::write(
dirs.input.join(vb::GUEST_ROOT_FILE),
holger_job_runner::guests::der_of(&pem).unwrap(),
)
.unwrap();
read.push(repo.join(holger_job_runner::guests::DIR));
}
let params = serde_json::to_value(vb::Params {
time: vb::TimeParam::At {
unix_ms: chrono::Utc::now().timestamp_millis(),
trusted: false,
},
product: None,
model: None,
revision: None,
guest: guest.then_some(vb::GuestParam { read: 0 }),
})
.unwrap();
let job = Job {
id,
nonce: Nonce::random(),
kind: worker(&[vb::PROGRAM]),
input_dir: dirs.input.clone(),
output_dir: dirs.output.clone(),
params,
policy: Policy {
read,
limits: server_lib::job_runner::heimdall_core::Limits {
wall: std::time::Duration::from_secs(120),
memory_bytes: if guest { 2 << 30 } else { 512 << 20 },
..Default::default()
},
..Default::default()
},
};
(job, dirs)
}
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn every_verify_bundle_job_runs_green_under_the_generated_profile() {
let tmp = tempfile::tempdir().unwrap();
let j = jobs(tmp.path());
let (bundle, policy) = common::signed_bundle(tmp.path());
let good = std::fs::read(&bundle).unwrap();
let mut bad = good.clone();
let n = bad.len() / 2;
bad[n] ^= 0x40;
for guest in [false, true] {
let (job, dirs) = verify_job(&tmp.path().join("jobs"), &good, &policy, guest);
let r = j.run(job).expect("a believed result");
assert_eq!(r.verdict, Verdict::Accepted, "guest {guest}: {r:?}");
assert_generated(&r);
let (members, _, raw) = vb::read_outputs(&dirs.output).unwrap();
assert_eq!(
members.get("payload/firmware.bin").map(Vec::as_slice),
Some(&b"hello"[..])
);
assert_eq!(raw.unwrap(), good);
let (job, _dirs) = verify_job(&tmp.path().join("jobs"), &bad, &policy, guest);
let r = j.run(job).expect("a believed refusal");
match &r.verdict {
Verdict::Refused { reason, .. } => assert!(
!reason.contains("heimdall-seccomp"),
"guest {guest}: the job's own refusal, not a kill: {reason}"
),
v => panic!("guest {guest}: a changed bundle is refused: {v:?}"),
}
assert_generated(&r);
}
}
/// The made-up subjects beside the real packages: each drives a scanner path the packages
/// do not (a bomb, a YARA hit, a secret, model files, an empty file).
fn made_up() -> Vec<(&'static str, String, Vec<u8>)> {
use std::io::Write as _;
let bomb = {
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
for _ in 0..64 {
e.write_all(&vec![0u8; 1 << 20]).unwrap();
}
e.finish().unwrap()
};
let eicar = b"X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*".to_vec();
let secret = b"aws_access_key_id = AKIAIOSFODNN7EXAMPLE\naws_secret_access_key = wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA\n-----END RSA PRIVATE KEY-----\n".to_vec();
let safetensors = {
let header = br#"{"w":{"dtype":"F32","shape":[2],"data_offsets":[0,8]},"__metadata__":{"format":"pt"}}"#;
let mut b = (header.len() as u64).to_le_bytes().to_vec();
b.extend_from_slice(header);
b.extend_from_slice(&[0u8; 8]);
b
};
// A protocol-2 pickle that names os.system (the pickle family is blocked).
let pickle = b"\x80\x02cos\nsystem\nq\x00X\x04\x00\x00\x00trueq\x01\x85q\x02Rq\x03.".to_vec();
vec![
("pip", "demo-1.0.tar.gz".into(), bomb),
("raw", "eicar.com".into(), eicar),
("raw", "settings.env".into(), secret),
("raw", "model.safetensors".into(), safetensors),
("raw", "model.pkl".into(), pickle),
("raw", "notes.txt".into(), b"hello".to_vec()),
("raw", "empty".into(), Vec::new()),
]
}
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn scans_of_real_packages_run_green_under_the_generated_profile() {
use server_lib::sandbox::{JobTemplate, ScanWorker};
use server_lib::scanner::{ScanSubject, ScannerKind};
let tmp = tempfile::tempdir().unwrap();
let j = Arc::new(jobs(tmp.path()));
let rules = tmp.path().join("yara");
std::fs::create_dir_all(&rules).unwrap();
std::fs::write(
rules.join("eicar.yar"),
"rule eicar_test_string { strings: $a = \"EICAR-STANDARD-ANTIVIRUS-TEST-FILE\" condition: $a }\n",
)
.unwrap();
let mut subjects: Vec<(String, String, Vec<u8>)> = Vec::new();
let pkgs = need("HOLGER_PROFILE_PACKAGES");
let mut formats: Vec<_> = std::fs::read_dir(&pkgs)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
formats.sort();
for d in formats {
let format = d.file_name().unwrap().to_string_lossy().into_owned();
let mut files: Vec<_> = std::fs::read_dir(&d)
.unwrap()
.map(|e| e.unwrap().path())
.collect();
files.sort();
for f in files {
let name = f.file_name().unwrap().to_string_lossy().into_owned();
subjects.push((format.clone(), name, std::fs::read(&f).unwrap()));
}
}
assert!(
subjects.len() >= 9,
"the package corpus is there: {}",
pkgs.display()
);
for (format, name, bytes) in made_up() {
subjects.push((format.into(), name, bytes));
}
let mut workers = std::collections::BTreeMap::new();
for (format, name, bytes) in &subjects {
let w = workers.entry(format.clone()).or_insert_with(|| {
let inspect = holger_inspect::InspectConfig {
yara: Some(holger_inspect::malware::YaraConfig {
rules: Some(rules.display().to_string()),
..Default::default()
}),
..Default::default()
};
ScanWorker::with_jobs_config(
j.clone(),
SandboxRule::Required,
Some(cfg(tmp.path())),
JobTemplate {
format_name: format.clone(),
inspect,
kinds: ScannerKind::ALL.to_vec(),
secrets: Some(server_lib::scanner::secrets::SecretRules::builtin()),
..Default::default()
},
)
.unwrap()
});
let r = w
.report(&ScanSubject::new(name, format, bytes))
.unwrap_or_else(|e| panic!("{format}/{name}: the scan job ran: {e:?}"));
eprintln!("{format}/{name}: {} finding(s)", r.inspect.findings.len());
}
}
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_job_past_its_wall_clock_is_still_holgers_timeout_under_the_generated_profile() {
use server_lib::sandbox::{JobTemplate, ScanWorker};
use server_lib::scanner::{ScanSubject, ScannerKind};
use std::io::Write as _;
let tmp = tempfile::tempdir().unwrap();
let j = Arc::new(jobs(tmp.path()));
let mut c = cfg(tmp.path());
c.limits.timeout_secs = 2;
let loose = holger_inspect::InspectConfig {
limits: holger_inspect::Limits {
max_ratio: u64::MAX / 2,
ratio_floor_bytes: u64::MAX / 2,
max_total_expanded: u64::MAX / 2,
max_entry_bytes: u64::MAX / 2,
..Default::default()
},
..Default::default()
};
let w = ScanWorker::with_jobs_config(
j,
SandboxRule::Required,
Some(c),
JobTemplate {
format_name: "pip".into(),
inspect: loose,
kinds: vec![ScannerKind::Inspection],
..Default::default()
},
)
.unwrap();
let member = {
let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::best());
for _ in 0..64 {
e.write_all(&vec![0u8; 1 << 20]).unwrap();
}
e.finish().unwrap()
};
let outer: Vec<u8> = std::iter::repeat_n(member.as_slice(), 512)
.flatten()
.copied()
.collect();
let f = w.inspect(&ScanSubject::new("demo-1.0.tar.gz", "pip", &outer));
let d = &f.findings[0].detail;
assert!(d.contains("holger-sandbox-2"), "a timeout: {d}");
assert!(!d.contains("heimdall-seccomp"), "not a seccomp kill: {d}");
}
fn probe_job(base: &Path, policy: Policy) -> (Job, JobDirs) {
let id = JobId::random();
let dirs = JobDirs::new(base, &id).unwrap();
let job = Job {
id,
nonce: Nonce::random(),
kind: worker(&[holger_job_runner::PROBE_PROGRAM]),
input_dir: dirs.input.clone(),
output_dir: dirs.output.clone(),
params: serde_json::json!({ "fill_mib": 65 }),
policy,
};
(job, dirs)
}
fn probe_policy() -> Policy {
Policy {
limits: server_lib::job_runner::heimdall_core::Limits {
wall: std::time::Duration::from_secs(60),
memory_bytes: 512 << 20,
..Default::default()
},
..Default::default()
}
}
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_call_outside_the_generated_profile_ends_the_job_as_a_seccomp_kill() {
if capture() {
return;
}
let tmp = tempfile::tempdir().unwrap();
let j = jobs(tmp.path());
let mut policy = probe_policy();
policy.seccomp = Some(the_profile(tmp.path()));
let (job, _dirs) = probe_job(&tmp.path().join("jobs"), policy);
let r = j.run(job).expect("a believed refusal");
match &r.verdict {
Verdict::Refused { code, reason } => {
assert_eq!(code, server_lib::errcode::SANDBOX_KILLED.code, "{r:?}");
// Holger's frozen code first, Heimdall's code first in the reason.
assert!(reason.contains("] heimdall-seccomp-1: "), "{reason}");
assert!(reason.contains(profile::SCAN_WORKER_SHA256), "{reason}");
}
v => panic!("the probe's connect is outside the profile: {v:?}"),
}
assert_generated(&r);
}
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn the_probe_keeps_heimdalls_fixed_profile() {
let tmp = tempfile::tempdir().unwrap();
let j = jobs(tmp.path());
let (job, _dirs) = probe_job(&tmp.path().join("jobs"), probe_policy());
let r = j.run(job).expect("a believed result");
assert_eq!(r.verdict, Verdict::Accepted, "{r:?}");
assert_eq!(r.runner.seccomp, Protection::Default, "{r:?}");
}