Skip to main content

oxide_browser/
engine.rs

1//! WebAssembly engine configuration for Oxide.
2//!
3//! This module configures [Wasmtime](https://wasmtime.dev/) for running guest modules in a
4//! sandboxed environment: bounded linear memory, instruction fuel metering, and a
5//! [`SandboxPolicy`] that gates host capabilities (filesystem, environment variables, network
6//! sockets)—all denied unless explicitly enabled.
7//!
8//! Default [`SandboxPolicy`] limits: **16 MiB** linear memory (256 × 64 KiB pages) and **~500M**
9//! Wasm instructions of fuel per [`Store`] before the guest is halted.
10//!
11//! [`WasmEngine`] owns a shared [`Engine`] plus policy and is the main entry point for creating
12//! stores, bounded memory, and compiled modules. [`ModuleLoader`] is a lighter bundle of engine
13//! plus limits for scenarios such as loading child or dynamically linked modules.
14
15use anyhow::{Context, Result};
16use wasmtime::*;
17
18const MAX_MEMORY_PAGES: u32 = 256; // 256 * 64KB = 16MB
19const FUEL_LIMIT: u64 = 500_000_000; // ~500M instructions before forced halt
20
21/// Policy describing what resources a Wasm guest may use and the hard limits applied at runtime.
22///
23/// Limits (`max_memory_pages`, `fuel_limit`) are enforced when building stores and memory via
24/// [`WasmEngine`]. Capability flags (`allow_*`) express intent for host integrations; by default
25/// all are `false` so filesystem, environment, and network access are denied unless the embedding
26/// layer opts in.
27#[allow(dead_code)]
28#[derive(Clone)]
29pub struct SandboxPolicy {
30    /// Maximum number of 64 KiB Wasm memory pages the guest may grow to (default: 256 → 16 MiB).
31    pub max_memory_pages: u32,
32    /// Maximum Wasm “fuel” (instruction budget) for a single [`Store`] before execution stops.
33    pub fuel_limit: u64,
34    /// When `true`, the embedding may expose filesystem-backed host APIs to the guest.
35    pub allow_filesystem: bool,
36    /// When `true`, the embedding may expose environment variable access to the guest.
37    pub allow_env_vars: bool,
38    /// When `true`, the embedding may expose network socket APIs to the guest.
39    pub allow_network_sockets: bool,
40}
41
42/// Minimal engine + limit bundle for loading additional Wasm modules (e.g. dynamic imports).
43///
44/// Holds a shared [`Engine`] alongside the same memory and fuel caps as [`SandboxPolicy`] so
45/// child modules can be compiled consistently without carrying the full policy struct.
46pub struct ModuleLoader {
47    /// Wasmtime engine instance shared with the parent embedding.
48    pub engine: Engine,
49    /// Upper bound on guest linear memory, in 64 KiB pages.
50    pub max_memory_pages: u32,
51    /// Instruction budget (fuel) aligned with the parent sandbox.
52    pub fuel_limit: u64,
53}
54
55impl Default for SandboxPolicy {
56    /// Returns the default policy: 256 memory pages (16 MiB cap), ~500M instruction fuel, all
57    /// `allow_*` flags `false`.
58    fn default() -> Self {
59        Self {
60            max_memory_pages: MAX_MEMORY_PAGES,
61            fuel_limit: FUEL_LIMIT,
62            allow_filesystem: false,
63            allow_env_vars: false,
64            allow_network_sockets: false,
65        }
66    }
67}
68
69/// Sandbox-aware wrapper around a Wasmtime [`Engine`].
70///
71/// Configures the engine for fuel-metered execution and compiles/instantiates modules according
72/// to the associated [`SandboxPolicy`].
73pub struct WasmEngine {
74    engine: Engine,
75    policy: SandboxPolicy,
76}
77
78impl WasmEngine {
79    /// Builds a [`WasmEngine`] with fuel metering enabled and Cranelift optimizations for speed.
80    ///
81    /// The returned engine is ready to compile modules; per-guest limits come from `policy` when
82    /// calling [`create_store`](Self::create_store) and [`create_bounded_memory`](Self::create_bounded_memory).
83    pub fn new(policy: SandboxPolicy) -> Result<Self> {
84        let mut config = Config::new();
85        config.consume_fuel(true);
86        config.cranelift_opt_level(OptLevel::Speed);
87
88        let engine = Engine::new(&config).context("failed to create wasmtime engine")?;
89        Ok(Self { engine, policy })
90    }
91
92    /// Returns the underlying Wasmtime [`Engine`] for compilation and linking.
93    pub fn engine(&self) -> &Engine {
94        &self.engine
95    }
96
97    /// Returns the active [`SandboxPolicy`] (memory cap, fuel, and capability flags).
98    #[allow(dead_code)]
99    pub fn policy(&self) -> &SandboxPolicy {
100        &self.policy
101    }
102
103    /// Creates a new [`Store`] with `data` as host state and sets fuel to `policy.fuel_limit`.
104    pub fn create_store<T>(&self, data: T) -> Result<Store<T>> {
105        let mut store = Store::new(&self.engine, data);
106        store
107            .set_fuel(self.policy.fuel_limit)
108            .context("failed to set fuel limit")?;
109        Ok(store)
110    }
111
112    /// Allocates a new linear [`Memory`] with minimum 1 page and maximum `policy.max_memory_pages`.
113    pub fn create_bounded_memory(&self, store: &mut Store<impl Send>) -> Result<Memory> {
114        let mem_type = MemoryType::new(1, Some(self.policy.max_memory_pages));
115        Memory::new(store, mem_type).context("failed to create bounded linear memory")
116    }
117
118    /// Compiles raw Wasm bytes into a [`Module`] using this engine’s configuration.
119    pub fn compile_module(&self, wasm_bytes: &[u8]) -> Result<Module> {
120        Module::new(&self.engine, wasm_bytes).context("failed to compile wasm module")
121    }
122}