use std::fmt;
use serde::{Deserialize, Serialize};
use crate::grant::{GrantAtom, GrantSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum Tier {
InProcess,
Subprocess,
Wasm,
MicroVm,
}
impl Tier {
pub const ALL: [Self; 4] = [Self::InProcess, Self::Subprocess, Self::Wasm, Self::MicroVm];
pub const fn name(self) -> &'static str {
match self {
Self::InProcess => "inproc",
Self::Subprocess => "process",
Self::Wasm => "wasm",
Self::MicroVm => "microvm",
}
}
pub const fn is_virtualising(self) -> bool {
matches!(self, Self::Wasm | Self::MicroVm)
}
pub const fn requires_trust(self) -> bool {
matches!(self, Self::InProcess)
}
pub const fn weaker(self, other: Self) -> Self {
if (self as u8) <= (other as u8) {
self
} else {
other
}
}
pub fn weakest(tiers: impl IntoIterator<Item = Self>) -> Option<Self> {
tiers.into_iter().reduce(Self::weaker)
}
}
impl Default for Tier {
fn default() -> Self {
Self::Subprocess
}
}
impl fmt::Display for Tier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Enforcement {
Kernel,
Interposed,
None,
}
impl Enforcement {
pub const ALL: [Self; 3] = [Self::Kernel, Self::Interposed, Self::None];
pub const fn permits_grants(self) -> bool {
!matches!(self, Self::None)
}
pub const fn name(self) -> &'static str {
match self {
Self::Kernel => "kernel",
Self::Interposed => "interposed",
Self::None => "none",
}
}
pub const fn describe(self) -> &'static str {
match self {
Self::Kernel => "enforced by the kernel",
Self::Interposed => "enforced by the host, which a tool making its own syscalls evades",
Self::None => "not enforced",
}
}
}
impl fmt::Display for Enforcement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.name())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Confinement {
pub filesystem: Enforcement,
pub network: Enforcement,
pub process: Enforcement,
pub environment: Enforcement,
pub secret: Enforcement,
}
impl Confinement {
pub const NONE: Self = Self {
filesystem: Enforcement::None,
network: Enforcement::None,
process: Enforcement::None,
environment: Enforcement::None,
secret: Enforcement::None,
};
pub const fn of(&self, domain: &str) -> Enforcement {
match domain.as_bytes() {
b"fs" => self.filesystem,
b"net" => self.network,
b"proc" => self.process,
b"env" => self.environment,
b"secret" => self.secret,
_ => Enforcement::None,
}
}
pub const fn domains(&self) -> [(&'static str, Enforcement); 5] {
[
("fs", self.filesystem),
("net", self.network),
("proc", self.process),
("env", self.environment),
("secret", self.secret),
]
}
pub fn enforces_nothing(&self) -> bool {
*self == Self::NONE
}
pub fn unenforceable<'g>(&self, grants: &'g GrantSet) -> Option<&'g GrantAtom> {
grants
.iter()
.find(|atom| !self.of(&atom.domain).permits_grants())
}
}
impl fmt::Display for Confinement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, (domain, enforcement)) in self.domains().into_iter().enumerate() {
if index > 0 {
f.write_str(" ")?;
}
write!(f, "{domain}={enforcement}")?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Isolation {
pub tier: Tier,
pub confinement: Confinement,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worker: Option<Worker>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct Worker {
pub generation: u64,
pub served: u32,
}
impl Worker {
pub const fn new(generation: u64, served: u32) -> Self {
Self { generation, served }
}
pub const fn is_first(&self) -> bool {
self.served == 0
}
}
impl fmt::Display for Worker {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"worker {} after {} call(s)",
self.generation, self.served
)
}
}
impl Isolation {
pub const fn new(tier: Tier, confinement: Confinement) -> Self {
Self {
tier,
confinement,
worker: None,
}
}
#[must_use]
pub const fn served_by(mut self, worker: Worker) -> Self {
self.worker = Some(worker);
self
}
pub const fn was_warm(&self) -> bool {
self.worker.is_some()
}
pub const fn unconfined(tier: Tier) -> Self {
Self::new(tier, Confinement::NONE)
}
pub const fn is_virtualising(&self) -> bool {
self.tier.is_virtualising()
}
pub fn enforces_nothing(&self) -> bool {
self.confinement.enforces_nothing()
}
pub fn is_kernel_enforced(&self) -> bool {
self.confinement
.domains()
.into_iter()
.all(|(_, enforcement)| enforcement == Enforcement::Kernel)
}
}
impl fmt::Display for Isolation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.tier, self.confinement)?;
match self.worker {
Some(worker) => write!(f, ", {worker}"),
None => Ok(()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_tier_has_a_stable_name() {
for (tier, name) in [
(Tier::InProcess, "inproc"),
(Tier::Subprocess, "process"),
(Tier::Wasm, "wasm"),
(Tier::MicroVm, "microvm"),
] {
assert_eq!(tier.name(), name);
assert_eq!(tier.to_string(), name);
}
}
#[test]
fn only_the_virtualising_tiers_say_they_are() {
assert!(!Tier::InProcess.is_virtualising());
assert!(!Tier::Subprocess.is_virtualising());
assert!(Tier::Wasm.is_virtualising());
assert!(Tier::MicroVm.is_virtualising());
}
#[test]
fn tiers_are_ordered_weakest_first() {
assert!(Tier::InProcess < Tier::Subprocess);
assert!(Tier::Subprocess < Tier::Wasm);
assert!(Tier::Wasm < Tier::MicroVm);
let mut shuffled = [Tier::Wasm, Tier::InProcess, Tier::MicroVm, Tier::Subprocess];
shuffled.sort();
assert_eq!(shuffled, Tier::ALL);
}
#[test]
fn the_weakest_tier_is_the_one_reported() {
assert_eq!(
Tier::weakest([Tier::Wasm, Tier::InProcess, Tier::Subprocess]),
Some(Tier::InProcess)
);
assert_eq!(Tier::weakest([Tier::Wasm, Tier::MicroVm]), Some(Tier::Wasm));
assert_eq!(Tier::weakest([]), None);
}
#[test]
fn the_default_is_the_safe_one_rather_than_the_convenient_one() {
assert_eq!(Tier::default(), Tier::Subprocess);
assert!(!Tier::default().requires_trust());
}
#[test]
fn only_the_in_process_tier_depends_on_trusting_the_tool() {
assert!(Tier::InProcess.requires_trust());
for tier in [Tier::Subprocess, Tier::Wasm, Tier::MicroVm] {
assert!(!tier.requires_trust(), "{tier}");
}
}
#[test]
fn a_tier_round_trips_through_its_serialised_name() {
for tier in Tier::ALL {
let json = serde_json::to_string(&tier).unwrap();
assert_eq!(serde_json::from_str::<Tier>(&json).unwrap(), tier);
}
assert_eq!(
serde_json::to_string(&Tier::InProcess).unwrap(),
"\"in_process\""
);
}
fn kernel_everywhere() -> Confinement {
Confinement {
filesystem: Enforcement::Kernel,
network: Enforcement::Kernel,
process: Enforcement::Kernel,
environment: Enforcement::Kernel,
secret: Enforcement::Kernel,
}
}
#[test]
fn the_confinement_floor_enforces_nothing_in_every_domain() {
assert!(Confinement::NONE.enforces_nothing());
for (domain, enforcement) in Confinement::NONE.domains() {
assert_eq!(enforcement, Enforcement::None, "{domain}");
assert!(!Confinement::NONE.of(domain).permits_grants());
}
}
#[test]
fn an_unknown_domain_is_never_reported_as_confined() {
assert_eq!(kernel_everywhere().of("quantum"), Enforcement::None);
assert!(!kernel_everywhere().of("quantum").permits_grants());
}
#[test]
fn interposed_permits_grants_and_is_still_not_the_kernel() {
assert!(Enforcement::Interposed.permits_grants());
assert!(Enforcement::Kernel.permits_grants());
assert!(!Enforcement::None.permits_grants());
assert_ne!(Enforcement::Interposed, Enforcement::Kernel);
}
#[test]
fn running_unconfined_is_a_claim_rather_than_a_silence() {
let ran_unconfined = Isolation::unconfined(Tier::InProcess);
let recorded: Option<Isolation> = Some(ran_unconfined);
let unrecorded: Option<Isolation> = None;
assert_ne!(recorded, unrecorded);
assert!(ran_unconfined.enforces_nothing());
assert!(!ran_unconfined.is_kernel_enforced());
}
#[test]
fn only_a_virtualising_tier_satisfies_profile_c_after_the_fact() {
assert!(!Isolation::unconfined(Tier::InProcess).is_virtualising());
assert!(!Isolation::new(Tier::Subprocess, kernel_everywhere()).is_virtualising());
assert!(Isolation::new(Tier::Wasm, kernel_everywhere()).is_virtualising());
}
#[test]
fn one_interposed_domain_defeats_a_kernel_claim() {
let mut confinement = kernel_everywhere();
assert!(Isolation::new(Tier::Subprocess, confinement).is_kernel_enforced());
confinement.secret = Enforcement::Interposed;
assert!(!Isolation::new(Tier::Subprocess, confinement).is_kernel_enforced());
}
#[test]
fn the_rendering_names_every_domain_including_the_unenforced_ones() {
let rendered = Isolation::unconfined(Tier::InProcess).to_string();
assert_eq!(
rendered,
"inproc (fs=none net=none proc=none env=none secret=none)"
);
for domain in ["fs", "net", "proc", "env", "secret"] {
assert!(
rendered.contains(domain),
"{domain} missing from {rendered}"
);
}
}
#[test]
fn a_reused_worker_is_named_and_a_child_of_its_own_is_not() {
let cold = Isolation::new(Tier::Subprocess, kernel_everywhere());
let warm = cold.served_by(Worker::new(3, 9));
assert!(!cold.was_warm());
assert!(warm.was_warm());
assert_ne!(cold, warm, "same confinement, different history");
let worker = warm.worker.expect("a warm step names its worker");
assert_eq!(worker.generation, 3);
assert_eq!(worker.served, 9);
assert!(!worker.is_first());
assert!(Worker::new(1, 0).is_first());
}
#[test]
fn a_generation_is_not_a_slot() {
assert_ne!(Worker::new(1, 0), Worker::new(2, 0));
assert!(Worker::new(1, 5) < Worker::new(2, 0), "ordered by age");
}
#[test]
fn the_rendering_names_the_worker_only_when_there_was_one() {
let cold = Isolation::new(Tier::Subprocess, Confinement::NONE);
assert!(!cold.to_string().contains("worker"));
assert!(
cold.served_by(Worker::new(2, 4))
.to_string()
.ends_with("worker 2 after 4 call(s)")
);
}
#[test]
fn an_isolation_record_round_trips_through_json() {
for isolation in [
Isolation::unconfined(Tier::InProcess),
Isolation::new(Tier::Subprocess, kernel_everywhere()),
Isolation::new(Tier::Subprocess, kernel_everywhere()).served_by(Worker::new(7, 2)),
Isolation::new(Tier::Wasm, Confinement::NONE),
] {
let json = serde_json::to_string(&isolation).unwrap();
assert_eq!(serde_json::from_str::<Isolation>(&json).unwrap(), isolation);
}
}
}