use crate::error::{Error, Result};
use crate::policy::{AccessPolicy, NetworkAccess, ReadAccess, UnixSocketAccess};
use std::ffi::{CStr, CString, OsStr, OsString};
use std::fmt::{self, Write};
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::ptr;
const SBPL_PROFILE_FLAGS: u64 = 0;
pub(crate) fn execute(
policy: &AccessPolicy,
_policy_base: &Path,
command: &OsStr,
args: &[OsString],
) -> Result<()> {
let profile =
render_profile(policy, command).map_err(|error| Error::system(error.to_string()))?;
<SystemSeatbelt as Seatbelt>::apply_profile(&profile)?;
let error = Command::new(command).args(args).exec();
Err(Error::command(
Some(command.to_os_string()),
error.to_string(),
))
}
trait Seatbelt {
fn apply_profile(profile: &str) -> Result<()>;
}
struct SystemSeatbelt;
impl Seatbelt for SystemSeatbelt {
fn apply_profile(profile: &str) -> Result<()> {
let profile = CString::new(profile).map_err(|source| {
let nul_position = source.nul_position();
Error::system(format!(
"generated SBPL profile contains an interior NUL byte at offset {nul_position}"
))
})?;
let mut errorbuf = ptr::null_mut();
let rc =
unsafe { ffi::sandbox_init(profile.as_ptr(), SBPL_PROFILE_FLAGS, &raw mut errorbuf) };
if rc == 0 {
Ok(())
} else {
Err(Error::system(take_sandbox_error(errorbuf)))
}
}
}
fn render_profile(
policy: &AccessPolicy,
command: &OsStr,
) -> std::result::Result<String, fmt::Error> {
let mut sb = String::new();
writeln!(sb, "(version 1)")?;
writeln!(sb, "(deny default)")?;
render_process_rules(&mut sb, command)?;
render_write_rules(&mut sb, &policy.write_roots)?;
render_read_rules(&mut sb, &policy.read_access)?;
render_network_rules(&mut sb, &policy.network_access)?;
Ok(sb)
}
fn render_process_rules(sb: &mut String, command: &OsStr) -> fmt::Result {
let cmd_str = command.to_string_lossy();
writeln!(
sb,
"(allow process-exec (literal \"{}\"))",
escape_sbpl_literal(&cmd_str)
)?;
writeln!(sb, "(allow process-fork)")
}
fn render_write_rules(sb: &mut String, write_roots: &[PathBuf]) -> fmt::Result {
for root in write_roots {
let escaped = escape_sbpl_literal(&root.to_string_lossy());
writeln!(sb, "(allow file-write* (subpath \"{escaped}\"))")?;
}
Ok(())
}
fn render_read_rules(sb: &mut String, read_access: &ReadAccess) -> fmt::Result {
match read_access {
ReadAccess::Unrestricted => sb.push_str("(allow file-read*)\n"),
ReadAccess::AllowRoots(roots) => {
writeln!(sb, "(deny file-read*)")?;
for root in roots {
let escaped = escape_sbpl_literal(&root.to_string_lossy());
writeln!(sb, "(allow file-read* (subpath \"{escaped}\"))")?;
}
}
}
Ok(())
}
fn render_network_rules(sb: &mut String, network: &NetworkAccess) -> fmt::Result {
if network.is_unrestricted() {
sb.push_str("(allow network*)\n");
return Ok(());
}
if network.restrict_connect_tcp {
sb.push_str("(deny network-outbound)\n");
for port in &network.connect_tcp_ports {
writeln!(
sb,
"(allow network-outbound (remote tcp \"localhost:{port}\"))"
)?;
}
}
if network.restrict_bind_tcp {
sb.push_str("(deny network-bind)\n");
sb.push_str("(deny network-inbound)\n");
}
if network.local_tcp_bind {
sb.push_str("(allow network-bind (local tcp \"localhost:*\"))\n");
sb.push_str("(allow network-inbound (local tcp \"localhost:*\"))\n");
}
match &network.unix_socket_access {
UnixSocketAccess::Unrestricted => {
}
UnixSocketAccess::AllowPaths(paths) if paths.is_empty() => {
sb.push_str("(deny network*)\n");
}
UnixSocketAccess::AllowPaths(paths) => {
sb.push_str("(deny network*)\n");
for path in paths {
let escaped = escape_sbpl_literal(&path.to_string_lossy());
writeln!(
sb,
"(allow network-outbound (remote unix-socket (path-literal \"{escaped}\")))"
)?;
writeln!(
sb,
"(allow network-bind (local unix-socket (path-literal \"{escaped}\")))"
)?;
}
}
}
Ok(())
}
fn escape_sbpl_literal(path: &str) -> String {
let mut escaped = String::with_capacity(path.len());
for ch in path.chars() {
match ch {
'"' => escaped.push_str("\\\""),
'\\' => escaped.push_str("\\\\"),
'(' => escaped.push_str("\\("),
')' => escaped.push_str("\\)"),
'\n' => escaped.push_str("\\n"),
_ => escaped.push(ch),
}
}
escaped
}
fn take_sandbox_error(errorbuf: *mut libc::c_char) -> String {
if errorbuf.is_null() {
return "sandbox_init failed without an error message".to_string();
}
let message = unsafe { CStr::from_ptr(errorbuf) }
.to_string_lossy()
.into_owned();
unsafe { ffi::sandbox_free_error(errorbuf) };
message
}
mod ffi {
use libc::{c_char, c_int};
#[link(name = "sandbox")]
unsafe extern "C" {
pub(super) fn sandbox_init(
profile: *const c_char,
flags: u64,
errorbuf: *mut *mut c_char,
) -> c_int;
pub(super) fn sandbox_free_error(errorbuf: *mut c_char);
}
}