├── .gitignore ├── .dockerignore ├── sandbox-ctl ├── src │ ├── logging.rs │ ├── commands.rs │ ├── main.rs │ ├── runner.rs │ ├── profiles.rs │ └── cli.rs └── Cargo.toml ├── Dockerfile ├── lib ├── resources │ ├── mod.rs │ └── tests.rs ├── isolation │ ├── mod.rs │ ├── tests.rs │ └── namespace.rs ├── storage │ ├── mod.rs │ ├── tests.rs │ ├── volumes.rs │ └── filesystem.rs ├── network │ ├── mod.rs │ ├── tests.rs │ └── config.rs ├── execution │ ├── mod.rs │ ├── stream.rs │ ├── init.rs │ └── tests.rs ├── monitoring │ ├── mod.rs │ ├── tests.rs │ ├── ebpf.rs │ └── monitor.rs ├── lib.rs ├── errors.rs └── utils.rs ├── examples ├── seccomp_profiles.rs ├── basic.rs ├── cgroup_limits.rs ├── stream_basic.rs ├── stream_long_running.rs ├── stream_non_blocking.rs ├── cli_basic.rs ├── overlay_filesystem.rs └── error_handling.rs ├── docker-compose.yml ├── LICENSE ├── Cargo.toml ├── flake.lock ├── flake.nix ├── .github └── workflows │ └── ci.yml ├── tests ├── integration_tests.rs └── stress_tests.rs ├── README.md └── TROUBLESHOOTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | tarpaulin-report.html 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .git/ 3 | .gitignore 4 | .github/ 5 | *.md 6 | docs/ 7 | .github/ 8 | .gitlab-ci.yml 9 | .idea/ 10 | *.swp 11 | *.swo 12 | *~ 13 | Dockerfile 14 | .dockerignore 15 | docker-compose.yml 16 | tests/ 17 | coverage/ 18 | *.profraw 19 | *.profdata 20 | .env 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /sandbox-ctl/src/logging.rs: -------------------------------------------------------------------------------- 1 | use env_logger::{Builder, Env}; 2 | use log::LevelFilter; 3 | use std::io::Write; 4 | 5 | /// Initialize logger based on verbose flag 6 | pub fn init_logger(verbose: bool) { 7 | let env = Env::default().filter_or("RUST_LOG", if verbose { "debug" } else { "info" }); 8 | 9 | Builder::from_env(env) 10 | .format(|buf, record| writeln!(buf, "{:5} {}", record.level(), record.args())) 11 | .filter_level(if verbose { 12 | LevelFilter::Debug 13 | } else { 14 | LevelFilter::Info 15 | }) 16 | .init(); 17 | } 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.91-bookworm AS builder 2 | 3 | WORKDIR /build 4 | 5 | # Copy workspace sources 6 | COPY Cargo.toml Cargo.lock ./ 7 | COPY lib ./lib 8 | COPY sandbox-ctl ./sandbox-ctl 9 | 10 | RUN cargo build --release --locked --package sandbox-ctl 11 | 12 | FROM debian:bookworm-slim 13 | 14 | RUN apt-get update && \ 15 | apt-get install -y \ 16 | ca-certificates \ 17 | libssl3 \ 18 | && rm -rf /var/lib/apt/lists/* 19 | 20 | COPY --from=builder /build/target/release/sandbox-ctl /usr/local/bin/sandbox-ctl 21 | RUN mkdir -p /sandbox/workdir /sandbox/volumes 22 | 23 | WORKDIR /sandbox 24 | ENTRYPOINT ["/usr/local/bin/sandbox-ctl"] 25 | CMD ["--help"] 26 | -------------------------------------------------------------------------------- /sandbox-ctl/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sandbox-ctl" 3 | version.workspace = true 4 | edition.workspace = true 5 | authors.workspace = true 6 | license.workspace = true 7 | repository.workspace = true 8 | homepage.workspace = true 9 | description = "CLI tool for sandbox-rs - A comprehensive Rust sandbox implementation" 10 | keywords = ["sandbox", "isolation", "security", "linux", "cli"] 11 | categories = ["command-line-utilities", "development-tools"] 12 | 13 | [dependencies] 14 | sandbox-rs.workspace = true 15 | clap = { version = "4.5", features = ["derive"] } 16 | log = "0.4" 17 | env_logger = "0.11" 18 | 19 | [[bin]] 20 | name = "sandbox-ctl" 21 | path = "src/main.rs" 22 | -------------------------------------------------------------------------------- /lib/resources/mod.rs: -------------------------------------------------------------------------------- 1 | //! Resource limits layer: Cgroup v2 management 2 | //! 3 | //! This module provides resource limit enforcement via Cgroup v2. 4 | //! 5 | //! # Features 6 | //! 7 | //! - **Memory limits**: Hard ceiling with OOM enforcement 8 | //! - **CPU limits**: Weight-based and quota-based scheduling 9 | //! - **Process limits**: Max PID restrictions 10 | //! - **Runtime statistics**: Real-time resource usage tracking 11 | //! 12 | //! # Examples 13 | //! 14 | //! ```ignore 15 | //! use sandbox_rs::resources::CgroupConfig; 16 | //! 17 | //! let config = CgroupConfig::with_memory(100 * 1024 * 1024); 18 | //! ``` 19 | 20 | pub mod cgroup; 21 | pub use cgroup::{Cgroup, CgroupConfig}; 22 | 23 | #[cfg(test)] 24 | mod tests; 25 | -------------------------------------------------------------------------------- /lib/isolation/mod.rs: -------------------------------------------------------------------------------- 1 | //! Isolation layer: Namespace + Seccomp filtering 2 | //! 3 | //! This module provides namespace isolation and syscall filtering 4 | //! for sandboxed processes. 5 | //! 6 | //! # Features 7 | //! 8 | //! - **Namespaces**: PID, IPC, NET, MOUNT, UTS, User 9 | //! - **Seccomp**: BPF-based syscall filtering with profiles 10 | //! 11 | //! # Examples 12 | //! 13 | //! ```ignore 14 | //! use sandbox_rs::isolation::{NamespaceConfig, SeccompProfile}; 15 | //! 16 | //! let ns = NamespaceConfig::default(); 17 | //! let profile = SeccompProfile::IoHeavy; 18 | //! ``` 19 | 20 | pub mod namespace; 21 | pub mod seccomp; 22 | pub mod seccomp_bpf; 23 | pub use namespace::{NamespaceConfig, NamespaceType}; 24 | pub use seccomp::{SeccompFilter, SeccompProfile}; 25 | pub use seccomp_bpf::SeccompBpf; 26 | 27 | #[cfg(test)] 28 | mod tests; 29 | -------------------------------------------------------------------------------- /examples/seccomp_profiles.rs: -------------------------------------------------------------------------------- 1 | //! Seccomp profiles example 2 | 3 | use sandbox_rs::SeccompProfile; 4 | 5 | fn main() { 6 | println!("=== Sandbox RS - Seccomp Profiles ===\n"); 7 | 8 | println!("Available seccomp profiles:\n"); 9 | 10 | for profile in SeccompProfile::all() { 11 | println!("Profile: {:?}", profile); 12 | println!(" Description: {}", profile.description()); 13 | 14 | // Note: Actual syscall filtering would require kernel-level seccomp 15 | // For now, we just demonstrate the API 16 | println!(" Note: Syscall filtering requires root and proper seccomp setup\n"); 17 | } 18 | 19 | println!("\nProfile Usage Example:"); 20 | println!(" let sandbox = SandboxBuilder::new(\"my-box\")"); 21 | println!(" .seccomp_profile(SeccompProfile::IoHeavy)"); 22 | println!(" .build()?;"); 23 | } 24 | -------------------------------------------------------------------------------- /lib/storage/mod.rs: -------------------------------------------------------------------------------- 1 | //! Storage layer: Filesystem and volume management 2 | //! 3 | //! This module provides persistent storage capabilities including 4 | //! overlay filesystems and volume mounts. 5 | //! 6 | //! # Features 7 | //! 8 | //! - **Overlay FS**: Copy-on-write filesystem with layers 9 | //! - **Volume mounts**: Bind mounts and tmpfs support 10 | //! - **Layered storage**: Efficient snapshot management 11 | //! - **Persistence**: Optional state between sandbox runs 12 | //! 13 | //! # Examples 14 | //! 15 | //! ```ignore 16 | //! use sandbox_rs::storage::{OverlayFS, OverlayConfig}; 17 | //! 18 | //! let config = OverlayConfig::new("/base", "/upper"); 19 | //! let fs = OverlayFS::new(config)?; 20 | //! ``` 21 | 22 | pub mod filesystem; 23 | pub mod volumes; 24 | pub use filesystem::{LayerInfo, OverlayConfig, OverlayFS}; 25 | pub use volumes::{VolumeManager, VolumeMount, VolumeType}; 26 | 27 | #[cfg(test)] 28 | mod tests; 29 | -------------------------------------------------------------------------------- /lib/network/mod.rs: -------------------------------------------------------------------------------- 1 | //! Network layer: Network isolation and configuration 2 | //! 3 | //! This module manages network namespace isolation and configuration, 4 | //! including bridge setup and port mapping. 5 | //! 6 | //! # Features 7 | //! 8 | //! - **Network modes**: Isolated, bridge, host, or custom 9 | //! - **Interface configuration**: IP addresses and gateways 10 | //! - **Port mapping**: Container to host port translation 11 | //! - **DNS management**: Custom DNS server configuration 12 | //! - **Bandwidth limiting**: Optional network rate limiting 13 | //! 14 | //! # Examples 15 | //! 16 | //! ```ignore 17 | //! use sandbox_rs::network::{NetworkConfig, NetworkMode}; 18 | //! 19 | //! let config = NetworkConfig::isolated() 20 | //! .with_dns_server("8.8.8.8")?; 21 | //! ``` 22 | 23 | pub mod config; 24 | pub use config::{NetworkConfig, NetworkInterface, NetworkMode, NetworkStats, PortMapping}; 25 | 26 | #[cfg(test)] 27 | mod tests; 28 | -------------------------------------------------------------------------------- /lib/execution/mod.rs: -------------------------------------------------------------------------------- 1 | //! Execution layer: Process management and initialization 2 | //! 3 | //! This module handles process execution within sandboxes, 4 | //! including namespace cloning, initialization, and lifecycle management. 5 | //! 6 | //! # Features 7 | //! 8 | //! - **Process execution**: Clone with namespace isolation 9 | //! - **Init process**: Zombie reaping and signal handling 10 | //! - **Chroot support**: Filesystem root isolation 11 | //! - **Credential switching**: UID/GID management 12 | //! 13 | //! # Examples 14 | //! 15 | //! ```ignore 16 | //! use sandbox_rs::execution::ProcessConfig; 17 | //! 18 | //! let config = ProcessConfig { 19 | //! program: "/bin/bash".to_string(), 20 | //! args: vec![], 21 | //! ..Default::default() 22 | //! }; 23 | //! ``` 24 | 25 | pub mod init; 26 | pub mod process; 27 | pub mod stream; 28 | pub use init::SandboxInit; 29 | pub use process::{ProcessConfig, ProcessExecutor, ProcessResult}; 30 | pub use stream::{ProcessStream, StreamChunk}; 31 | 32 | #[cfg(test)] 33 | mod tests; 34 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | sandbox-ctl: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | image: sandbox-rs:latest 7 | container_name: sandbox-ctl 8 | 9 | # Required for namespace and seccomp operations 10 | privileged: true 11 | 12 | # Use host cgroup namespace to allow cgroup management 13 | cgroup: host 14 | 15 | # Alternative to privileged mode (more restrictive, but may not work for all features) 16 | # cap_add: 17 | # - SYS_ADMIN 18 | # - SYS_PTRACE 19 | # - SYS_CHROOT 20 | # - NET_ADMIN 21 | # - SETUID 22 | # - SETGID 23 | # security_opt: 24 | # - seccomp=unconfined 25 | 26 | # Mount volumes for persistent data and cgroup access 27 | volumes: 28 | - ./workdir:/sandbox/workdir 29 | - ./volumes:/sandbox/volumes 30 | - /sys/fs/cgroup:/sys/fs/cgroup:rw 31 | 32 | # Override command to run a specific sandbox 33 | # command: run --id test --memory 256M --cpu 50 --timeout 30 /bin/echo "Hello from Docker!" 34 | 35 | stdin_open: true 36 | tty: true 37 | -------------------------------------------------------------------------------- /lib/monitoring/mod.rs: -------------------------------------------------------------------------------- 1 | //! Monitoring layer: Process and syscall monitoring 2 | //! 3 | //! This module provides real-time monitoring of sandboxed processes, 4 | //! including resource usage tracking via /proc and syscall tracing via eBPF. 5 | //! 6 | //! # Features 7 | //! 8 | //! - **/proc-based monitoring**: Track memory, CPU, and process state 9 | //! - **eBPF syscall tracing**: Event-driven syscall monitoring 10 | //! - **Performance metrics**: Detect slow operations (>10ms) 11 | //! - **Resource statistics**: Peak memory, CPU time, thread count 12 | //! - **Graceful shutdown**: SIGTERM → SIGKILL progression 13 | //! 14 | //! # Examples 15 | //! 16 | //! ```ignore 17 | //! use sandbox_rs::monitoring::ProcessMonitor; 18 | //! 19 | //! let monitor = ProcessMonitor::new(pid)?; 20 | //! let stats = monitor.collect_stats()?; 21 | //! println!("Memory: {}MB", stats.memory_usage_mb); 22 | //! println!("CPU time: {}ms", stats.cpu_time_ms); 23 | //! ``` 24 | 25 | pub mod ebpf; 26 | pub mod monitor; 27 | pub use ebpf::EBpfMonitor; 28 | pub use monitor::{ProcessMonitor, ProcessState, ProcessStats}; 29 | 30 | #[cfg(test)] 31 | mod tests; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Erick Jesus 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [".", "sandbox-ctl"] 3 | resolver = "2" 4 | 5 | [workspace.package] 6 | version = "0.1.5" 7 | edition = "2024" 8 | authors = ["Erick Jesus "] 9 | license = "MIT" 10 | repository = "https://github.com/ErickJ3/sandbox-rs" 11 | homepage = "https://github.com/ErickJ3/sandbox-rs" 12 | 13 | [package] 14 | name = "sandbox-rs" 15 | version.workspace = true 16 | edition.workspace = true 17 | authors.workspace = true 18 | license.workspace = true 19 | repository.workspace = true 20 | homepage.workspace = true 21 | description = "A comprehensive Rust sandbox implementation that provides process isolation, resource limiting, and syscall filtering for secure program execution." 22 | documentation = "https://docs.rs/sandbox-rs" 23 | keywords = ["sandbox", "isolation", "security", "linux", "seccomp"] 24 | categories = ["development-tools", "os"] 25 | readme = "README.md" 26 | 27 | [workspace.dependencies] 28 | sandbox-rs = { path = "." } 29 | 30 | [dependencies] 31 | nix = { version = "0.30", features = ["process", "sched", "mount", "resource", "signal", "fs"] } 32 | libc = "0.2" 33 | thiserror = "2.0" 34 | anyhow = "1.0" 35 | serde = { version = "1.0", features = ["derive"] } 36 | serde_json = "1.0" 37 | tempfile = "3.23" 38 | log = "0.4" 39 | seccompiler = "0.5" 40 | walkdir = "2.5" 41 | 42 | [lib] 43 | name = "sandbox_rs" 44 | path = "lib/lib.rs" 45 | 46 | [profile.release] 47 | opt-level = 3 48 | lto = true 49 | strip = true 50 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | //! Basic sandbox example 2 | 3 | use sandbox_rs::{SandboxBuilder, SeccompProfile}; 4 | use std::time::Duration; 5 | 6 | fn main() -> Result<(), Box> { 7 | println!("=== Sandbox RS - Basic Example ===\n"); 8 | 9 | // Create a sandbox with basic configuration 10 | println!("[1] Creating sandbox with memory limit..."); 11 | let mut sandbox = SandboxBuilder::new("example-1") 12 | .memory_limit(50 * 1024 * 1024) // 50MB 13 | .cpu_limit_percent(50) // 50% of one CPU 14 | .timeout(Duration::from_secs(5)) 15 | .seccomp_profile(SeccompProfile::Minimal) 16 | .build()?; 17 | 18 | println!("[*] Sandbox created: {}", sandbox.id()); 19 | println!("[*] Root: {}", sandbox.root().display()); 20 | println!( 21 | "[*] Status: {}\n", 22 | if sandbox.is_running() { 23 | "running" 24 | } else { 25 | "idle" 26 | } 27 | ); 28 | 29 | // Try to run a simple command 30 | println!("[2] Running 'echo hello' in sandbox..."); 31 | let result = sandbox.run("/bin/echo", &["hello", "world"])?; 32 | 33 | println!("[*] Execution result:"); 34 | println!("Exit code: {}", result.exit_code); 35 | println!("Wall time: {} ms", result.wall_time_ms); 36 | println!("Memory peak: {} bytes", result.memory_peak); 37 | println!("CPU time: {} μs", result.cpu_time_us); 38 | println!("Timed out: {}\n", result.timed_out); 39 | 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /examples/cgroup_limits.rs: -------------------------------------------------------------------------------- 1 | //! Cgroup resource limits example 2 | 3 | use sandbox_rs::SandboxBuilder; 4 | use std::time::Duration; 5 | 6 | fn main() -> Result<(), Box> { 7 | println!("=== Sandbox RS - Cgroup Resource Limits ===\n"); 8 | 9 | // Example 1: Memory limited sandbox 10 | println!("[1] Example: Memory-limited sandbox (100MB)"); 11 | let sandbox1 = SandboxBuilder::new("mem-limited") 12 | .memory_limit_str("100M")? 13 | .cpu_limit_percent(100) 14 | .build()?; 15 | println!("[*] Created: {}", sandbox1.id()); 16 | println!("[*] Root: {}\n", sandbox1.root().display()); 17 | 18 | // Example 2: CPU limited sandbox 19 | println!("[2] Example: CPU-limited sandbox (25% of one core)"); 20 | let sandbox2 = SandboxBuilder::new("cpu-limited") 21 | .cpu_limit_percent(25) 22 | .memory_limit(512 * 1024 * 1024) // 512MB 23 | .timeout(Duration::from_secs(10)) 24 | .build()?; 25 | println!("[*] Created: {}", sandbox2.id()); 26 | println!("[*] Root: {}\n", sandbox2.root().display()); 27 | 28 | // Example 3: Tight limits for untrusted code 29 | println!("[3] Example: Tight limits for untrusted code"); 30 | let sandbox3 = SandboxBuilder::new("untrusted") 31 | .memory_limit_str("64M")? 32 | .cpu_limit_percent(10) 33 | .max_pids(8) 34 | .timeout(Duration::from_secs(5)) 35 | .seccomp_profile(sandbox_rs::SeccompProfile::Minimal) 36 | .build()?; 37 | println!("[*] Created: {}", sandbox3.id()); 38 | println!("[*] Root: {}\n", sandbox3.root().display()); 39 | 40 | println!("[*] All sandboxes created successfully!"); 41 | println!("[*] Note: Actual resource enforcement requires root permissions"); 42 | 43 | Ok(()) 44 | } 45 | -------------------------------------------------------------------------------- /sandbox-ctl/src/commands.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | 3 | use crate::profiles::SecurityProfile; 4 | 5 | pub fn list_security_profiles() { 6 | info!("Listing available security profiles"); 7 | println!("Available security profiles:\n"); 8 | 9 | for profile in SecurityProfile::all() { 10 | println!( 11 | " {:12} - {}", 12 | format!("{:?}", profile).to_lowercase(), 13 | profile.description() 14 | ); 15 | println!(" {}", profile.details()); 16 | println!(); 17 | } 18 | 19 | println!("Use --profile to select a profile"); 20 | println!("Individual settings can be overridden with specific flags"); 21 | } 22 | 23 | pub fn list_seccomp_profiles() { 24 | info!("Listing available seccomp profiles"); 25 | println!("Available seccomp profiles:\n"); 26 | println!(" minimal - Minimal syscalls only (most secure)"); 27 | println!(" io-heavy - With file I/O operations"); 28 | println!(" compute - With memory operations"); 29 | println!(" network - With socket operations"); 30 | println!(" unrestricted - Allow most syscalls (least secure)"); 31 | println!(); 32 | println!("Use --seccomp to override the profile's default seccomp setting"); 33 | } 34 | 35 | pub fn check_requirements() { 36 | use sandbox_rs::utils; 37 | 38 | info!("Checking sandbox requirements"); 39 | println!("Checking sandbox requirements...\n"); 40 | 41 | if utils::is_root() { 42 | println!("[✓] Running as root"); 43 | } else { 44 | println!("[✗] NOT running as root (required)"); 45 | } 46 | 47 | if utils::has_cgroup_v2() { 48 | println!("[✓] Cgroup v2 available"); 49 | } else { 50 | println!("[✗] Cgroup v2 NOT available"); 51 | } 52 | 53 | println!("\nSystem info:"); 54 | println!(" UID: {}", utils::get_uid()); 55 | println!(" GID: {}", utils::get_gid()); 56 | } 57 | -------------------------------------------------------------------------------- /lib/lib.rs: -------------------------------------------------------------------------------- 1 | //! sandbox-rs: sandbox in Rust 2 | //! 3 | //! A comprehensive Rust sandbox solution, implements Linux namespace isolation, Cgroup v2 4 | //! resource limits, Seccomp BPF filtering, and eBPF-based syscall monitoring. 5 | //! 6 | //! # Modules 7 | //! 8 | //! - **isolation**: Namespace + Seccomp filtering 9 | //! - **resources**: Cgroup v2 resource limits 10 | //! - **execution**: Process execution and initialization 11 | //! - **monitoring**: Process and syscall monitoring 12 | //! - **storage**: Filesystem and volume management 13 | //! - **network**: Network isolation and configuration 14 | //! - **controller**: Main sandbox orchestration 15 | //! 16 | //! # Example 17 | //! 18 | //! ```ignore 19 | //! use sandbox_rs::SandboxBuilder; 20 | //! use std::time::Duration; 21 | //! 22 | //! let mut sandbox = SandboxBuilder::new("my-sandbox") 23 | //! .memory_limit_str("256M")? 24 | //! .cpu_limit_percent(50) 25 | //! .timeout(Duration::from_secs(30)) 26 | //! .build()?; 27 | //! 28 | //! let result = sandbox.run("/bin/echo", &["hello world"])?; 29 | //! println!("Exit code: {}", result.exit_code); 30 | //! ``` 31 | pub mod controller; 32 | pub mod errors; 33 | pub mod execution; 34 | pub mod isolation; 35 | pub mod monitoring; 36 | pub mod network; 37 | pub mod resources; 38 | pub mod storage; 39 | pub mod utils; 40 | pub use controller::{Sandbox, SandboxBuilder, SandboxConfig}; 41 | pub use errors::{Result, SandboxError}; 42 | pub use execution::{ProcessConfig, ProcessResult, ProcessStream, StreamChunk}; 43 | pub use isolation::{NamespaceConfig, SeccompProfile}; 44 | pub use monitoring::{ProcessMonitor, ProcessState, ProcessStats}; 45 | pub use network::{NetworkConfig, NetworkMode}; 46 | pub use storage::{OverlayConfig, OverlayFS}; 47 | 48 | #[cfg(test)] 49 | pub mod test_support { 50 | use std::sync::{Mutex, MutexGuard, OnceLock}; 51 | 52 | pub fn serial_guard() -> MutexGuard<'static, ()> { 53 | static LOCK: OnceLock> = OnceLock::new(); 54 | LOCK.get_or_init(|| Mutex::new(())) 55 | .lock() 56 | .unwrap_or_else(|poison| poison.into_inner()) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /lib/resources/tests.rs: -------------------------------------------------------------------------------- 1 | use super::{Cgroup, CgroupConfig}; 2 | use nix::unistd::Pid; 3 | use std::path::Path; 4 | 5 | fn is_root() -> bool { 6 | unsafe { libc::geteuid() == 0 } 7 | } 8 | 9 | #[test] 10 | fn cgroup_config_combines_multiple_limits() { 11 | let mut config = CgroupConfig::with_memory(256 * 1024 * 1024); 12 | config.cpu_weight = Some(500); 13 | config.cpu_quota = Some(50_000); 14 | config.cpu_period = Some(100_000); 15 | config.max_pids = Some(32); 16 | 17 | assert!(config.validate().is_ok()); 18 | assert_eq!(config.memory_limit, Some(256 * 1024 * 1024)); 19 | assert_eq!(config.cpu_weight, Some(500)); 20 | assert_eq!(config.max_pids, Some(32)); 21 | } 22 | 23 | #[test] 24 | fn cgroup_config_rejects_invalid_values() { 25 | let bad_memory = CgroupConfig { 26 | memory_limit: Some(0), 27 | ..Default::default() 28 | }; 29 | assert!(bad_memory.validate().is_err()); 30 | 31 | let bad_weight_low = CgroupConfig { 32 | cpu_weight: Some(10), 33 | ..Default::default() 34 | }; 35 | assert!(bad_weight_low.validate().is_err()); 36 | 37 | let bad_weight_high = CgroupConfig { 38 | cpu_weight: Some(20_000), 39 | ..Default::default() 40 | }; 41 | assert!(bad_weight_high.validate().is_err()); 42 | } 43 | 44 | #[test] 45 | fn cgroup_config_helpers_set_expected_fields() { 46 | let memory = CgroupConfig::with_memory(64 * 1024 * 1024); 47 | assert_eq!(memory.memory_limit, Some(64 * 1024 * 1024)); 48 | 49 | let quota = CgroupConfig::with_cpu_quota(100_000, 200_000); 50 | assert_eq!(quota.cpu_quota, Some(100_000)); 51 | assert_eq!(quota.cpu_period, Some(200_000)); 52 | } 53 | 54 | #[test] 55 | #[ignore] 56 | fn cgroup_creation_handles_permissions_gracefully() { 57 | let pid = Pid::from_raw(std::process::id() as i32); 58 | let name = format!("sandbox-rs-test-{}", pid); 59 | let result = Cgroup::new(&name, pid); 60 | 61 | if Path::new("/sys/fs/cgroup").exists() && is_root() { 62 | let cgroup = result.expect("root should create cgroup"); 63 | assert!(cgroup.exists()); 64 | let _ = cgroup.delete(); 65 | } else { 66 | assert!(result.is_err()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/errors.rs: -------------------------------------------------------------------------------- 1 | //! Error types for sandbox operations 2 | 3 | use std::io; 4 | use thiserror::Error; 5 | 6 | /// Result type for sandbox operations 7 | pub type Result = std::result::Result; 8 | 9 | /// Errors that can occur during sandbox operations 10 | #[derive(Error, Debug)] 11 | pub enum SandboxError { 12 | #[error("IO error: {0}")] 13 | Io(#[from] io::Error), 14 | 15 | #[error("Syscall error: {0}")] 16 | Syscall(String), 17 | 18 | #[error("Cgroup error: {0}")] 19 | Cgroup(String), 20 | 21 | #[error("Namespace error: {0}")] 22 | Namespace(String), 23 | 24 | #[error("Seccomp error: {0}")] 25 | Seccomp(String), 26 | 27 | #[error("Invalid configuration: {0}")] 28 | InvalidConfig(String), 29 | 30 | #[error("Sandbox already running")] 31 | AlreadyRunning, 32 | 33 | #[error("Sandbox not running")] 34 | NotRunning, 35 | 36 | #[error("Timeout exceeded")] 37 | Timeout, 38 | 39 | #[error("Process exited with code {code}")] 40 | ProcessExit { code: i32 }, 41 | 42 | #[error("Process killed by signal {signal}")] 43 | ProcessSignal { signal: i32 }, 44 | 45 | #[error("Permission denied: {0}")] 46 | PermissionDenied(String), 47 | 48 | #[error("Resource exhausted: {0}")] 49 | ResourceExhausted(String), 50 | 51 | #[error("Process monitoring error: {0}")] 52 | ProcessMonitoring(String), 53 | 54 | #[error("Unknown error: {0}")] 55 | Unknown(String), 56 | } 57 | 58 | #[cfg(test)] 59 | mod tests { 60 | use super::*; 61 | 62 | #[test] 63 | fn test_error_display() { 64 | let err = SandboxError::Timeout; 65 | assert_eq!(err.to_string(), "Timeout exceeded"); 66 | } 67 | 68 | #[test] 69 | fn test_error_from_io() { 70 | let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found"); 71 | let sandbox_err = SandboxError::from(io_err); 72 | assert!(sandbox_err.to_string().contains("IO error")); 73 | } 74 | 75 | #[test] 76 | fn test_result_type() { 77 | fn returns_result() -> Result { 78 | Ok(42) 79 | } 80 | assert_eq!(returns_result().unwrap(), 42); 81 | } 82 | 83 | #[test] 84 | fn test_result_error() { 85 | fn returns_error() -> Result { 86 | Err(SandboxError::Timeout) 87 | } 88 | assert!(returns_error().is_err()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /examples/stream_basic.rs: -------------------------------------------------------------------------------- 1 | //! Example: Basic streaming output from sandboxed process 2 | //! 3 | //! This example demonstrates how to capture and stream stdout/stderr 4 | //! from a sandboxed process in real-time. 5 | 6 | use sandbox_rs::{SandboxBuilder, StreamChunk}; 7 | use std::time::Duration; 8 | use tempfile::tempdir; 9 | 10 | fn main() -> sandbox_rs::Result<()> { 11 | // Create a temporary directory for the sandbox 12 | let tmp = tempdir().expect("Failed to create temp dir"); 13 | 14 | // Create a sandbox with streaming enabled 15 | let mut sandbox = SandboxBuilder::new("stream-example") 16 | .memory_limit_str("256M")? 17 | .cpu_limit_percent(50) 18 | .timeout(Duration::from_secs(30)) 19 | .root(tmp.path()) 20 | .build()?; 21 | 22 | println!("Starting sandboxed process with streaming...\n"); 23 | 24 | // Run process with streaming 25 | let (result, stream) = sandbox.run_with_stream("/bin/echo", &["Hello from sandbox!"])?; 26 | 27 | println!("Process output (streaming):"); 28 | 29 | // Iterate through all output chunks 30 | let mut final_exit_code = result.exit_code; 31 | let mut final_signal = result.signal; 32 | 33 | for chunk in stream.into_iter() { 34 | match chunk { 35 | StreamChunk::Stdout(line) => { 36 | println!("[STDOUT] {}", line); 37 | } 38 | StreamChunk::Stderr(line) => { 39 | eprintln!("[STDERR] {}", line); 40 | } 41 | StreamChunk::Exit { exit_code, signal } => { 42 | println!("\nProcess exited with code: {}", exit_code); 43 | if let Some(sig) = signal { 44 | println!("Killed by signal: {}", sig); 45 | } 46 | final_exit_code = exit_code; 47 | final_signal = signal; 48 | } 49 | } 50 | } 51 | 52 | // Update result with the actual exit code from streaming 53 | let mut result = result; 54 | result.exit_code = final_exit_code; 55 | result.signal = final_signal; 56 | 57 | println!("\nExecution stats:"); 58 | println!(" Exit code: {}", result.exit_code); 59 | println!(" Wall time: {} ms", result.wall_time_ms); 60 | println!(" Memory peak: {} bytes", result.memory_peak); 61 | 62 | // Check for seccomp errors and return error if found 63 | result.check_seccomp_error()?; 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /examples/stream_long_running.rs: -------------------------------------------------------------------------------- 1 | //! Example: Streaming from a long-running process 2 | //! 3 | //! This example shows how to stream output from a process that outputs 4 | //! multiple lines over time, including both stdout and stderr. 5 | //! 6 | //! Note: This example uses SeccompProfile::Unrestricted because bash requires 7 | //! many syscalls for proper operation. For simple commands like `echo`, you can 8 | //! use more restrictive profiles like Minimal or Compute. 9 | 10 | use sandbox_rs::{SandboxBuilder, SeccompProfile, StreamChunk}; 11 | use std::time::Duration; 12 | use tempfile::tempdir; 13 | 14 | fn main() -> sandbox_rs::Result<()> { 15 | let tmp = tempdir().expect("Failed to create temp dir"); 16 | 17 | let mut sandbox = SandboxBuilder::new("long-running-example") 18 | .memory_limit_str("512M")? 19 | .cpu_limit_percent(100) 20 | .timeout(Duration::from_secs(30)) 21 | .seccomp_profile(SeccompProfile::Unrestricted) 22 | .root(tmp.path()) 23 | .build()?; 24 | 25 | println!("Running bash script with streaming output...\n"); 26 | 27 | // Run a bash script that outputs multiple lines 28 | let (result, stream) = sandbox.run_with_stream( 29 | "/bin/bash", 30 | &[ 31 | "-c", 32 | "for i in {1..5}; do echo \"Line $i from stdout\"; echo \"Error line $i\" >&2; done", 33 | ], 34 | )?; 35 | 36 | println!("Streaming output:"); 37 | 38 | let mut stdout_count = 0; 39 | let mut stderr_count = 0; 40 | let mut final_exit_code = result.exit_code; 41 | 42 | for chunk in stream.into_iter() { 43 | match chunk { 44 | StreamChunk::Stdout(line) => { 45 | stdout_count += 1; 46 | println!(" [OUT #{}] {}", stdout_count, line); 47 | } 48 | StreamChunk::Stderr(line) => { 49 | stderr_count += 1; 50 | eprintln!(" [ERR #{}] {}", stderr_count, line); 51 | } 52 | StreamChunk::Exit { 53 | exit_code, 54 | signal: _, 55 | } => { 56 | println!("Process finished with exit code: {}", exit_code); 57 | final_exit_code = exit_code; 58 | } 59 | } 60 | } 61 | 62 | let mut result = result; 63 | result.exit_code = final_exit_code; 64 | 65 | println!("\nSummary:"); 66 | println!(" Stdout lines: {}", stdout_count); 67 | println!(" Stderr lines: {}", stderr_count); 68 | println!(" Wall time: {} ms", result.wall_time_ms); 69 | println!(" Exit code: {}", result.exit_code); 70 | 71 | result.check_seccomp_error()?; 72 | 73 | Ok(()) 74 | } 75 | -------------------------------------------------------------------------------- /sandbox-ctl/src/main.rs: -------------------------------------------------------------------------------- 1 | //! Sandbox controller CLI - Run applications in secure isolated environments 2 | 3 | mod cli; 4 | mod commands; 5 | mod logging; 6 | mod profiles; 7 | mod runner; 8 | 9 | use clap::Parser; 10 | use cli::{Cli, Commands}; 11 | use commands::{check_requirements, list_seccomp_profiles, list_security_profiles}; 12 | use runner::run_sandbox; 13 | 14 | fn main() { 15 | let cli = Cli::parse(); 16 | 17 | logging::init_logger(cli.verbose); 18 | 19 | if cli.check { 20 | check_requirements(); 21 | return; 22 | } 23 | 24 | if cli.list_profiles { 25 | list_security_profiles(); 26 | return; 27 | } 28 | 29 | if cli.list_seccomp { 30 | list_seccomp_profiles(); 31 | return; 32 | } 33 | 34 | if let Some(command) = cli.command { 35 | match command { 36 | Commands::Run { program, args } => { 37 | if let Err(e) = run_sandbox( 38 | cli.id, 39 | &program, 40 | &args, 41 | cli.profile, 42 | cli.memory, 43 | cli.cpu, 44 | cli.timeout, 45 | cli.seccomp, 46 | cli.root, 47 | ) { 48 | eprintln!("Error: {}", e); 49 | std::process::exit(1); 50 | } 51 | } 52 | Commands::Profiles => list_security_profiles(), 53 | Commands::Check => check_requirements(), 54 | Commands::Seccomp => list_seccomp_profiles(), 55 | } 56 | return; 57 | } 58 | 59 | let Some(program) = cli.program else { 60 | eprintln!("Error: No program specified"); 61 | eprintln!("Try 'sandbox-ctl --help' for more information"); 62 | std::process::exit(1); 63 | }; 64 | 65 | if let Err(e) = run_sandbox( 66 | cli.id, 67 | &program, 68 | &cli.args, 69 | cli.profile, 70 | cli.memory, 71 | cli.cpu, 72 | cli.timeout, 73 | cli.seccomp, 74 | cli.root, 75 | ) { 76 | eprintln!("Error: {}", e); 77 | std::process::exit(1); 78 | } 79 | } 80 | 81 | #[cfg(test)] 82 | mod tests { 83 | use super::*; 84 | 85 | #[test] 86 | fn list_profiles_runs() { 87 | list_security_profiles(); 88 | } 89 | 90 | #[test] 91 | fn list_seccomp_runs() { 92 | list_seccomp_profiles(); 93 | } 94 | 95 | #[test] 96 | fn check_requirements_runs() { 97 | check_requirements(); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/isolation/tests.rs: -------------------------------------------------------------------------------- 1 | use super::namespace::{get_namespace_inode, shares_namespace}; 2 | use super::{NamespaceConfig, SeccompFilter, SeccompProfile}; 3 | use crate::isolation::seccomp_bpf::SeccompBpf; 4 | use nix::sched::CloneFlags; 5 | 6 | #[test] 7 | fn namespace_configuration_can_toggle_individual_namespaces() { 8 | let mut config = NamespaceConfig::default(); 9 | assert!(config.pid); 10 | config.user = true; 11 | assert!(config.all_enabled()); 12 | 13 | config.uts = false; 14 | assert!(!config.all_enabled()); 15 | assert_eq!(config.enabled_count(), 5); 16 | 17 | let flags = config.to_clone_flags(); 18 | assert!(flags.contains(CloneFlags::CLONE_NEWUSER)); 19 | } 20 | 21 | #[test] 22 | fn namespace_inode_queries_fail_for_invalid_target() { 23 | let result = get_namespace_inode("nonexistent"); 24 | assert!(result.is_err()); 25 | } 26 | 27 | #[test] 28 | fn namespace_sharing_detects_matching_inodes() { 29 | let shared = shares_namespace("pid", None, None).expect("pid namespace should be readable"); 30 | assert!(shared); 31 | } 32 | 33 | #[test] 34 | fn seccomp_profiles_cover_expected_variants() { 35 | let profiles = SeccompProfile::all(); 36 | assert!(profiles.contains(&SeccompProfile::Minimal)); 37 | assert!(profiles.contains(&SeccompProfile::Network)); 38 | assert_eq!(profiles.len(), 5); 39 | } 40 | 41 | #[test] 42 | fn seccomp_filter_export_is_sorted_and_non_empty() { 43 | let filter = SeccompFilter::from_profile(SeccompProfile::Compute); 44 | let exported = filter.export().expect("export should succeed"); 45 | 46 | assert!(!exported.is_empty()); 47 | let mut sorted = exported.clone(); 48 | sorted.sort(); 49 | assert_eq!(exported, sorted); 50 | } 51 | 52 | #[test] 53 | fn seccomp_filter_blocking_compiles_successfully() { 54 | let mut filter = SeccompFilter::minimal(); 55 | filter.block_syscall("read"); 56 | filter.set_kill_on_violation(false); 57 | 58 | let result = SeccompBpf::compile(&filter); 59 | assert!(result.is_ok()); 60 | let instrs = result.unwrap(); 61 | assert!(instrs.len() > 5); 62 | } 63 | 64 | #[test] 65 | fn seccomp_bpf_compiles_with_kill_and_trap_modes() { 66 | let filter_kill = SeccompFilter::minimal(); 67 | assert!(filter_kill.is_kill_on_violation()); 68 | let result_kill = SeccompBpf::compile(&filter_kill); 69 | assert!(result_kill.is_ok()); 70 | let instrs_kill = result_kill.unwrap(); 71 | assert!(instrs_kill.len() > 5); 72 | 73 | let mut filter_trap = SeccompFilter::minimal(); 74 | filter_trap.set_kill_on_violation(false); 75 | assert!(!filter_trap.is_kill_on_violation()); 76 | let result_trap = SeccompBpf::compile(&filter_trap); 77 | assert!(result_trap.is_ok()); 78 | let instrs_trap = result_trap.unwrap(); 79 | assert!(instrs_trap.len() > 5); 80 | } 81 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-utils": { 4 | "inputs": { 5 | "systems": "systems" 6 | }, 7 | "locked": { 8 | "lastModified": 1731533236, 9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 | "owner": "numtide", 11 | "repo": "flake-utils", 12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 | "type": "github" 14 | }, 15 | "original": { 16 | "owner": "numtide", 17 | "repo": "flake-utils", 18 | "type": "github" 19 | } 20 | }, 21 | "nixpkgs": { 22 | "locked": { 23 | "lastModified": 1763835633, 24 | "narHash": "sha256-HzxeGVID5MChuCPESuC0dlQL1/scDKu+MmzoVBJxulM=", 25 | "owner": "NixOS", 26 | "repo": "nixpkgs", 27 | "rev": "050e09e091117c3d7328c7b2b7b577492c43c134", 28 | "type": "github" 29 | }, 30 | "original": { 31 | "owner": "NixOS", 32 | "ref": "nixos-unstable", 33 | "repo": "nixpkgs", 34 | "type": "github" 35 | } 36 | }, 37 | "nixpkgs_2": { 38 | "locked": { 39 | "lastModified": 1744536153, 40 | "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", 41 | "owner": "NixOS", 42 | "repo": "nixpkgs", 43 | "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", 44 | "type": "github" 45 | }, 46 | "original": { 47 | "owner": "NixOS", 48 | "ref": "nixpkgs-unstable", 49 | "repo": "nixpkgs", 50 | "type": "github" 51 | } 52 | }, 53 | "root": { 54 | "inputs": { 55 | "flake-utils": "flake-utils", 56 | "nixpkgs": "nixpkgs", 57 | "rust-overlay": "rust-overlay" 58 | } 59 | }, 60 | "rust-overlay": { 61 | "inputs": { 62 | "nixpkgs": "nixpkgs_2" 63 | }, 64 | "locked": { 65 | "lastModified": 1764038373, 66 | "narHash": "sha256-M6w2wNBRelcavoDAyFL2iO4NeWknD40ASkH1S3C0YGM=", 67 | "owner": "oxalica", 68 | "repo": "rust-overlay", 69 | "rev": "ab3536fe850211a96673c6ffb2cb88aab8071cc9", 70 | "type": "github" 71 | }, 72 | "original": { 73 | "owner": "oxalica", 74 | "repo": "rust-overlay", 75 | "type": "github" 76 | } 77 | }, 78 | "systems": { 79 | "locked": { 80 | "lastModified": 1681028828, 81 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 | "owner": "nix-systems", 83 | "repo": "default", 84 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "nix-systems", 89 | "repo": "default", 90 | "type": "github" 91 | } 92 | } 93 | }, 94 | "root": "root", 95 | "version": 7 96 | } 97 | -------------------------------------------------------------------------------- /lib/network/tests.rs: -------------------------------------------------------------------------------- 1 | use super::{NetworkConfig, NetworkInterface, NetworkMode, NetworkStats, PortMapping}; 2 | use std::net::{IpAddr, Ipv4Addr}; 3 | 4 | #[test] 5 | fn network_config_supports_multiple_interfaces() { 6 | let mut config = NetworkConfig::isolated(); 7 | let iface1 = NetworkInterface::new("eth0", Ipv4Addr::new(10, 10, 0, 1)); 8 | let iface2 = NetworkInterface::new("eth1", Ipv4Addr::new(10, 10, 0, 2)); 9 | config.add_interface(iface1).expect("interface valid"); 10 | config.add_interface(iface2).expect("interface valid"); 11 | 12 | assert_eq!(config.interfaces.len(), 2); 13 | assert!(config.validate().is_ok()); 14 | } 15 | 16 | #[test] 17 | fn network_config_accepts_port_mappings_with_protocols() { 18 | let mut config = NetworkConfig::isolated(); 19 | let mut mapping = PortMapping::new(8080, 18080); 20 | mapping.protocol = "udp".to_string(); 21 | config.add_port_mapping(mapping).expect("mapping valid"); 22 | 23 | assert_eq!(config.port_mappings.len(), 1); 24 | assert_eq!(config.port_mappings[0].protocol, "udp"); 25 | } 26 | 27 | #[test] 28 | fn network_config_validation_catches_invalid_interface() { 29 | let mut iface = NetworkInterface::default(); 30 | iface.name.clear(); 31 | assert!(iface.validate().is_err()); 32 | } 33 | 34 | #[test] 35 | fn port_mapping_validation_rejects_bad_protocol() { 36 | let mut mapping = PortMapping::new(80, 8080); 37 | mapping.protocol = "icmp".to_string(); 38 | assert!(mapping.validate().is_err()); 39 | } 40 | 41 | #[test] 42 | fn port_mapping_addresses_match_host_and_container() { 43 | let mapping = PortMapping::new(8080, 18080); 44 | let host_addr = mapping.get_host_addr(); 45 | let container_addr = mapping.get_container_addr(Ipv4Addr::new(172, 18, 0, 2)); 46 | 47 | assert_eq!(host_addr.port(), 18080); 48 | assert_eq!(container_addr.port(), 8080); 49 | } 50 | 51 | #[test] 52 | fn network_config_serialization_roundtrip() { 53 | let mut config = NetworkConfig::isolated(); 54 | config.dns_servers = vec![IpAddr::V4(Ipv4Addr::new(9, 9, 9, 9))]; 55 | 56 | let json = serde_json::to_string(&config).expect("serialize network config"); 57 | let restored: NetworkConfig = serde_json::from_str(&json).expect("deserialize config"); 58 | 59 | assert_eq!(restored.mode, NetworkMode::Isolated); 60 | assert_eq!( 61 | restored.dns_servers[0], 62 | IpAddr::V4(Ipv4Addr::new(9, 9, 9, 9)) 63 | ); 64 | } 65 | 66 | #[test] 67 | fn network_stats_accumulates_values() { 68 | let stats = NetworkStats { 69 | bytes_recv: 1024, 70 | bytes_sent: 2048, 71 | packets_recv: 10, 72 | packets_sent: 20, 73 | ..Default::default() 74 | }; 75 | 76 | assert_eq!(stats.bytes_recv, 1024); 77 | assert_eq!(stats.bytes_sent, 2048); 78 | assert_eq!(stats.packets_recv + stats.packets_sent, 30); 79 | } 80 | -------------------------------------------------------------------------------- /sandbox-ctl/src/runner.rs: -------------------------------------------------------------------------------- 1 | use log::{debug, info}; 2 | use sandbox_rs::{SandboxBuilder, SeccompProfile}; 3 | use std::path::PathBuf; 4 | use std::time::Duration; 5 | 6 | use crate::profiles::SecurityProfile; 7 | 8 | pub fn run_sandbox( 9 | id: Option, 10 | program: &str, 11 | args: &[String], 12 | profile: Option, 13 | memory: Option, 14 | cpu: Option, 15 | timeout: Option, 16 | seccomp: Option, 17 | root: Option, 18 | ) -> Result<(), Box> { 19 | let sandbox_id = id.unwrap_or_else(|| format!("sandbox-{}", std::process::id())); 20 | 21 | let mut builder = SandboxBuilder::new(&sandbox_id); 22 | 23 | let selected_profile = profile.unwrap_or(SecurityProfile::Strict); 24 | builder = selected_profile.apply(builder); 25 | 26 | debug!("Using profile: {:?}", selected_profile); 27 | debug!("{}", selected_profile.details()); 28 | 29 | if let Some(m) = memory { 30 | debug!("Overriding memory limit: {}", m); 31 | builder = builder.memory_limit_str(&m)?; 32 | } 33 | 34 | if let Some(c) = cpu { 35 | debug!("Overriding CPU limit: {}%", c); 36 | builder = builder.cpu_limit_percent(c); 37 | } 38 | 39 | if let Some(t) = timeout { 40 | debug!("Overriding timeout: {}s", t); 41 | builder = builder.timeout(Duration::from_secs(t)); 42 | } 43 | 44 | if let Some(s) = seccomp { 45 | debug!("Overriding seccomp profile: {}", s); 46 | builder = builder.seccomp_profile(match s.to_lowercase().as_str() { 47 | "minimal" => SeccompProfile::Minimal, 48 | "io-heavy" => SeccompProfile::IoHeavy, 49 | "compute" => SeccompProfile::Compute, 50 | "network" => SeccompProfile::Network, 51 | "unrestricted" => SeccompProfile::Unrestricted, 52 | _ => { 53 | return Err(format!("Unknown seccomp profile: {}", s).into()); 54 | } 55 | }); 56 | } 57 | 58 | if let Some(r) = root { 59 | debug!("Using custom root: {:?}", r); 60 | builder = builder.root(&r); 61 | } 62 | 63 | info!("Building sandbox '{}'", sandbox_id); 64 | 65 | let mut sandbox = builder.build()?; 66 | 67 | info!("Executing: {} {:?}", program, args); 68 | 69 | let args_refs: Vec<&str> = args.iter().map(|s| s.as_str()).collect(); 70 | let result = sandbox.run(program, &args_refs)?; 71 | 72 | info!("Execution completed in {}ms", result.wall_time_ms); 73 | 74 | println!( 75 | "exit_code={} | wall_time_ms={} | memory_peak_bytes={} | cpu_time_us={}{}", 76 | result.exit_code, 77 | result.wall_time_ms, 78 | result.memory_peak, 79 | result.cpu_time_us, 80 | if result.timed_out { 81 | " | timed_out=true" 82 | } else { 83 | "" 84 | } 85 | ); 86 | 87 | std::process::exit(result.exit_code); 88 | } 89 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "sandbox-rs - Linux process sandbox library in Rust"; 3 | 4 | inputs = { 5 | flake-utils.url = "github:numtide/flake-utils"; 6 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 7 | rust-overlay.url = "github:oxalica/rust-overlay"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, flake-utils, rust-overlay }: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | overlays = [ (import rust-overlay) ]; 14 | pkgs = import nixpkgs { 15 | inherit system overlays; 16 | }; 17 | 18 | rust = pkgs.rust-bin.stable.latest.default.override { 19 | extensions = [ "rust-src" "rust-analyzer" ]; 20 | targets = [ "x86_64-unknown-linux-gnu" ]; 21 | }; 22 | 23 | in 24 | { 25 | devShells.default = pkgs.mkShell { 26 | buildInputs = with pkgs; [ 27 | rust 28 | cargo-audit 29 | cargo-edit 30 | cargo-outdated 31 | cargo-tarpaulin 32 | cargo-watch 33 | pkg-config 34 | openssl 35 | gcc 36 | cmake 37 | gnumake 38 | git 39 | curl 40 | wget 41 | jq 42 | yq 43 | groff 44 | strace 45 | ltrace 46 | gdb 47 | nixpkgs-fmt 48 | ]; 49 | 50 | shellHook = '' 51 | export RUST_BACKTRACE=1 52 | export CARGO_INCREMENTAL=1 53 | echo "sandbox-rs development environment loaded" 54 | echo "Available commands:" 55 | echo " cargo build - Build the library" 56 | echo " cargo test - Run tests (some require sudo)" 57 | echo " cargo doc - Build documentation" 58 | echo " cargo fmt - Format code" 59 | echo " cargo clippy - Run linter" 60 | echo " cargo tarpaulin - Generate coverage report" 61 | echo "" 62 | echo "For full sandbox isolation, some commands require root:" 63 | echo " sudo cargo test" 64 | echo " sudo cargo run --example basic_sandbox" 65 | ''; 66 | }; 67 | 68 | packages.default = pkgs.rustPlatform.buildRustPackage { 69 | pname = "sandbox-rs"; 70 | version = "0.1.0"; 71 | 72 | src = ./.; 73 | 74 | cargoHash = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; 75 | 76 | nativeBuildInputs = with pkgs; [ 77 | pkg-config 78 | cmake 79 | gnumake 80 | ]; 81 | 82 | buildInputs = with pkgs; [ 83 | openssl 84 | ]; 85 | 86 | meta = { 87 | description = "Linux sandbox library in rust"; 88 | homepage = "https://github.com/ErickJ3/sandbox-rs"; 89 | license = pkgs.lib.licenses.mit; 90 | maintainers = [ ]; 91 | }; 92 | }; 93 | } 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /sandbox-ctl/src/profiles.rs: -------------------------------------------------------------------------------- 1 | use clap::ValueEnum; 2 | use sandbox_rs::{SandboxBuilder, SeccompProfile}; 3 | use std::time::Duration; 4 | 5 | struct SecurityConfig { 6 | memory: u64, 7 | cpu: u32, 8 | timeout: Duration, 9 | seccomp: SeccompProfile, 10 | } 11 | 12 | const STRICT: SecurityConfig = SecurityConfig { 13 | memory: 128 * 1024 * 1024, 14 | cpu: 50, 15 | timeout: Duration::from_secs(30), 16 | seccomp: SeccompProfile::Minimal, 17 | }; 18 | 19 | const MODERATE: SecurityConfig = SecurityConfig { 20 | memory: 512 * 1024 * 1024, 21 | cpu: 75, 22 | timeout: Duration::from_secs(300), 23 | seccomp: SeccompProfile::IoHeavy, 24 | }; 25 | 26 | const PERMISSIVE: SecurityConfig = SecurityConfig { 27 | memory: 2 * 1024 * 1024 * 1024, 28 | cpu: 90, 29 | timeout: Duration::from_secs(3600), 30 | seccomp: SeccompProfile::Unrestricted, 31 | }; 32 | 33 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] 34 | pub enum SecurityProfile { 35 | /// Maximum security: strict limits and minimal syscalls 36 | Strict, 37 | /// Balanced security: reasonable limits and common syscalls 38 | Moderate, 39 | /// Minimal restrictions: good for development 40 | Permissive, 41 | } 42 | 43 | impl SecurityProfile { 44 | fn config(&self) -> &'static SecurityConfig { 45 | match self { 46 | SecurityProfile::Strict => &STRICT, 47 | SecurityProfile::Moderate => &MODERATE, 48 | SecurityProfile::Permissive => &PERMISSIVE, 49 | } 50 | } 51 | 52 | pub fn apply(&self, builder: SandboxBuilder) -> SandboxBuilder { 53 | let cfg = self.config(); 54 | builder 55 | .memory_limit(cfg.memory) 56 | .cpu_limit_percent(cfg.cpu) 57 | .timeout(cfg.timeout) 58 | .seccomp_profile(cfg.seccomp.clone()) 59 | } 60 | 61 | pub fn description(&self) -> &str { 62 | match self { 63 | SecurityProfile::Strict => "Maximum security with strict resource limits", 64 | SecurityProfile::Moderate => "Balanced security for general applications", 65 | SecurityProfile::Permissive => "Minimal restrictions for development work", 66 | } 67 | } 68 | 69 | pub fn details(&self) -> String { 70 | match self { 71 | SecurityProfile::Strict => { 72 | "Memory: 128MB | CPU: 50% | Timeout: 30s | Seccomp: Minimal".to_string() 73 | } 74 | SecurityProfile::Moderate => { 75 | "Memory: 512MB | CPU: 75% | Timeout: 5m | Seccomp: IO-Heavy".to_string() 76 | } 77 | SecurityProfile::Permissive => { 78 | "Memory: 2GB | CPU: 90% | Timeout: 1h | Seccomp: Unrestricted".to_string() 79 | } 80 | } 81 | } 82 | 83 | pub fn all() -> [SecurityProfile; 3] { 84 | [ 85 | SecurityProfile::Strict, 86 | SecurityProfile::Moderate, 87 | SecurityProfile::Permissive, 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /sandbox-ctl/src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, Subcommand}; 2 | use std::path::PathBuf; 3 | 4 | use crate::profiles::SecurityProfile; 5 | 6 | #[derive(Parser)] 7 | #[command(name = "sandbox-ctl")] 8 | #[command(version, about = "Run applications in secure isolated environments", long_about = None)] 9 | #[command(after_help = "EXAMPLES: 10 | # Direct execution with default security (strict) 11 | sandbox-ctl firefox 12 | sandbox-ctl --profile moderate bash 13 | sandbox-ctl --memory 512M --cpu 50 python script.py 14 | 15 | # Using subcommands 16 | sandbox-ctl run --id test --memory 100M /bin/ls 17 | sandbox-ctl profiles 18 | sandbox-ctl check 19 | 20 | # List available profiles 21 | sandbox-ctl --list-profiles 22 | sandbox-ctl --list-seccomp 23 | ")] 24 | pub struct Cli { 25 | #[command(subcommand)] 26 | pub command: Option, 27 | 28 | /// Program to run in sandbox (direct mode, yep :) 29 | #[arg(value_name = "PROGRAM", global = true)] 30 | pub program: Option, 31 | 32 | /// Program arguments 33 | #[arg(trailing_var_arg = true, allow_hyphen_values = true)] 34 | pub args: Vec, 35 | 36 | /// Security profile preset 37 | #[arg(short = 'P', long, value_name = "PROFILE", global = true)] 38 | pub profile: Option, 39 | 40 | /// Memory limit (100M, 1G, 2G) 41 | #[arg(short, long, value_name = "SIZE", global = true)] 42 | pub memory: Option, 43 | 44 | /// CPU limit percentage (0-100) 45 | #[arg(short, long, value_name = "PERCENT", global = true)] 46 | pub cpu: Option, 47 | 48 | /// Timeout in seconds 49 | #[arg(short, long, value_name = "SECONDS", global = true)] 50 | pub timeout: Option, 51 | 52 | /// Seccomp profile 53 | #[arg(short = 's', long, value_name = "SECCOMP", global = true)] 54 | pub seccomp: Option, 55 | 56 | /// Sandbox root directory 57 | #[arg(short, long, value_name = "PATH", global = true)] 58 | pub root: Option, 59 | 60 | /// Sandbox ID (auto-generated if not provided) 61 | #[arg(short, long, value_name = "ID", global = true)] 62 | pub id: Option, 63 | 64 | /// Show verbose output 65 | #[arg(short, long, global = true)] 66 | pub verbose: bool, 67 | 68 | /// List available security profiles 69 | #[arg(long)] 70 | pub list_profiles: bool, 71 | 72 | /// Check sandbox requirements 73 | #[arg(long)] 74 | pub check: bool, 75 | 76 | /// List available seccomp profiles 77 | #[arg(long)] 78 | pub list_seccomp: bool, 79 | } 80 | 81 | #[derive(Subcommand)] 82 | pub enum Commands { 83 | /// Run a program in sandbox 84 | Run { 85 | /// Program to run 86 | program: String, 87 | 88 | /// Program arguments 89 | #[arg(trailing_var_arg = true, allow_hyphen_values = true)] 90 | args: Vec, 91 | }, 92 | 93 | /// List available security profiles 94 | Profiles, 95 | 96 | /// Check sandbox requirements 97 | Check, 98 | 99 | /// List available seccomp profiles 100 | Seccomp, 101 | } 102 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | RUST_BACKTRACE: 1 12 | 13 | jobs: 14 | test: 15 | name: test 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | 20 | - name: Install Rust 21 | uses: dtolnay/rust-toolchain@stable 22 | 23 | - name: Cache cargo registry 24 | uses: actions/cache@v3 25 | with: 26 | path: ~/.cargo/registry 27 | key: runner-os-cargo-registry-Cargo-lock-hash 28 | 29 | - name: Run tests (without root-only tests) 30 | run: cargo test --lib 31 | 32 | - name: Run unit tests verbose 33 | run: cargo test --lib -- --nocapture 34 | 35 | fmt: 36 | name: rustfmt 37 | runs-on: ubuntu-latest 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: dtolnay/rust-toolchain@stable 41 | with: 42 | components: rustfmt 43 | - run: cargo fmt -- --check 44 | 45 | clippy: 46 | name: clippy 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: dtolnay/rust-toolchain@stable 51 | with: 52 | components: clippy 53 | - run: cargo clippy --all-targets --all-features 54 | 55 | build: 56 | name: build 57 | runs-on: ubuntu-latest 58 | steps: 59 | - uses: actions/checkout@v4 60 | 61 | - name: Install Rust 62 | uses: dtolnay/rust-toolchain@stable 63 | 64 | - name: Build library 65 | run: cargo build --lib --release 66 | 67 | - name: Build binaries 68 | run: cargo build --bins --release 69 | 70 | - name: Build examples 71 | run: cargo build --examples --release 72 | 73 | coverage: 74 | name: coverage 75 | runs-on: ubuntu-latest 76 | steps: 77 | - uses: actions/checkout@v4 78 | 79 | - name: Install Rust stable 80 | uses: dtolnay/rust-toolchain@stable 81 | 82 | - name: Install tarpaulin 83 | run: cargo install cargo-tarpaulin 84 | 85 | - name: gen-cov 86 | run: cargo tarpaulin --lib --timeout 300 --out Xml 87 | 88 | - name: Upload coverage to Codecov 89 | uses: codecov/codecov-action@v3 90 | with: 91 | token: ${{ secrets.CODECOV_TOKEN }} 92 | files: ./cobertura.xml 93 | fail_ci_if_error: false 94 | 95 | docs: 96 | name: docs 97 | runs-on: ubuntu-latest 98 | steps: 99 | - uses: actions/checkout@v4 100 | 101 | - name: Install Rust 102 | uses: dtolnay/rust-toolchain@stable 103 | 104 | - name: Build docs 105 | run: cargo doc --no-deps --release 106 | env: 107 | RUSTDOCFLAGS: "-D warnings" 108 | 109 | - name: Test documentation examples 110 | run: cargo test --doc 111 | 112 | check: 113 | name: check 114 | runs-on: ubuntu-latest 115 | steps: 116 | - uses: actions/checkout@v4 117 | 118 | - name: Install Rust 119 | uses: dtolnay/rust-toolchain@stable 120 | 121 | - name: Run cargo check 122 | run: cargo check --all-targets 123 | -------------------------------------------------------------------------------- /examples/stream_non_blocking.rs: -------------------------------------------------------------------------------- 1 | //! Example: Non-blocking stream reading 2 | //! 3 | //! This example demonstrates how to do non-blocking reads from the process stream, 4 | //! allowing your application to continue doing other work while checking for output. 5 | //! 6 | //! Note: This example uses SeccompProfile::Unrestricted because bash requires 7 | //! many syscalls for proper operation. For simple commands like `echo`, you can 8 | //! use more restrictive profiles like Minimal or Compute. 9 | 10 | use sandbox_rs::{SandboxBuilder, SeccompProfile, StreamChunk}; 11 | use std::time::Duration; 12 | use tempfile::tempdir; 13 | 14 | fn main() -> sandbox_rs::Result<()> { 15 | let tmp = tempdir().expect("Failed to create temp dir"); 16 | 17 | let mut sandbox = SandboxBuilder::new("non-blocking-example") 18 | .memory_limit_str("256M")? 19 | .cpu_limit_percent(50) 20 | .seccomp_profile(SeccompProfile::Unrestricted) 21 | .root(tmp.path()) 22 | .build()?; 23 | 24 | println!("Running process with non-blocking stream reads...\n"); 25 | 26 | // Run a process that outputs slowly 27 | let (result, stream) = sandbox.run_with_stream( 28 | "/bin/bash", 29 | &[ 30 | "-c", 31 | "for i in {1..3}; do echo \"Message $i\"; sleep 0.1; done", 32 | ], 33 | )?; 34 | 35 | println!("Non-blocking polling:"); 36 | 37 | let mut received_chunks = 0; 38 | let mut polling_attempts = 0; 39 | let mut final_exit_code = result.exit_code; 40 | 41 | loop { 42 | polling_attempts += 1; 43 | 44 | // Try to read without blocking 45 | match stream.try_recv()? { 46 | Some(chunk) => match chunk { 47 | StreamChunk::Stdout(line) => { 48 | println!("[STDOUT] {}", line); 49 | received_chunks += 1; 50 | } 51 | StreamChunk::Stderr(line) => { 52 | eprintln!("[STDERR] {}", line); 53 | received_chunks += 1; 54 | } 55 | StreamChunk::Exit { 56 | exit_code, 57 | signal: _, 58 | } => { 59 | println!("Process exited with code: {}", exit_code); 60 | // Capture the real exit code from the stream 61 | final_exit_code = exit_code; 62 | break; 63 | } 64 | }, 65 | None => { 66 | // No data available right now, we could do other work here 67 | std::thread::sleep(Duration::from_millis(10)); 68 | } 69 | } 70 | 71 | // Safety limit to prevent infinite loop in case of issues 72 | if polling_attempts > 10000 { 73 | println!("Safety timeout reached"); 74 | break; 75 | } 76 | } 77 | 78 | // Update result with the actual exit code from streaming 79 | let mut result = result; 80 | result.exit_code = final_exit_code; 81 | 82 | println!("\nStatistics:"); 83 | println!(" Chunks received: {}", received_chunks); 84 | println!(" Polling attempts: {}", polling_attempts); 85 | println!(" Exit code: {}", result.exit_code); 86 | println!(" Wall time: {} ms", result.wall_time_ms); 87 | 88 | // Check for seccomp errors and return error if found 89 | result.check_seccomp_error()?; 90 | 91 | Ok(()) 92 | } 93 | -------------------------------------------------------------------------------- /lib/execution/stream.rs: -------------------------------------------------------------------------------- 1 | //! Stream handling for process output 2 | 3 | use crate::errors::{Result, SandboxError}; 4 | use std::os::fd::FromRawFd; 5 | use std::os::unix::io::RawFd; 6 | use std::sync::mpsc::{Receiver, Sender, channel}; 7 | use std::thread; 8 | 9 | /// A chunk of process output 10 | #[derive(Debug, Clone)] 11 | pub enum StreamChunk { 12 | /// Data from stdout 13 | Stdout(String), 14 | /// Data from stderr 15 | Stderr(String), 16 | /// Process has exited 17 | Exit { exit_code: i32, signal: Option }, 18 | } 19 | 20 | /// Handle for receiving process output streams 21 | pub struct ProcessStream { 22 | receiver: Receiver, 23 | } 24 | 25 | impl ProcessStream { 26 | /// Create new process stream handler 27 | pub fn new() -> (ProcessStreamWriter, Self) { 28 | let (tx, rx) = channel(); 29 | (ProcessStreamWriter { tx }, ProcessStream { receiver: rx }) 30 | } 31 | 32 | /// Receive next chunk from process streams 33 | pub fn recv(&self) -> Result> { 34 | self.receiver 35 | .recv() 36 | .ok() 37 | .ok_or_else(|| SandboxError::Io(std::io::Error::other("stream closed"))) 38 | .map(Some) 39 | } 40 | 41 | /// Try to receive next chunk without blocking 42 | pub fn try_recv(&self) -> Result> { 43 | match self.receiver.try_recv() { 44 | Ok(chunk) => Ok(Some(chunk)), 45 | Err(std::sync::mpsc::TryRecvError::Empty) => Ok(None), 46 | Err(std::sync::mpsc::TryRecvError::Disconnected) => Ok(None), 47 | } 48 | } 49 | } 50 | 51 | impl Default for ProcessStream { 52 | fn default() -> Self { 53 | Self::new().1 54 | } 55 | } 56 | 57 | pub struct StreamIter { 58 | receiver: Receiver, 59 | } 60 | 61 | impl Iterator for StreamIter { 62 | type Item = StreamChunk; 63 | 64 | fn next(&mut self) -> Option { 65 | self.receiver.recv().ok() 66 | } 67 | } 68 | 69 | impl IntoIterator for ProcessStream { 70 | type Item = StreamChunk; 71 | type IntoIter = StreamIter; 72 | 73 | fn into_iter(self) -> Self::IntoIter { 74 | StreamIter { 75 | receiver: self.receiver, 76 | } 77 | } 78 | } 79 | 80 | /// Writer side for process streams 81 | pub struct ProcessStreamWriter { 82 | pub tx: Sender, 83 | } 84 | 85 | impl ProcessStreamWriter { 86 | /// Send stdout chunk 87 | pub fn send_stdout(&self, data: String) -> Result<()> { 88 | self.tx 89 | .send(StreamChunk::Stdout(data)) 90 | .map_err(|_| SandboxError::Io(std::io::Error::other("failed to send stdout chunk"))) 91 | } 92 | 93 | /// Send stderr chunk 94 | pub fn send_stderr(&self, data: String) -> Result<()> { 95 | self.tx 96 | .send(StreamChunk::Stderr(data)) 97 | .map_err(|_| SandboxError::Io(std::io::Error::other("failed to send stderr chunk"))) 98 | } 99 | 100 | /// Send exit status 101 | pub fn send_exit(&self, exit_code: i32, signal: Option) -> Result<()> { 102 | self.tx 103 | .send(StreamChunk::Exit { exit_code, signal }) 104 | .map_err(|_| SandboxError::Io(std::io::Error::other("failed to send exit chunk"))) 105 | } 106 | } 107 | 108 | /// Spawn a reader thread for a file descriptor 109 | pub fn spawn_fd_reader( 110 | fd: RawFd, 111 | is_stderr: bool, 112 | tx: Sender, 113 | ) -> Result> { 114 | let handle = thread::spawn(move || { 115 | // SAFETY: This FD comes from a properly-managed pipe created by the parent. 116 | // We wrap it in OwnedFd to ensure proper cleanup. 117 | use std::io::Read; 118 | use std::os::unix::io::OwnedFd; 119 | 120 | let owned_fd = unsafe { OwnedFd::from_raw_fd(fd) }; 121 | let mut file = std::fs::File::from(owned_fd); 122 | 123 | let mut buffer = vec![0u8; 4096]; 124 | 125 | loop { 126 | match file.read(&mut buffer) { 127 | Ok(0) => break, 128 | Ok(n) => { 129 | let data = String::from_utf8_lossy(&buffer[..n]).to_string(); 130 | let chunk = if is_stderr { 131 | StreamChunk::Stderr(data) 132 | } else { 133 | StreamChunk::Stdout(data) 134 | }; 135 | 136 | if tx.send(chunk).is_err() { 137 | break; 138 | } 139 | } 140 | Err(_) => break, 141 | } 142 | } 143 | }); 144 | 145 | Ok(handle) 146 | } 147 | -------------------------------------------------------------------------------- /lib/utils.rs: -------------------------------------------------------------------------------- 1 | //! Utility functions for sandbox operations 2 | 3 | use crate::errors::{Result, SandboxError}; 4 | #[cfg(test)] 5 | use std::cell::Cell; 6 | use std::path::Path; 7 | 8 | #[cfg(test)] 9 | thread_local! { 10 | static ROOT_OVERRIDE: Cell> = const { Cell::new(None) }; 11 | } 12 | 13 | /// Check if running as root 14 | pub fn is_root() -> bool { 15 | #[cfg(test)] 16 | { 17 | if let Some(value) = ROOT_OVERRIDE.with(|cell| cell.get()) { 18 | return value; 19 | } 20 | } 21 | 22 | unsafe { libc::geteuid() == 0 } 23 | } 24 | 25 | /// Get current UID 26 | pub fn get_uid() -> u32 { 27 | unsafe { libc::geteuid() } 28 | } 29 | 30 | /// Get current GID 31 | pub fn get_gid() -> u32 { 32 | unsafe { libc::getegid() } 33 | } 34 | 35 | /// Ensure we have root privileges 36 | pub fn require_root() -> Result<()> { 37 | if !is_root() { 38 | Err(SandboxError::PermissionDenied( 39 | "This operation requires root privileges".to_string(), 40 | )) 41 | } else { 42 | Ok(()) 43 | } 44 | } 45 | 46 | /// Check if cgroup v2 is available 47 | pub fn has_cgroup_v2() -> bool { 48 | Path::new("/sys/fs/cgroup/cgroup.controllers").exists() 49 | } 50 | 51 | /// Check if a cgroup path exists 52 | pub fn cgroup_exists(path: &Path) -> bool { 53 | path.exists() 54 | } 55 | 56 | /// Parse memory size string (e.g., "100M", "1G") 57 | pub fn parse_memory_size(s: &str) -> Result { 58 | let s = s.trim().to_uppercase(); 59 | 60 | let (num_str, multiplier) = if s.ends_with("G") { 61 | (&s[..s.len() - 1], 1024u64 * 1024 * 1024) 62 | } else if s.ends_with("M") { 63 | (&s[..s.len() - 1], 1024u64 * 1024) 64 | } else if s.ends_with("K") { 65 | (&s[..s.len() - 1], 1024u64) 66 | } else if s.ends_with("B") { 67 | (&s[..s.len() - 1], 1u64) 68 | } else { 69 | (s.as_str(), 1u64) 70 | }; 71 | 72 | let num: u64 = num_str 73 | .parse() 74 | .map_err(|_| SandboxError::InvalidConfig(format!("Invalid memory size: {}", s)))?; 75 | 76 | num.checked_mul(multiplier) 77 | .ok_or_else(|| SandboxError::InvalidConfig(format!("Memory size overflow: {}", s))) 78 | } 79 | 80 | #[cfg(test)] 81 | pub fn set_root_override(value: Option) { 82 | ROOT_OVERRIDE.with(|cell| cell.set(value)); 83 | } 84 | 85 | #[cfg(test)] 86 | mod tests { 87 | use super::*; 88 | 89 | #[test] 90 | fn test_parse_memory_size_bytes() { 91 | assert_eq!(parse_memory_size("100").unwrap(), 100); 92 | assert_eq!(parse_memory_size("100B").unwrap(), 100); 93 | } 94 | 95 | #[test] 96 | fn test_parse_memory_size_kilobytes() { 97 | assert_eq!(parse_memory_size("1K").unwrap(), 1024); 98 | assert_eq!(parse_memory_size("10K").unwrap(), 10 * 1024); 99 | } 100 | 101 | #[test] 102 | fn test_parse_memory_size_megabytes() { 103 | assert_eq!(parse_memory_size("1M").unwrap(), 1024 * 1024); 104 | assert_eq!(parse_memory_size("100M").unwrap(), 100 * 1024 * 1024); 105 | } 106 | 107 | #[test] 108 | fn test_parse_memory_size_gigabytes() { 109 | assert_eq!(parse_memory_size("1G").unwrap(), 1024 * 1024 * 1024); 110 | assert_eq!(parse_memory_size("2G").unwrap(), 2 * 1024 * 1024 * 1024); 111 | } 112 | 113 | #[test] 114 | fn test_parse_memory_size_case_insensitive() { 115 | assert_eq!(parse_memory_size("1m").unwrap(), 1024 * 1024); 116 | assert_eq!(parse_memory_size("1g").unwrap(), 1024 * 1024 * 1024); 117 | } 118 | 119 | #[test] 120 | fn test_parse_memory_size_whitespace() { 121 | assert_eq!(parse_memory_size(" 100M ").unwrap(), 100 * 1024 * 1024); 122 | } 123 | 124 | #[test] 125 | fn test_parse_memory_size_invalid() { 126 | assert!(parse_memory_size("not_a_number").is_err()); 127 | assert!(parse_memory_size("10X").is_err()); 128 | } 129 | 130 | #[test] 131 | fn test_get_uid_gid() { 132 | let uid = get_uid(); 133 | let gid = get_gid(); 134 | assert!(uid < u32::MAX); 135 | assert!(gid < u32::MAX); 136 | } 137 | 138 | #[test] 139 | fn test_is_root() { 140 | let is_root = is_root(); 141 | assert_eq!(is_root, get_uid() == 0); 142 | } 143 | 144 | #[test] 145 | fn test_root_override() { 146 | set_root_override(Some(true)); 147 | assert!(is_root()); 148 | set_root_override(Some(false)); 149 | assert!(!is_root()); 150 | set_root_override(None); 151 | } 152 | 153 | #[test] 154 | fn test_has_cgroup_v2() { 155 | let result = has_cgroup_v2(); 156 | let _valid = match result { 157 | true | false => true, 158 | }; 159 | } 160 | 161 | #[test] 162 | fn test_cgroup_exists() { 163 | use std::path::Path; 164 | assert!(cgroup_exists(Path::new("/"))); 165 | assert!(!cgroup_exists(Path::new( 166 | "/nonexistent/path/that/should/not/exist" 167 | ))); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /lib/monitoring/tests.rs: -------------------------------------------------------------------------------- 1 | use crate::monitoring::{EBpfMonitor, ProcessMonitor, ProcessState}; 2 | use crate::test_support::serial_guard; 3 | use nix::unistd::Pid; 4 | use std::time::Duration; 5 | 6 | #[test] 7 | fn process_monitor_collects_and_caches_stats() { 8 | let pid = Pid::from_raw(std::process::id() as i32); 9 | let mut monitor = ProcessMonitor::new(pid).expect("current process exists"); 10 | 11 | let first = monitor.collect_stats().expect("collect stats"); 12 | assert_eq!(first.pid, pid.as_raw()); 13 | assert!(monitor.peak_memory_mb() >= first.memory_usage_mb); 14 | 15 | let cached = monitor.last_stats().expect("stats cached"); 16 | assert_eq!(cached.pid, first.pid); 17 | assert!(monitor.elapsed() >= Duration::from_millis(0)); 18 | } 19 | 20 | #[test] 21 | fn process_monitor_rejects_missing_pid() { 22 | let invalid_pid = Pid::from_raw(9_999_999); 23 | let result = ProcessMonitor::new(invalid_pid); 24 | assert!(result.is_err()); 25 | } 26 | 27 | #[test] 28 | fn process_monitor_graceful_shutdown_terminates_child() { 29 | let _guard = serial_guard(); 30 | let mut child = std::process::Command::new("sleep") 31 | .arg("5") 32 | .spawn() 33 | .expect("failed to spawn sleep"); 34 | 35 | let pid = Pid::from_raw(child.id() as i32); 36 | let monitor = ProcessMonitor::new(pid).expect("child process exists"); 37 | monitor 38 | .graceful_shutdown(Duration::from_millis(50)) 39 | .expect("graceful shutdown"); 40 | 41 | let status = child.wait().expect("wait on child"); 42 | assert!(!status.success()); 43 | } 44 | 45 | #[test] 46 | fn process_monitor_send_sigterm_stops_child() { 47 | let _guard = serial_guard(); 48 | let mut child = std::process::Command::new("sleep") 49 | .arg("5") 50 | .spawn() 51 | .expect("spawn sleep"); 52 | let pid = Pid::from_raw(child.id() as i32); 53 | let monitor = ProcessMonitor::new(pid).expect("child process exists"); 54 | monitor.send_sigterm().expect("send sigterm"); 55 | let status = child.wait().expect("wait on child"); 56 | assert!(!status.success()); 57 | } 58 | 59 | #[test] 60 | fn process_monitor_send_sigkill_always_stops_child() { 61 | let _guard = serial_guard(); 62 | let mut child = std::process::Command::new("sleep") 63 | .arg("5") 64 | .spawn() 65 | .expect("spawn sleep"); 66 | let pid = Pid::from_raw(child.id() as i32); 67 | let monitor = ProcessMonitor::new(pid).expect("child process exists"); 68 | monitor.send_sigkill().expect("send sigkill"); 69 | let status = child.wait().expect("wait on child"); 70 | assert!(!status.success()); 71 | } 72 | 73 | #[test] 74 | fn process_monitor_is_alive_false_after_exit() { 75 | let _guard = serial_guard(); 76 | let mut child = std::process::Command::new("true") 77 | .spawn() 78 | .expect("spawn true"); 79 | let pid = Pid::from_raw(child.id() as i32); 80 | let monitor = ProcessMonitor::new(pid).expect("child process exists"); 81 | child.wait().expect("wait on child"); 82 | assert!(!monitor.is_alive().expect("is_alive result")); 83 | } 84 | 85 | #[test] 86 | fn ebpf_monitor_aggregates_events_and_slowest_calls() { 87 | let pid = Pid::from_raw(std::process::id() as i32); 88 | let mut monitor = EBpfMonitor::new(pid); 89 | 90 | monitor.add_event(crate::monitoring::ebpf::SyscallEvent { 91 | syscall_id: 1, 92 | syscall_name: "read".to_string(), 93 | duration_us: 5_000, 94 | timestamp: 0, 95 | is_slow: false, 96 | }); 97 | monitor.add_event(crate::monitoring::ebpf::SyscallEvent { 98 | syscall_id: 2, 99 | syscall_name: "write".to_string(), 100 | duration_us: 25_000, 101 | timestamp: 1, 102 | is_slow: true, 103 | }); 104 | 105 | let stats = monitor.collect_stats().expect("collect ebpf stats"); 106 | assert_eq!(stats.total_syscalls, 2); 107 | assert_eq!(stats.slow_syscalls, 1); 108 | assert_eq!(monitor.slowest_syscalls(1).len(), 1); 109 | assert_eq!(monitor.slowest_syscalls(1)[0].syscall_name, "write"); 110 | } 111 | 112 | #[test] 113 | fn ebpf_monitor_clear_resets_state() { 114 | let pid = Pid::from_raw(std::process::id() as i32); 115 | let mut monitor = EBpfMonitor::new(pid); 116 | 117 | monitor.add_event(crate::monitoring::ebpf::SyscallEvent { 118 | syscall_id: 3, 119 | syscall_name: "open".to_string(), 120 | duration_us: 1_000, 121 | timestamp: 2, 122 | is_slow: false, 123 | }); 124 | assert_eq!(monitor.slow_syscall_count(), 0); 125 | 126 | monitor.clear(); 127 | let stats = monitor.collect_stats().expect("collect stats after clear"); 128 | assert_eq!(stats.total_syscalls, 0); 129 | assert_eq!(monitor.slow_syscall_count(), 0); 130 | } 131 | 132 | #[test] 133 | fn process_state_from_char_variants() { 134 | assert_eq!(ProcessState::from_char('R'), ProcessState::Running); 135 | assert_eq!(ProcessState::from_char('S'), ProcessState::Sleeping); 136 | assert_eq!(ProcessState::from_char('Z'), ProcessState::Zombie); 137 | assert_eq!(ProcessState::from_char('X'), ProcessState::Unknown); 138 | } 139 | -------------------------------------------------------------------------------- /lib/storage/tests.rs: -------------------------------------------------------------------------------- 1 | use super::{LayerInfo, OverlayConfig, OverlayFS, VolumeManager, VolumeMount}; 2 | use std::fs; 3 | use std::io::Write; 4 | use std::path::Path; 5 | use tempfile::tempdir; 6 | 7 | fn overlay_environment() -> (tempfile::TempDir, OverlayConfig) { 8 | let base = tempdir().expect("tempdir"); 9 | let lower = base.path().join("lower"); 10 | let upper = base.path().join("upper/layer"); 11 | fs::create_dir_all(&lower).unwrap(); 12 | fs::create_dir_all(upper.parent().unwrap()).unwrap(); 13 | 14 | let config = OverlayConfig::new(&lower, &upper); 15 | (base, config) 16 | } 17 | 18 | #[test] 19 | fn overlay_config_validation_checks_lower_layer() { 20 | let config = OverlayConfig::new("/does/not/exist", "/tmp/upper"); 21 | assert!(config.validate().is_err()); 22 | } 23 | 24 | #[test] 25 | #[ignore] // Requires root privileges for mount syscall 26 | fn overlay_fs_setup_creates_directories() { 27 | let (_temp, config) = overlay_environment(); 28 | assert!(config.validate().is_ok()); 29 | 30 | let mut overlay = OverlayFS::new(config.clone()); 31 | overlay.setup().expect("overlay setup"); 32 | assert!(overlay.is_mounted()); 33 | assert!( 34 | overlay 35 | .merged_path() 36 | .starts_with(config.upper.parent().unwrap()) 37 | ); 38 | } 39 | 40 | #[test] 41 | #[ignore] // Requires root privileges for mount syscall 42 | fn overlay_fs_reports_changes_size() { 43 | let (_temp, config) = overlay_environment(); 44 | config.setup_directories().unwrap(); 45 | 46 | let mut overlay = OverlayFS::new(config.clone()); 47 | overlay.setup().unwrap(); 48 | 49 | let sample_file = overlay.upper_path().join("file.txt"); 50 | fs::write(&sample_file, b"hello world").unwrap(); 51 | let size = overlay.get_changes_size().unwrap(); 52 | 53 | assert!(size >= 11); 54 | overlay.cleanup().unwrap(); 55 | } 56 | 57 | #[test] 58 | fn layer_info_counts_files_and_bytes() { 59 | let temp = tempdir().unwrap(); 60 | let layer_path = temp.path().join("layer"); 61 | fs::create_dir_all(&layer_path).unwrap(); 62 | let file_path = layer_path.join("data.bin"); 63 | let mut file = fs::File::create(&file_path).unwrap(); 64 | file.write_all(&[0u8; 32]).unwrap(); 65 | 66 | let info = LayerInfo::from_path("layer", &layer_path, true).unwrap(); 67 | assert_eq!(info.file_count, 1); 68 | assert!(info.size >= 32); 69 | assert!(info.writable); 70 | } 71 | 72 | #[test] 73 | fn overlay_config_setup_directories_create_paths() { 74 | let (_temp, config) = overlay_environment(); 75 | config.setup_directories().unwrap(); 76 | assert!(config.upper.exists()); 77 | assert!(config.work.exists()); 78 | assert!(config.merged.exists()); 79 | } 80 | 81 | #[test] 82 | #[ignore] // Requires root privileges for mount syscall 83 | fn overlay_fs_cleanup_resets_state_and_removes_workdir() { 84 | let (_temp, config) = overlay_environment(); 85 | let work_dir = config.work.clone(); 86 | let mut overlay = OverlayFS::new(config); 87 | overlay.setup().unwrap(); 88 | assert!(overlay.is_mounted()); 89 | overlay.cleanup().unwrap(); 90 | assert!(!overlay.is_mounted()); 91 | assert!(!work_dir.exists()); 92 | } 93 | 94 | #[test] 95 | fn volume_mount_validation_for_bind_paths() { 96 | let temp = tempdir().unwrap(); 97 | let src = temp.path().join("src"); 98 | fs::create_dir_all(&src).unwrap(); 99 | 100 | let mount = VolumeMount::bind(&src, "/data"); 101 | assert!(mount.validate().is_ok()); 102 | 103 | let invalid = VolumeMount::bind("/missing", "/data"); 104 | assert!(invalid.validate().is_err()); 105 | } 106 | 107 | #[test] 108 | fn volume_manager_lifecycle_for_named_volumes() { 109 | let temp = tempdir().unwrap(); 110 | let manager = VolumeManager::new(temp.path()); 111 | 112 | manager.create_volume("vol1").expect("create volume"); 113 | let volumes = manager.list_volumes().expect("list volumes"); 114 | assert!(volumes.contains(&"vol1".to_string())); 115 | 116 | manager.delete_volume("vol1").expect("delete volume"); 117 | let volumes_after = manager.list_volumes().expect("list volumes"); 118 | assert!(!volumes_after.contains(&"vol1".to_string())); 119 | } 120 | 121 | #[test] 122 | fn volume_manager_adds_and_clears_mounts() { 123 | let temp = tempdir().unwrap(); 124 | let mut manager = VolumeManager::new(temp.path()); 125 | let mount = VolumeMount::tmpfs(Path::new("/data"), Some(1024)); 126 | 127 | manager.add_mount(mount).expect("add mount"); 128 | assert_eq!(manager.mounts().len(), 1); 129 | 130 | manager.clear_mounts(); 131 | assert!(manager.mounts().is_empty()); 132 | } 133 | 134 | #[test] 135 | fn volume_mount_validation_rejects_empty_destination() { 136 | let mount = VolumeMount::bind("/tmp", Path::new("")); 137 | assert!(mount.validate().is_err()); 138 | } 139 | 140 | #[test] 141 | fn volume_manager_reports_volume_size() { 142 | let temp = tempdir().unwrap(); 143 | let manager = VolumeManager::new(temp.path()); 144 | let vol_path = manager.create_volume("metrics").unwrap(); 145 | fs::write(vol_path.join("file.txt"), b"abc").unwrap(); 146 | 147 | let size = manager.get_volume_size("metrics").unwrap(); 148 | assert!(size >= 3); 149 | } 150 | 151 | #[test] 152 | fn volume_manager_delete_nonexistent_volume_is_noop() { 153 | let temp = tempdir().unwrap(); 154 | let manager = VolumeManager::new(temp.path()); 155 | manager.delete_volume("missing").unwrap(); 156 | } 157 | 158 | #[test] 159 | fn volume_mount_options_cover_variants() { 160 | let bind = VolumeMount::bind("/tmp", "/data"); 161 | assert_eq!(bind.get_mount_options(), "bind"); 162 | 163 | let readonly = VolumeMount::bind_readonly("/tmp", "/data"); 164 | assert_eq!(readonly.get_mount_options(), "bind,ro"); 165 | 166 | let tmpfs = VolumeMount::tmpfs("/run", Some(2048)); 167 | assert!(tmpfs.get_mount_options().contains("2048")); 168 | 169 | let named = VolumeMount::named("cache", "/cache"); 170 | assert_eq!(named.get_mount_options(), "named"); 171 | } 172 | 173 | #[test] 174 | fn volume_mount_named_validate() { 175 | let mount = VolumeMount::named("workspace", "/workspace"); 176 | assert!(mount.validate().is_ok()); 177 | } 178 | -------------------------------------------------------------------------------- /examples/cli_basic.rs: -------------------------------------------------------------------------------- 1 | //! CLI-based Sandbox Examples 2 | //! 3 | //! This example demonstrates how to use the sandbox-ctl CLI to create and manage sandboxes 4 | //! from the command line. It shows various ways to invoke the sandbox with different options. 5 | //! 6 | //! ## Prerequisites 7 | //! 8 | //! 1. Build the sandbox-ctl CLI: 9 | //! cargo build --bin sandbox-ctl 10 | //! 11 | //! 2. Run with root privileges (required for full isolation): 12 | //! sudo ./target/debug/sandbox-ctl run --id test-1 /bin/echo "hello world" 13 | //! 14 | //! ## Examples shown 15 | //! 16 | //! This file documents the CLI usage patterns. Here are the common invocations: 17 | //! 18 | //! ### 1. Basic execution (no resource limits) 19 | //! ```bash 20 | //! sandbox-ctl run --id my-sandbox /bin/echo "hello" 21 | //! ``` 22 | //! 23 | //! ### 2. With memory limit 24 | //! ```bash 25 | //! sandbox-ctl run --id memory-limited \ 26 | //! --memory 100M \ 27 | //! /bin/bash -c "echo 'Running with 100MB memory limit'" 28 | //! ``` 29 | //! 30 | //! ### 3. With CPU limit (50% of one core) 31 | //! ```bash 32 | //! sandbox-ctl run --id cpu-limited \ 33 | //! --cpu 50 \ 34 | //! /bin/stress-ng --cpu 1 --timeout 5s 35 | //! ``` 36 | //! 37 | //! ### 4. With timeout 38 | //! ```bash 39 | //! sandbox-ctl run --id timeout-example \ 40 | //! --timeout 2 \ 41 | //! /bin/sleep 10 # Will be killed after 2 seconds 42 | //! ``` 43 | //! 44 | //! ### 5. With seccomp profile 45 | //! ```bash 46 | //! sandbox-ctl run --id minimal-syscalls \ 47 | //! --seccomp minimal \ 48 | //! /bin/echo "Only essential syscalls allowed" 49 | //! ``` 50 | //! 51 | //! ### 6. Combined: Memory + CPU + Timeout + Seccomp 52 | //! ```bash 53 | //! sandbox-ctl run --id restricted \ 54 | //! --memory 256M \ 55 | //! --cpu 25 \ 56 | //! --timeout 30 \ 57 | //! --seccomp io-heavy \ 58 | //! /usr/bin/python3 script.py 59 | //! ``` 60 | //! 61 | //! ### 7. With custom sandbox root directory 62 | //! ```bash 63 | //! sandbox-ctl run --id custom-root \ 64 | //! --root /tmp/my-sandbox-root \ 65 | //! /bin/ls -la 66 | //! ``` 67 | //! 68 | //! ### 8. List available seccomp profiles 69 | //! ```bash 70 | //! sandbox-ctl profiles 71 | //! ``` 72 | //! 73 | //! ### 9. Check system requirements 74 | //! ```bash 75 | //! sandbox-ctl check 76 | //! ``` 77 | //! 78 | //! ## Memory limit formats 79 | //! 80 | //! The --memory flag supports multiple formats: 81 | //! - "64M" or "64MB" - megabytes 82 | //! - "1G" or "1GB" - gigabytes 83 | //! - "512K" or "512KB" - kilobytes 84 | //! - Direct bytes as number 85 | //! 86 | //! ## CPU limit (percentage) 87 | //! 88 | //! The --cpu flag accepts 0-100: 89 | //! - 25 = 25% of one CPU core 90 | //! - 50 = 50% of one CPU core (half core) 91 | //! - 100 = One full CPU core 92 | //! - 200 = Two CPU cores (on multi-core systems) 93 | //! 94 | //! ## Seccomp profiles 95 | //! 96 | //! Available profiles (use with --seccomp): 97 | //! - minimal: Only essential syscalls (exit, read, write) 98 | //! - io-heavy: Minimal + file I/O (open, close, seek, stat) 99 | //! - compute: IO-heavy + memory operations (mmap, brk, mprotect) 100 | //! - network: Compute + socket operations (socket, bind, listen) 101 | //! - unrestricted: Most syscalls allowed (for debugging) 102 | //! 103 | //! ## Running with sudo 104 | //! 105 | //! Since full isolation requires root: 106 | //! ```bash 107 | //! # Option 1: Run entire command as root 108 | //! sudo ./target/debug/sandbox-ctl run --id test /bin/echo "hello" 109 | //! 110 | //! # Option 2: Configure sudo to allow without password (advanced) 111 | //! # Add to /etc/sudoers: 112 | //! # user ALL=(ALL) NOPASSWD: /path/to/sandbox-ctl 113 | //! ``` 114 | //! 115 | //! ## Exit codes 116 | //! 117 | //! The CLI returns: 118 | //! - Exit code of the sandboxed program (0-255) 119 | //! - 1 if sandbox creation or execution failed 120 | //! 121 | //! ## Performance considerations 122 | //! 123 | //! - Memory limits are enforced at kernel level via Cgroup v2 124 | //! - CPU limits use CFS scheduler quotas 125 | //! - Seccomp filtering happens in kernel BPF 126 | //! - Namespace isolation has minimal overhead 127 | 128 | fn main() { 129 | println!("=== Sandbox-ctl CLI Usage Examples ===\n"); 130 | 131 | println!("This is a documentation file showing CLI usage patterns."); 132 | println!("Build and run sandbox-ctl with the examples below:\n"); 133 | 134 | println!("Basic usage:"); 135 | println!(" cargo build --bin sandbox-ctl"); 136 | println!(" sudo ./target/debug/sandbox-ctl run --id my-box /bin/echo hello\n"); 137 | 138 | println!("With resource limits:"); 139 | println!(" sudo ./target/debug/sandbox-ctl run \\"); 140 | println!(" --id limited \\"); 141 | println!(" --memory 256M \\"); 142 | println!(" --cpu 50 \\"); 143 | println!(" --timeout 30 \\"); 144 | println!(" --seccomp io-heavy \\"); 145 | println!(" /bin/bash -c 'echo test && sleep 2'\n"); 146 | 147 | println!("Check system requirements:"); 148 | println!(" sudo ./target/debug/sandbox-ctl check\n"); 149 | 150 | println!("List seccomp profiles:"); 151 | println!(" ./target/debug/sandbox-ctl profiles\n"); 152 | 153 | println!("Try memory-limited execution:"); 154 | println!(" sudo ./target/debug/sandbox-ctl run \\"); 155 | println!(" --id memory-test \\"); 156 | println!(" --memory 100M \\"); 157 | println!(" /usr/bin/yes > /dev/null\n"); 158 | 159 | println!("Try CPU-limited execution:"); 160 | println!(" sudo ./target/debug/sandbox-ctl run \\"); 161 | println!(" --id cpu-test \\"); 162 | println!(" --cpu 25 \\"); 163 | println!(" /bin/sh -c 'for i in $(seq 1 1000000); do :; done'\n"); 164 | 165 | println!("Try timeout:"); 166 | println!(" sudo ./target/debug/sandbox-ctl run \\"); 167 | println!(" --id timeout-test \\"); 168 | println!(" --timeout 2 \\"); 169 | println!(" /bin/sleep 10\n"); 170 | 171 | println!("Minimal seccomp profile (restricted syscalls):"); 172 | println!(" sudo ./target/debug/sandbox-ctl run \\"); 173 | println!(" --id strict \\"); 174 | println!(" --seccomp minimal \\"); 175 | println!(" /bin/echo 'Only essential syscalls'\n"); 176 | 177 | println!("See the source code comments for more details on each option."); 178 | } 179 | -------------------------------------------------------------------------------- /tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | //! Integration tests for sandbox-rs 2 | //! 3 | //! These tests verify the sandbox API and configuration. 4 | //! Tests that require root are marked with #[ignore] and can be run with: 5 | //! sudo cargo test -- --ignored 6 | 7 | use sandbox_rs::{NamespaceConfig, SandboxBuilder, SeccompProfile}; 8 | use std::sync::Mutex; 9 | use std::time::Duration; 10 | 11 | static INTEGRATION_TEST_LOCK: Mutex<()> = Mutex::new(()); 12 | 13 | /// Test that basic sandbox builder works 14 | #[test] 15 | fn test_sandbox_builder_creation() { 16 | let _lock = INTEGRATION_TEST_LOCK.lock(); 17 | let _ = SandboxBuilder::new("test-builder"); 18 | } 19 | 20 | /// Test builder with memory limit 21 | #[test] 22 | fn test_sandbox_memory_limit_parsing() { 23 | let _lock = INTEGRATION_TEST_LOCK.lock(); 24 | 25 | let result = SandboxBuilder::new("test-memory").memory_limit_str("256M"); 26 | 27 | assert!(result.is_ok()); 28 | } 29 | 30 | /// Test builder with CPU limit 31 | #[test] 32 | fn test_sandbox_cpu_limit_config() { 33 | let _lock = INTEGRATION_TEST_LOCK.lock(); 34 | 35 | let _builder = SandboxBuilder::new("test-cpu").cpu_limit_percent(50); 36 | } 37 | 38 | /// Test builder with max PIDs 39 | #[test] 40 | fn test_sandbox_max_pids_config() { 41 | let _lock = INTEGRATION_TEST_LOCK.lock(); 42 | 43 | let _builder = SandboxBuilder::new("test-pids").max_pids(16); 44 | } 45 | 46 | /// Test builder with timeout 47 | #[test] 48 | fn test_sandbox_timeout_config() { 49 | let _lock = INTEGRATION_TEST_LOCK.lock(); 50 | 51 | let _builder = SandboxBuilder::new("test-timeout").timeout(Duration::from_secs(10)); 52 | } 53 | 54 | /// Test namespace configuration 55 | #[test] 56 | fn test_namespace_default_config() { 57 | let ns = NamespaceConfig::default(); 58 | 59 | assert!(ns.pid); 60 | assert!(ns.mount); 61 | assert!(ns.net); 62 | assert!(!ns.user); 63 | } 64 | 65 | /// Test minimal namespace config 66 | #[test] 67 | fn test_namespace_minimal_config() { 68 | let ns = NamespaceConfig::minimal(); 69 | 70 | assert!(ns.pid); 71 | assert_eq!(ns.enabled_count(), 4); // PID, IPC, NET, MOUNT 72 | } 73 | 74 | /// Test all namespaces config 75 | #[test] 76 | fn test_namespace_all_config() { 77 | let ns = NamespaceConfig::all(); 78 | 79 | assert!(ns.pid); 80 | assert!(ns.ipc); 81 | assert!(ns.net); 82 | assert!(ns.mount); 83 | assert!(ns.uts); 84 | assert!(ns.user); 85 | } 86 | 87 | /// Test seccomp profile descriptions 88 | #[test] 89 | fn test_seccomp_profiles_have_descriptions() { 90 | for profile in SeccompProfile::all() { 91 | let desc = profile.description(); 92 | assert!(!desc.is_empty()); 93 | } 94 | } 95 | 96 | /// Test seccomp filter creation from profile 97 | #[test] 98 | fn test_seccomp_filter_from_profile() { 99 | let filter = sandbox_rs::isolation::SeccompFilter::from_profile(SeccompProfile::IoHeavy); 100 | 101 | assert!(filter.allowed_count() > 0); 102 | } 103 | 104 | /// Test memory format parsing with various units 105 | #[test] 106 | fn test_memory_limit_formats() { 107 | let cases = vec!["64M", "256M", "1G"]; 108 | 109 | for input in cases { 110 | let result = SandboxBuilder::new("test").memory_limit_str(input); 111 | 112 | assert!(result.is_ok(), "Should parse {}", input); 113 | } 114 | } 115 | 116 | /// Test CPU limit percentages 117 | #[test] 118 | fn test_cpu_limit_percentages() { 119 | let percentages = vec![1, 10, 50, 100, 200]; 120 | 121 | for percent in percentages { 122 | let _builder = SandboxBuilder::new("test").cpu_limit_percent(percent); 123 | } 124 | } 125 | 126 | /// Test combined builder configuration 127 | #[test] 128 | fn test_combined_builder_config() { 129 | let _builder = SandboxBuilder::new("combined") 130 | .memory_limit_str("256M") 131 | .expect("Should parse memory") 132 | .cpu_limit_percent(50) 133 | .max_pids(16) 134 | .timeout(Duration::from_secs(30)) 135 | .seccomp_profile(SeccompProfile::IoHeavy); 136 | } 137 | 138 | /// Test that zero CPU percent is handled 139 | #[test] 140 | fn test_cpu_limit_zero_percent() { 141 | let _builder = SandboxBuilder::new("test").cpu_limit_percent(0); 142 | } 143 | 144 | // Tests below require root privileges 145 | // Run with: sudo cargo test -- --ignored 146 | 147 | /// Test actual sandbox building (requires proper namespace support) 148 | #[test] 149 | #[ignore] 150 | fn test_sandbox_build_with_isolation() { 151 | let _lock = INTEGRATION_TEST_LOCK.lock(); 152 | 153 | let result = SandboxBuilder::new("isolation-test") 154 | .namespaces(NamespaceConfig::minimal()) 155 | .build(); 156 | 157 | let _ = result; 158 | } 159 | 160 | /// Test execution with echo (requires working sandbox) 161 | #[test] 162 | #[ignore] 163 | fn test_sandbox_echo_execution() { 164 | let _lock = INTEGRATION_TEST_LOCK.lock(); 165 | 166 | let mut sandbox = match SandboxBuilder::new("echo-test").build() { 167 | Ok(sb) => sb, 168 | Err(_) => return, 169 | }; 170 | 171 | let result = sandbox 172 | .run("/bin/echo", &["hello"]) 173 | .expect("Failed to run echo"); 174 | 175 | assert_eq!(result.exit_code, 0); 176 | } 177 | 178 | /// Test process exit codes 179 | #[test] 180 | #[ignore] 181 | fn test_exit_codes() { 182 | let _lock = INTEGRATION_TEST_LOCK.lock(); 183 | 184 | let mut sandbox_ok = match SandboxBuilder::new("exit-ok").build() { 185 | Ok(sb) => sb, 186 | Err(_) => return, 187 | }; 188 | 189 | let result_ok = sandbox_ok 190 | .run("/bin/true", &[]) 191 | .expect("Should run /bin/true"); 192 | assert_eq!(result_ok.exit_code, 0); 193 | 194 | let mut sandbox_fail = match SandboxBuilder::new("exit-fail").build() { 195 | Ok(sb) => sb, 196 | Err(_) => return, 197 | }; 198 | 199 | let result_fail = sandbox_fail 200 | .run("/bin/false", &[]) 201 | .expect("Should run /bin/false"); 202 | assert_ne!(result_fail.exit_code, 0); 203 | } 204 | 205 | /// Test resource tracking 206 | #[test] 207 | #[ignore] 208 | fn test_resource_tracking() { 209 | let _lock = INTEGRATION_TEST_LOCK.lock(); 210 | 211 | let mut sandbox = match SandboxBuilder::new("resources-test") 212 | .memory_limit_str("256M") 213 | .expect("Should parse memory") 214 | .cpu_limit_percent(50) 215 | .build() 216 | { 217 | Ok(sb) => sb, 218 | Err(_) => return, 219 | }; 220 | 221 | let result = sandbox.run("/bin/echo", &["test"]).expect("Failed to run"); 222 | 223 | assert!(result.wall_time_ms > 0); 224 | assert_eq!(result.exit_code, 0); 225 | } 226 | -------------------------------------------------------------------------------- /tests/stress_tests.rs: -------------------------------------------------------------------------------- 1 | //! Stress tests for sandbox-rs builder and configuration 2 | //! 3 | //! These tests verify that the sandbox configuration API is robust. 4 | 5 | use sandbox_rs::{NamespaceConfig, SandboxBuilder, SeccompProfile}; 6 | use std::sync::Mutex; 7 | use std::time::Duration; 8 | 9 | static STRESS_TEST_LOCK: Mutex<()> = Mutex::new(()); 10 | 11 | /// Test rapid builder creation 12 | #[test] 13 | fn stress_rapid_builder_creation() { 14 | let _lock = STRESS_TEST_LOCK.lock(); 15 | 16 | for i in 0..50 { 17 | let _builder = SandboxBuilder::new(&format!("stress-builder-{}", i)) 18 | .memory_limit_str("256M") 19 | .expect("Should parse memory"); 20 | } 21 | } 22 | 23 | /// Test many CPU limit configurations 24 | #[test] 25 | fn stress_cpu_limit_configurations() { 26 | let _lock = STRESS_TEST_LOCK.lock(); 27 | 28 | let cpu_limits = vec![1, 5, 10, 25, 50, 75, 100, 150, 200, 300, 400]; 29 | 30 | for (i, percent) in cpu_limits.iter().enumerate() { 31 | let _builder = 32 | SandboxBuilder::new(&format!("stress-cpu-{}", i)).cpu_limit_percent(*percent); 33 | } 34 | } 35 | 36 | /// Test many memory limit configurations 37 | #[test] 38 | fn stress_memory_limit_configurations() { 39 | let _lock = STRESS_TEST_LOCK.lock(); 40 | 41 | let memory_limits = vec![ 42 | "4M", "8M", "16M", "32M", "64M", "128M", "256M", "512M", "1G", "2G", 43 | ]; 44 | 45 | for (i, limit) in memory_limits.iter().enumerate() { 46 | let _builder = SandboxBuilder::new(&format!("stress-mem-{}", i)) 47 | .memory_limit_str(limit) 48 | .unwrap_or_else(|_| panic!("Should parse {}", limit)); 49 | } 50 | } 51 | 52 | /// Test combined configurations 53 | #[test] 54 | fn stress_combined_configurations() { 55 | let _lock = STRESS_TEST_LOCK.lock(); 56 | 57 | let configs = [(64, 10), (128, 25), (256, 50), (512, 75), (1024, 100)]; 58 | 59 | for (i, (mem_mb, cpu_percent)) in configs.iter().enumerate() { 60 | let _builder = SandboxBuilder::new(&format!("stress-combined-{}", i)) 61 | .memory_limit_str(&format!("{}M", mem_mb)) 62 | .expect("Should parse memory") 63 | .cpu_limit_percent(*cpu_percent); 64 | } 65 | } 66 | 67 | /// Test timeout variety 68 | #[test] 69 | fn stress_timeout_configurations() { 70 | let _lock = STRESS_TEST_LOCK.lock(); 71 | 72 | let timeouts = [ 73 | Duration::from_millis(100), 74 | Duration::from_millis(500), 75 | Duration::from_secs(1), 76 | Duration::from_secs(5), 77 | Duration::from_secs(10), 78 | Duration::from_secs(60), 79 | ]; 80 | 81 | for (i, timeout) in timeouts.iter().enumerate() { 82 | let _builder = SandboxBuilder::new(&format!("stress-timeout-{}", i)).timeout(*timeout); 83 | } 84 | } 85 | 86 | /// Test max_pids variety 87 | #[test] 88 | fn stress_max_pids_configurations() { 89 | let _lock = STRESS_TEST_LOCK.lock(); 90 | 91 | let max_pids = vec![1, 2, 4, 8, 16, 32, 64, 128, 256]; 92 | 93 | for (i, pids) in max_pids.iter().enumerate() { 94 | let _builder = SandboxBuilder::new(&format!("stress-pids-{}", i)).max_pids(*pids); 95 | } 96 | } 97 | 98 | /// Test seccomp profile changes 99 | #[test] 100 | fn stress_seccomp_profiles() { 101 | let _lock = STRESS_TEST_LOCK.lock(); 102 | 103 | let profiles = [ 104 | SeccompProfile::Minimal, 105 | SeccompProfile::IoHeavy, 106 | SeccompProfile::Compute, 107 | SeccompProfile::Network, 108 | SeccompProfile::Unrestricted, 109 | ]; 110 | 111 | for (i, profile) in profiles.iter().enumerate() { 112 | let _builder = 113 | SandboxBuilder::new(&format!("stress-seccomp-{}", i)).seccomp_profile(profile.clone()); 114 | } 115 | } 116 | 117 | /// Test namespace variety 118 | #[test] 119 | fn stress_namespace_configurations() { 120 | let _lock = STRESS_TEST_LOCK.lock(); 121 | 122 | let configs = [ 123 | NamespaceConfig::minimal(), 124 | NamespaceConfig::default(), 125 | NamespaceConfig::all(), 126 | ]; 127 | 128 | for (i, config) in configs.iter().enumerate() { 129 | let _builder = SandboxBuilder::new(&format!("stress-ns-{}", i)).namespaces(config.clone()); 130 | } 131 | } 132 | 133 | /// Test extreme memory limits 134 | #[test] 135 | fn stress_extreme_memory_limits() { 136 | let _lock = STRESS_TEST_LOCK.lock(); 137 | 138 | let limits = vec!["1M", "4M", "8M", "512M", "1G", "2G", "4G", "8G", "16G"]; 139 | 140 | for (i, limit) in limits.iter().enumerate() { 141 | let result = 142 | SandboxBuilder::new(&format!("stress-mem-extreme-{}", i)).memory_limit_str(limit); 143 | 144 | assert!(result.is_ok(), "Should parse {}", limit); 145 | } 146 | } 147 | 148 | /// Test extreme CPU limits 149 | #[test] 150 | fn stress_extreme_cpu_limits() { 151 | let _lock = STRESS_TEST_LOCK.lock(); 152 | 153 | let limits = [1, 10, 50, 100, 200, 400, 800]; 154 | 155 | for (i, percent) in limits.iter().enumerate() { 156 | let _builder = 157 | SandboxBuilder::new(&format!("stress-cpu-extreme-{}", i)).cpu_limit_percent(*percent); 158 | } 159 | } 160 | 161 | /// Test all seccomp profiles available 162 | #[test] 163 | fn stress_all_seccomp_profiles() { 164 | let _lock = STRESS_TEST_LOCK.lock(); 165 | 166 | let all_profiles = SeccompProfile::all(); 167 | assert!(!all_profiles.is_empty()); 168 | 169 | for (i, profile) in all_profiles.iter().enumerate() { 170 | let _builder = 171 | SandboxBuilder::new(&format!("stress-profile-{}", i)).seccomp_profile(profile.clone()); 172 | } 173 | } 174 | 175 | /// Test very long sandbox IDs 176 | #[test] 177 | fn stress_long_sandbox_ids() { 178 | let _lock = STRESS_TEST_LOCK.lock(); 179 | 180 | let long_id = "very_long_sandbox_id_".repeat(5); 181 | let _builder = SandboxBuilder::new(&long_id); 182 | } 183 | 184 | /// Test special characters in sandbox IDs 185 | #[test] 186 | fn stress_special_char_sandbox_ids() { 187 | let _lock = STRESS_TEST_LOCK.lock(); 188 | 189 | let ids = [ 190 | "sandbox-with-dashes", 191 | "sandbox_with_underscores", 192 | "sandbox123numbers", 193 | "UPPERCASE", 194 | "MixedCase", 195 | ]; 196 | 197 | for id in ids.iter() { 198 | let _builder = SandboxBuilder::new(id); 199 | } 200 | } 201 | 202 | /// Test memory format parsing edge cases 203 | #[test] 204 | fn stress_memory_format_variations() { 205 | let _lock = STRESS_TEST_LOCK.lock(); 206 | 207 | let formats = ["1M", "10M", "100M", "256M", "512M", "1G", "2G", "10G"]; 208 | 209 | for (i, format) in formats.iter().enumerate() { 210 | let result = SandboxBuilder::new(&format!("stress-fmt-{}", i)).memory_limit_str(format); 211 | 212 | assert!(result.is_ok(), "Should parse {}", format); 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /lib/execution/init.rs: -------------------------------------------------------------------------------- 1 | //! Minimal init process for sandbox 2 | 3 | use nix::sys::signal::{SigHandler, Signal, signal}; 4 | use nix::unistd::execv; 5 | use std::ffi::CString; 6 | use std::process::exit; 7 | 8 | /// Simple init process that manages sandbox 9 | pub struct SandboxInit { 10 | /// Arguments to pass to user program 11 | pub program: String, 12 | pub args: Vec, 13 | } 14 | 15 | impl SandboxInit { 16 | /// Create new init process 17 | pub fn new(program: String, args: Vec) -> Self { 18 | Self { program, args } 19 | } 20 | 21 | /// Run init process 22 | /// This becomes PID 1 inside the sandbox 23 | pub fn run(&self) -> ! { 24 | // Setup signal handlers 25 | Self::setup_signals(); 26 | 27 | Self::mount_procfs(); 28 | Self::mount_sysfs(); 29 | 30 | // Execute user program 31 | self.exec_user_program(); 32 | } 33 | 34 | /// Setup signal handlers for init 35 | fn setup_signals() { 36 | // Ignore SIGCHLD so we don't become zombie 37 | unsafe { 38 | let _ = signal(Signal::SIGCHLD, SigHandler::SigIgn); 39 | let _ = signal(Signal::SIGTERM, SigHandler::SigDfl); 40 | } 41 | } 42 | 43 | fn mount_procfs() { 44 | use std::ffi::CString; 45 | 46 | let _ = std::fs::create_dir("/proc"); 47 | 48 | // Use mount syscall instead of external mount command 49 | let source = CString::new("proc").unwrap(); 50 | let target = CString::new("/proc").unwrap(); 51 | let fstype = CString::new("proc").unwrap(); 52 | 53 | unsafe { 54 | libc::mount( 55 | source.as_ptr(), 56 | target.as_ptr(), 57 | fstype.as_ptr(), 58 | 0, 59 | std::ptr::null(), 60 | ); 61 | } 62 | } 63 | 64 | fn mount_sysfs() { 65 | use std::ffi::CString; 66 | 67 | let _ = std::fs::create_dir("/sys"); 68 | 69 | // Use mount syscall instead of external mount command 70 | let source = CString::new("sysfs").unwrap(); 71 | let target = CString::new("/sys").unwrap(); 72 | let fstype = CString::new("sysfs").unwrap(); 73 | 74 | unsafe { 75 | libc::mount( 76 | source.as_ptr(), 77 | target.as_ptr(), 78 | fstype.as_ptr(), 79 | 0, 80 | std::ptr::null(), 81 | ); 82 | } 83 | } 84 | 85 | /// Execute user program 86 | fn exec_user_program(&self) -> ! { 87 | let program_cstr = match CString::new(self.program.clone()) { 88 | Ok(s) => s, 89 | Err(_) => { 90 | eprintln!("Invalid program name"); 91 | exit(1); 92 | } 93 | }; 94 | 95 | let args_cstr: Vec = self 96 | .args 97 | .iter() 98 | .map(|arg| CString::new(arg.clone()).unwrap_or_else(|_| CString::new("").unwrap())) 99 | .collect(); 100 | 101 | let args_refs: Vec<&CString> = vec![&program_cstr] 102 | .into_iter() 103 | .chain(args_cstr.iter()) 104 | .collect(); 105 | 106 | match execv(&program_cstr, &args_refs) { 107 | Ok(_) => { 108 | // execv replaces process, never returns on success 109 | exit(0); 110 | } 111 | Err(e) => { 112 | eprintln!("Failed to execute program: {}", e); 113 | exit(1); 114 | } 115 | } 116 | } 117 | 118 | /// Reap zombie children 119 | pub fn reap_children() { 120 | use nix::sys::wait::{WaitStatus, waitpid}; 121 | use nix::unistd::Pid; 122 | 123 | loop { 124 | match waitpid( 125 | Pid::from_raw(-1), 126 | Some(nix::sys::wait::WaitPidFlag::WNOHANG), 127 | ) { 128 | Ok(WaitStatus::Exited(pid, _status)) => { 129 | eprintln!("[init] Child {} exited", pid); 130 | } 131 | Ok(WaitStatus::Signaled(pid, signal, _core)) => { 132 | eprintln!("[init] Child {} killed by {:?}", pid, signal); 133 | } 134 | Ok(WaitStatus::StillAlive) => break, 135 | Ok(_) => continue, 136 | Err(_) => break, 137 | } 138 | } 139 | } 140 | } 141 | 142 | #[cfg(test)] 143 | mod tests { 144 | use super::*; 145 | 146 | #[test] 147 | fn test_init_creation() { 148 | let init = SandboxInit::new("/bin/echo".to_string(), vec!["hello".to_string()]); 149 | 150 | assert_eq!(init.program, "/bin/echo"); 151 | assert_eq!(init.args.len(), 1); 152 | assert_eq!(init.args[0], "hello"); 153 | } 154 | 155 | #[test] 156 | fn test_init_with_multiple_args() { 157 | let init = SandboxInit::new( 158 | "/bin/echo".to_string(), 159 | vec![ 160 | "hello".to_string(), 161 | "world".to_string(), 162 | "from".to_string(), 163 | "init".to_string(), 164 | ], 165 | ); 166 | 167 | assert_eq!(init.args.len(), 4); 168 | } 169 | 170 | #[test] 171 | fn test_init_empty_args() { 172 | let init = SandboxInit::new("/bin/sh".to_string(), Vec::new()); 173 | 174 | assert!(init.args.is_empty()); 175 | } 176 | 177 | #[test] 178 | fn test_mount_helpers_are_best_effort() { 179 | SandboxInit::mount_procfs(); 180 | SandboxInit::mount_sysfs(); 181 | } 182 | 183 | #[test] 184 | fn test_setup_signals_runs() { 185 | // Store original handlers so we can restore them 186 | let original_sigchld = unsafe { signal(Signal::SIGCHLD, SigHandler::SigDfl) }; 187 | 188 | // Test the setup 189 | SandboxInit::setup_signals(); 190 | 191 | // Restore original handlers to not affect other tests 192 | unsafe { 193 | let _ = signal( 194 | Signal::SIGCHLD, 195 | original_sigchld.unwrap_or(SigHandler::SigDfl), 196 | ); 197 | } 198 | } 199 | 200 | #[test] 201 | fn test_init_program_stored_correctly() { 202 | let program = "/usr/bin/python3".to_string(); 203 | let init = SandboxInit::new(program.clone(), vec![]); 204 | 205 | assert_eq!(init.program, program); 206 | } 207 | 208 | #[test] 209 | fn test_init_args_stored_correctly() { 210 | let args = vec!["arg1".to_string(), "arg2".to_string(), "arg3".to_string()]; 211 | let init = SandboxInit::new("/bin/test".to_string(), args.clone()); 212 | 213 | assert_eq!(init.args, args); 214 | } 215 | 216 | #[test] 217 | fn test_init_clone() { 218 | let init1 = SandboxInit::new("/bin/echo".to_string(), vec!["test".to_string()]); 219 | 220 | let init2 = SandboxInit::new(init1.program.clone(), init1.args.clone()); 221 | 222 | assert_eq!(init1.program, init2.program); 223 | assert_eq!(init1.args, init2.args); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # sandbox-rs 2 | 3 | ⚠️ In active development, use with caution. 4 | 5 | A comprehensive Rust sandbox implementation that provides process isolation, resource limiting, and syscall filtering for secure program execution. 6 | 7 | ![Tests](https://img.shields.io/github/actions/workflow/status/ErickJ3/sandbox-rs/ci.yml?branch=main&label=test) 8 | [![codecov](https://codecov.io/gh/ErickJ3/sandbox-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/ErickJ3/sandbox-rs) 9 | [![Crates.io](https://img.shields.io/crates/v/sandbox-rs.svg)](https://crates.io/crates/sandbox-rs) 10 | [![Documentation](https://docs.rs/sandbox-rs/badge.svg)](https://docs.rs/sandbox-rs) 11 | ![Rust](https://img.shields.io/badge/rust-1.91%2B-orange.svg) 12 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 13 | 14 | ## Overview 15 | 16 | sandbox-rs is a library and CLI tool for creating lightweight, secure sandboxes on Linux systems. It combines multiple isolation mechanisms—Linux namespaces, Seccomp BPF filtering, Cgroup v2 resource limits, and filesystem isolation—into a unified, easy-to-use interface. 17 | 18 | ## Features 19 | 20 | ### Isolation 21 | - **Linux Namespaces**: PID, IPC, network, mount, UTS, and user namespaces for complete process isolation 22 | - **Seccomp Filtering**: BPF-based syscall filtering with five predefined profiles (minimal, io-heavy, compute, network, unrestricted) 23 | - **Filesystem Isolation**: OverlayFS support with direct mount syscalls (requires root) 24 | 25 | ### Resource Management 26 | - **Memory Limits**: Hard ceiling with out-of-memory enforcement 27 | - **CPU Limits**: Quota-based scheduling with percentage-based controls 28 | - **Process Limits**: Maximum PID restrictions per sandbox 29 | - **Runtime Monitoring**: Real-time resource usage tracking 30 | 31 | ### Execution 32 | - **Process Isolation**: Namespace-based cloning with independent lifecycles 33 | - **Init Process**: Zombie reaping and signal handling 34 | - **Root Isolation**: Chroot support with credential switching (UID/GID) 35 | 36 | ## Requirements 37 | 38 | - **Linux kernel 5.10+** (Cgroup v2 support required) 39 | - **Root privileges REQUIRED** - The sandbox enforces proper isolation through cgroups, namespaces, and filesystem operations that all require root access. Running without root will result in an error. 40 | - libc support for namespace and seccomp operations 41 | 42 | ## Installation 43 | 44 | Add to your `Cargo.toml`: 45 | 46 | ```toml 47 | [dependencies] 48 | sandbox-rs = "0.1" 49 | ``` 50 | 51 | ## Usage 52 | 53 | ### Library 54 | 55 | **One-shot execution:** 56 | 57 | ```rust 58 | use sandbox_rs::{SandboxBuilder, SeccompProfile}; 59 | use std::time::Duration; 60 | 61 | fn main() -> Result<(), Box> { 62 | let mut sandbox = SandboxBuilder::new("my-sandbox") 63 | .memory_limit_str("256M")? 64 | .cpu_limit_percent(50) 65 | .timeout(Duration::from_secs(30)) 66 | .seccomp_profile(SeccompProfile::IoHeavy) 67 | .build()?; 68 | 69 | let result = sandbox.run("/bin/echo", &["hello world"])?; 70 | println!("Exit code: {}", result.exit_code); 71 | println!("Memory peak: {} bytes", result.memory_peak); 72 | println!("CPU time: {} μs", result.cpu_time_us); 73 | 74 | Ok(()) 75 | } 76 | ``` 77 | 78 | **Streaming output (real-time):** 79 | 80 | ```rust 81 | use sandbox_rs::{SandboxBuilder, StreamChunk}; 82 | use std::time::Duration; 83 | 84 | fn main() -> Result<(), Box> { 85 | let mut sandbox = SandboxBuilder::new("my-sandbox") 86 | .memory_limit_str("256M")? 87 | .cpu_limit_percent(50) 88 | .timeout(Duration::from_secs(30)) 89 | .build()?; 90 | 91 | let (result, stream) = sandbox.run_with_stream("/bin/echo", &["hello world"])?; 92 | 93 | // Process output in real-time 94 | for chunk in stream.into_iter() { 95 | match chunk { 96 | StreamChunk::Stdout(line) => println!("out: {}", line), 97 | StreamChunk::Stderr(line) => eprintln!("err: {}", line), 98 | StreamChunk::Exit { exit_code, signal } => { 99 | println!("Exit code: {}", exit_code); 100 | } 101 | } 102 | } 103 | 104 | Ok(()) 105 | } 106 | ``` 107 | 108 | ### CLI 109 | 110 | **Native:** 111 | 112 | ```bash 113 | # Run program with sandbox 114 | sandbox-ctl run --id test-run --memory 256M --cpu 50 --timeout 30 /bin/echo "hello world" 115 | 116 | # List available seccomp profiles 117 | sandbox-ctl profiles 118 | 119 | # Check system requirements 120 | sandbox-ctl check 121 | ``` 122 | 123 | **Docker:** 124 | 125 | Run sandbox-ctl in a container without requiring root on the host machine: 126 | 127 | ```bash 128 | # Build the Docker image 129 | docker build -t sandbox-rs . 130 | 131 | # Run sandbox-ctl with required permissions and cgroup access 132 | docker run --privileged --cgroupns=host \ 133 | -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ 134 | sandbox-rs run \ 135 | --id test-run \ 136 | --memory 256M \ 137 | --cpu 50 \ 138 | --timeout 30 \ 139 | /bin/echo "hello world" 140 | 141 | # Or use docker-compose (already configured) 142 | docker-compose run sandbox-ctl run \ 143 | --id test-run \ 144 | --memory 256M \ 145 | --cpu 50 \ 146 | /bin/echo "hello world" 147 | 148 | # Interactive mode 149 | docker run -it --privileged --cgroupns=host \ 150 | -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ 151 | sandbox-rs --help 152 | ``` 153 | 154 | **Note:** The container requires: 155 | - Privileged mode for Linux namespaces and seccomp filtering 156 | - Host cgroup namespace (`--cgroupns=host`) to manage cgroups 157 | - Mount of `/sys/fs/cgroup` from host for resource limiting 158 | 159 | ## Architecture 160 | 161 | sandbox-rs is organized into modular layers: 162 | 163 | - **isolation**: Namespace and Seccomp filtering mechanisms 164 | - **resources**: Cgroup v2 resource limit enforcement 165 | - **execution**: Process lifecycle management and initialization 166 | - **storage**: Filesystem isolation with overlay and volume support 167 | - **monitoring**: Process and syscall observation 168 | - **network**: Network namespace configuration 169 | - **controller**: Main orchestration layer coordinating all subsystems 170 | 171 | ## Configuration 172 | 173 | ### Memory Limits 174 | 175 | Accepts human-readable formats: 176 | - `100M` - 100 megabytes 177 | - `1G` - 1 gigabyte 178 | - Direct byte count as u64 179 | 180 | ### CPU Limits 181 | 182 | CPU quotas are enforced per sandbox: 183 | - Percentage mode (0-100): `cpu_limit_percent(50)` → 50% of one CPU core 184 | - Raw quota mode: `cpu_quota(50000, 100000)` → 50ms per 100ms period 185 | 186 | ### Seccomp Profiles 187 | 188 | Five builtin profiles control allowed syscalls: 189 | 190 | - **minimal**: Basic syscalls only (exit, read, write) 191 | - **io-heavy**: Minimal + file operations (open, close, seek, stat) 192 | - **compute**: IO-heavy + memory operations (mmap, brk, mprotect) 193 | - **network**: Compute + socket operations (socket, bind, listen, connect) 194 | - **unrestricted**: Most syscalls allowed (for debugging) 195 | 196 | ## Security Considerations 197 | 198 | This implementation provides defense-in-depth through multiple isolation layers. However: 199 | 200 | - Sandbox escapes are possible through kernel vulnerabilities 201 | - Not suitable for untrusted code execution without additional hardening 202 | - Should be combined with AppArmor or SELinux for production use 203 | - Requires ongoing kernel security updates 204 | 205 | ## Testing 206 | 207 | ```bash 208 | cargo test 209 | ``` 210 | 211 | Tests are marked `serial` where required due to global state (root and cgroup manipulation). 212 | 213 | ## License 214 | 215 | See LICENSE file for details. 216 | -------------------------------------------------------------------------------- /examples/overlay_filesystem.rs: -------------------------------------------------------------------------------- 1 | //! Overlay Filesystem Example 2 | //! 3 | //! This example demonstrates how to use overlay filesystems with sandboxes 4 | //! for persistent storage with copy-on-write semantics. 5 | //! 6 | //! ## Overview 7 | //! 8 | //! An overlay filesystem combines: 9 | //! - **Lower layer**: Read-only base filesystem (immutable) 10 | //! - **Upper layer**: Read-write changes (modifications) 11 | //! - **Merged view**: Combined view of both layers 12 | //! - **Work directory**: Kernel internal state for overlayfs 13 | //! 14 | //! ## Use Cases 15 | //! 16 | //! 1. **Snapshots**: Run programs with same base, but isolate changes 17 | //! 2. **Recovery**: Revert to base state by deleting upper layer 18 | //! 3. **Efficiency**: Share common base across multiple sandboxes 19 | //! 4. **Auditing**: Track exactly what files changed during execution 20 | //! 21 | //! ## Running this example 22 | //! 23 | //! ```bash 24 | //! cargo run --example overlay_filesystem 25 | //! ``` 26 | 27 | use sandbox_rs::{OverlayConfig, OverlayFS}; 28 | use std::fs; 29 | use tempfile::TempDir; 30 | 31 | fn main() -> Result<(), Box> { 32 | println!("=== Sandbox-rs: Overlay Filesystem Example ===\n"); 33 | 34 | // Create temporary directories for demonstration 35 | let temp_base = TempDir::new()?; 36 | let base_dir = temp_base.path().join("base"); 37 | let upper_dir = temp_base.path().join("upper"); 38 | 39 | println!("[1] Setting up overlay filesystem layers\n"); 40 | 41 | // Create base directory with initial content 42 | fs::create_dir_all(&base_dir)?; 43 | fs::write( 44 | base_dir.join("original.txt"), 45 | "This is the original immutable file\n", 46 | )?; 47 | fs::write(base_dir.join("readme.txt"), "Base layer - read-only\n")?; 48 | 49 | println!(" Created base layer at: {}", base_dir.display()); 50 | println!(" - original.txt"); 51 | println!(" - readme.txt\n"); 52 | 53 | // Create overlay configuration 54 | println!("[2] Creating overlay configuration\n"); 55 | let overlay_config = OverlayConfig::new(&base_dir, &upper_dir); 56 | 57 | println!( 58 | " Lower layer (read-only): {}", 59 | overlay_config.lower.display() 60 | ); 61 | println!( 62 | " Upper layer (read-write): {}", 63 | overlay_config.upper.display() 64 | ); 65 | println!(" Work directory: {}", overlay_config.work.display()); 66 | println!(" Merged view: {}\n", overlay_config.merged.display()); 67 | 68 | // Initialize overlay filesystem 69 | println!("[3] Initializing overlay filesystem\n"); 70 | let mut overlay = OverlayFS::new(overlay_config); 71 | overlay.setup()?; 72 | 73 | println!(" Overlay filesystem initialized"); 74 | println!(" Mounted: {}\n", overlay.is_mounted()); 75 | 76 | // Simulate operations in the sandbox 77 | println!("[4] Simulating sandbox operations\n"); 78 | 79 | // Create new file in upper layer (simulating sandbox write) 80 | let upper_path = overlay.upper_path(); 81 | fs::create_dir_all(upper_path)?; 82 | 83 | let new_file_path = upper_path.join("sandbox-output.txt"); 84 | fs::write( 85 | &new_file_path, 86 | "This file was created in the sandbox\nModified during execution\n", 87 | )?; 88 | println!(" Created new file: {}", new_file_path.display()); 89 | 90 | let modified_file_path = upper_path.join("modified.txt"); 91 | fs::write( 92 | &modified_file_path, 93 | "This original file was modified in the sandbox\n", 94 | )?; 95 | println!( 96 | " Created modified file: {}\n", 97 | modified_file_path.display() 98 | ); 99 | 100 | // Query layer information 101 | println!("[5] Layer Information\n"); 102 | 103 | let lower_info = 104 | sandbox_rs::storage::LayerInfo::from_path("lower", overlay.lower_path(), false)?; 105 | println!(" Lower Layer (Read-only):"); 106 | println!(" Files: {}", lower_info.file_count); 107 | println!(" Total size: {} bytes", lower_info.size); 108 | println!(" Writable: {}\n", lower_info.writable); 109 | 110 | let upper_info = 111 | sandbox_rs::storage::LayerInfo::from_path("upper", overlay.upper_path(), true)?; 112 | println!(" Upper Layer (Read-write changes):"); 113 | println!(" Files: {}", upper_info.file_count); 114 | println!(" Total size: {} bytes", upper_info.size); 115 | println!(" Writable: {}\n", upper_info.writable); 116 | 117 | // Get total changes size 118 | println!("[6] Sandbox Modifications Summary\n"); 119 | let changes_size = overlay.get_changes_size()?; 120 | println!(" Total changes in upper layer: {} bytes", changes_size); 121 | println!(" Files modified/created: {}\n", upper_info.file_count); 122 | 123 | // Show how to use the merged view 124 | println!("[7] Accessing Merged View\n"); 125 | println!(" In a real mount, you would access both layers transparently:"); 126 | println!(" - {} (combined view)", overlay.merged_path().display()); 127 | println!(" - Files from lower layer are visible"); 128 | println!(" - Files from upper layer override lower layer"); 129 | println!(" - New files only appear in upper layer\n"); 130 | 131 | // Demonstrate cleanup and recovery 132 | println!("[8] Cleanup and Recovery Options\n"); 133 | println!(" Option A: Keep upper layer for audit trail"); 134 | println!( 135 | " - Preserve {} for reviewing changes", 136 | upper_path.display() 137 | ); 138 | println!(" - Original base layer remains untouched\n"); 139 | 140 | println!(" Option B: Discard changes (reset to base)"); 141 | println!(" - Delete upper layer: {}", upper_path.display()); 142 | println!(" - Next execution gets fresh base\n"); 143 | 144 | println!(" Option C: Commit changes to new base"); 145 | println!(" - Copy merged view to new base layer"); 146 | println!(" - Create fresh upper layer for next sandbox\n"); 147 | 148 | // Cleanup 149 | println!("[9] Cleaning up\n"); 150 | overlay.cleanup()?; 151 | println!(" Overlay filesystem cleaned up\n"); 152 | 153 | // Practical use case demonstration 154 | println!("=== Practical Use Case ===\n"); 155 | println!("Scenario: Run Python data processing pipeline with multiple stages\n"); 156 | 157 | println!("Stage 1: Initial execution"); 158 | println!(" Base layer: /data/pipeline-v1.0 (read-only)"); 159 | println!(" Upper layer: /sandbox-run-1/changes"); 160 | println!(" Output: preprocessing complete, 1.2GB changes\n"); 161 | 162 | println!("Stage 2: Different parameters"); 163 | println!(" Base layer: /data/pipeline-v1.0 (same - shared!)"); 164 | println!(" Upper layer: /sandbox-run-2/changes (fresh)"); 165 | println!(" Output: processing complete, 2.1GB changes\n"); 166 | 167 | println!("Benefits:"); 168 | println!(" - Disk efficient: Base shared across runs"); 169 | println!(" - Isolation: Each run has independent changes"); 170 | println!(" - Auditability: See exactly what each run produced"); 171 | println!(" - Recoverability: Can revert to base state\n"); 172 | 173 | // Volume mount for persistent storage 174 | println!("=== Combined with Volume Mounts ===\n"); 175 | println!("For persistent storage across sandbox runs:"); 176 | println!(" - Overlay FS: For temporary isolation per execution"); 177 | println!(" - Volume mounts: For data that needs to survive"); 178 | println!(" - Example setup:"); 179 | println!(" - Mount /home/user/data as /data (read-write)"); 180 | println!(" - Overlay base /usr/lib as /lib (read-only with changes)"); 181 | println!(" - Any writes to /data persist beyond sandbox"); 182 | println!(" - Any writes to /lib are isolated per run\n"); 183 | 184 | println!("=== Example completed successfully ==="); 185 | Ok(()) 186 | } 187 | -------------------------------------------------------------------------------- /lib/isolation/namespace.rs: -------------------------------------------------------------------------------- 1 | //! Namespace management for sandbox isolation 2 | 3 | use crate::errors::{Result, SandboxError}; 4 | use nix::sched::CloneFlags; 5 | use nix::unistd::Pid; 6 | 7 | /// Namespace types that can be isolated 8 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 9 | pub enum NamespaceType { 10 | /// PID namespace - isolate process IDs 11 | Pid, 12 | /// IPC namespace - isolate System V IPC 13 | Ipc, 14 | /// Network namespace - isolate network 15 | Net, 16 | /// Mount namespace - isolate mounts 17 | Mount, 18 | /// UTS namespace - isolate hostname 19 | Uts, 20 | /// User namespace - isolate UIDs/GIDs 21 | User, 22 | } 23 | 24 | /// Configuration for namespace isolation 25 | #[derive(Debug, Clone, PartialEq)] 26 | pub struct NamespaceConfig { 27 | /// PID namespace enabled 28 | pub pid: bool, 29 | /// IPC namespace enabled 30 | pub ipc: bool, 31 | /// Network namespace enabled 32 | pub net: bool, 33 | /// Mount namespace enabled 34 | pub mount: bool, 35 | /// UTS namespace enabled 36 | pub uts: bool, 37 | /// User namespace enabled 38 | pub user: bool, 39 | } 40 | 41 | impl Default for NamespaceConfig { 42 | fn default() -> Self { 43 | Self { 44 | pid: true, 45 | ipc: true, 46 | net: true, 47 | mount: true, 48 | uts: true, 49 | user: false, 50 | } 51 | } 52 | } 53 | 54 | impl NamespaceConfig { 55 | /// Create a new configuration with all namespaces enabled 56 | pub fn all() -> Self { 57 | Self { 58 | pid: true, 59 | ipc: true, 60 | net: true, 61 | mount: true, 62 | uts: true, 63 | user: true, 64 | } 65 | } 66 | 67 | /// Create a minimal configuration 68 | pub fn minimal() -> Self { 69 | Self { 70 | pid: true, 71 | ipc: true, 72 | net: true, 73 | mount: true, 74 | uts: false, 75 | user: false, 76 | } 77 | } 78 | 79 | /// Convert to clone flags 80 | pub fn to_clone_flags(&self) -> CloneFlags { 81 | let mut flags = CloneFlags::empty(); 82 | 83 | if self.pid { 84 | flags |= CloneFlags::CLONE_NEWPID; 85 | } 86 | if self.ipc { 87 | flags |= CloneFlags::CLONE_NEWIPC; 88 | } 89 | if self.net { 90 | flags |= CloneFlags::CLONE_NEWNET; 91 | } 92 | if self.mount { 93 | flags |= CloneFlags::CLONE_NEWNS; 94 | } 95 | if self.uts { 96 | flags |= CloneFlags::CLONE_NEWUTS; 97 | } 98 | if self.user { 99 | flags |= CloneFlags::CLONE_NEWUSER; 100 | } 101 | 102 | flags 103 | } 104 | 105 | /// Check if all namespaces are enabled 106 | pub fn all_enabled(&self) -> bool { 107 | self.pid && self.ipc && self.net && self.mount && self.uts && self.user 108 | } 109 | 110 | /// Count enabled namespaces 111 | pub fn enabled_count(&self) -> usize { 112 | [ 113 | self.pid, self.ipc, self.net, self.mount, self.uts, self.user, 114 | ] 115 | .iter() 116 | .filter(|&&x| x) 117 | .count() 118 | } 119 | } 120 | 121 | /// Information about a namespace 122 | #[derive(Debug, Clone)] 123 | pub struct NamespaceInfo { 124 | pub ns_type: NamespaceType, 125 | pub inode: u64, 126 | } 127 | 128 | /// Get namespace inode (for identification) 129 | pub fn get_namespace_inode(ns_type: &str) -> Result { 130 | get_namespace_inode_for_pid(ns_type, None) 131 | } 132 | 133 | /// Get namespace inode for a specific process 134 | pub fn get_namespace_inode_for_pid(ns_type: &str, pid: Option) -> Result { 135 | let pid_str = match pid { 136 | Some(p) => p.as_raw().to_string(), 137 | None => "self".to_string(), 138 | }; 139 | let path = format!("/proc/{}/ns/{}", pid_str, ns_type); 140 | let stat = std::fs::metadata(&path).map_err(|e| { 141 | SandboxError::Namespace(format!( 142 | "Failed to get namespace info for pid={} ns={}: {}", 143 | pid_str, ns_type, e 144 | )) 145 | })?; 146 | 147 | // Get inode number 148 | #[cfg(unix)] 149 | { 150 | use std::os::unix::fs::MetadataExt; 151 | Ok(stat.ino()) 152 | } 153 | 154 | #[cfg(not(unix))] 155 | { 156 | Err(SandboxError::Namespace( 157 | "Namespace info not available on this platform".to_string(), 158 | )) 159 | } 160 | } 161 | 162 | /// Check if two processes share a namespace 163 | pub fn shares_namespace(ns_type: &str, pid1: Option, pid2: Option) -> Result { 164 | let inode1 = get_namespace_inode_for_pid(ns_type, pid1)?; 165 | let inode2 = get_namespace_inode_for_pid(ns_type, pid2)?; 166 | 167 | Ok(inode1 == inode2) 168 | } 169 | 170 | #[cfg(test)] 171 | mod tests { 172 | use super::*; 173 | 174 | #[test] 175 | fn test_namespace_config_default() { 176 | let config = NamespaceConfig::default(); 177 | assert!(config.pid); 178 | assert!(config.ipc); 179 | assert!(config.net); 180 | assert!(config.mount); 181 | assert!(config.uts); 182 | assert!(!config.user); 183 | } 184 | 185 | #[test] 186 | fn test_namespace_config_all() { 187 | let config = NamespaceConfig::all(); 188 | assert!(config.all_enabled()); 189 | } 190 | 191 | #[test] 192 | fn test_namespace_config_minimal() { 193 | let config = NamespaceConfig::minimal(); 194 | assert!(config.pid); 195 | assert!(!config.uts); 196 | assert!(!config.user); 197 | } 198 | 199 | #[test] 200 | fn test_enabled_count() { 201 | let config = NamespaceConfig::default(); 202 | assert_eq!(config.enabled_count(), 5); // pid, ipc, net, mount, uts 203 | 204 | let config = NamespaceConfig::all(); 205 | assert_eq!(config.enabled_count(), 6); 206 | 207 | let config = NamespaceConfig::minimal(); 208 | assert_eq!(config.enabled_count(), 4); // pid, ipc, net, mount 209 | } 210 | 211 | #[test] 212 | fn test_clone_flags_conversion() { 213 | let config = NamespaceConfig::default(); 214 | let flags = config.to_clone_flags(); 215 | 216 | // Should not be empty 217 | assert!(!flags.is_empty()); 218 | 219 | // Should have NEWPID, NEWIPC, NEWNET, NEWNS 220 | assert!(flags.contains(CloneFlags::CLONE_NEWPID)); 221 | } 222 | 223 | #[test] 224 | fn test_get_namespace_inode() { 225 | // Should be able to get inode for current process 226 | let result = get_namespace_inode("pid"); 227 | match result { 228 | Ok(inode) => { 229 | assert!(inode > 0); 230 | } 231 | Err(e) => { 232 | // May fail if /proc is not available in test environment 233 | eprintln!("Warning: namespace inode check failed: {}", e); 234 | } 235 | } 236 | } 237 | 238 | #[test] 239 | fn test_shares_namespace_with_self() { 240 | // Current process should share namespace with itself 241 | let result = shares_namespace("pid", None, None); 242 | match result { 243 | Ok(shares) => assert!(shares, "Process should share namespace with itself"), 244 | Err(e) => eprintln!("Warning: namespace sharing check failed: {}", e), 245 | } 246 | } 247 | 248 | #[test] 249 | fn test_namespace_inode_for_self() { 250 | // Should be able to get inode for current process 251 | let result = get_namespace_inode_for_pid("pid", None); 252 | match result { 253 | Ok(inode) => { 254 | assert!(inode > 0, "Namespace inode should be positive"); 255 | } 256 | Err(e) => { 257 | eprintln!("Warning: namespace inode check failed: {}", e); 258 | } 259 | } 260 | } 261 | 262 | #[test] 263 | fn test_namespace_inode_consistency() { 264 | // Getting inode twice should return same value 265 | let inode1 = get_namespace_inode("pid"); 266 | let inode2 = get_namespace_inode("pid"); 267 | 268 | match (inode1, inode2) { 269 | (Ok(i1), Ok(i2)) => { 270 | assert_eq!(i1, i2, "Namespace inode should be consistent"); 271 | } 272 | _ => { 273 | eprintln!("Warning: namespace inode check failed"); 274 | } 275 | } 276 | } 277 | 278 | #[test] 279 | fn test_namespace_type_equality() { 280 | assert_eq!(NamespaceType::Pid, NamespaceType::Pid); 281 | assert_ne!(NamespaceType::Pid, NamespaceType::Net); 282 | } 283 | } 284 | -------------------------------------------------------------------------------- /lib/monitoring/ebpf.rs: -------------------------------------------------------------------------------- 1 | //! eBPF-based syscall monitoring 2 | //! 3 | //! Provides event-driven syscall tracing using eBPF programs. 4 | //! Monitors syscall frequency, duration, and detects slow operations (>10ms). 5 | //! 6 | //! Note: Full eBPF functionality requires kernel 5.0+ and BPF_RING_BUFFER support. 7 | 8 | use crate::errors::Result; 9 | use nix::unistd::Pid; 10 | use std::collections::HashMap; 11 | 12 | /// Syscall event information 13 | #[derive(Debug, Clone)] 14 | pub struct SyscallEvent { 15 | /// Syscall number 16 | pub syscall_id: u64, 17 | /// Syscall name (e.g., "read", "write") 18 | pub syscall_name: String, 19 | /// Duration in microseconds 20 | pub duration_us: u64, 21 | /// Timestamp when syscall occurred 22 | pub timestamp: u64, 23 | /// Whether this was a slow syscall (>10ms) 24 | pub is_slow: bool, 25 | } 26 | 27 | impl SyscallEvent { 28 | /// Check if this syscall is considered slow (>10ms) 29 | pub fn is_slow_syscall(&self) -> bool { 30 | self.duration_us > 10_000 // 10ms in microseconds 31 | } 32 | 33 | /// Get duration in milliseconds 34 | pub fn duration_ms(&self) -> f64 { 35 | self.duration_us as f64 / 1000.0 36 | } 37 | } 38 | 39 | /// Aggregated syscall statistics 40 | #[derive(Debug, Clone, Default)] 41 | pub struct SyscallStats { 42 | /// Total number of syscalls 43 | pub total_syscalls: u64, 44 | /// Number of slow syscalls (>10ms) 45 | pub slow_syscalls: u64, 46 | /// Total time spent in syscalls (microseconds) 47 | pub total_time_us: u64, 48 | /// Syscalls by name with their count and total duration 49 | pub syscalls_by_name: HashMap, // (count, total_time_us) 50 | /// Top N slowest syscalls 51 | pub slowest_syscalls: Vec, 52 | } 53 | 54 | /// eBPF-based syscall monitor 55 | pub struct EBpfMonitor { 56 | pid: Pid, 57 | events: Vec, 58 | stats: SyscallStats, 59 | } 60 | 61 | impl EBpfMonitor { 62 | /// Create new eBPF monitor for process 63 | pub fn new(pid: Pid) -> Self { 64 | EBpfMonitor { 65 | pid, 66 | events: Vec::new(), 67 | stats: SyscallStats::default(), 68 | } 69 | } 70 | 71 | /// Collect syscall statistics 72 | pub fn collect_stats(&mut self) -> Result { 73 | // This is a placeholder implementation 74 | self._compute_statistics(); 75 | Ok(self.stats.clone()) 76 | } 77 | 78 | /// Add raw syscall event (for testing/manual injection) 79 | pub fn add_event(&mut self, event: SyscallEvent) { 80 | self.events.push(event); 81 | self._compute_statistics(); 82 | } 83 | 84 | /// Clear collected events 85 | pub fn clear(&mut self) { 86 | self.events.clear(); 87 | self.stats = SyscallStats::default(); 88 | } 89 | 90 | /// Get process ID being monitored 91 | pub fn pid(&self) -> Pid { 92 | self.pid 93 | } 94 | 95 | /// Get total slow syscalls 96 | pub fn slow_syscall_count(&self) -> u64 { 97 | self.stats.slow_syscalls 98 | } 99 | 100 | /// Get top N slowest syscalls 101 | pub fn slowest_syscalls(&self, n: usize) -> Vec { 102 | self.stats 103 | .slowest_syscalls 104 | .iter() 105 | .take(n) 106 | .cloned() 107 | .collect() 108 | } 109 | 110 | /// Recompute statistics from events 111 | fn _compute_statistics(&mut self) { 112 | let mut stats = SyscallStats::default(); 113 | let mut by_name: HashMap = HashMap::new(); 114 | let mut slowest: Vec = Vec::new(); 115 | 116 | for event in &self.events { 117 | stats.total_syscalls += 1; 118 | stats.total_time_us += event.duration_us; 119 | 120 | if event.is_slow { 121 | stats.slow_syscalls += 1; 122 | } 123 | 124 | // Aggregate by syscall name 125 | let entry = by_name.entry(event.syscall_name.clone()).or_insert((0, 0)); 126 | entry.0 += 1; 127 | entry.1 += event.duration_us; 128 | 129 | // Track slowest syscalls 130 | slowest.push(event.clone()); 131 | } 132 | 133 | // Sort slowest and keep top 10 134 | slowest.sort_by(|a, b| b.duration_us.cmp(&a.duration_us)); 135 | slowest.truncate(10); 136 | 137 | stats.syscalls_by_name = by_name; 138 | stats.slowest_syscalls = slowest; 139 | 140 | self.stats = stats; 141 | } 142 | } 143 | 144 | #[cfg(test)] 145 | mod tests { 146 | use super::*; 147 | 148 | #[test] 149 | fn test_syscall_event_is_slow() { 150 | let event_slow = SyscallEvent { 151 | syscall_id: 1, 152 | syscall_name: "read".to_string(), 153 | duration_us: 15_000, // 15ms 154 | timestamp: 0, 155 | is_slow: true, 156 | }; 157 | assert!(event_slow.is_slow_syscall()); 158 | 159 | let event_fast = SyscallEvent { 160 | syscall_id: 1, 161 | syscall_name: "read".to_string(), 162 | duration_us: 5_000, // 5ms 163 | timestamp: 0, 164 | is_slow: false, 165 | }; 166 | assert!(!event_fast.is_slow_syscall()); 167 | } 168 | 169 | #[test] 170 | fn test_syscall_event_duration_ms() { 171 | let event = SyscallEvent { 172 | syscall_id: 1, 173 | syscall_name: "write".to_string(), 174 | duration_us: 10_000, // 10ms 175 | timestamp: 0, 176 | is_slow: false, 177 | }; 178 | assert_eq!(event.duration_ms(), 10.0); 179 | } 180 | 181 | #[test] 182 | fn test_ebpf_monitor_new() { 183 | let pid = Pid::from_raw(std::process::id() as i32); 184 | let monitor = EBpfMonitor::new(pid); 185 | assert_eq!(monitor.pid(), pid); 186 | assert_eq!(monitor.slow_syscall_count(), 0); 187 | } 188 | 189 | #[test] 190 | fn test_ebpf_monitor_add_event() { 191 | let pid = Pid::from_raw(std::process::id() as i32); 192 | let mut monitor = EBpfMonitor::new(pid); 193 | 194 | let event = SyscallEvent { 195 | syscall_id: 1, 196 | syscall_name: "read".to_string(), 197 | duration_us: 5_000, 198 | timestamp: 0, 199 | is_slow: false, 200 | }; 201 | 202 | monitor.add_event(event); 203 | assert_eq!(monitor.stats.total_syscalls, 1); 204 | assert_eq!(monitor.stats.slow_syscalls, 0); 205 | } 206 | 207 | #[test] 208 | fn test_ebpf_monitor_slow_syscalls() { 209 | let pid = Pid::from_raw(std::process::id() as i32); 210 | let mut monitor = EBpfMonitor::new(pid); 211 | 212 | // Add fast syscall 213 | monitor.add_event(SyscallEvent { 214 | syscall_id: 1, 215 | syscall_name: "read".to_string(), 216 | duration_us: 5_000, 217 | timestamp: 0, 218 | is_slow: false, 219 | }); 220 | 221 | // Add slow syscall 222 | monitor.add_event(SyscallEvent { 223 | syscall_id: 2, 224 | syscall_name: "write".to_string(), 225 | duration_us: 15_000, 226 | timestamp: 1, 227 | is_slow: true, 228 | }); 229 | 230 | assert_eq!(monitor.stats.total_syscalls, 2); 231 | assert_eq!(monitor.stats.slow_syscalls, 1); 232 | } 233 | 234 | #[test] 235 | fn test_ebpf_monitor_slowest_syscalls() { 236 | let pid = Pid::from_raw(std::process::id() as i32); 237 | let mut monitor = EBpfMonitor::new(pid); 238 | 239 | for i in 0..5 { 240 | monitor.add_event(SyscallEvent { 241 | syscall_id: i, 242 | syscall_name: format!("syscall_{}", i), 243 | duration_us: (i + 1) * 1000, 244 | timestamp: i, 245 | is_slow: (i + 1) * 1000 > 10_000, 246 | }); 247 | } 248 | 249 | let slowest = monitor.slowest_syscalls(3); 250 | assert_eq!(slowest.len(), 3); 251 | } 252 | 253 | #[test] 254 | fn test_ebpf_monitor_clear() { 255 | let pid = Pid::from_raw(std::process::id() as i32); 256 | let mut monitor = EBpfMonitor::new(pid); 257 | 258 | monitor.add_event(SyscallEvent { 259 | syscall_id: 1, 260 | syscall_name: "read".to_string(), 261 | duration_us: 5_000, 262 | timestamp: 0, 263 | is_slow: false, 264 | }); 265 | 266 | assert_eq!(monitor.stats.total_syscalls, 1); 267 | 268 | monitor.clear(); 269 | assert_eq!(monitor.stats.total_syscalls, 0); 270 | assert_eq!(monitor.stats.slow_syscalls, 0); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /TROUBLESHOOTING.md: -------------------------------------------------------------------------------- 1 | # Troubleshooting Guide - sandbox-rs 2 | 3 | This guide helps you diagnose and resolve common issues when using sandbox-rs. 4 | 5 | ## Permission Denied Errors 6 | 7 | ### "This operation requires root privileges" 8 | 9 | **Cause:** Full sandbox isolation (namespaces, cgroups, seccomp) requires root privileges on Linux. 10 | 11 | **Solution:** 12 | 13 | ```bash 14 | # Option 1: Run with sudo 15 | sudo cargo run --example basic 16 | 17 | # Option 2: Run CLI with sudo 18 | sudo ./target/debug/sandbox-ctl run --id test /bin/echo "hello" 19 | 20 | # Option 3: Add user to sudoers (use with caution) 21 | # Edit with: sudo visudo 22 | # Add line: username ALL=(ALL) NOPASSWD: /path/to/sandbox-ctl 23 | ``` 24 | 25 | **Why:** Linux namespaces and cgroups are privileged kernel features. They require root to prevent unprivileged users from escalating privileges or affecting the entire system. 26 | 27 | --- 28 | 29 | ## Cgroup Not Found 30 | 31 | ### "cgroup directory {x} does not exist" 32 | 33 | **Cause:** Cgroup v2 is not mounted on your system, or the cgroup root path is incorrect. 34 | 35 | **Solution:** 36 | 37 | Check if cgroup v2 is available: 38 | ```bash 39 | mount | grep cgroup 40 | # Should show: cgroup2 on /sys/fs/cgroup type cgroup2 41 | ``` 42 | 43 | If not mounted, try: 44 | ```bash 45 | sudo mount -t cgroup2 none /sys/fs/cgroup 46 | ``` 47 | 48 | **Verify cgroup v2 support:** 49 | ```bash 50 | ls /sys/fs/cgroup/ 51 | # Should show files like: cgroup.max.depth, cgroup.max.pids, cpu.max, memory.max 52 | ``` 53 | 54 | If your system uses cgroup v1 only: 55 | - This is older kernel (pre-5.10) 56 | - Upgrade kernel or use container runtime instead 57 | - sandbox-rs targets cgroup v2 58 | 59 | --- 60 | 61 | ## Memory Limit Not Enforced 62 | 63 | ### Process exceeds memory limit without being killed 64 | 65 | **Cause (non-root):** Without root privileges, memory limits cannot be enforced at the kernel level. 66 | 67 | **Verification:** 68 | 69 | ```bash 70 | # Check if running as root 71 | sudo cargo run --example basic 72 | 73 | # Run with memory limit 74 | sudo cargo run --example cgroup_limits 75 | ``` 76 | 77 | **Expected behavior:** 78 | - With root: Process killed when exceeding limit 79 | - Without root: Limit is set but not enforced 80 | 81 | --- 82 | 83 | ## Namespace Isolation Not Working 84 | 85 | ### Processes see same PID or network as host 86 | 87 | **Cause:** Running without root means no actual namespace isolation. 88 | 89 | **Solution:** 90 | 91 | ```bash 92 | # Verify you're running as root 93 | whoami # Should output: root 94 | 95 | # Run with full isolation 96 | sudo cargo build 97 | sudo ./target/debug/sandbox-ctl run --id test /bin/echo "in sandbox" 98 | ``` 99 | 100 | **Check namespace support:** 101 | ```bash 102 | # Verify Linux has namespace support 103 | ls /proc/self/ns/ 104 | # Should show: cgroup ipc mnt net pid uts user 105 | 106 | # Verify seccomp is available 107 | grep SECCOMP /boot/config-$(uname -r) || echo "Check kernel config" 108 | ``` 109 | 110 | --- 111 | 112 | ## Seccomp Setup Issues 113 | 114 | ### "Failed to load seccomp filter" 115 | 116 | **Cause:** Seccomp BPF loading requires specific kernel capabilities and proper setup. 117 | 118 | **Solution:** 119 | 120 | Check seccomp support: 121 | ```bash 122 | sudo cat /proc/sys/kernel/unprivileged_userns_clone 123 | # 0 = restricted, 1 = allowed 124 | 125 | # Check seccomp is compiled in 126 | grep CONFIG_SECCOMP /boot/config-$(uname -r) 127 | # Should show: CONFIG_SECCOMP=y 128 | ``` 129 | 130 | If seccomp doesn't work: 131 | 1. Kernel might not support BPF seccomp 132 | 2. SELinux or AppArmor might block it 133 | 3. Use permissive seccomp profile: 134 | ```bash 135 | ./sandbox-ctl run --id test --seccomp unrestricted /bin/echo "test" 136 | ``` 137 | 138 | --- 139 | 140 | ## Process Execution Fails 141 | 142 | ### "execve failed: No such file or directory" 143 | 144 | **Cause:** Program path doesn't exist in the sandbox environment. 145 | 146 | **Solution:** 147 | 148 | 1. Use absolute paths: 149 | ```bash 150 | # Wrong: 151 | sandbox-ctl run --id test echo "hello" 152 | 153 | # Correct: 154 | sandbox-ctl run --id test /bin/echo "hello" 155 | ``` 156 | 157 | 2. Verify program exists: 158 | ```bash 159 | which echo 160 | # Output: /usr/bin/echo or /bin/echo 161 | ``` 162 | 163 | 3. If using chroot or overlay FS, ensure program exists in sandbox root: 164 | ```bash 165 | ls /sandbox/root/bin/echo # Should exist 166 | ``` 167 | 168 | --- 169 | 170 | ## Timeout Not Enforced 171 | 172 | ### Process continues running past timeout 173 | 174 | **Cause:** Timeout enforcement requires root privileges and proper process monitoring. 175 | 176 | **Solution:** 177 | 178 | 1. Ensure running as root 179 | 2. Verify timeout is reasonable: 180 | ```bash 181 | # Wrong: timeout longer than actual operation 182 | sudo sandbox-ctl run --id test --timeout 60 /bin/echo "test" 183 | 184 | # Correct: timeout shorter than operation 185 | sudo sandbox-ctl run --id test --timeout 1 /bin/sleep 10 186 | # Should be killed after 1 second 187 | ``` 188 | 189 | --- 190 | 191 | ## Out of Memory (OOM) Behavior 192 | 193 | ### Process killed with no output 194 | 195 | **Cause:** Memory limit exceeded - kernel's OOM killer activated. 196 | 197 | **Solution:** 198 | 199 | 1. Increase memory limit: 200 | ```bash 201 | # Before: 202 | --memory 64M # Too tight 203 | 204 | # After: 205 | --memory 256M # More reasonable 206 | ``` 207 | 208 | 2. Profile memory usage: 209 | ```bash 210 | time -v ./my-program 211 | # Shows peak memory usage 212 | ``` 213 | 214 | 3. Check if it's a true leak or just normal memory use: 215 | ```bash 216 | # Monitor memory during execution 217 | watch -n 0.1 'ps aux | grep my-program' 218 | ``` 219 | 220 | --- 221 | 222 | ## CPU Limit Seems Ineffective 223 | 224 | ### Process runs at full speed despite CPU limit 225 | 226 | **Cause:** CPU limits throttle the scheduler, but execution still completes. Effect is visible with sustained load. 227 | 228 | **Solution:** 229 | 230 | CPU limits work differently than you might expect: 231 | - **CPU limit 50%:** Process gets interrupted more often, takes 2x longer on single workload 232 | - **CPU limit 100%:** Can use all resources of one core (system dependent) 233 | - Effect only visible with sustained compute 234 | 235 | Test proper limits: 236 | ```bash 237 | # Monitor CPU usage 238 | watch -n 1 'ps aux | grep process-name' 239 | 240 | # Should see consistent CPU% around the limit 241 | ``` 242 | 243 | --- 244 | 245 | ## Tests Failing with "PermissionDenied" 246 | 247 | ### Test suite requires root 248 | 249 | **Solution:** 250 | 251 | Run tests with root privileges: 252 | ```bash 253 | # Run all tests with root 254 | sudo cargo test 255 | 256 | # Run specific test file 257 | sudo cargo test --test integration_tests 258 | 259 | # Run with output 260 | sudo cargo test -- --nocapture 261 | ``` 262 | 263 | **Or configure test environment:** 264 | ```bash 265 | # Set test to skip if not root 266 | export SANDBOX_SKIP_ROOT_TESTS=1 267 | cargo test 268 | ``` 269 | 270 | --- 271 | 272 | ## Building Fails with Missing Dependencies 273 | 274 | ### "cannot find -lnix" or similar 275 | 276 | **Cause:** Native library dependencies not installed. 277 | 278 | **Solution:** 279 | 280 | Install development libraries: 281 | ```bash 282 | # Ubuntu/Debian: 283 | sudo apt-get install build-essential libssl-dev pkg-config 284 | 285 | # Fedora/RHEL: 286 | sudo dnf install gcc openssl-devel pkg-config 287 | 288 | # Arch: 289 | sudo pacman -S base-devel openssl 290 | ``` 291 | 292 | --- 293 | 294 | ## Nix Development Environment 295 | 296 | ### "nix: command not found" 297 | 298 | **Solution:** 299 | 300 | Install Nix and use flake.nix: 301 | ```bash 302 | # Install Nix (on non-NixOS systems) 303 | curl -L https://nixos.org/nix/install | sh 304 | 305 | # Enter development environment 306 | nix flake update 307 | nix develop 308 | 309 | # Now building should work 310 | cargo build 311 | ``` 312 | 313 | --- 314 | 315 | ## Common Configuration Issues 316 | 317 | ### Invalid Memory Format 318 | 319 | ```bash 320 | # Wrong formats: 321 | --memory 256 # No unit 322 | --memory "256MB" # Wrong unit (use M not MB) 323 | 324 | # Correct formats: 325 | --memory 256M # 256 megabytes 326 | --memory 1G # 1 gigabyte 327 | --memory 1024K # 1024 kilobytes 328 | ``` 329 | 330 | ### Invalid CPU Limit 331 | 332 | ```bash 333 | # CPU limits should be 1-400+ percent: 334 | --cpu 0 # Invalid (ignored) 335 | --cpu 50 # OK: 50% of one core 336 | --cpu 100 # OK: 100% of one core (full core) 337 | --cpu 200 # OK: 200% (can use 2 cores) 338 | ``` 339 | 340 | --- 341 | 342 | ## Performance Issues 343 | 344 | ### Sandbox creation is slow 345 | 346 | **Cause:** Namespace cloning and cgroup setup have overhead. 347 | 348 | **Solutions:** 349 | 1. Reuse sandbox instances instead of creating new ones 350 | 2. Use minimal namespace configuration 351 | 3. Disable unused features (e.g., `--seccomp unrestricted`) 352 | 353 | ### Memory overhead per sandbox 354 | 355 | Expected overhead: 356 | - Base process: 1-2 MB 357 | - With namespaces: +2-5 MB 358 | - With cgroups: +1-2 MB 359 | - Total typical: 5-10 MB per inactive sandbox 360 | 361 | --- 362 | 363 | ## Debug Mode 364 | 365 | ### Enable detailed logging 366 | 367 | ```bash 368 | # Set log level to debug 369 | RUST_LOG=debug cargo run --example basic 370 | 371 | # With components 372 | RUST_LOG=sandbox_rs=debug cargo run --example basic 373 | 374 | # Very verbose 375 | RUST_LOG=trace cargo run --example basic 376 | ``` 377 | 378 | ### Run with strace (for system calls) 379 | 380 | ```bash 381 | # See all syscalls made by sandbox process 382 | sudo strace -f ./target/debug/sandbox-ctl run --id test /bin/echo "hello" 383 | 384 | # Filter to specific syscalls 385 | sudo strace -f -e trace=open,read,write,clone ./target/debug/sandbox-ctl run --id test /bin/echo "hello" 386 | ``` 387 | 388 | --- 389 | 390 | ## Getting Help 391 | 392 | If you encounter an issue not listed here: 393 | 394 | 1. **Check the logs:** 395 | ```bash 396 | RUST_LOG=debug cargo run --example basic 2>&1 | head -100 397 | ``` 398 | 399 | 2. **Check system requirements:** 400 | ```bash 401 | ./target/debug/sandbox-ctl check 402 | ``` 403 | 404 | 3. **Verify your kernel:** 405 | ```bash 406 | uname -a 407 | # Should be Linux 5.10+ for full support 408 | ``` 409 | 410 | 4. **Test with minimal setup:** 411 | ```bash 412 | sudo cargo run --example basic 413 | ``` 414 | 415 | 5. **Report issue with:** 416 | - Full error message 417 | - Kernel version (`uname -r`) 418 | - Whether running with root 419 | - Reproducible test case 420 | -------------------------------------------------------------------------------- /lib/storage/volumes.rs: -------------------------------------------------------------------------------- 1 | //! Volume management for persistent storage in sandbox 2 | 3 | use crate::errors::{Result, SandboxError}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::fs; 6 | use std::path::{Path, PathBuf}; 7 | 8 | /// Volume mount type 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] 10 | pub enum VolumeType { 11 | /// Bind mount (host directory) 12 | Bind, 13 | /// Tmpfs mount 14 | Tmpfs, 15 | /// Named volume 16 | Named, 17 | /// Read-only mount 18 | ReadOnly, 19 | } 20 | 21 | impl std::fmt::Display for VolumeType { 22 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 23 | match self { 24 | VolumeType::Bind => write!(f, "bind"), 25 | VolumeType::Tmpfs => write!(f, "tmpfs"), 26 | VolumeType::Named => write!(f, "named"), 27 | VolumeType::ReadOnly => write!(f, "readonly"), 28 | } 29 | } 30 | } 31 | 32 | /// Volume mount configuration 33 | #[derive(Debug, Clone, Serialize, Deserialize)] 34 | pub struct VolumeMount { 35 | /// Volume type 36 | pub volume_type: VolumeType, 37 | /// Source (host path or volume name) 38 | pub source: String, 39 | /// Destination (container path) 40 | pub destination: PathBuf, 41 | /// Read-only flag 42 | pub read_only: bool, 43 | /// Optional size limit (for tmpfs) 44 | pub size_limit: Option, 45 | } 46 | 47 | impl VolumeMount { 48 | /// Create bind mount 49 | pub fn bind(source: impl AsRef, destination: impl AsRef) -> Self { 50 | Self { 51 | volume_type: VolumeType::Bind, 52 | source: source.as_ref().display().to_string(), 53 | destination: destination.as_ref().to_path_buf(), 54 | read_only: false, 55 | size_limit: None, 56 | } 57 | } 58 | 59 | /// Create read-only bind mount 60 | pub fn bind_readonly(source: impl AsRef, destination: impl AsRef) -> Self { 61 | Self { 62 | volume_type: VolumeType::ReadOnly, 63 | source: source.as_ref().display().to_string(), 64 | destination: destination.as_ref().to_path_buf(), 65 | read_only: true, 66 | size_limit: None, 67 | } 68 | } 69 | 70 | /// Create tmpfs mount 71 | pub fn tmpfs(destination: impl AsRef, size_limit: Option) -> Self { 72 | Self { 73 | volume_type: VolumeType::Tmpfs, 74 | source: "tmpfs".to_string(), 75 | destination: destination.as_ref().to_path_buf(), 76 | read_only: false, 77 | size_limit, 78 | } 79 | } 80 | 81 | /// Create named volume 82 | pub fn named(name: &str, destination: impl AsRef) -> Self { 83 | Self { 84 | volume_type: VolumeType::Named, 85 | source: name.to_string(), 86 | destination: destination.as_ref().to_path_buf(), 87 | read_only: false, 88 | size_limit: None, 89 | } 90 | } 91 | 92 | /// Validate volume mount 93 | pub fn validate(&self) -> Result<()> { 94 | if self.source.is_empty() { 95 | return Err(SandboxError::InvalidConfig( 96 | "Volume source cannot be empty".to_string(), 97 | )); 98 | } 99 | 100 | if self.destination.as_os_str().is_empty() { 101 | return Err(SandboxError::InvalidConfig( 102 | "Volume destination cannot be empty".to_string(), 103 | )); 104 | } 105 | 106 | if self.volume_type == VolumeType::Bind || self.volume_type == VolumeType::ReadOnly { 107 | let source_path = Path::new(&self.source); 108 | if !source_path.exists() { 109 | return Err(SandboxError::InvalidConfig(format!( 110 | "Bind mount source does not exist: {}", 111 | self.source 112 | ))); 113 | } 114 | } 115 | 116 | Ok(()) 117 | } 118 | 119 | /// Get mount options for mount command 120 | pub fn get_mount_options(&self) -> String { 121 | match self.volume_type { 122 | VolumeType::Bind | VolumeType::ReadOnly => { 123 | if self.read_only { 124 | "bind,ro".to_string() 125 | } else { 126 | "bind".to_string() 127 | } 128 | } 129 | VolumeType::Tmpfs => { 130 | if let Some(size) = self.size_limit { 131 | format!("size={}", size) 132 | } else { 133 | String::new() 134 | } 135 | } 136 | VolumeType::Named => "named".to_string(), 137 | } 138 | } 139 | } 140 | 141 | /// Volume manager 142 | pub struct VolumeManager { 143 | /// Mount points 144 | mounts: Vec, 145 | /// Volume storage directory 146 | volume_root: PathBuf, 147 | } 148 | 149 | impl VolumeManager { 150 | /// Create new volume manager 151 | pub fn new(volume_root: impl AsRef) -> Self { 152 | Self { 153 | mounts: Vec::new(), 154 | volume_root: volume_root.as_ref().to_path_buf(), 155 | } 156 | } 157 | 158 | /// Add volume mount 159 | pub fn add_mount(&mut self, mount: VolumeMount) -> Result<()> { 160 | mount.validate()?; 161 | self.mounts.push(mount); 162 | Ok(()) 163 | } 164 | 165 | /// Get all mounts 166 | pub fn mounts(&self) -> &[VolumeMount] { 167 | &self.mounts 168 | } 169 | 170 | /// Create named volume 171 | pub fn create_volume(&self, name: &str) -> Result { 172 | let vol_path = self.volume_root.join(name); 173 | fs::create_dir_all(&vol_path).map_err(|e| { 174 | SandboxError::Syscall(format!("Failed to create volume {}: {}", name, e)) 175 | })?; 176 | Ok(vol_path) 177 | } 178 | 179 | /// Delete named volume 180 | pub fn delete_volume(&self, name: &str) -> Result<()> { 181 | let vol_path = self.volume_root.join(name); 182 | if vol_path.exists() { 183 | fs::remove_dir_all(&vol_path).map_err(|e| { 184 | SandboxError::Syscall(format!("Failed to delete volume {}: {}", name, e)) 185 | })?; 186 | } 187 | Ok(()) 188 | } 189 | 190 | /// List named volumes 191 | pub fn list_volumes(&self) -> Result> { 192 | let mut volumes = Vec::new(); 193 | 194 | if self.volume_root.exists() { 195 | for entry in fs::read_dir(&self.volume_root) 196 | .map_err(|e| SandboxError::Syscall(format!("Cannot list volumes: {}", e)))? 197 | { 198 | let entry = entry.map_err(|e| SandboxError::Syscall(e.to_string()))?; 199 | 200 | if let Ok(name) = entry.file_name().into_string() { 201 | volumes.push(name); 202 | } 203 | } 204 | } 205 | 206 | Ok(volumes) 207 | } 208 | 209 | /// Get volume size (recursive) 210 | pub fn get_volume_size(&self, name: &str) -> Result { 211 | use walkdir::WalkDir; 212 | 213 | let vol_path = self.volume_root.join(name); 214 | 215 | if !vol_path.exists() { 216 | return Err(SandboxError::Syscall(format!( 217 | "Volume does not exist: {}", 218 | name 219 | ))); 220 | } 221 | 222 | let mut total = 0u64; 223 | 224 | for entry in WalkDir::new(&vol_path).into_iter().filter_map(|e| e.ok()) { 225 | if entry.file_type().is_file() { 226 | total += entry 227 | .metadata() 228 | .map_err(|e| SandboxError::Syscall(e.to_string()))? 229 | .len(); 230 | } 231 | } 232 | 233 | Ok(total) 234 | } 235 | 236 | /// Clear all mounts 237 | pub fn clear_mounts(&mut self) { 238 | self.mounts.clear(); 239 | } 240 | } 241 | 242 | #[cfg(test)] 243 | mod tests { 244 | use super::*; 245 | 246 | #[test] 247 | fn test_volume_type_display() { 248 | assert_eq!(VolumeType::Bind.to_string(), "bind"); 249 | assert_eq!(VolumeType::Tmpfs.to_string(), "tmpfs"); 250 | } 251 | 252 | #[test] 253 | fn test_volume_mount_bind() { 254 | let mount = VolumeMount::bind("/tmp", "/mnt"); 255 | assert_eq!(mount.volume_type, VolumeType::Bind); 256 | assert_eq!(mount.source, "/tmp"); 257 | assert_eq!(mount.destination, PathBuf::from("/mnt")); 258 | assert!(!mount.read_only); 259 | } 260 | 261 | #[test] 262 | fn test_volume_mount_readonly() { 263 | let mount = VolumeMount::bind_readonly("/tmp", "/mnt"); 264 | assert_eq!(mount.volume_type, VolumeType::ReadOnly); 265 | assert!(mount.read_only); 266 | } 267 | 268 | #[test] 269 | fn test_volume_mount_tmpfs() { 270 | let mount = VolumeMount::tmpfs("/tmp", Some(1024)); 271 | assert_eq!(mount.volume_type, VolumeType::Tmpfs); 272 | assert_eq!(mount.size_limit, Some(1024)); 273 | } 274 | 275 | #[test] 276 | fn test_volume_mount_named() { 277 | let mount = VolumeMount::named("mydata", "/data"); 278 | assert_eq!(mount.volume_type, VolumeType::Named); 279 | assert_eq!(mount.source, "mydata"); 280 | } 281 | 282 | #[test] 283 | fn test_volume_mount_options() { 284 | let bind_mount = VolumeMount::bind("/tmp", "/mnt"); 285 | assert_eq!(bind_mount.get_mount_options(), "bind"); 286 | 287 | let ro_mount = VolumeMount::bind_readonly("/tmp", "/mnt"); 288 | assert_eq!(ro_mount.get_mount_options(), "bind,ro"); 289 | 290 | let tmpfs_mount = VolumeMount::tmpfs("/tmp", Some(1024)); 291 | assert!(tmpfs_mount.get_mount_options().contains("size=")); 292 | } 293 | 294 | #[test] 295 | fn test_volume_manager_creation() { 296 | let manager = VolumeManager::new("/tmp"); 297 | assert!(manager.mounts().is_empty()); 298 | } 299 | 300 | #[test] 301 | fn test_volume_manager_add_mount() { 302 | let mut manager = VolumeManager::new("/tmp"); 303 | let mount = VolumeMount::tmpfs("/tmp", None); 304 | 305 | assert!(manager.add_mount(mount).is_ok()); 306 | assert_eq!(manager.mounts().len(), 1); 307 | } 308 | 309 | #[test] 310 | fn test_volume_manager_clear_mounts() { 311 | let mut manager = VolumeManager::new("/tmp"); 312 | let mount = VolumeMount::tmpfs("/tmp", None); 313 | 314 | manager.add_mount(mount).ok(); 315 | assert!(!manager.mounts().is_empty()); 316 | 317 | manager.clear_mounts(); 318 | assert!(manager.mounts().is_empty()); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /lib/storage/filesystem.rs: -------------------------------------------------------------------------------- 1 | //! Overlay filesystem support for persistent sandbox storage 2 | 3 | use crate::errors::{Result, SandboxError}; 4 | use std::fs; 5 | use std::path::{Path, PathBuf}; 6 | 7 | /// Overlay filesystem configuration 8 | #[derive(Debug, Clone)] 9 | pub struct OverlayConfig { 10 | /// Lower layer (read-only base) 11 | pub lower: PathBuf, 12 | /// Upper layer (read-write changes) 13 | pub upper: PathBuf, 14 | /// Work directory (required by overlayfs) 15 | pub work: PathBuf, 16 | /// Merged mount point 17 | pub merged: PathBuf, 18 | } 19 | 20 | impl OverlayConfig { 21 | /// Create new overlay configuration 22 | pub fn new(lower: impl AsRef, upper: impl AsRef) -> Self { 23 | let lower_path = lower.as_ref().to_path_buf(); 24 | let upper_path = upper.as_ref().to_path_buf(); 25 | let work_path = upper_path 26 | .parent() 27 | .unwrap_or_else(|| Path::new("/tmp")) 28 | .join("overlayfs-work"); 29 | let merged_path = upper_path 30 | .parent() 31 | .unwrap_or_else(|| Path::new("/tmp")) 32 | .join("overlayfs-merged"); 33 | 34 | Self { 35 | lower: lower_path, 36 | upper: upper_path, 37 | work: work_path, 38 | merged: merged_path, 39 | } 40 | } 41 | 42 | /// Validate overlay configuration 43 | pub fn validate(&self) -> Result<()> { 44 | if !self.lower.exists() { 45 | return Err(SandboxError::Syscall(format!( 46 | "Lower layer does not exist: {}", 47 | self.lower.display() 48 | ))); 49 | } 50 | 51 | Ok(()) 52 | } 53 | 54 | /// Create necessary directories 55 | pub fn setup_directories(&self) -> Result<()> { 56 | fs::create_dir_all(&self.upper) 57 | .map_err(|e| SandboxError::Syscall(format!("Failed to create upper layer: {}", e)))?; 58 | 59 | if self.work.exists() { 60 | fs::remove_dir_all(&self.work).map_err(|e| { 61 | SandboxError::Syscall(format!("Failed to clean work directory: {}", e)) 62 | })?; 63 | } 64 | 65 | fs::create_dir_all(&self.work).map_err(|e| { 66 | SandboxError::Syscall(format!("Failed to create work directory: {}", e)) 67 | })?; 68 | 69 | fs::create_dir_all(&self.merged).map_err(|e| { 70 | SandboxError::Syscall(format!("Failed to create merged directory: {}", e)) 71 | })?; 72 | 73 | Ok(()) 74 | } 75 | 76 | /// Get overlay mount options as String for mount syscall 77 | pub fn get_mount_options(&self) -> Result { 78 | let lower_str = self 79 | .lower 80 | .to_str() 81 | .ok_or_else(|| SandboxError::Syscall("Lower path is not valid UTF-8".to_string()))?; 82 | let upper_str = self 83 | .upper 84 | .to_str() 85 | .ok_or_else(|| SandboxError::Syscall("Upper path is not valid UTF-8".to_string()))?; 86 | let work_str = self 87 | .work 88 | .to_str() 89 | .ok_or_else(|| SandboxError::Syscall("Work path is not valid UTF-8".to_string()))?; 90 | 91 | Ok(format!( 92 | "lowerdir={},upperdir={},workdir={}", 93 | lower_str, upper_str, work_str 94 | )) 95 | } 96 | } 97 | 98 | /// Overlay filesystem manager 99 | pub struct OverlayFS { 100 | config: OverlayConfig, 101 | mounted: bool, 102 | } 103 | 104 | impl OverlayFS { 105 | /// Create new overlay filesystem 106 | pub fn new(config: OverlayConfig) -> Self { 107 | Self { 108 | config, 109 | mounted: false, 110 | } 111 | } 112 | 113 | /// Setup overlay filesystem 114 | pub fn setup(&mut self) -> Result<()> { 115 | self.config.validate()?; 116 | self.config.setup_directories()?; 117 | 118 | use std::ffi::CString; 119 | 120 | let fstype = CString::new("overlay") 121 | .map_err(|_| SandboxError::Syscall("Invalid filesystem type".to_string()))?; 122 | 123 | let source = CString::new("overlay") 124 | .map_err(|_| SandboxError::Syscall("Invalid source".to_string()))?; 125 | 126 | let target_str = 127 | self.config.merged.to_str().ok_or_else(|| { 128 | SandboxError::Syscall("Merged path is not valid UTF-8".to_string()) 129 | })?; 130 | let target = CString::new(target_str) 131 | .map_err(|_| SandboxError::Syscall("Invalid target path".to_string()))?; 132 | 133 | let options_str = self.config.get_mount_options()?; 134 | let options = CString::new(options_str.as_str()) 135 | .map_err(|_| SandboxError::Syscall("Invalid mount options".to_string()))?; 136 | 137 | let ret = unsafe { 138 | libc::mount( 139 | source.as_ptr(), 140 | target.as_ptr(), 141 | fstype.as_ptr(), 142 | 0, 143 | options.as_ptr() as *const libc::c_void, 144 | ) 145 | }; 146 | 147 | if ret != 0 { 148 | return Err(SandboxError::Syscall(format!( 149 | "Failed to mount overlay filesystem: {}", 150 | std::io::Error::last_os_error() 151 | ))); 152 | } 153 | 154 | self.mounted = true; 155 | Ok(()) 156 | } 157 | 158 | /// Check if filesystem is mounted 159 | pub fn is_mounted(&self) -> bool { 160 | self.mounted 161 | } 162 | 163 | /// Get merged (visible) directory 164 | pub fn merged_path(&self) -> &Path { 165 | &self.config.merged 166 | } 167 | 168 | /// Get upper (writable) directory 169 | pub fn upper_path(&self) -> &Path { 170 | &self.config.upper 171 | } 172 | 173 | /// Get lower (read-only) directory 174 | pub fn lower_path(&self) -> &Path { 175 | &self.config.lower 176 | } 177 | 178 | /// Cleanup overlay filesystem 179 | pub fn cleanup(&mut self) -> Result<()> { 180 | if self.mounted { 181 | use std::ffi::CString; 182 | use std::os::unix::ffi::OsStrExt; 183 | 184 | let target = CString::new(self.config.merged.as_os_str().as_bytes()).map_err(|_| { 185 | SandboxError::Syscall("Invalid target path for unmount".to_string()) 186 | })?; 187 | 188 | let ret = unsafe { libc::umount2(target.as_ptr(), libc::MNT_DETACH) }; 189 | 190 | if ret != 0 { 191 | let err = std::io::Error::last_os_error(); 192 | if err.raw_os_error() != Some(libc::EINVAL) 193 | && err.raw_os_error() != Some(libc::ENOENT) 194 | { 195 | return Err(SandboxError::Syscall(format!( 196 | "Failed to unmount overlay filesystem: {}", 197 | err 198 | ))); 199 | } 200 | } 201 | 202 | self.mounted = false; 203 | } 204 | 205 | // Clean up work directory 206 | let _ = fs::remove_dir_all(&self.config.work); 207 | 208 | Ok(()) 209 | } 210 | 211 | /// Get total size of changes in upper layer (recursive) 212 | pub fn get_changes_size(&self) -> Result { 213 | use walkdir::WalkDir; 214 | 215 | let mut total = 0u64; 216 | 217 | for entry in WalkDir::new(&self.config.upper) 218 | .into_iter() 219 | .filter_map(|e| e.ok()) 220 | { 221 | if entry.file_type().is_file() { 222 | total += entry 223 | .metadata() 224 | .map_err(|e| SandboxError::Syscall(e.to_string()))? 225 | .len(); 226 | } 227 | } 228 | 229 | Ok(total) 230 | } 231 | } 232 | 233 | /// File layer information 234 | #[derive(Debug, Clone)] 235 | pub struct LayerInfo { 236 | /// Layer name 237 | pub name: String, 238 | /// Layer size in bytes 239 | pub size: u64, 240 | /// Number of files 241 | pub file_count: usize, 242 | /// Whether layer is writable 243 | pub writable: bool, 244 | } 245 | 246 | impl LayerInfo { 247 | /// Get layer info from path (recursive) 248 | pub fn from_path(name: &str, path: &Path, writable: bool) -> Result { 249 | use walkdir::WalkDir; 250 | 251 | let mut size = 0u64; 252 | let mut file_count = 0; 253 | 254 | if path.exists() { 255 | for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { 256 | if entry.file_type().is_file() { 257 | file_count += 1; 258 | size += entry 259 | .metadata() 260 | .map_err(|e| SandboxError::Syscall(e.to_string()))? 261 | .len(); 262 | } 263 | } 264 | } 265 | 266 | Ok(Self { 267 | name: name.to_string(), 268 | size, 269 | file_count, 270 | writable, 271 | }) 272 | } 273 | } 274 | 275 | #[cfg(test)] 276 | mod tests { 277 | use super::*; 278 | 279 | #[test] 280 | fn test_overlay_config_creation() { 281 | let config = OverlayConfig::new("/base", "/upper"); 282 | assert_eq!(config.lower, PathBuf::from("/base")); 283 | assert_eq!(config.upper, PathBuf::from("/upper")); 284 | } 285 | 286 | #[test] 287 | fn test_overlay_config_mount_options() { 288 | let config = OverlayConfig::new("/lower", "/upper"); 289 | let opts = config.get_mount_options().unwrap(); 290 | 291 | assert!(opts.contains("lowerdir=/lower")); 292 | assert!(opts.contains("upperdir=/upper")); 293 | assert!(opts.contains("workdir=")); 294 | } 295 | 296 | #[test] 297 | fn test_overlay_fs_creation() { 298 | let config = OverlayConfig::new("/base", "/upper"); 299 | let fs = OverlayFS::new(config); 300 | 301 | assert!(!fs.is_mounted()); 302 | } 303 | 304 | #[test] 305 | fn test_layer_info_size_calculation() { 306 | let info = LayerInfo { 307 | name: "test".to_string(), 308 | size: 1024, 309 | file_count: 5, 310 | writable: true, 311 | }; 312 | 313 | assert_eq!(info.size, 1024); 314 | assert_eq!(info.file_count, 5); 315 | assert!(info.writable); 316 | } 317 | 318 | #[test] 319 | fn test_overlay_paths() { 320 | let config = OverlayConfig::new("/lower", "/upper"); 321 | let fs = OverlayFS::new(config); 322 | 323 | assert_eq!(fs.lower_path(), Path::new("/lower")); 324 | assert_eq!(fs.upper_path(), Path::new("/upper")); 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /lib/monitoring/monitor.rs: -------------------------------------------------------------------------------- 1 | //! Process monitoring via /proc 2 | //! 3 | //! Provides real-time monitoring of process resources using /proc filesystem. 4 | //! Tracks memory usage, CPU time, thread count, and process state. 5 | 6 | use std::fs; 7 | use std::path::Path; 8 | use std::time::{Duration, Instant}; 9 | 10 | use nix::sys::signal::{Signal, kill}; 11 | use nix::unistd::Pid; 12 | 13 | use crate::errors::{Result, SandboxError}; 14 | 15 | /// Process state enumeration 16 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 17 | pub enum ProcessState { 18 | /// Process is running 19 | Running, 20 | /// Process is sleeping 21 | Sleeping, 22 | /// Process is zombie 23 | Zombie, 24 | /// Process state is unknown 25 | Unknown, 26 | } 27 | 28 | impl ProcessState { 29 | /// Parse state from /proc stat first character 30 | pub fn from_char(c: char) -> Self { 31 | match c { 32 | 'R' => ProcessState::Running, 33 | 'S' => ProcessState::Sleeping, 34 | 'Z' => ProcessState::Zombie, 35 | _ => ProcessState::Unknown, 36 | } 37 | } 38 | } 39 | 40 | /// Process statistics snapshot 41 | #[derive(Debug, Clone)] 42 | pub struct ProcessStats { 43 | /// Process ID 44 | pub pid: i32, 45 | /// Virtual memory size in bytes 46 | pub vsize: u64, 47 | /// Resident set size in bytes (physical memory) 48 | pub rss: u64, 49 | /// RSS in MB (for convenience) 50 | pub memory_usage_mb: u64, 51 | /// CPU time in milliseconds 52 | pub cpu_time_ms: u64, 53 | /// Number of threads 54 | pub num_threads: u32, 55 | /// Current process state 56 | pub state: ProcessState, 57 | /// Timestamp of this snapshot 58 | pub timestamp: Instant, 59 | } 60 | 61 | impl ProcessStats { 62 | /// Create stats from /proc data 63 | fn from_proc(pid: i32, timestamp: Instant) -> Result { 64 | let stat_path = format!("/proc/{}/stat", pid); 65 | let status_path = format!("/proc/{}/status", pid); 66 | 67 | // Read /proc/{pid}/stat 68 | let stat_content = fs::read_to_string(&stat_path).map_err(|e| { 69 | SandboxError::ProcessMonitoring(format!("Failed to read {}: {}", stat_path, e)) 70 | })?; 71 | 72 | // Parse stat: pid (comm) state ppid pgrp session tty_nr tpgid flags minflt cminflt majflt cmajflt utime stime cutime cstime priority nice num_threads ... 73 | let parts: Vec<&str> = stat_content.split_whitespace().collect(); 74 | if parts.len() < 20 { 75 | return Err(SandboxError::ProcessMonitoring( 76 | "Invalid /proc/stat format".to_string(), 77 | )); 78 | } 79 | 80 | let state = ProcessState::from_char(parts[2].chars().next().unwrap_or('?')); 81 | let utime: u64 = parts[13] 82 | .parse() 83 | .map_err(|_| SandboxError::ProcessMonitoring("Invalid utime".to_string()))?; 84 | let stime: u64 = parts[14] 85 | .parse() 86 | .map_err(|_| SandboxError::ProcessMonitoring("Invalid stime".to_string()))?; 87 | let num_threads: u32 = parts[19] 88 | .parse() 89 | .map_err(|_| SandboxError::ProcessMonitoring("Invalid num_threads".to_string()))?; 90 | let vsize: u64 = parts[22] 91 | .parse() 92 | .map_err(|_| SandboxError::ProcessMonitoring("Invalid vsize".to_string()))?; 93 | let rss: u64 = parts[23] 94 | .parse() 95 | .map_err(|_| SandboxError::ProcessMonitoring("Invalid rss".to_string()))?; 96 | 97 | // Read /proc/{pid}/status for additional info (placeholder for future enhancements) 98 | let _status_content = fs::read_to_string(&status_path).unwrap_or_default(); 99 | 100 | // Calculate CPU time in milliseconds 101 | // Kernel reports in clock ticks, get actual CLK_TCK from system 102 | let clk_tck = unsafe { libc::sysconf(libc::_SC_CLK_TCK) } as u64; 103 | let cpu_time_ms = if clk_tck > 0 { 104 | ((utime + stime) * 1000) / clk_tck 105 | } else { 106 | 0 107 | }; 108 | 109 | // RSS is in pages, convert to bytes using actual page size 110 | let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as u64; 111 | let rss_bytes = rss * page_size; 112 | let memory_usage_mb = rss_bytes / (1024 * 1024); 113 | 114 | Ok(ProcessStats { 115 | pid, 116 | vsize, 117 | rss: rss_bytes, 118 | memory_usage_mb, 119 | cpu_time_ms, 120 | num_threads, 121 | state, 122 | timestamp, 123 | }) 124 | } 125 | } 126 | 127 | /// Process monitor for tracking sandbox resource usage 128 | pub struct ProcessMonitor { 129 | pid: Pid, 130 | creation_time: Instant, 131 | peak_memory_mb: u64, 132 | last_stats: Option, 133 | } 134 | 135 | impl ProcessMonitor { 136 | /// Create new monitor for process 137 | pub fn new(pid: Pid) -> Result { 138 | // Verify process exists 139 | let stat_path = format!("/proc/{}/stat", pid.as_raw()); 140 | if !Path::new(&stat_path).exists() { 141 | return Err(SandboxError::ProcessMonitoring(format!( 142 | "Process {} not found", 143 | pid 144 | ))); 145 | } 146 | 147 | Ok(ProcessMonitor { 148 | pid, 149 | creation_time: Instant::now(), 150 | peak_memory_mb: 0, 151 | last_stats: None, 152 | }) 153 | } 154 | 155 | /// Collect current statistics 156 | pub fn collect_stats(&mut self) -> Result { 157 | let now = Instant::now(); 158 | let stats = ProcessStats::from_proc(self.pid.as_raw(), now)?; 159 | 160 | // Track peak memory 161 | if stats.memory_usage_mb > self.peak_memory_mb { 162 | self.peak_memory_mb = stats.memory_usage_mb; 163 | } 164 | 165 | self.last_stats = Some(stats.clone()); 166 | Ok(stats) 167 | } 168 | 169 | /// Get peak memory usage since monitor creation (in MB) 170 | pub fn peak_memory_mb(&self) -> u64 { 171 | self.peak_memory_mb 172 | } 173 | 174 | /// Get elapsed time since monitor creation 175 | pub fn elapsed(&self) -> Duration { 176 | self.creation_time.elapsed() 177 | } 178 | 179 | /// Check if process is still alive 180 | pub fn is_alive(&self) -> Result { 181 | let stat_path = format!("/proc/{}/stat", self.pid.as_raw()); 182 | Ok(Path::new(&stat_path).exists()) 183 | } 184 | 185 | /// Send SIGTERM (graceful shutdown) 186 | pub fn send_sigterm(&self) -> Result<()> { 187 | kill(self.pid, Signal::SIGTERM) 188 | .map_err(|e| SandboxError::Syscall(format!("Failed to send SIGTERM: {}", e))) 189 | } 190 | 191 | /// Send SIGKILL (force termination) 192 | pub fn send_sigkill(&self) -> Result<()> { 193 | kill(self.pid, Signal::SIGKILL) 194 | .map_err(|e| SandboxError::Syscall(format!("Failed to send SIGKILL: {}", e))) 195 | } 196 | 197 | /// Graceful shutdown: SIGTERM → wait → SIGKILL 198 | pub fn graceful_shutdown(&self, wait_duration: Duration) -> Result<()> { 199 | // First try SIGTERM 200 | self.send_sigterm()?; 201 | 202 | // Wait for process to exit 203 | let start = Instant::now(); 204 | while start.elapsed() < wait_duration && self.is_alive()? { 205 | std::thread::sleep(Duration::from_millis(10)); 206 | } 207 | 208 | // If still alive, SIGKILL 209 | if self.is_alive()? { 210 | self.send_sigkill()?; 211 | } 212 | 213 | Ok(()) 214 | } 215 | 216 | /// Get last collected stats 217 | pub fn last_stats(&self) -> Option<&ProcessStats> { 218 | self.last_stats.as_ref() 219 | } 220 | } 221 | 222 | #[cfg(test)] 223 | mod tests { 224 | use super::*; 225 | 226 | #[test] 227 | fn test_process_state_from_char() { 228 | assert_eq!(ProcessState::from_char('R'), ProcessState::Running); 229 | assert_eq!(ProcessState::from_char('S'), ProcessState::Sleeping); 230 | assert_eq!(ProcessState::from_char('Z'), ProcessState::Zombie); 231 | assert_eq!(ProcessState::from_char('X'), ProcessState::Unknown); 232 | } 233 | 234 | #[test] 235 | fn test_process_stats_creation() { 236 | // We can at least create stats for the test runner process itself 237 | let pid = std::process::id() as i32; 238 | let timestamp = Instant::now(); 239 | let result = ProcessStats::from_proc(pid, timestamp); 240 | assert!(result.is_ok()); 241 | 242 | if let Ok(stats) = result { 243 | assert_eq!(stats.pid, pid); 244 | assert!(stats.memory_usage_mb > 0); 245 | } 246 | } 247 | 248 | #[test] 249 | fn test_process_monitor_new() { 250 | let pid = Pid::from_raw(std::process::id() as i32); 251 | let result = ProcessMonitor::new(pid); 252 | assert!(result.is_ok()); 253 | } 254 | 255 | #[test] 256 | fn test_process_monitor_is_alive() { 257 | let pid = Pid::from_raw(std::process::id() as i32); 258 | let monitor = ProcessMonitor::new(pid).unwrap(); 259 | assert!(monitor.is_alive().unwrap()); 260 | } 261 | 262 | #[test] 263 | fn test_process_monitor_collect_stats() { 264 | let pid = Pid::from_raw(std::process::id() as i32); 265 | let mut monitor = ProcessMonitor::new(pid).unwrap(); 266 | let stats = monitor.collect_stats().unwrap(); 267 | 268 | assert_eq!(stats.pid, pid.as_raw()); 269 | assert!(stats.memory_usage_mb > 0); 270 | assert_eq!(monitor.peak_memory_mb(), stats.memory_usage_mb); 271 | } 272 | 273 | #[test] 274 | fn test_process_monitor_peak_memory() { 275 | let pid = Pid::from_raw(std::process::id() as i32); 276 | let mut monitor = ProcessMonitor::new(pid).unwrap(); 277 | 278 | monitor.collect_stats().unwrap(); 279 | let peak1 = monitor.peak_memory_mb(); 280 | 281 | monitor.collect_stats().unwrap(); 282 | let peak2 = monitor.peak_memory_mb(); 283 | 284 | assert!(peak1 > 0); 285 | assert!(peak2 >= peak1); 286 | } 287 | 288 | #[test] 289 | fn test_process_stats_from_proc_missing_file() { 290 | let invalid_pid = 9_999_999i32; 291 | let timestamp = Instant::now(); 292 | let result = ProcessStats::from_proc(invalid_pid, timestamp); 293 | assert!(result.is_err()); 294 | } 295 | 296 | #[test] 297 | fn test_process_stats_from_proc_invalid_format() { 298 | let pid = std::process::id() as i32; 299 | let timestamp = Instant::now(); 300 | let result = ProcessStats::from_proc(pid, timestamp); 301 | assert!(result.is_ok()); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /lib/network/config.rs: -------------------------------------------------------------------------------- 1 | //! Network configuration for sandbox isolation 2 | //! 3 | //! NOTE: NetworkConfig types are defined but NOT currently integrated into the main 4 | //! sandbox execution. Network isolation is handled via namespace configuration. 5 | //! This module provides types for future network management features. 6 | 7 | use crate::errors::{Result, SandboxError}; 8 | use serde::{Deserialize, Serialize}; 9 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 10 | 11 | /// Network interface configuration 12 | #[derive(Debug, Clone, Serialize, Deserialize)] 13 | pub struct NetworkInterface { 14 | /// Interface name 15 | pub name: String, 16 | /// IPv4 address 17 | pub ipv4: Ipv4Addr, 18 | /// Netmask 19 | pub netmask: Ipv4Addr, 20 | /// Gateway 21 | pub gateway: Option, 22 | /// Whether to enable 23 | pub enabled: bool, 24 | } 25 | 26 | impl Default for NetworkInterface { 27 | fn default() -> Self { 28 | Self { 29 | name: "eth0".to_string(), 30 | ipv4: Ipv4Addr::new(172, 17, 0, 2), 31 | netmask: Ipv4Addr::new(255, 255, 255, 0), 32 | gateway: Some(Ipv4Addr::new(172, 17, 0, 1)), 33 | enabled: true, 34 | } 35 | } 36 | } 37 | 38 | impl NetworkInterface { 39 | /// Create new network interface 40 | pub fn new(name: &str, ipv4: Ipv4Addr) -> Self { 41 | Self { 42 | name: name.to_string(), 43 | ipv4, 44 | netmask: Ipv4Addr::new(255, 255, 255, 0), 45 | gateway: Some(Ipv4Addr::new(172, 17, 0, 1)), 46 | enabled: true, 47 | } 48 | } 49 | 50 | /// Validate interface configuration 51 | pub fn validate(&self) -> Result<()> { 52 | if self.name.is_empty() { 53 | return Err(SandboxError::InvalidConfig( 54 | "Interface name cannot be empty".to_string(), 55 | )); 56 | } 57 | 58 | // Check if IP is valid container range (not 0.0.0.0 or broadcast) 59 | if self.ipv4.is_unspecified() || self.ipv4.is_broadcast() { 60 | return Err(SandboxError::InvalidConfig( 61 | "Invalid IP address for interface".to_string(), 62 | )); 63 | } 64 | 65 | Ok(()) 66 | } 67 | 68 | /// Get CIDR notation 69 | pub fn get_cidr(&self) -> String { 70 | format!("{}/{}", self.ipv4, self.netmask_bits()) 71 | } 72 | 73 | /// Get netmask bits (/24, /16, etc.) 74 | pub fn netmask_bits(&self) -> u8 { 75 | let octets = self.netmask.octets(); 76 | let mut bits = 0u8; 77 | 78 | for octet in octets { 79 | bits += octet.count_ones() as u8; 80 | } 81 | 82 | bits 83 | } 84 | } 85 | 86 | /// Network mode for sandbox 87 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] 88 | pub enum NetworkMode { 89 | /// Isolated network namespace 90 | #[default] 91 | Isolated, 92 | /// Bridge mode (connected via virtual bridge) 93 | Bridge, 94 | /// Host network namespace 95 | Host, 96 | /// Custom configuration 97 | Custom, 98 | } 99 | 100 | impl std::fmt::Display for NetworkMode { 101 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 102 | match self { 103 | NetworkMode::Isolated => write!(f, "isolated"), 104 | NetworkMode::Bridge => write!(f, "bridge"), 105 | NetworkMode::Host => write!(f, "host"), 106 | NetworkMode::Custom => write!(f, "custom"), 107 | } 108 | } 109 | } 110 | 111 | /// Network configuration 112 | #[derive(Debug, Clone, Serialize, Deserialize)] 113 | pub struct NetworkConfig { 114 | /// Network mode 115 | pub mode: NetworkMode, 116 | /// Network interfaces 117 | pub interfaces: Vec, 118 | /// DNS servers 119 | pub dns_servers: Vec, 120 | /// Exposed ports (container:host) 121 | pub port_mappings: Vec, 122 | /// Enable IP forwarding 123 | pub ip_forward: bool, 124 | /// Maximum bandwidth (bytes/sec, 0 = unlimited) 125 | pub bandwidth_limit: u64, 126 | } 127 | 128 | impl Default for NetworkConfig { 129 | fn default() -> Self { 130 | Self { 131 | mode: NetworkMode::Isolated, 132 | interfaces: vec![NetworkInterface::default()], 133 | dns_servers: vec![ 134 | IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8)), 135 | IpAddr::V4(Ipv4Addr::new(1, 1, 1, 1)), 136 | ], 137 | port_mappings: Vec::new(), 138 | ip_forward: false, 139 | bandwidth_limit: 0, 140 | } 141 | } 142 | } 143 | 144 | impl NetworkConfig { 145 | /// Create isolated network config (no DNS) 146 | pub fn isolated() -> Self { 147 | Self { 148 | mode: NetworkMode::Isolated, 149 | interfaces: vec![], 150 | dns_servers: vec![], 151 | port_mappings: Vec::new(), 152 | ip_forward: false, 153 | bandwidth_limit: 0, 154 | } 155 | } 156 | 157 | /// Create host network config 158 | pub fn host() -> Self { 159 | Self { 160 | mode: NetworkMode::Host, 161 | interfaces: Vec::new(), 162 | dns_servers: Vec::new(), 163 | port_mappings: Vec::new(), 164 | ip_forward: true, 165 | bandwidth_limit: 0, 166 | } 167 | } 168 | 169 | /// Add interface 170 | pub fn add_interface(&mut self, iface: NetworkInterface) -> Result<()> { 171 | iface.validate()?; 172 | self.interfaces.push(iface); 173 | Ok(()) 174 | } 175 | 176 | /// Add port mapping 177 | pub fn add_port_mapping(&mut self, mapping: PortMapping) -> Result<()> { 178 | mapping.validate()?; 179 | self.port_mappings.push(mapping); 180 | Ok(()) 181 | } 182 | 183 | /// Validate configuration 184 | pub fn validate(&self) -> Result<()> { 185 | for iface in &self.interfaces { 186 | iface.validate()?; 187 | } 188 | 189 | for mapping in &self.port_mappings { 190 | mapping.validate()?; 191 | } 192 | 193 | Ok(()) 194 | } 195 | } 196 | 197 | /// Port mapping configuration 198 | #[derive(Debug, Clone, Serialize, Deserialize)] 199 | pub struct PortMapping { 200 | /// Container port 201 | pub container_port: u16, 202 | /// Host port 203 | pub host_port: u16, 204 | /// Protocol (tcp/udp) 205 | pub protocol: String, 206 | } 207 | 208 | impl PortMapping { 209 | /// Create new port mapping 210 | pub fn new(container_port: u16, host_port: u16) -> Self { 211 | Self { 212 | container_port, 213 | host_port, 214 | protocol: "tcp".to_string(), 215 | } 216 | } 217 | 218 | /// Validate port mapping 219 | pub fn validate(&self) -> Result<()> { 220 | if self.container_port == 0 || self.host_port == 0 { 221 | return Err(SandboxError::InvalidConfig( 222 | "Port numbers must be > 0".to_string(), 223 | )); 224 | } 225 | 226 | if !["tcp", "udp"].contains(&self.protocol.as_str()) { 227 | return Err(SandboxError::InvalidConfig( 228 | "Protocol must be tcp or udp".to_string(), 229 | )); 230 | } 231 | 232 | Ok(()) 233 | } 234 | 235 | /// Get socket address for host 236 | pub fn get_host_addr(&self) -> SocketAddr { 237 | SocketAddr::from((Ipv4Addr::LOCALHOST, self.host_port)) 238 | } 239 | 240 | /// Get socket address for container 241 | pub fn get_container_addr(&self, ip: Ipv4Addr) -> SocketAddr { 242 | SocketAddr::from((ip, self.container_port)) 243 | } 244 | } 245 | 246 | /// Network statistics 247 | #[derive(Debug, Clone, Default, Serialize, Deserialize)] 248 | pub struct NetworkStats { 249 | /// Bytes received 250 | pub bytes_recv: u64, 251 | /// Bytes sent 252 | pub bytes_sent: u64, 253 | /// Packets received 254 | pub packets_recv: u64, 255 | /// Packets sent 256 | pub packets_sent: u64, 257 | /// Errors 258 | pub errors: u64, 259 | /// Dropped packets 260 | pub dropped: u64, 261 | } 262 | 263 | #[cfg(test)] 264 | mod tests { 265 | use super::*; 266 | 267 | #[test] 268 | fn test_network_interface_creation() { 269 | let iface = NetworkInterface::new("eth0", Ipv4Addr::new(192, 168, 1, 10)); 270 | assert_eq!(iface.name, "eth0"); 271 | assert_eq!(iface.ipv4, Ipv4Addr::new(192, 168, 1, 10)); 272 | } 273 | 274 | #[test] 275 | fn test_network_interface_validation() { 276 | let mut iface = NetworkInterface::default(); 277 | assert!(iface.validate().is_ok()); 278 | 279 | iface.ipv4 = Ipv4Addr::UNSPECIFIED; 280 | assert!(iface.validate().is_err()); 281 | } 282 | 283 | #[test] 284 | fn test_network_interface_cidr() { 285 | let iface = NetworkInterface::default(); 286 | let cidr = iface.get_cidr(); 287 | assert!(cidr.contains("/")); 288 | } 289 | 290 | #[test] 291 | fn test_netmask_bits() { 292 | let iface = NetworkInterface { 293 | netmask: Ipv4Addr::new(255, 255, 255, 0), 294 | ..Default::default() 295 | }; 296 | assert_eq!(iface.netmask_bits(), 24); 297 | } 298 | 299 | #[test] 300 | fn test_network_mode_display() { 301 | assert_eq!(NetworkMode::Isolated.to_string(), "isolated"); 302 | assert_eq!(NetworkMode::Bridge.to_string(), "bridge"); 303 | assert_eq!(NetworkMode::Host.to_string(), "host"); 304 | } 305 | 306 | #[test] 307 | fn test_network_config_default() { 308 | let config = NetworkConfig::default(); 309 | assert_eq!(config.mode, NetworkMode::Isolated); 310 | assert!(!config.interfaces.is_empty()); 311 | } 312 | 313 | #[test] 314 | fn test_network_config_isolated() { 315 | let config = NetworkConfig::isolated(); 316 | assert_eq!(config.mode, NetworkMode::Isolated); 317 | } 318 | 319 | #[test] 320 | fn test_network_config_host() { 321 | let config = NetworkConfig::host(); 322 | assert_eq!(config.mode, NetworkMode::Host); 323 | assert!(config.ip_forward); 324 | } 325 | 326 | #[test] 327 | fn test_port_mapping_creation() { 328 | let mapping = PortMapping::new(8080, 8080); 329 | assert_eq!(mapping.container_port, 8080); 330 | assert_eq!(mapping.host_port, 8080); 331 | } 332 | 333 | #[test] 334 | fn test_port_mapping_validation() { 335 | let mapping = PortMapping::new(8080, 8080); 336 | assert!(mapping.validate().is_ok()); 337 | 338 | let bad_mapping = PortMapping { 339 | container_port: 0, 340 | host_port: 8080, 341 | protocol: "tcp".to_string(), 342 | }; 343 | assert!(bad_mapping.validate().is_err()); 344 | } 345 | 346 | #[test] 347 | fn test_port_mapping_addresses() { 348 | let mapping = PortMapping::new(8080, 8080); 349 | let host_addr = mapping.get_host_addr(); 350 | assert_eq!(host_addr.port(), 8080); 351 | } 352 | 353 | #[test] 354 | fn test_network_stats_default() { 355 | let stats = NetworkStats::default(); 356 | assert_eq!(stats.bytes_recv, 0); 357 | assert_eq!(stats.bytes_sent, 0); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /examples/error_handling.rs: -------------------------------------------------------------------------------- 1 | //! Error Handling and Resource Limits Example 2 | //! 3 | //! This example demonstrates how to handle common error scenarios 4 | //! when using sandboxes, including: 5 | //! - Timeout handling 6 | //! - Out-of-memory (OOM) conditions 7 | //! - Seccomp violations 8 | //! - Invalid configurations 9 | //! - Resource exhaustion 10 | //! 11 | //! ## Running this example 12 | //! 13 | //! ```bash 14 | //! cargo run --example error_handling 15 | //! ``` 16 | //! 17 | //! Note: Some scenarios require root privileges for full enforcement 18 | 19 | fn main() -> Result<(), Box> { 20 | println!("=== Sandbox Error Handling Examples ===\n"); 21 | 22 | // Scenario 1: Timeout handling 23 | println!("[Scenario 1] Timeout Handling\n"); 24 | scenario_timeout()?; 25 | println!(); 26 | 27 | // Scenario 2: Invalid configuration 28 | println!("[Scenario 2] Invalid Configuration Detection\n"); 29 | scenario_invalid_config(); 30 | println!(); 31 | 32 | // Scenario 3: Proper error matching 33 | println!("[Scenario 3] Error Matching and Recovery\n"); 34 | scenario_error_matching()?; 35 | println!(); 36 | 37 | // Scenario 4: Resource-limited execution 38 | println!("[Scenario 4] Resource Enforcement\n"); 39 | scenario_resource_limits()?; 40 | println!(); 41 | 42 | // Scenario 5: Seccomp violation (informational) 43 | println!("[Scenario 5] Seccomp Violation Handling\n"); 44 | scenario_seccomp_violation(); 45 | println!(); 46 | 47 | // Scenario 6: Best practices 48 | println!("[Scenario 6] Error Handling Best Practices\n"); 49 | best_practices(); 50 | println!(); 51 | 52 | println!("=== All scenarios completed ==="); 53 | Ok(()) 54 | } 55 | 56 | /// Example 1: Timeout handling 57 | fn scenario_timeout() -> Result<(), Box> { 58 | println!("When a sandbox times out:"); 59 | println!(" - The process is killed"); 60 | println!(" - SandboxResult.timed_out = true"); 61 | println!(" - Exit code may be None (killed by signal)\n"); 62 | 63 | println!("Code example:"); 64 | println!(" let mut sandbox = SandboxBuilder::new(\"timeout-test\")"); 65 | println!(" .timeout(Duration::from_secs(2))"); 66 | println!(" .build()?;\n"); 67 | 68 | println!(" let result = sandbox.run(\"/bin/sleep\", &[\"10\"])?;"); 69 | println!(" if result.timed_out {{"); 70 | println!(" println!(\"Sandbox exceeded 2 second timeout\");"); 71 | println!(" // Handle timeout: cleanup, retry, report error, etc."); 72 | println!(" }}\n"); 73 | 74 | println!("Common timeout scenarios:"); 75 | println!(" 1. Process hangs indefinitely"); 76 | println!(" 2. Infinite loop in sandboxed code"); 77 | println!(" 3. Deadlock or resource contention"); 78 | println!(" 4. Network I/O waiting for external service\n"); 79 | 80 | println!("Mitigation strategies:"); 81 | println!(" - Set appropriate timeout based on expected runtime"); 82 | println!(" - Use shorter timeouts for untrusted code"); 83 | println!(" - Log which operations timeout for debugging"); 84 | println!(" - Increase timeout for I/O intensive operations"); 85 | 86 | Ok(()) 87 | } 88 | 89 | /// Example 2: Invalid configuration detection 90 | fn scenario_invalid_config() { 91 | println!("The sandbox validates configuration at build time:"); 92 | println!(" 1. Requires at least one namespace"); 93 | println!(" 2. Validates sandbox ID is non-empty"); 94 | println!(" 3. Checks resource limits are reasonable"); 95 | println!(" 4. May require root privileges\n"); 96 | 97 | println!("Example: Building with invalid config\n"); 98 | println!(" // This would fail at build time:"); 99 | println!(" let result = SandboxBuilder::new(\"\") // Empty ID!"); 100 | println!(" .build();"); 101 | println!(" // result is Err: \"Sandbox ID cannot be empty\"\n"); 102 | 103 | println!("Another example: Disabled all namespaces\n"); 104 | println!(" let config = NamespaceConfig {{"); 105 | println!(" pid: false,"); 106 | println!(" ipc: false,"); 107 | println!(" net: false,"); 108 | println!(" mount: false,"); 109 | println!(" uts: false,"); 110 | println!(" user: false,"); 111 | println!(" }};"); 112 | println!(" // Error: \"At least one namespace must be enabled\"\n"); 113 | 114 | println!("Best practice:"); 115 | println!(" - Always check build() result"); 116 | println!(" - Use ? operator for early error propagation"); 117 | println!(" - Log configuration errors for debugging\n"); 118 | } 119 | 120 | /// Example 3: Error matching and recovery 121 | fn scenario_error_matching() -> Result<(), Box> { 122 | println!("Handling different error types:\n"); 123 | 124 | println!("Example: Create sandbox with detailed error handling\n"); 125 | println!(" match SandboxBuilder::new(\"test\")"); 126 | println!(" .memory_limit_str(\"invalid\")"); 127 | println!(" .build() {{"); 128 | println!(" Ok(sandbox) => {{ /* success */ }},"); 129 | println!(" Err(e) => {{"); 130 | println!(" match e {{"); 131 | println!(" SandboxError::InvalidConfig(msg) => {{"); 132 | println!(" eprintln!(\"Config error: {{}}\", msg);"); 133 | println!(" // Recover: use defaults or prompt user"); 134 | println!(" }},"); 135 | println!(" SandboxError::Io(io_err) => {{"); 136 | println!(" eprintln!(\"IO error: {{}}\", io_err);"); 137 | println!(" // Recover: check permissions, disk space"); 138 | println!(" }},"); 139 | println!(" SandboxError::Syscall(msg) => {{"); 140 | println!(" eprintln!(\"Syscall failed: {{}}\", msg);"); 141 | println!(" // Recover: may need root or kernel features"); 142 | println!(" }},"); 143 | println!(" _ => {{"); 144 | println!(" eprintln!(\"Other error: {{}}\", e);"); 145 | println!(" }}"); 146 | println!(" }}"); 147 | println!(" }}"); 148 | println!(" }}\n"); 149 | 150 | println!("Common error sources:"); 151 | println!(" - InvalidConfig: User provided bad parameters"); 152 | println!(" - Io: File system operations failed"); 153 | println!(" - Syscall: Kernel operations failed (may need root)"); 154 | println!(" - AlreadyRunning: Tried to run program twice"); 155 | println!(" - ProcessMonitoring: Failed to read process stats\n"); 156 | 157 | Ok(()) 158 | } 159 | 160 | /// Example 4: Resource limits enforcement 161 | fn scenario_resource_limits() -> Result<(), Box> { 162 | println!("Memory limit example:"); 163 | println!(" let mut sandbox = SandboxBuilder::new(\"memory-test\")"); 164 | println!(" .memory_limit(100 * 1024 * 1024) // 100MB"); 165 | println!(" .build()?;"); 166 | println!(" let result = sandbox.run(\"/usr/bin/yes\", &[])?;"); 167 | println!(" if result.memory_peak > 100 * 1024 * 1024 {{"); 168 | println!(" println!(\"Process exceeded memory limit!\");"); 169 | println!(" }}\n"); 170 | 171 | println!("CPU limit example:"); 172 | println!(" let mut sandbox = SandboxBuilder::new(\"cpu-test\")"); 173 | println!(" .cpu_limit_percent(50) // Max 50% of one core"); 174 | println!(" .build()?;"); 175 | println!(" let result = sandbox.run(\"/bin/sh\", &[\"-c\", \"...\"])?;"); 176 | println!(" println!(\"CPU time: {{}} us\", result.cpu_time_us);\n"); 177 | 178 | println!("What happens when limits are exceeded:"); 179 | println!(" Memory: Process is OOMkilled by kernel"); 180 | println!(" CPU: Process is throttled (runs slower)"); 181 | println!(" PIDs: New fork() calls fail with EAGAIN\n"); 182 | 183 | println!("Recommended limits by use case:"); 184 | println!(" Untrusted code: 64MB memory, 10-25% CPU, 5s timeout"); 185 | println!(" Data processing: 512MB-1GB memory, 50-100% CPU, 60s timeout"); 186 | println!(" Compute intensive: 1-2GB memory, 100%+ CPU, 120s+ timeout\n"); 187 | 188 | Ok(()) 189 | } 190 | 191 | /// Example 5: Seccomp violation information 192 | fn scenario_seccomp_violation() { 193 | println!("Seccomp filtering prevents dangerous syscalls:"); 194 | println!(" - Minimal profile: Only exit, read, write"); 195 | println!(" - IoHeavy: Adds file operations"); 196 | println!(" - Compute: Adds memory operations"); 197 | println!(" - Network: Adds socket operations\n"); 198 | 199 | println!("Example: Attempting forbidden syscall with Minimal profile\n"); 200 | println!(" let mut sandbox = SandboxBuilder::new(\"strict\")"); 201 | println!(" .seccomp_profile(SeccompProfile::Minimal)"); 202 | println!(" .build()?;"); 203 | println!(" // Running code that tries to open a file:"); 204 | println!(" sandbox.run(\"/bin/sh\", &[\"-c\", \"cat /etc/passwd\"])?;"); 205 | println!(" // Result: Process is killed (SIGSYS or similar)\n"); 206 | 207 | println!("Checking which syscalls are allowed:"); 208 | println!(" for profile in SeccompProfile::all() {{"); 209 | println!(" println!(\"{{}}: {{}}\", profile.description());"); 210 | println!(" }}\n"); 211 | 212 | println!("Design principles:"); 213 | println!(" - Use strictest profile that allows needed operations"); 214 | println!(" - Test sandbox before deploying to production"); 215 | println!(" - Monitor for seccomp violations (SIGSYS signals)"); 216 | println!(" - Log violations for security analysis\n"); 217 | } 218 | 219 | /// Example 6: Best practices 220 | fn best_practices() { 221 | println!("Error Handling Best Practices:\n"); 222 | 223 | println!("1. VALIDATE EARLY"); 224 | println!(" - Check sandbox.build() result immediately"); 225 | println!(" - Fail fast with clear error messages\n"); 226 | 227 | println!("2. USE APPROPRIATE TIMEOUTS"); 228 | println!(" - Set realistic timeouts based on expected runtime"); 229 | println!(" - Use shorter timeouts (5-10s) for untrusted code"); 230 | println!(" - Increase for I/O intensive operations\n"); 231 | 232 | println!("3. LIMIT RESOURCES APPROPRIATELY"); 233 | println!(" - Start with tight limits, relax if needed"); 234 | println!(" - Memory: Match actual requirements + safety margin"); 235 | println!(" - CPU: Based on system capacity and fairness"); 236 | println!(" - PIDs: Prevent fork-bomb (usually 4-16)\n"); 237 | 238 | println!("4. LOG COMPREHENSIVELY"); 239 | println!(" - Log sandbox creation parameters"); 240 | println!(" - Log execution results (exit code, resources used)"); 241 | println!(" - Log errors with full context"); 242 | println!(" - Example:"); 243 | println!(" info!(\"Sandbox {{id}} created with {{memory}} memory\");"); 244 | println!(" match sandbox.run(...) {{"); 245 | println!(" Ok(result) => warn!(\"{{id}} timed out\"),"); 246 | println!(" Err(e) => error!(\"{{id}} failed: {{}}\", e),"); 247 | println!(" }}\n"); 248 | 249 | println!("5. HANDLE CLEANUP"); 250 | println!(" - Always cleanup even on error"); 251 | println!(" - Use guard patterns for automatic cleanup:"); 252 | println!(" struct SandboxGuard(Sandbox);"); 253 | println!(" impl Drop for SandboxGuard {{ ... }}\n"); 254 | 255 | println!("6. MONITOR RESOURCE USAGE"); 256 | println!(" - Check memory_peak, cpu_time_us"); 257 | println!(" - Alert if approaching limits"); 258 | println!(" - Adjust limits based on actual usage\n"); 259 | 260 | println!("7. TEST ERROR PATHS"); 261 | println!(" - Test with invalid configs"); 262 | println!(" - Test with tight resource limits"); 263 | println!(" - Test with short timeouts"); 264 | println!(" - Test seccomp violations\n"); 265 | } 266 | -------------------------------------------------------------------------------- /lib/execution/tests.rs: -------------------------------------------------------------------------------- 1 | use super::*; 2 | use crate::isolation::{NamespaceConfig, SeccompFilter, SeccompProfile}; 3 | use crate::test_support::serial_guard; 4 | use libc; 5 | use nix::unistd::{ForkResult, fork}; 6 | 7 | fn sample_process_config() -> ProcessConfig { 8 | ProcessConfig { 9 | program: "/bin/echo".to_string(), 10 | args: vec!["hello".to_string(), "sandbox".to_string()], 11 | env: vec![ 12 | ("RUST_BACKTRACE".to_string(), "1".to_string()), 13 | ("SANDBOX".to_string(), "true".to_string()), 14 | ], 15 | cwd: Some("/tmp".to_string()), 16 | chroot_dir: Some("/".to_string()), 17 | uid: Some(1000), 18 | gid: Some(1000), 19 | seccomp: Some(SeccompFilter::from_profile(SeccompProfile::Minimal)), 20 | inherit_env: true, 21 | } 22 | } 23 | 24 | fn no_isolation_namespace() -> NamespaceConfig { 25 | NamespaceConfig { 26 | pid: false, 27 | ipc: false, 28 | net: false, 29 | mount: false, 30 | uts: false, 31 | user: false, 32 | } 33 | } 34 | 35 | #[test] 36 | fn process_config_captures_full_configuration() { 37 | let config = sample_process_config(); 38 | 39 | assert_eq!(config.program, "/bin/echo"); 40 | assert_eq!(config.args, vec!["hello", "sandbox"]); 41 | assert_eq!(config.env.len(), 2); 42 | assert_eq!(config.cwd.as_deref(), Some("/tmp")); 43 | assert_eq!(config.chroot_dir.as_deref(), Some("/")); 44 | assert_eq!(config.uid, Some(1000)); 45 | assert_eq!(config.gid, Some(1000)); 46 | assert!(config.seccomp.is_some()); 47 | } 48 | 49 | #[test] 50 | fn process_config_clone_preserves_data() { 51 | let original = sample_process_config(); 52 | let cloned = original.clone(); 53 | 54 | assert_eq!(original.program, cloned.program); 55 | assert_eq!(original.args, cloned.args); 56 | assert_eq!(original.env, cloned.env); 57 | assert_eq!(original.cwd, cloned.cwd); 58 | assert_eq!( 59 | original.seccomp.as_ref().unwrap().allowed_count(), 60 | cloned.seccomp.as_ref().unwrap().allowed_count() 61 | ); 62 | } 63 | 64 | #[test] 65 | fn process_config_accepts_custom_namespace_config() { 66 | let mut namespace = NamespaceConfig::minimal(); 67 | assert_eq!(namespace.enabled_count(), 4); 68 | 69 | namespace.uts = true; 70 | assert_eq!(namespace.enabled_count(), 5); 71 | 72 | namespace.user = true; 73 | assert!(namespace.all_enabled()); 74 | let flags = namespace.to_clone_flags(); 75 | assert!(flags.bits() != 0); 76 | } 77 | 78 | #[test] 79 | fn process_result_records_exit_information() { 80 | let result = ProcessResult { 81 | pid: nix::unistd::Pid::from_raw(4242), 82 | exit_status: 137, 83 | signal: Some(9), 84 | exec_time_ms: 250, 85 | }; 86 | 87 | assert_eq!(result.pid.as_raw(), 4242); 88 | assert_eq!(result.exit_status, 137); 89 | assert_eq!(result.signal, Some(9)); 90 | assert_eq!(result.exec_time_ms, 250); 91 | } 92 | 93 | #[test] 94 | fn sandbox_init_reaps_finished_children_without_panicking() { 95 | let _guard = serial_guard(); 96 | use nix::sys::wait::{WaitStatus, waitpid}; 97 | 98 | match unsafe { fork() } { 99 | Ok(ForkResult::Child) => { 100 | std::process::exit(0); 101 | } 102 | Ok(ForkResult::Parent { child }) => { 103 | // Wait for the child to exit 104 | match waitpid(child, None) { 105 | Ok(WaitStatus::Exited(_, _)) => { 106 | // Child exited successfully 107 | } 108 | Ok(_) => { 109 | // Child stopped or signaled 110 | } 111 | Err(e) => panic!("waitpid failed: {}", e), 112 | } 113 | } 114 | Err(err) => panic!("fork failed: {}", err), 115 | } 116 | } 117 | 118 | #[test] 119 | fn process_executor_skips_privileged_operations_without_root() { 120 | let _guard = serial_guard(); 121 | let current_gid = unsafe { libc::getgid() as u32 }; 122 | let config = ProcessConfig { 123 | program: "/bin/true".to_string(), 124 | args: vec![], 125 | chroot_dir: Some("/".to_string()), 126 | gid: Some(current_gid), 127 | seccomp: Some(SeccompFilter::minimal()), 128 | ..Default::default() 129 | }; 130 | 131 | let namespace = no_isolation_namespace(); 132 | let result = ProcessExecutor::execute(config, namespace).unwrap(); 133 | assert_eq!(result.exit_status, 0); 134 | } 135 | 136 | #[test] 137 | fn stream_new_creates_channel_pair() { 138 | use crate::execution::stream::ProcessStream; 139 | 140 | let (writer, reader) = ProcessStream::new(); 141 | writer.send_stdout("test".to_string()).unwrap(); 142 | 143 | match reader.recv() { 144 | Ok(Some(chunk)) => match chunk { 145 | crate::execution::stream::StreamChunk::Stdout(data) => { 146 | assert_eq!(data, "test"); 147 | } 148 | _ => panic!("Expected Stdout chunk"), 149 | }, 150 | _ => panic!("Failed to receive chunk"), 151 | } 152 | } 153 | 154 | #[test] 155 | fn stream_default_creates_empty_stream() { 156 | use crate::execution::stream::ProcessStream; 157 | 158 | let stream = ProcessStream::default(); 159 | assert!(stream.try_recv().unwrap().is_none()); 160 | } 161 | 162 | #[test] 163 | fn stream_send_stdout_and_receive() { 164 | use crate::execution::stream::{ProcessStream, StreamChunk}; 165 | 166 | let (writer, reader) = ProcessStream::new(); 167 | writer.send_stdout("hello stdout".to_string()).unwrap(); 168 | 169 | let chunk = reader.recv().unwrap().unwrap(); 170 | match chunk { 171 | StreamChunk::Stdout(data) => assert_eq!(data, "hello stdout"), 172 | _ => panic!("Expected Stdout"), 173 | } 174 | } 175 | 176 | #[test] 177 | fn stream_send_stderr_and_receive() { 178 | use crate::execution::stream::{ProcessStream, StreamChunk}; 179 | 180 | let (writer, reader) = ProcessStream::new(); 181 | writer.send_stderr("error message".to_string()).unwrap(); 182 | 183 | let chunk = reader.recv().unwrap().unwrap(); 184 | match chunk { 185 | StreamChunk::Stderr(data) => assert_eq!(data, "error message"), 186 | _ => panic!("Expected Stderr"), 187 | } 188 | } 189 | 190 | #[test] 191 | fn stream_send_exit_and_receive() { 192 | use crate::execution::stream::{ProcessStream, StreamChunk}; 193 | 194 | let (writer, reader) = ProcessStream::new(); 195 | writer.send_exit(42, Some(9)).unwrap(); 196 | 197 | let chunk = reader.recv().unwrap().unwrap(); 198 | match chunk { 199 | StreamChunk::Exit { exit_code, signal } => { 200 | assert_eq!(exit_code, 42); 201 | assert_eq!(signal, Some(9)); 202 | } 203 | _ => panic!("Expected Exit"), 204 | } 205 | } 206 | 207 | #[test] 208 | fn stream_try_recv_non_blocking() { 209 | use crate::execution::stream::ProcessStream; 210 | let (writer, reader) = ProcessStream::new(); 211 | assert!(reader.try_recv().unwrap().is_none()); 212 | writer.send_stdout("data".to_string()).unwrap(); 213 | assert!(reader.try_recv().unwrap().is_some()); 214 | } 215 | 216 | #[test] 217 | fn stream_try_recv_after_disconnect() { 218 | use crate::execution::stream::ProcessStream; 219 | let (_writer, reader) = ProcessStream::new(); 220 | drop(_writer); 221 | assert!(reader.try_recv().unwrap().is_none()); 222 | } 223 | 224 | #[test] 225 | fn stream_iterator_collects_all_chunks() { 226 | use crate::execution::stream::{ProcessStream, StreamChunk}; 227 | use std::thread; 228 | 229 | let (writer, reader) = ProcessStream::new(); 230 | 231 | thread::spawn(move || { 232 | writer.send_stdout("line1".to_string()).unwrap(); 233 | writer.send_stdout("line2".to_string()).unwrap(); 234 | writer.send_stderr("error".to_string()).unwrap(); 235 | writer.send_exit(0, None).unwrap(); 236 | }); 237 | 238 | let chunks: Vec<_> = reader.into_iter().collect(); 239 | 240 | assert_eq!(chunks.len(), 4); 241 | match &chunks[0] { 242 | StreamChunk::Stdout(s) => assert_eq!(s, "line1"), 243 | _ => panic!(), 244 | } 245 | match &chunks[1] { 246 | StreamChunk::Stdout(s) => assert_eq!(s, "line2"), 247 | _ => panic!(), 248 | } 249 | match &chunks[2] { 250 | StreamChunk::Stderr(s) => assert_eq!(s, "error"), 251 | _ => panic!(), 252 | } 253 | match &chunks[3] { 254 | StreamChunk::Exit { exit_code, signal } => { 255 | assert_eq!(*exit_code, 0); 256 | assert_eq!(*signal, None); 257 | } 258 | _ => panic!(), 259 | } 260 | } 261 | 262 | #[test] 263 | fn stream_multiple_stdout_messages() { 264 | use crate::execution::stream::{ProcessStream, StreamChunk}; 265 | 266 | let (writer, reader) = ProcessStream::new(); 267 | 268 | writer.send_stdout("msg1".to_string()).unwrap(); 269 | writer.send_stdout("msg2".to_string()).unwrap(); 270 | writer.send_stdout("msg3".to_string()).unwrap(); 271 | 272 | let chunk1 = reader.recv().unwrap().unwrap(); 273 | let chunk2 = reader.recv().unwrap().unwrap(); 274 | let chunk3 = reader.recv().unwrap().unwrap(); 275 | 276 | match chunk1 { 277 | StreamChunk::Stdout(s) => assert_eq!(s, "msg1"), 278 | _ => panic!(), 279 | } 280 | match chunk2 { 281 | StreamChunk::Stdout(s) => assert_eq!(s, "msg2"), 282 | _ => panic!(), 283 | } 284 | match chunk3 { 285 | StreamChunk::Stdout(s) => assert_eq!(s, "msg3"), 286 | _ => panic!(), 287 | } 288 | } 289 | 290 | #[test] 291 | fn stream_interleaved_stdout_stderr() { 292 | use crate::execution::stream::{ProcessStream, StreamChunk}; 293 | 294 | let (writer, reader) = ProcessStream::new(); 295 | 296 | writer.send_stdout("out1".to_string()).unwrap(); 297 | writer.send_stderr("err1".to_string()).unwrap(); 298 | writer.send_stdout("out2".to_string()).unwrap(); 299 | writer.send_stderr("err2".to_string()).unwrap(); 300 | 301 | let chunks: Vec<_> = (0..4).map(|_| reader.recv().unwrap().unwrap()).collect(); 302 | 303 | assert_eq!(chunks.len(), 4); 304 | 305 | match &chunks[0] { 306 | StreamChunk::Stdout(s) => assert_eq!(s, "out1"), 307 | _ => panic!(), 308 | } 309 | match &chunks[1] { 310 | StreamChunk::Stderr(s) => assert_eq!(s, "err1"), 311 | _ => panic!(), 312 | } 313 | match &chunks[2] { 314 | StreamChunk::Stdout(s) => assert_eq!(s, "out2"), 315 | _ => panic!(), 316 | } 317 | match &chunks[3] { 318 | StreamChunk::Stderr(s) => assert_eq!(s, "err2"), 319 | _ => panic!(), 320 | } 321 | } 322 | 323 | #[test] 324 | fn realtime_streaming_returns_immediately() { 325 | use crate::execution::{ProcessExecutor, stream::StreamChunk}; 326 | let _guard = serial_guard(); 327 | 328 | let config = ProcessConfig { 329 | program: "/bin/bash".to_string(), 330 | args: vec![ 331 | "-c".to_string(), 332 | "echo 'chunk1'; sleep 0.1; echo 'chunk2'".to_string(), 333 | ], 334 | ..Default::default() 335 | }; 336 | 337 | let namespace = no_isolation_namespace(); 338 | let start = std::time::Instant::now(); 339 | 340 | let (_result, stream_opt) = 341 | ProcessExecutor::execute_with_stream(config, namespace, true).unwrap(); 342 | 343 | let elapsed_at_return = start.elapsed(); 344 | 345 | assert!( 346 | elapsed_at_return.as_millis() < 50, 347 | "execute_with_stream should return immediately, took {}ms", 348 | elapsed_at_return.as_millis() 349 | ); 350 | 351 | assert!(stream_opt.is_some(), "Stream should be available"); 352 | 353 | let stream = stream_opt.unwrap(); 354 | 355 | let mut chunks = Vec::new(); 356 | let mut exit_received = false; 357 | let timeout = std::time::Instant::now() + std::time::Duration::from_secs(5); 358 | 359 | while std::time::Instant::now() < timeout { 360 | match stream.try_recv() { 361 | Ok(Some(chunk)) => match &chunk { 362 | StreamChunk::Exit { .. } => { 363 | exit_received = true; 364 | chunks.push(chunk); 365 | break; 366 | } 367 | _ => chunks.push(chunk), 368 | }, 369 | Ok(None) => { 370 | std::thread::sleep(std::time::Duration::from_millis(10)); 371 | continue; 372 | } 373 | Err(_) => break, 374 | } 375 | } 376 | 377 | assert!(exit_received, "Should receive exit chunk"); 378 | assert!(chunks.len() > 1, "Should have received output before exit"); 379 | } 380 | --------------------------------------------------------------------------------