crates/heimdall-container/tests/workspace_kernel.rs
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Ignalina ApS and Vetra Automation AB
//! The job workspace on a real kernel (owner decision 2026-11-01): writable,
//! capped, not executable unless asked, deleted after the job. Heimdall is a
//! defensive sandbox; every job here writes and runs only its own files.
//! `#[ignore]`d in plain CI: scripts/container-tests.sh runs them.
#[path = "../../heimdall-plain/tests/common/mod.rs"]
mod common;
use std::path::PathBuf;
use std::time::Duration;
use common::*;
use heimdall_container::*;
use heimdall_core::*;
struct Fx {
base: PathBuf,
image: ImageRef,
runner: HeimdallRunner,
}
fn fx(name: &str, allow_exec: bool) -> Fx {
let base = scratch(&format!("hc-ws-{name}"));
let layout = Layout::new(&base.join("layout"));
let image = busybox_image(&layout);
let mut c = HeimdallConfig::new(&layout.dir, base.join("work"));
c.allow_workspace_exec = allow_exec;
Fx {
runner: HeimdallRunner::new(c).expect("this kernel has every feature (strict)"),
base,
image,
}
}
impl Fx {
/// Run `script` with a workspace; the report it wrote to /out/r.
fn run(&self, script: &str, bytes: u64, exec: bool) -> (String, JobResult) {
let d = job_dirs(&self.base);
let mut j = native_job(&self.image, &sh(script), &d);
j.policy.workspace = Some(Workspace { bytes, exec });
j.policy.limits.wall = Duration::from_secs(60);
j.policy.limits.memory_bytes = 256 << 20;
let s = self.runner.run(j).expect("a signed result");
verify(&s, &self.runner.verifying_key()).expect("it verifies");
let r = std::fs::read_to_string(d.output.join("r")).unwrap_or_default();
(r, s.result)
}
fn workspaces_left(&self) -> usize {
std::fs::read_dir(self.base.join("work/workspaces"))
.map(|d| d.count())
.unwrap_or(0)
}
}
const MIB: u64 = 1 << 20;
#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_tmpfs_workspace_is_writable_and_readable_and_capped() {
let f = fx("tmpfs", false);
let (r, res) = f.run(
"echo hello > /work/a && cat /work/a > /out/r && \
(dd if=/dev/zero of=/work/big bs=1M count=20 2>/dev/null && echo over >> /out/r || echo capped >> /out/r)",
8 * MIB,
false,
);
assert!(res.verdict.is_accepted(), "{:?} {r}", res.verdict);
assert_eq!(r, "hello\ncapped\n");
}
#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn nothing_in_a_workspace_runs_unless_exec_is_asked_and_allowed() {
let script = "cp /bin/busybox /work/busybox && chmod +x /work/busybox && \
(/work/busybox true 2>/tmp/e && echo ran > /out/r || (echo refused; cat /tmp/e) > /out/r)";
let f = fx("noexec", false);
let (r, _) = f.run(script, 64 * MIB, false);
assert!(r.starts_with("refused\n"), "{r}");
let f = fx("exec", true);
let (r, res) = f.run(script, 64 * MIB, true);
assert!(res.verdict.is_accepted(), "{:?}", res.verdict);
assert_eq!(r, "ran\n", "{r}");
// A host-folder workspace the same.
let (r, _) = fx("noexec-host", false).run(script, (1 << 30) + MIB, false);
assert!(r.starts_with("refused\n"), "{r}");
let (r, _) = fx("exec-host", true).run(script, (1 << 30) + MIB, true);
assert_eq!(r, "ran\n", "{r}");
}
#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_host_workspace_is_writable_and_deleted_when_the_job_ends() {
let f = fx("host", false);
let (r, res) = f.run(
"dd if=/dev/zero of=/work/f bs=1M count=8 2>/dev/null && test -s /work/f && echo 1 > /out/r",
(1 << 30) + MIB,
false,
);
assert!(res.verdict.is_accepted(), "{:?} {r}", res.verdict);
assert_eq!(r.trim(), "1");
assert_eq!(f.workspaces_left(), 0, "the workspace outlived its job");
}
#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn a_host_workspace_over_its_cap_kills_the_job() {
let f = fx("hostcap", false);
let cap = (1 << 30) + 16 * MIB;
let (r, res) = f.run(
"dd if=/dev/zero of=/work/f bs=1M count=1100 oflag=direct 2>/dev/null; sleep 30; echo survived > /out/r",
cap,
false,
);
assert_eq!(
res.verdict.code(),
Some(codes::LIMIT_WORKSPACE),
"{:?} {r}",
res.verdict
);
assert_eq!(res.exit.killed, Some(KillReason::Workspace));
assert_eq!(f.workspaces_left(), 0);
}
// ==== a host workspace under with_host_ids (0.3.1; found by the k13 bench) ====
#[test]
#[ignore = "needs user namespaces + delegated cgroup v2 + Landlock: scripts/container-tests.sh"]
fn with_host_ids_a_host_workspace_is_the_jobs_and_writable() {
use std::os::unix::fs::MetadataExt;
assert_eq!(
std::fs::metadata("/proc/self").unwrap().uid(),
0,
"the kernel tests run the runner as root"
);
let mut f = fx("host-ids", false);
f.runner = f
.runner
.with_host_ids(10_000, 1000)
.expect("a root runner takes the range");
let (r, res) = f.run(
"echo w > /work/f && stat -c %u /work/f > /out/r",
(1 << 30) + MIB,
false,
);
assert!(res.verdict.is_accepted(), "{:?} {r}", res.verdict);
assert_eq!(r.trim(), "0", "the job wrote its workspace as its own root");
assert_eq!(f.workspaces_left(), 0, "the workspace outlived its job");
}