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)
);
}
}