#![cfg(all(target_os = "linux", feature = "backend-linux"))]
use bvisor::linux::launch::transcript_confinement_unavailable;
use bvisor::linux::protocol::{
DescriptorKind, DescriptorRole, DescriptorShape, DescriptorSlotV1, LinuxLaunchBodyV1,
LinuxLaunchPlanV1, LoweringWireEntryV1, LoweringWireV1, TargetSpecV1,
};
use bvisor::{AdmissionProgramHash, AttemptId, BackendProfileHash, BoundaryPlanHash};
use std::io::{Read, Write};
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
const ID_AMBIENT_SCRUB: &str = "linux.ambient.scrub.v1";
const ID_LANDLOCK_APPLY: &str = "linux.landlock.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 LANDLOCK_ABI_FLOOR: i64 = 3;
const FIXED_EXE_FD: RawFd = 10;
const FIXED_CONTROL_FD: RawFd = 11;
const FIXED_ERROR_WRITE_FD: RawFd = 12;
const FIXED_PLAN_FD: RawFd = 13;
const FIXED_ERROR_READ_FD: RawFd = 14;
const FIXED_READ_ROOT_FD: RawFd = 15; const FIXED_WRITE_ROOT_FD: RawFd = 16; const FIXED_SYS_ROOT_BASE: RawFd = 20;
const SYSTEM_EXEC_ROOTS: &[&str] = &["/usr", "/lib", "/lib64", "/bin", "/sbin", "/etc"];
fn live_landlock_abi() -> i64 {
const LANDLOCK_CREATE_RULESET_VERSION: libc::c_uint = 1;
let raw = unsafe {
libc::syscall(
libc::SYS_landlock_create_ruleset,
std::ptr::null::<libc::c_void>(),
0usize,
LANDLOCK_CREATE_RULESET_VERSION,
)
};
if raw < 0 {
0
} else {
raw
}
}
fn landlock_available() -> bool {
live_landlock_abi() >= LANDLOCK_ABI_FLOOR
}
struct Scratch {
root: PathBuf,
}
impl Scratch {
fn new(tag: &str) -> Self {
let pid = std::process::id();
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let root = std::env::temp_dir().join(format!("bvisor-launcher-fs-{tag}-{pid}-{nanos}"));
std::fs::create_dir_all(&root).expect("scratch root");
Self { root }
}
fn path(&self, name: &str) -> PathBuf {
self.root.join(name)
}
}
impl Drop for Scratch {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
struct FsGroundTruth {
marker: String,
witness_path: PathBuf,
}
impl FsGroundTruth {
fn danger_occurred(&self) -> bool {
match std::fs::read(&self.witness_path) {
Ok(bytes) => String::from_utf8_lossy(&bytes).contains(&self.marker),
Err(_) => false,
}
}
fn effect_landed(&self) -> bool {
self.danger_occurred()
}
}
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],
}
}
fn body_with(
lowering: LoweringWireV1,
table: Vec<DescriptorSlotV1>,
argv: Vec<String>,
) -> LinuxLaunchBodyV1 {
let bytes = batpak::canonical::to_bytes(&lowering).expect("encode lowering");
let h_l = batpak::event::hash::compute_hash(&bytes);
LinuxLaunchBodyV1 {
attempt_id: AttemptId([7u8; 32]),
plan_id: BoundaryPlanHash([1u8; 32]),
h_a: AdmissionProgramHash([2u8; 32]),
h_p: BackendProfileHash([3u8; 32]),
h_l,
lowering,
descriptor_table: table,
target: TargetSpecV1 {
argv,
envp: vec![("PATH".to_owned(), "/usr/bin:/bin".to_owned())],
exe_slot: u32::try_from(FIXED_EXE_FD).expect("fd fits u32"),
user_namespace: None,
network_namespace: None,
seccomp: None,
},
}
}
fn exe_slot() -> DescriptorSlotV1 {
DescriptorSlotV1 {
slot_index: u32::try_from(FIXED_EXE_FD).expect("fd"),
role: DescriptorRole::TargetExe,
expected: DescriptorShape {
kind: DescriptorKind::Regular,
writable: false,
},
}
}
fn root_slot(fd: RawFd, role: DescriptorRole) -> DescriptorSlotV1 {
DescriptorSlotV1 {
slot_index: u32::try_from(fd).expect("fd"),
role,
expected: DescriptorShape {
kind: DescriptorKind::Directory,
writable: false,
},
}
}
fn present_system_roots() -> Vec<&'static str> {
SYSTEM_EXEC_ROOTS
.iter()
.copied()
.filter(|p| Path::new(p).is_dir())
.collect()
}
fn confined_plan(argv: Vec<String>, n_sys: usize) -> LinuxLaunchPlanV1 {
let lowering = LoweringWireV1 {
entries: vec![
entry(ID_AMBIENT_SCRUB, PHASE_CODE_SCRUB),
entry(ID_LANDLOCK_APPLY, PHASE_CODE_CONFINE),
entry(ID_EXEC, PHASE_CODE_EXEC),
],
};
let mut table = vec![
exe_slot(),
root_slot(FIXED_READ_ROOT_FD, DescriptorRole::ReadRoot),
root_slot(FIXED_WRITE_ROOT_FD, DescriptorRole::WriteRoot),
];
for i in 0..n_sys {
let fd = FIXED_SYS_ROOT_BASE + RawFd::try_from(i).expect("fd");
table.push(root_slot(fd, DescriptorRole::ReadRoot));
}
LinuxLaunchPlanV1 {
body: body_with(lowering, table, argv),
}
}
fn unconfined_plan(argv: Vec<String>) -> LinuxLaunchPlanV1 {
let lowering = LoweringWireV1 {
entries: vec![
entry(ID_AMBIENT_SCRUB, PHASE_CODE_SCRUB),
entry(ID_EXEC, PHASE_CODE_EXEC),
],
};
LinuxLaunchPlanV1 {
body: body_with(lowering, vec![exe_slot()], argv),
}
}
fn open_sh() -> OwnedFd {
OwnedFd::from(std::fs::File::open("/bin/sh").expect("open /bin/sh"))
}
fn open_dir(path: &Path) -> OwnedFd {
OwnedFd::from(std::fs::File::open(path).expect("open dir"))
}
fn socketpair() -> (OwnedFd, OwnedFd) {
let mut fds = [0 as libc::c_int; 2];
let rc = unsafe {
libc::socketpair(
libc::AF_UNIX,
libc::SOCK_STREAM | libc::SOCK_CLOEXEC,
0,
fds.as_mut_ptr(),
)
};
assert_eq!(rc, 0, "socketpair");
unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) }
}
fn error_pipe() -> (OwnedFd, OwnedFd) {
let mut fds = [0 as libc::c_int; 2];
let rc = unsafe { libc::pipe2(fds.as_mut_ptr(), libc::O_CLOEXEC) };
assert_eq!(rc, 0, "pipe2");
unsafe { (OwnedFd::from_raw_fd(fds[0]), OwnedFd::from_raw_fd(fds[1])) }
}
fn plan_fd(plan: &LinuxLaunchPlanV1) -> OwnedFd {
use std::io::{Seek, SeekFrom, Write};
let bytes = plan.encode().expect("encode plan");
let mut f = tempfile::tempfile().expect("tempfile");
f.write_all(&bytes).expect("write plan");
f.seek(SeekFrom::Start(0)).expect("rewind");
OwnedFd::from(f)
}
const FD_RELOCATE_BASE: RawFd = 100;
fn relocate_high(fd: OwnedFd) -> OwnedFd {
let new = unsafe { libc::fcntl(fd.as_raw_fd(), libc::F_DUPFD_CLOEXEC, FD_RELOCATE_BASE) };
assert!(new >= FD_RELOCATE_BASE, "F_DUPFD_CLOEXEC relocate");
let relocated = unsafe { OwnedFd::from_raw_fd(new) };
drop(fd); relocated
}
fn dup_to(src: RawFd, target: RawFd, keep_cloexec: bool) -> std::io::Result<()> {
unsafe {
if libc::dup2(src, target) < 0 {
return Err(std::io::Error::last_os_error());
}
if !keep_cloexec {
let flags = libc::fcntl(target, libc::F_GETFD);
if flags >= 0 {
let _ = libc::fcntl(target, libc::F_SETFD, flags & !libc::FD_CLOEXEC);
}
}
}
Ok(())
}
struct Roots {
read: OwnedFd,
write: OwnedFd,
system: Vec<OwnedFd>,
}
fn spawn_launcher(
plan: &LinuxLaunchPlanV1,
exe_fd: OwnedFd,
roots: Option<Roots>,
) -> (std::process::Child, OwnedFd) {
static SPAWN_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
let _guard = SPAWN_LOCK.lock().unwrap_or_else(|p| p.into_inner());
let exe_fd = relocate_high(exe_fd);
let pfd = relocate_high(plan_fd(plan));
let (control_launcher, control_test) = socketpair();
let (error_read, error_write) = error_pipe();
let control_launcher = relocate_high(control_launcher);
let error_write = relocate_high(error_write);
let error_read = relocate_high(error_read);
let roots = roots.map(|r| Roots {
read: relocate_high(r.read),
write: relocate_high(r.write),
system: r.system.into_iter().map(relocate_high).collect(),
});
let exe_raw = exe_fd.as_raw_fd();
let control_raw = control_launcher.as_raw_fd();
let error_w_raw = error_write.as_raw_fd();
let error_r_raw = error_read.as_raw_fd();
let plan_raw = pfd.as_raw_fd();
let (read_raw, write_raw, sys_raw): (Option<RawFd>, Option<RawFd>, Vec<RawFd>) = match &roots {
Some(r) => (
Some(r.read.as_raw_fd()),
Some(r.write.as_raw_fd()),
r.system.iter().map(|f| f.as_raw_fd()).collect(),
),
None => (None, None, Vec::new()),
};
let mut cmd = Command::new(env!("CARGO_BIN_EXE_bvisor-linux-launcher"));
cmd.env_clear()
.env("BVISOR_LAUNCH_PLAN_FD", FIXED_PLAN_FD.to_string())
.env("BVISOR_CONTROL_FD", FIXED_CONTROL_FD.to_string())
.env("BVISOR_ERROR_FD", FIXED_ERROR_WRITE_FD.to_string())
.env("BVISOR_ERROR_READ_FD", FIXED_ERROR_READ_FD.to_string());
unsafe {
cmd.pre_exec(move || {
dup_to(exe_raw, FIXED_EXE_FD, false)?;
dup_to(control_raw, FIXED_CONTROL_FD, false)?;
dup_to(plan_raw, FIXED_PLAN_FD, false)?;
dup_to(error_r_raw, FIXED_ERROR_READ_FD, false)?;
if let Some(rr) = read_raw {
dup_to(rr, FIXED_READ_ROOT_FD, false)?;
}
if let Some(wr) = write_raw {
dup_to(wr, FIXED_WRITE_ROOT_FD, false)?;
}
let mut i = 0;
while i < sys_raw.len() {
let offset = RawFd::try_from(i).unwrap_or(RawFd::MAX);
dup_to(sys_raw[i], FIXED_SYS_ROOT_BASE + offset, false)?;
i += 1;
}
if libc::dup2(error_w_raw, FIXED_ERROR_WRITE_FD) < 0 {
return Err(std::io::Error::last_os_error());
}
let flags = libc::fcntl(FIXED_ERROR_WRITE_FD, libc::F_GETFD);
if flags >= 0 {
let _ = libc::fcntl(
FIXED_ERROR_WRITE_FD,
libc::F_SETFD,
flags | libc::FD_CLOEXEC,
);
}
Ok(())
});
}
let child = cmd.spawn().expect("spawn launcher");
drop(control_launcher);
drop(error_write);
drop(error_read);
drop(pfd);
drop(exe_fd);
drop(roots);
(child, control_test)
}
fn read_all(fd: OwnedFd) -> String {
let mut f = std::fs::File::from(fd);
let mut s = String::new();
let _ = f.read_to_string(&mut s);
s
}
#[test]
fn launcher_landlock_denies_secret_read_and_escape_write_allows_in_root() {
if !landlock_available() {
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP launcher_landlock G1/G3: live landlock ABI {} < floor {} \
(kernel lacks landlock or the sandbox blocks it)",
live_landlock_abi(),
LANDLOCK_ABI_FLOOR
);
return;
}
let scratch = Scratch::new("g1g3");
let quarantine = scratch.path("quarantine");
std::fs::create_dir_all(&quarantine).expect("quarantine dir");
let readable = scratch.path("readable");
std::fs::create_dir_all(&readable).expect("readable dir");
let in_root_src = readable.join("inside.txt");
let control_marker = "CONTROL-IN-ROOT-MARKER".to_string();
std::fs::write(&in_root_src, &control_marker).expect("plant in-root source");
let secret = scratch.path("secret.key");
let secret_marker = "TOPSECRET-LAUNCHER-G1".to_string();
std::fs::write(&secret, &secret_marker).expect("plant secret");
let exfil = quarantine.join("exfil.txt"); let control_sink = quarantine.join("control.txt");
let escape = scratch.path("escape.txt");
let escape_marker = "ESCAPED-LAUNCHER-G3".to_string();
let cmd = format!(
"cat {secret} > {exfil} ; echo {escape_marker} > {escape} ; cat {src} > {control_sink}",
secret = secret.to_string_lossy(),
exfil = exfil.to_string_lossy(),
escape_marker = escape_marker,
escape = escape.to_string_lossy(),
src = in_root_src.to_string_lossy(),
control_sink = control_sink.to_string_lossy(),
);
let sys_dirs = present_system_roots();
let plan = confined_plan(vec!["sh".into(), "-c".into(), cmd], sys_dirs.len());
let roots = Roots {
read: open_dir(&readable),
write: open_dir(&quarantine),
system: sys_dirs.iter().map(|p| open_dir(Path::new(p))).collect(),
};
let (mut child, control) = spawn_launcher(&plan, open_sh(), Some(roots));
let transcript = read_all(control);
let _ = child.wait();
if transcript_confinement_unavailable(&transcript) {
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP launcher_landlock_denies_secret_read_and_escape_write_allows_in_root: \
kernel/container lacks landlock/userns/seccomp (ENOSYS); the launcher faulted before \
exec — exercised on capable kernels + the bvisor-linux CI lane"
);
return;
}
let g1 = FsGroundTruth {
marker: secret_marker,
witness_path: exfil,
};
assert!(
!g1.danger_occurred(),
"G1: landlock must block the out-of-root secret READ; the secret leaked into \
the in-root exfil sink on disk. transcript:\n{transcript}"
);
let g3 = FsGroundTruth {
marker: escape_marker,
witness_path: escape,
};
assert!(
!g3.danger_occurred(),
"G3: landlock must block the out-of-quarantine WRITE; the escape file exists \
on disk. transcript:\n{transcript}"
);
let control = FsGroundTruth {
marker: control_marker,
witness_path: control_sink,
};
assert!(
control.effect_landed(),
"CONTROL: an in-root read→in-root write must be ALLOWED through landlock (the \
deny tests above are otherwise vacuous). transcript:\n{transcript}"
);
assert!(
transcript.contains("ConfinementPhaseResolved"),
"transcript must resolve the Confinement phase: {transcript}"
);
assert!(
transcript.contains("installed=true"),
"transcript must record REAL confinement evidence (installed=true): {transcript}"
);
assert!(
transcript.trim_end().ends_with("ExecSucceeded"),
"transcript must end ExecSucceeded (the workload ran, confined): {transcript}"
);
}
#[test]
fn launcher_without_landlock_lets_the_escape_land() {
let scratch = Scratch::new("novacuous");
let escape = scratch.path("escape.txt");
let marker = "ESCAPED-NOLANDLOCK-MARKER".to_string();
let cmd = format!(
"echo {marker} > {escape}",
marker = marker,
escape = escape.to_string_lossy(),
);
let plan = unconfined_plan(vec!["sh".into(), "-c".into(), cmd]);
let (mut child, control) = spawn_launcher(&plan, open_sh(), None);
let transcript = read_all(control);
let _ = child.wait();
if transcript_confinement_unavailable(&transcript) {
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP launcher_without_landlock_lets_the_escape_land: kernel/container lacks \
landlock/userns/seccomp (ENOSYS); the launcher faulted before exec — exercised on \
capable kernels + the bvisor-linux CI lane"
);
return;
}
let gt = FsGroundTruth {
marker,
witness_path: escape,
};
assert!(
gt.danger_occurred(),
"NON-VACUOUS: an UNCONFINED launch must let the escape write land on disk \
(so the confined G3 deny is meaningful). transcript:\n{transcript}"
);
assert!(
transcript.contains("installed=false"),
"an exec-only plan must report installed=false (no over-claim): {transcript}"
);
}