crates/heimdall-probe/src/facts.rs
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Ignalina ApS and Vetra Automation AB
//! Which argument facts the probe records per syscall (spec §3.1 step 2).
//! Heimdall is a defensive sandbox: these are the arguments that change what
//! a call can do; the rest (fds, buffers, lengths, pointers) vary freely.
use heimdall_seccomp::Width;
/// How the facts of a call are minimised into rules.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Kind {
/// The observed value tuples are kept exactly while they are few
/// (`exact_limit`), else the call becomes a bare allow (reported).
Exact,
/// A flag field: the union of the observed bits; no other bit may be set.
Flags,
}
/// The recorded arguments of one syscall.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct CallSpec {
pub syscall: &'static str,
pub kind: Kind,
/// (argument index, width).
pub args: &'static [(u8, Width)],
/// A protection field: `PROT_WRITE|PROT_EXEC` together is flagged and,
/// unless the workload allows it, left out of the profile.
pub prot: bool,
}
const W32: Width = Width::W32;
const W64: Width = Width::W64;
/// The calls whose arguments are recorded.
pub const SPECS: &[CallSpec] = &[
CallSpec {
syscall: "socket",
kind: Kind::Exact,
args: &[(0, W32), (1, W32), (2, W32)],
prot: false,
},
CallSpec {
syscall: "socketpair",
kind: Kind::Exact,
args: &[(0, W32), (1, W32), (2, W32)],
prot: false,
},
CallSpec {
syscall: "open",
kind: Kind::Flags,
args: &[(1, W32)],
prot: false,
},
CallSpec {
syscall: "openat",
kind: Kind::Flags,
args: &[(2, W32)],
prot: false,
},
CallSpec {
syscall: "ioctl",
kind: Kind::Exact,
args: &[(1, W32)],
prot: false,
},
CallSpec {
syscall: "mmap",
kind: Kind::Exact,
args: &[(2, W32)],
prot: true,
},
CallSpec {
syscall: "mprotect",
kind: Kind::Exact,
args: &[(2, W32)],
prot: true,
},
CallSpec {
syscall: "pkey_mprotect",
kind: Kind::Exact,
args: &[(2, W32)],
prot: true,
},
CallSpec {
syscall: "clone",
kind: Kind::Flags,
args: &[(0, W64)],
prot: false,
},
CallSpec {
syscall: "prctl",
kind: Kind::Exact,
args: &[(0, W32)],
prot: false,
},
CallSpec {
syscall: "fcntl",
kind: Kind::Exact,
args: &[(1, W32)],
prot: false,
},
];
/// The spec of a call, if its arguments are recorded.
pub fn spec(syscall: &str) -> Option<&'static CallSpec> {
SPECS.iter().find(|s| s.syscall == syscall)
}
/// `PROT_WRITE|PROT_EXEC` both set.
pub fn is_wx(prot: u64) -> bool {
let w = libc::PROT_WRITE as u64;
let x = libc::PROT_EXEC as u64;
prot & (w | x) == (w | x)
}
/// Calls the probe answers itself in learning mode, and the profile keeps
/// as fixed rules: `clone3` gets `ENOSYS`, so the libc falls back to `clone`,
/// whose flags a filter can check (`clone3` passes them in memory).
pub fn fixed_rules() -> Vec<heimdall_seccomp::Rule> {
vec![heimdall_seccomp::Rule::bare(
"clone3",
heimdall_seccomp::Action::Errno(libc::ENOSYS as u16),
)]
}
/// The note every generated profile carries about the vDSO.
pub const VDSO_NOTE: &str = "clock_gettime, clock_getres, gettimeofday, time and getcpu normally run in the vDSO and never reach seccomp; their syscall fallbacks are in the profile only if the probe saw them";