ghidra-cli/tests/bubblewrap_probe.rs

114 lines
3.3 KiB
Rust

#![allow(clippy::expect_used)]
#![doc = "Host-permitted production Bubblewrap network-isolation probe."]
use std::{
env,
ffi::OsString,
net::TcpListener,
path::{Path, PathBuf},
process::Command,
};
use ghidra_cli::{
cli::SandboxMode,
sandbox::{BubblewrapConfig, WorkerCommand},
};
#[test]
fn production_bubblewrap_command_cannot_reach_host_listener_when_supported() {
let Some(bubblewrap) = bubblewrap_path() else {
return;
};
let fake_child = PathBuf::from(env!("CARGO_BIN_EXE_ghidr-fake-child"));
let Ok(listener) = TcpListener::bind("127.0.0.1:0") else {
// Some CI sandboxes prohibit even loopback listeners.
return;
};
let address = listener.local_addr().expect("listener address").to_string();
let direct = Command::new(&fake_child)
.args(["network-probe", &address])
.status()
.expect("direct probe process");
assert!(
direct.success(),
"control probe must reach the host listener"
);
let private = tempfile::tempdir().expect("sandbox private directory");
let home = private.path().join("home");
let temporary = private.path().join("tmp");
std::fs::create_dir(&home).expect("private home");
std::fs::create_dir(&temporary).expect("private tmp");
let benign = bubblewrap_command(
&bubblewrap,
&fake_child,
vec![OsString::from("exit-success")],
&home,
&temporary,
);
let benign_status = Command::new(&benign.program)
.args(&benign.arguments)
.status()
.expect("Bubblewrap capability probe");
if !benign_status.success() {
// User namespaces are legitimately unavailable on some test hosts.
return;
}
let isolated = bubblewrap_command(
&bubblewrap,
&fake_child,
vec![OsString::from("network-probe"), OsString::from(address)],
&home,
&temporary,
);
let isolated_status = Command::new(&isolated.program)
.args(&isolated.arguments)
.status()
.expect("isolated probe");
assert!(
!isolated_status.success(),
"worker unexpectedly reached a host listener across --unshare-all"
);
}
fn bubblewrap_command(
bubblewrap: &Path,
fake_child: &Path,
worker_arguments: Vec<OsString>,
home: &Path,
temporary: &Path,
) -> WorkerCommand {
WorkerCommand::for_policy(
SandboxMode::Bubblewrap,
fake_child.to_path_buf(),
worker_arguments.clone(),
Some(BubblewrapConfig {
bubblewrap: bubblewrap.to_path_buf(),
worker_program: fake_child.to_path_buf(),
worker_arguments,
readonly_paths: vec![PathBuf::from("/nix/store"), fake_child.to_path_buf()],
staged_sample: None,
writable_paths: Vec::new(),
private_home: home.to_path_buf(),
private_tmp: temporary.to_path_buf(),
}),
)
.expect("production Bubblewrap command")
}
fn bubblewrap_path() -> Option<PathBuf> {
env::var_os("GHIDR_TEST_BWRAP")
.map(PathBuf::from)
.or_else(|| find_in_path("bwrap"))
}
fn find_in_path(executable: &str) -> Option<PathBuf> {
env::var_os("PATH").and_then(|path| {
env::split_paths(&path)
.map(|directory| directory.join(executable))
.find(|candidate| candidate.is_file())
})
}