Skip to main content

rustant_tools/sandbox/
mod.rs

1//! Universal WASM-Based Sandboxing for Rustant.
2//!
3//! Provides a sandboxed execution environment using WebAssembly (via the `wasmi`
4//! interpreter) with capability-based permissions and resource limits. WASM modules
5//! execute in a fully isolated environment with controlled access to host resources.
6//!
7//! ## Architecture
8//!
9//! ```text
10//! SandboxedExecutor
11//!     │
12//!     ├── WasmRuntime (wasmi Engine)
13//!     │       ├── Module compilation & validation
14//!     │       ├── Fuel metering (CPU budget)
15//!     │       └── Memory limits
16//!     │
17//!     ├── HostState (guest ↔ host bridge)
18//!     │       ├── Input/output buffers
19//!     │       ├── Stdout/stderr capture
20//!     │       └── Capability checks
21//!     │
22//!     └── SandboxConfig
23//!             ├── ResourceLimits (memory, fuel, time)
24//!             └── Capabilities (FileRead, FileWrite, Network, etc.)
25//! ```
26
27pub mod config;
28pub mod executor;
29pub mod host;
30pub mod runtime;
31
32// Re-export primary types for convenient access.
33pub use config::{Capability, ResourceLimits, SandboxConfig};
34pub use executor::{SandboxExecution, SandboxedExecutor};
35pub use runtime::{ExecutionResult, SandboxError, WasmRuntime};
36
37use std::sync::Arc;
38
39/// Create a [`SandboxedExecutor`] with default settings.
40///
41/// This is a convenience function that creates a shared [`WasmRuntime`] and
42/// wraps it in a [`SandboxedExecutor`] with default resource limits.
43pub fn create_sandbox() -> SandboxedExecutor {
44    SandboxedExecutor::with_defaults()
45}
46
47/// Create a [`SandboxedExecutor`] with a custom configuration.
48pub fn create_sandbox_with_config(config: SandboxConfig) -> SandboxedExecutor {
49    let runtime = Arc::new(WasmRuntime::new());
50    SandboxedExecutor::new(runtime, config)
51}
52
53/// Validate a WASM module without executing it.
54///
55/// Returns `Ok(())` if the bytes represent a valid WASM module,
56/// or `Err(SandboxError::ModuleInvalid)` otherwise.
57pub fn validate_module(wasm_bytes: &[u8]) -> Result<(), SandboxError> {
58    let runtime = WasmRuntime::new();
59    runtime.validate_module(wasm_bytes)
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_create_sandbox() {
68        let executor = create_sandbox();
69        assert_eq!(
70            executor.default_config().resource_limits.max_fuel,
71            ResourceLimits::default().max_fuel
72        );
73    }
74
75    #[test]
76    fn test_create_sandbox_with_config() {
77        let config = SandboxConfig::new().with_fuel_limit(42);
78        let executor = create_sandbox_with_config(config);
79        assert_eq!(executor.default_config().resource_limits.max_fuel, 42);
80    }
81
82    #[test]
83    fn test_validate_valid_module() {
84        let wat = br#"(module (func (export "_start")))"#;
85        assert!(validate_module(wat).is_ok());
86    }
87
88    #[test]
89    fn test_validate_invalid_module() {
90        assert!(validate_module(b"garbage data").is_err());
91    }
92
93    #[test]
94    fn test_end_to_end_simple_execution() {
95        let executor = create_sandbox();
96        let wat = br#"(module (func (export "_start")))"#;
97        let result = executor.execute(wat, b"").unwrap();
98        assert!(result.output().is_empty());
99        assert!(result.fuel_consumed() > 0);
100    }
101
102    #[test]
103    fn test_end_to_end_with_output() {
104        let executor = create_sandbox();
105        let wat = br#"
106            (module
107                (import "env" "host_write_output" (func $write (param i32 i32)))
108                (memory (export "memory") 1)
109                (data (i32.const 0) "sandboxed")
110                (func (export "_start")
111                    i32.const 0
112                    i32.const 9
113                    call $write
114                )
115            )
116        "#;
117        let result = executor.execute(wat, b"").unwrap();
118        assert_eq!(result.output_str(), Some("sandboxed"));
119    }
120
121    #[test]
122    fn test_end_to_end_fuel_exhaustion() {
123        let config = SandboxConfig::new().with_fuel_limit(50);
124        let executor = create_sandbox_with_config(config);
125        let wat = br#"
126            (module
127                (func (export "_start")
128                    (local $i i32)
129                    (loop $loop
130                        (local.set $i (i32.add (local.get $i) (i32.const 1)))
131                        (br_if $loop (i32.lt_u (local.get $i) (i32.const 999999)))
132                    )
133                )
134            )
135        "#;
136        let err = executor.execute(wat, b"").unwrap_err();
137        assert!(matches!(err, SandboxError::OutOfFuel));
138    }
139
140    #[test]
141    fn test_end_to_end_input_output() {
142        let executor = create_sandbox();
143        let wat = br#"
144            (module
145                (import "env" "host_get_input_len" (func $input_len (result i32)))
146                (import "env" "host_read_input" (func $read (param i32 i32) (result i32)))
147                (import "env" "host_write_output" (func $write (param i32 i32)))
148                (memory (export "memory") 1)
149                (func (export "_start")
150                    (local $len i32)
151                    (local.set $len (call $input_len))
152                    (drop (call $read (i32.const 0) (local.get $len)))
153                    (call $write (i32.const 0) (local.get $len))
154                )
155            )
156        "#;
157        let result = executor.execute(wat, b"echo-test").unwrap();
158        assert_eq!(result.output_str(), Some("echo-test"));
159    }
160
161    #[test]
162    fn test_capabilities_in_config() {
163        let config = SandboxConfig::new()
164            .with_capability(Capability::Stdout)
165            .with_capability(Capability::Stderr)
166            .with_capability(Capability::FileRead(vec!["/tmp".into()]));
167
168        let executor = create_sandbox_with_config(config);
169        assert_eq!(executor.default_config().capabilities.len(), 3);
170    }
171
172    #[test]
173    fn test_resource_limits_applied() {
174        let config = SandboxConfig::new()
175            .with_fuel_limit(100_000)
176            .with_memory_limit(4 * 1024 * 1024);
177
178        let executor = create_sandbox_with_config(config);
179
180        let wat = br#"(module (func (export "_start")))"#;
181        let result = executor.execute(wat, b"").unwrap();
182        assert!(result.within_limits());
183    }
184}