Skip to main content

rns_hooks/
runtime.rs

1#[cfg(feature = "wasm")]
2use crate::engine_access::EngineAccess;
3#[cfg(feature = "wasm")]
4use crate::wire::ActionWire;
5
6#[derive(Debug, Clone)]
7pub struct RawProviderEvent {
8    pub payload_type: String,
9    pub payload: Vec<u8>,
10}
11
12/// Default fuel budget per WASM invocation.
13pub const DEFAULT_FUEL: u64 = 10_000_000;
14/// Default maximum memory for WASM modules (16 MB).
15pub const DEFAULT_MAX_MEMORY: usize = 16 * 1024 * 1024;
16/// Maximum number of tables a hook instance may define.
17pub const MAX_TABLES: usize = 4;
18/// Maximum number of elements across a hook instance's tables.
19pub const MAX_TABLE_ELEMENTS: usize = 65_536;
20/// Maximum number of log messages forwarded per invocation; extras are counted and dropped.
21pub const MAX_LOG_MESSAGES_PER_CALL: usize = 64;
22/// Maximum length of a single forwarded log message; longer messages are truncated.
23pub const MAX_LOG_MESSAGE_BYTES: usize = 4 * 1024;
24/// Maximum number of actions a hook may inject per invocation.
25pub const MAX_INJECTED_ACTIONS_PER_CALL: usize = 64;
26/// Maximum total payload bytes of actions injected per invocation.
27pub const MAX_INJECTED_ACTION_BYTES_PER_CALL: usize = 256 * 1024;
28/// Maximum number of provider events a hook may emit per invocation.
29pub const MAX_PROVIDER_EVENTS_PER_CALL: usize = 256;
30/// Maximum total bytes (type + payload) of provider events emitted per invocation.
31pub const MAX_PROVIDER_EVENT_BYTES_PER_CALL: usize = 256 * 1024;
32/// WebAssembly page size.
33#[cfg(feature = "wasm")]
34const WASM_PAGE_SIZE: u64 = 64 * 1024;
35
36/// Data stored in each wasmtime `Store`.
37///
38/// Uses a raw pointer for `EngineAccess` because `Linker<T>` requires `T`
39/// without lifetime parameters. The Store is cached across calls for instance
40/// persistence, but `engine_access` is refreshed each call via `reset_per_call`
41/// and must only be dereferenced during the active call.
42#[cfg(feature = "wasm")]
43pub struct StoreData {
44    pub engine_access: *const dyn EngineAccess,
45    pub now: f64,
46    pub injected_actions: Vec<ActionWire>,
47    /// Total payload bytes of `injected_actions` in the current call.
48    pub injected_action_bytes: usize,
49    /// Name of the program owning this store, used to tag forwarded log lines.
50    pub program_name: String,
51    /// Log messages forwarded to the host logger in the current call.
52    pub log_messages_forwarded: usize,
53    /// Log messages dropped in the current call because the per-call cap was reached.
54    pub log_messages_dropped: usize,
55    pub provider_events: Vec<RawProviderEvent>,
56    /// Total bytes (type + payload) of `provider_events` in the current call.
57    pub provider_event_bytes: usize,
58    pub provider_events_enabled: bool,
59    /// Resource limits (memory size, instance/table/memory counts) for the store.
60    pub limits: wasmtime::StoreLimits,
61}
62
63// Safety: StoreData is only used within a single-threaded driver loop.
64// The `engine_access` raw pointer is refreshed each call and only dereferenced
65// during that call while the borrow is live.
66#[cfg(feature = "wasm")]
67unsafe impl Send for StoreData {}
68#[cfg(feature = "wasm")]
69unsafe impl Sync for StoreData {}
70
71#[cfg(feature = "wasm")]
72impl StoreData {
73    /// Create store data for one program with resource limits capping linear
74    /// memory at `max_memory` bytes.
75    pub fn new(
76        program_name: String,
77        engine_access: *const dyn EngineAccess,
78        now: f64,
79        provider_events_enabled: bool,
80        max_memory: usize,
81    ) -> Self {
82        StoreData {
83            engine_access,
84            now,
85            injected_actions: Vec::new(),
86            injected_action_bytes: 0,
87            program_name,
88            log_messages_forwarded: 0,
89            log_messages_dropped: 0,
90            provider_events: Vec::new(),
91            provider_event_bytes: 0,
92            provider_events_enabled,
93            limits: wasmtime::StoreLimitsBuilder::new()
94                .memory_size(max_memory)
95                .instances(1)
96                .memories(1)
97                .tables(MAX_TABLES)
98                .table_elements(MAX_TABLE_ELEMENTS)
99                .trap_on_grow_failure(true)
100                .build(),
101        }
102    }
103
104    /// Access the engine through the raw pointer.
105    ///
106    /// # Safety
107    /// The caller must ensure the pointer is still valid.
108    pub unsafe fn engine(&self) -> &dyn EngineAccess {
109        &*self.engine_access
110    }
111
112    /// Reset per-call fields while preserving the store (and WASM linear memory).
113    pub fn reset_per_call(
114        &mut self,
115        engine_access: *const dyn EngineAccess,
116        now: f64,
117        provider_events_enabled: bool,
118    ) {
119        self.engine_access = engine_access;
120        self.now = now;
121        self.injected_actions.clear();
122        self.injected_action_bytes = 0;
123        self.log_messages_forwarded = 0;
124        self.log_messages_dropped = 0;
125        self.provider_events.clear();
126        self.provider_event_bytes = 0;
127        self.provider_events_enabled = provider_events_enabled;
128    }
129}
130
131/// Wrapper around `wasmtime::Engine` with fuel-metering enabled.
132#[cfg(feature = "wasm")]
133pub struct WasmRuntime {
134    engine: wasmtime::Engine,
135    fuel: u64,
136    max_memory: usize,
137}
138
139#[cfg(feature = "wasm")]
140impl WasmRuntime {
141    pub fn new() -> Result<Self, wasmtime::Error> {
142        let mut config = wasmtime::Config::new();
143        config.consume_fuel(true);
144        // Hooks use the stable core-module ABI only. Keep proposals which add
145        // reference types and exception control flow outside the sandbox even
146        // if a Wasmtime release enables them by default.
147        config.wasm_gc(false);
148        config.wasm_exceptions(false);
149        // Host functions treat guest pointers as 32-bit offsets.
150        config.wasm_memory64(false);
151        let engine = wasmtime::Engine::new(&config)?;
152        Ok(WasmRuntime {
153            engine,
154            fuel: DEFAULT_FUEL,
155            max_memory: DEFAULT_MAX_MEMORY,
156        })
157    }
158
159    /// Compile a module, rejecting any memory whose declared minimum size
160    /// exceeds the runtime's memory limit.
161    pub fn compile(&self, bytes: &[u8]) -> Result<wasmtime::Module, wasmtime::Error> {
162        let module = wasmtime::Module::new(&self.engine, bytes)?;
163        let memories = module
164            .exports()
165            .map(|e| e.ty())
166            .chain(module.imports().map(|i| i.ty()))
167            .filter_map(|ty| ty.memory().cloned());
168        for memory in memories {
169            let min_bytes = memory.minimum().saturating_mul(WASM_PAGE_SIZE);
170            if min_bytes > self.max_memory as u64 {
171                wasmtime::bail!(
172                    "module declares a minimum memory of {} bytes, above the limit of {} bytes",
173                    min_bytes,
174                    self.max_memory
175                );
176            }
177        }
178        Ok(module)
179    }
180
181    pub fn engine(&self) -> &wasmtime::Engine {
182        &self.engine
183    }
184
185    pub fn fuel(&self) -> u64 {
186        self.fuel
187    }
188
189    /// Maximum linear memory size, in bytes, for each hook instance.
190    pub fn max_memory(&self) -> usize {
191        self.max_memory
192    }
193
194    /// Create a store for one program with fuel and memory limits applied.
195    pub fn new_store(
196        &self,
197        data: StoreData,
198    ) -> Result<wasmtime::Store<StoreData>, wasmtime::Error> {
199        let mut store = wasmtime::Store::new(&self.engine, data);
200        store.limiter(|data| &mut data.limits);
201        store.set_fuel(self.fuel)?;
202        Ok(store)
203    }
204}
205
206#[cfg(all(test, feature = "wasm"))]
207mod tests {
208    use super::WasmRuntime;
209
210    #[test]
211    fn compiles_core_hook_instruction_set() {
212        let runtime = WasmRuntime::new().unwrap();
213        runtime
214            .compile(
215                br#"(module
216                    (memory 2)
217                    (func (export "bulk")
218                        i32.const 0
219                        i32.const 90
220                        i32.const 4096
221                        memory.fill
222                        i32.const 8192
223                        i32.const 0
224                        i32.const 4096
225                        memory.copy))"#,
226            )
227            .unwrap();
228    }
229
230    #[test]
231    fn rejects_webassembly_gc_modules() {
232        let runtime = WasmRuntime::new().unwrap();
233        let result = runtime.compile(
234            br#"(module
235                (type $record (struct (field i32)))
236                (func (export "make") (result (ref null $record))
237                    ref.null $record))"#,
238        );
239
240        assert!(result.is_err(), "the hook sandbox must not enable Wasm GC");
241    }
242
243    #[test]
244    fn rejects_webassembly_exception_modules() {
245        let runtime = WasmRuntime::new().unwrap();
246        let result = runtime.compile(
247            br#"(module
248                (tag $failure)
249                (func (export "raise")
250                    throw $failure))"#,
251        );
252
253        assert!(
254            result.is_err(),
255            "the hook sandbox must not enable Wasm exception handling"
256        );
257    }
258}