#![cfg(all(
feature = "backend-linux",
feature = "dangerous-test-hooks",
target_os = "linux"
))]
use bvisor::linux::launch::{self, AuthorityFd, LaunchObservation};
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, EnvEntry, EnvPolicy, EvidenceRequirements, HostControl, LinuxBackend,
MapSecretResolver, MinGuarantee, Outcome, PlanError, SecretRef, StdStreams, Workload,
};
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: std::os::fd::RawFd = 10;
const LEAK_SENTINELS: &[&str] = &[
"BVISOR_LAUNCH_PLAN_FD",
"BVISOR_CONTROL_FD",
"BVISOR_ERROR_FD",
"BVISOR_ERROR_READ_FD",
];
fn has_ambient_leak(env: &[String]) -> bool {
env.iter().any(|line| {
LEAK_SENTINELS.iter().any(|s| line.contains(s)) || line.contains("BVISOR_LAUNCH")
})
}
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-ENV-MARKER-{pid}-{nanos}")
}
fn host_read_child_environ(
marker: &str,
expected_len: usize,
deadline: Instant,
) -> Option<Vec<String>> {
while Instant::now() < deadline {
if let Some(env) = scan_proc_for_marker(marker, expected_len) {
return Some(env);
}
std::thread::sleep(Duration::from_millis(10));
}
None
}
fn scan_proc_for_marker(marker: &str, expected_len: usize) -> Option<Vec<String>> {
let entries = std::fs::read_dir("/proc").ok()?;
for entry in entries.flatten() {
let name = entry.file_name();
let Some(pid) = name.to_str() else { continue };
if !pid.bytes().all(|b| b.is_ascii_digit()) {
continue;
}
let path = format!("/proc/{pid}/environ");
let Ok(bytes) = std::fs::read(&path) else {
continue;
};
let env: Vec<String> = bytes
.split(|&b| b == 0)
.filter(|r| !r.is_empty())
.map(|r| String::from_utf8_lossy(r).into_owned())
.collect();
if env.len() == expected_len && env.iter().any(|line| line.contains(marker)) {
return Some(env);
}
}
None
}
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>, envp: Vec<(String, 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,
exe_slot: u32::try_from(SLOT_EXE).expect("fd fits u32"),
user_namespace: None,
network_namespace: None,
seccomp: None,
},
};
LinuxLaunchPlanV1 { body }
}
fn bin_authority(name: &str) -> AuthorityFd {
let usr = format!("/usr/bin/{name}");
let bin = format!("/bin/{name}");
let path = if std::path::Path::new(&usr).is_file() {
usr
} else {
bin
};
AuthorityFd {
slot_index: SLOT_EXE,
handle: std::os::fd::OwnedFd::from(
std::fs::File::open(&path).expect("open the exec target coreutil"),
),
}
}
fn sleep_authority() -> AuthorityFd {
bin_authority("sleep")
}
fn env_authority() -> AuthorityFd {
bin_authority("env")
}
fn workload_reported_env(obs: &LaunchObservation) -> Vec<String> {
String::from_utf8_lossy(&obs.captured_stdout)
.lines()
.map(str::to_owned)
.collect()
}
#[test]
fn child_env_equals_the_admitted_table_with_no_ambient_leak() {
let marker = unique_marker();
let secret_value = "RESOLVED-SECRET-IN-CHILD";
let policy = EnvPolicy::Exact(vec![
EnvEntry::literal("PATH", "/usr/bin:/bin"),
EnvEntry::literal("BVISOR_ENV_MARKER", &marker),
EnvEntry::lease("CHILD_TOKEN", SecretRef::new("lease://child/token")),
]);
assert_eq!(
policy.validate(),
Ok(()),
"the admitted table must be valid"
);
let resolver = MapSecretResolver::new().with("lease://child/token", secret_value);
let envp = bvisor::lower_env(&policy, &resolver).expect("the policy lowers cleanly");
let mut expected: Vec<String> = envp
.iter()
.map(|(name, value)| format!("{name}={value}"))
.collect();
expected.sort();
let sleep_argv = vec!["sleep".to_string(), "3".to_string()];
let sleep_plan = exec_only_plan(sleep_argv, envp.clone());
let launcher = test_launcher_path();
let deadline = Instant::now() + Duration::from_millis(2500);
let handle = std::thread::Builder::new()
.name("env-oracle-launcher".to_string())
.spawn(move || {
launch::run_launcher(&launcher, &sleep_plan, vec![sleep_authority()])
.expect("the launcher runs the sleep workload to a verdict")
})
.expect("spawn the launcher driver thread");
let host_env = host_read_child_environ(&marker, expected.len(), deadline);
let sleep_obs = handle.join().expect("sleep launcher thread joins");
if launch::launch_confinement_unavailable(&sleep_obs) {
use std::io::Write as _;
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP child_env_equals_the_admitted_table_with_no_ambient_leak: kernel/container \
lacks landlock/userns/seccomp (ENOSYS); the launcher faulted before exec — \
exercised on capable kernels + the bvisor-linux CI lane"
);
return;
}
assert!(
sleep_obs.exec_succeeded(),
"the sleep workload must reach ExecSucceeded; terminal={:?} notes={:?}",
sleep_obs.terminal,
sleep_obs.notes
);
let mut host_env = host_env.expect(
"CHANNEL A: the host must observe the child's /proc/<pid>/environ while it is alive",
);
host_env.sort();
assert_eq!(
host_env, expected,
"CHANNEL A: the child's /proc environ must EQUAL the admitted table exactly"
);
assert!(
!has_ambient_leak(&host_env),
"CHANNEL A: a launcher-env sentinel leaked into the child env: {host_env:?}"
);
assert!(
host_env
.iter()
.any(|l| l == &format!("CHILD_TOKEN={secret_value}")),
"CHANNEL A: the secret lease must resolve to its value in the child: {host_env:?}"
);
let env_argv = vec!["env".to_string()];
let env_plan = exec_only_plan(env_argv, envp);
let env_obs = launch::run_launcher(&test_launcher_path(), &env_plan, vec![env_authority()])
.expect("the launcher runs the env workload to a verdict");
assert!(
env_obs.exec_succeeded(),
"the env workload must reach ExecSucceeded; terminal={:?} notes={:?}",
env_obs.terminal,
env_obs.notes
);
let mut reported = workload_reported_env(&env_obs);
reported.sort();
assert_eq!(
reported, expected,
"CHANNEL B: the workload's reported env must EQUAL the admitted table exactly"
);
assert!(
!has_ambient_leak(&reported),
"CHANNEL B: a launcher-env sentinel leaked into the workload's reported env: {reported:?}"
);
}
fn env_spec(policy: EnvPolicy) -> BoundarySpec {
BoundarySpec {
workload: Workload::Process {
exe: "/bin/sh".to_string(),
args: vec!["-c".to_string(), "env".to_string()],
},
capabilities: vec![Capability::Environment { policy }],
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, resolver: MapSecretResolver) -> (BoundaryReportBody, Vec<u8>) {
let backend = Arc::new(
LinuxBackend::with_launcher_path(test_launcher_path())
.with_secret_resolver(Arc::new(resolver)),
);
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)
.expect("the LinuxBackend admits an Environment::Exact spec");
let plan_bytes = batpak::canonical::to_bytes(&plan).expect("encode the durable plan");
let report = bvisor::BoundaryRunner::new(®istry)
.run(&plan)
.expect("the run seals a terminal report")
.body;
(report, plan_bytes)
}
#[test]
fn a_secret_lease_resolves_but_the_durable_plan_and_report_carry_only_the_ref() {
let secret_value = "DURABLE-MUST-NOT-CONTAIN-THIS-SECRET";
let lease_ref = "lease://durable/token";
let policy = EnvPolicy::Exact(vec![
EnvEntry::literal("PATH", "/usr/bin:/bin"),
EnvEntry::lease("DB_TOKEN", SecretRef::new(lease_ref)),
]);
let resolver = MapSecretResolver::new().with(lease_ref, secret_value);
let (report, plan_bytes) = run_execute(&env_spec(policy), resolver);
if launch::report_confinement_unavailable(&report.observed) {
use std::io::Write as _;
let mut sink = std::io::stderr();
let _ = writeln!(
sink,
"SKIP a_secret_lease_resolves_but_the_durable_plan_and_report_carry_only_the_ref: \
kernel/container lacks landlock/userns/seccomp (ENOSYS); confinement cannot install \
here — exercised on capable kernels + the bvisor-linux CI lane"
);
return;
}
assert_eq!(
report.outcome,
Outcome::Completed,
"the lease resolved ⇒ the workload runs: {:?}",
report.observed
);
let plan_text = String::from_utf8_lossy(&plan_bytes);
assert!(
plan_text.contains(lease_ref),
"the durable plan must carry the lease REF"
);
assert!(
!plan_text.contains(secret_value),
"the durable plan must NOT carry the resolved secret value"
);
let report_bytes = batpak::canonical::to_bytes(&report).expect("encode the durable report");
let report_text = String::from_utf8_lossy(&report_bytes);
assert!(
!report_text.contains(secret_value),
"the durable report must NOT carry the resolved secret value"
);
assert!(
report_text.contains(lease_ref),
"the durable report must carry the lease REF (the policy identity)"
);
}
#[test]
fn an_unresolvable_lease_fails_closed_and_the_target_never_runs() {
let policy = EnvPolicy::Exact(vec![EnvEntry::lease(
"MISSING_TOKEN",
SecretRef::new("lease://does-not-exist"),
)]);
let (report, _plan_bytes) = run_execute(&env_spec(policy), MapSecretResolver::new());
assert_ne!(
report.outcome,
Outcome::Completed,
"an unresolvable lease must NOT complete the workload: {:?}",
report.observed
);
assert!(
report
.observed
.iter()
.any(|f| f.kind == "environment_lowering_failed"),
"the report must record the fail-closed lowering refusal: {:?}",
report.observed
);
assert!(
!report
.observed
.iter()
.any(|f| f.kind == "workload_launched"),
"the target must NEVER run when a lease is unresolvable: {:?}",
report.observed
);
}
#[test]
fn a_contract_invalid_policy_is_refused_before_execution() {
let policy = EnvPolicy::Exact(vec![
EnvEntry::literal("DUP", "a"),
EnvEntry::literal("DUP", "b"),
]);
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 result = BoundaryPlanner::new(®istry).plan(&env_spec(policy), &id);
assert!(
matches!(result, Err(PlanError::InvalidPolicy { .. })),
"a contract-invalid Environment policy must be REFUSED at admission, got {result:?}"
);
}