#![allow(unsafe_code)]
use std::{collections::HashMap, process::ExitStatus};
use tokio::process::Command;
use super::{landlock, seccomp};
use crate::{
error::CoreError,
profile::SandboxProfile,
sandbox::{BackendOptions, linux::probe::ProbeResult},
};
pub(super) async fn run_sandboxed(
profile: &SandboxProfile,
proxy_port: Option<u16>,
command: &[String],
extra_env: &HashMap<String, String>,
probe: &ProbeResult,
options: BackendOptions,
) -> Result<ExitStatus, CoreError> {
enforce_network_capability(profile, proxy_port, probe, options)?;
let compiled_landlock = landlock::compile(profile, proxy_port, probe, options)?;
let compiled_seccomp = seccomp::compile(profile, proxy_port, probe, options)?;
let program = command
.first()
.ok_or_else(|| CoreError::Backend("empty command vector".to_owned()))?
.clone();
let mut cmd = Command::new(&program);
cmd.args(&command[1..]);
for (k, v) in extra_env {
cmd.env(k, v);
}
cmd.stdin(std::process::Stdio::inherit());
cmd.stdout(std::process::Stdio::inherit());
cmd.stderr(std::process::Stdio::inherit());
let landlock::CompiledLandlock { ruleset } = compiled_landlock;
let seccomp::CompiledSeccomp { kill, errno } = compiled_seccomp;
let mut ruleset_slot = Some(ruleset);
unsafe {
cmd.pre_exec(move || {
let rc = libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
if rc != 0 {
return Err(std::io::Error::last_os_error());
}
let ruleset = ruleset_slot
.take()
.ok_or_else(|| std::io::Error::from(std::io::ErrorKind::Other))?;
ruleset
.restrict_self()
.map_err(|_| std::io::Error::from(std::io::ErrorKind::PermissionDenied))?;
seccompiler::apply_filter_all_threads(&kill)
.map_err(|_| std::io::Error::from(std::io::ErrorKind::PermissionDenied))?;
seccompiler::apply_filter_all_threads(&errno)
.map_err(|_| std::io::Error::from(std::io::ErrorKind::PermissionDenied))?;
Ok(())
});
}
let status = cmd
.status()
.await
.map_err(|e| CoreError::Backend(format!("failed to spawn sandboxed command: {e}")))?;
Ok(status)
}
#[cfg_attr(not(test), allow(dead_code))]
const PRE_EXEC_FORBIDDEN_TOKENS: &[&str] = &[
"format!",
"println!",
"eprintln!",
"write!",
"writeln!",
"String::from",
"String::new",
"Vec::new",
"Vec::with_capacity",
"Box::new",
"tokio::",
"tracing::",
];
fn enforce_network_capability(
profile: &SandboxProfile,
proxy_port: Option<u16>,
probe: &ProbeResult,
options: BackendOptions,
) -> Result<(), CoreError> {
if probe.abi.supports_net_port_filter() {
return Ok(());
}
if profile.allow_all_network {
return Ok(());
}
let needs_net_filter =
profile.enable_proxy || !profile.allow_domains.is_empty() || proxy_port.is_some();
if !needs_net_filter {
return Ok(());
}
if options.allow_degraded {
tracing::warn!(
abi = probe.abi.as_str(),
"Landlock ABI <v4 detected; falling back to seccomp connect() arg filter — egress \
port pinning is best-effort. Continuing under --allow-degraded."
);
return Ok(());
}
Err(CoreError::BackendDegraded {
capability: "landlock-net-connect-tcp",
detail: format!(
"kernel ABI is {} but the resolved profile requires TCP egress pinning. Upgrade to a \
kernel with Landlock ABI v4 (Linux 6.7+) or pass --allow-degraded to proceed with a \
best-effort seccomp arg-filter.",
probe.abi.as_str()
),
})
}
#[cfg(test)]
mod tests {
use super::PRE_EXEC_FORBIDDEN_TOKENS;
#[test]
fn test_pre_exec_closure_uses_no_forbidden_tokens() {
let src = include_str!("exec.rs");
let begin = src
.find("// BEGIN PRE_EXEC")
.expect("missing BEGIN PRE_EXEC sentinel in exec.rs");
let end = src
.find("// END PRE_EXEC")
.expect("missing END PRE_EXEC sentinel in exec.rs");
assert!(end > begin, "END PRE_EXEC before BEGIN PRE_EXEC");
let window = &src[begin..end];
for token in PRE_EXEC_FORBIDDEN_TOKENS {
assert!(
!window.contains(token),
"pre_exec closure body contains forbidden token `{token}` — see §6 invariants in \
specs/cross-platform-backend-design.md"
);
}
}
}