use std::collections::HashSet;
use std::path::Path;
use std::process::Command;
use std::sync::{Mutex, OnceLock};
use super::protocol::bodies::{ProbeName, ProbeNetRequest, ProbeOutcome, ProbeReport};
use super::{ClosureFault, Limits, Runner, RunnerError, RunnerMessage};
use crate::hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RuntimeProfile {
Plain,
Accelerator,
Provider,
}
impl RuntimeProfile {
pub(super) fn label(self) -> &'static str {
match self {
RuntimeProfile::Plain => "plain",
RuntimeProfile::Accelerator => "accelerator",
RuntimeProfile::Provider => "provider",
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProviderJail {
closures: Vec<std::path::PathBuf>,
service_prefix: Option<String>,
temp_env: Option<String>,
}
impl ProviderJail {
pub fn new(
closures: Vec<std::path::PathBuf>,
service_prefix: Option<String>,
temp_env: Option<String>,
) -> Result<ProviderJail, RunnerError> {
let mut real_closures = Vec::with_capacity(closures.len());
let total = closures.len();
for (index, closure) in closures.iter().enumerate() {
let position = index + 1;
let refuse = |fault| {
RunnerError::new(
"sandbox_unavailable",
RunnerMessage::Closure {
position,
count: total,
fault,
},
)
};
if !closure.is_absolute() {
return Err(refuse(ClosureFault::NotAbsolute));
}
if !crate::convert::provider::is_normal_form(closure) {
return Err(refuse(ClosureFault::NotNormal));
}
if crate::convert::provider::is_symlink_entry(closure) {
return Err(refuse(ClosureFault::Symlink));
}
if crate::convert::provider::is_shared_root(closure) {
return Err(refuse(ClosureFault::SharedRoot));
}
let Some(real) = crate::convert::provider::canonical(closure) else {
return Err(refuse(ClosureFault::Unresolved));
};
if !crate::convert::provider::is_real_entry(closure) {
return Err(refuse(ClosureFault::NotFileOrDirectory));
}
if crate::convert::provider::is_shared_root(&real) {
return Err(refuse(ClosureFault::ResolvedSharedRoot));
}
real_closures.push(real);
}
if let Some(prefix) = &service_prefix
&& !crate::convert::provider::is_service_namespace(prefix)
{
return Err(RunnerError::new(
"sandbox_unavailable",
RunnerMessage::ServiceNamespace,
));
}
if let Some(name) = &temp_env
&& !crate::convert::provider::is_env_name(name)
{
return Err(RunnerError::new(
"sandbox_unavailable",
RunnerMessage::TempDirectoryVariable,
));
}
Ok(ProviderJail {
closures: real_closures,
service_prefix,
temp_env,
})
}
pub fn closures(&self) -> &[std::path::PathBuf] {
&self.closures
}
pub fn service_prefix(&self) -> Option<&str> {
self.service_prefix.as_deref()
}
pub fn temp_env(&self) -> Option<&str> {
self.temp_env.as_deref()
}
}
pub struct SpawnSpec<'a> {
pub worker: &'a Path,
pub jail: &'a Path,
pub mode: &'a str,
pub limits: &'a Limits,
pub seccomp: bool,
pub exec_grants: &'a [std::path::PathBuf],
pub read_grants: &'a [std::path::PathBuf],
pub runtime_profile: RuntimeProfile,
pub provider: Option<&'a ProviderJail>,
}
pub trait JailBackend: Send + Sync {
fn name(&self) -> &'static str;
fn command(&self, spec: &SpawnSpec<'_>) -> Result<Command, RunnerError>;
fn policy_material(&self) -> String;
fn probe(&self, runner: &Runner) -> Result<(), RunnerError>;
}
pub fn platform_backend() -> Result<Box<dyn JailBackend>, RunnerError> {
#[cfg(target_os = "linux")]
{
Ok(Box::new(super::linux::NamespaceJail))
}
#[cfg(target_os = "macos")]
{
Ok(Box::new(super::macos::SeatbeltJail))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err(RunnerError::new(
"sandbox_unavailable",
RunnerMessage::PlatformBackendMissing,
))
}
}
fn probe_cache() -> &'static Mutex<HashSet<String>> {
static CACHE: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
CACHE.get_or_init(|| Mutex::new(HashSet::new()))
}
pub(super) fn policy_digest(
backend: &dyn JailBackend,
worker: &Path,
exec_grants: &[std::path::PathBuf],
read_grants: &[std::path::PathBuf],
runtime_profile: RuntimeProfile,
provider: Option<&ProviderJail>,
) -> Result<String, RunnerError> {
let worker_hash = hash::hash_file(worker).map_err(|e| {
let kind = match e {
crate::Error::Io { source, .. } => source.kind(),
_ => std::io::ErrorKind::Other,
};
RunnerError::new("worker_not_found", RunnerMessage::WorkerHash(kind))
})?;
let grant_lines = |label: &str, grants: &[std::path::PathBuf]| {
grants
.iter()
.map(|path| format!("{label}={}", path.display()))
.collect::<Vec<_>>()
.join("\n")
};
let provider_material = match provider {
Some(provider) => format!(
"closures={}\nservice-prefix={}\ntemp-env={}",
provider
.closures
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(","),
provider.service_prefix().unwrap_or_default(),
provider.temp_env().unwrap_or_default()
),
None => String::new(),
};
let material = format!(
"{}\n{}\n{}\n{}\n{}\n{}\nruntime-profile={}\n{}",
backend.name(),
backend.policy_material(),
worker.display(),
worker_hash,
grant_lines("exec", exec_grants),
grant_lines("read", read_grants),
runtime_profile.label(),
provider_material,
);
Ok(hash::hash_bytes(material.as_bytes()))
}
pub(super) fn ensure_probed(runner: &Runner, digest: &str) -> Result<(), RunnerError> {
if probe_cache()
.lock()
.is_ok_and(|cache| cache.contains(digest))
{
return Ok(());
}
runner.backend().probe(runner)?;
if let Ok(mut cache) = probe_cache().lock() {
cache.insert(digest.to_string());
}
Ok(())
}
pub fn probe_cached(digest: &str) -> bool {
probe_cache()
.lock()
.is_ok_and(|cache| cache.contains(digest))
}
pub(super) fn probe_net(runner: &Runner, seccomp: bool) -> Result<(), RunnerError> {
let listener = UnixProbeListener::bind()?;
let tcp4 = std::net::TcpListener::bind("127.0.0.1:0").map_err(probe_bind_error)?;
let tcp6 = std::net::TcpListener::bind("[::1]:0").map_err(probe_bind_error)?;
let tcp4_target = tcp4.local_addr().map_err(probe_bind_error)?.to_string();
let tcp6_target = tcp6.local_addr().map_err(probe_bind_error)?.to_string();
let payload = serde_json::to_value(ProbeNetRequest {
unix_target: listener.target.clone(),
tcp4_target,
tcp6_target,
})
.map_err(|_| RunnerError::new("sandbox_probe_failed", RunnerMessage::ProbeRequestEncode))?;
let response = runner.run_unprobed("probe-net", payload, &[], seccomp)?;
let Some(ok) = response.ok else {
return Err(RunnerError::new(
"sandbox_probe_failed",
RunnerMessage::ProbeReportedFailure,
));
};
let report: ProbeReport = serde_json::from_value(ok)
.map_err(|_| RunnerError::new("sandbox_probe_failed", RunnerMessage::ProbeResponseParse))?;
let expected = [
ProbeName::Ipv4Tcp,
ProbeName::Ipv6Tcp,
ProbeName::UnixSocket,
ProbeName::UdpSend,
];
for name in expected {
let Some(attempt) = report.attempts.iter().find(|a| a.name == name) else {
return Err(RunnerError::new(
"sandbox_probe_failed",
RunnerMessage::ProbeAttemptMissing(name),
));
};
if attempt.outcome != ProbeOutcome::Denied {
return Err(RunnerError::new(
"sandbox_probe_failed",
RunnerMessage::ProbeAttempt {
name,
outcome: attempt.outcome,
error_kind: attempt.error_kind,
},
));
}
}
Ok(())
}
struct UnixProbeListener {
target: String,
_listener: std::os::unix::net::UnixListener,
_dir: Option<tempfile::TempDir>,
}
impl UnixProbeListener {
#[cfg(target_os = "linux")]
fn bind() -> Result<UnixProbeListener, RunnerError> {
use std::os::linux::net::SocketAddrExt;
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(0);
let name = format!(
"text-mirror-probe-{}-{}",
std::process::id(),
NEXT.fetch_add(1, Ordering::Relaxed)
);
let address = std::os::unix::net::SocketAddr::from_abstract_name(name.as_bytes())
.map_err(probe_bind_error)?;
let listener =
std::os::unix::net::UnixListener::bind_addr(&address).map_err(probe_bind_error)?;
Ok(UnixProbeListener {
target: name,
_listener: listener,
_dir: None,
})
}
#[cfg(not(target_os = "linux"))]
fn bind() -> Result<UnixProbeListener, RunnerError> {
let dir = tempfile::tempdir().map_err(probe_bind_error)?;
let path = dir.path().join("probe.sock");
let listener = std::os::unix::net::UnixListener::bind(&path).map_err(probe_bind_error)?;
Ok(UnixProbeListener {
target: path.display().to_string(),
_listener: listener,
_dir: Some(dir),
})
}
}
fn probe_bind_error(e: std::io::Error) -> RunnerError {
RunnerError::new(
"sandbox_probe_failed",
RunnerMessage::ProbeListener(e.kind()),
)
}