fluentbase_runtime/runtime/system_runtime.rs
1//! System runtime backed by Wasmtime.
2//!
3//! This module implements **system runtimes** (trusted, privileged rWasm programs) executed via
4//! Wasmtime. The key difference from `ContractRuntime` is that system runtimes:
5//! - may be reused across multiple calls (store/instance caching),
6//! - signal "soft exits" via *returned output* rather than trapping/unwinding.
7//!
8//! ## Fuel metering modes
9//!
10//! System runtimes support two fuel metering strategies:
11//!
12//! 1. **Self-metering** (`consume_fuel=false`): The contract manages fuel internally by calling
13//! the `_charge_fuel` syscall. Wasmtime fuel metering is disabled. This is used by runtimes
14//! like EVM_RUNTIME and SVM_RUNTIME that have their own gas accounting.
15//!
16//! 2. **Engine-metered** (`consume_fuel=true`): Wasmtime automatically meters fuel for both
17//! wasm instructions and builtin syscalls. This is used by precompiles that don't self-meter:
18//! NITRO_VERIFIER, OAUTH2_VERIFIER, WASM_RUNTIME, WEBAUTHN_VERIFIER.
19//!
20//! ## Why "return output" instead of trapping?
21//! Some system runtimes intentionally avoid `trap` / `halt` paths because they do not always unwind
22//! the stack in the desired way for this environment. Instead, they return an output buffer where
23//! the first 4 bytes are the little-endian `exit_code`, and the remaining bytes are the payload.
24//!
25//! ## Caching model
26//! - `COMPILED_MODULES` caches compiled `wasmtime::Module` by code hash globally.
27//! - `COMPILED_RUNTIMES` caches instantiated `CompiledRuntime` per thread (thread-local) by code hash.
28//!
29//! The store is reused, so the `RuntimeContext` is swapped in/out on every call.
30
31use crate::{syscall_handler::runtime_syscall_handler, RuntimeContext};
32use alloc::sync::Arc;
33use core::{cell::RefCell, mem::take};
34use fluentbase_types::{ExitCode, HashMap, SysFuncIdx, B256, STATE_DEPLOY, STATE_MAIN};
35use rwasm::{
36 CompilationConfig, ImportLinker, Opcode, RwasmModule, StateRouterConfig, StoreTr,
37 StrategyDefinition, StrategyExecutor, TrapCode, Value, N_MAX_ALLOWED_MEMORY_PAGES,
38};
39
40/// A system runtime instance.
41///
42/// System runtimes are **trusted** programs executed through Wasmtime and reused across calls.
43/// This wrapper owns:
44/// - a cached, compiled+instantiated Wasmtime runtime (`CompiledRuntime`),
45/// - the per-call `RuntimeContext` (`ctx`) which is swapped into the cached store on execution,
46/// - an optional resumable interruption state used when system runtimes request an interruption.
47/// - a flag indicating whether Wasmtime fuel metering is enabled (`consume_fuel`).
48///
49/// The runtime is keyed by `code_hash` so that we can cache compiled artifacts and instances.
50pub struct SystemRuntime {
51 /// Cached compiled runtime (Module + Store + Instance + Memory + entry functions).
52 ///
53 /// NOTE: This is currently not `no_std` friendly due to Wasmtime usage.
54 /// The intent is to replace/relax this once rWasm ships an optimized embedded backend.
55 compiled_runtime: Arc<RefCell<CompiledRuntime>>,
56
57 /// Per-call execution context.
58 ///
59 /// This context is swapped into the cached store before execution and swapped back after,
60 /// so that a single cached store/instance can serve multiple contract calls sequentially.
61 ctx: RuntimeContext,
62
63 /// Code hash of the system runtime program.
64 ///
65 /// Used as a cache key for both compiled modules and instantiated runtimes.
66 code_hash: B256,
67
68 /// Whether Wasmtime fuel metering is enabled for this runtime.
69 ///
70 /// When `true`, the engine automatically charges fuel for wasm instructions and syscalls.
71 /// When `false`, the contract is expected to self-meter via `_charge_fuel` syscall.
72 consume_fuel: bool,
73}
74
75/// Fully initialized compiled runtime artifacts.
76///
77/// This structure is cached and reused. It contains:
78/// - a compiled Wasmtime `Module`,
79/// - a `Store<RuntimeContext>` holding runtime state and a swap-in context,
80/// - an instantiated `Instance` and its exported memory,
81/// - cached exported entry functions.
82type CompiledRuntime = StrategyExecutor<RuntimeContext>;
83
84thread_local! {
85 /// Thread-local cache of fully instantiated runtimes keyed by code hash.
86 ///
87 /// We keep this thread-local because Wasmtime components are not generally inexpensive to share across
88 /// threads without careful synchronization, and because per-thread reuse is often enough.
89 pub static COMPILED_RUNTIMES: RefCell<HashMap<B256, Arc<RefCell<CompiledRuntime>>>> =
90 RefCell::new(HashMap::new());
91}
92
93impl SystemRuntime {
94 /// Clears the per-thread cache of instantiated runtimes.
95 ///
96 /// Useful in tests or when a process needs to drop cached instances (e.g. after an upgrade).
97 pub fn reset_cached_runtimes() {
98 COMPILED_RUNTIMES.with_borrow_mut(|compiled_runtimes| {
99 compiled_runtimes.clear();
100 });
101 }
102
103 /// Creates a new `SystemRuntime`.
104 ///
105 /// If a compiled runtime for `code_hash` is present in the thread-local cache, it will be reused.
106 /// Otherwise, this function compiles/loads the module and instantiates it with imports wired via
107 /// `import_linker`.
108 ///
109 /// ## Fuel metering
110 ///
111 /// The `consume_fuel` parameter determines whether Wasmtime fuel metering is enabled:
112 /// - `true`: Engine automatically meters fuel (for NITRO, OAUTH2, WASM_RUNTIME, WEBAUTHN)
113 /// - `false`: Contract self-meters via `_charge_fuel` syscall (for EVM_RUNTIME, etc.)
114 pub fn new(
115 rwasm_module: RwasmModule,
116 import_linker: Arc<ImportLinker>,
117 code_hash: B256,
118 ctx: RuntimeContext,
119 consume_fuel: bool,
120 ) -> Self {
121 let compiled_runtime = COMPILED_RUNTIMES.with_borrow_mut(|compiled_runtimes| {
122 if let Some(compiled_runtime) = compiled_runtimes.get(&code_hash).cloned() {
123 return compiled_runtime;
124 }
125
126 let config = CompilationConfig::default()
127 .with_state_router(StateRouterConfig {
128 states: Box::new([
129 ("deploy".into(), STATE_DEPLOY),
130 ("main".into(), STATE_MAIN),
131 ]),
132 opcode: Some(Opcode::Call(SysFuncIdx::STATE as u32)),
133 })
134 .with_import_linker(import_linker.clone())
135 .with_allow_malformed_entrypoint_func_type(true)
136 .with_consume_fuel(consume_fuel)
137 .with_consume_fuel_for_params_and_locals(false)
138 .with_builtins_consume_fuel(false)
139 .with_max_allowed_memory_pages(N_MAX_ALLOWED_MEMORY_PAGES);
140 // `hint_section` contains Wasmtime-compatible wasm bytes for the system runtime.
141 // Any compilation failure here is fatal: genesis/runtime packaging is inconsistent.
142 let typed_module =
143 StrategyDefinition::new(config, &rwasm_module.hint_section, Some(code_hash.0))
144 .expect("runtime: failed to compile system runtime module");
145 let Ok(executor) = typed_module.create_executor(
146 import_linker,
147 RuntimeContext::default(),
148 runtime_syscall_handler,
149 // We can't set a fuel limit here because it's not known until execution.
150 None,
151 Some(N_MAX_ALLOWED_MEMORY_PAGES),
152 ) else {
153 unreachable!("runtime: failed to create executor for system runtime module")
154 };
155
156 #[allow(clippy::arc_with_non_send_sync)]
157 let compiled_runtime = Arc::new(RefCell::new(executor));
158 compiled_runtimes.insert(code_hash, compiled_runtime.clone());
159 compiled_runtime
160 });
161
162 Self {
163 compiled_runtime,
164 ctx,
165 code_hash,
166 consume_fuel,
167 }
168 }
169
170 /// Executes the system runtime entrypoint and updates `self.ctx.execution_result`.
171 ///
172 /// Execution uses the cached store/instance. Before calling into Wasmtime, we swap
173 /// `self.ctx` into the store to ensure syscalls and state access refer to the correct context.
174 ///
175 /// ## Fuel metering
176 ///
177 /// If `consume_fuel=true`, the fuel limit is set in the store before execution. Wasmtime
178 /// will automatically decrement fuel as instructions execute.
179 ///
180 /// ## Error handling model
181 /// - If Wasmtime traps unexpectedly, we **do not propagate** the trap outward as fatal.
182 /// Instead, we mark `UnexpectedFatalExecutionFailure` in `execution_result` and return `Ok(())`
183 /// so the outer executor can treat it as a partially controlled failure.
184 /// - Normal completion is signaled by output where the first 4 bytes are LE `exit_code`.
185 /// - Interruption is requested by returning `ExitCode::InterruptionCalled` in that header.
186 pub fn execute(&mut self) -> Result<(), TrapCode> {
187 let mut compiled_runtime = self.compiled_runtime.borrow_mut();
188
189 // Rewrite runtime context before each call, since we reuse the same store/runtime instance
190 // across multiple calls. We must replace whatever context was left from the previous call.
191 //
192 // Safety: Calls into a cached runtime must be strictly sequential. No reentrancy or
193 // overlapping calls are allowed because we swap a single `RuntimeContext` in/out.
194 core::mem::swap(compiled_runtime.data_mut(), &mut self.ctx);
195
196 // If fuel metering is enabled, set the fuel limit before execution.
197 // The store is reused, so we must reset fuel for each new call.
198 let fuel_limit = compiled_runtime.data().fuel_limit;
199 compiled_runtime.reset_fuel(fuel_limit);
200
201 // Choose an entrypoint based on the current execution state.
202 let entrypoint = match compiled_runtime.data().state {
203 STATE_MAIN => "main",
204 STATE_DEPLOY => "deploy",
205 _ => unreachable!(),
206 };
207
208 let mut output = [Value::I32(0)];
209 // Rust generates a C-style `main(argc: i32, argv: i32) -> i32` signature for wasm targets when `main` returns `i32`.
210 // Even in `no_std`, the toolchain follows the traditional C/WASI ABI convention where `argc` and `argv` are passed
211 // as 32-bit values (wasm pointers).
212 //
213 // We do not use command-line arguments in this environment, so we pass `0, 0` as dummy values.
214 // The generated shim ignores them unless argument handling is explicitly implemented.
215 //
216 // https://reviews.llvm.org/D70700
217 let result = if entrypoint == "main" {
218 compiled_runtime.execute(entrypoint, &[Value::I32(0), Value::I32(0)], &mut output)
219 } else {
220 compiled_runtime.execute(entrypoint, &[], &mut output)
221 };
222 let exit_code = output[0].i32().unwrap();
223
224 // Always swap back immediately after the call, so we keep `self.ctx` authoritative.
225 core::mem::swap(compiled_runtime.data_mut(), &mut self.ctx);
226
227 // The application can return trap code though exit code, we should handle such cases as well
228 if self.ctx.execution_result.exit_code != ExitCode::Ok.into_i32() {
229 // If panic happens, then we can only forward into output
230 if self.ctx.execution_result.exit_code == ExitCode::Panic.into_i32() {
231 eprintln!(
232 "WARN: system execution panicked: {} (investigate)",
233 core::str::from_utf8(&self.ctx.execution_result.output)
234 .unwrap_or("unable to decode UTF-8 panic message")
235 );
236 }
237 // We assume any not `Ok` error can happen, for example, due to OOM (because our EVM runtime is limited with 64mB only),
238 // but we should handle it gracefully if it happens. When it happens, we have a corrupted state: stack and memory.
239 // Ideally, we should terminate the latest frame and expect the caller to continue its execution because we can free
240 // resources we consumed. But here we just terminate the runtime entirely. It means that all previous calls
241 // will fail because they lose their state. It's the best here because we automatically terminate all nested
242 // execution to avoid potential memory or stack access violations.
243 COMPILED_RUNTIMES.with_borrow_mut(|compiled_runtimes| {
244 compiled_runtimes.remove(&self.code_hash);
245 });
246 // We return `Ok` here because the exit code is already set
247 return Ok(());
248 } else {
249 self.ctx.execution_result.exit_code = exit_code;
250 }
251
252 // If wasmtime trapped, treat it as an unexpected fatal failure and degrade into a safe
253 // error code. This avoids propagating a raw trap across the execution boundary.
254 if let Err(trap_code) = result.as_ref() {
255 // The trap `OutOfFuel` is expected for engine-metered precompiles when fuel is exhausted.
256 if *trap_code == TrapCode::OutOfFuel {
257 // This case is tricky, because if it happens, then we might have corrupted stack and
258 // uncleaned memory. Since we can't handle it gracefully, then we can only reset the existing
259 // runtime to make sure memory and stack are clean. There is no ddos attack here because,
260 // to achieve this, the user must pay a penalty.
261 COMPILED_RUNTIMES.with_borrow_mut(|compiled_runtimes| {
262 compiled_runtimes.remove(&self.code_hash);
263 });
264 // Forward the `OutOfFuel` trap to the outer executor, so it can handle it gracefully.
265 return Err(*trap_code);
266 }
267 eprintln!(
268 "runtime: unexpected trap inside system runtime: {:?} ({}) (investigate)",
269 trap_code, trap_code,
270 );
271 self.ctx.execution_result.exit_code =
272 ExitCode::UnexpectedFatalExecutionFailure.into_i32();
273 return Ok(());
274 }
275
276 // If exit code indicates an interruption, convert it into a trap that the outer executor
277 // understands (`TrapCode::InterruptionCalled`).
278 //
279 // Safety: `InterruptionCalled` is expected only from trusted system runtimes.
280 // Untrusted contracts might use the same numeric code but will not be executed here.
281 if exit_code == ExitCode::InterruptionCalled.into_i32() {
282 // Move output into return_data. For system runtimes we don't expose a dedicated
283 // "interrupt params" ABI, so we treat the returned output payload as the interruption
284 // parameters.
285 self.ctx.execution_result.return_data = take(&mut self.ctx.execution_result.output);
286 assert!(
287 !self.ctx.execution_result.return_data.is_empty(),
288 "runtime: interruption payload must not be empty"
289 );
290 return Err(TrapCode::InterruptionCalled);
291 }
292
293 result
294 }
295
296 /// Resumes execution after an interruption.
297 ///
298 /// System runtimes do not support "native" resumable interruptions internally in the same way
299 /// as contract runtimes. Therefore, resume currently re-enters `execute()` after clearing any
300 /// stale output.
301 ///
302 /// Note: `exit_code` and `fuel_consumed` are intentionally ignored here because fuel metering
303 /// is handled by `RuntimeContext`, and exit codes are encoded in returned output.
304 pub fn resume(&mut self, _exit_code: i32, _fuel_consumed: u64) -> Result<(), TrapCode> {
305 // Ensure the output is clear before resuming; output is used to carry interruption params.
306 self.ctx.clear_output();
307
308 // Re-enter execution. Possible scenarios:
309 // 1) With return_data: current frame interruption outcome.
310 // 2) Without return_data: new frame call.
311 self.execute()
312 }
313
314 /// Writes bytes into the system runtime's linear memory.
315 ///
316 /// Bounds violations are mapped into `TrapCode::MemoryOutOfBounds`.
317 pub fn memory_write(&mut self, offset: usize, data: &[u8]) -> Result<(), TrapCode> {
318 let mut compiled_runtime = self.compiled_runtime.borrow_mut();
319 compiled_runtime.memory_write(offset, data)
320 }
321
322 /// Reads bytes from the system runtime's linear memory.
323 ///
324 /// Bounds violations are mapped into `TrapCode::MemoryOutOfBounds`.
325 pub fn memory_read(&mut self, offset: usize, buffer: &mut [u8]) -> Result<(), TrapCode> {
326 let mut compiled_runtime = self.compiled_runtime.borrow_mut();
327 compiled_runtime.memory_read(offset, buffer)
328 }
329
330 /// Returns remaining fuel if fuel metering is enabled.
331 ///
332 /// For engine-metered precompiles (`consume_fuel=true`), returns the actual remaining fuel
333 /// from the Wasmtime store.
334 ///
335 /// For self-metering runtimes (`consume_fuel=false`), returns `None` because fuel is
336 /// tracked in `RuntimeContext` via `_charge_fuel` syscall, not in the Wasmtime store.
337 pub fn remaining_fuel(&self) -> Option<u64> {
338 let compiled_runtime = self.compiled_runtime.borrow();
339 if self.consume_fuel {
340 compiled_runtime.remaining_fuel()
341 } else {
342 None
343 }
344 }
345
346 /// Provides mutable access to the per-call runtime context.
347 pub fn context_mut(&mut self) -> &mut RuntimeContext {
348 &mut self.ctx
349 }
350
351 /// Provides immutable access to the per-call runtime context.
352 pub fn context(&self) -> &RuntimeContext {
353 &self.ctx
354 }
355}