Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/scan-worker/tests/jobs_under_heimdall.rs

//! **Holger's jobs under Heimdall's protected runner, on a real kernel** (lane
//! HOL-HEIMDALL-02): `engine: heimdall` with the real `heimdall-container-shim`
//! (nordisk-heimdall-container 0.2.0 from crates.io, `--features youki`) and the real,
//! static `holger-scan-worker` in an image minted from the medium's rootfs tar
//! (`media::worker_rootfs_tar`), exactly as a box lays it:
//!
//! * a signed bundle is verified by the `verify-bundle` job and its members come back,
//!   believed through Heimdall's signature and then Holger's;
//! * a scan (the INGEST walk) runs in the worker and finds the planted bomb;
//! * a bundle with a changed byte is the job's refusal; a job past its wall clock is
//!   Holger's timeout, Heimdall's code in the reason;
//! * every result says `heimdall`, Landlock, the fixed seccomp profile and the kernel report.
//!
//! `#[ignore]`d: they need user namespaces, Landlock, seccomp and cgroup v2 with memory,
//! pids and cpu delegated. `scripts/heimdall-kernel-tests.sh` runs them in a privileged
//! rootless podman container (like heimdall's own `scripts/container-tests.sh`), with
//! `HEIMDALL_CONTAINER_SHIM` and `HOLGER_SCAN_WORKER_STATIC` set.

mod common;

use std::path::{Path, PathBuf};
use std::sync::Arc;

use server_lib::job_runner::heimdall_core::{Job, JobId, Nonce, Policy, Verdict};
use server_lib::job_runner::holger_job_runner::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")),
    )
}

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();
    SandboxConfig {
        engine: EngineKind::Heimdall,
        // The privileged rootless podman the script runs in maps ids 0..65535: as its root,
        // a job's root maps to 1.. (Heimdall 0.2.1 host ids), never to 0.
        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,
            ..Default::default()
        }),
        ..Default::default()
    }
}

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");
    assert!(!j.is_interim());
    j
}

fn verify_job(base: &Path, bundle: &[u8], policy: &update_bundle::TrustPolicy) -> (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 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: None,
    })
    .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 {
            limits: server_lib::job_runner::heimdall_core::Limits {
                wall: std::time::Duration::from_secs(60),
                memory_bytes: 512 << 20,
                ..Default::default()
            },
            ..Default::default()
        },
    };
    (job, dirs)
}

#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_bundle_is_verified_in_heimdall_and_its_members_come_back() {
    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 (job, dirs) = verify_job(&tmp.path().join("jobs"), &good, &policy);
    let r = j.run(job).expect("a believed result");
    assert_eq!(r.verdict, Verdict::Accepted, "{r:?}");
    assert_eq!(r.runner.runner, "heimdall");
    assert!(r.runner.landlock, "Landlock reported from inside the job");
    assert!(r.runner.container && r.runner.image_verified_each_start);
    let k = r.kernel.as_ref().expect("Heimdall's kernel report");
    assert!(
        k.seccomp_filter && k.landlock_abi.is_some() && k.user_namespaces,
        "{k:?}"
    );
    assert_eq!(j.kernel(), Some(k));
    let (members, manifest, raw) = vb::read_outputs(&dirs.output).unwrap();
    assert_eq!(
        members.get("payload/firmware.bin").map(Vec::as_slice),
        Some(&b"hello"[..])
    );
    assert!(!manifest.export);
    assert_eq!(raw.unwrap(), good, "the bytes that verified come back");
    // Heimdall 0.2.1: as root, the job's root is not host root. What it wrote is owned by the
    // first id of the range (1 here), not by 0.
    {
        use std::os::unix::fs::MetadataExt as _;
        let me = std::fs::metadata("/proc/self").unwrap().uid();
        let owner = std::fs::metadata(dirs.output.join(vb::MEMBERS_FILE))
            .unwrap()
            .uid();
        if me == 0 {
            assert_eq!(owner, 1, "a job's root maps to host id 1, not to host root");
        } else {
            assert_eq!(owner, me, "an ordinary runner's jobs are its own uid");
        }
    }

    let mut bad = good.clone();
    let n = bad.len() / 2;
    bad[n] ^= 0x40;
    let (job, dirs) = verify_job(&tmp.path().join("jobs"), &bad, &policy);
    let r = j.run(job).expect("a believed refusal");
    assert!(matches!(r.verdict, Verdict::Refused { .. }), "{r:?}");
    assert!(std::fs::read_dir(&dirs.output).unwrap().next().is_none());
}

#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_scan_runs_in_heimdall_and_finds_the_planted_bomb() {
    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 mut c = cfg(tmp.path());
    c.limits.timeout_secs = 60;
    let w = ScanWorker::with_jobs_config(
        j,
        SandboxRule::Required,
        Some(c),
        JobTemplate {
            format_name: "pip".into(),
            kinds: vec![ScannerKind::Inspection],
            ..Default::default()
        },
    )
    .unwrap();
    assert!(w.is_isolated());
    let bomb = {
        use std::io::Write as _;
        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 f = w.inspect(&ScanSubject::new("demo-1.0.tar.gz", "pip", &bomb));
    assert!(
        f.has(
            holger_inspect::FindingKind::ZipBomb,
            holger_inspect::Severity::Block
        ),
        "{:?}",
        f.findings
    );
    let clean = w.inspect(&ScanSubject::new("notes.txt", "raw", b"hello"));
    assert_eq!(
        clean.verdict(),
        holger_inspect::Verdict::Pass,
        "{:?}",
        clean.findings
    );
    assert!(w.runs() >= 2 || w.jobs().runs() >= 2);
}

#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_job_past_its_wall_clock_is_holgers_timeout_with_heimdalls_code() {
    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;
    // The inspector told to let everything through, so only the wall clock stops the walk:
    // 32 GiB of zeros as 512 gzip members of 64 MiB (about 32 MiB on the wire).
    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));
    assert_eq!(
        f.verdict(),
        holger_inspect::Verdict::Fail,
        "{:?}",
        f.findings
    );
    assert!(
        f.findings[0].detail.contains("[holger-sandbox-2]")
            && f.findings[0].detail.contains("heimdall-limit-wall")
            || f.findings[0]
                .detail
                .starts_with("sandbox-timeout: [holger-sandbox-2]"),
        "{:?}",
        f.findings
    );
}

/// Lane HOL-HEIMDALL-04: the released split guest runs inside the worker under Heimdall's
/// protections (wasmtime's JIT under Heimdall's seccomp profile and Landlock), and its
/// members come back as the native ones do.
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn a_bundle_is_verified_by_the_split_guest_inside_heimdall() {
    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 repo = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
    let guests = repo.join(holger_job_runner::guests::DIR);
    let (mut job, dirs) = verify_job(&tmp.path().join("jobs"), &good, &policy);
    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();
    let mut params: vb::Params = serde_json::from_value(job.params.clone()).unwrap();
    params.guest = Some(vb::GuestParam { read: 0 });
    job.params = serde_json::to_value(&params).unwrap();
    job.policy.read = vec![guests];
    job.policy.limits.memory_bytes = 2 << 30;
    job.policy.limits.wall = std::time::Duration::from_secs(120);
    let r = j.run(job).expect("a believed result");
    assert_eq!(r.verdict, Verdict::Accepted, "{r:?}");
    assert_eq!(r.runner.runner, "heimdall");
    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);
}

/// HOL-HEIMDALL-06: the probe door looks into Heimdall's runner: the worker's `probe` as one
/// job, from inside: no network, no capabilities, no new privileges, a seccomp filter.
#[test]
#[ignore = "a real kernel: scripts/heimdall-kernel-tests.sh"]
fn the_probe_looks_into_heimdalls_runner() {
    use server_lib::sandbox::{JobTemplate, ScanWorker};
    let tmp = tempfile::tempdir().unwrap();
    let j = Arc::new(jobs(tmp.path()));
    let w = ScanWorker::with_jobs_config(
        j,
        SandboxRule::Required,
        Some(cfg(tmp.path())),
        JobTemplate::default(),
    )
    .unwrap();
    let v = w
        .probe()
        .expect("the probe runs as a job of Heimdall's runner");
    assert!(
        v["engine"].as_str().is_some_and(|e| e.contains("Heimdall")),
        "{v}"
    );
    assert!(
        v["connect_public"]
            .as_str()
            .is_some_and(|s| s.starts_with("refused")),
        "no network inside: {v}"
    );
    assert_eq!(v["no_new_privs"], "1", "{v}");
    assert_eq!(v["cap_eff"], "0000000000000000", "{v}");
    assert_ne!(v["seccomp"], "0", "a seccomp filter: {v}");
}