Skip to main content

ave_core/model/common/
contract.rs

1use std::collections::HashMap;
2
3use wasmtime::{
4    Caller, Config, Engine, Error as WasmError, Linker, StoreLimits,
5    StoreLimitsBuilder,
6};
7
8use thiserror::Error;
9
10use crate::config::MachineSpec;
11
12// ── WasmLimits ────────────────────────────────────────────────────────────────
13
14/// Resolved wasmtime resource limits, ready to configure the engine and stores.
15///
16/// All limits are security upper-bounds. Typical digital-twin contracts use
17/// kilobytes of state — these caps prevent runaway allocations by malicious or
18/// buggy contracts.
19#[derive(Debug, Clone)]
20pub struct WasmLimits {
21    /// Maximum WASM stack depth in bytes. Fixed for security regardless of RAM.
22    pub max_wasm_stack: usize,
23    /// Maximum WASM linear memory per contract instance (demand-paged virtual).
24    pub memory_size: usize,
25    /// Maximum single host-side I/O allocation (state_in, event_in, or result_out).
26    pub max_single_alloc: usize,
27    /// Maximum total host-side I/O buffer per contract call.
28    pub max_total_memory: usize,
29    /// WASM function-table cap. Scales with CPU: more cores → heavier contracts supported.
30    pub max_table_elements: usize,
31    /// Use Cranelift `SpeedAndSize` opt level (true when cpu_cores ≥ 4).
32    /// Produces smaller, faster code at the cost of longer JIT compilation.
33    pub aggressive_compilation: bool,
34}
35
36impl Default for WasmLimits {
37    fn default() -> Self {
38        Self::build(4_096, 2)
39    }
40}
41
42impl WasmLimits {
43    /// Derive resource limits from total machine RAM and CPU cores.
44    ///
45    /// ## Scaling rationale
46    ///
47    /// - **`memory_size`** (WASM linear memory): virtual and demand-paged, so the
48    ///   cost is proportional to pages actually touched, not the cap. Scales from
49    ///   4 MB (Nano) to 32 MB (Large+) to bound worst-case VM RSS per instance.
50    ///
51    /// - **`max_total_memory`** (host I/O): bounds the total byte-transfer between
52    ///   host and WASM per call (state_in + event_in + result_out). Scales from
53    ///   3 MB (Nano) to 24 MB (Medium+).
54    ///
55    /// - **`max_single_alloc`**: cap on a single buffer; ≈ ⅓ of total I/O budget.
56    ///
57    /// - **`max_wasm_stack`**: fixed at 1 MB — a security/correctness bound
58    ///   independent of available RAM.
59    ///
60    /// - **`max_table_elements`** (WASM function table): scales with CPU cores.
61    ///   Each Rust contract uses <50 real entries; this cap prevents runaway
62    ///   tables in adversarial modules. 256 entries per core, floor 512, cap 2 048.
63    ///
64    /// - **`aggressive_compilation`**: enables Cranelift `SpeedAndSize` when
65    ///   cpu_cores ≥ 4. Produces smaller, faster JIT code at the cost of longer
66    ///   compilation time — only worthwhile when spare cores are available.
67    ///
68    /// - **Fuel limits** (`MAX_FUEL`, `MAX_FUEL_COMPILATION`): DOS-prevention
69    ///   constants, machine-independent.
70    pub fn build(ram_mb: u64, cpu_cores: usize) -> Self {
71        // WASM linear memory per instance: floor 4 MB, cap 32 MB.
72        let memory_size = ((ram_mb / 512) as usize)
73            .saturating_mul(4 * 1024 * 1024)
74            .clamp(4 * 1024 * 1024, 32 * 1024 * 1024);
75
76        // Host I/O total per call: floor 3 MB, cap 24 MB.
77        let max_total_memory = ((ram_mb / 512) as usize)
78            .saturating_mul(3 * 1024 * 1024)
79            .clamp(3 * 1024 * 1024, 24 * 1024 * 1024);
80
81        // Single alloc cap ≈ ⅓ of I/O budget: floor 1 MB, cap 8 MB.
82        let max_single_alloc =
83            (max_total_memory / 3).clamp(1024 * 1024, 8 * 1024 * 1024);
84
85        // Function table: 256 entries per core, floor 512, cap 2 048.
86        let max_table_elements = (256 * cpu_cores.max(2)).min(2_048);
87
88        Self {
89            max_wasm_stack: 1024 * 1024, // 1 MB — security bound
90            memory_size,
91            max_single_alloc,
92            max_total_memory,
93            max_table_elements,
94            // SpeedAndSize: slower to compile, faster/smaller output.
95            // Only worthwhile when we have spare cores for the JIT.
96            aggressive_compilation: cpu_cores >= 4,
97        }
98    }
99}
100
101/// Resolve wasmtime resource limits from a [`MachineSpec`]:
102///
103/// - `Profile(p)` → use the profile's canonical RAM and vCPU.
104/// - `Custom { ram_mb, cpu_cores }` → use the supplied values directly.
105/// - `None` → auto-detect total RAM and available CPU cores from the host.
106pub fn resolve_wasm_limits(spec: Option<MachineSpec>) -> WasmLimits {
107    let resolved = crate::config::resolve_spec(spec.as_ref());
108    WasmLimits::build(resolved.ram_mb, resolved.cpu_cores)
109}
110
111#[derive(Debug, Error, Clone)]
112pub enum ContractError {
113    #[error("memory allocation failed: {details}")]
114    MemoryAllocationFailed { details: String },
115
116    #[error("invalid pointer: {pointer}")]
117    InvalidPointer { pointer: usize },
118
119    #[error("write out of bounds: offset {offset} >= allocation size {size}")]
120    WriteOutOfBounds { offset: usize, size: usize },
121
122    #[error("allocation size {size} exceeds maximum of {max} bytes")]
123    AllocationTooLarge { size: usize, max: usize },
124
125    #[error("total memory {total} exceeds maximum of {max} bytes")]
126    TotalMemoryExceeded { total: usize, max: usize },
127
128    #[error("memory allocation would overflow")]
129    AllocationOverflow,
130
131    #[error("linker error [{function}]: {details}")]
132    LinkerError {
133        function: &'static str,
134        details: String,
135    },
136}
137
138#[derive(Debug)]
139pub struct MemoryManager {
140    memory: Vec<u8>,
141    map: HashMap<usize, usize>,
142    pub store_limits: StoreLimits,
143    max_single_alloc: usize,
144    max_total_memory: usize,
145}
146
147impl MemoryManager {
148    /// Create a `MemoryManager` sized according to resolved [`WasmLimits`].
149    pub fn from_limits(limits: &WasmLimits) -> Self {
150        Self {
151            memory: Vec::new(),
152            map: HashMap::new(),
153            // Limits applied to the WASM module's own linear memory and tables.
154            store_limits: StoreLimitsBuilder::new()
155                .memory_size(limits.memory_size)
156                .table_elements(limits.max_table_elements) // scales with cpu_cores
157                .instances(1)
158                .tables(1)
159                .memories(1)
160                .trap_on_grow_failure(true)
161                .build(),
162            max_single_alloc: limits.max_single_alloc,
163            max_total_memory: limits.max_total_memory,
164        }
165    }
166}
167
168impl Default for MemoryManager {
169    fn default() -> Self {
170        Self::from_limits(&WasmLimits::default())
171    }
172}
173
174// Fuel limits for contract execution (~1 fuel unit per WASM instruction)
175pub const MAX_FUEL: u64 = 10_000_000;
176// Compilation/validation limit (one-time cost when new schemas are compiled)
177pub const MAX_FUEL_COMPILATION: u64 = 50_000_000;
178
179impl MemoryManager {
180    pub fn alloc(&mut self, len: usize) -> Result<usize, ContractError> {
181        // Security check: prevent excessive single allocations
182        if len > self.max_single_alloc {
183            return Err(ContractError::AllocationTooLarge {
184                size: len,
185                max: self.max_single_alloc,
186            });
187        }
188
189        let current_len = self.memory.len();
190
191        // Security check: prevent total memory exhaustion
192        let new_len = current_len
193            .checked_add(len)
194            .ok_or(ContractError::AllocationOverflow)?;
195
196        if new_len > self.max_total_memory {
197            return Err(ContractError::TotalMemoryExceeded {
198                total: new_len,
199                max: self.max_total_memory,
200            });
201        }
202
203        self.memory.resize(new_len, 0);
204        self.map.insert(current_len, len);
205        Ok(current_len)
206    }
207
208    pub fn write_byte(
209        &mut self,
210        start_ptr: usize,
211        offset: usize,
212        data: u8,
213    ) -> Result<(), ContractError> {
214        // Security check: validate pointer exists in allocation map
215        let len = self
216            .map
217            .get(&start_ptr)
218            .ok_or(ContractError::InvalidPointer { pointer: start_ptr })?;
219
220        // Security check: validate write is within bounds
221        if offset >= *len {
222            return Err(ContractError::WriteOutOfBounds { offset, size: *len });
223        }
224
225        self.memory[start_ptr + offset] = data;
226        Ok(())
227    }
228
229    pub fn read_byte(&self, ptr: usize) -> Result<u8, ContractError> {
230        if ptr >= self.memory.len() {
231            return Err(ContractError::InvalidPointer { pointer: ptr });
232        }
233        Ok(self.memory[ptr])
234    }
235
236    pub fn read_data(&self, ptr: usize) -> Result<&[u8], ContractError> {
237        let len = self
238            .map
239            .get(&ptr)
240            .ok_or(ContractError::InvalidPointer { pointer: ptr })?;
241        Ok(&self.memory[ptr..ptr + len])
242    }
243
244    pub fn get_pointer_len(&self, ptr: usize) -> isize {
245        let Some(result) = self.map.get(&ptr) else {
246            return -1;
247        };
248        *result as isize
249    }
250
251    pub fn add_data_raw(
252        &mut self,
253        bytes: &[u8],
254    ) -> Result<usize, ContractError> {
255        let ptr = self.alloc(bytes.len())?;
256        for (index, byte) in bytes.iter().enumerate() {
257            self.memory[ptr + index] = *byte;
258        }
259        Ok(ptr)
260    }
261}
262
263/// Creates a secure Wasmtime engine configuration scaled to the given limits.
264///
265/// Shared between contract compilation and execution to ensure consistency.
266/// The engine-level settings (fuel, stack, opt level) are performance/security
267/// constants; only `max_wasm_stack` is taken from the resolved limits.
268pub fn create_secure_wasmtime_config(limits: &WasmLimits) -> Config {
269    let mut config = Config::default();
270
271    // Enable fuel metering for gas-like execution limits.
272    config.consume_fuel(true);
273
274    // Stack depth cap: security bound, derived from resolved limits.
275    config.max_wasm_stack(limits.max_wasm_stack);
276
277    // SpeedAndSize on multi-core machines (≥4 cores): produces smaller, faster
278    // JIT code. On low-core machines use Speed to keep compilation quick.
279    let opt_level = if limits.aggressive_compilation {
280        wasmtime::OptLevel::SpeedAndSize
281    } else {
282        wasmtime::OptLevel::Speed
283    };
284    config.cranelift_opt_level(opt_level);
285
286    config
287}
288
289/// Wasmtime engine and resource limits bundled together.
290///
291/// Stored as a single system helper so actors only need one helper access
292/// instead of two separate lookups for "engine" and "wasm_limits".
293pub struct WasmRuntime {
294    pub engine: Engine,
295    pub limits: WasmLimits,
296}
297
298impl WasmRuntime {
299    /// Build a `WasmRuntime` from an optional [`MachineSpec`].
300    /// Returns an error if the Wasmtime engine cannot be created.
301    pub fn new(spec: Option<MachineSpec>) -> Result<Self, wasmtime::Error> {
302        let limits = resolve_wasm_limits(spec);
303        let engine = Engine::new(&create_secure_wasmtime_config(&limits))?;
304        Ok(Self { engine, limits })
305    }
306}
307
308pub fn generate_linker(
309    engine: &Engine,
310) -> Result<Linker<MemoryManager>, ContractError> {
311    let mut linker: Linker<MemoryManager> = Linker::new(engine);
312
313    // functions are created for webasembly modules, the logic of which is programmed in Rust
314    linker
315        .func_wrap(
316            "env",
317            "pointer_len",
318            |caller: Caller<'_, MemoryManager>, pointer: i32| {
319                caller.data().get_pointer_len(pointer as usize) as u32
320            },
321        )
322        .map_err(|e| ContractError::LinkerError {
323            function: "pointer_len",
324            details: e.to_string(),
325        })?;
326
327    linker
328        .func_wrap(
329            "env",
330            "alloc",
331            |mut caller: Caller<'_, MemoryManager>,
332             len: u32|
333             -> Result<u32, WasmError> {
334                caller
335                    .data_mut()
336                    .alloc(len as usize)
337                    .map(|ptr| ptr as u32)
338                    .map_err(WasmError::from)
339            },
340        )
341        .map_err(|e| ContractError::LinkerError {
342            function: "alloc",
343            details: e.to_string(),
344        })?;
345
346    linker
347        .func_wrap(
348            "env",
349            "write_byte",
350            |mut caller: Caller<'_, MemoryManager>,
351             ptr: u32,
352             offset: u32,
353             data: u32|
354             -> Result<(), WasmError> {
355                caller
356                    .data_mut()
357                    .write_byte(ptr as usize, offset as usize, data as u8)
358                    .map_err(WasmError::from)
359            },
360        )
361        .map_err(|e| ContractError::LinkerError {
362            function: "write_byte",
363            details: e.to_string(),
364        })?;
365
366    linker
367        .func_wrap(
368            "env",
369            "read_byte",
370            |caller: Caller<'_, MemoryManager>,
371             index: i32|
372             -> Result<u32, WasmError> {
373                let ptr = usize::try_from(index).map_err(|_| {
374                    ContractError::InvalidPointer { pointer: 0 }
375                })?;
376                caller
377                    .data()
378                    .read_byte(ptr)
379                    .map(|b| b as u32)
380                    .map_err(WasmError::from)
381            },
382        )
383        .map_err(|e| ContractError::LinkerError {
384            function: "read_byte",
385            details: e.to_string(),
386        })?;
387
388    Ok(linker)
389}