Skip to main content

sandbox_run/
lib.rs

1//! A sandboxed process execution library for Linux and macOS.
2//!
3//! This crate provides a `Command` API similar to `std::process::Command` but with
4//! the ability to restrict process execution using platform-native sandboxing:
5//!
6//! - **Linux**: Uses [Landlock](https://landlock.io/) (requires kernel 5.13+)
7//! - **macOS**: Uses the native sandbox framework
8//!
9//! The API mirrors systemd's execution environment options where applicable.
10//!
11//! # Example
12//!
13//! ```no_run
14//! use std::path::Path;
15//! use sandbox_run::{Command, Sandbox, ProtectSystem, ProtectHome};
16//!
17//! // Create a sandbox with systemd-style options
18//! let sandbox = Sandbox::new()
19//!     .protect_system(ProtectSystem::Strict)
20//!     .protect_home(ProtectHome::Yes)
21//!     .read_write_paths([Path::new("/tmp/myapp")])
22//!     .private_network(true)
23//!     .no_new_privileges(true)
24//!     .limit_nofile(256)
25//!     .build()
26//!     .unwrap();
27//!
28//! // Run a command under the sandbox
29//! let status = Command::new("myapp")
30//!     .sandbox(sandbox)
31//!     .status()
32//!     .expect("failed to execute process");
33//! ```
34//!
35//! # Systemd-Compatible Options
36//!
37//! | Sandbox Method | systemd Directive |
38//! |----------------|-------------------|
39//! | `protect_system()` | `ProtectSystem=` |
40//! | `protect_home()` | `ProtectHome=` |
41//! | `read_only_paths()` | `ReadOnlyPaths=` |
42//! | `read_write_paths()` | `ReadWritePaths=` |
43//! | `inaccessible_paths()` | `InaccessiblePaths=` |
44//! | `exec_paths()` | `ExecPaths=` |
45//! | `no_exec_paths()` | `NoExecPaths=` |
46//! | `private_network()` | `PrivateNetwork=` |
47//! | `no_new_privileges()` | `NoNewPrivileges=` |
48//! | `limit_nofile()` | `LimitNOFILE=` |
49//! | `limit_nproc()` | `LimitNPROC=` |
50//! | `limit_core()` | `LimitCORE=` |
51//! | `limit_fsize()` | `LimitFSIZE=` |
52//!
53//! # Platform Notes
54//!
55//! ## Linux
56//!
57//! The Linux implementation uses Landlock, which is an allow-list based sandboxing
58//! mechanism. `ProtectSystem::Strict` works best as it makes the entire filesystem
59//! read-only and you can add exceptions with `read_write_paths()`.
60//!
61//! ## macOS
62//!
63//! The macOS implementation uses the native sandbox framework with generated profiles.
64//! All protection modes work well on macOS.
65
66mod command;
67mod error;
68mod platform;
69mod sandbox;
70
71pub use command::Command;
72pub use error::SandboxError;
73pub use sandbox::{ProtectHome, ProtectSystem, Sandbox, SandboxPolicy};
74
75/// Whether the running kernel enforces Landlock (Linux 5.13+ with
76/// Landlock enabled). Callers that fail open when sandboxing is
77/// unavailable can use this to warn that spawned processes will run
78/// unconfined.
79#[cfg(target_os = "linux")]
80pub use platform::landlock_available;
81
82/// Apply a compiled policy to the **current** process. Intended for
83/// integrations that build their own `pre_exec` closure (e.g. when
84/// running under a custom runtime that owns the child handle, like
85/// `tokio::process::Command`). Must be called in the child after fork
86/// and before exec.
87pub fn apply_sandbox(policy: &SandboxPolicy) -> Result<(), SandboxError> {
88    platform::apply_sandbox(policy)
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use std::path::Path;
95    use std::process::Stdio;
96
97    #[test]
98    fn test_command_without_sandbox() {
99        let output = Command::new("echo")
100            .arg("hello")
101            .stdout(Stdio::piped())
102            .output()
103            .expect("failed to execute echo");
104
105        assert!(output.status.success());
106        assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "hello");
107    }
108
109    #[test]
110    fn test_sandbox_builder_fluent_api() {
111        let sandbox = Sandbox::new()
112            .protect_system(ProtectSystem::Strict)
113            .protect_home(ProtectHome::Yes)
114            .read_write_paths([Path::new("/tmp")])
115            .private_network(true)
116            .no_new_privileges(true)
117            .limit_nofile(256)
118            .limit_nproc(10)
119            .limit_core(0)
120            .build()
121            .expect("failed to build sandbox");
122
123        // Just verify the sandbox builds successfully
124        let _ = sandbox;
125    }
126
127    #[test]
128    #[cfg(target_os = "macos")]
129    fn test_protect_home_blocks_home_access() {
130        let sandbox = Sandbox::new()
131            .protect_home(ProtectHome::Yes)
132            .build()
133            .expect("failed to build sandbox");
134
135        // Try to list home directory
136        let home = std::env::var("HOME").unwrap_or_else(|_| "/Users".to_string());
137        let output = Command::new("ls")
138            .arg(&home)
139            .sandbox(sandbox)
140            .stderr(Stdio::piped())
141            .output()
142            .expect("failed to execute ls");
143
144        // Should fail because home is inaccessible
145        assert!(!output.status.success());
146    }
147
148    #[test]
149    #[cfg(target_os = "macos")]
150    fn test_private_network_blocks_network() {
151        let sandbox = Sandbox::new()
152            .private_network(true)
153            .build()
154            .expect("failed to build sandbox");
155
156        // Try to make a network connection
157        let output = Command::new("curl")
158            .arg("--connect-timeout")
159            .arg("1")
160            .arg("http://127.0.0.1:1")
161            .sandbox(sandbox)
162            .stderr(Stdio::piped())
163            .output()
164            .expect("failed to execute curl");
165
166        // Should fail because network is blocked
167        // Note: curl may fail with different exit codes, but the sandbox should deny the connection
168        assert!(!output.status.success());
169    }
170
171    #[test]
172    #[cfg(target_os = "macos")]
173    fn test_inaccessible_paths() {
174        // Create a test file
175        let temp_dir = std::env::temp_dir();
176        let test_file = temp_dir.join("sandbox_test_inaccessible.txt");
177        std::fs::write(&test_file, "secret content").expect("failed to write test file");
178
179        let sandbox = Sandbox::new()
180            .inaccessible_paths([&test_file])
181            .build()
182            .expect("failed to build sandbox");
183
184        let output = Command::new("cat")
185            .arg(&test_file)
186            .sandbox(sandbox)
187            .stderr(Stdio::piped())
188            .output()
189            .expect("failed to execute cat");
190
191        // Clean up
192        let _ = std::fs::remove_file(&test_file);
193
194        // Should fail because file is inaccessible
195        assert!(!output.status.success());
196    }
197
198    #[test]
199    #[cfg(target_os = "macos")]
200    fn test_read_only_paths() {
201        let temp_dir = std::env::temp_dir();
202        let test_file = temp_dir.join("sandbox_test_readonly.txt");
203        std::fs::write(&test_file, "initial content").expect("failed to write test file");
204
205        let sandbox = Sandbox::new()
206            .read_only_paths([&temp_dir])
207            .build()
208            .expect("failed to build sandbox");
209
210        // Try to write to the file - should fail
211        let output = Command::new("sh")
212            .arg("-c")
213            .arg(format!("echo 'new content' > {}", test_file.display()))
214            .sandbox(sandbox)
215            .stderr(Stdio::piped())
216            .output()
217            .expect("failed to execute sh");
218
219        // Clean up
220        let _ = std::fs::remove_file(&test_file);
221
222        // Should fail because the path is read-only
223        assert!(!output.status.success());
224    }
225
226    #[test]
227    #[cfg(target_os = "linux")]
228    fn test_protect_system_strict() {
229        // With strict mode, only read_write_paths are writable
230        let sandbox = Sandbox::new()
231            .protect_system(ProtectSystem::Strict)
232            .read_write_paths([Path::new("/tmp")])
233            .build()
234            .expect("failed to build sandbox");
235
236        // Create a temp file to verify /tmp is writable
237        let output = Command::new("sh")
238            .arg("-c")
239            .arg("echo test > /tmp/sandbox_test_strict.txt && cat /tmp/sandbox_test_strict.txt")
240            .sandbox(sandbox)
241            .stdout(Stdio::piped())
242            .stderr(Stdio::piped())
243            .output()
244            .expect("failed to execute sh");
245
246        // Clean up
247        let _ = std::fs::remove_file("/tmp/sandbox_test_strict.txt");
248
249        assert!(
250            output.status.success(),
251            "Command failed: {}",
252            String::from_utf8_lossy(&output.stderr)
253        );
254    }
255}