1use super::config::{Capability, SandboxConfig};
9#[allow(unused_imports)]
10use wasmi::{Caller, Engine, Extern, Func, Instance, Linker, Memory, Module, Store};
11
12#[derive(Debug, Clone, thiserror::Error)]
18pub enum SandboxError {
19 #[error("invalid WASM module: {0}")]
21 ModuleInvalid(String),
22
23 #[error("module instantiation failed: {0}")]
25 InstantiationFailed(String),
26
27 #[error("execution failed: {0}")]
29 ExecutionFailed(String),
30
31 #[error("fuel/instruction budget exhausted")]
33 OutOfFuel,
34
35 #[error("memory limit exceeded")]
37 OutOfMemory,
38
39 #[error("execution timed out")]
41 Timeout,
42
43 #[error("capability denied: {0}")]
45 CapabilityDenied(String),
46
47 #[error("host function error: {0}")]
49 HostError(String),
50}
51
52#[derive(Debug, Clone)]
58pub struct ExecutionResult {
59 pub output: Vec<u8>,
61 pub fuel_consumed: u64,
63 pub memory_peak_bytes: usize,
65}
66
67pub struct HostState {
76 pub stdout: Vec<u8>,
78 pub stderr: Vec<u8>,
80 pub output: Vec<u8>,
82 pub input: Vec<u8>,
84 pub capabilities: Vec<Capability>,
86 pub memory_peak: usize,
88}
89
90pub struct WasmRuntime {
100 engine: Engine,
101}
102
103impl WasmRuntime {
104 pub fn new() -> Self {
106 let mut config = wasmi::Config::default();
107 config.consume_fuel(true);
108 let engine = Engine::new(&config);
109 Self { engine }
110 }
111
112 pub fn validate_module(&self, wasm_bytes: &[u8]) -> Result<(), SandboxError> {
117 Module::new(&self.engine, wasm_bytes)
118 .map(|_| ())
119 .map_err(|e| SandboxError::ModuleInvalid(e.to_string()))
120 }
121
122 pub fn execute(
141 &self,
142 wasm_bytes: &[u8],
143 input: &[u8],
144 config: &SandboxConfig,
145 ) -> Result<ExecutionResult, SandboxError> {
146 let module = Module::new(&self.engine, wasm_bytes)
148 .map_err(|e| SandboxError::ModuleInvalid(e.to_string()))?;
149
150 let host_state = HostState {
152 stdout: Vec::new(),
153 stderr: Vec::new(),
154 output: Vec::new(),
155 input: input.to_vec(),
156 capabilities: config.capabilities.clone(),
157 memory_peak: 0,
158 };
159 let mut store = Store::new(&self.engine, host_state);
160 store
161 .set_fuel(config.resource_limits.max_fuel)
162 .map_err(|e| SandboxError::ExecutionFailed(e.to_string()))?;
163
164 let mut linker = <Linker<HostState>>::new(&self.engine);
166 Self::register_host_functions(&mut linker)?;
167
168 let instance: Instance =
170 linker
171 .instantiate_and_start(&mut store, &module)
172 .map_err(|e: wasmi::Error| {
173 let msg = e.to_string();
174 if msg.contains("fuel") {
175 SandboxError::OutOfFuel
176 } else {
177 SandboxError::InstantiationFailed(msg)
178 }
179 })?;
180
181 if let Ok(func) = instance.get_typed_func::<(i32, i32), i32>(&store, "execute") {
184 func.call(&mut store, (0, input.len() as i32))
185 .map_err(map_wasmi_error)?;
186 } else if let Ok(func) = instance.get_typed_func::<(), ()>(&store, "_start") {
187 func.call(&mut store, ()).map_err(map_wasmi_error)?;
188 } else {
189 return Err(SandboxError::ExecutionFailed(
190 "module exports neither '_start' nor 'execute' function".to_string(),
191 ));
192 }
193
194 let fuel_remaining = store.get_fuel().unwrap_or(0);
196 let fuel_consumed = config
197 .resource_limits
198 .max_fuel
199 .saturating_sub(fuel_remaining);
200
201 let memory_peak_bytes: usize = instance
202 .get_export(&store, "memory")
203 .and_then(Extern::into_memory)
204 .map(|m: Memory| m.data(&store).len())
205 .unwrap_or(0);
206
207 let host_peak = store.data().memory_peak;
208 let output = std::mem::take(&mut store.data_mut().output);
209
210 Ok(ExecutionResult {
211 output,
212 fuel_consumed,
213 memory_peak_bytes: std::cmp::max(memory_peak_bytes, host_peak),
214 })
215 }
216
217 fn register_host_functions(linker: &mut Linker<HostState>) -> Result<(), SandboxError> {
228 linker
230 .func_wrap(
231 "env",
232 "host_log",
233 |caller: Caller<'_, HostState>, ptr: i32, len: i32| {
234 let mem = match caller.get_export("memory").and_then(|e| e.into_memory()) {
235 Some(m) => m,
236 None => return,
237 };
238 let (ptr, len) = (ptr as usize, len as usize);
239 let data = mem.data(&caller);
240 if let Some(end) = ptr.checked_add(len)
241 && end <= data.len()
242 && let Ok(msg) = std::str::from_utf8(&data[ptr..end])
243 {
244 tracing::debug!(target: "wasm_guest", "{}", msg);
245 }
246 },
247 )
248 .map_err(|e| SandboxError::InstantiationFailed(e.to_string()))?;
249
250 linker
252 .func_wrap(
253 "env",
254 "host_write_output",
255 |mut caller: Caller<'_, HostState>, ptr: i32, len: i32| {
256 let mem = match caller.get_export("memory").and_then(|e| e.into_memory()) {
257 Some(m) => m,
258 None => return,
259 };
260 let (ptr, len) = (ptr as usize, len as usize);
261 let data = mem.data(&caller);
262 let bytes = match ptr.checked_add(len) {
263 Some(end) if end <= data.len() => data[ptr..end].to_vec(),
264 _ => return,
265 };
266 caller.data_mut().output.extend_from_slice(&bytes);
267 },
268 )
269 .map_err(|e| SandboxError::InstantiationFailed(e.to_string()))?;
270
271 linker
273 .func_wrap(
274 "env",
275 "host_read_input",
276 |mut caller: Caller<'_, HostState>, ptr: i32, len: i32| -> i32 {
277 let input_bytes = caller.data().input.clone();
278 let mem = match caller.get_export("memory").and_then(|e| e.into_memory()) {
279 Some(m) => m,
280 None => return 0,
281 };
282 let (ptr, len) = (ptr as usize, len as usize);
283 let to_copy = std::cmp::min(len, input_bytes.len());
284 let mem_data = mem.data_mut(&mut caller);
285 match ptr.checked_add(to_copy) {
286 Some(end) if end <= mem_data.len() => {
287 mem_data[ptr..end].copy_from_slice(&input_bytes[..to_copy]);
288 to_copy as i32
289 }
290 _ => 0,
291 }
292 },
293 )
294 .map_err(|e| SandboxError::InstantiationFailed(e.to_string()))?;
295
296 linker
298 .func_wrap(
299 "env",
300 "host_get_input_len",
301 |caller: Caller<'_, HostState>| -> i32 { caller.data().input.len() as i32 },
302 )
303 .map_err(|e| SandboxError::InstantiationFailed(e.to_string()))?;
304
305 Ok(())
306 }
307}
308
309impl Default for WasmRuntime {
310 fn default() -> Self {
311 Self::new()
312 }
313}
314
315fn map_wasmi_error(err: wasmi::Error) -> SandboxError {
321 let msg = err.to_string();
322 if msg.contains("fuel") {
323 SandboxError::OutOfFuel
324 } else {
325 SandboxError::ExecutionFailed(msg)
326 }
327}
328
329#[cfg(test)]
334mod tests {
335 use super::*;
336
337 fn default_config() -> SandboxConfig {
339 SandboxConfig::new()
340 .with_fuel_limit(1_000_000)
341 .allow_host_calls()
342 }
343
344 #[test]
347 fn test_runtime_creation() {
348 let _runtime = WasmRuntime::new();
349 }
350
351 #[test]
354 fn test_validate_valid_module() {
355 let runtime = WasmRuntime::new();
356 let wat = b"(module (func (export \"_start\")))";
357 assert!(runtime.validate_module(wat).is_ok());
358 }
359
360 #[test]
361 fn test_validate_invalid_module() {
362 let runtime = WasmRuntime::new();
363 let invalid = b"this is definitely not valid wasm";
364 let result = runtime.validate_module(invalid);
365 assert!(result.is_err());
366 match result {
367 Err(SandboxError::ModuleInvalid(_)) => {}
368 other => panic!("expected ModuleInvalid, got {:?}", other),
369 }
370 }
371
372 #[test]
375 fn test_execute_simple_module() {
376 let runtime = WasmRuntime::new();
377 let wat = b"(module (func (export \"_start\")))";
378 let config = default_config();
379 let result = runtime.execute(wat, &[], &config).unwrap();
380 assert!(result.output.is_empty());
381 }
382
383 #[test]
384 fn test_execute_module_with_output() {
385 let runtime = WasmRuntime::new();
386 let wat = br#"
387 (module
388 (import "env" "host_write_output" (func $write (param i32 i32)))
389 (memory (export "memory") 1)
390 (data (i32.const 0) "hello")
391 (func (export "_start")
392 i32.const 0
393 i32.const 5
394 call $write
395 )
396 )
397 "#;
398 let config = default_config();
399 let result = runtime.execute(wat, &[], &config).unwrap();
400 assert_eq!(result.output, b"hello");
401 }
402
403 #[test]
404 fn test_execute_fuel_limit() {
405 let runtime = WasmRuntime::new();
406 let wat = br#"
407 (module
408 (func (export "_start")
409 (local $i i32)
410 (loop $loop
411 (local.set $i (i32.add (local.get $i) (i32.const 1)))
412 (br_if $loop (i32.lt_u (local.get $i) (i32.const 999999999)))
413 )
414 )
415 )
416 "#;
417 let config = SandboxConfig::new()
418 .with_fuel_limit(1_000)
419 .allow_host_calls();
420 let result = runtime.execute(wat, &[], &config);
421 assert!(
422 matches!(result, Err(SandboxError::OutOfFuel)),
423 "expected OutOfFuel, got {:?}",
424 result,
425 );
426 }
427
428 #[test]
429 fn test_execute_reads_input() {
430 let runtime = WasmRuntime::new();
431 let wat = br#"
432 (module
433 (import "env" "host_get_input_len" (func $get_len (result i32)))
434 (import "env" "host_read_input" (func $read (param i32 i32) (result i32)))
435 (import "env" "host_write_output" (func $write (param i32 i32)))
436 (memory (export "memory") 1)
437 (func (export "_start")
438 (local $len i32)
439 (local $read_len i32)
440 (local.set $len (call $get_len))
441 (local.set $read_len (call $read (i32.const 0) (local.get $len)))
442 (call $write (i32.const 0) (local.get $read_len))
443 )
444 )
445 "#;
446 let config = default_config();
447 let input = b"world";
448 let result = runtime.execute(wat, input, &config).unwrap();
449 assert_eq!(result.output, b"world");
450 }
451
452 #[test]
453 fn test_fuel_consumption_tracked() {
454 let runtime = WasmRuntime::new();
455 let wat = b"(module (func (export \"_start\") nop))";
456 let config = default_config();
457 let result = runtime.execute(wat, &[], &config).unwrap();
458 assert!(
459 result.fuel_consumed > 0,
460 "fuel_consumed should be non-zero after execution",
461 );
462 }
463}