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

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

//! Resolving a job's GENERATED seccomp profile (heimdall_container::genprof):
//! by workload name + digest from the checked-in profiles, or from an OCI
//! seccomp JSON file pinned by sha256. Heimdall is a defensive sandbox; these
//! tests run no job and install no filter (plain CI).

use std::path::PathBuf;

use heimdall_container::genprof::{resolve, MAX_PROFILE_BYTES};
use heimdall_core::{ProfileSource, RunError, SeccompProfileRef, Sha256Digest};
use heimdall_seccomp::{Arch, Profile};

fn scratch(name: &str) -> PathBuf {
    let d = std::env::temp_dir().join(format!("heim-genprof-{name}-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&d);
    std::fs::create_dir_all(&d).unwrap();
    d
}

fn native_entries() -> Vec<&'static heimdall_profiles::Entry> {
    heimdall_profiles::ALL
        .iter()
        .filter(|e| Some(e.profile.arch) == Arch::native())
        .collect()
}

fn reference(source: ProfileSource, bytes: &[u8]) -> SeccompProfileRef {
    SeccompProfileRef {
        source,
        sha256: Sha256Digest::of(bytes).to_prefixed(),
        fallback_to_default: false,
    }
}

fn refusal(r: Result<impl std::fmt::Debug, RunError>) -> String {
    match r {
        Err(RunError::InvalidJob { reason }) => reason,
        other => panic!("expected a refusal: {other:?}"),
    }
}

#[test]
fn every_checked_in_oci_json_reads_back_as_its_compiled_profile() {
    for e in heimdall_profiles::ALL {
        let back = heimdall_seccomp::oci::from_oci(e.oci_json, e.profile.workload, e.profile.libc)
            .unwrap();
        assert_eq!(
            back,
            Profile::from(e.profile),
            "{} {} {}",
            e.profile.workload,
            e.profile.arch,
            e.profile.libc
        );
    }
}

#[test]
fn a_checked_in_profile_is_found_by_workload_and_digest_and_each_libc_by_its_own_digest() {
    let mine = native_entries();
    assert!(!mine.is_empty());
    for e in mine {
        let r = reference(
            ProfileSource::Workload(e.profile.workload.into()),
            e.oci_json.as_bytes(),
        );
        let p = resolve(&r).unwrap().expect("applied, not a fallback");
        assert_eq!(p.digest, r.sha256);
        assert_eq!(p.profile, Profile::from(e.profile));
    }
}

#[test]
fn an_oci_file_pinned_by_its_sha256_resolves_to_the_same_rules() {
    let d = scratch("file");
    let e = native_entries()[0];
    let path = d.join(e.profile.workload).join("x86_64-glibc.oci.json");
    std::fs::create_dir_all(path.parent().unwrap()).unwrap();
    std::fs::write(&path, e.oci_json).unwrap();
    let r = reference(ProfileSource::OciFile(path), e.oci_json.as_bytes());
    let p = resolve(&r).unwrap().unwrap();
    assert_eq!(p.profile.rules, Profile::from(e.profile).rules);
    assert_eq!(p.profile.workload, e.profile.workload);
}

#[test]
fn a_changed_byte_in_the_oci_file_refuses_the_job_naming_the_digest_it_found() {
    let d = scratch("tamper");
    let e = native_entries()[0];
    let path = d.join("p.oci.json");
    let r = reference(ProfileSource::OciFile(path.clone()), e.oci_json.as_bytes());
    // One more allowed call: the attacker's widening.
    let widened = e.oci_json.replacen("\"access\"", "\"socket\"", 1);
    assert_ne!(widened, e.oci_json);
    std::fs::write(&path, &widened).unwrap();
    let why = refusal(resolve(&r));
    assert!(why.contains("digest mismatch"), "{why}");
    assert!(
        why.contains(&Sha256Digest::of(widened.as_bytes()).to_prefixed()),
        "{why}"
    );
}

#[test]
fn a_missing_file_a_relative_path_a_symlink_or_a_huge_file_refuses_the_job() {
    let d = scratch("missing");
    let e = native_entries()[0];
    let ok = d.join("ok.oci.json");
    std::fs::write(&ok, e.oci_json).unwrap();
    let mk = |p: PathBuf| reference(ProfileSource::OciFile(p), e.oci_json.as_bytes());
    assert!(refusal(resolve(&mk(d.join("nope.json")))).contains("missing"));
    assert!(refusal(resolve(&mk("rel/p.json".into()))).contains("not absolute"));
    let link = d.join("link.json");
    std::os::unix::fs::symlink(&ok, &link).unwrap();
    refusal(resolve(&mk(link)));
    let huge = d.join("huge.json");
    std::fs::write(&huge, vec![b' '; MAX_PROFILE_BYTES as usize + 1]).unwrap();
    assert!(refusal(resolve(&mk(huge))).contains("larger"));
}

#[test]
fn a_profile_that_is_not_enforcing_refuses_the_job_even_with_the_right_digest() {
    let d = scratch("audit");
    let e = native_entries()[0];
    let audit = e.oci_json.replace("SCMP_ACT_KILL_PROCESS", "SCMP_ACT_LOG");
    let path = d.join("audit.oci.json");
    std::fs::write(&path, &audit).unwrap();
    let r = reference(ProfileSource::OciFile(path), audit.as_bytes());
    refusal(resolve(&r));
}

#[test]
fn with_fallback_a_missing_or_tampered_profile_resolves_to_none_but_a_bad_pin_still_refuses() {
    let d = scratch("fallback");
    let e = native_entries()[0];
    let mut r = reference(
        ProfileSource::OciFile(d.join("nope.json")),
        e.oci_json.as_bytes(),
    );
    r.fallback_to_default = true;
    assert_eq!(resolve(&r).unwrap(), None);
    r.source = ProfileSource::Workload("no-such-workload".into());
    assert_eq!(resolve(&r).unwrap(), None);
    r.source = ProfileSource::Workload(e.profile.workload.into());
    r.sha256 = format!("sha256:{}", "ee".repeat(32));
    assert_eq!(resolve(&r).unwrap(), None);
    r.sha256 = "sha256:XYZ".into();
    refusal(resolve(&r));
}

#[test]
fn a_policy_naming_a_profile_round_trips_through_json_and_old_policies_have_none() {
    let e = native_entries()[0];
    let p = heimdall_core::Policy {
        seccomp: Some(reference(
            ProfileSource::Workload(e.profile.workload.into()),
            e.oci_json.as_bytes(),
        )),
        ..Default::default()
    };
    let j = serde_json::to_string(&p).unwrap();
    assert!(j.contains("\"workload\""), "{j}");
    assert_eq!(
        serde_json::from_str::<heimdall_core::Policy>(&j).unwrap(),
        p
    );
    let old = serde_json::to_value(heimdall_core::Policy::default()).unwrap();
    assert!(old.get("seccomp").is_none(), "{old}");
    assert_eq!(
        serde_json::from_value::<heimdall_core::Policy>(old)
            .unwrap()
            .seccomp,
        None
    );
}