procjail/lib.rs
1//! # procjail
2//!
3//! Process sandbox for running untrusted code in real runtimes.
4//!
5//! When security tools need to execute untrusted code (npm packages, pip
6//! packages, browser extensions, binaries), they need containment that
7//! actually works. This crate provides kernel-level isolation using the
8//! best available mechanism on the host.
9//!
10//! # Containment Strategies (ordered by preference)
11//!
12//! 1. **unshare** — Linux namespaces (PID, network, mount, user). No root needed.
13//! 2. **bubblewrap (bwrap)** — Lightweight container (Flatpak uses this). Rootless.
14//! 3. **firejail** — Feature-rich sandbox. Needs installation.
15//! 4. **rlimits** — Basic resource limits only. Always available. Least secure.
16//!
17//! The sandbox auto-selects the best available strategy, or you can force one.
18//!
19//! # Usage
20//!
21//! ```rust,no_run
22//! use std::path::Path;
23//! use procjail::{SandboxConfig, SandboxedProcess};
24//!
25//! let config = SandboxConfig::builder()
26//! .runtime("/usr/bin/node")
27//! .max_memory_mb(256)
28//! .max_cpu_seconds(30)
29//! .max_fds(64)
30//! .allow_localhost(false)
31//! .env_passthrough(&["HOME", "PATH", "NODE_PATH"])
32//! .env_strip_secrets(true)
33//! .build();
34//!
35//! let mut proc = SandboxedProcess::spawn(
36//! Path::new("/path/to/harness.js"),
37//! Path::new("/path/to/package"),
38//! &config,
39//! ).unwrap();
40//!
41//! proc.send(r#"{"method":"eval","args":["1+1"]}"#).unwrap();
42//! if let Some(line) = proc.recv().unwrap() {
43//! println!("observation: {}", line);
44//! }
45//! ```
46//!
47//! # Architecture
48//!
49//! ```text
50//! Parent (full privileges)
51//! │
52//! ├── stdin pipe → probes flow in
53//! ├── stdout pipe ← observations flow out
54//! │
55//! └── [containment layer]
56//! ├── PID namespace (process isolation)
57//! ├── NET namespace (no external network)
58//! ├── MNT namespace (read-only filesystem)
59//! ├── USER namespace (unprivileged)
60//! ├── rlimits (memory, CPU, FDs)
61//! └── env stripping (no secrets leak)
62//! ```
63
64// Note: unsafe code is used in process.rs for libc calls
65#![warn(missing_docs)]
66
67mod config;
68mod detect;
69mod error;
70mod process;
71mod strategy;
72
73#[cfg(test)]
74mod adversarial_tests;
75
76pub use config::{EnvMode, SandboxConfig, SandboxConfigBuilder};
77pub use detect::{available_strategy, probe_capabilities, ContainmentLevel};
78pub use error::{ProcjailError, Result};
79pub use process::{ResourceUsage, SandboxedProcess};
80pub use strategy::Strategy;
81
82/// Trait for communicating with a sandboxed process.
83pub trait SandboxedIO {
84 /// Send a line to the process.
85 fn send(&mut self, line: &str) -> Result<()>;
86 /// Receive a line from the process. None = EOF.
87 fn recv(&mut self) -> Result<Option<String>>;
88 /// Kill the process.
89 fn kill(&mut self);
90 /// Check if alive.
91 fn is_alive(&mut self) -> bool;
92}
93
94/// Convenience helper that spawns a sandboxed process with a minimal default config.
95///
96/// Example:
97/// ```rust,no_run
98/// use std::path::Path;
99/// use procjail::quick_spawn;
100///
101/// let _child = quick_spawn(
102/// "node",
103/// Path::new("/abs/path/to/harness.js"),
104/// Path::new("/abs/path/to/workdir"),
105/// )?;
106/// # Ok::<(), procjail::ProcjailError>(())
107/// ```
108pub fn quick_spawn(
109 runtime: &str,
110 harness: impl AsRef<std::path::Path>,
111 work_dir: impl AsRef<std::path::Path>,
112) -> Result<process::SandboxedProcess> {
113 let config = config::SandboxConfig::builder()
114 .runtime(runtime)
115 .timeout_seconds(30)
116 .build();
117 process::SandboxedProcess::spawn(harness.as_ref(), work_dir.as_ref(), &config)
118}