#![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;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::os::unix::process::CommandExt;
use std::process::Command;
const ID_AMBIENT_SCRUB: &str = "linux.ambient.scrub.v1";
const ID_EXEC: &str = "linux.exec.v1";
const PHASE_CODE_SCRUB: u8 = 3; const PHASE_CODE_EXEC: u8 = 5;
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;
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".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 happy_plan() -> 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()], vec!["true".to_owned()]),
}
}
fn open_true() -> OwnedFd {
let file = std::fs::File::open("/bin/true").expect("open /bin/true");
OwnedFd::from(file)
}
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(())
}
fn spawn_launcher(plan: &LinuxLaunchPlanV1, exe_fd: OwnedFd) -> (std::process::Child, OwnedFd) {
spawn_with_plan_fd(plan_fd(plan), exe_fd)
}
fn spawn_with_plan_fd(pfd: OwnedFd, exe_fd: OwnedFd) -> (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(pfd);
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 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 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 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);
(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
}
fn raw_plan_fd(bytes: &[u8]) -> OwnedFd {
use std::io::{Seek, SeekFrom, Write};
let mut f = tempfile::tempfile().expect("tempfile");
f.write_all(bytes).expect("write plan bytes");
f.seek(SeekFrom::Start(0)).expect("rewind");
OwnedFd::from(f)
}
#[test]
fn happy_path_execs_a_real_child_and_reports_success() {
let plan = happy_plan();
let exe = open_true();
let (mut child, control) = spawn_launcher(&plan, exe);
let transcript = read_all(control);
let status = child.wait().expect("wait launcher");
if transcript_confinement_unavailable(&transcript) {
use std::io::Write as _;
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP happy_path_execs_a_real_child_and_reports_success: kernel/container lacks \
landlock/userns/seccomp (ENOSYS); the launcher faulted before exec — exercised on \
capable kernels + the bvisor-linux CI lane"
);
return;
}
let coordinator_pid = i64::from(child.id());
assert!(
transcript.contains("LauncherStarted"),
"transcript should start: {transcript}"
);
let child_pid = child_pid_from(&transcript);
assert!(
transcript.contains("ChildCreated"),
"a real child was created: {transcript}"
);
assert!(
child_pid != coordinator_pid && child_pid > 0,
"the workload child pid ({child_pid}) must differ from the coordinator pid \
({coordinator_pid}) — proves a real clone3 child, NOT a self-exec"
);
assert!(
transcript.trim_end().ends_with("ExecSucceeded"),
"transcript must end ExecSucceeded (⟺ error pipe EOF, no errno): {transcript}"
);
assert!(
!transcript.contains("errno="),
"no child errno on exec success: {transcript}"
);
assert!(status.success(), "launcher exit 0 on success: {status:?}");
}
#[test]
fn missing_primitive_refuses_before_any_child() {
let lowering = LoweringWireV1 {
entries: vec![
entry("linux.landlock.v1", 4),
entry(ID_EXEC, PHASE_CODE_EXEC),
],
};
let plan = LinuxLaunchPlanV1 {
body: body_with(lowering, vec![exe_slot()], vec!["true".to_owned()]),
};
let exe = open_true();
let (mut child, control) = spawn_launcher(&plan, exe);
let transcript = read_all(control);
let _ = child.wait();
assert!(
transcript.contains("SetupRefused"),
"must refuse: {transcript}"
);
assert!(
transcript.contains("MissingPrimitive"),
"reason MissingPrimitive: {transcript}"
);
assert!(
!transcript.contains("ChildCreated"),
"NO child on refusal: {transcript}"
);
}
#[test]
fn handle_mismatch_refuses() {
let plan = happy_plan(); let dir_fd = OwnedFd::from(std::fs::File::open("/").expect("open /"));
let (mut child, control) = spawn_launcher(&plan, dir_fd);
let transcript = read_all(control);
let _ = child.wait();
assert!(
transcript.contains("SetupRefused"),
"must refuse: {transcript}"
);
assert!(
transcript.contains("HandleMismatch"),
"reason HandleMismatch: {transcript}"
);
assert!(
!transcript.contains("ChildCreated"),
"NO child on handle mismatch: {transcript}"
);
}
#[test]
fn bad_plan_refuses() {
let plan = happy_plan();
let mut bytes = plan.encode().expect("encode");
bytes[0] ^= 0xFF; let exe = open_true();
let (mut child, control) = spawn_with_plan_fd(raw_plan_fd(&bytes), exe);
let transcript = read_all(control);
let _ = child.wait();
assert!(
transcript.contains("SetupRefused") || transcript.contains("SetupFaulted"),
"bad plan must refuse/fault: {transcript}"
);
assert!(
!transcript.contains("ChildCreated"),
"NO child on a bad plan: {transcript}"
);
}
const FIXED_EXTRA_FD: RawFd = 30;
fn sh_plan(script: 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()],
vec!["sh".to_owned(), "-c".to_owned(), script],
),
}
}
fn spawn_with_extra_fd(
plan: &LinuxLaunchPlanV1,
exe_fd: OwnedFd,
extra_fd: OwnedFd,
) -> (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 extra_fd = relocate_high(extra_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 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 extra_raw = extra_fd.as_raw_fd();
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)?;
dup_to(extra_raw, FIXED_EXTRA_FD, false)?;
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(extra_fd);
(child, control_test)
}
#[test]
fn scrub_closes_undeclared_inherited_fd_no_refusal() {
let dir = tempfile::tempdir().expect("tempdir");
let witness = dir.path().join("g6-witness.txt");
let witness_str = witness.to_string_lossy().into_owned();
let script = format!(
"if [ -e /proc/self/fd/{extra} ]; then echo EXTRA_OPEN; else echo EXTRA_CLOSED; fi > {w}; \
if [ -e /proc/self/fd/1 ]; then echo STDOUT_OPEN; else echo STDOUT_CLOSED; fi >> {w}",
extra = FIXED_EXTRA_FD,
w = witness_str,
);
let plan = sh_plan(script);
let extra = OwnedFd::from(std::fs::File::open("/bin/true").expect("open /bin/true"));
let (mut child, control) = spawn_with_extra_fd(&plan, open_sh_exe(), extra);
let transcript = read_all(control);
let status = child.wait().expect("wait launcher");
if transcript_confinement_unavailable(&transcript) {
use std::io::Write as _;
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP scrub_closes_undeclared_inherited_fd_no_refusal: kernel/container lacks \
landlock/userns/seccomp (ENOSYS); the launcher faulted before exec — exercised on \
capable kernels + the bvisor-linux CI lane"
);
return;
}
let recorded = std::fs::read_to_string(&witness).unwrap_or_default();
assert!(
recorded.contains("EXTRA_CLOSED"),
"G6: the scrub must CLOSE the undeclared inherited fd {FIXED_EXTRA_FD} (workload \
must see EBADF). witness:\n{recorded}\ntranscript:\n{transcript}"
);
assert!(
recorded.contains("STDOUT_OPEN"),
"NON-VACUOUS CONTROL: an ALLOWLISTED fd (stdout) must remain OPEN in the workload \
(the scrub is not a blanket close). witness:\n{recorded}\ntranscript:\n{transcript}"
);
assert!(
transcript.contains("ChildCreated"),
"the extra fd is scrubbed, not refused — a child must be created: {transcript}"
);
assert!(
!transcript.contains("SetupRefused"),
"an undeclared inherited fd must NOT cause a refusal (it is scrubbed): {transcript}"
);
assert!(
transcript.trim_end().ends_with("ExecSucceeded"),
"the workload ran to success (extra fd scrubbed): {transcript}"
);
assert!(status.success(), "launcher exit 0 on success: {status:?}");
}
fn open_sh_exe() -> OwnedFd {
OwnedFd::from(std::fs::File::open("/bin/sh").expect("open /bin/sh"))
}
fn child_pid_from(transcript: &str) -> i64 {
for line in transcript.lines() {
if let Some(rest) = line.split("child_pid=").nth(1) {
if let Ok(pid) = rest.trim().parse::<i64>() {
return pid;
}
}
}
-1
}