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

nordisk/heimdall

crates/heimdall-container/tests/runner_fake_shim.rs

// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Ignalina ApS and Vetra Automation AB

//! HeimdallRunner end to end with a FAKE shim (a shell script that plays the
//! container and answers a scripted response): runs in plain CI, no
//! namespaces needed. Proves the job checks, image verification at every
//! start, output listing, verdict mapping, signing, the truthful features,
//! and that a job whose protections were not really applied is never signed.
//! Heimdall is a defensive sandbox; these tests exercise only its own logic.

#[path = "../../heimdall-plain/tests/common/mod.rs"]
mod common;

use std::path::PathBuf;
use std::sync::Arc;

use common::*;
use heimdall_container::kernel::KernelReport;
use heimdall_container::shim::ShimRequest;
use heimdall_container::*;
use heimdall_core::*;

struct Fx {
    base: PathBuf,
    layout: Layout,
    image: ImageRef,
}

fn fx(name: &str) -> Fx {
    let base = scratch(&format!("hc-{name}"));
    let layout = Layout::new(&base.join("layout"));
    let image = layout.image(
        &[(
            Layout::tar(&[
                Entry::Dir("bin"),
                Entry::Dir("etc"),
                Entry::File("bin/tool".into(), b"#!/bin/sh\n".to_vec(), 0o755),
            ]),
            true,
        )],
        &["PATH=/bin"],
    );
    Fx {
        base,
        layout,
        image,
    }
}

fn kernel() -> KernelReport {
    KernelReport {
        release: "7.0.0".into(),
        seccomp_filter: true,
        user_notif: true,
        notif_addfd: true,
        landlock_abi: Some(8),
        cgroup_v2: true,
        cgroup_controllers: vec!["cpu".into(), "memory".into(), "pids".into()],
        user_namespaces: true,
    }
}

const SEED: [u8; 32] = [7; 32];

/// A config whose shim is `sh <script>`; the script saves its request to
/// `<base>/req-<n>.json`, runs `body`, and prints `resp`.
fn cfg_with(f: &Fx, body: &str, resp: &str) -> HeimdallConfig {
    let script = f.base.join(format!("shim-{}.sh", JobId::random()));
    std::fs::write(
        &script,
        format!(
            "cat > {}/req-$$.json\n{body}\ncat <<'EOF'\n{resp}\nEOF\n",
            f.base.display()
        ),
    )
    .unwrap();
    let mut cfg = HeimdallConfig::new(&f.layout.dir, f.base.join("work"));
    cfg.shim = ShimCommand {
        program: "/bin/sh".into(),
        args: vec![script.display().to_string()],
    };
    cfg
}

fn runner_cfg(cfg: HeimdallConfig) -> HeimdallRunner {
    HeimdallRunner::with_kernel(cfg, BootKey::from_seed(SEED, BootId([1; 16])), kernel()).unwrap()
}

fn runner(f: &Fx, body: &str, resp: &str) -> HeimdallRunner {
    runner_cfg(cfg_with(f, body, resp))
}

fn requests(f: &Fx) -> Vec<String> {
    std::fs::read_dir(&f.base)
        .unwrap()
        .filter_map(|e| e.ok())
        .filter(|e| e.file_name().to_string_lossy().starts_with("req-"))
        .map(|e| std::fs::read_to_string(e.path()).unwrap())
        .collect()
}

const INSIDE: &str = r#"{"no_new_privs":true,"seccomp_mode":2,"landlock":"Full","error":null}"#;

/// The broker's report of a full-mode run with one allowed open.
const BROKER: &str = r#"{"job_id":"00000000000000000000000000000000","mode":"full","events":[{"at_ms":1,"call":"openat","args":["path=/in/a","flags=O_RDONLY","fd=3"],"decision":"Allowed"}],"dropped_events":0,"warnings":[]}"#;

/// A shim response. Unless the test says otherwise, the inside report says
/// the broker filter was installed (full) and the shim served it.
fn resp(exit: &str, extra: &str, inside: &str) -> String {
    let inside = if inside.starts_with('{') && !inside.contains(r#""broker""#) {
        inside.replacen('{', r#"{"broker":"full","#, 1)
    } else {
        inside.to_string()
    };
    let extra = if extra.contains(r#""broker""#) {
        extra.to_string()
    } else {
        format!(r#"{extra},"broker":{BROKER}"#)
    };
    format!(
        r#"{{"error":null,{exit},"wall_killed":false,"oom_kills":0,"pids_max":0,"ran_ms":3,"swap_max":"0","inside":{inside}{extra}}}"#
    )
}

fn ok() -> String {
    resp(r#""exit_code":0,"signal":null"#, "", INSIDE)
}

fn job(f: &Fx, d: &JobDirs) -> Job {
    native_job(&f.image, &["/bin/tool"], d)
}

fn run_ok(f: &Fx, body: &str, resp: &str) -> (HeimdallRunner, JobDirs, SignedResult) {
    let d = job_dirs(&f.base);
    let body = body.replace("OUT", &d.output.display().to_string());
    let r = runner(f, &body, resp);
    let s = r.run(job(f, &d)).unwrap();
    (r, d, s)
}

fn setup_err(r: Result<SignedResult, RunError>) -> String {
    match r {
        Err(RunError::Setup { reason }) => reason,
        other => panic!("expected a setup error, nothing signed: {other:?}"),
    }
}

#[test]
fn an_accepted_run_lists_and_hashes_the_outputs_and_verifies() {
    let f = fx("ok");
    let (r, d, s) = run_ok(&f, "printf hello > OUT/a; printf x > OUT/b", &ok());
    let res = verify(&s, &r.verifying_key()).unwrap();
    assert!(res.verdict.is_accepted(), "{:?}", res.verdict);
    assert_eq!(
        res.outputs,
        vec![Output::of("a", b"hello"), Output::of("b", b"x")]
    );
    assert_eq!(res.exit, ExitInfo::exited(0));
    check_outputs(res, &d.output).unwrap();
}

#[test]
fn every_result_states_what_the_job_really_got() {
    let f = fx("feat");
    let (r, _d, s) = run_ok(&f, "", &ok());
    assert_eq!(s.result.runner, r.features());
    assert_eq!(s.result.runner.runner, "heimdall");
    assert!(s.result.runner.landlock);
    assert_eq!(s.result.runner.seccomp, Protection::Default);
    assert!(s.result.runner.broker && !s.result.runner.wasm);
    assert!(s.result.runner.image_verified_each_start);
}

#[test]
fn every_signed_result_names_the_kernel_features_the_runner_got() {
    let f = fx("kernelfeat");
    let (r, _d, s) = run_ok(&f, "", &ok());
    let k = s
        .result
        .kernel
        .clone()
        .expect("the result names its kernel");
    assert_eq!(k, r.kernel().features());
    assert_eq!(k, kernel().features());
    assert_eq!(k.release, "7.0.0");
    assert_eq!(k.landlock_abi, Some(8));
    assert!(k.seccomp_filter && k.user_notif && k.notif_addfd);
    assert_eq!(k.cgroup_controllers, ["cpu", "memory", "pids"]);
    // and it is inside the signature
    let mut forged = s.clone();
    forged.result.kernel.as_mut().unwrap().landlock_abi = None;
    assert!(verify(&forged, &r.verifying_key()).is_err());
    assert!(verify(&s, &r.verifying_key()).is_ok());
}

#[test]
fn a_partially_enforced_landlock_still_counts_as_landlock() {
    let f = fx("partial");
    let inside = INSIDE.replace("Full", "Partial");
    let (_r, _d, s) = run_ok(&f, "", &resp(r#""exit_code":0,"signal":null"#, "", &inside));
    assert!(s.result.runner.landlock);
}

#[test]
fn no_inside_report_when_landlock_was_expected_is_a_setup_error() {
    let f = fx("noinside");
    let d = job_dirs(&f.base);
    let r = runner(&f, "", &resp(r#""exit_code":0,"signal":null"#, "", "null"));
    let why = setup_err(r.run(job(&f, &d)));
    assert!(why.contains("inside"), "{why}");
}

#[test]
fn landlock_not_applied_inside_the_job_is_a_setup_error_never_a_signed_result() {
    let f = fx("nolandlock");
    let d = job_dirs(&f.base);
    let inside = r#"{"no_new_privs":true,"seccomp_mode":2,"landlock":null,"error":null}"#;
    let r = runner(&f, "", &resp(r#""exit_code":0,"signal":null"#, "", inside));
    let why = setup_err(r.run(job(&f, &d)));
    assert!(why.contains("Landlock"), "{why}");
}

#[test]
fn an_executor_error_inside_the_job_is_a_setup_error() {
    let f = fx("execerr");
    let d = job_dirs(&f.base);
    let inside =
        r#"{"no_new_privs":true,"seccomp_mode":2,"landlock":null,"error":"landlock: EINVAL"}"#;
    let r = runner(&f, "", &resp(r#""exit_code":1,"signal":null"#, "", inside));
    let why = setup_err(r.run(job(&f, &d)));
    assert!(why.contains("EINVAL"), "{why}");
}

#[test]
fn no_seccomp_filter_or_no_new_privs_inside_is_a_setup_error() {
    for inside in [
        INSIDE.replace(r#""seccomp_mode":2"#, r#""seccomp_mode":0"#),
        INSIDE.replace(r#""no_new_privs":true"#, r#""no_new_privs":false"#),
    ] {
        let f = fx("noseccomp");
        let d = job_dirs(&f.base);
        let r = runner(&f, "", &resp(r#""exit_code":0,"signal":null"#, "", &inside));
        setup_err(r.run(job(&f, &d)));
    }
}

#[test]
fn swap_not_zero_is_a_setup_error() {
    let f = fx("swap");
    let d = job_dirs(&f.base);
    let body = ok().replace(r#""swap_max":"0""#, r#""swap_max":"max""#);
    let r = runner(&f, "", &body);
    let why = setup_err(r.run(job(&f, &d)));
    assert!(why.contains("swap"), "{why}");
}

#[test]
fn with_landlock_off_the_request_has_no_plan_and_the_result_says_landlock_false() {
    let f = fx("off");
    let mut cfg = cfg_with(
        &f,
        "",
        &resp(
            r#""exit_code":0,"signal":null"#,
            "",
            r#"{"no_new_privs":true,"seccomp_mode":2,"landlock":null,"error":null}"#,
        ),
    );
    cfg.strict = false;
    cfg.landlock = LandlockMode::Off;
    let r = runner_cfg(cfg);
    let d = job_dirs(&f.base);
    let s = r.run(job(&f, &d)).unwrap();
    assert!(!s.result.runner.landlock);
    assert!(!r.features().landlock);
    let req: ShimRequest = serde_json::from_str(&requests(&f)[0]).unwrap();
    assert_eq!(req.landlock, None);
}

#[test]
fn the_request_carries_the_landlock_plan_and_no_key_material() {
    let f = fx("req");
    let (r, _d, _s) = run_ok(&f, "", &ok());
    let raw = &requests(&f)[0];
    let req: ShimRequest = serde_json::from_str(raw).unwrap();
    let plan = req.landlock.expect("a plan");
    assert_eq!(plan.write_only, vec![PathBuf::from("/out")]);
    assert!(plan.read.contains(&"/in".into()));
    assert_eq!(
        plan.read_exec,
        vec![PathBuf::from("/bin"), PathBuf::from("/etc")]
    );
    let seed_hex = heimdall_core::hex::encode(&SEED);
    assert!(!raw.contains(&seed_hex));
    let _ = r.pinned();
}

#[test]
fn a_wall_kill_is_refused_with_the_wall_code() {
    let f = fx("wall");
    let body = resp(r#""exit_code":null,"signal":9"#, "", INSIDE)
        .replace(r#""wall_killed":false"#, r#""wall_killed":true"#);
    let (_r, _d, s) = run_ok(&f, "", &body);
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_WALL));
    assert_eq!(s.result.exit.killed, Some(KillReason::Wall));
}

#[test]
fn an_oom_kill_is_refused_with_the_memory_code() {
    let f = fx("oom");
    let body = resp(r#""exit_code":null,"signal":9"#, "", INSIDE)
        .replace(r#""oom_kills":0"#, r#""oom_kills":1"#);
    let (_r, _d, s) = run_ok(&f, "", &body);
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_MEMORY));
    assert_eq!(s.result.exit.killed, Some(KillReason::Memory));
}

#[test]
fn a_nonzero_exit_and_a_signal_death_are_refused_with_their_codes() {
    let f = fx("exit");
    let (_r, _d, s) = run_ok(
        &f,
        "printf r > OUT/report",
        &resp(r#""exit_code":3,"signal":null"#, "", INSIDE),
    );
    assert_eq!(s.result.verdict.code(), Some(codes::EXIT_NONZERO));
    assert_eq!(s.result.outputs, vec![Output::of("report", b"r")]);
    let (_r, _d, s) = run_ok(&f, "", &resp(r#""exit_code":null,"signal":9"#, "", INSIDE));
    assert_eq!(s.result.verdict.code(), Some(codes::EXIT_SIGNAL));
    assert_eq!(s.result.exit.signal, Some(9));
    // SIGSYS = the seccomp profile killed it: its own stable code.
    let (_r, _d, s) = run_ok(&f, "", &resp(r#""exit_code":null,"signal":31"#, "", INSIDE));
    assert_eq!(s.result.verdict.code(), Some(codes::SECCOMP_KILL));
    assert_eq!(s.result.exit.signal, Some(31));
}

#[test]
fn an_extra_directory_or_symlink_in_output_dir_is_refused_out_1() {
    let f = fx("xdir");
    let (_r, _d, s) = run_ok(&f, "mkdir OUT/sub", &ok());
    assert_eq!(s.result.verdict.code(), Some(codes::OUT_EXTRA));
    let (_r, _d, s) = run_ok(&f, "ln -s /etc/passwd OUT/passwd", &ok());
    assert_eq!(s.result.verdict.code(), Some(codes::OUT_EXTRA));
}

#[test]
fn a_shim_error_a_missing_shim_or_a_garbled_response_is_a_setup_error() {
    let f = fx("shimerr");
    let d = job_dirs(&f.base);
    let r = runner(&f, "", r#"{"error":"create: no user namespaces"}"#);
    setup_err(r.run(job(&f, &d)));
    let d = job_dirs(&f.base);
    let r = runner(&f, "", "not json");
    setup_err(r.run(job(&f, &d)));
    let mut cfg = HeimdallConfig::new(&f.layout.dir, f.base.join("work"));
    cfg.shim = ShimCommand {
        program: f.base.join("does-not-exist"),
        args: vec![],
    };
    let d = job_dirs(&f.base);
    setup_err(runner_cfg(cfg).run(job(&f, &d)));
}

#[test]
fn a_wasm_job_is_unsupported_and_a_peer_named_not_addressed_is_invalid() {
    let f = fx("unsup");
    let r = runner(&f, "", &ok());
    let d = job_dirs(&f.base);
    let mut j = job(&f, &d);
    j.kind = JobKind::Wasm {
        module: GuestRef {
            path: "/g.wasm".into(),
            blake3: "00".repeat(32),
        },
        decoder: None,
    };
    assert!(matches!(r.run(j), Err(RunError::Unsupported { .. })));
    let mut j = job(&f, &d);
    j.policy.net = Some(NetAllow::one("pypi.org", "pypi.org", 443));
    match r.run(j) {
        Err(RunError::InvalidJob { reason }) => {
            assert!(reason.contains("not an IP literal"), "{reason}")
        }
        other => panic!("{other:?}"),
    }
}

#[test]
fn bad_jobs_are_invalid_before_anything_runs() {
    let f = fx("bad");
    let marker = f.base.join("ran");
    let r = runner(&f, &format!(": > {}", marker.display()), &ok());
    let d = job_dirs(&f.base);
    type JobMut = Box<dyn Fn(&mut Job)>;
    let muts: Vec<(&str, JobMut)> = vec![
        (
            "unpinned digest",
            Box::new(|j| {
                if let JobKind::Native { image, .. } = &mut j.kind {
                    image.digest = "latest".into();
                }
            }),
        ),
        (
            "empty argv",
            Box::new(|j| {
                if let JobKind::Native { argv, .. } = &mut j.kind {
                    argv.clear();
                }
            }),
        ),
        (
            "zero wall",
            Box::new(|j| j.policy.limits.wall = std::time::Duration::ZERO),
        ),
        ("nan cpus", Box::new(|j| j.policy.limits.cpus = f32::NAN)),
        ("zero pids", Box::new(|j| j.policy.limits.pids = 0)),
        (
            "tiny memory",
            Box::new(|j| j.policy.limits.memory_bytes = 1024),
        ),
        (
            "relative read path",
            Box::new(|j| j.policy.read = vec!["rel".into()]),
        ),
        (
            "dotdot write path",
            Box::new(|j| j.policy.write = vec!["/a/../b".into()]),
        ),
        (
            "missing input",
            Box::new(|j| j.input_dir = "/nonexistent-heimdall".into()),
        ),
        (
            "a policy path over /in",
            Box::new(|j| j.policy.read = vec!["/in".into()]),
        ),
        (
            "a policy path over /proc",
            Box::new(|j| j.policy.write = vec!["/proc/x".into()]),
        ),
    ];
    for (what, m) in muts {
        let mut j = job(&f, &d);
        m(&mut j);
        assert!(
            matches!(r.run(j), Err(RunError::InvalidJob { .. })),
            "{what}"
        );
    }
    assert!(!marker.exists(), "the shim must not have run");
}

#[test]
fn a_non_empty_output_dir_or_a_reused_job_id_is_invalid() {
    let f = fx("dirty");
    let d = job_dirs(&f.base);
    std::fs::write(d.output.join("stale"), b"").unwrap();
    let r = runner(&f, "", &ok());
    assert!(matches!(
        r.run(job(&f, &d)),
        Err(RunError::InvalidJob { .. })
    ));
    let d = job_dirs(&f.base);
    let j = job(&f, &d);
    std::fs::create_dir_all(r.config().work_dir.join("jobs").join(j.id.to_hex())).unwrap();
    assert!(matches!(r.run(j), Err(RunError::InvalidJob { .. })));
}

#[test]
fn the_bundle_is_removed_after_every_run() {
    let f = fx("clean");
    let (r, _d, _s) = run_ok(&f, "", &ok());
    let jobs = r.config().work_dir.join("jobs");
    assert_eq!(std::fs::read_dir(jobs).unwrap().count(), 0);
}

#[test]
fn a_changed_image_byte_is_refused_at_the_next_start() {
    let f = fx("reverify");
    let r = runner(&f, "", &ok());
    let d1 = job_dirs(&f.base);
    assert!(r.run(job(&f, &d1)).is_ok());
    let m = f.layout.manifest(&f.image);
    let layer = m["layers"][0]["digest"].as_str().unwrap().to_string();
    let p = f.layout.blob_path(&layer);
    let mut b = std::fs::read(&p).unwrap();
    let n = b.len();
    b[n - 9] ^= 0xff;
    std::fs::write(&p, b).unwrap();
    let d2 = job_dirs(&f.base);
    assert!(matches!(r.run(job(&f, &d2)), Err(RunError::Image { .. })));
}

#[test]
fn the_config_given_to_the_shim_is_heimdalls_spec() {
    let f = fx("cfg");
    let d = job_dirs(&f.base);
    let dump = f.base.join("config-copy.json");
    let body = format!(
        "b=$(sed 's/.*\"bundle\":\"\\([^\"]*\\)\".*/\\1/' {}/req-$$.json)\ncp \"$b/config.json\" {}",
        f.base.display(),
        dump.display()
    );
    let r = runner(&f, &body, &ok());
    r.run(job(&f, &d)).unwrap();
    let c: serde_json::Value = serde_json::from_slice(&std::fs::read(&dump).unwrap()).unwrap();
    assert_eq!(c["linux"]["seccomp"], profile::seccomp_json());
    assert_eq!(c["root"]["readonly"], true);
    assert!(c["process"]["rlimits"]
        .as_array()
        .unwrap()
        .iter()
        .any(|r| r["type"] == "RLIMIT_NPROC"));
    let seed_hex = heimdall_core::hex::encode(&SEED);
    assert!(!std::fs::read_to_string(&dump).unwrap().contains(&seed_hex));
}

#[test]
fn jobs_run_in_parallel_through_a_shared_trait_object() {
    let f = fx("par");
    let r: Arc<dyn JobRunner> = Arc::new(runner(&f, "", &ok()));
    let hs: Vec<_> = (0..8)
        .map(|_| {
            let r = Arc::clone(&r);
            let d = job_dirs(&f.base);
            let j = job(&f, &d);
            std::thread::spawn(move || r.run(j))
        })
        .collect();
    for h in hs {
        let s = h.join().unwrap().unwrap();
        assert!(verify(&s, &r.verifying_key()).is_ok());
    }
}

#[test]
fn the_shim_is_found_through_the_environment_variable_or_next_to_the_executable() {
    let c = locate_shim();
    assert!(
        c.program.ends_with("heimdall-container-shim")
            || std::env::var_os("HEIMDALL_CONTAINER_SHIM").is_some()
    );
}

// ==== generated seccomp profiles (lane genprof) ====

/// The checked-in wasm-host profile for this arch, as a job's reference.
fn builtin_ref() -> (SeccompProfileRef, String) {
    let e = heimdall_profiles::ALL
        .iter()
        .find(|e| Some(e.profile.arch) == heimdall_seccomp::Arch::native())
        .expect("a checked-in profile for this arch");
    let d = Sha256Digest::of(e.oci_json.as_bytes()).to_prefixed();
    (
        SeccompProfileRef {
            source: ProfileSource::Workload(e.profile.workload.into()),
            sha256: d.clone(),
            fallback_to_default: false,
        },
        d,
    )
}

fn inside_generated(d: &str) -> String {
    format!(
        r#"{{"no_new_privs":true,"seccomp_mode":2,"landlock":"Full","error":null,"generated":"{d}"}}"#
    )
}

fn run_job_with(
    f: &Fx,
    body: &str,
    resp: &str,
    m: impl FnOnce(&mut Job),
) -> (HeimdallRunner, Result<SignedResult, RunError>) {
    let d = job_dirs(&f.base);
    let r = runner(f, body, resp);
    let mut j = job(f, &d);
    m(&mut j);
    let s = r.run(j);
    (r, s)
}

#[test]
fn a_job_naming_a_generated_profile_sends_it_to_the_shim_and_its_result_says_generated_with_the_digest(
) {
    let f = fx("gen-ok");
    let (pr, d) = builtin_ref();
    let resp = resp(r#""exit_code":0,"signal":null"#, "", &inside_generated(&d));
    let (r, s) = run_job_with(&f, "", &resp, |j| j.policy.seccomp = Some(pr));
    let s = s.unwrap();
    let res = verify(&s, &r.verifying_key()).unwrap();
    assert!(res.verdict.is_accepted(), "{:?}", res.verdict);
    assert_eq!(
        res.runner.seccomp,
        Protection::Generated {
            profile_digest: d.clone()
        }
    );
    // The runner's own features still say what a job WITHOUT a profile gets.
    assert_eq!(r.features().seccomp, Protection::Default);
    let req: ShimRequest = serde_json::from_str(&requests(&f)[0]).unwrap();
    let g = req.generated.expect("the shim was asked to apply it");
    assert_eq!(g.digest, d);
    assert!(!g.profile.rules.is_empty());
}

#[test]
fn a_generated_profile_the_executor_did_not_confirm_is_never_signed() {
    let f = fx("gen-unconfirmed");
    let (pr, _d) = builtin_ref();
    // The executor's report has no `generated`: it was not installed.
    let (_r, s) = run_job_with(&f, "", &ok(), |j| j.policy.seccomp = Some(pr.clone()));
    assert!(setup_err(s).contains("not confirmed"));
    // A report naming ANOTHER digest is no confirmation either.
    let other = format!("sha256:{}", "0".repeat(64));
    let resp = resp(
        r#""exit_code":0,"signal":null"#,
        "",
        &inside_generated(&other),
    );
    let (_r, s) = run_job_with(&f, "", &resp, |j| j.policy.seccomp = Some(pr));
    assert!(setup_err(s).contains("not confirmed"));
}

#[test]
fn a_tampered_or_missing_profile_refuses_the_job_before_the_shim_runs() {
    let f = fx("gen-tamper");
    let (mut pr, d) = builtin_ref();
    let mut bad = d.clone().into_bytes();
    let last = bad.len() - 1;
    bad[last] = if bad[last] == b'0' { b'1' } else { b'0' };
    pr.sha256 = String::from_utf8(bad).unwrap();
    let (_r, s) = run_job_with(&f, "", &ok(), |j| j.policy.seccomp = Some(pr.clone()));
    match s {
        Err(RunError::InvalidJob { reason }) => {
            assert!(reason.contains("digest mismatch"), "{reason}")
        }
        other => panic!("expected a refusal: {other:?}"),
    }
    pr.source = ProfileSource::Workload("no-such-workload".into());
    pr.sha256 = d;
    let (_r, s) = run_job_with(&f, "", &ok(), |j| j.policy.seccomp = Some(pr.clone()));
    match s {
        Err(RunError::InvalidJob { reason }) => assert!(reason.contains("missing"), "{reason}"),
        other => panic!("expected a refusal: {other:?}"),
    }
    pr.sha256 = "sha256:not-hex".into();
    assert!(matches!(
        run_job_with(&f, "", &ok(), |j| j.policy.seccomp = Some(pr)).1,
        Err(RunError::InvalidJob { .. })
    ));
    assert!(requests(&f).is_empty(), "the shim never ran");
}

#[test]
fn with_fallback_a_tampered_profile_runs_under_the_fixed_one_and_says_default() {
    let f = fx("gen-fallback");
    let (mut pr, _d) = builtin_ref();
    pr.sha256 = format!("sha256:{}", "ab".repeat(32));
    pr.fallback_to_default = true;
    let (_r, s) = run_job_with(&f, "", &ok(), |j| j.policy.seccomp = Some(pr));
    let s = s.unwrap();
    assert_eq!(s.result.runner.seccomp, Protection::Default);
    let req: ShimRequest = serde_json::from_str(&requests(&f)[0]).unwrap();
    assert!(req.generated.is_none());
}

#[test]
fn a_runner_configured_with_a_generated_profile_says_generated_in_features_and_every_result() {
    let f = fx("gen-cfg");
    let (pr, d) = builtin_ref();
    let resp = resp(r#""exit_code":0,"signal":null"#, "", &inside_generated(&d));
    let mut cfg = cfg_with(&f, "", &resp);
    cfg.seccomp = Some(pr);
    let r = runner_cfg(cfg);
    let want = Protection::Generated { profile_digest: d };
    assert_eq!(r.features().seccomp, want);
    let dirs = job_dirs(&f.base);
    let s = r.run(job(&f, &dirs)).unwrap();
    assert_eq!(s.result.runner.seccomp, want);
    assert_eq!(s.result.runner, r.features());
}

#[test]
fn a_runner_configured_with_a_tampered_profile_refuses_to_start() {
    let f = fx("gen-cfg-bad");
    let (mut pr, _d) = builtin_ref();
    pr.sha256 = format!("sha256:{}", "cd".repeat(32));
    let mut cfg = cfg_with(&f, "", &ok());
    cfg.seccomp = Some(pr.clone());
    let e = HeimdallRunner::with_kernel(cfg, BootKey::from_seed(SEED, BootId([1; 16])), kernel())
        .unwrap_err();
    assert!(e.to_string().contains("digest mismatch"), "{e}");
    // With the fallback it starts and says Default.
    pr.fallback_to_default = true;
    let mut cfg = cfg_with(&f, "", &ok());
    cfg.seccomp = Some(pr);
    assert_eq!(runner_cfg(cfg).features().seccomp, Protection::Default);
}

#[test]
fn a_sigsys_death_under_a_generated_profile_is_a_signed_refusal_naming_the_digest() {
    let f = fx("gen-sigsys");
    let (pr, d) = builtin_ref();
    let resp = resp(r#""exit_code":null,"signal":31"#, "", &inside_generated(&d));
    let (r, s) = run_job_with(&f, "", &resp, |j| j.policy.seccomp = Some(pr));
    let res = verify(&s.unwrap(), &r.verifying_key()).unwrap().clone();
    assert_eq!(res.verdict.code(), Some(codes::SECCOMP_KILL));
    match &res.verdict {
        Verdict::Refused { reason, .. } => assert!(reason.contains(&d), "{reason}"),
        v => panic!("{v:?}"),
    }
    assert_eq!(
        res.runner.seccomp,
        Protection::Generated { profile_digest: d }
    );
}

// ==== the pids limit and self-consistent output refusals ====

fn run_pids(f: &Fx, exit: &str, pids_max: u64, body: &str) -> (JobDirs, SignedResult) {
    let d = job_dirs(&f.base);
    let body = body.replace("OUT", &d.output.display().to_string());
    let resp =
        resp(exit, "", INSIDE).replace(r#""pids_max":0"#, &format!(r#""pids_max":{pids_max}"#));
    let r = runner(f, &body, &resp);
    let s = r.run(job(f, &d)).unwrap();
    verify(&s, &r.verifying_key()).unwrap();
    (d, s)
}

#[test]
fn a_job_that_hit_the_pids_limit_and_still_exited_zero_is_refused_heimdall_limit_pids() {
    let f = fx("pids0");
    let (d, s) = run_pids(
        &f,
        r#""exit_code":0,"signal":null"#,
        3,
        "printf r > OUT/report",
    );
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_PIDS));
    match &s.result.verdict {
        Verdict::Refused { reason, .. } => assert!(reason.contains("process limit"), "{reason}"),
        v => panic!("{v:?}"),
    }
    // The job's own outputs stay listed (like a non-zero exit) and check.
    assert_eq!(s.result.outputs, vec![Output::of("report", b"r")]);
    check_outputs(&s.result, &d.output).unwrap();
}

#[test]
fn a_nonzero_exit_after_hitting_the_pids_limit_is_also_heimdall_limit_pids() {
    let f = fx("pids1");
    let (_d, s) = run_pids(&f, r#""exit_code":2,"signal":null"#, 1, "");
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_PIDS));
    let (_d, s) = run_pids(&f, r#""exit_code":0,"signal":null"#, 0, "");
    assert!(s.result.verdict.is_accepted());
}

#[test]
fn an_out_1_or_out_2_refusal_leaves_output_dir_empty_so_the_signed_empty_list_checks() {
    let f = fx("outcons");
    for body in [
        "printf a > OUT/a; mkdir OUT/sub; printf x > OUT/sub/hidden",
        "printf a > OUT/a; ln -s /etc/passwd OUT/passwd",
    ] {
        let (_r, d, s) = run_ok(&f, body, &ok());
        assert_eq!(s.result.verdict.code(), Some(codes::OUT_EXTRA));
        assert!(s.result.outputs.is_empty());
        assert_eq!(std::fs::read_dir(&d.output).unwrap().count(), 0, "{body}");
        check_outputs(&s.result, &d.output).unwrap();
    }
    // out-2: one file over max_output_file_bytes.
    let d = job_dirs(&f.base);
    let body = format!(
        "head -c 2048 /dev/zero > {}/big; printf a > {}/a",
        d.output.display(),
        d.output.display()
    );
    let r = runner(&f, &body, &ok());
    let mut j = job(&f, &d);
    j.policy.limits.max_output_file_bytes = 1024;
    let s = r.run(j).unwrap();
    assert_eq!(s.result.verdict.code(), Some(codes::OUT_TOO_LARGE));
    assert!(s.result.outputs.is_empty());
    assert_eq!(std::fs::read_dir(&d.output).unwrap().count(), 0);
    check_outputs(&s.result, &d.output).unwrap();
}

// ---- the syscall broker (spec §4, §10) ----

fn broker_req(f: &Fx) -> serde_json::Value {
    let r = requests(f);
    assert_eq!(r.len(), 1);
    let v: serde_json::Value = serde_json::from_str(&r[0]).unwrap();
    v["broker"].clone()
}

#[test]
fn the_shim_is_asked_for_the_broker_in_full_mode_with_the_jobs_policy() {
    let f = fx("brokerreq");
    let d = job_dirs(&f.base);
    let r = runner(&f, "", &ok());
    let mut j = job(&f, &d);
    j.policy.read = vec![f.base.clone()];
    r.run(j).unwrap();
    let b = broker_req(&f);
    assert_eq!(b["mode"], "full");
    let pre: Vec<(String, String)> = b["policy"]["prefixes"]
        .as_array()
        .unwrap()
        .iter()
        .map(|p| {
            (
                p["path"].as_str().unwrap().to_string(),
                p["access"].as_str().unwrap().to_string(),
            )
        })
        .collect();
    let has = |p: &str, a: &str| pre.iter().any(|(x, y)| x == p && y == a);
    assert!(
        has("/", "read") && has("/in", "read") && has("/out", "write_only"),
        "{pre:?}"
    );
    assert!(
        has("/tmp", "read_write") && has("/proc", "read") && has("/dev/null", "read_write"),
        "{pre:?}"
    );
    assert!(
        has(f.base.to_str().unwrap(), "read"),
        "policy.read is a read prefix: {pre:?}"
    );
    assert_eq!(b["policy"]["fetch"], serde_json::json!([]), "no network");
    assert_eq!(b["policy"]["exec"], "allow");
}

#[test]
fn the_brokers_decisions_are_signed_into_the_result_after_a_mode_event() {
    let f = fx("brokerev");
    let (r, _d, s) = run_ok(&f, "", &ok());
    let res = verify(&s, &r.verifying_key()).unwrap();
    assert!(res.runner.broker);
    assert_eq!(res.events.len(), 2, "{:?}", res.events);
    assert_eq!(res.events[0].call, "broker");
    assert_eq!(res.events[0].args, vec!["mode=full".to_string()]);
    assert_eq!(res.events[1].call, "openat");
    assert_eq!(res.events[1].args[0], "path=/in/a");
}

#[test]
fn a_broker_filter_missing_inside_the_job_is_a_setup_error_never_signed() {
    let f = fx("brokermissing");
    let d = job_dirs(&f.base);
    let inside =
        r#"{"broker":null,"no_new_privs":true,"seccomp_mode":2,"landlock":"Full","error":null}"#;
    let r = runner(&f, "", &resp(r#""exit_code":0,"signal":null"#, "", inside));
    let why = setup_err(r.run(job(&f, &d)));
    assert!(why.contains("broker filter"), "{why}");
}

#[test]
fn a_broker_that_never_served_or_ran_weaker_than_promised_is_a_setup_error() {
    let f = fx("brokernone");
    let d = job_dirs(&f.base);
    let r = runner(
        &f,
        "",
        &resp(
            r#""exit_code":0,"signal":null"#,
            r#","broker":null"#,
            INSIDE,
        ),
    );
    let why = setup_err(r.run(job(&f, &d)));
    assert!(why.contains("never served"), "{why}");
    let weaker = BROKER.replace(r#""mode":"full""#, r#""mode":"check_only""#);
    let r = runner(
        &f,
        "",
        &resp(
            r#""exit_code":0,"signal":null"#,
            &format!(r#","broker":{weaker}"#),
            INSIDE,
        ),
    );
    let why = setup_err(r.run(job(&f, &job_dirs(&f.base))));
    assert!(why.contains("CheckOnly"), "{why}");
}

#[test]
fn on_a_kernel_without_addfd_jobs_get_check_only_a_warning_event_and_no_broker_feature() {
    let f = fx("brokercheck");
    let mut k = kernel();
    k.notif_addfd = false;
    let inside = INSIDE.replacen('{', r#"{"broker":"check_only","#, 1);
    let rep = BROKER.replace(r#""mode":"full""#, r#""mode":"check_only""#);
    let cfg = cfg_with(
        &f,
        "",
        &resp(
            r#""exit_code":0,"signal":null"#,
            &format!(r#","broker":{rep}"#),
            &inside,
        ),
    );
    let r = HeimdallRunner::with_kernel(cfg, BootKey::from_seed(SEED, BootId([1; 16])), k).unwrap();
    assert!(!r.features().broker);
    let d = job_dirs(&f.base);
    let s = r.run(job(&f, &d)).unwrap();
    assert!(!s.result.runner.broker);
    assert_eq!(s.result.events[0].args[0], "mode=check-only");
    assert!(
        s.result.events[0].args[1].contains("not TOCTOU-safe"),
        "{:?}",
        s.result.events[0]
    );
    assert_eq!(broker_req(&f)["mode"], "check_only");
}

#[test]
fn on_a_kernel_without_user_notif_jobs_get_deny_only_and_no_broker_process() {
    let f = fx("brokerdeny");
    let mut k = kernel();
    k.user_notif = false;
    k.notif_addfd = false;
    let inside = INSIDE.replacen('{', r#"{"broker":"deny_only","#, 1);
    let cfg = cfg_with(
        &f,
        "",
        &resp(
            r#""exit_code":0,"signal":null"#,
            r#","broker":null"#,
            &inside,
        ),
    );
    let r = HeimdallRunner::with_kernel(cfg, BootKey::from_seed(SEED, BootId([1; 16])), k).unwrap();
    assert!(!r.features().broker);
    let d = job_dirs(&f.base);
    let s = r.run(job(&f, &d)).unwrap();
    assert!(!s.result.runner.broker);
    assert_eq!(s.result.events.len(), 1);
    assert_eq!(s.result.events[0].args[0], "mode=deny-only");
    assert!(s.result.events[0].args[1].contains("fail closed"));
}

#[test]
fn with_the_broker_switched_off_the_shim_gets_no_broker_and_features_say_so() {
    let f = fx("brokeroff");
    let inside = INSIDE.replacen('{', r#"{"broker":null,"#, 1);
    let mut cfg = cfg_with(
        &f,
        "",
        &resp(
            r#""exit_code":0,"signal":null"#,
            r#","broker":null"#,
            &inside,
        ),
    );
    cfg.broker = BrokerSetting::Off;
    let r = runner_cfg(cfg);
    assert!(!r.features().broker);
    let s = r.run(job(&f, &job_dirs(&f.base))).unwrap();
    assert!(!s.result.runner.broker && s.result.events.is_empty());
    assert!(broker_req(&f).is_null());
}

// ==== host_ids: container root mapped to a host id range (0.2.1) ====

fn whoami() -> (u32, u32) {
    use std::os::unix::fs::MetadataExt;
    let m = std::fs::metadata("/proc/self").unwrap();
    (m.uid(), m.gid())
}

/// Root with ids 10000..11000 mapped (the ownership half needs it).
fn can_map_10000() -> bool {
    let p = scratch("hc-idmap-probe").join("f");
    std::fs::write(&p, b"").unwrap();
    whoami().0 == 0 && std::os::unix::fs::lchown(&p, Some(10_999), Some(10_999)).is_ok()
}

/// Run one job whose shim copies config.json to `<base>/config.json` and the
/// owners of the rootfs, a file in it, /in, /in/a and /out to `<base>/owners`.
fn run_dumping(f: &Fx, ids: Option<(u32, u32)>) -> (serde_json::Value, String) {
    let d = job_dirs(&f.base);
    std::fs::write(d.input.join("a"), b"in").unwrap();
    let body = format!(
        "b=$(sed 's/.*\"bundle\":\"\\([^\"]*\\)\".*/\\1/' {base}/req-$$.json)\n\
         cp \"$b/config.json\" {base}/config.json\n\
         stat -c %u:%g \"$b/rootfs\" \"$b/rootfs/bin/tool\" {i} {i}/a {o} > {base}/owners",
        base = f.base.display(),
        i = d.input.display(),
        o = d.output.display()
    );
    let mut r = runner(f, &body, &ok());
    if let Some((b, c)) = ids {
        r = r.with_host_ids(b, c).unwrap();
    }
    let s = r.run(job(f, &d)).unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    (
        serde_json::from_slice(&std::fs::read(f.base.join("config.json")).unwrap()).unwrap(),
        std::fs::read_to_string(f.base.join("owners")).unwrap(),
    )
}

#[test]
fn without_host_ids_heimdall_maps_container_root_to_its_own_uid_and_gid_as_in_0_2_0() {
    let f = fx("idmap-off");
    assert_eq!(runner(&f, "", &ok()).host_ids(), None);
    let (c, owners) = run_dumping(&f, None);
    let (uid, gid) = whoami();
    assert_eq!(
        c["linux"]["uidMappings"],
        serde_json::json!([{"containerID": 0, "hostID": uid, "size": 1}])
    );
    assert_eq!(
        c["linux"]["gidMappings"],
        serde_json::json!([{"containerID": 0, "hostID": gid, "size": 1}])
    );
    for l in owners.lines() {
        assert_eq!(l, format!("{uid}:{gid}"), "{owners}");
    }
}

#[test]
fn with_host_ids_the_job_root_is_the_range_and_owns_its_root_input_and_output() {
    let f = fx("idmap-on");
    if whoami().0 != 0 {
        let e = runner(&f, "", &ok())
            .with_host_ids(10_000, 1000)
            .unwrap_err();
        assert!(
            matches!(&e, RunError::Unsupported { what } if what.contains("host_ids")),
            "{e}"
        );
        eprintln!("not root: refused as expected; the mapping half needs root");
        return;
    }
    if !can_map_10000() {
        eprintln!("skipped: ids 10000..11000 are not mapped here");
        return;
    }
    let (c, owners) = run_dumping(&f, Some((10_000, 1000)));
    let want = serde_json::json!([{"containerID": 0, "hostID": 10_000, "size": 1000}]);
    assert_eq!(c["linux"]["uidMappings"], want);
    assert_eq!(c["linux"]["gidMappings"], want);
    // The rest of Heimdall's spec is unchanged.
    assert_eq!(c["linux"]["seccomp"], profile::seccomp_json());
    let lines: Vec<&str> = owners.lines().collect();
    assert_eq!(lines, ["10000:10000"; 5], "{owners}");
}

#[test]
fn heimdall_with_host_ids_refuses_an_invalid_range_or_a_non_root_runner_by_name() {
    let f = fx("idmap-refuse");
    let mut cases = vec![(0, 10), (10_000, 0), (u32::MAX, 1), (u32::MAX - 5, 10)];
    let (uid, gid) = whoami();
    if uid != 0 {
        cases.extend([(uid, 1), (gid, 1), (100_000, 65_536)]);
    }
    for (b, c) in cases {
        let e = runner(&f, "", &ok()).with_host_ids(b, c).unwrap_err();
        assert!(
            matches!(&e, RunError::Unsupported { what } if what.contains("host_ids")),
            "{b} {c}: {e}"
        );
    }
}

// ==== io limits (io.max) ====

/// A fake sysfs with one disk, 7:3.
fn io_sysfs(f: &Fx) -> PathBuf {
    let s = f.base.join("sysfs");
    let disk = s.join("devices/virtual/block/loop3");
    std::fs::create_dir_all(&disk).unwrap();
    std::fs::write(disk.join("dev"), "7:3\n").unwrap();
    std::fs::create_dir_all(s.join("dev/block")).unwrap();
    std::os::unix::fs::symlink(&disk, s.join("dev/block/7:3")).unwrap();
    s
}

fn io_runner(f: &Fx, resp: &str) -> HeimdallRunner {
    let mut cfg = cfg_with(f, "", resp);
    cfg.sysfs_root = io_sysfs(f);
    cfg.io_max = vec![iolimit::IoMax {
        device: iolimit::IoDevice::Number(iolimit::DevNum { major: 7, minor: 3 }),
        rates: iolimit::IoRates {
            rbps: Some(2 << 20),
            ..Default::default()
        },
    }];
    let mut k = kernel();
    k.cgroup_controllers.push("io".into());
    HeimdallRunner::with_kernel(cfg, BootKey::from_seed(SEED, BootId([1; 16])), k).unwrap()
}

#[test]
fn io_limits_not_read_back_from_the_job_cgroup_are_a_setup_error() {
    let f = fx("io-none");
    let d = job_dirs(&f.base);
    let why = setup_err(io_runner(&f, &ok()).run(job(&f, &d)));
    assert!(why.contains("io.max"), "{why}");
}

#[test]
fn io_limits_read_back_wrong_are_a_setup_error() {
    let f = fx("io-wrong");
    let d = job_dirs(&f.base);
    let body = ok().replace(
        r#""swap_max":"0""#,
        r#""swap_max":"0","io_max":"7:3 rbps=max wbps=max riops=max wiops=max""#,
    );
    let why = setup_err(io_runner(&f, &body).run(job(&f, &d)));
    assert!(why.contains("rbps=max"), "{why}");
}

#[test]
fn io_limits_read_back_right_are_signed() {
    let f = fx("io-ok");
    let d = job_dirs(&f.base);
    let body = ok().replace(
        r#""swap_max":"0""#,
        r#""swap_max":"0","io_weight":"default 50","io_max":"7:3 rbps=2097152 wbps=max riops=max wiops=max\n""#,
    );
    let r = io_runner(&f, &body);
    let s = r.run(job(&f, &d)).unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
}

#[test]
fn the_shim_is_asked_for_io_weight_and_a_weight_not_read_back_is_a_setup_error() {
    let f = fx("ioweight");
    let mut k = kernel();
    k.cgroup_controllers.push("io".into());
    let mk = |resp: &str| {
        HeimdallRunner::with_kernel(
            cfg_with(&f, "", resp),
            BootKey::from_seed(SEED, BootId([1; 16])),
            k.clone(),
        )
        .unwrap()
    };
    let d = job_dirs(&f.base);
    let why = setup_err(mk(&ok()).run(job(&f, &d)));
    assert!(why.contains("io.weight"), "{why}");
    assert!(
        requests(&f).iter().all(|r| r.contains(r#""io_weight":50"#)),
        "{:?}",
        requests(&f)
    );
    let d = job_dirs(&f.base);
    let wrong = ok().replace(
        r#""swap_max":"0""#,
        r#""swap_max":"0","io_weight":"default 7""#,
    );
    let why = setup_err(mk(&wrong).run(job(&f, &d)));
    assert!(why.contains("default 7"), "{why}");
    let d = job_dirs(&f.base);
    let right = ok().replace(
        r#""swap_max":"0""#,
        r#""swap_max":"0","io_weight":"default 50""#,
    );
    let s = mk(&right).run(job(&f, &d)).unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
}

#[test]
fn without_the_io_controller_no_io_weight_is_asked_or_checked() {
    let f = fx("noioweight");
    let (_r, _d, s) = run_ok(&f, "", &ok());
    assert!(s.result.verdict.is_accepted());
    assert!(requests(&f).iter().all(|r| !r.contains("io_weight")));
}

// ==== the job workspace (owner decision 2026-11-01) ====

fn ws_job(f: &Fx, d: &JobDirs, bytes: u64, exec: bool) -> Job {
    let mut j = job(f, d);
    j.policy.workspace = Some(Workspace { bytes, exec });
    j
}

#[test]
fn an_exec_workspace_is_refused_unless_the_runner_allows_it() {
    let f = fx("ws-exec");
    let d = job_dirs(&f.base);
    match runner(&f, "", &ok()).run(ws_job(&f, &d, 1 << 20, true)) {
        Err(RunError::Unsupported { what }) => assert!(what.contains("exec"), "{what}"),
        other => panic!("expected a refusal, got {other:?}"),
    }
    let mut cfg = cfg_with(&f, "", &ok());
    cfg.allow_workspace_exec = true;
    let d = job_dirs(&f.base);
    let s = runner_cfg(cfg).run(ws_job(&f, &d, 1 << 20, true)).unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
}

#[test]
fn a_tmpfs_workspace_is_in_the_landlock_plan_and_needs_no_host_folder() {
    let f = fx("ws-tmpfs");
    let d = job_dirs(&f.base);
    let s = runner(&f, "", &ok())
        .run(ws_job(&f, &d, 1 << 20, false))
        .unwrap();
    assert!(s.result.verdict.is_accepted());
    let reqs = requests(&f);
    let req: serde_json::Value = serde_json::from_str(&reqs[0]).unwrap();
    assert!(
        req["landlock"]["read_write"]
            .as_array()
            .unwrap()
            .contains(&serde_json::json!("/work")),
        "{req}"
    );
    assert!(req.get("workspace").is_none(), "{req}");
}

#[test]
fn a_host_workspace_exists_while_the_job_runs_and_is_deleted_after() {
    let f = fx("ws-host");
    let d = job_dirs(&f.base);
    // The fake shim proves the folder is there (and writable) while it runs.
    let body = format!(
        "dir=$(sed -n 's/.*\"dir\":\"\\([^\"]*\\)\".*/\\1/p' {}/req-$$.json); test -d \"$dir\" && touch \"$dir/x\" && echo \"$dir\" > {}/ws-seen",
        f.base.display(),
        f.base.display()
    );
    let r = runner(&f, &body, &ok());
    let s = r.run(ws_job(&f, &d, 2 << 30, false)).unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    let seen = std::fs::read_to_string(f.base.join("ws-seen")).expect("the shim saw the folder");
    let seen = std::path::PathBuf::from(seen.trim());
    assert!(
        seen.starts_with(f.base.join("work/workspaces")),
        "{}",
        seen.display()
    );
    assert!(!seen.exists(), "the workspace outlived its job");
    let req: serde_json::Value = serde_json::from_str(&requests(&f)[0]).unwrap();
    assert_eq!(req["workspace"]["cap"], 2u64 << 30, "{req}");
}

#[test]
fn a_workspace_over_its_cap_is_refused_and_killed_for_it() {
    let f = fx("ws-cap");
    let d = job_dirs(&f.base);
    let body = ok().replace(
        r#""swap_max":"0""#,
        r#""swap_max":"0","workspace_killed":true,"workspace_bytes":3221225472"#,
    );
    let s = runner(&f, "", &body)
        .run(ws_job(&f, &d, 2 << 30, false))
        .unwrap();
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_WORKSPACE));
    assert_eq!(s.result.exit.killed, Some(KillReason::Workspace));
}

// ==== the jobs' parent cgroup exists before any shim starts ====

/// libcgroups 0.7.0 creates each missing path component with a plain
/// mkdir after an exists() check, so two parallel jobs that both find
/// `<root>/heimdall` missing race and one fails EEXIST ("failed to create
/// dir /sys/fs/cgroup/heimdall: File exists", 1 of 12 container-test runs
/// on oden). The runner creates the parent itself, race-free, first.
#[test]
fn the_runner_creates_the_jobs_parent_cgroup_before_the_shim_runs() {
    let f = fx("cg-parent");
    let d = job_dirs(&f.base);
    let root = f.base.join("cgroot");
    std::fs::create_dir_all(&root).unwrap();
    let body = format!(
        "test -d {}/heimdall && echo yes > {}/parent-seen",
        root.display(),
        f.base.display()
    );
    let mut cfg = cfg_with(&f, &body, &ok());
    cfg.cgroup_root = root.clone();
    let _ = runner_cfg(cfg).run(job(&f, &d));
    assert!(
        f.base.join("parent-seen").exists(),
        "the shim started before <cgroup_root>/heimdall existed"
    );
}

// ==== fetch jobs ====

#[test]
fn a_fetch_job_gets_its_declared_peers_in_the_broker_policy() {
    let f = fx("fetchreq");
    let d = job_dirs(&f.base);
    let r = runner(&f, "", &ok());
    let mut j = job(&f, &d);
    j.policy.net = Some(NetAllow {
        peers: vec![
            NetPeer::new("pypi.org", "10.1.2.3", 443),
            NetPeer::new("files.pythonhosted.org", "10.1.2.4", 443),
        ],
    });
    r.run(j).unwrap();
    let req: serde_json::Value = serde_json::from_str(&requests(&f)[0]).unwrap();
    assert_eq!(
        req["broker"]["policy"]["fetch"],
        serde_json::json!(["10.1.2.3:443", "10.1.2.4:443"])
    );
    assert_eq!(req["broker"]["mode"], "full");
}

#[test]
fn a_fetch_job_needs_the_broker_in_full_mode_and_a_valid_peer_set() {
    let f = fx("fetchmode");
    let peer = || Some(NetAllow::one("a.example", "10.1.2.3", 443));
    let mut k = kernel();
    k.notif_addfd = false;
    let r = HeimdallRunner::with_kernel(cfg_with(&f, "", &ok()), BootKey::generate(), k).unwrap();
    let mut j = job(&f, &job_dirs(&f.base));
    j.policy.net = peer();
    assert!(
        matches!(r.run(j), Err(RunError::Unsupported { .. })),
        "check-only cannot fetch"
    );
    let mut c = cfg_with(&f, "", &ok());
    c.broker = BrokerSetting::Off;
    let r = runner_cfg(c);
    let mut j = job(&f, &job_dirs(&f.base));
    j.policy.net = peer();
    assert!(
        matches!(r.run(j), Err(RunError::Unsupported { .. })),
        "no broker cannot fetch"
    );
    let r = runner(&f, "", &ok());
    let bad: Vec<(NetAllow, &str)> = vec![
        (NetAllow { peers: vec![] }, "no peers"),
        (
            NetAllow {
                peers: (0..17)
                    .map(|i| NetPeer::new(&format!("h{i}.example"), "10.0.0.1", 1000 + i))
                    .collect(),
            },
            "at most 16",
        ),
        (
            NetAllow::one("pypi.org", "pypi.org", 443),
            "not an IP literal",
        ),
        (NetAllow::one("a.example", "10.1.2.3", 0), "port 0"),
    ];
    for (n, why) in bad {
        let mut j = job(&f, &job_dirs(&f.base));
        j.policy.net = Some(n);
        match r.run(j) {
            Err(RunError::InvalidJob { reason }) => assert!(reason.contains(why), "{reason}"),
            other => panic!("{why}: {other:?}"),
        }
    }
    assert!(requests(&f).is_empty(), "nothing ran");
}

// ==== RLIMIT_NOFILE set by the runner (0.3.1) ====

#[test]
fn with_nofile_is_kept_and_the_default_is_1024() {
    let f = fx("nofile-ok");
    assert_eq!(runner(&f, "", &ok()).nofile(), spec::NOFILE);
    assert_eq!(
        runner(&f, "", &ok()).with_nofile(4096).unwrap().nofile(),
        4096
    );
}

#[test]
fn with_nofile_refuses_too_few_too_many_or_more_than_the_runner_has_by_name() {
    let f = fx("nofile-refuse");
    let mut hard = libc::rlimit {
        rlim_cur: 0,
        rlim_max: 0,
    };
    assert_eq!(
        unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut hard) },
        0
    );
    for n in [
        0,
        spec::NOFILE_MIN - 1,
        spec::NOFILE_MAX + 1,
        u64::MAX,
        hard.rlim_max + 1,
    ] {
        if (spec::NOFILE_MIN..=spec::NOFILE_MAX).contains(&n) && n <= hard.rlim_max {
            continue;
        }
        let e = runner(&f, "", &ok()).with_nofile(n).unwrap_err();
        assert!(
            matches!(&e, RunError::Unsupported { what } if what.contains("nofile")),
            "{n}: {e}"
        );
    }
}

#[test]
fn with_nofile_reaches_the_jobs_config() {
    let f = fx("nofile-cfg");
    let d = job_dirs(&f.base);
    let body = format!(
        r#"b=$(sed 's/.*"bundle":"\([^"]*\)".*/\1/' {base}/req-$$.json)
cp "$b/config.json" {base}/config.json"#,
        base = f.base.display()
    );
    let r = runner(&f, &body, &ok()).with_nofile(2048).unwrap();
    let s = r.run(job(&f, &d)).unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    let c: serde_json::Value =
        serde_json::from_slice(&std::fs::read(f.base.join("config.json")).unwrap()).unwrap();
    let n = c["process"]["rlimits"]
        .as_array()
        .unwrap()
        .iter()
        .find(|r| r["type"] == "RLIMIT_NOFILE")
        .cloned()
        .unwrap();
    assert_eq!(
        (n["soft"].as_u64(), n["hard"].as_u64()),
        (Some(2048), Some(2048))
    );
}

// ── learning runs (probe-container) next to the broker ─────────────────────

fn learn_job(f: &Fx, d: &JobDirs) -> heimdall_container::learn::LearnJob {
    heimdall_container::learn::LearnJob {
        job: job(f, d),
        env: vec![],
        spec: heimdall_container::learn::LearnSpec::default(),
        libc: Some(heimdall_seccomp::Libc::Musl),
        term_grace: std::time::Duration::from_secs(1),
        log: None,
    }
}

/// A learning job declares no network: its job has an empty netns and no
/// broker to inject a peer, so `policy.net` is refused before anything runs.
#[test]
fn a_learning_job_with_network_peers_is_refused_before_anything_runs() {
    let f = fx("learn-net");
    let marker = f.base.join("ran");
    let r = runner(&f, &format!(": > {}", marker.display()), &ok());
    let d = job_dirs(&f.base);
    let mut lj = learn_job(&f, &d);
    lj.job.policy.net = Some(NetAllow::one("a.example", "10.1.2.3", 443));
    let signals = std::sync::atomic::AtomicU64::new(0);
    match r.learn(lj, &signals, |_| panic!("nothing starts")) {
        Err(RunError::InvalidJob { reason }) => {
            assert!(reason.contains("no network"), "{reason}")
        }
        other => panic!("{other:?}"),
    }
    assert!(!marker.exists(), "the shim never ran");
}

/// The learning filter holds the job's one USER_NOTIF listener: a learning
/// request never asks the shim for a broker, even on a broker-capable runner.
#[test]
fn a_learning_request_carries_the_learning_plan_and_no_broker() {
    let f = fx("learn-nobroker");
    let r = runner(&f, "", &ok());
    assert!(r.broker_mode().is_some(), "this runner brokers normal jobs");
    let d = job_dirs(&f.base);
    let signals = std::sync::atomic::AtomicU64::new(0);
    // The canned response is a normal job's: the learning run is refused
    // after the shim answered, which is all this test needs.
    let _ = r.learn(learn_job(&f, &d), &signals, |_| {});
    let reqs = requests(&f);
    assert_eq!(reqs.len(), 1, "{reqs:?}");
    let req: serde_json::Value = serde_json::from_str(&reqs[0]).unwrap();
    assert!(req.get("learning").is_some_and(|l| !l.is_null()), "{req}");
    assert!(req.get("broker").is_none_or(|b| b.is_null()), "{req}");
}

/// A runner's `with_nofile` covers its learning jobs too: the program is
/// probed with the RLIMIT_NOFILE it will run with.
#[test]
fn a_learning_job_gets_the_runners_nofile() {
    let f = fx("learn-nofile");
    let cfg_copy = f.base.join("learn-config.json");
    let body = format!(
        "cp {}/work/jobs/*/config.json {}",
        f.base.display(),
        cfg_copy.display()
    );
    let r = runner(&f, &body, &ok()).with_nofile(64).unwrap();
    let d = job_dirs(&f.base);
    let signals = std::sync::atomic::AtomicU64::new(0);
    let _ = r.learn(learn_job(&f, &d), &signals, |_| {});
    let c: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&cfg_copy).unwrap()).unwrap();
    let nofile = c["process"]["rlimits"]
        .as_array()
        .unwrap()
        .iter()
        .find(|r| r["type"] == "RLIMIT_NOFILE")
        .cloned()
        .unwrap();
    assert_eq!((nofile["soft"].as_u64(), nofile["hard"].as_u64()), (Some(64), Some(64)), "{nofile}");
}