114 lines
3.3 KiB
Rust
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())
|
|
})
|
|
}
|