Skip to main content

vanta_provider/
wasm.rs

1//! The sandboxed execution primitive for WASM provider hooks
2//! (`docs/22-provider-sdk.md`, `docs/15-security.md`).
3//!
4//! Guest modules run under Wasmtime with **no ambient authority** — no WASI, no
5//! host imports are provided unless explicitly granted — and with a **fuel
6//! budget** so a malicious or runaway hook traps instead of hanging the host.
7//! This is the security core of the provider model; the full component-model WIT
8//! ABI (scoped `http-get`, `hash`) builds on this primitive.
9
10use vanta_core::{Area, VtaError, VtaResult};
11use wasmtime::{Config, Engine, Instance, Module, Store};
12
13/// A capability-free WASM sandbox.
14pub struct Sandbox {
15    engine: Engine,
16}
17
18impl Sandbox {
19    /// Create a sandbox with fuel metering enabled and no host capabilities.
20    pub fn new() -> VtaResult<Sandbox> {
21        let mut config = Config::new();
22        config.consume_fuel(true);
23        let engine = Engine::new(&config).map_err(|e| err(format!("engine: {e}")))?;
24        Ok(Sandbox { engine })
25    }
26
27    /// Run an exported `func(i32) -> i32` in a fresh, capability-free instance
28    /// under a fuel budget. Instantiation fails if the module imports anything
29    /// (no ambient authority is granted); the call traps cleanly if it exhausts
30    /// its fuel. Compute-only hooks use this; richer hooks extend the host set.
31    pub fn run_i32(&self, wasm: &[u8], func: &str, arg: i32, fuel: u64) -> VtaResult<i32> {
32        let module = Module::new(&self.engine, wasm).map_err(|e| err(format!("compile: {e}")))?;
33        let mut store = Store::new(&self.engine, ());
34        store
35            .set_fuel(fuel)
36            .map_err(|e| err(format!("set fuel: {e}")))?;
37        // Empty import list: a module requiring any import cannot instantiate.
38        let instance = Instance::new(&mut store, &module, &[])
39            .map_err(|e| err(format!("instantiate (no capabilities granted): {e}")))?;
40        let typed = instance
41            .get_typed_func::<i32, i32>(&mut store, func)
42            .map_err(|e| err(format!("export `{func}`: {e}")))?;
43        typed
44            .call(&mut store, arg)
45            .map_err(|e| err(format!("guest trap: {e}")))
46    }
47}
48
49fn err(msg: String) -> VtaError {
50    VtaError::new(Area::Prov, 1, msg)
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    // A compute-only module: (func (export "double") (param i32) (result i32) ...).
58    const DOUBLE: &str = r#"(module
59        (func (export "double") (param i32) (result i32)
60            local.get 0
61            i32.const 2
62            i32.mul))"#;
63
64    // A module that loops forever — must trap on fuel exhaustion, not hang.
65    const SPIN: &str = r#"(module
66        (func (export "spin") (param i32) (result i32)
67            (loop (br 0))
68            i32.const 0))"#;
69
70    // A module that imports a host function it is not granted — must fail to
71    // instantiate (no ambient authority).
72    const NEEDS_IMPORT: &str = r#"(module
73        (import "env" "secret" (func $secret (result i32)))
74        (func (export "go") (param i32) (result i32) (call $secret)))"#;
75
76    #[test]
77    fn runs_pure_compute() {
78        let wasm = wat::parse_str(DOUBLE).unwrap();
79        let sb = Sandbox::new().unwrap();
80        assert_eq!(sb.run_i32(&wasm, "double", 21, 1_000_000).unwrap(), 42);
81    }
82
83    #[test]
84    fn fuel_exhaustion_traps_not_hangs() {
85        let wasm = wat::parse_str(SPIN).unwrap();
86        let sb = Sandbox::new().unwrap();
87        let err = sb.run_i32(&wasm, "spin", 0, 10_000).unwrap_err();
88        assert_eq!(err.area, Area::Prov); // trapped (out of fuel), did not hang
89    }
90
91    #[test]
92    fn imports_are_denied() {
93        let wasm = wat::parse_str(NEEDS_IMPORT).unwrap();
94        let sb = Sandbox::new().unwrap();
95        // No host functions are provided, so instantiation must fail.
96        assert!(sb.run_i32(&wasm, "go", 0, 1_000_000).is_err());
97    }
98}