use std::os::fd::AsRawFd;
use std::path::Path;
use std::process::Command;
use std::time::{Duration, Instant};
use super::{executable, sealed_filter, DescriptorTransfer};
pub(super) enum ProbeOutcome {
Completed(bool),
Interrupted,
Incomplete,
}
pub(super) fn probe() -> ProbeOutcome {
if crate::op_interrupt::requested() {
return ProbeOutcome::Interrupted;
}
if !std::fs::read("/etc/passwd").is_ok_and(|bytes| !bytes.is_empty()) {
return ProbeOutcome::Completed(false);
}
let Some(executable) = executable() else {
return ProbeOutcome::Completed(false);
};
let mut command = Command::new(executable);
command.env_clear();
command.args([
"--unshare-user",
"--unshare-pid",
"--unshare-ipc",
"--unshare-net",
]);
let mut descriptors = Vec::new();
for path in ["/usr", "/lib", "/lib64", "/bin"] {
if Path::new(path).exists() {
let Ok(file) = std::fs::File::open(path) else {
return ProbeOutcome::Completed(false);
};
command.args(["--ro-bind-fd", &file.as_raw_fd().to_string(), path]);
descriptors.push(file.into());
}
}
let Ok(filter) = sealed_filter(&[0x06, 0, 0, 0, 0, 0, 0xff, 0x7f]) else {
return ProbeOutcome::Completed(false);
};
command.args(["--seccomp", &filter.as_raw_fd().to_string()]);
descriptors.push(filter);
DescriptorTransfer::new(descriptors).attach(&mut command);
command.args([
"--",
"/usr/bin/sh",
"-c",
"test ! -e /etc/passwd && printf harn-bwrap-boundary",
]);
capture_probe(&mut command)
}
fn capture_probe(command: &mut Command) -> ProbeOutcome {
capture_probe_within(command, PROBE_SETUP_BUDGET)
}
const PROBE_SETUP_BUDGET: Duration = Duration::from_secs(1);
fn capture_probe_within(command: &mut Command, budget: Duration) -> ProbeOutcome {
let _budget = crate::op_interrupt::with_operation_budget(Instant::now() + budget);
let output = crate::op_interrupt::capture_output_interruptible(command);
if crate::op_interrupt::operation_budget_expired() {
ProbeOutcome::Incomplete
} else if crate::op_interrupt::requested() {
ProbeOutcome::Interrupted
} else {
ProbeOutcome::Completed(output.is_ok_and(|output| probe_output_is_available(&output)))
}
}
pub(super) fn probe_output_is_available(output: &std::process::Output) -> bool {
output.status.success() && output.stdout == b"harn-bwrap-boundary"
}
#[cfg(test)]
mod tests {
use super::*;
use crate::orchestration::{CapabilityPolicy, ProcessSandboxPolicy, SandboxProfile};
const CHILD_ROOT: &str = "HARN_TEST_NESTED_BWRAP_PROBE_ROOT";
#[test]
fn cancelled_setup_does_not_cache_unavailability() {
let cache = std::sync::Mutex::new(None);
let cancel = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
let interrupted = crate::op_interrupt::install(Some(cancel), None);
let mut first = Command::new("/usr/bin/printf");
first.env_clear().arg("harn-bwrap-boundary");
assert!(!super::super::cached_availability(
&cache,
|| capture_probe(&mut first)
));
assert!(crate::op_interrupt::requested());
assert!(
cache.lock().unwrap().is_none(),
"caller cancellation is not a host fact"
);
drop(interrupted);
let mut next = Command::new("/usr/bin/printf");
next.env_clear().arg("harn-bwrap-boundary");
assert!(super::super::cached_availability(&cache, || capture_probe(
&mut next
)));
assert_eq!(*cache.lock().unwrap(), Some(true));
assert!(super::super::cached_availability(&cache, || panic!(
"completed host fact must be reused"
)));
}
#[test]
fn an_expired_setup_budget_is_an_incomplete_probe_not_a_caller_deadline() {
let cache = std::sync::Mutex::new(None);
let started = Instant::now();
let outcome = super::super::cached_availability(&cache, || {
let mut command = Command::new("/bin/sleep");
command.env_clear().arg("5");
let outcome = capture_probe_within(&mut command, Duration::from_millis(200));
assert!(
crate::op_interrupt::requested_error().is_none(),
"the restored caller context has no interrupt to report"
);
assert!(
matches!(outcome, ProbeOutcome::Incomplete),
"budget expiry must be the probe's own outcome"
);
outcome
});
assert!(!outcome, "an incomplete probe refuses Bubblewrap");
assert!(
started.elapsed() < Duration::from_secs(4),
"{:?}",
started.elapsed()
);
assert!(
cache.lock().unwrap().is_none(),
"slow setup is not cached as a host fact"
);
}
#[test]
fn preparation_preserves_caller_cancellation() {
let root = tempfile::tempdir().unwrap();
let _cancel = crate::op_interrupt::install(
Some(std::sync::Arc::new(std::sync::atomic::AtomicBool::new(
true,
))),
None,
);
let error = super::super::prepare(
"/usr/bin/printf",
&["known-child".into()],
&policy(root.path()),
SandboxProfile::OsHardened,
)
.err()
.expect("cancelled preparation must not launch the payload");
assert!(crate::cancellation::is_cancellation(&error), "{error}");
assert!(error.sandbox_mechanism_unavailable().is_none());
}
#[test]
fn preparation_preserves_caller_deadline() {
let root = tempfile::tempdir().unwrap();
let _deadline = crate::op_interrupt::install(None, Some(Instant::now()));
let error = super::super::prepare(
"/usr/bin/printf",
&["known-child".into()],
&policy(root.path()),
SandboxProfile::OsHardened,
)
.err()
.expect("expired preparation must not launch the payload");
assert!(
matches!(&error, crate::VmError::Thrown(crate::VmValue::String(message))
if message.as_str() == "Deadline exceeded"),
"{error}"
);
assert!(error.sandbox_mechanism_unavailable().is_none());
}
fn policy(root: &Path) -> CapabilityPolicy {
CapabilityPolicy {
workspace_roots: vec![root.display().to_string()],
sandbox_profile: SandboxProfile::OsHardened,
process_sandbox: Box::new(ProcessSandboxPolicy {
allow_child_workspace_write: true,
..Default::default()
}),
..Default::default()
}
}
#[test]
fn nested_availability_refuses_before_command_deadline() {
if let Some(root) = std::env::var_os(CHILD_ROOT) {
let policy = policy(Path::new(&root));
assert!(
super::super::super::landlock_available(),
"a Landlock-confined child must still see Landlock"
);
crate::stdlib::sandbox::build_std_command::<super::super::super::Backend>(
"/bin/sh",
&["-c".into(), "printf known-child".into()],
&policy,
SandboxProfile::OsHardened,
)
.expect("a nested command stacks Landlock");
let error = super::super::prepare(
"/bin/sh",
&["-c".into(), "printf known-child".into()],
&policy,
SandboxProfile::OsHardened,
)
.err()
.expect("inherited syscall ceiling must refuse Bubblewrap");
assert!(error.sandbox_mechanism_unavailable().is_some(), "{error}");
println!("nested probe returned typed sandbox refusal");
return;
}
if executable().is_none() || !super::super::super::landlock_available() {
eprintln!("[nested-bwrap] exercised=0: Bubblewrap executable and functional Landlock required");
assert_ne!(std::env::var("BWRAP_REQUIRE_TESTS").as_deref(), Ok("1"));
return;
}
let root = tempfile::tempdir().unwrap();
let executable = std::env::current_exe().unwrap();
let mut policy = policy(root.path());
policy.process_sandbox.read_roots =
vec![executable.parent().unwrap().display().to_string()];
let mut control =
crate::stdlib::sandbox::build_std_command::<super::super::super::Backend>(
"/bin/sh",
&["-c".into(), "printf known-child".into()],
&policy,
policy.sandbox_profile,
)
.unwrap();
let control = crate::op_interrupt::capture_output_interruptible(&mut control).unwrap();
assert!(control.status.success(), "{control:?}");
assert_eq!(control.stdout, b"known-child");
let args = vec![
"--exact".into(),
"stdlib::sandbox::linux::bwrap::probe::tests::nested_availability_refuses_before_command_deadline".into(),
"--nocapture".into(),
];
let mut command =
crate::stdlib::sandbox::build_std_command::<super::super::super::Backend>(
executable.to_str().unwrap(),
&args,
&policy,
policy.sandbox_profile,
)
.unwrap();
command
.env_clear()
.env(CHILD_ROOT, root.path())
.env("TMPDIR", root.path());
let _deadline = crate::op_interrupt::with_deadline(Instant::now() + Duration::from_secs(5));
let output = crate::op_interrupt::capture_output_interruptible(&mut command).unwrap();
assert!(output.status.success(), "{output:?}");
assert!(
String::from_utf8_lossy(&output.stdout)
.contains("nested probe returned typed sandbox refusal"),
"{output:?}"
);
}
}