use crate::backend::linux::launch::AuthorityFd;
use crate::backend::linux::protocol::{
DescriptorKind, DescriptorRole, DescriptorShape, DescriptorSlotV1, LinuxLaunchBodyV1,
LinuxLaunchPlanV1, LoweringWireEntryV1, LoweringWireV1, NetworkNsRequest, SeccompRequest,
TargetSpecV1, UserNsRequest,
};
use crate::contract::capability::{FsAccess, PathSet};
use crate::contract::ids::{
AdmissionProgramHash, AttemptId, BackendProfileHash, BoundaryPlanHash, Digest32,
};
use crate::contract::plan::BoundaryPlan;
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 PHASE_CODE_SCRUB: u8 = 3;
const PHASE_CODE_CONFINE: u8 = 4;
const PHASE_CODE_EXEC: u8 = 5;
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 mut entries = vec![entry(ID_AMBIENT_SCRUB, PHASE_CODE_SCRUB)];
if confined {
entries.push(entry(ID_LANDLOCK_APPLY, PHASE_CODE_CONFINE));
}
if seccomp_request.is_some() {
entries.push(entry(ID_SECCOMP_APPLY, PHASE_CODE_CONFINE));
}
entries.push(entry(ID_EXEC, PHASE_CODE_EXEC));
let lowering = LoweringWireV1 { entries };
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 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,
},
}
}
fn entry(id: &str, phase_code: u8) -> LoweringWireEntryV1 {
LoweringWireEntryV1 {
id: id.to_owned(),
version: 1,
phase_code,
param_digest: [0u8; 32],
decl_digest: [0u8; 32],
}
}
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)
}