mod process;
mod sandbox;
use std::path::PathBuf;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::log::{LogBuffer, LogEntry};
pub use process::ChildProcess;
pub(crate) use process::{TypedReader, TypedWriter};
pub(crate) enum SandboxConfig {
Disabled,
Enforce {
read_base: Option<PathBuf>,
extra_read_dirs: Vec<PathBuf>,
},
}
impl SandboxConfig {
pub(crate) fn deny_all() -> Self {
SandboxConfig::Enforce {
read_base: None,
extra_read_dirs: Vec::new(),
}
}
}
pub(crate) fn fork_sandboxed<Req, Resp, F>(
sandbox: SandboxConfig,
child_fn: F,
) -> Result<(TypedWriter<Req>, TypedReader<Resp>, ChildProcess)>
where
Req: Serialize + DeserializeOwned,
Resp: Serialize + DeserializeOwned,
F: FnOnce(TypedReader<Req>, TypedWriter<Resp>),
{
process::fork_with_channels(move |req_rx, resp_tx| {
if let SandboxConfig::Enforce {
ref read_base,
ref extra_read_dirs,
} = sandbox
&& let Err(e) = sandbox::enforce_sandbox(read_base.as_deref(), extra_read_dirs)
{
log::warn!("child: sandbox failed: {e:#}");
}
child_fn(req_rx, resp_tx);
})
}
#[derive(Serialize, Deserialize)]
enum ComputeResult<T> {
Ok { value: T, logs: Vec<LogEntry> },
Panicked { logs: Vec<LogEntry> },
}
pub(crate) fn fork_compute<T, F>(sandbox: SandboxConfig, log_buffer: &LogBuffer, f: F) -> Result<T>
where
T: Serialize + DeserializeOwned,
F: FnOnce() -> T,
{
let log_buf = log_buffer.clone();
let (_, mut rx, mut child) =
fork_sandboxed::<(), ComputeResult<T>, _>(sandbox, move |_req_rx, mut resp_tx| {
log_buf.drain();
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
Ok(value) => {
let logs = log_buf.drain();
let _ = resp_tx.send(&ComputeResult::Ok { value, logs });
}
Err(_) => {
log::error!("child: fork_compute panicked");
let logs = log_buf.drain();
let _ = resp_tx.send(&ComputeResult::<T>::Panicked { logs });
}
}
})?;
let result = rx.recv().context("fork_compute: child failed")?;
match result {
ComputeResult::Ok { value, logs } => {
for entry in logs {
log_buffer.push(entry);
}
child.wait()?;
Ok(value)
}
ComputeResult::Panicked { logs } => {
for entry in logs {
log_buffer.push(entry);
}
child.wait()?;
anyhow::bail!("fork_compute: child panicked")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fork_compute_nosandbox<T, F>(log_buffer: &LogBuffer, f: F) -> Result<T>
where
T: Serialize + DeserializeOwned,
F: FnOnce() -> T,
{
fork_compute(SandboxConfig::Disabled, log_buffer, f)
}
#[test]
fn fork_compute_ok_returns_value() {
let log_buf = LogBuffer::new(16);
let result = fork_compute_nosandbox(&log_buf, || 42u64);
assert_eq!(result.unwrap(), 42);
}
#[test]
fn fork_compute_panic_returns_error() {
let log_buf = LogBuffer::new(16);
let result = fork_compute_nosandbox::<String, _>(&log_buf, || {
panic!("deliberate test panic");
});
let err = result.unwrap_err();
assert!(
format!("{err:#}").contains("panicked"),
"expected panic error, got: {err:#}"
);
}
}