Skip to main content

submilli_wasm/module/
mod.rs

1//! `Module` — parse + validate + compile a wasm binary to internal bytecode.
2
3pub(crate) mod code;
4pub(crate) mod compile;
5pub(crate) mod const_expr;
6pub(crate) mod debug;
7pub(crate) mod handler;
8pub(crate) mod inner;
9pub(crate) mod op;
10#[cfg(feature = "simd")]
11pub(crate) mod op_simd;
12mod op_types;
13pub(crate) mod parse;
14mod serialize;
15mod typesec;
16
17use std::path::Path;
18use std::sync::Arc;
19
20use wasmparser::{Validator, WasmFeatures};
21
22use crate::engine::Engine;
23use crate::module::inner::ModuleInner;
24use crate::value::{ExportType, ExternType, ImportType};
25use crate::{Error, Result};
26
27/// The set of WebAssembly proposals our interpreter currently enables. GC/exceptions/
28/// SIMD/threads are intentionally off until implemented (and will be `Config`-driven later).
29pub(crate) fn enabled_features() -> WasmFeatures {
30    WasmFeatures::MUTABLE_GLOBAL
31        | WasmFeatures::SIGN_EXTENSION
32        | WasmFeatures::MULTI_VALUE
33        | WasmFeatures::BULK_MEMORY
34        | WasmFeatures::SATURATING_FLOAT_TO_INT
35        | WasmFeatures::FLOATS
36        | WasmFeatures::REFERENCE_TYPES
37        // `externref` (the `extern` heap type) is gated behind GC_TYPES in wasmparser
38        // even though it's a reference-types value.
39        | WasmFeatures::GC_TYPES
40        // Typed/non-nullable refs, `call_ref`, `ref.as_non_null`, `br_on_null`/`br_on_non_null`.
41        | WasmFeatures::FUNCTION_REFERENCES
42        // Tail calls: `return_call`/`return_call_indirect`/`return_call_ref` (#39).
43        | WasmFeatures::TAIL_CALL
44        // Full GC: struct/array type definitions, rec groups, sub/final. The aggregate
45        // instructions + casts are deferred — validated modules using them skip in the harness.
46        | WasmFeatures::GC
47        // Exception handling (`exnref` + `try_table`). Tags + their imports/exports are decoded
48        // here (#28a); the `throw`/`throw_ref`/`try_table` instructions are deferred — validated
49        // modules using them skip in the harness until #28c–#28e. Legacy `try/catch/delegate`
50        // (`LEGACY_EXCEPTIONS`) stays off.
51        | WasmFeatures::EXCEPTIONS
52        // Const-expr `i32`/`i64` `add`/`sub`/`mul` + `global.get` of prior immutable globals (#40).
53        | WasmFeatures::EXTENDED_CONST
54        // More than one memory per module; every memory op carries an explicit index (#41).
55        | WasmFeatures::MULTI_MEMORY
56        // 64-bit memories and tables (`i64` index type); one flag covers both (#42).
57        | WasmFeatures::MEMORY64
58        // Fixed-width SIMD (`v128`), gated behind the `simd` Cargo feature (#37).
59        | simd_features()
60}
61
62#[cfg(feature = "simd")]
63fn simd_features() -> WasmFeatures {
64    // Relaxed SIMD (#38) builds on v128, so it rides the same `simd` feature.
65    WasmFeatures::SIMD | WasmFeatures::RELAXED_SIMD
66}
67
68#[cfg(not(feature = "simd"))]
69fn simd_features() -> WasmFeatures {
70    WasmFeatures::empty()
71}
72
73/// Default validation-time module-size ceiling (bytes) — the **untrusted tier** (#32). Generous
74/// for real guest modules, finite for safety: the multi-tenant threat model forbids an unbounded
75/// default (a hostile module must not be able to OOM the compiler). Tunable via
76/// [`crate::Config::max_module_bytes`]; raised per-module for trusted packages via
77/// [`Module::new_with_limits`].
78pub(crate) const DEFAULT_MAX_MODULE_BYTES: usize = 256 << 20;
79
80/// Default aggregate expanded-locals ceiling. Real modules declare ~7 locals per KiB of binary
81/// (measured on SpiderMonkey, sharp, astro), so a 256 MiB module lands under 2M; this leaves ~4×
82/// headroom while capping the arena a hostile module can demand at tens of MiB.
83pub(crate) const DEFAULT_MAX_EXPANDED_LOCALS: usize = 1 << 23;
84
85/// Per-module validation-time complexity limits (#32). Bounds compiler memory so a hostile
86/// *module* can't OOM the compiler before it ever executes — layered on top of `wasmparser`'s
87/// hard per-dimension limits (function body size, locals, segment/type/function counts), which
88/// it does not expose for tuning. Each knob is additive: a module must pass every cap.
89///
90/// **Additive deviation from wasmtime** — no analog in `wasmtime`. [`Default`] yields the
91/// untrusted-tier ceilings ([`crate::Config::max_module_bytes`] /
92/// [`crate::Config::max_expanded_locals`]); trusted/curated packages pass higher — but still
93/// finite — limits via [`Module::new_with_limits`].
94#[derive(Clone, Copy, Debug)]
95pub struct ModuleLimits {
96    /// Maximum accepted module binary size, in bytes. Always finite (no unbounded option).
97    pub max_module_bytes: usize,
98    /// Maximum number of locals across *all* functions after run-length expansion. `wasmparser`
99    /// bounds locals per function only; this bounds the module-wide arena they expand into.
100    pub max_expanded_locals: usize,
101}
102
103impl Default for ModuleLimits {
104    fn default() -> Self {
105        ModuleLimits {
106            max_module_bytes: DEFAULT_MAX_MODULE_BYTES,
107            max_expanded_locals: DEFAULT_MAX_EXPANDED_LOCALS,
108        }
109    }
110}
111
112/// A compiled, reusable WebAssembly module. Shareable across stores of the same
113/// engine; cloning is a cheap `Arc` bump.
114#[derive(Clone, Debug)]
115pub struct Module(Arc<ModuleInner>);
116
117impl Module {
118    /// Parses, validates, and compiles a module from a `.wasm` binary.
119    ///
120    /// (Unlike `wasmtime` with its `wat` feature, we accept binary only; convert
121    /// `.wat` with the `wat` crate first.)
122    pub fn new(engine: &Engine, bytes: impl AsRef<[u8]>) -> Result<Module> {
123        Module::from_binary(engine, bytes.as_ref())
124    }
125
126    /// Like [`Module::new`] but accepts binary `.wasm` only.
127    ///
128    /// Validation is fused into the decode/compile pass (see [`parse::parse_module`]), so the
129    /// binary is walked once — not validated by a separate full pass and then re-parsed.
130    pub fn from_binary(engine: &Engine, binary: &[u8]) -> Result<Module> {
131        let inner = parse::parse_module(engine, binary, &engine.module_limits())?;
132        Ok(Module(Arc::new(inner)))
133    }
134
135    /// Like [`Module::new`] but with explicit per-module [`ModuleLimits`] — the entry point for
136    /// **trusted/curated packages** that the embedder allows a higher (but still finite)
137    /// complexity ceiling than the engine's untrusted-tier default (#32).
138    ///
139    /// **Additive deviation from wasmtime** — there is no analog in `wasmtime`.
140    pub fn new_with_limits(
141        engine: &Engine,
142        bytes: impl AsRef<[u8]>,
143        limits: &ModuleLimits,
144    ) -> Result<Module> {
145        let inner = parse::parse_module(engine, bytes.as_ref(), limits)?;
146        Ok(Module(Arc::new(inner)))
147    }
148
149    /// Reads and compiles a module from a file path.
150    pub fn from_file(engine: &Engine, file: impl AsRef<Path>) -> Result<Module> {
151        let bytes = std::fs::read(file).map_err(|e| Error::msg(e.to_string()))?;
152        Module::from_binary(engine, &bytes)
153    }
154
155    /// Serializes this compiled module to the binary artifact format consumed by
156    /// [`Module::deserialize`]. The artifact is the *compiled* form, not the original
157    /// wasm — restoring it skips validation + compilation.
158    pub fn serialize(&self) -> Result<Vec<u8>> {
159        serialize::encode(&self.0)
160    }
161
162    /// Restores a [`Module`] from an artifact produced by [`Module::serialize`] /
163    /// [`Engine::precompile_module`], **without** re-validating or recompiling.
164    ///
165    /// # Safety
166    /// Matches `wasmtime::Module::deserialize`: the artifact must be a trusted blob
167    /// produced by a matching version of this crate (it is not guest-reachable input).
168    /// A corrupted artifact cannot cause undefined behavior here — the interpreter runs
169    /// in safe Rust with bounds-checked dispatch, so the worst case is a trap rather than
170    /// memory unsafety — but cross-version/garbage blobs are rejected up front.
171    ///
172    /// The validation-time module-size ceiling (#32) is intentionally **not** applied here:
173    /// the artifact is a trusted, embedder-only blob (not guest-reachable), matching wasmtime's
174    /// `unsafe` deserialize contract.
175    #[allow(unsafe_code)] // No unsafe operations; `unsafe` is wasmtime API parity only.
176    pub unsafe fn deserialize(engine: &Engine, bytes: impl AsRef<[u8]>) -> Result<Module> {
177        let mut inner = serialize::decode(bytes.as_ref())?;
178        // Canonical type ids are engine-specific — re-intern the (module-relative) artifact
179        // against the target engine.
180        inner.intern(engine);
181        Ok(Module(Arc::new(inner)))
182    }
183
184    /// Like [`Module::deserialize`] but reads the artifact from a file.
185    ///
186    /// # Safety
187    /// See [`Module::deserialize`].
188    #[allow(unsafe_code)] // No unsafe operations; `unsafe` is wasmtime API parity only.
189    pub unsafe fn deserialize_file(engine: &Engine, path: impl AsRef<Path>) -> Result<Module> {
190        let bytes = std::fs::read(path).map_err(|e| Error::msg(e.to_string()))?;
191        let mut inner = serialize::decode(&bytes)?;
192        inner.intern(engine);
193        Ok(Module(Arc::new(inner)))
194    }
195
196    /// Validates a module without compiling it. Applies the engine's untrusted-tier
197    /// module-size ceiling (`Config::max_module_bytes`, #32) before parsing.
198    pub fn validate(engine: &Engine, binary: &[u8]) -> Result<()> {
199        let max_module_bytes = engine.module_limits().max_module_bytes;
200        if binary.len() > max_module_bytes {
201            return Err(Error::msg(format!(
202                "module size {} exceeds configured limit {max_module_bytes}",
203                binary.len(),
204            )));
205        }
206        Validator::new_with_features(enabled_features())
207            .validate_all(binary)
208            .map(|_| ())
209            .map_err(|e| Error::msg(e.to_string()))
210    }
211
212    pub fn imports(&self) -> impl ExactSizeIterator<Item = ImportType<'_>> + '_ {
213        self.0.imports.iter().map(|imp| {
214            ImportType::new(&imp.module, &imp.name, self.0.import_extern_type(&imp.kind))
215        })
216    }
217
218    pub fn exports(&self) -> impl ExactSizeIterator<Item = ExportType<'_>> + '_ {
219        self.0
220            .exports
221            .iter()
222            .map(|e| ExportType::new(&e.name, self.0.export_extern_type(e.kind)))
223    }
224
225    pub fn get_export(&self, name: &str) -> Option<ExternType> {
226        self.0
227            .exports
228            .iter()
229            .find(|e| e.name == name)
230            .map(|e| self.0.export_extern_type(e.kind))
231    }
232
233    /// The module's name from the `name` custom section's module subsection, if present.
234    /// Mirrors `wasmtime::Module::name`.
235    pub fn name(&self) -> Option<&str> {
236        self.0.debug.module_name()
237    }
238
239    /// Internal access to the compiled representation (for instantiation/exec).
240    pub(crate) fn inner(&self) -> &ModuleInner {
241        &self.0
242    }
243
244    /// Runtime handle to the compiled body of a *defined* module-space function index.
245    pub(crate) fn code(&self, module_func_idx: u32) -> code::Code {
246        code::Code {
247            module: self.0.clone(),
248            index: module_func_idx - self.0.num_imported_funcs,
249        }
250    }
251}