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

nordisk/heimdall

crates/heimdall-seccomp/tests/oci.rs

// SPDX-License-Identifier: AGPL-3.0-only
//! The OCI seccomp JSON form. Heimdall is a defensive sandbox; the OCI form
//! carries the same allowlist for a pod's `seccompProfile: Localhost`.

use heimdall_seccomp::bpf::Mode;
use heimdall_seccomp::oci::to_oci;
use heimdall_seccomp::{Action, Arch, ArgCmp, Libc, Profile, Rule};

#[test]
fn oci_json_has_kill_default_one_entry_per_alternative_and_masked_eq_args() {
    let p = Profile {
        workload: "w".into(),
        arch: Arch::X86_64,
        libc: Libc::Musl,
        rules: vec![
            Rule::bare("read", Action::Allow),
            Rule {
                syscall: "socket".into(),
                action: Action::Allow,
                when: vec![
                    vec![ArgCmp::eq32(0, 1), ArgCmp::eq32(1, 1)],
                    vec![ArgCmp::eq32(0, 10)],
                ],
            },
            Rule::bare("clone3", Action::Errno(38)),
        ],
    };
    let j = to_oci(&p, &Mode::Enforce);
    assert_eq!(j["defaultAction"], "SCMP_ACT_KILL_PROCESS");
    assert_eq!(j["architectures"][0], "SCMP_ARCH_X86_64");
    let s = j["syscalls"].as_array().unwrap();
    assert_eq!(s.len(), 4);
    assert_eq!(s[0]["names"][0], "read");
    assert!(s[0].get("args").is_none());
    assert_eq!(s[1]["args"].as_array().unwrap().len(), 2);
    assert_eq!(s[1]["args"][0]["op"], "SCMP_CMP_MASKED_EQ");
    assert_eq!(s[1]["args"][0]["value"], 0xffff_ffffu64);
    assert_eq!(s[1]["args"][0]["valueTwo"], 1);
    assert_eq!(s[2]["args"][0]["valueTwo"], 10);
    assert_eq!(s[3]["action"], "SCMP_ACT_ERRNO");
    assert_eq!(s[3]["errnoRet"], 38);
}

// ---- from_oci: the OCI form read back (lane genprof) ----

use heimdall_seccomp::oci::from_oci;
use heimdall_seccomp::ProfileError;

fn sample() -> Profile {
    Profile {
        workload: "w".into(),
        arch: Arch::X86_64,
        libc: Libc::Glibc,
        rules: vec![
            Rule::bare("read", Action::Allow),
            Rule {
                syscall: "socket".into(),
                action: Action::Allow,
                when: vec![
                    vec![ArgCmp::eq32(0, 1), ArgCmp::eq32(1, 1)],
                    vec![ArgCmp::eq32(0, 10)],
                ],
            },
            Rule {
                syscall: "clone".into(),
                action: Action::Allow,
                when: vec![vec![ArgCmp::only_bits64(0, 0x3d0f00)]],
            },
            Rule {
                syscall: "mmap".into(),
                action: Action::Allow,
                when: vec![vec![ArgCmp::eq64(3, 0x22)]],
            },
            Rule::bare("clone3", Action::Errno(38)),
            Rule::bare("exit_group", Action::Allow),
        ],
    }
}

#[test]
fn from_oci_reads_back_exactly_what_to_oci_wrote_for_an_enforcing_profile() {
    let p = sample();
    let j = to_oci(&p, &Mode::Enforce).to_string();
    assert_eq!(from_oci(&j, "w", Libc::Glibc).unwrap(), p);
}

#[test]
fn from_oci_refuses_a_profile_whose_default_lets_unknown_calls_through() {
    let mut j = to_oci(&sample(), &Mode::Enforce);
    for d in [
        "SCMP_ACT_ALLOW",
        "SCMP_ACT_LOG",
        "SCMP_ACT_ERRNO",
        "SCMP_ACT_KILL_THREAD",
    ] {
        j["defaultAction"] = d.into();
        assert!(
            matches!(
                from_oci(&j.to_string(), "w", Libc::Glibc),
                Err(ProfileError::Oci(_))
            ),
            "{d} accepted"
        );
    }
}

type Edit = Box<dyn Fn(&mut serde_json::Value)>;

#[test]
fn from_oci_refuses_notify_trace_other_ops_includes_foreign_arch_and_unknown_names() {
    let base = to_oci(&sample(), &Mode::Enforce);
    let edits: Vec<(&str, Edit)> = vec![
        (
            "notify",
            Box::new(|j| j["syscalls"][0]["action"] = "SCMP_ACT_NOTIFY".into()),
        ),
        (
            "trace",
            Box::new(|j| j["syscalls"][0]["action"] = "SCMP_ACT_TRACE".into()),
        ),
        (
            "op",
            Box::new(|j| j["syscalls"][1]["args"][0]["op"] = "SCMP_CMP_GT".into()),
        ),
        (
            "includes",
            Box::new(|j| {
                j["syscalls"][0]["includes"] = serde_json::json!({"caps": ["CAP_SYS_ADMIN"]})
            }),
        ),
        (
            "arch",
            Box::new(|j| j["architectures"] = serde_json::json!(["SCMP_ARCH_X86"])),
        ),
        (
            "two arches",
            Box::new(|j| {
                j["architectures"] = serde_json::json!(["SCMP_ARCH_X86_64", "SCMP_ARCH_X86"])
            }),
        ),
        (
            "name",
            Box::new(|j| j["syscalls"][0]["names"][0] = "no_such_call".into()),
        ),
        (
            "index",
            Box::new(|j| j["syscalls"][1]["args"][0]["index"] = 6.into()),
        ),
        ("not json", Box::new(|j| *j = "[]".into())),
    ];
    for (what, edit) in edits {
        let mut j = base.clone();
        edit(&mut j);
        assert!(
            from_oci(&j.to_string(), "w", Libc::Glibc).is_err(),
            "{what} accepted"
        );
    }
}

#[test]
fn from_oci_reads_scmp_cmp_eq_as_a_full_compare_and_several_names_as_several_rules() {
    let j = serde_json::json!({
        "defaultAction": "SCMP_ACT_KILL_PROCESS",
        "architectures": ["SCMP_ARCH_X86_64"],
        "syscalls": [
            {"names": ["read", "write"], "action": "SCMP_ACT_ALLOW"},
            {"names": ["mmap"], "action": "SCMP_ACT_ALLOW", "args": [{"index": 3, "value": 34, "op": "SCMP_CMP_EQ"}]}
        ]
    });
    let p = from_oci(&j.to_string(), "x", Libc::Musl).unwrap();
    assert_eq!(p.workload, "x");
    assert_eq!(p.libc, Libc::Musl);
    assert_eq!(
        p.rules,
        vec![
            Rule::bare("read", Action::Allow),
            Rule::bare("write", Action::Allow),
            Rule {
                syscall: "mmap".into(),
                action: Action::Allow,
                when: vec![vec![ArgCmp::eq64(3, 34)]],
            },
        ]
    );
}

proptest::proptest! {
    #[test]
    fn from_oci_of_to_oci_evaluates_every_call_like_the_original(
        nr in 0u32..450, a0 in proptest::prelude::any::<u64>(), a1 in 0u64..16, a3 in 0u64..64,
    ) {
        let p = sample();
        let back = from_oci(&to_oci(&p, &Mode::Enforce).to_string(), "w", Libc::Glibc).unwrap();
        let args = [a0, a1, 0, a3, 0, 0];
        proptest::prop_assert_eq!(
            back.evaluate(nr, &args, Action::KillProcess),
            p.evaluate(nr, &args, Action::KillProcess)
        );
    }
}