pub mod checks;
mod error;
mod pre_exec;
pub mod security;
#[cfg(target_os = "linux")]
mod bwrap;
#[cfg(target_os = "linux")]
mod landlock_setup;
#[cfg(target_os = "macos")]
mod seatbelt;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
pub use error::{Error, Result};
pub use security::{LayerStatus, SandboxKind, SecurityReport};
#[cfg(target_os = "linux")]
use bwrap::BwrapSandbox;
#[cfg(target_os = "macos")]
use seatbelt::SeatbeltSandbox;
pub const ENV_WATCHDOG_FD: &str = "BUX_WATCHDOG_FD";
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
#[allow(clippy::struct_excessive_bools, reason = "capability flags struct")]
pub struct SandboxCapabilities {
pub namespaces: bool,
pub seccomp: bool,
pub mandatory_access_control: bool,
}
pub trait Sandbox: std::fmt::Debug + Send + Sync {
fn wrap(&self, shim: &Path, config_path: &Path, jail: &JailConfig) -> Option<Command>;
fn capabilities(&self) -> SandboxCapabilities {
SandboxCapabilities::default()
}
fn kind(&self) -> SandboxKind {
SandboxKind::Noop
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Default)]
pub struct NoopSandbox;
impl Sandbox for NoopSandbox {
fn wrap(&self, shim: &Path, config_path: &Path, _jail: &JailConfig) -> Option<Command> {
let mut cmd = Command::new(shim);
cmd.arg(config_path);
Some(cmd)
}
fn kind(&self) -> SandboxKind {
SandboxKind::Noop
}
}
#[derive(Debug)]
#[allow(
clippy::struct_excessive_bools,
reason = "isolation flags are independent booleans, not a state machine"
)]
pub struct JailConfig {
pub rootfs: Option<PathBuf>,
pub root_disk: Option<PathBuf>,
pub readonly_paths: Vec<PathBuf>,
pub socks_dir: PathBuf,
pub virtiofs_paths: Vec<PathBuf>,
pub watchdog_fd: Option<RawFd>,
pub sandbox: Option<Box<dyn Sandbox>>,
pub stderr_file: Option<std::fs::File>,
pub landlock: bool,
pub allow_degraded_security: bool,
pub die_with_parent: bool,
pub network_host: bool,
pub bwrap_path: Option<PathBuf>,
}
#[derive(Debug)]
pub struct SpawnResult {
pub child: Child,
pub security: SecurityReport,
}
#[allow(
unsafe_code,
reason = "own the landlock ruleset fd so Drop closes it on every error path"
)]
pub fn spawn(shim: &Path, config_path: &Path, config: JailConfig) -> Result<SpawnResult> {
let (mut cmd, sandbox_kind) = build_command(shim, config_path, &config)?;
let (landlock_raw, landlock_status) = prepare_landlock(&config, shim, config_path)?;
let landlock_owned = landlock_raw.map(|fd| {
unsafe { OwnedFd::from_raw_fd(fd) }
});
cmd.stdin(Stdio::null());
let watchdog_fd = config.watchdog_fd;
let die_with_parent = config.die_with_parent;
if let Some(file) = config.stderr_file {
cmd.stderr(Stdio::from(file));
}
if let Some(fd) = watchdog_fd {
cmd.env(ENV_WATCHDOG_FD, fd.to_string());
}
pre_exec::apply(
&mut cmd,
pre_exec::PreserveFds {
watchdog: watchdog_fd,
landlock: landlock_owned.as_ref().map(AsRawFd::as_raw_fd),
},
die_with_parent,
);
let child = cmd.spawn()?;
drop(landlock_owned);
let mac = match sandbox_kind {
SandboxKind::Seatbelt => LayerStatus::Enforced,
SandboxKind::Bwrap | SandboxKind::Noop => {
if cfg!(target_os = "macos") {
LayerStatus::Disabled
} else {
LayerStatus::NotApplicable
}
}
};
Ok(SpawnResult {
child,
security: SecurityReport {
sandbox: sandbox_kind,
landlock: landlock_status,
mac,
},
})
}
#[allow(
clippy::missing_const_for_fn,
clippy::unnecessary_wraps,
reason = "Result/errors only arise on Linux Landlock path; macOS always succeeds"
)]
fn prepare_landlock(
config: &JailConfig,
shim: &Path,
config_path: &Path,
) -> Result<(Option<RawFd>, LayerStatus)> {
if !config.landlock {
return Ok((
None,
if cfg!(target_os = "linux") {
LayerStatus::Disabled
} else {
LayerStatus::NotApplicable
},
));
}
#[cfg(target_os = "linux")]
{
match landlock_setup::build_fd(config, shim, config_path) {
Ok(Some(fd)) => Ok((Some(fd), LayerStatus::Enforced)),
Ok(None) => {
if config.allow_degraded_security {
Ok((None, LayerStatus::Degraded))
} else {
Err(Error::LandlockUnavailable)
}
}
Err(msg) => Err(Error::Landlock(msg)),
}
}
#[cfg(not(target_os = "linux"))]
{
let _ = (shim, config_path);
Ok((None, LayerStatus::NotApplicable))
}
}
#[allow(
clippy::unnecessary_wraps,
reason = "Linux returns BwrapUnavailable; other platforms always succeed"
)]
fn build_command(
shim: &Path,
config_path: &Path,
config: &JailConfig,
) -> Result<(Command, SandboxKind)> {
if let Some(ref sandbox) = config.sandbox
&& let Some(cmd) = sandbox.wrap(shim, config_path, config)
{
return Ok((cmd, sandbox.kind()));
}
if let Some((cmd, kind)) = platform_sandbox(shim, config_path, config) {
return Ok((cmd, kind));
}
#[cfg(target_os = "linux")]
{
Err(Error::BwrapUnavailable)
}
#[cfg(not(target_os = "linux"))]
{
let mut cmd = Command::new(shim);
cmd.arg(config_path);
Ok((cmd, SandboxKind::Noop))
}
}
fn platform_sandbox(
shim: &Path,
config_path: &Path,
config: &JailConfig,
) -> Option<(Command, SandboxKind)> {
#[cfg(target_os = "linux")]
{
let sandbox = BwrapSandbox;
if let Some(cmd) = sandbox.wrap(shim, config_path, config) {
return Some((cmd, SandboxKind::Bwrap));
}
}
#[cfg(target_os = "macos")]
{
let sandbox = SeatbeltSandbox;
if let Some(cmd) = sandbox.wrap(shim, config_path, config) {
return Some((cmd, SandboxKind::Seatbelt));
}
}
let _ = (shim, config_path, config);
None
}
#[cfg(test)]
#[allow(clippy::unwrap_used, reason = "unit tests")]
mod tests {
use super::*;
fn jail(bwrap_path: Option<PathBuf>) -> JailConfig {
JailConfig {
rootfs: None,
root_disk: None,
readonly_paths: vec![],
socks_dir: PathBuf::from("/tmp/bux-socks"),
virtiofs_paths: vec![],
watchdog_fd: None,
sandbox: None,
stderr_file: None,
landlock: false,
allow_degraded_security: false,
die_with_parent: true,
network_host: false,
bwrap_path,
}
}
#[cfg(target_os = "linux")]
#[test]
fn linux_auto_detect_without_bwrap_is_unavailable() {
let err = build_command(
Path::new("/usr/bin/true"),
Path::new("/tmp/cfg.json"),
&jail(None),
)
.unwrap_err();
assert!(
matches!(err, Error::BwrapUnavailable),
"missing bwrap must not fall through to Noop: {err}"
);
assert!(
err.to_string().contains("sh.qntx.org/bux"),
"error must name the install URL: {err}"
);
}
#[cfg(target_os = "linux")]
fn fd_count() -> usize {
std::fs::read_dir("/proc/self/fd").map_or(0, Iterator::count)
}
#[cfg(target_os = "linux")]
#[test]
fn spawn_without_bwrap_does_not_leak_landlock_fd() {
let cfg = JailConfig {
landlock: true,
..jail(None)
};
let before = fd_count();
let err = spawn(Path::new("/usr/bin/true"), Path::new("/tmp/cfg.json"), cfg).unwrap_err();
assert!(
matches!(err, Error::BwrapUnavailable),
"auto-detect without bwrap must fail closed: {err}"
);
let after = fd_count();
assert_eq!(
after, before,
"BwrapUnavailable must not leak the landlock ruleset fd"
);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_explicit_noop_still_wraps() {
let mut cfg = jail(None);
cfg.sandbox = Some(Box::new(NoopSandbox));
let (cmd, kind) =
build_command(Path::new("/usr/bin/true"), Path::new("/tmp/cfg.json"), &cfg).unwrap();
assert_eq!(kind, SandboxKind::Noop);
assert_eq!(cmd.get_program(), "/usr/bin/true");
}
#[cfg(target_os = "linux")]
#[test]
fn linux_auto_detect_with_bwrap_path_is_bwrap() {
let (cmd, kind) = build_command(
Path::new("/usr/bin/true"),
Path::new("/tmp/cfg.json"),
&jail(Some(PathBuf::from("/bin/true"))),
)
.unwrap();
assert_eq!(kind, SandboxKind::Bwrap);
assert_eq!(cmd.get_program(), "/bin/true");
}
#[cfg(not(target_os = "linux"))]
#[test]
fn non_linux_auto_detect_without_bwrap_is_noop_or_seatbelt() {
let (cmd, kind) = build_command(
Path::new("/usr/bin/true"),
Path::new("/tmp/cfg.json"),
&jail(None),
)
.unwrap();
assert!(
matches!(kind, SandboxKind::Noop | SandboxKind::Seatbelt),
"non-Linux must not require bwrap: {kind:?}"
);
let program = cmd.get_program();
assert!(
program == "/usr/bin/true"
|| program == "sandbox-exec"
|| program == "/usr/bin/sandbox-exec",
"program must be shim or sandbox-exec: {program:?}"
);
}
}