use crate::backend::linux::protocol::{LauncherState, LinuxLaunchPlanV1};
use crate::backend::linux::sys::{self, LaunchFd};
use std::io::Read;
use std::os::fd::{OwnedFd, RawFd};
#[cfg(feature = "dangerous-test-hooks")]
use std::path::PathBuf;
pub const ENV_LAUNCHER_BIN: &str = "BVISOR_LAUNCHER_BIN";
const ENV_PLAN_FD: &str = "BVISOR_LAUNCH_PLAN_FD";
const ENV_CONTROL_FD: &str = "BVISOR_CONTROL_FD";
const ENV_ERROR_FD: &str = "BVISOR_ERROR_FD";
const ENV_ERROR_READ_FD: &str = "BVISOR_ERROR_READ_FD";
#[cfg(feature = "dangerous-test-hooks")]
pub const ENV_FORCE_USERNS_MAP_FAIL: &str = "BVISOR_TEST_FORCE_USERNS_MAP_FAIL";
pub struct AuthorityFd {
pub slot_index: RawFd,
pub handle: OwnedFd,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum HarnessError {
Encode(crate::backend::linux::protocol::EncodeError),
BadSlot {
slot_index: RawFd,
},
Os(std::io::Error),
}
impl std::fmt::Display for HarnessError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Encode(e) => write!(f, "could not encode the launch plan: {e}"),
Self::BadSlot { slot_index } => {
write!(
f,
"authority slot index {slot_index} is invalid or collides"
)
}
Self::Os(e) => write!(f, "launcher harness OS fault: {e}"),
}
}
}
impl std::error::Error for HarnessError {}
impl From<std::io::Error> for HarnessError {
fn from(e: std::io::Error) -> Self {
Self::Os(e)
}
}
#[derive(Clone, Debug)]
pub struct LaunchObservation {
pub terminal: Option<LauncherState>,
#[cfg(feature = "dangerous-test-hooks")]
pub transcript: Vec<LauncherState>,
pub notes: Vec<String>,
pub confinement_installed: bool,
pub launcher_exit: std::process::ExitStatus,
pub captured_stdout: Vec<u8>,
pub captured_stderr: Vec<u8>,
}
impl LaunchObservation {
#[cfg(feature = "dangerous-test-hooks")]
#[must_use]
pub fn exec_succeeded(&self) -> bool {
self.terminal == Some(LauncherState::ExecSucceeded)
}
#[must_use]
pub fn outcome(&self) -> Option<crate::contract::report::Outcome> {
self.terminal
.and_then(crate::backend::linux::protocol::outcome_class)
}
}
#[cfg(feature = "dangerous-test-hooks")]
#[must_use]
pub fn resolve_launcher_path(default_path: &str) -> PathBuf {
match std::env::var(ENV_LAUNCHER_BIN) {
Ok(p) if !p.trim().is_empty() => PathBuf::from(p),
_ => PathBuf::from(default_path),
}
}
#[must_use]
pub fn unprivileged_userns_available() -> bool {
if let Ok(text) = std::fs::read_to_string("/proc/sys/user/max_user_namespaces") {
if text.trim() == "0" {
return false;
}
}
if let Ok(text) = std::fs::read_to_string("/proc/sys/kernel/unprivileged_userns_clone") {
if text.trim() == "0" {
return false;
}
}
true
}
#[cfg(feature = "dangerous-test-hooks")]
fn detail_is_capability_absence(detail: &str) -> bool {
detail.contains("Function not implemented")
|| detail.contains("os error 38")
|| detail.contains("errno=38")
}
#[cfg(feature = "dangerous-test-hooks")]
#[must_use]
pub fn report_confinement_unavailable(observed: &[crate::contract::report::ObservedFact]) -> bool {
let capability_absent = observed
.iter()
.any(|f| detail_is_capability_absence(&f.detail));
let confinement_uninstalled = observed.iter().any(|f| {
f.kind == "launcher_terminal"
&& f.detail.contains("SetupFaulted")
&& f.detail.contains("confinement_installed=false")
}) || observed
.iter()
.any(|f| f.kind == "confinement_not_installed");
capability_absent && confinement_uninstalled
}
#[cfg(feature = "dangerous-test-hooks")]
#[must_use]
pub fn launch_confinement_unavailable(obs: &LaunchObservation) -> bool {
let capability_absent = obs.notes.iter().any(|n| detail_is_capability_absence(n));
let confinement_uninstalled =
obs.terminal == Some(LauncherState::SetupFaulted) && !obs.confinement_installed;
capability_absent && confinement_uninstalled
}
#[cfg(feature = "dangerous-test-hooks")]
#[must_use]
pub fn transcript_confinement_unavailable(transcript: &str) -> bool {
let capability_absent = transcript.lines().any(detail_is_capability_absence);
let setup_faulted = transcript.lines().any(|l| l.trim() == "SetupFaulted");
capability_absent && setup_faulted
}
pub fn run_launcher(
launcher_path: &std::path::Path,
plan: &LinuxLaunchPlanV1,
authority: Vec<AuthorityFd>,
) -> Result<LaunchObservation, HarnessError> {
run_launcher_inner(launcher_path, plan, authority, &[])
}
#[cfg(feature = "dangerous-test-hooks")]
pub fn run_launcher_with_env(
launcher_path: &std::path::Path,
plan: &LinuxLaunchPlanV1,
authority: Vec<AuthorityFd>,
extra_env: &[(&str, String)],
) -> Result<LaunchObservation, HarnessError> {
run_launcher_inner(launcher_path, plan, authority, extra_env)
}
fn run_launcher_inner(
launcher_path: &std::path::Path,
plan: &LinuxLaunchPlanV1,
authority: Vec<AuthorityFd>,
extra_env: &[(&str, String)],
) -> Result<LaunchObservation, HarnessError> {
use std::os::unix::net::UnixStream;
let bytes = plan.encode().map_err(HarnessError::Encode)?;
let plan_fd = sys::seal_plan_memfd(&bytes)?;
let (control_host, control_launcher) = UnixStream::pair()?;
let (error_read, error_write) = std::io::pipe()?;
let mut max_slot: RawFd = 2; for a in &authority {
if a.slot_index < 0 {
return Err(HarnessError::BadSlot {
slot_index: a.slot_index,
});
}
max_slot = max_slot.max(a.slot_index);
}
let base = max_slot.checked_add(1).ok_or(HarnessError::BadSlot {
slot_index: max_slot,
})?;
let plan_target = base;
let control_target = base + 1;
let error_write_target = base + 2;
let error_read_target = base + 3;
let mut fds: Vec<LaunchFd> = Vec::with_capacity(authority.len() + 4);
for a in authority {
fds.push(LaunchFd {
src: a.handle,
target: a.slot_index,
keep_cloexec: false,
});
}
fds.push(LaunchFd {
src: plan_fd,
target: plan_target,
keep_cloexec: false,
});
fds.push(LaunchFd {
src: OwnedFd::from(control_launcher),
target: control_target,
keep_cloexec: false,
});
fds.push(LaunchFd {
src: OwnedFd::from(error_write),
target: error_write_target,
keep_cloexec: true,
});
fds.push(LaunchFd {
src: OwnedFd::from(error_read),
target: error_read_target,
keep_cloexec: false,
});
let mut env: Vec<(&str, String)> = vec![
(ENV_PLAN_FD, plan_target.to_string()),
(ENV_CONTROL_FD, control_target.to_string()),
(ENV_ERROR_FD, error_write_target.to_string()),
(ENV_ERROR_READ_FD, error_read_target.to_string()),
];
for (name, value) in extra_env {
env.push((name, value.clone()));
}
let (mut child, relocated) = sys::spawn_launcher_with_fds(launcher_path, &env, fds)?;
drop(relocated);
let stdout = child.stdout.take();
let stderr = child.stderr.take();
let mut transcript_text = String::new();
let (captured_stdout, captured_stderr) =
std::thread::scope(|scope| -> Result<(Vec<u8>, Vec<u8>), HarnessError> {
let out = scope.spawn(move || drain_owned(stdout));
let err = scope.spawn(move || drain_owned(stderr));
let mut control = control_host;
control.read_to_string(&mut transcript_text)?;
let captured_stdout = out.join().map_err(|_| drain_panicked("stdout"))??;
let captured_stderr = err.join().map_err(|_| drain_panicked("stderr"))??;
Ok((captured_stdout, captured_stderr))
})?;
let launcher_exit = child.wait()?;
Ok(parse_observation(
&transcript_text,
launcher_exit,
captured_stdout,
captured_stderr,
))
}
fn drain_owned<R: Read>(stream: Option<R>) -> Result<Vec<u8>, HarnessError> {
let mut buf = Vec::new();
if let Some(mut s) = stream {
s.read_to_end(&mut buf)?;
}
Ok(buf)
}
fn drain_panicked(which: &str) -> HarnessError {
HarnessError::Os(std::io::Error::other(format!(
"{which} drain thread panicked"
)))
}
fn parse_observation(
text: &str,
launcher_exit: std::process::ExitStatus,
captured_stdout: Vec<u8>,
captured_stderr: Vec<u8>,
) -> LaunchObservation {
let mut transcript: Vec<LauncherState> = Vec::new();
let mut notes: Vec<String> = Vec::new();
let mut installed = false;
for line in text.lines() {
let line = line.trim_end();
if let Some(note) = line.strip_prefix("# ") {
if note.contains("installed=true") {
installed = true;
}
notes.push(note.to_string());
} else if let Some(state) = state_from_name(line) {
transcript.push(state);
}
}
let terminal = transcript.iter().rev().copied().find(|s| s.is_terminal());
LaunchObservation {
terminal,
#[cfg(feature = "dangerous-test-hooks")]
transcript,
notes,
confinement_installed: installed,
launcher_exit,
captured_stdout,
captured_stderr,
}
}
fn state_from_name(name: &str) -> Option<LauncherState> {
let state = match name {
"LauncherStarted" => LauncherState::LauncherStarted,
"IdentityVerified" => LauncherState::IdentityVerified,
"PlanVerified" => LauncherState::PlanVerified,
"HandlesVerified" => LauncherState::HandlesVerified,
"ChildCreated" => LauncherState::ChildCreated,
"IdentityPhaseResolved" => LauncherState::IdentityPhaseResolved,
"VisibilityPhaseResolved" => LauncherState::VisibilityPhaseResolved,
"AmbientAuthorityPhaseResolved" => LauncherState::AmbientAuthorityPhaseResolved,
"ConfinementPhaseResolved" => LauncherState::ConfinementPhaseResolved,
"ReadyToExec" => LauncherState::ReadyToExec,
"ExecSucceeded" => LauncherState::ExecSucceeded,
"SetupRefused" => LauncherState::SetupRefused,
"SetupFaulted" => LauncherState::SetupFaulted,
_ => return None,
};
Some(state)
}