Skip to main content

rustant_tools/sandbox/
executor.rs

1//! Sandboxed tool executor that wraps tool execution in a WASM sandbox.
2//!
3//! Provides [`SandboxedExecutor`] which can execute WASM modules with
4//! configurable resource limits and capability-based permissions, as well
5//! as sandbox existing tool invocations for additional isolation.
6
7use std::sync::Arc;
8use std::time::Instant;
9
10use super::config::{Capability, SandboxConfig};
11use super::runtime::{ExecutionResult, SandboxError, WasmRuntime};
12
13/// Executes WASM modules within a sandboxed environment.
14///
15/// The executor manages a [`WasmRuntime`] and applies [`SandboxConfig`]
16/// settings to each execution, enforcing resource limits and capability
17/// restrictions.
18pub struct SandboxedExecutor {
19    runtime: Arc<WasmRuntime>,
20    default_config: SandboxConfig,
21}
22
23impl SandboxedExecutor {
24    /// Create a new executor with a shared runtime and default config.
25    pub fn new(runtime: Arc<WasmRuntime>, default_config: SandboxConfig) -> Self {
26        Self {
27            runtime,
28            default_config,
29        }
30    }
31
32    /// Create an executor with default settings.
33    pub fn with_defaults() -> Self {
34        let runtime = Arc::new(WasmRuntime::new());
35        Self {
36            runtime,
37            default_config: SandboxConfig::default(),
38        }
39    }
40
41    /// Get a reference to the underlying runtime.
42    pub fn runtime(&self) -> &WasmRuntime {
43        &self.runtime
44    }
45
46    /// Get the default configuration.
47    pub fn default_config(&self) -> &SandboxConfig {
48        &self.default_config
49    }
50
51    /// Validate a WASM module without executing it.
52    pub fn validate(&self, wasm_bytes: &[u8]) -> Result<(), SandboxError> {
53        self.runtime.validate_module(wasm_bytes)
54    }
55
56    /// Execute a WASM module with the default configuration.
57    pub fn execute(
58        &self,
59        wasm_bytes: &[u8],
60        input: &[u8],
61    ) -> Result<SandboxExecution, SandboxError> {
62        self.execute_with_config(wasm_bytes, input, &self.default_config)
63    }
64
65    /// Execute a WASM module with a specific configuration.
66    pub fn execute_with_config(
67        &self,
68        wasm_bytes: &[u8],
69        input: &[u8],
70        config: &SandboxConfig,
71    ) -> Result<SandboxExecution, SandboxError> {
72        let start = Instant::now();
73
74        let result = self.runtime.execute(wasm_bytes, input, config)?;
75
76        let elapsed = start.elapsed();
77
78        Ok(SandboxExecution {
79            result,
80            wall_time_ms: elapsed.as_millis() as u64,
81            config_snapshot: ConfigSnapshot {
82                max_memory_bytes: config.resource_limits.max_memory_bytes,
83                max_fuel: config.resource_limits.max_fuel,
84                capabilities_count: config.capabilities.len(),
85                host_calls_allowed: config.allow_host_calls,
86            },
87        })
88    }
89
90    /// Execute a WASM module with additional capabilities beyond the default.
91    pub fn execute_with_extra_capabilities(
92        &self,
93        wasm_bytes: &[u8],
94        input: &[u8],
95        extra_caps: Vec<Capability>,
96    ) -> Result<SandboxExecution, SandboxError> {
97        let mut config = self.default_config.clone();
98        for cap in extra_caps {
99            config.capabilities.push(cap);
100        }
101        self.execute_with_config(wasm_bytes, input, &config)
102    }
103}
104
105/// The complete result of a sandboxed execution, including timing and config info.
106#[derive(Debug, Clone)]
107pub struct SandboxExecution {
108    /// The execution result from the WASM runtime.
109    pub result: ExecutionResult,
110    /// Wall-clock time of execution in milliseconds.
111    pub wall_time_ms: u64,
112    /// Snapshot of the configuration used for this execution.
113    pub config_snapshot: ConfigSnapshot,
114}
115
116impl SandboxExecution {
117    /// Get the output bytes from the execution.
118    pub fn output(&self) -> &[u8] {
119        &self.result.output
120    }
121
122    /// Get the output as a UTF-8 string, if valid.
123    pub fn output_str(&self) -> Option<&str> {
124        std::str::from_utf8(&self.result.output).ok()
125    }
126
127    /// Get fuel consumed during execution.
128    pub fn fuel_consumed(&self) -> u64 {
129        self.result.fuel_consumed
130    }
131
132    /// Get peak memory usage in bytes.
133    pub fn memory_peak_bytes(&self) -> usize {
134        self.result.memory_peak_bytes
135    }
136
137    /// Check if the execution was within resource limits.
138    pub fn within_limits(&self) -> bool {
139        self.result.fuel_consumed <= self.config_snapshot.max_fuel
140            && self.result.memory_peak_bytes <= self.config_snapshot.max_memory_bytes
141    }
142}
143
144/// Snapshot of configuration at time of execution.
145#[derive(Debug, Clone)]
146pub struct ConfigSnapshot {
147    pub max_memory_bytes: usize,
148    pub max_fuel: u64,
149    pub capabilities_count: usize,
150    pub host_calls_allowed: bool,
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use crate::sandbox::config::ResourceLimits;
157
158    #[test]
159    fn test_executor_with_defaults() {
160        let executor = SandboxedExecutor::with_defaults();
161        assert_eq!(
162            executor.default_config().resource_limits.max_fuel,
163            ResourceLimits::default().max_fuel
164        );
165    }
166
167    #[test]
168    fn test_executor_custom_config() {
169        let config = SandboxConfig::new()
170            .with_fuel_limit(500_000)
171            .with_memory_limit(8 * 1024 * 1024);
172        let runtime = Arc::new(WasmRuntime::new());
173        let executor = SandboxedExecutor::new(runtime, config);
174
175        assert_eq!(executor.default_config().resource_limits.max_fuel, 500_000);
176        assert_eq!(
177            executor.default_config().resource_limits.max_memory_bytes,
178            8 * 1024 * 1024
179        );
180    }
181
182    #[test]
183    fn test_executor_validate_valid_module() {
184        let executor = SandboxedExecutor::with_defaults();
185        let wat = br#"(module (func (export "_start")))"#;
186        assert!(executor.validate(wat).is_ok());
187    }
188
189    #[test]
190    fn test_executor_validate_invalid_module() {
191        let executor = SandboxedExecutor::with_defaults();
192        assert!(executor.validate(b"not wasm").is_err());
193    }
194
195    #[test]
196    fn test_executor_execute_simple() {
197        let executor = SandboxedExecutor::with_defaults();
198        let wat = br#"(module (func (export "_start")))"#;
199        let result = executor.execute(wat, b"").unwrap();
200
201        assert!(result.output().is_empty());
202        assert!(result.fuel_consumed() > 0);
203        assert!(result.within_limits());
204        assert!(result.wall_time_ms < 5000); // should be fast
205    }
206
207    #[test]
208    fn test_executor_execute_with_output() {
209        let executor = SandboxedExecutor::with_defaults();
210        let wat = br#"
211            (module
212                (import "env" "host_write_output" (func $write (param i32 i32)))
213                (memory (export "memory") 1)
214                (data (i32.const 0) "sandbox-out")
215                (func (export "_start")
216                    i32.const 0
217                    i32.const 11
218                    call $write
219                )
220            )
221        "#;
222        let result = executor.execute(wat, b"").unwrap();
223        assert_eq!(result.output(), b"sandbox-out");
224    }
225
226    #[test]
227    fn test_executor_fuel_exhaustion() {
228        let config = SandboxConfig::new().with_fuel_limit(100);
229        let runtime = Arc::new(WasmRuntime::new());
230        let executor = SandboxedExecutor::new(runtime, config);
231
232        let wat = br#"
233            (module
234                (func (export "_start")
235                    (local $i i32)
236                    (loop $loop
237                        (local.set $i (i32.add (local.get $i) (i32.const 1)))
238                        (br_if $loop (i32.lt_u (local.get $i) (i32.const 999999)))
239                    )
240                )
241            )
242        "#;
243        let err = executor.execute(wat, b"").unwrap_err();
244        assert!(matches!(err, SandboxError::OutOfFuel));
245    }
246
247    #[test]
248    fn test_executor_with_extra_capabilities() {
249        let executor = SandboxedExecutor::with_defaults();
250        let wat = br#"(module (func (export "_start")))"#;
251        let result = executor
252            .execute_with_extra_capabilities(wat, b"", vec![Capability::Stdout])
253            .unwrap();
254        assert!(result.within_limits());
255    }
256
257    #[test]
258    fn test_sandbox_execution_output_str() {
259        let exec = SandboxExecution {
260            result: ExecutionResult {
261                output: b"hello".to_vec(),
262                fuel_consumed: 10,
263                memory_peak_bytes: 1024,
264            },
265            wall_time_ms: 1,
266            config_snapshot: ConfigSnapshot {
267                max_memory_bytes: 16 * 1024 * 1024,
268                max_fuel: 1_000_000,
269                capabilities_count: 0,
270                host_calls_allowed: false,
271            },
272        };
273        assert_eq!(exec.output_str(), Some("hello"));
274        assert!(exec.within_limits());
275    }
276
277    #[test]
278    fn test_sandbox_execution_invalid_utf8() {
279        let exec = SandboxExecution {
280            result: ExecutionResult {
281                output: vec![0xFF, 0xFE],
282                fuel_consumed: 5,
283                memory_peak_bytes: 512,
284            },
285            wall_time_ms: 1,
286            config_snapshot: ConfigSnapshot {
287                max_memory_bytes: 16 * 1024 * 1024,
288                max_fuel: 1_000_000,
289                capabilities_count: 0,
290                host_calls_allowed: false,
291            },
292        };
293        assert!(exec.output_str().is_none());
294    }
295
296    #[test]
297    fn test_config_snapshot_fields() {
298        let config = SandboxConfig::new()
299            .with_fuel_limit(42)
300            .with_memory_limit(1024)
301            .with_capability(Capability::Stdout)
302            .with_capability(Capability::Stderr)
303            .allow_host_calls();
304
305        let runtime = Arc::new(WasmRuntime::new());
306        let executor = SandboxedExecutor::new(runtime, config);
307
308        let wat = br#"(module (func (export "_start")))"#;
309        let result = executor.execute(wat, b"").unwrap();
310
311        assert_eq!(result.config_snapshot.max_fuel, 42);
312        assert_eq!(result.config_snapshot.max_memory_bytes, 1024);
313        assert_eq!(result.config_snapshot.capabilities_count, 2);
314        assert!(result.config_snapshot.host_calls_allowed);
315    }
316}