use crate::backend::{Backend, SandboxBackend};
use crate::lifecycle::ExecResult;
use crate::policy::SandboxPolicy;
pub struct CompositeBackend {
outer: Box<dyn SandboxBackend>,
inner_policy: SandboxPolicy,
outer_backend: Backend,
inner_backend: Backend,
}
impl std::fmt::Debug for CompositeBackend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CompositeBackend")
.field("outer", &self.outer_backend)
.field("inner", &self.inner_backend)
.finish()
}
}
impl CompositeBackend {
pub fn new(
outer: Box<dyn SandboxBackend>,
outer_backend: Backend,
inner_backend: Backend,
inner_policy: SandboxPolicy,
) -> Self {
Self {
outer,
inner_policy,
outer_backend,
inner_backend,
}
}
#[must_use]
pub fn outer_type(&self) -> Backend {
self.outer_backend
}
#[must_use]
pub fn inner_type(&self) -> Backend {
self.inner_backend
}
}
#[async_trait::async_trait]
impl SandboxBackend for CompositeBackend {
fn backend_type(&self) -> Backend {
self.outer_backend
}
async fn exec(&self, command: &str, policy: &SandboxPolicy) -> crate::Result<ExecResult> {
let merged = merge_policies(policy, &self.inner_policy);
tracing::debug!(
outer = %self.outer_backend,
inner = %self.inner_backend,
"composite exec with merged policy"
);
self.outer.exec(command, &merged).await
}
async fn health_check(&self) -> crate::Result<bool> {
self.outer.health_check().await
}
async fn spawn(
&self,
command: &str,
policy: &SandboxPolicy,
) -> crate::Result<Option<crate::backend::exec_util::SpawnedProcess>> {
let merged = merge_policies(policy, &self.inner_policy);
self.outer.spawn(command, &merged).await
}
async fn destroy(&self) -> crate::Result<()> {
self.outer.destroy().await
}
}
#[must_use]
pub fn merge_policies(base: &SandboxPolicy, overlay: &SandboxPolicy) -> SandboxPolicy {
SandboxPolicy {
seccomp_enabled: base.seccomp_enabled || overlay.seccomp_enabled,
seccomp_profile: match (&base.seccomp_profile, &overlay.seccomp_profile) {
(Some(b), Some(o)) => {
Some(if o == "strict" || b == "strict" {
"strict".into()
} else {
b.clone()
})
}
(Some(p), None) | (None, Some(p)) => Some(p.clone()),
(None, None) => None,
},
landlock_rules: {
let mut rules = base.landlock_rules.clone();
rules.extend(overlay.landlock_rules.iter().cloned());
rules
},
network: crate::policy::NetworkPolicy {
enabled: base.network.enabled && overlay.network.enabled,
allowed_hosts: intersect_or_nonempty(
&base.network.allowed_hosts,
&overlay.network.allowed_hosts,
),
allowed_ports: intersect_or_nonempty_ports(
&base.network.allowed_ports,
&overlay.network.allowed_ports,
),
},
read_only_rootfs: base.read_only_rootfs || overlay.read_only_rootfs,
memory_limit_mb: match (base.memory_limit_mb, overlay.memory_limit_mb) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(v), None) | (None, Some(v)) => Some(v),
(None, None) => None,
},
cpu_limit: match (base.cpu_limit, overlay.cpu_limit) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(v), None) | (None, Some(v)) => Some(v),
(None, None) => None,
},
max_pids: match (base.max_pids, overlay.max_pids) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(v), None) | (None, Some(v)) => Some(v),
(None, None) => None,
},
data_dir: base.data_dir.clone().or_else(|| overlay.data_dir.clone()),
}
}
fn intersect_or_nonempty(a: &[String], b: &[String]) -> Vec<String> {
if a.is_empty() {
return b.to_vec();
}
if b.is_empty() {
return a.to_vec();
}
a.iter().filter(|h| b.contains(h)).cloned().collect()
}
fn intersect_or_nonempty_ports(a: &[u16], b: &[u16]) -> Vec<u16> {
if a.is_empty() {
return b.to_vec();
}
if b.is_empty() {
return a.to_vec();
}
a.iter().filter(|p| b.contains(p)).copied().collect()
}
#[must_use]
pub fn score_composite(
outer: Backend,
inner: Backend,
policy: &SandboxPolicy,
) -> crate::scoring::StrengthScore {
let outer_score = crate::scoring::score_backend(outer, policy).value();
let inner_score = crate::scoring::score_backend(inner, policy).value();
let composite = outer_score.max(inner_score).saturating_add(5);
crate::scoring::StrengthScore(composite.min(100))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::backend::NoopBackend;
use crate::policy::LandlockRule;
#[test]
fn merge_seccomp_strict_wins() {
let base = SandboxPolicy {
seccomp_enabled: true,
seccomp_profile: Some("basic".into()),
..Default::default()
};
let overlay = SandboxPolicy {
seccomp_enabled: true,
seccomp_profile: Some("strict".into()),
..Default::default()
};
let merged = merge_policies(&base, &overlay);
assert!(merged.seccomp_enabled);
assert_eq!(merged.seccomp_profile.as_deref(), Some("strict"));
}
#[test]
fn merge_seccomp_either_enables() {
let off = SandboxPolicy::default();
let on = SandboxPolicy {
seccomp_enabled: true,
seccomp_profile: Some("basic".into()),
..Default::default()
};
let merged = merge_policies(&off, &on);
assert!(merged.seccomp_enabled);
}
#[test]
fn merge_network_stricter() {
let net_on = SandboxPolicy {
network: crate::policy::NetworkPolicy {
enabled: true,
..Default::default()
},
..Default::default()
};
let net_off = SandboxPolicy::default(); let merged = merge_policies(&net_on, &net_off);
assert!(!merged.network.enabled);
}
#[test]
fn merge_resource_limits_min() {
let a = SandboxPolicy {
memory_limit_mb: Some(1024),
cpu_limit: Some(2.0),
max_pids: Some(128),
..Default::default()
};
let b = SandboxPolicy {
memory_limit_mb: Some(512),
cpu_limit: Some(4.0),
max_pids: Some(64),
..Default::default()
};
let merged = merge_policies(&a, &b);
assert_eq!(merged.memory_limit_mb, Some(512));
assert_eq!(merged.cpu_limit, Some(2.0));
assert_eq!(merged.max_pids, Some(64));
}
#[test]
fn merge_landlock_concatenated() {
let a = SandboxPolicy {
landlock_rules: vec![LandlockRule {
path: "/tmp".into(),
access: "rw".into(),
}],
..Default::default()
};
let b = SandboxPolicy {
landlock_rules: vec![LandlockRule {
path: "/var".into(),
access: "ro".into(),
}],
..Default::default()
};
let merged = merge_policies(&a, &b);
assert_eq!(merged.landlock_rules.len(), 2);
}
#[test]
fn merge_readonly_rootfs() {
let a = SandboxPolicy::default();
let b = SandboxPolicy {
read_only_rootfs: true,
..Default::default()
};
let merged = merge_policies(&a, &b);
assert!(merged.read_only_rootfs);
}
#[tokio::test]
async fn composite_exec_noop() {
let outer = Box::new(NoopBackend);
let inner_policy = SandboxPolicy::strict();
let composite = CompositeBackend::new(outer, Backend::Noop, Backend::Process, inner_policy);
let policy = SandboxPolicy::basic();
let result = composite.exec("echo hello", &policy).await.unwrap();
assert_eq!(result.exit_code, 0);
}
#[tokio::test]
async fn composite_health_check() {
let outer = Box::new(NoopBackend);
let composite = CompositeBackend::new(
outer,
Backend::Noop,
Backend::Process,
SandboxPolicy::default(),
);
assert!(composite.health_check().await.unwrap());
}
#[tokio::test]
async fn composite_destroy() {
let outer = Box::new(NoopBackend);
let composite = CompositeBackend::new(
outer,
Backend::Noop,
Backend::Process,
SandboxPolicy::default(),
);
composite.destroy().await.unwrap();
}
#[test]
fn composite_debug() {
let outer = Box::new(NoopBackend);
let composite = CompositeBackend::new(
outer,
Backend::Firecracker,
Backend::Process,
SandboxPolicy::default(),
);
let debug = format!("{composite:?}");
assert!(debug.contains("Firecracker"));
assert!(debug.contains("Process"));
}
#[test]
fn composite_score_bonus() {
let policy = SandboxPolicy::strict();
let outer_only = crate::scoring::score_backend(Backend::GVisor, &policy);
let composite = score_composite(Backend::GVisor, Backend::Process, &policy);
assert!(composite.value() > outer_only.value());
}
#[test]
fn composite_score_clamped() {
let policy = SandboxPolicy::strict();
let score = score_composite(Backend::Firecracker, Backend::Process, &policy);
assert!(score.value() <= 100);
}
#[test]
fn outer_inner_types() {
let outer = Box::new(NoopBackend);
let composite = CompositeBackend::new(
outer,
Backend::GVisor,
Backend::Process,
SandboxPolicy::default(),
);
assert_eq!(composite.outer_type(), Backend::GVisor);
assert_eq!(composite.inner_type(), Backend::Process);
}
}