1use std::sync::Arc;
8use std::time::Instant;
9
10use super::config::{Capability, SandboxConfig};
11use super::runtime::{ExecutionResult, SandboxError, WasmRuntime};
12
13pub struct SandboxedExecutor {
19 runtime: Arc<WasmRuntime>,
20 default_config: SandboxConfig,
21}
22
23impl SandboxedExecutor {
24 pub fn new(runtime: Arc<WasmRuntime>, default_config: SandboxConfig) -> Self {
26 Self {
27 runtime,
28 default_config,
29 }
30 }
31
32 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 pub fn runtime(&self) -> &WasmRuntime {
43 &self.runtime
44 }
45
46 pub fn default_config(&self) -> &SandboxConfig {
48 &self.default_config
49 }
50
51 pub fn validate(&self, wasm_bytes: &[u8]) -> Result<(), SandboxError> {
53 self.runtime.validate_module(wasm_bytes)
54 }
55
56 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 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 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#[derive(Debug, Clone)]
107pub struct SandboxExecution {
108 pub result: ExecutionResult,
110 pub wall_time_ms: u64,
112 pub config_snapshot: ConfigSnapshot,
114}
115
116impl SandboxExecution {
117 pub fn output(&self) -> &[u8] {
119 &self.result.output
120 }
121
122 pub fn output_str(&self) -> Option<&str> {
124 std::str::from_utf8(&self.result.output).ok()
125 }
126
127 pub fn fuel_consumed(&self) -> u64 {
129 self.result.fuel_consumed
130 }
131
132 pub fn memory_peak_bytes(&self) -> usize {
134 self.result.memory_peak_bytes
135 }
136
137 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#[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); }
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}