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/container.rs

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

//! Real protected containers. Heimdall is a defensive sandbox; every job here
//! attacks only its own container: its input, its root, its limits, its
//! network namespace, its Landlock domain, its seccomp profile.
//!
//! Part one repeats every heimdall-plain container test on HeimdallRunner;
//! part two proves what heimdall-container adds. These need user namespaces,
//! cgroup v2 with memory, pids and cpu delegated, Landlock, and a static
//! busybox, so they are `#[ignore]`d in plain CI: `scripts/container-tests.sh`
//! runs them in a privileged rootless podman container.

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

use std::io::Read;
use std::net::TcpListener;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};

use common::*;
use heimdall_container::*;
use heimdall_core::*;

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

fn cfg(base: &std::path::Path, layout: &Layout) -> HeimdallConfig {
    HeimdallConfig::new(&layout.dir, base.join("work"))
}

fn fx_with(name: &str, m: impl FnOnce(&mut HeimdallConfig, &std::path::Path)) -> Fx {
    let base = scratch(&format!("hc-{name}"));
    let layout = Layout::new(&base.join("layout"));
    let image = busybox_image(&layout);
    let mut c = cfg(&base, &layout);
    m(&mut c, &base);
    Fx {
        runner: HeimdallRunner::new(c).expect("this kernel has every feature (strict)"),
        base,
        image,
        layout,
    }
}

fn fx(name: &str) -> Fx {
    fx_with(name, |_, _| {})
}

impl Fx {
    fn run(&self, script: &str) -> (JobDirs, SignedResult) {
        self.run_with(script, |_| {})
    }
    fn run_with(&self, script: &str, m: impl FnOnce(&mut Job)) -> (JobDirs, SignedResult) {
        let d = job_dirs(&self.base);
        std::fs::write(d.input.join("a"), b"untrusted input\n").unwrap();
        let mut j = native_job(&self.image, &sh(script), &d);
        m(&mut j);
        let s = self
            .runner
            .run(j)
            .expect("the runner returns a signed result");
        verify(&s, &self.runner.verifying_key()).expect("it verifies");
        (d, s)
    }
    /// Run `script` (which writes a report to /out/r) and return the report.
    fn report(&self, script: &str) -> (String, JobResult) {
        self.report_with(script, |_| {})
    }
    fn report_with(&self, script: &str, m: impl FnOnce(&mut Job)) -> (String, JobResult) {
        let (d, s) = self.run_with(script, m);
        let r = std::fs::read_to_string(d.output.join("r")).unwrap_or_default();
        (r, s.result)
    }
}

// ==== part one: every heimdall-plain container test, on HeimdallRunner ====

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn run_copies_in_a_to_out_a_with_the_right_sha256_and_verify_and_check_outputs_pass() {
    let f = fx("copy");
    let d = job_dirs(&f.base);
    std::fs::write(d.input.join("a"), b"hello heimdall").unwrap();
    let job = native_job(&f.image, &["/bin/cp", "/in/a", "/out/a"], &d);
    let id = job.id;
    let s = f.runner.run(job).unwrap();
    let r = verify(&s, &f.runner.verifying_key()).unwrap();
    assert_eq!(r.job_id, id);
    assert!(r.verdict.is_accepted(), "{:?}", r.verdict);
    assert_eq!(r.outputs, vec![Output::of("a", b"hello heimdall")]);
    assert_eq!(r.exit, ExitInfo::exited(0));
    check_outputs(r, &d.output).unwrap();
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_tampered_signature_a_different_key_or_an_altered_output_each_fail_with_a_named_error() {
    let f = fx("sigs");
    let (d, s) = f.run("cp /in/a /out/a");
    let mut t = s.clone();
    t.sig.0[0] ^= 1;
    assert_eq!(
        verify(&t, &f.runner.verifying_key()),
        Err(VerifyError::BadSignature)
    );
    let other = BootKey::generate();
    assert!(matches!(
        verify(&s, &other.verifying_key()),
        Err(VerifyError::WrongKey { .. })
    ));
    std::fs::write(d.output.join("a"), b"altered").unwrap();
    assert!(matches!(
        check_outputs(&s.result, &d.output),
        Err(VerifyError::Output(OutputError::LengthMismatch { .. }))
    ));
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn writing_into_the_input_fails_and_the_input_is_unchanged() {
    let f = fx("roin");
    let (d, s) = f.run("echo pwned > /in/a && echo pwned > /in/new");
    assert_eq!(s.result.verdict.code(), Some(codes::EXIT_NONZERO));
    assert_eq!(
        std::fs::read(d.input.join("a")).unwrap(),
        b"untrusted input\n"
    );
    assert!(!d.input.join("new").exists());
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn with_no_network_a_job_cannot_open_a_tcp_connection() {
    let f = fx("nonet");
    let l = TcpListener::bind("0.0.0.0:0").unwrap();
    l.set_nonblocking(true).unwrap();
    let port = l.local_addr().unwrap().port();
    let mut targets = vec!["127.0.0.1".to_string(), "1.1.1.1".to_string()];
    if let Ok(o) = std::process::Command::new("hostname").arg("-i").output() {
        targets.extend(
            String::from_utf8_lossy(&o.stdout)
                .split_whitespace()
                .map(String::from),
        );
    }
    let mut script = String::new();
    for t in &targets {
        script.push_str(&format!(
            "if echo x | nc -w 2 {t} {port}; then echo CONNECTED {t} >> /out/r; fi; "
        ));
    }
    script.push_str("cat /proc/net/dev >> /out/dev; true");
    let (d, s) = f.run(&script);
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    assert!(!d.output.join("r").exists(), "a connection got out");
    assert!(l.accept().is_err(), "the host listener saw a connection");
    let dev = std::fs::read_to_string(d.output.join("dev")).unwrap();
    let ifaces: Vec<&str> = dev
        .lines()
        .skip(2)
        .filter_map(|l| l.split(':').next())
        .map(str::trim)
        .collect();
    assert_eq!(ifaces, vec!["lo"], "only a loopback in the job's netns");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_memory_limit_kills_the_job_refused_with_a_reason() {
    let f = fx("mem");
    let (_d, s) = f.run_with(
        "x=$(head -c 200000000 /dev/zero | tr '\\0' a); echo survived ${#x} > /out/r",
        |j| j.policy.limits.memory_bytes = 32 << 20,
    );
    let r = &s.result;
    assert_eq!(
        r.verdict.code(),
        Some(codes::LIMIT_MEMORY),
        "{:?} {:?}",
        r.verdict,
        r.exit
    );
    assert_eq!(r.exit.killed, Some(KillReason::Memory));
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_wall_limit_kills_the_job_refused_with_a_reason_and_the_host_stays_fine() {
    let f = fx("wall");
    let t0 = Instant::now();
    let (_d, s) = f.run_with("sleep 600", |j| {
        j.policy.limits.wall = Duration::from_secs(2)
    });
    let took = t0.elapsed();
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_WALL));
    assert_eq!(s.result.exit.killed, Some(KillReason::Wall));
    assert!(took < Duration::from_secs(20), "took {took:?}");
    let (_d, s) = f.run("true");
    assert!(s.result.verdict.is_accepted());
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_wall_limit_kills_every_process_of_the_job() {
    let f = fx("wallall");
    let (_d, s) = f.run_with("(sleep 600 &) ; (sleep 600 &) ; sleep 600", |j| {
        j.policy.limits.wall = Duration::from_secs(2)
    });
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_WALL));
    let ps = std::process::Command::new("ps")
        .args(["-eo", "args"])
        .output();
    if let Ok(o) = ps {
        let text = String::from_utf8_lossy(&o.stdout);
        assert!(!text.lines().any(|l| l.trim() == "sleep 600"), "{text}");
    }
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn an_extra_file_in_output_dir_is_refused_out_1() {
    let f = fx("extra");
    let (_d, s) = f.run("cp /in/a /out/a; mkdir /out/sub; echo x > /out/sub/hidden");
    assert_eq!(s.result.verdict.code(), Some("heimdall-out-1"));
    let (_d, s) = f.run("ln -s /etc/marker /out/link");
    assert_eq!(s.result.verdict.code(), Some("heimdall-out-1"));
    let (_d, s) = f.run_with("for i in 1 2 3; do echo $i > /out/f$i; done", |j| {
        j.policy.limits.max_output_files = 2
    });
    assert_eq!(s.result.verdict.code(), Some("heimdall-out-1"));
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn features_report_default_seccomp_landlock_and_the_broker_in_full_mode() {
    // heimdall-plain's features test, with Heimdall's truthful answers.
    let f = fx("features");
    let ft = f.runner.features();
    assert_eq!(ft.runner, "heimdall");
    assert_eq!(ft.seccomp, Protection::Default);
    assert!(ft.landlock);
    assert!(
        ft.broker,
        "kernel 5.9+ with USER_NOTIF and ADDFD: the broker in full mode"
    );
    assert!(ft.image_verified_each_start);
    let (_d, s) = f.run("true");
    assert_eq!(s.result.runner, ft);
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_root_is_read_only_and_tmp_is_writable() {
    let f = fx("rofs");
    let (r, _) = f.report(
        "touch /bin/x 2>/dev/null && echo ROOT-WRITABLE >> /out/r; \
         echo t > /tmp/t && echo tmp-ok >> /out/r; cat /etc/marker >> /out/r",
    );
    assert!(!r.contains("ROOT-WRITABLE"), "{r}");
    assert!(r.contains("tmp-ok"), "{r}");
    assert!(r.contains("from-the-image"), "{r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn every_capability_is_dropped_no_new_privs_and_seccomp_filter_are_on() {
    let f = fx("caps");
    let (r, _) = f.report(
        "grep -E '^(Cap(Inh|Prm|Eff|Bnd|Amb)|NoNewPrivs|Seccomp):' /proc/self/status > /out/r",
    );
    for cap in ["CapInh", "CapPrm", "CapEff", "CapBnd", "CapAmb"] {
        let line = r
            .lines()
            .find(|l| l.starts_with(cap))
            .unwrap_or_else(|| panic!("{r}"));
        assert!(line.ends_with("0000000000000000"), "{line}");
    }
    assert!(
        r.lines()
            .any(|l| l.split_whitespace().collect::<Vec<_>>() == ["NoNewPrivs:", "1"]),
        "{r}"
    );
    assert!(
        r.lines()
            .any(|l| l.split_whitespace().collect::<Vec<_>>() == ["Seccomp:", "2"]),
        "{r}"
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_default_seccomp_profile_blocks_new_namespaces_and_mount() {
    let f = fx("seccomp");
    let (r, _) = f.report(
        "unshare -U true 2>/dev/null && echo UNSHARE >> /out/r; \
         unshare -n true 2>/dev/null && echo NETNS >> /out/r; \
         mount -t tmpfs none /tmp 2>/dev/null && echo MOUNT >> /out/r; echo done >> /out/r",
    );
    assert_eq!(r.trim(), "done", "{r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_job_is_pid_1_in_its_own_pid_namespace_and_sees_no_host_process() {
    let f = fx("pidns");
    let (r, _) = f.report("echo $$ > /out/r; ls /proc | grep -c '^[0-9]' >> /out/r");
    let mut l = r.lines();
    assert_eq!(l.next(), Some("1"), "{r}");
    let n: usize = l.next().unwrap().trim().parse().unwrap();
    assert!(n <= 4, "{n} processes visible");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn container_root_is_one_mapped_id_and_the_hostname_is_its_own() {
    let f = fx("userns");
    let (r, _) = f.report("cat /proc/self/uid_map > /out/r; hostname >> /out/r; id -u >> /out/r");
    let lines: Vec<&str> = r.lines().collect();
    let map: Vec<&str> = lines[0].split_whitespace().collect();
    assert_eq!(map[0], "0");
    assert_eq!(map[2], "1", "exactly one id mapped: {r}");
    assert_eq!(lines[1], "heimdall-job");
    assert_eq!(lines[2], "0");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_pids_limit_stops_a_fork_bomb_and_the_host_stays_fine() {
    let f = fx("pids");
    let (_d, s) = f.run_with(
        "i=0; while [ $i -lt 200 ]; do sleep 30 & i=$((i+1)); done; wait",
        |j| {
            j.policy.limits.pids = 16;
            j.policy.limits.wall = Duration::from_secs(10);
        },
    );
    assert!(!s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    let (_d, s) = f.run("true");
    assert!(s.result.verdict.is_accepted());
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn every_job_gets_a_fresh_root_and_tmp() {
    let f = fx("fresh");
    let (_d, s) = f.run("echo leak > /tmp/leak");
    assert!(s.result.verdict.is_accepted());
    let (r, _) = f.report("test -e /tmp/leak && echo LEAKED > /out/r; echo ok >> /out/r");
    assert_eq!(r.trim(), "ok");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_image_env_is_the_job_env_and_nothing_of_the_host_leaks() {
    std::env::set_var("HEIMDALL_HOST_SECRET", "s3cret");
    let f = fx("env");
    let (r, _) = f.report("env > /out/r");
    assert!(r.contains("HEIMDALL_TEST_ENV=from-image"), "{r}");
    assert!(!r.contains("s3cret"), "{r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn proc_kcore_is_masked_and_proc_sys_is_read_only() {
    let f = fx("proc");
    let (r, _) = f.report(
        "head -c 1 /proc/kcore 2>/dev/null | grep -q . && echo KCORE >> /out/r; \
         echo 1 > /proc/sys/kernel/printk 2>/dev/null && echo SYSRW >> /out/r; echo ok >> /out/r",
    );
    assert_eq!(r.trim(), "ok", "{r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_nonzero_exit_is_refused_and_its_outputs_are_listed() {
    let f = fx("exit");
    let (_d, s) = f.run("echo report > /out/report; exit 7");
    assert_eq!(s.result.verdict.code(), Some(codes::EXIT_NONZERO));
    assert_eq!(s.result.exit.code, Some(7));
    assert_eq!(s.result.outputs.len(), 1);
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn extra_read_and_write_paths_are_honoured_and_nothing_else_of_the_host_is_visible() {
    let f = fx("paths");
    let ro = f.base.join("ro-data");
    let rw = f.base.join("rw-data");
    std::fs::create_dir_all(&ro).unwrap();
    std::fs::create_dir_all(&rw).unwrap();
    std::fs::write(ro.join("x"), b"ro").unwrap();
    let script = format!(
        "cat {ro}/x > /out/r; echo y > {ro}/y 2>/dev/null && echo RO-WRITABLE >> /out/r; \
         echo w > {rw}/w; test -e {layout} && echo LAYOUT-VISIBLE >> /out/r; true",
        ro = ro.display(),
        rw = rw.display(),
        layout = f.base.join("layout").display()
    );
    let (d, s) = f.run_with(&script, |j| {
        j.policy.read = vec![ro.clone()];
        j.policy.write = vec![rw.clone()];
    });
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    let r = std::fs::read_to_string(d.output.join("r")).unwrap();
    assert_eq!(r.trim(), "ro", "{r}");
    assert_eq!(std::fs::read(rw.join("w")).unwrap(), b"w\n");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn jobs_run_in_parallel_through_a_shared_trait_object() {
    let f = fx("par");
    let image = f.image.clone();
    let base = f.base.clone();
    let r: Arc<dyn JobRunner> = Arc::new(f.runner);
    let hs: Vec<_> = (0..8)
        .map(|i| {
            let r = Arc::clone(&r);
            let d = job_dirs(&base);
            let j = native_job(&image, &sh(&format!("echo {i} > /out/n")), &d);
            std::thread::spawn(move || (r.run(j), d))
        })
        .collect();
    for (i, h) in hs.into_iter().enumerate() {
        let (s, d) = h.join().unwrap();
        let s = s.unwrap();
        let res = verify(&s, &r.verifying_key()).unwrap();
        assert!(res.verdict.is_accepted(), "{:?}", res.verdict);
        check_outputs(res, &d.output).unwrap();
        let mut n = String::new();
        std::fs::File::open(d.output.join("n"))
            .unwrap()
            .read_to_string(&mut n)
            .unwrap();
        assert_eq!(n.trim(), i.to_string());
    }
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_signing_key_is_not_reachable_from_the_job() {
    let f = fx("key");
    let pk = f.runner.pinned().public_key.to_hex();
    let (r, _) = f.report("cat /proc/*/environ /proc/*/cmdline 2>/dev/null > /out/r; true");
    assert!(!r.contains(&pk));
}

// ==== part two: what heimdall-container adds ====

/// A host dir with a secret file, given to the job as a mount Landlock does
/// not list (`writable` decides the mount's own mode). The broker is off:
/// these prove what Landlock ALONE refuses.
fn with_unlisted(name: &str, writable: bool, landlock: LandlockMode) -> Fx {
    fx_with(name, |c, base| {
        c.broker = BrokerSetting::Off;
        let src = base.join("unlisted");
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("secret"), b"host-secret").unwrap();
        if landlock == LandlockMode::Off {
            c.strict = false;
            c.landlock = LandlockMode::Off;
        }
        c.unlisted_mounts = vec![UnlistedMount {
            source: src,
            destination: "/unlisted".into(),
            writable,
        }];
    })
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn landlock_refuses_reading_outside_in_even_through_a_readable_mount() {
    let script = "cat /unlisted/secret >> /out/r 2>/dev/null; ls /unlisted >> /out/r 2>/dev/null; \
                  ls /dev >> /out/r 2>/dev/null; cat /in/a >> /out/r";
    let f = with_unlisted("llread", false, LandlockMode::Auto);
    let (r, res) = f.report(script);
    assert!(res.runner.landlock);
    assert!(!r.contains("host-secret"), "{r}");
    assert!(!r.contains("secret"), "the dir listing got out: {r}");
    assert!(!r.contains("null"), "/dev was listed: {r}");
    assert!(r.contains("untrusted input"), "/in must stay readable: {r}");
    // The control: the same job without Landlock reads it (so Landlock refused it).
    let f = with_unlisted("llread-off", false, LandlockMode::Off);
    let (r, res) = f.report(script);
    assert!(!res.runner.landlock);
    assert!(r.contains("host-secret"), "{r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn landlock_refuses_writing_outside_out_even_into_a_writable_mount() {
    let script = "echo x > /unlisted/new 2>/dev/null && echo WROTE-UNLISTED >> /out/r; \
                  echo x > /dev/shm/new 2>/dev/null && echo WROTE-SHM >> /out/r; \
                  echo x > /dev/new 2>/dev/null && echo WROTE-DEV >> /out/r; \
                  echo x > /out/ok && echo out-ok >> /out/r";
    let f = with_unlisted("llwrite", true, LandlockMode::Auto);
    let (r, _) = f.report(script);
    assert_eq!(r.trim(), "out-ok", "{r}");
    assert!(!f.base.join("unlisted/new").exists());
    let f = with_unlisted("llwrite-off", true, LandlockMode::Off);
    let (r, _) = f.report(script);
    assert!(
        r.contains("WROTE-UNLISTED") && r.contains("WROTE-SHM"),
        "{r}"
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn out_is_write_only_the_job_cannot_read_back_or_list_it() {
    let f = fx("wo");
    let (d, s) = f.run(
        "echo data > /out/a; cat /out/a > /out/b 2>/dev/null; ls /out > /out/c 2>/dev/null; true",
    );
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    assert_eq!(std::fs::read(d.output.join("a")).unwrap(), b"data\n");
    assert_eq!(std::fs::read(d.output.join("b")).unwrap_or_default(), b"");
    assert_eq!(std::fs::read(d.output.join("c")).unwrap_or_default(), b"");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn nothing_written_into_tmp_can_be_executed() {
    let f = fx("noexec");
    let (r, _) = f.report(
        "cp /bin/busybox /tmp/busybox && chmod 755 /tmp/busybox && echo copied >> /out/r; \
         /tmp/busybox true 2>/dev/null && echo RAN-FROM-TMP >> /out/r; echo done >> /out/r",
    );
    assert!(r.contains("copied"), "{r}");
    assert!(!r.contains("RAN-FROM-TMP"), "{r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_changed_image_byte_is_refused_at_the_next_start() {
    let f = fx("image");
    let (_d, s) = f.run("true");
    assert!(s.result.verdict.is_accepted());
    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 / 2] ^= 0x01;
    std::fs::write(&p, b).unwrap();
    let d = job_dirs(&f.base);
    let j = native_job(&f.image, &sh("true"), &d);
    assert!(matches!(f.runner.run(j), Err(RunError::Image { .. })));
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn features_are_truthful_when_landlock_is_switched_off() {
    let f = fx_with("off", |c, _| {
        c.strict = false;
        c.landlock = LandlockMode::Off;
        c.broker = BrokerSetting::Off;
    });
    assert!(!f.runner.features().landlock);
    let (r, res) = f.report("ls /dev > /out/r");
    assert!(!res.runner.landlock);
    assert_eq!(res.runner, f.runner.features());
    assert!(r.contains("null"), "without Landlock /dev lists: {r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_runner_reports_the_kernel_features_it_got() {
    let f = fx("kernel");
    let k = f.runner.kernel();
    assert!(
        k.seccomp_filter && k.cgroup_v2 && k.user_namespaces,
        "{k:?}"
    );
    assert!(k.landlock_abi.unwrap_or(0) >= 1, "{k:?}");
    for c in ["memory", "pids", "cpu"] {
        assert!(k.has_controller(c), "{c}: {k:?}");
    }
    eprintln!("kernel report: {k:?}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_killed_syscall_kills_the_calling_process_with_sigsys() {
    let f = fx("kill");
    let (r, _) = f.report("unshare -U true 2>/dev/null; echo $? > /out/r");
    // 128 + SIGSYS (31): the profile's KILL_PROCESS, not an EPERM.
    assert_eq!(r.trim(), "159", "{r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn rlimits_inside_are_nofile_nproc_fsize_and_core_zero() {
    let f = fx("rlimits");
    let (r, _) = f.report_with("cat /proc/self/limits > /out/r", |j| {
        j.policy.limits.pids = 40;
        j.policy.limits.max_output_file_bytes = 3 << 20;
    });
    let row = |name: &str| -> Vec<String> {
        let l = r
            .lines()
            .find(|l| l.starts_with(name))
            .unwrap_or_else(|| panic!("{r}"));
        l[name.len()..]
            .split_whitespace()
            .map(String::from)
            .collect()
    };
    assert_eq!(row("Max processes")[..2], ["41", "41"]);
    assert_eq!(row("Max open files")[..2], ["1024", "1024"]);
    assert_eq!(row("Max core file size")[..2], ["0", "0"]);
    let fsize = (3u64 << 20).to_string();
    assert_eq!(row("Max file size")[..2], [fsize.clone(), fsize]);
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_file_over_the_fsize_limit_is_cut_and_the_job_is_refused() {
    let f = fx("fsize");
    let (d, s) = f.run_with("head -c 4000000 /dev/zero > /out/big", |j| {
        j.policy.limits.max_output_file_bytes = 1 << 20;
    });
    assert!(!s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    let n = std::fs::metadata(d.output.join("big"))
        .map(|m| m.len())
        .unwrap_or(0);
    assert!(n <= 1 << 20, "{n}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_recursive_fork_bomb_is_stopped_and_the_host_stays_fine() {
    let f = fx("bomb");
    let t0 = Instant::now();
    let (r, res) = f.report_with(
        "b() { b | b & }; b; sleep 2; ls /proc | grep -c '^[0-9]' > /out/r",
        |j| {
            j.policy.limits.pids = 32;
            j.policy.limits.wall = Duration::from_secs(8);
        },
    );
    assert!(t0.elapsed() < Duration::from_secs(45));
    // Never more processes than the limit, whatever the verdict.
    if let Ok(n) = r.trim().parse::<u32>() {
        assert!(n <= 32, "{n} processes: {:?}", res.verdict);
    }
    let (_d, s) = f.run("true");
    assert!(s.result.verdict.is_accepted());
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn memory_swap_max_is_zero_while_the_job_runs() {
    let f = fx("swap");
    let d = job_dirs(&f.base);
    let j = native_job(&f.image, &sh("sleep 3"), &d);
    let cg = f
        .runner
        .config()
        .cgroup_root
        .join(&f.runner.config().cgroup_parent)
        .join(format!("job-{}", j.id));
    let seen = std::thread::spawn(move || {
        // Wait for the job's cgroup, give the shim time to start the job,
        // then read what the running job has.
        for _ in 0..1000 {
            if cg.join("memory.swap.max").exists() {
                std::thread::sleep(Duration::from_millis(1000));
                return std::fs::read_to_string(cg.join("memory.swap.max"))
                    .ok()
                    .map(|s| s.trim().to_string());
            }
            std::thread::sleep(Duration::from_millis(5));
        }
        None
    });
    let s = f.runner.run(j).unwrap();
    assert!(s.result.verdict.is_accepted());
    assert_eq!(seen.join().unwrap().as_deref(), Some("0"));
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn landlock_refuses_tcp_connect_before_routing_abi_4() {
    // The broker off: this proves Landlock alone (the broker refuses the
    // socket before it; see the broker tests).
    let f = fx_with("llnet", |c, _| c.broker = BrokerSetting::Off);
    assert!(f.runner.kernel().landlock_net(), "{:?}", f.runner.kernel());
    // In the empty netns the loopback is down, so a plain container answers
    // "Network is unreachable"; Landlock ABI 4 refuses the connect first.
    let script = "nc -w 1 127.0.0.1 1 2>> /out/r; true";
    let (r, _) = f.report(script);
    assert!(r.contains("Permission denied"), "{r}");
    let f = fx_with("llnet-off", |c, _| {
        c.strict = false;
        c.landlock = LandlockMode::Off;
        c.broker = BrokerSetting::Off;
    });
    let (r, _) = f.report(script);
    assert!(!r.contains("Permission denied"), "{r}");
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn many_jobs_in_parallel_each_get_their_own_container_and_landlock() {
    let f = fx("par16");
    let image = f.image.clone();
    let base = f.base.clone();
    let r: Arc<dyn JobRunner> = Arc::new(f.runner);
    let hs: Vec<_> = (0..16)
        .map(|i| {
            let r = Arc::clone(&r);
            let d = job_dirs(&base);
            let j = native_job(
                &image,
                &sh(&format!(
                    "echo {i} > /tmp/me; cat /tmp/me > /out/n; hostname >> /out/n"
                )),
                &d,
            );
            std::thread::spawn(move || (r.run(j), d))
        })
        .collect();
    for (i, h) in hs.into_iter().enumerate() {
        let (s, d) = h.join().unwrap();
        let s = s.unwrap();
        assert!(s.result.runner.landlock);
        assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
        let n = std::fs::read_to_string(d.output.join("n")).unwrap();
        assert_eq!(n, format!("{i}\nheimdall-job\n"));
    }
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_runner_process_its_key_and_the_work_dir_are_not_visible_to_the_job() {
    let f = fx("keyfs");
    let pk = f.runner.pinned().public_key.to_hex();
    let work = f.runner.config().work_dir.display().to_string();
    let (r, _) = f.report(&format!(
        "test -e {work} && echo WORK-VIS''IBLE >> /out/r; \
         ls /proc | grep -c '^[0-9]' >> /out/r; \
         cat /proc/1/cmdline >> /out/r; echo >> /out/r; true"
    ));
    assert!(!r.contains("WORK-VISIBLE"), "{r}");
    assert!(!r.contains(&pk));
    assert!(!r.contains("heimdall-container-shim"), "{r}");
}

// ==== part three: GENERATED seccomp profiles (lane genprof) ====
//
// The profile of a busybox applet is generated the real way: heimdall-probe
// learns the applet's calls (USER_NOTIF, log and allow) outside the
// container, `heimdall profile gen`'s minimiser makes the profile, and its OCI
// seccomp JSON is pinned by sha256 in the job's policy.

/// Probe static busybox running each of `runs` (applet + args, `{d}` = a
/// scratch dir holding `a`), generate the profile and write its OCI JSON.
/// Returns the job policy's reference to it.
fn generated_busybox(base: &std::path::Path, name: &str, runs: &[&[&str]]) -> SeccompProfileRef {
    use heimdall_probe::{generate, learn, GenOptions, Observations, ProbeRun};
    let bb = busybox().expect("a static busybox");
    let arch = heimdall_seccomp::Arch::native().unwrap();
    let mut obs = Observations::new(name, arch, heimdall_seccomp::Libc::Glibc);
    for (i, args) in runs.iter().enumerate() {
        let d = base.join(format!("probe-{name}-{i}"));
        std::fs::create_dir_all(&d).unwrap();
        std::fs::write(d.join("a"), b"probe input\n").unwrap();
        let mut run = ProbeRun::new(&format!("{name}-{i}"), &bb);
        run.args = args
            .iter()
            .map(|a| a.replace("{d}", &d.display().to_string()).into())
            .collect();
        run.env = vec![("PATH".into(), "/bin".into())];
        run.cwd = Some(d.clone());
        let out = learn(&run, &mut obs).expect("the probe runs");
        assert!(out.exit.success(), "probe run {args:?}: {:?}", out.exit);
    }
    let g = generate(&obs, GenOptions::default());
    let oci = heimdall_probe::codegen::to_oci_json(&g);
    let path = base.join(name).join("x86_64-glibc.oci.json");
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(&path, &oci).unwrap();
    SeccompProfileRef {
        source: ProfileSource::OciFile(path),
        sha256: Sha256Digest::of(oci.as_bytes()).to_prefixed(),
        fallback_to_default: false,
    }
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_job_runs_under_its_generated_profile_and_the_signed_result_names_the_digest() {
    let f = fx("gen-run");
    let pr = generated_busybox(&f.base, "bb-cp", &[&["cp", "{d}/a", "{d}/b"]]);
    let d = job_dirs(&f.base);
    std::fs::write(d.input.join("a"), b"hello generated").unwrap();
    let mut job = native_job(&f.image, &["/bin/cp", "/in/a", "/out/a"], &d);
    job.policy.seccomp = Some(pr.clone());
    let s = f.runner.run(job).unwrap();
    let r = verify(&s, &f.runner.verifying_key()).unwrap();
    assert!(r.verdict.is_accepted(), "{:?} {:?}", r.verdict, r.exit);
    assert_eq!(r.outputs, vec![Output::of("a", b"hello generated")]);
    assert_eq!(
        r.runner.seccomp,
        Protection::Generated {
            profile_digest: pr.sha256.clone()
        }
    );
    assert!(r.runner.landlock);
    check_outputs(r, &d.output).unwrap();
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_call_outside_the_generated_profile_kills_the_job_with_sigsys_and_a_signed_seccomp_refusal() {
    let f = fx("gen-kill");
    // The profile of `true`: no open, no read, no write.
    let pr = generated_busybox(&f.base, "bb-true", &[&["true"]]);
    let d = job_dirs(&f.base);
    std::fs::write(d.input.join("a"), b"x").unwrap();
    let mut job = native_job(&f.image, &["/bin/cp", "/in/a", "/out/a"], &d);
    job.policy.seccomp = Some(pr.clone());
    let s = f.runner.run(job).unwrap();
    let r = verify(&s, &f.runner.verifying_key()).unwrap();
    assert_eq!(
        r.verdict.code(),
        Some(codes::SECCOMP_KILL),
        "{:?}",
        r.verdict
    );
    assert_eq!(r.exit.signal, Some(libc::SIGSYS));
    assert_eq!(
        r.runner.seccomp,
        Protection::Generated {
            profile_digest: pr.sha256
        }
    );
    // The same job under `true`'s profile but running `true` is fine.
    let d = job_dirs(&f.base);
    let mut job = native_job(&f.image, &["/bin/true"], &d);
    job.policy.seccomp = Some(generated_busybox(&f.base, "bb-true2", &[&["true"]]));
    let r = f.runner.run(job).unwrap().result;
    assert!(r.verdict.is_accepted(), "{:?} {:?}", r.verdict, r.exit);
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_tampered_generated_profile_refuses_the_job_and_nothing_runs() {
    let f = fx("gen-tamper");
    let pr = generated_busybox(&f.base, "bb-cp2", &[&["cp", "{d}/a", "{d}/b"]]);
    let ProfileSource::OciFile(p) = &pr.source else {
        unreachable!()
    };
    // The attacker's widening: one more allowed call after the pin was taken.
    let mut j: serde_json::Value =
        serde_json::from_str(&std::fs::read_to_string(p).unwrap()).unwrap();
    j["syscalls"]
        .as_array_mut()
        .unwrap()
        .push(serde_json::json!({"names": ["ptrace"], "action": "SCMP_ACT_ALLOW"}));
    std::fs::write(p, serde_json::to_string_pretty(&j).unwrap()).unwrap();
    let d = job_dirs(&f.base);
    let mut job = native_job(&f.image, &["/bin/sh", "-c", "echo ran > /out/r"], &d);
    job.policy.seccomp = Some(pr);
    match f.runner.run(job) {
        Err(RunError::InvalidJob { reason }) => {
            assert!(reason.contains("digest mismatch"), "{reason}")
        }
        other => panic!("expected a refusal: {other:?}"),
    }
    assert!(!d.output.join("r").exists());
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_runner_configured_with_a_generated_profile_says_so_in_features_and_applies_it_to_every_job() {
    let base = scratch("hc-gen-cfg");
    let pr = generated_busybox(&base, "bb-cfg", &[&["cp", "{d}/a", "{d}/b"]]);
    let f = fx_with("gen-cfg", |c, _| c.seccomp = Some(pr.clone()));
    let want = Protection::Generated {
        profile_digest: pr.sha256.clone(),
    };
    assert_eq!(f.runner.features().seccomp, want);
    let d = job_dirs(&f.base);
    std::fs::write(d.input.join("a"), b"cfg").unwrap();
    let s = f
        .runner
        .run(native_job(&f.image, &["/bin/cp", "/in/a", "/out/a"], &d))
        .unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    assert_eq!(s.result.runner, f.runner.features());
    // A shell is outside cp's profile: killed, not run unfiltered.
    let (_d, s) = f.run("echo hi > /out/r");
    assert_eq!(s.result.verdict.code(), Some(codes::SECCOMP_KILL));
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_job_that_survives_failed_forks_and_exits_zero_is_refused_heimdall_limit_pids() {
    let f = fx("pids-zero");
    let (_d, s) = f.run_with(
        "i=0; while [ $i -lt 40 ]; do (sleep 5 &) 2>/dev/null; i=$((i+1)); done; exit 0",
        |j| {
            j.policy.limits.pids = 8;
            j.policy.limits.wall = Duration::from_secs(15);
        },
    );
    assert_eq!(
        s.result.verdict.code(),
        Some(codes::LIMIT_PIDS),
        "{:?}",
        s.result
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn an_out_1_refusal_leaves_output_dir_empty_and_check_outputs_passes() {
    let f = fx("out1-empty");
    let (d, s) =
        f.run("cp /in/a /out/a; mkdir /out/sub; echo x > /out/sub/hidden; ln -s /in/a /out/l");
    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);
    check_outputs(&s.result, &d.output).unwrap();
}

// ==== part three: the syscall broker (spec §4) and Landlock on its own ====

fn broker_events(r: &JobResult) -> String {
    r.events
        .iter()
        .map(|e| format!("{} {:?} {}", e.call, e.decision, e.args.join(" ")))
        .collect::<Vec<_>>()
        .join("\n")
}

fn has_event(r: &JobResult, call: &str, d: Decision, needle: &str) -> bool {
    r.events
        .iter()
        .any(|e| e.call == call && e.decision == d && e.args.iter().any(|a| a.contains(needle)))
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_job_reads_in_and_writes_out_through_the_broker_and_every_open_is_a_signed_event() {
    let f = fx("broker-cp");
    let (d, s) = f.run("cp /in/a /out/a");
    let r = verify(&s, &f.runner.verifying_key()).unwrap();
    assert!(r.verdict.is_accepted(), "{:?}", r.verdict);
    assert!(r.runner.broker, "full mode on this kernel");
    assert_eq!(
        std::fs::read(d.output.join("a")).unwrap(),
        b"untrusted input\n"
    );
    assert_eq!(r.events[0].call, "broker");
    assert_eq!(r.events[0].args, vec!["mode=full".to_string()]);
    let ev = broker_events(r);
    assert!(
        has_event(r, "openat", Decision::Allowed, "path=/in/a"),
        "{ev}"
    );
    assert!(
        has_event(r, "openat", Decision::Allowed, "path=/out/a"),
        "{ev}"
    );
    assert!(
        has_event(r, "execve", Decision::Allowed, "path=/bin/sh"),
        "{ev}"
    );
    // The events are inside the signature.
    let mut forged = s.clone();
    forged.result.events.pop();
    assert!(verify(&forged, &f.runner.verifying_key()).is_err());
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_broker_alone_refuses_an_unlisted_mount_with_landlock_off_and_logs_it() {
    let f = fx_with("broker-unlisted", |c, base| {
        let src = base.join("unlisted");
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(src.join("secret"), b"host-secret").unwrap();
        c.strict = false;
        c.landlock = LandlockMode::Off;
        c.unlisted_mounts = vec![UnlistedMount {
            source: src,
            destination: "/unlisted".into(),
            writable: true,
        }];
    });
    let (r, res) = f.report(
        "cat /unlisted/secret >> /out/r 2>/dev/null; echo x > /unlisted/new 2>/dev/null && echo WROTE >> /out/r; \
         cat /in/a >> /out/r",
    );
    assert!(!res.runner.landlock && res.runner.broker);
    assert!(!r.contains("host-secret") && !r.contains("WROTE"), "{r}");
    assert!(r.contains("untrusted input"), "{r}");
    assert!(!f.base.join("unlisted/new").exists());
    let ev = broker_events(&res);
    assert!(
        has_event(&res, "openat", Decision::Refused, "path=/unlisted/secret"),
        "{ev}"
    );
    assert!(
        has_event(&res, "openat", Decision::Refused, "crosses a mount"),
        "{ev}"
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn path_tricks_are_refused_by_the_broker_dotdot_symlink_to_out_and_proc_self_fd() {
    let f = fx("broker-tricks");
    let d = job_dirs(&f.base);
    std::fs::write(d.input.join("a"), b"untrusted input\n").unwrap();
    std::os::unix::fs::symlink("/out/a", d.input.join("to-out")).unwrap();
    std::os::unix::fs::symlink("../etc/marker", d.input.join("up")).unwrap();
    let script = "echo secret-out > /out/a; \
                  cat /in/../out/a >> /out/r 2>/dev/null; \
                  cat /in/to-out >> /out/r 2>/dev/null; \
                  cat /in/up >> /out/r 2>/dev/null; \
                  exec 3>/out/x; cat /proc/self/fd/3 >> /out/r 2>/dev/null; \
                  cat /proc/self/root/out/a >> /out/r 2>/dev/null; \
                  cat /proc/self/status | grep -c ^Pid: >> /out/r; \
                  cd /in && cat a >> /out/r; cd /out && cat a >> /out/r 2>/dev/null; true";
    let j = native_job(&f.image, &sh(script), &d);
    let s = f.runner.run(j).unwrap();
    let res = &s.result;
    let r = std::fs::read_to_string(d.output.join("r")).unwrap_or_default();
    assert!(res.verdict.is_accepted(), "{:?}", res.verdict);
    assert!(!r.contains("secret-out"), "read back /out: {r}");
    assert!(
        !r.contains("from-the-image"),
        "a link climbing out of /in: {r}"
    );
    assert_eq!(
        r.lines().collect::<Vec<_>>(),
        vec!["1", "untrusted input"],
        "{r}"
    );
    let ev = broker_events(res);
    assert!(
        has_event(res, "openat", Decision::Refused, "`..` in path"),
        "{ev}"
    );
    assert!(
        has_event(res, "openat", Decision::Refused, "path=/in/to-out"),
        "{ev}"
    );
    assert!(
        has_event(res, "openat", Decision::Refused, "path=/proc/self/fd/3"),
        "{ev}"
    );
    assert!(
        has_event(res, "openat", Decision::Allowed, "resolved=/in/a"),
        "relative open from cwd /in: {ev}"
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn with_the_broker_on_a_job_cannot_open_any_socket_and_the_refusal_is_signed() {
    let f = fx("broker-net");
    let (r, res) = f.report("nc -w 1 127.0.0.1 1 2>> /out/r; true");
    assert!(r.contains("Permission denied"), "{r}");
    let ev = broker_events(&res);
    assert!(
        has_event(&res, "socket", Decision::Refused, "no network for this job"),
        "{ev}"
    );
}

/// The broker's selftest worker (static, musl), when the test script built it.
fn selftest() -> Option<Vec<u8>> {
    std::env::var_os("HEIMDALL_BROKER_SELFTEST").and_then(|p| std::fs::read(p).ok())
}

fn selftest_image(layout: &Layout) -> ImageRef {
    let bb = std::fs::read(busybox().expect("a static busybox")).unwrap();
    let st = selftest().expect("HEIMDALL_BROKER_SELFTEST: a static heimdall-broker-selftest");
    let mut entries = vec![
        Entry::Dir("bin"),
        Entry::Dir("tmp"),
        Entry::Dir("in"),
        Entry::Dir("out"),
        Entry::Dir("proc"),
        Entry::Dir("dev"),
        Entry::Dir("etc"),
        Entry::File("bin/busybox".into(), bb, 0o755),
        Entry::File("bin/selftest".into(), st, 0o755),
    ];
    for a in APPLETS {
        entries.push(Entry::Symlink(format!("bin/{a}"), "busybox".into()));
    }
    layout.image(&[(Layout::tar(&entries), true)], &["PATH=/bin"])
}

/// Runs `/bin/selftest seq <args>` in a protected job; stdout goes to
/// /out/r through the shell.
fn selftest_job(name: &str, args: &str, prep: impl FnOnce(&JobDirs)) -> (String, JobResult) {
    let base = scratch(&format!("hc-{name}"));
    let layout = Layout::new(&base.join("layout"));
    let image = selftest_image(&layout);
    let runner = HeimdallRunner::new(cfg(&base, &layout)).unwrap();
    let d = job_dirs(&base);
    prep(&d);
    let script = format!("printf BAD > /out/g; /bin/selftest seq {args} > /out/r");
    let mut j = native_job(&image, &sh(&script), &d);
    j.policy.limits.wall = Duration::from_secs(60);
    let s = runner.run(j).unwrap();
    let r = std::fs::read_to_string(d.output.join("r")).unwrap_or_default();
    (r, s.result)
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn in_a_container_a_racing_thread_flipping_the_path_never_gets_out_read_back() {
    if selftest().is_none() {
        eprintln!("skipped: HEIMDALL_BROKER_SELFTEST not set");
        return;
    }
    // /in/gg (GOOD) is allowed; /out/g (BAD, written first by the job) is
    // write-only: same length, flipped by a second thread while the broker
    // holds each call.
    let (r, res) = selftest_job("broker-race", "race /in/gg /out/g 3000", |d| {
        std::fs::write(d.input.join("gg"), b"GOOD").unwrap()
    });
    let lines: Vec<&str> = r.lines().collect();
    assert_eq!(lines.len(), 2, "{r} {}", broker_events(&res));
    assert!(
        lines[0].contains(" bad=0 ") && !lines[0].contains("good=0 "),
        "{r}"
    );
    assert!(
        !lines[0].contains("refused=0 "),
        "the race flipped both ways: {r}"
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn in_a_container_fd_exhaustion_openat2_resolve_and_o_path_behave() {
    if selftest().is_none() {
        eprintln!("skipped: HEIMDALL_BROKER_SELFTEST not set");
        return;
    }
    let (r, res) = selftest_job(
        "broker-adv",
        "fd-exhaust /in/g -- openat2-resolve /in/g 16 -- openat2-resolve /in/g 4 -- \
         dirfd /in g -- proc-fd /out/x -- open /out/x r -- socket tcp4 -- exec /tmp/x",
        |d| std::fs::write(d.input.join("g"), b"GOOD").unwrap(),
    );
    let lines: Vec<&str> = r.lines().collect();
    let ev = broker_events(&res);
    assert_eq!(lines.len(), 9, "{r} {ev}");
    assert!(
        lines[0].contains(&format!("errno={}", libc::EMFILE)) && lines[0].ends_with("again=true"),
        "{r}"
    );
    assert_eq!(
        lines[1],
        format!("err {}", libc::EACCES),
        "RESOLVE_IN_ROOT from the worker"
    );
    assert!(
        lines[2].ends_with("GOOD"),
        "RESOLVE_NO_SYMLINKS on a plain file: {r}"
    );
    assert!(lines[3].ends_with("GOOD"), "dirfd-relative open: {r}");
    assert_eq!(
        lines[4],
        format!("err {}", libc::EACCES),
        "/proc/self/fd reopen of /out"
    );
    assert_eq!(
        lines[5],
        format!("err {}", libc::EACCES),
        "read-back of /out"
    );
    assert_eq!(
        lines[6],
        format!("err {}", libc::EACCES),
        "a socket without a fetch peer"
    );
    // exec of a file that is not there: exec is allowed (and logged); the
    // kernel answers ENOENT.
    assert_eq!(lines[7], format!("err {}", libc::ENOENT), "{r}");
    assert!(
        has_event(&res, "execve", Decision::Allowed, "path=/tmp/x"),
        "{ev}"
    );
}

/// This kernel, with USER_NOTIF or ADDFD taken away (spec §10 fallbacks).
fn degraded(name: &str, user_notif: bool, addfd: bool) -> Fx {
    let base = scratch(&format!("hc-{name}"));
    let layout = Layout::new(&base.join("layout"));
    let image = busybox_image(&layout);
    let c = cfg(&base, &layout);
    let mut k = heimdall_container::kernel::KernelReport::probe(&c.cgroup_root, &c.cgroup_parent);
    k.user_notif = user_notif;
    k.notif_addfd = addfd;
    Fx {
        runner: HeimdallRunner::with_kernel(c, BootKey::generate(), k).unwrap(),
        base,
        image,
        layout,
    }
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn without_addfd_the_broker_checks_only_warns_and_features_say_no_broker() {
    let f = degraded("broker-checkonly", true, false);
    assert!(!f.runner.features().broker);
    let (r, res) = f.report(
        "cat /in/a > /out/r; cat /etc/marker >> /out/r; cat /out/r >> /out/s 2>/dev/null; true",
    );
    assert!(res.verdict.is_accepted(), "{:?}", res.verdict);
    assert!(!res.runner.broker);
    assert!(
        r.contains("untrusted input") && r.contains("from-the-image"),
        "{r}"
    );
    assert_eq!(res.events[0].args[0], "mode=check-only");
    assert!(
        res.events[0]
            .args
            .iter()
            .any(|a| a.contains("not TOCTOU-safe")),
        "{:?}",
        res.events[0]
    );
    assert!(
        has_event(
            &res,
            "openat",
            Decision::Refused,
            "read under a write-only prefix"
        ),
        "{}",
        broker_events(&res)
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn without_user_notif_every_brokered_call_fails_closed_and_the_result_says_so() {
    let f = degraded("broker-denyonly", false, false);
    assert!(!f.runner.features().broker);
    let (d, s) = f.run("cat /in/a > /out/r");
    let res = &s.result;
    assert!(!res.runner.broker);
    assert_eq!(
        res.verdict.code(),
        Some(codes::EXIT_NONZERO),
        "{:?}",
        res.verdict
    );
    assert!(
        !d.output.join("r").exists(),
        "the open of /out/r was refused too"
    );
    assert_eq!(res.events.len(), 1);
    assert_eq!(res.events[0].args[0], "mode=deny-only");
}

// ---- Landlock alone (broker off): /in read-only, /out write-only, /tmp no exec,
// each refused even where the mount allows it ----

fn landlock_only(name: &str, landlock: LandlockMode, unlisted: Vec<UnlistedMount>) -> Fx {
    fx_with(name, move |c, _| {
        c.broker = BrokerSetting::Off;
        if landlock == LandlockMode::Off {
            c.strict = false;
            c.landlock = LandlockMode::Off;
        }
        c.unlisted_mounts = unlisted;
    })
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn landlock_alone_refuses_reading_back_and_listing_out_which_the_mount_allows() {
    let script =
        "echo data > /out/a; cat /out/a > /out/b 2>/dev/null; ls /out > /out/c 2>/dev/null; true";
    let f = landlock_only("ll-wo", LandlockMode::Auto, vec![]);
    let (d, s) = f.run(script);
    assert!(s.result.runner.landlock && !s.result.runner.broker);
    assert_eq!(std::fs::read(d.output.join("b")).unwrap_or_default(), b"");
    assert_eq!(std::fs::read(d.output.join("c")).unwrap_or_default(), b"");
    // Control: the same job with Landlock off reads /out back and lists it.
    let f = landlock_only("ll-wo-off", LandlockMode::Off, vec![]);
    let (d, _) = f.run(script);
    assert_eq!(std::fs::read(d.output.join("b")).unwrap(), b"data\n");
    assert!(std::fs::read_to_string(d.output.join("c"))
        .unwrap()
        .contains('a'));
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn landlock_alone_refuses_writing_into_in_even_through_a_writable_in_mount() {
    let run = |name: &str, ll: LandlockMode| {
        let base = scratch(&format!("hc-{name}"));
        let layout = Layout::new(&base.join("layout"));
        let image = busybox_image(&layout);
        let d = job_dirs(&base);
        std::fs::write(d.input.join("a"), b"untrusted input\n").unwrap();
        // The job's own input dir again at /in, but writable.
        let mut c = cfg(&base, &layout);
        c.broker = BrokerSetting::Off;
        if ll == LandlockMode::Off {
            c.strict = false;
            c.landlock = LandlockMode::Off;
        }
        c.unlisted_mounts = vec![UnlistedMount {
            source: d.input.clone(),
            destination: "/in".into(),
            writable: true,
        }];
        let runner = HeimdallRunner::new(c).unwrap();
        let j = native_job(
            &image,
            &sh("echo pwned > /in/a; echo pwned > /in/new; true"),
            &d,
        );
        let s = runner.run(j).unwrap();
        (d, s)
    };
    let (d, s) = run("ll-rwin", LandlockMode::Auto);
    assert!(s.result.runner.landlock);
    assert_eq!(
        std::fs::read(d.input.join("a")).unwrap(),
        b"untrusted input\n"
    );
    assert!(!d.input.join("new").exists());
    let (d, _) = run("ll-rwin-off", LandlockMode::Off);
    assert_eq!(
        std::fs::read(d.input.join("a")).unwrap(),
        b"pwned\n",
        "control: the mount allows it"
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn landlock_alone_refuses_exec_from_tmp_which_the_mount_allows() {
    // Named busybox: called as anything else it looks for that applet.
    let script =
        "cp /bin/busybox /tmp/busybox && chmod 755 /tmp/busybox && echo copied >> /out/r; \
                  /tmp/busybox true 2>> /out/r && echo RAN-FROM-TMP >> /out/r; echo done >> /out/r";
    let f = landlock_only("ll-tmpx", LandlockMode::Auto, vec![]);
    let (r, _) = f.report(script);
    assert!(r.contains("copied") && !r.contains("RAN-FROM-TMP"), "{r}");
    let f = landlock_only("ll-tmpx-off", LandlockMode::Off, vec![]);
    let (r, _) = f.report(script);
    assert!(
        r.contains("RAN-FROM-TMP"),
        "control: /tmp is not noexec: {r}"
    );
}

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

const IDS_BASE: u32 = 10_000;
const IDS_COUNT: u32 = 1000;

/// The `Uid:` and `Gid:` lines of the job's program (a process in the job's
/// cgroup past its execve), read from the HOST side while the job runs.
fn job_ids_from_host(cgroup_root: &std::path::Path, job: JobId) -> (String, String) {
    let procs = cgroup_root.join(format!("heimdall/job-{job}/cgroup.procs"));
    let t0 = Instant::now();
    let mut last = None;
    while t0.elapsed() < Duration::from_secs(15) {
        let pids = std::fs::read_to_string(&procs).unwrap_or_default();
        for pid in pids.lines() {
            // Only the job's program, after its execve: before it the process
            // is still Heimdall's executor (the shim binary), whose creds are
            // host root until it sets its ids inside the mapped namespace.
            let exe = std::fs::read_link(format!("/proc/{pid}/exe")).unwrap_or_default();
            if exe.as_os_str().is_empty()
                || exe.to_string_lossy().contains("heimdall-container-shim")
            {
                continue;
            }
            let Ok(st) = std::fs::read_to_string(format!("/proc/{pid}/status")) else {
                continue;
            };
            let line = |k: &str| {
                st.lines()
                    .find(|l| l.starts_with(k))
                    .map(|l| {
                        l[k.len()..]
                            .split_whitespace()
                            .collect::<Vec<_>>()
                            .join(" ")
                    })
                    .unwrap_or_default()
            };
            let ids = (line("Uid:"), line("Gid:"));
            // Host root may still be a moment before the ids are set: keep
            // looking while the job runs (it sleeps 2 s); a job that really
            // runs as host root stays root and is returned at the end.
            if ids.0 != "0 0 0 0" {
                return ids;
            }
            last = Some(ids);
        }
        if last.is_some() && t0.elapsed() > Duration::from_secs(4) {
            break;
        }
        std::thread::sleep(Duration::from_millis(20));
    }
    last.unwrap_or_else(|| panic!("no process appeared in {}", procs.display()))
}

fn idmap_run(runner: Arc<HeimdallRunner>, f: &Fx) -> (JobDirs, SignedResult, (String, String)) {
    use std::os::unix::fs::PermissionsExt;
    let d = job_dirs(&f.base);
    std::fs::write(d.input.join("a"), b"root-only input\n").unwrap();
    std::fs::set_permissions(d.input.join("a"), std::fs::Permissions::from_mode(0o600)).unwrap();
    let job = native_job(
        &f.image,
        &sh("cp /in/a /out/a; echo w > /out/w; cat /proc/self/uid_map > /out/m; sleep 2"),
        &d,
    );
    let id = job.id;
    let r = runner.clone();
    let h = std::thread::spawn(move || r.run(job));
    let ids = job_ids_from_host(std::path::Path::new("/sys/fs/cgroup"), id);
    let s = h.join().unwrap().expect("a signed result");
    verify(&s, &runner.verifying_key()).unwrap();
    (d, s, ids)
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn with_host_ids_a_root_runners_job_root_is_not_host_root_seen_from_the_host() {
    use std::os::unix::fs::MetadataExt;
    let f = fx("idmap-k");
    assert_eq!(
        std::fs::metadata("/proc/self").unwrap().uid(),
        0,
        "the kernel tests run the runner as root"
    );
    let runner = HeimdallRunner::new(cfg(&f.base, &f.layout))
        .unwrap()
        .with_host_ids(IDS_BASE, IDS_COUNT)
        .expect("a root runner takes the range");
    let (d, s, (uid, gid)) = idmap_run(Arc::new(runner), &f);
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    // The job's root, from the host: every Uid/Gid field is the range's base.
    assert_eq!(uid, "10000 10000 10000 10000");
    assert_eq!(gid, "10000 10000 10000 10000");
    // A file the job wrote belongs to the base, not to host root.
    let m = std::fs::metadata(d.output.join("w")).unwrap();
    assert_eq!((m.uid(), m.gid()), (IDS_BASE, IDS_BASE));
    // The job read its 0600 input (given to it) and saw the one-line map.
    assert_eq!(
        std::fs::read(d.output.join("a")).unwrap(),
        b"root-only input\n"
    );
    let map = std::fs::read_to_string(d.output.join("m")).unwrap();
    assert_eq!(
        map.split_whitespace().collect::<Vec<_>>(),
        ["0", "10000", "1000"],
        "{map}"
    );
    check_outputs(&s.result, &d.output).unwrap();
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn with_host_ids_a_job_creates_files_in_its_tmp_through_the_broker() {
    // MEASURED on Holger's appliance (lane lsm-shim, 2026-11-03): a root runner with a host id
    // range, the broker in full mode; the job's `open(/tmp/x, O_CREAT)` answered EOVERFLOW,
    // because the broker created the file as host root, which the job's tmpfs (mounted in the
    // job's user namespace) cannot map.
    let f = fx("idmap-tmp");
    let runner = HeimdallRunner::new(cfg(&f.base, &f.layout))
        .unwrap()
        .with_host_ids(IDS_BASE, IDS_COUNT)
        .expect("a root runner takes the range");
    let d = job_dirs(&f.base);
    std::fs::write(d.input.join("a"), b"untrusted input\n").unwrap();
    let j = native_job(
        &f.image,
        &sh(
            "echo t > /tmp/t && cat /tmp/t >> /out/r && echo tmp-ok >> /out/r; \
             ls -ln /tmp/t >> /out/r",
        ),
        &d,
    );
    let s = runner.run(j).expect("a signed result");
    verify(&s, &runner.verifying_key()).unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    let r = std::fs::read_to_string(d.output.join("r")).unwrap_or_default();
    assert!(
        r.contains("tmp-ok"),
        "the job could not create /tmp/t: {r} {:?}",
        s.result.events
    );
    // The job made it: it is its own root's file, seen from inside.
    assert!(
        r.lines()
            .any(|l| l.contains(" 0 ") && l.ends_with("/tmp/t")),
        "{r}"
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn without_host_ids_a_root_runners_job_root_is_the_runners_uid_as_in_0_2_0() {
    use std::os::unix::fs::MetadataExt;
    let f = fx("idmap-k-off");
    let runner = HeimdallRunner::new(cfg(&f.base, &f.layout)).unwrap();
    assert_eq!(runner.host_ids(), None);
    let (d, s, (uid, _gid)) = idmap_run(Arc::new(runner), &f);
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    assert_eq!(uid, "0 0 0 0");
    assert_eq!(std::fs::metadata(d.output.join("w")).unwrap().uid(), 0);
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn many_rounds_of_parallel_jobs_never_fail_with_a_busy_program() {
    // ETXTBSY: while one job's root is unpacked, another thread's spawn holds
    // a copy of the write descriptor until its exec. Measured: 1 run in 3 of
    // the parallel tests failed "Text file busy" under load before the retry.
    let f = fx("busy");
    let image = f.image.clone();
    let base = f.base.clone();
    let r: Arc<dyn JobRunner> = Arc::new(f.runner);
    for round in 0..4 {
        let hs: Vec<_> = (0..16)
            .map(|i| {
                let r = Arc::clone(&r);
                let d = job_dirs(&base);
                let j = native_job(&image, &sh(&format!("echo {round}-{i} > /out/n")), &d);
                std::thread::spawn(move || r.run(j))
            })
            .collect();
        for h in hs {
            let s = h
                .join()
                .unwrap()
                .unwrap_or_else(|e| panic!("round {round}: {e:?}"));
            assert!(
                s.result.verdict.is_accepted(),
                "round {round}: {:?}",
                s.result.verdict
            );
        }
    }
}

// ---- fetch jobs: a declared set of peers, each connected by the broker ----

fn listen_on(ip: &str, tag: &'static [u8]) -> u16 {
    let l = TcpListener::bind(format!("{ip}:0")).unwrap();
    let port = l.local_addr().unwrap().port();
    std::thread::spawn(move || {
        for mut c in l.incoming().flatten() {
            use std::io::Write;
            let _ = c.write_all(tag);
        }
    });
    port
}

fn listen(tag: &'static [u8]) -> u16 {
    listen_on("127.0.0.1", tag)
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_fetch_job_reaches_both_declared_peers_and_nothing_undeclared() {
    let one = listen(b"ONE\n");
    let two = listen(b"TWO\n");
    let other = listen(b"FORBIDDEN\n");
    // The same port as a declared peer, on another loopback address.
    let other_addr = listen_on("127.0.0.2", b"FORBIDDEN-ADDR\n");
    let f = fx("fetch2");
    let script = format!(
        "nc -w 3 127.0.0.1 {one} < /dev/null >> /out/r 2>&1; \
         nc -w 3 127.0.0.1 {two} < /dev/null >> /out/r 2>&1; \
         nc -w 3 127.0.0.1 {other} < /dev/null >> /out/r 2>&1; \
         nc -w 3 127.0.0.2 {other_addr} < /dev/null >> /out/r 2>&1; \
         nc -l -p 9999 -w 1 >> /out/r 2>&1; true"
    );
    let peers = vec![
        NetPeer::new("one.example", "127.0.0.1", one),
        NetPeer::new("two.example", "127.0.0.1", two),
    ];
    let declared = peers.clone();
    let (r, res) = f.report_with(&script, move |j| {
        j.policy.net = Some(NetAllow { peers: declared })
    });
    let ev = broker_events(&res);
    assert!(res.verdict.is_accepted(), "{:?}", res.verdict);
    assert!(res.runner.broker);
    assert!(r.contains("ONE") && r.contains("TWO"), "{r}\n{ev}");
    assert!(!r.contains("FORBIDDEN"), "{r}\n{ev}");
    for p in [one, two] {
        assert!(
            has_event(
                &res,
                "connect",
                Decision::Allowed,
                &format!("addr=127.0.0.1:{p}")
            ),
            "{ev}"
        );
    }
    assert!(
        has_event(
            &res,
            "connect",
            Decision::Refused,
            &format!("addr=127.0.0.1:{other}")
        ),
        "{ev}"
    );
    assert!(
        has_event(
            &res,
            "connect",
            Decision::Refused,
            &format!("addr=127.0.0.2:{other_addr}")
        ),
        "{ev}"
    );
    assert!(
        has_event(&res, "bind", Decision::Refused, "bind is never allowed")
            || has_event(&res, "listen", Decision::Refused, "no listening sockets"),
        "{ev}"
    );
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn the_job_reads_its_declared_peers_with_their_host_names() {
    let one = listen(b"ONE\n");
    let f = fx("fetch-peers");
    // The job sees every declared peer with its host name (for SNI and the
    // certificate check) as JSON in HEIMDALL_NET_PEERS.
    let n = NetAllow {
        peers: vec![
            NetPeer::new("pypi.org", "127.0.0.1", one),
            NetPeer::new("files.pythonhosted.org", "127.0.0.3", one),
        ],
    };
    let want = n.peers.clone();
    let (r, res) = f.report_with("printf '%s' \"$HEIMDALL_NET_PEERS\" > /out/r", move |j| {
        j.policy.net = Some(n)
    });
    assert!(res.verdict.is_accepted(), "{:?}", res.verdict);
    let got: Vec<NetPeer> = serde_json::from_str(&r).unwrap_or_else(|e| panic!("{e}: {r:?}"));
    assert_eq!(got, want);
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn more_than_sixteen_peers_are_refused_before_anything_runs() {
    let f = fx("fetch-cap");
    let d = job_dirs(&f.base);
    let mut j = native_job(&f.image, &sh("echo ran > /out/r"), &d);
    j.policy.net = Some(NetAllow {
        peers: (0..17u16)
            .map(|i| NetPeer::new(&format!("h{i}.example"), "127.0.0.1", 2000 + i))
            .collect(),
    });
    match f.runner.run(j) {
        Err(RunError::InvalidJob { reason }) => assert!(reason.contains("at most 16"), "{reason}"),
        other => panic!("{other:?}"),
    }
    assert!(!d.output.join("r").exists());
}

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_fetch_jobs_own_network_namespace_stays_empty_and_a_raw_socket_is_refused() {
    let allowed = listen(b"ALLOWED\n");
    if selftest().is_none() {
        eprintln!("skipped: HEIMDALL_BROKER_SELFTEST not set");
        return;
    }
    let base = scratch("hc-fetch-raw");
    let layout = Layout::new(&base.join("layout"));
    let image = selftest_image(&layout);
    let runner = HeimdallRunner::new(cfg(&base, &layout)).unwrap();
    let d = job_dirs(&base);
    let script = format!(
        "/bin/selftest seq connect 127.0.0.1:{allowed} -- socket raw -- socket udp -- socket tcp6 -- socket unix -- bind 127.0.0.1:0 > /out/r; \
         cat /proc/net/dev > /out/dev"
    );
    let mut j = native_job(&image, &sh(&script), &d);
    j.policy.net = Some(NetAllow::one("allowed.example", "127.0.0.1", allowed));
    let s = runner.run(j).unwrap();
    let r = std::fs::read_to_string(d.output.join("r")).unwrap_or_default();
    let lines: Vec<&str> = r.lines().collect();
    let ev = broker_events(&s.result);
    assert_eq!(lines.len(), 7, "{r}\n{ev}");
    assert_eq!(lines[0], "ok connected ALLOWED", "{ev}");
    for l in &lines[1..6] {
        assert_eq!(*l, format!("err {}", libc::EACCES), "{r}");
    }
    let dev = std::fs::read_to_string(d.output.join("dev")).unwrap();
    let ifaces: Vec<&str> = dev
        .lines()
        .skip(2)
        .filter_map(|l| l.split(':').next())
        .map(str::trim)
        .collect();
    assert_eq!(ifaces, vec!["lo"], "the job's own netns is still empty");
}

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

#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn with_nofile_the_job_gets_that_many_open_files_and_no_more() {
    let base = scratch("hc-nofile");
    let layout = Layout::new(&base.join("layout"));
    let image = busybox_image(&layout);
    let runner = HeimdallRunner::new(cfg(&base, &layout))
        .unwrap()
        .with_nofile(4096)
        .expect("4096 is within this runner's own limit");
    let d = job_dirs(&base);
    let job = native_job(
        &image,
        &sh("grep 'Max open files' /proc/self/limits > /out/r"),
        &d,
    );
    let s = runner.run(job).expect("a signed result");
    verify(&s, &runner.verifying_key()).unwrap();
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    let r = std::fs::read_to_string(d.output.join("r")).unwrap();
    let v: Vec<&str> = r["Max open files".len()..].split_whitespace().collect();
    assert_eq!(v[..2], ["4096", "4096"], "{r}");
}