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}