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}