use crate::backend::linux::launch::AuthorityFd;
use crate::backend::linux::protocol::{
DescriptorKind, DescriptorRole, DescriptorShape, DescriptorSlotV1, LinuxLaunchBodyV1,
LinuxLaunchPlanV1, LoweringWireV1, NetworkNsRequest, SeccompRequest, TargetSpecV1,
UserNsRequest,
};
use crate::contract::capability::{EvidenceSet, FsAccess, PathSet, SupportVerdict};
use crate::contract::ids::{
AdmissionProgramHash, AttemptId, BackendProfileHash, BoundaryPlanHash, Digest32,
};
use crate::contract::lowering::compile_schedule;
use crate::contract::plan::{BoundaryPlan, BoundaryRequirement};
use crate::contract::primitive::{
LoweringPhase, PrimitiveDecl, PrimitiveId, PrimitiveVersion, Privilege,
};
use crate::contract::report::BoundaryReportBody;
use crate::contract::support::{BackendProfile, RequirementKind};
use std::os::fd::{OwnedFd, RawFd};
const SYSTEM_EXEC_ROOTS: &[&str] = &["/usr", "/lib", "/lib64", "/bin", "/sbin", "/etc"];
const ID_AMBIENT_SCRUB: &str = "linux.ambient.scrub.v1";
const ID_LANDLOCK_APPLY: &str = "linux.landlock.apply.v1";
const ID_SECCOMP_APPLY: &str = "linux.seccomp.apply.v1";
const ID_EXEC: &str = "linux.exec.v1";
const SCHEDULE_PRIMITIVE_VERSION: u32 = 1;
const SLOT_EXE: RawFd = 10;
const SLOT_READ_ROOT: RawFd = 15;
const SLOT_WRITE_ROOT: RawFd = 16;
const SLOT_CGROUP: RawFd = 17;
const SLOT_SYS_ROOT_BASE: RawFd = 20;
pub(super) struct Prepared {
pub(super) launch_plan: LinuxLaunchPlanV1,
pub(super) authority: Vec<AuthorityFd>,
pub(super) read_roots: Vec<String>,
pub(super) write_roots: Vec<String>,
pub(super) confined: bool,
}
pub(super) struct LaunchInputs<'a> {
pub(super) exe: &'a str,
pub(super) args: &'a [String],
pub(super) plan: &'a BoundaryPlan,
pub(super) fs: Option<&'a (FsAccess, PathSet)>,
pub(super) cgroup_dir_fd: Option<OwnedFd>,
pub(super) envp: Vec<(String, String)>,
pub(super) deny_network: bool,
pub(super) deny_new_tasks: bool,
}
pub(super) fn prepare_launch(inputs: LaunchInputs<'_>) -> Result<Prepared, String> {
let LaunchInputs {
exe,
args,
plan,
fs,
cgroup_dir_fd,
envp,
deny_network,
deny_new_tasks,
} = inputs;
let mut table: Vec<DescriptorSlotV1> = Vec::new();
let mut authority: Vec<AuthorityFd> = Vec::new();
let mut read_roots: Vec<String> = Vec::new();
let mut write_roots: Vec<String> = Vec::new();
let exe_handle = open_handle(exe)?;
authority.push(AuthorityFd {
slot_index: SLOT_EXE,
handle: exe_handle,
});
table.push(exe_slot());
let confined = fs.is_some();
if let Some((access, scope)) = fs {
let writable = matches!(access, FsAccess::Write | FsAccess::ReadWrite);
for path in &scope.roots {
let handle = open_handle(path)?;
let (slot, role) = if writable {
write_roots.push(path.clone());
(SLOT_WRITE_ROOT, DescriptorRole::WriteRoot)
} else {
read_roots.push(path.clone());
(SLOT_READ_ROOT, DescriptorRole::ReadRoot)
};
authority.push(AuthorityFd {
slot_index: slot,
handle,
});
table.push(root_slot(slot, role));
}
let mut sys_i: RawFd = 0;
for sys_root in SYSTEM_EXEC_ROOTS {
if !std::path::Path::new(sys_root).is_dir() {
continue;
}
let handle = open_handle(sys_root)?;
let slot = SLOT_SYS_ROOT_BASE
.checked_add(sys_i)
.ok_or_else(|| "system-exec root slot overflow".to_string())?;
authority.push(AuthorityFd {
slot_index: slot,
handle,
});
table.push(root_slot(slot, DescriptorRole::ReadRoot));
read_roots.push((*sys_root).to_string());
sys_i += 1;
}
}
if let Some(fd) = cgroup_dir_fd {
authority.push(AuthorityFd {
slot_index: SLOT_CGROUP,
handle: fd,
});
table.push(cgroup_slot());
}
let seccomp_request = seccomp_request(deny_new_tasks, deny_network);
let lowering = compile_lowering(confined, seccomp_request.is_some())?;
let body = build_body(BuildBody {
plan,
lowering,
table,
exe,
args,
envp,
deny_network,
seccomp_request,
})?;
Ok(Prepared {
launch_plan: LinuxLaunchPlanV1 { body },
authority,
read_roots,
write_roots,
confined,
})
}
fn compile_lowering(confined: bool, seccomp: bool) -> Result<LoweringWireV1, String> {
let scrub = LauncherActionDecl::new(ID_AMBIENT_SCRUB, LoweringPhase::FdHygiene);
let landlock = LauncherActionDecl::new(ID_LANDLOCK_APPLY, LoweringPhase::PolicyInstall);
let seccomp_apply = LauncherActionDecl::new(ID_SECCOMP_APPLY, LoweringPhase::PolicyInstall);
let exec = LauncherActionDecl::new(ID_EXEC, LoweringPhase::Launch);
let mut decls: Vec<&dyn PrimitiveDecl> = vec![&scrub];
if confined {
decls.push(&landlock);
}
if seccomp {
decls.push(&seccomp_apply);
}
decls.push(&exec);
let schedule = compile_schedule(&decls)
.map_err(|e| format!("cannot compile the lowering schedule: {e}"))?;
Ok(LoweringWireV1::from_schedule(&schedule))
}
struct LauncherActionDecl {
id: PrimitiveId,
phase: LoweringPhase,
}
impl LauncherActionDecl {
fn new(id: &str, phase: LoweringPhase) -> Self {
Self {
id: PrimitiveId::new(id),
phase,
}
}
}
impl PrimitiveDecl for LauncherActionDecl {
fn id(&self) -> PrimitiveId {
self.id.clone()
}
fn version(&self) -> PrimitiveVersion {
PrimitiveVersion::new(SCHEDULE_PRIMITIVE_VERSION)
}
fn covers(&self) -> &[RequirementKind] {
&[]
}
fn classify(&self, _req: &BoundaryRequirement, _profile: &BackendProfile) -> SupportVerdict {
SupportVerdict::unsupported()
}
fn phase(&self) -> LoweringPhase {
self.phase
}
fn prerequisites(&self) -> &[PrimitiveId] {
&[]
}
fn conflicts(&self) -> &[PrimitiveId] {
&[]
}
fn required_privileges(&self) -> &[Privilege] {
&[]
}
fn witness(&self, _observed: &BoundaryReportBody, _out: &mut EvidenceSet) {}
}
fn seccomp_request(deny_new_tasks: bool, deny_network: bool) -> Option<SeccompRequest> {
if !deny_new_tasks && !deny_network {
return None;
}
let request = SeccompRequest {
deny_new_tasks,
deny_inet_sockets: deny_network,
};
request.denies_anything().then_some(request)
}
fn open_handle(path: &str) -> Result<OwnedFd, String> {
std::fs::File::open(path)
.map(OwnedFd::from)
.map_err(|e| format!("cannot open authority path {path}: {e}"))
}
fn exe_slot() -> DescriptorSlotV1 {
DescriptorSlotV1 {
slot_index: slot_u32(SLOT_EXE),
role: DescriptorRole::TargetExe,
expected: DescriptorShape {
kind: DescriptorKind::Regular,
writable: false,
},
}
}
fn root_slot(fd: RawFd, role: DescriptorRole) -> DescriptorSlotV1 {
DescriptorSlotV1 {
slot_index: slot_u32(fd),
role,
expected: DescriptorShape {
kind: DescriptorKind::Directory,
writable: false,
},
}
}
fn cgroup_slot() -> DescriptorSlotV1 {
DescriptorSlotV1 {
slot_index: slot_u32(SLOT_CGROUP),
role: DescriptorRole::CgroupDir,
expected: DescriptorShape {
kind: DescriptorKind::Directory,
writable: false,
},
}
}
struct BuildBody<'a> {
plan: &'a BoundaryPlan,
lowering: LoweringWireV1,
table: Vec<DescriptorSlotV1>,
exe: &'a str,
args: &'a [String],
envp: Vec<(String, String)>,
deny_network: bool,
seccomp_request: Option<SeccompRequest>,
}
fn build_body(b: BuildBody<'_>) -> Result<LinuxLaunchBodyV1, String> {
let BuildBody {
plan,
lowering,
table,
exe,
args,
envp,
deny_network,
seccomp_request,
} = b;
let lowering_bytes = batpak::canonical::to_bytes(&lowering)
.map_err(|e| format!("cannot canonically encode the lowering schedule: {e}"))?;
let h_l: Digest32 = batpak::event::hash::compute_hash(&lowering_bytes);
let profile_bytes = batpak::canonical::to_bytes(&plan.profile)
.map_err(|e| format!("cannot canonically encode the profile snapshot: {e}"))?;
let h_p = BackendProfileHash::of(&profile_bytes);
let mut argv = Vec::with_capacity(args.len() + 1);
argv.push(exe.to_string());
argv.extend(args.iter().cloned());
Ok(LinuxLaunchBodyV1 {
attempt_id: AttemptId(derive_id(plan.plan_id, b"bvisor.attempt.v1")),
plan_id: plan.plan_id,
h_a: AdmissionProgramHash(derive_id(plan.plan_id, b"bvisor.h_a.v1")),
h_p,
h_l,
lowering,
descriptor_table: table,
target: TargetSpecV1 {
argv,
envp,
exe_slot: slot_u32(SLOT_EXE),
user_namespace: deny_network.then(UserNsRequest::new),
network_namespace: deny_network.then(NetworkNsRequest::new),
seccomp: seccomp_request,
},
})
}
fn derive_id(plan_id: BoundaryPlanHash, domain: &[u8]) -> Digest32 {
let mut framed = Vec::with_capacity(domain.len() + 1 + 32);
framed.extend_from_slice(domain);
framed.push(0u8); framed.extend_from_slice(&plan_id.0);
batpak::event::hash::compute_hash(&framed)
}
fn slot_u32(fd: RawFd) -> u32 {
u32::try_from(fd).unwrap_or(u32::MAX)
}
#[cfg(test)]
mod schedule_wire_tests {
use super::{compile_lowering, ID_AMBIENT_SCRUB, ID_EXEC, ID_LANDLOCK_APPLY, ID_SECCOMP_APPLY};
const PHASE_SCRUB: u8 = 3;
const PHASE_CONFINE: u8 = 4;
const PHASE_EXEC: u8 = 5;
#[test]
fn compiled_wire_matches_the_launcher_served_shape() {
let wire = compile_lowering(true, true).expect("schedule compiles");
let shape: Vec<(&str, u8)> = wire
.entries
.iter()
.map(|e| (e.id.as_str(), e.phase_code))
.collect();
assert_eq!(
shape,
vec![
(ID_AMBIENT_SCRUB, PHASE_SCRUB),
(ID_LANDLOCK_APPLY, PHASE_CONFINE),
(ID_SECCOMP_APPLY, PHASE_CONFINE),
(ID_EXEC, PHASE_EXEC),
],
"PROPERTY: the compiled+projected wire IS the launcher's canonical served shape"
);
}
#[test]
fn unconfined_schedule_is_scrub_then_exec_only() {
let wire = compile_lowering(false, false).expect("schedule compiles");
let ids: Vec<&str> = wire.entries.iter().map(|e| e.id.as_str()).collect();
assert_eq!(
ids,
vec![ID_AMBIENT_SCRUB, ID_EXEC],
"PROPERTY: a no-confinement plan schedules only the mandatory scrub + exec"
);
}
#[test]
fn projected_entries_carry_real_compiled_digests_not_zero_sentinels() {
let wire = compile_lowering(true, true).expect("schedule compiles");
for entry in &wire.entries {
assert_ne!(
entry.decl_digest, [0u8; 32],
"PROPERTY: decl_digest is the real compiled digest, never a zero sentinel"
);
assert_ne!(
entry.param_digest, [0u8; 32],
"PROPERTY: param_digest is the real compiled (empty-param) digest, never zero"
);
}
}
}