#![cfg(all(
feature = "backend-linux",
feature = "dangerous-test-hooks",
target_os = "linux"
))]
use bvisor::linux::launch::{self, AuthorityFd};
use bvisor::linux::protocol::{
DescriptorKind, DescriptorRole, DescriptorShape, DescriptorSlotV1, LinuxLaunchBodyV1,
LinuxLaunchPlanV1, LoweringWireEntryV1, LoweringWireV1, TargetSpecV1,
};
use bvisor::{
AdmissionProgramHash, AttemptId, Backend, BackendId, BackendProfileHash, BackendRegistry,
BoundaryPlanHash, BoundaryPlanner, BoundaryReportBody, BoundarySpec, BudgetRequirements,
Capability, EnvPolicy, EvidenceRequirements, FdPolicy, HostControl, LinuxBackend, MinGuarantee,
Outcome, StdStreams, Workload,
};
use std::io::Read;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, Instant};
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 SLOT_EXE: RawFd = 10;
const SENTINEL_FD: RawFd = 50;
fn test_launcher_path() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_bvisor-linux-launcher"))
}
fn unique_marker() -> String {
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);
format!("BVISOR-FD-MARKER-{pid}-{nanos}")
}
fn place_inheritable_high(fd: RawFd) -> OwnedFd {
let new = unsafe { libc::fcntl(fd, libc::F_DUPFD, SENTINEL_FD) };
assert!(
(SENTINEL_FD..100).contains(&new),
"F_DUPFD must land in the collision-free band [{SENTINEL_FD},100); got {new}"
);
unsafe { OwnedFd::from_raw_fd(new) }
}
fn host_find_child(marker: &str, deadline: Instant) -> Option<RawFd> {
while Instant::now() < deadline {
if let Some(pid) = scan_proc_cmdline(marker) {
return Some(pid);
}
std::thread::sleep(Duration::from_millis(10));
}
None
}
fn scan_proc_cmdline(marker: &str) -> Option<RawFd> {
let entries = std::fs::read_dir("/proc").ok()?;
for entry in entries.flatten() {
let name = entry.file_name();
let Some(pid_str) = name.to_str() else {
continue;
};
if !pid_str.bytes().all(|b| b.is_ascii_digit()) {
continue;
}
let Ok(pid) = pid_str.parse::<RawFd>() else {
continue;
};
let path = format!("/proc/{pid_str}/cmdline");
let Ok(bytes) = std::fs::read(&path) else {
continue;
};
let cmdline = String::from_utf8_lossy(&bytes);
if cmdline.contains(marker) {
return Some(pid);
}
}
None
}
fn host_read_child_fds(pid: RawFd) -> Option<Vec<RawFd>> {
let dir = std::fs::read_dir(format!("/proc/{pid}/fd")).ok()?;
let mut fds: Vec<RawFd> = Vec::new();
for entry in dir.flatten() {
if let Some(name) = entry.file_name().to_str() {
if let Ok(fd) = name.parse::<RawFd>() {
fds.push(fd);
}
}
}
fds.sort_unstable();
Some(fds)
}
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 exe_slot() -> DescriptorSlotV1 {
DescriptorSlotV1 {
slot_index: u32::try_from(SLOT_EXE).expect("fd fits u32"),
role: DescriptorRole::TargetExe,
expected: DescriptorShape {
kind: DescriptorKind::Regular,
writable: false,
},
}
}
fn exec_only_plan(argv: Vec<String>) -> LinuxLaunchPlanV1 {
let lowering = LoweringWireV1 {
entries: vec![
entry(ID_AMBIENT_SCRUB, PHASE_CODE_SCRUB),
entry(ID_EXEC, PHASE_CODE_EXEC),
],
};
let bytes = batpak::canonical::to_bytes(&lowering).expect("encode lowering");
let h_l = batpak::event::hash::compute_hash(&bytes);
let body = 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: vec![exe_slot()],
target: TargetSpecV1 {
argv,
envp: vec![("PATH".to_owned(), "/usr/bin:/bin".to_owned())],
exe_slot: u32::try_from(SLOT_EXE).expect("fd fits u32"),
user_namespace: None,
network_namespace: None,
seccomp: None,
},
};
LinuxLaunchPlanV1 { body }
}
fn sh_authority() -> AuthorityFd {
AuthorityFd {
slot_index: SLOT_EXE,
handle: OwnedFd::from(std::fs::File::open("/bin/sh").expect("open /bin/sh")),
}
}
#[test]
fn child_inherits_only_the_declared_fds_no_sentinel_leak() {
let marker = unique_marker();
let (mut reader, writer) = std::io::pipe().expect("create pipe");
let sentinel = place_inheritable_high(writer.as_raw_fd());
let sentinel_fd = sentinel.as_raw_fd();
let script = format!(
": {marker}; \
if printf LEAK >&{sentinel_fd}; then printf WROTE; else printf SCRUBBED; fi; \
sleep 3; true"
);
let argv = vec!["sh".to_string(), "-c".to_string(), script];
let plan = exec_only_plan(argv);
let launcher = test_launcher_path();
let deadline = Instant::now() + Duration::from_millis(2500);
let handle = std::thread::Builder::new()
.name("fd-oracle-launcher".to_string())
.spawn(move || {
launch::run_launcher(&launcher, &plan, vec![sh_authority()])
.expect("the launcher runs the fd-scrub workload to a verdict")
})
.expect("spawn the launcher driver thread");
let mut host_fds: Option<Vec<RawFd>> = None;
if let Some(pid) = host_find_child(&marker, deadline) {
while Instant::now() < deadline {
if let Some(fds) = host_read_child_fds(pid) {
host_fds = Some(fds);
break;
}
std::thread::sleep(Duration::from_millis(10));
}
}
let obs = handle.join().expect("fd-oracle launcher thread joins");
if launch::launch_confinement_unavailable(&obs) {
use std::io::Write as _;
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP child_inherits_only_the_declared_fds_no_sentinel_leak: kernel/container lacks \
landlock/userns/seccomp (ENOSYS); the launcher faulted before exec — exercised on \
capable kernels + the bvisor-linux CI lane"
);
return;
}
drop(sentinel);
drop(writer);
let mut leaked = Vec::new();
reader
.read_to_end(&mut leaked)
.expect("read the pipe read end");
let mut failures: Vec<String> = Vec::new();
if !obs.exec_succeeded() {
failures.push(format!(
"the workload must reach ExecSucceeded; terminal={:?} notes={:?}",
obs.terminal, obs.notes
));
}
match host_fds {
None => failures.push(
"CHANNEL A: the host must observe the child's /proc/<pid>/fd while it is alive"
.to_string(),
),
Some(host_fds) => {
let declared = [0, 1, 2, SLOT_EXE];
let undeclared: Vec<RawFd> = host_fds
.iter()
.copied()
.filter(|fd| !declared.contains(fd) && *fd < 100)
.collect();
if !undeclared.is_empty() {
failures.push(format!(
"CHANNEL A: the child's /proc/<pid>/fd must contain ONLY the declared \
allowlist; undeclared low fds survived: {undeclared:?} (full set {host_fds:?})"
));
}
if host_fds.contains(&sentinel_fd) {
failures.push(format!(
"CHANNEL A (no-leak): the undeclared sentinel fd {sentinel_fd} was NOT \
scrubbed — it survived into the child: {host_fds:?}"
));
}
for std_fd in [0, 1, 2] {
if !host_fds.contains(&std_fd) {
failures.push(format!(
"CHANNEL A: the declared stdio fd {std_fd} must survive the scrub: \
{host_fds:?}"
));
}
}
}
}
let out = String::from_utf8_lossy(&obs.captured_stdout);
if !out.contains("SCRUBBED") || out.contains("WROTE") {
failures.push(format!(
"CHANNEL B: the workload must report the sentinel fd was SCRUBBED; got stdout={out:?}"
));
}
if !leaked.is_empty() {
failures.push(format!(
"no-leak: the sentinel fd LEAKED across the boundary: host read {leaked:?} from the pipe"
));
}
assert!(
failures.is_empty(),
"fd-scrub oracle failures: {failures:#?}"
);
}
fn fds_spec(policy: FdPolicy) -> BoundarySpec {
BoundarySpec {
workload: Workload::Process {
exe: "/bin/sh".to_string(),
args: vec!["-c".to_string(), "exit 0".to_string()],
},
capabilities: vec![
Capability::InheritedFds { policy },
Capability::Environment {
policy: EnvPolicy::Exact(Vec::new()),
},
],
controls: vec![
HostControl::LaunchWorkload,
HostControl::CaptureStreams {
streams: StdStreams::capture_out_err(),
},
],
budgets: BudgetRequirements::uniform(8, MinGuarantee::Mediated),
evidence: EvidenceRequirements::default(),
}
}
fn run_execute(spec: &BoundarySpec) -> Option<BoundaryReportBody> {
let backend = Arc::new(LinuxBackend::with_launcher_path(test_launcher_path()));
let id: BackendId = backend.id();
let mut registry = BackendRegistry::new();
registry.register(Arc::clone(&backend) as Arc<dyn Backend>);
let plan = BoundaryPlanner::new(®istry).plan(spec, &id).ok()?;
Some(
bvisor::BoundaryRunner::new(®istry)
.run(&plan)
.expect("the run seals a terminal report")
.body,
)
}
#[test]
fn a_none_policy_spec_runs_through_the_execute_path() {
let report = run_execute(&fds_spec(FdPolicy::None))
.expect("an InheritedFds::None spec must ADMIT (the cell is Enforced)");
if launch::report_confinement_unavailable(&report.observed) {
use std::io::Write as _;
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP a_none_policy_spec_runs_through_the_execute_path: kernel/container lacks \
landlock/userns/seccomp (ENOSYS); confinement cannot install here — exercised on \
capable kernels + the bvisor-linux CI lane"
);
return;
}
let mut failures: Vec<String> = Vec::new();
if report.outcome != Outcome::Completed {
failures.push(format!(
"the None-policy workload must run to Completed: {:?} / {:?}",
report.outcome, report.observed
));
}
if !report
.observed
.iter()
.any(|f| f.kind == "inherited_fds_lowered")
{
failures.push(format!(
"the execute() path must record the fd lowering: {:?}",
report.observed
));
}
assert!(
failures.is_empty(),
"execute()-path witness failures: {failures:#?}"
);
}
#[test]
fn an_unrealized_fd_policy_fails_closed_and_the_target_never_runs() {
let report = run_execute(&fds_spec(FdPolicy::Only(vec![7])));
assert!(
report.is_none(),
"an InheritedFds::Only spec must FAIL CLOSED at admission (the cell is Unsupported) — \
the target never runs; got a sealed report {report:?}"
);
}